Scala/FP Idiom: Update as You Copy, Don’t Mutate
In functional programming you don’t modify (mutate) existing objects, you create new objects with updated fields based on existing objects. For instance, last year my niece’s name was “Emily Means,” so I could have created a Person
instance to represent her, like this:
val emily = Person("Emily", "Means")
Then she got married, and her last name became “Walls.” In an imperative programming language you would just change her last name, like this:
emily.setLastName("Walls")
But in FP you don’t do this, you don’t mutate existing objects. Instead, what you do is (a) you copy the existing object to a new object, and (b) during the copy process you update any fields you want to change by supplying their new values.
The way you do this in Scala/FP is with the copy
method that comes with the Scala case class. This lesson shows a few examples of how to use copy
, including how to use it with nested objects.
So you can follow along, the source code for this lesson is available at github.com/alvinj/FpUpdateAsYouCopy
When you’re working with a simple object it’s easy to use copy
. Given a case
class like this:
case class Person (firstName: String, lastName: String)
if you want to update a person’s last name, you just “update as you copy,” like this:
val emily1 = Person("Emily", "Means")
val emily2 = emily1.copy(lastName = "Walls")
As shown, in simple situations like this all you have to do to use copy
is:
Make sure your class is a
case
class.Create an initial object (
emily1
), as usual.When a field in that object needs to be updated, use
copy
to create a new object (emily2
) from the original object, and specify the name of the field to be changed, along with its new value.
When you’re updating one field, that’s all you have to do.
That’s also all you have to do to update multiple fields, as I’ll show shortly.
An important point to note about this is that the first instance remains unchanged. You can verify that by running a little App
like this:
object CopyTest1 extends App {
println("--- Before Copy ---")
val emily1 = Person("Emily", "Means")
println(s"emily1 = $emily1")
// emily got married
println("\n--- After Copy ---")
val emily2 = emily1.copy(lastName = "Walls")
println(s"emily1 = $emily1")
println(s"emily2 = $emily2")
}
The output of CopyTest1
looks as follows, showing that the original emily1
instance is unchanged after the copy
:
--- Before Copy ---
emily1 = Person(Emily,Means)
--- After Copy ---
emily1 = Person(Emily,Means)
emily2 = Person(Emily,Walls)
What happens in practice is that you discard the original object, so thinking about the old instance isn’t typically an issue; I just want to mention it. (You’ll see more examples of how this works as we go along.)
In practice you also won’t use intermediate variables with names like
emily1
,emily2
, etc. We just need to do that now, until we learn a few more things.
It’s also easy to update multiple fields at one time using copy
. For instance, had Person
been defined like this:
case class Person (
firstName: String,
lastName: String,
age: Int
)
you could create an instance like this:
val emily1 = Person("Emily", "Means", 25)
and then create a new instance by updating several parameters at once, like this:
// emily is married, and a year older
val emily2 = emily1.copy(lastName = "Walls", age = 26)
That’s all you have to do to update two or more fields in a simple case
class.
As shown, using copy
with simple case
classes is straightforward. But when a case
class contains other case
classes, and those contain more case
classes, things get more complicated and the required code gets more verbose.
For instance, let’s say that you have a case
class hierarchy like this:
case class BillingInfo(
creditCards: Seq[CreditCard]
)
case class Name(
firstName: String,
mi: String,
lastName: String
)
case class User(
id: Int,
name: Name,
billingInfo: BillingInfo,
phone: String,
email: String
)
case class CreditCard(
name: Name,
number: String,
month: Int,
year: Int,
cvv: String
)
Visually the relationship between these classes looks like Figure [fig:umlRelationshipBetweenClasses].
Notice a few things about this code:
User
has fields of typeName
andBillingInfo
CreditCard
also has a field of theName
type
Despite a little complexity, creating an initial instance of User
with this hierarchy is straightforward:
object NestedCopy1 extends App {
val hannahsName = Name(
firstName = "Hannah",
mi = "C",
lastName = "Jones"
)
// create a user
val hannah1 = User(
id = 1,
name = hannahsName,
phone = "907-555-1212",
email = "hannah@hannahjones.com",
billingInfo = BillingInfo(
creditCards = Seq(
CreditCard(
name = hannahsName,
number = "1111111111111111",
month = 3,
year = 2020,
cvv = "123"
)
)
)
)
}
So far, so good. Now let’s take a look at what you have to do when a few of the fields need to be updated.
First, let’s suppose that Hannah moves. I kept the address out of the model to keep things relatively simple, but let’s suppose that her phone number needs to be updated. Because the phone number is stored as a top-level field in User
, this is a simple copy
operation:
// hannah moved, update the phone number
val hannah2 = hannah1.copy(phone = "720-555-1212")
Next, suppose that a little while later Hannah gets married and we need to update her last name. In this case you need to reach down into the Name
instance of the User
object and update the lastName
field. I’ll do this in a two-step process to keep it clear.
First, create a copy of the name
field, changing lastName
during the copy
process:
// hannah got married, update her last name
val newName = hannah2.name.copy(lastName = "Smith")
If you print newName
at this point, you’ll see that it is “Hannah C Smith.”
Now that you have this newName
instance, the second step is to create a new “Hannah” instance with this new Name
. You do that by (a) calling copy
on the hannah2
instance to make a new hannah3
instance, and (b) within copy
you bind the name
field to newName
:
val hannah3 = hannah2.copy(name = newName)
Suppose you also need to update the “Hannah” instance with new credit card information. To do this you follow the same pattern as before. First, you create a new CreditCard
instance from the existing instance. Because the creditCards
field inside the billingInfo
instance is a Seq
, you need to reference the first credit card instance while making the copy. That is, you reference creditCards(0)
:
val oldCC = hannah3.billingInfo.creditCards(0)
val newCC = oldCC.copy(name = newName)
Because (a) BillingInfo
takes a Seq[CreditCard]
, and (b) there’s only one credit card, I make a new Seq[CreditCard]
like this:
val newCCs = Seq(newCC)
With this new Seq[CreditCard]
I create a new “Hannah” instance by copying hannah3
to hannah4
, updating the BillingInfo
during the copy
process:
val hannah4 = hannah3.copy(billingInfo = BillingInfo(newCCs))
Put together, those lines of code look like this:
val oldCC = hannah3.billingInfo.creditCards(0)
val newCC = oldCC.copy(name = newName)
val newCCs = Seq(newCC)
val hannah4 = hannah3.copy(billingInfo = BillingInfo(newCCs))
You can shorten that code if you want, but I show the individual steps so it’s easier to read.
These examples show how the “update as you copy” process works with nested objects in Scala/FP. (More on this after the attribution.)
The examples I just showed are a simplification of the code and description found at these URLs:
As you saw, the “update as you copy” technique gets more complicated when you deal with real-world, nested objects, and the deeper the nesting gets, the more complicated the problem becomes. But fear not: there are Scala/FP libraries that make this easier. The general idea of these libraries is known as a “lens” (or “lenses”), and they make copying nested objects much simpler. I cover lenses in a lesson later in this book.
Here’s a summary of what I just covered:
Because functional programmers don’t mutate objects, when an object needs to be updated it’s necessary to follow a pattern which I describe as “update as you copy”.
The way you do this in Scala is with the
copy
method, which comes with Scalacase
classes.As you can imagine, from here on out you’re going to be using
case
classes more than you’ll use the default Scala class. Thecopy
method is just one reason for this, but it’s a good reason. (You’ll see even more reasons to usecase
classes as you go along.)
As mentioned, I write about lenses later in the book, when we get to a point where we have to “update as you copy” complicated objects.
But for now the next thing we need to dig into is for
comprehensions. Once I cover those, you’ll be close to being able to write small, simple, functional applications with everything I’ve covered so far.
The source code for this lesson is available at at this Github repository
Alessandro Lacava has some notes about case classes, including a little about
copy
, currying, and arity