Say I have got following two case class
es:
case class Address(street: String, city: String, state: String, zipCode: Int)
case class Person(firstName: String, lastName: String, address: Address)
and the following instance of Person
class:
val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg",
"Mumbai",
"Maharashtra",
411342))
Now if I want to update zipCode
of raj
then I will have to do:
val updatedRaj = raj.copy(address = raj.address.copy(zipCode = raj.address.zipCode + 1))
With more levels of nesting this gets even more uglier. Is there a cleaner way (something like Clojure's update-in
) to update such nested structures?
Funny that no one added lenses, since they were MADE for this kind of stuff. So, here is a CS background paper on it, here is a blog which touch briefly on lenses use in Scala, here is a lenses implementation for Scalaz and here is some code using it, which looks surprisingly like your question. And, to cut down on boiler plate, here's a plugin that generate Scalaz lenses for case classes.
For bonus points, here's another S.O. question which touches on lenses, and a paper by Tony Morris.
The big deal about lenses is that they are composable. So they are a bit cumbersome at first, but they keep gaining ground the more you use them. Also, they are great for testability, since you only need to test individual lenses, and can take for granted their composition.
So, based on an implementation provided at the end of this answer, here's how you'd do it with lenses. First, declare lenses to change a zip code in an address, and an address in a person:
val addressZipCodeLens = Lens(
get = (_: Address).zipCode,
set = (addr: Address, zipCode: Int) => addr.copy(zipCode = zipCode))
val personAddressLens = Lens(
get = (_: Person).address,
set = (p: Person, addr: Address) => p.copy(address = addr))
Now, compose them to get a lens that changes zipcode in a person:
val personZipCodeLens = personAddressLens andThen addressZipCodeLens
Finally, use that lens to change raj:
val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)
Or, using some syntactic sugar:
val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens(raj) + 1)
Or even:
val updatedRaj = personZipCodeLens.mod(raj, zip => zip + 1)
Here's the simple implementation, taken from Scalaz, used for this example:
case class Lens[A,B](get: A => B, set: (A,B) => A) extends Function1[A,B] with Immutable {
def apply(whole: A): B = get(whole)
def updated(whole: A, part: B): A = set(whole, part) // like on immutable maps
def mod(a: A, f: B => B) = set(a, f(this(a)))
def compose[C](that: Lens[C,A]) = Lens[C,B](
c => this(that(c)),
(c, b) => that.mod(c, set(_, b))
)
def andThen[C](that: Lens[B,C]) = that compose this
}