Cleaner way to update nested structures
Useful tools to use Lenses:
Just want to add that the Macrocosm and Rillit projects, based on Scala 2.10 macros, provides Dynamic Lens Creation.
Using Rillit:
case class Email(user: String, domain: String)
case class Contact(email: Email, web: String)
case class Person(name: String, contact: Contact)
val person = Person(
name = "Aki Saarinen",
contact = Contact(
email = Email("aki", "akisaarinen.fi"),
web = "http://akisaarinen.fi"
)
)
scala> Lenser[Person].contact.email.user.set(person, "john")
res1: Person = Person(Aki Saarinen,Contact(Email(john,akisaarinen.fi),http://akisaarinen.fi))
Using Macrocosm:
This even works for case classes defined in the current compile run.
case class Person(name: String, age: Int)
val p = Person("brett", 21)
scala> lens[Person].name._1(p)
res1: String = brett
scala> lens[Person].name._2(p, "bill")
res2: Person = Person(bill,21)
scala> lens[Person].namexx(()) // Compilation error
I've been looking around for what Scala library that has the nicest syntax and the best functionality and one library not mentioned here is monocle which for me has been really good. An example follows:
import monocle.Macro._
import monocle.syntax._
case class A(s: String)
case class B(a: A)
val aLens = mkLens[B, A]("a")
val sLens = aLens |-> mkLens[A, String]("s")
//Usage
val b = B(A("hi"))
val newB = b |-> sLens set("goodbye") // gives B(A("goodbye"))
These are very nice and there are many ways to combine the lenses. Scalaz for example demands a lot of boilerplate and this compiles quick and runs great.
To use them in your project just add this to your dependencies:
resolvers ++= Seq(
"Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/",
"Sonatype OSS Snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/"
)
val scalaVersion = "2.11.0" // or "2.10.4"
val libraryVersion = "0.4.0" // or "0.5-SNAPSHOT"
libraryDependencies ++= Seq(
"com.github.julien-truffaut" %% "monocle-core" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-generic" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-macro" % libraryVersion, // since 0.4.0
"com.github.julien-truffaut" %% "monocle-law" % libraryVersion % test // since 0.4.0
)
Zippers
Huet's Zipper provides convenient traversal and 'mutation' of an immutable data structure. Scalaz provides Zippers for Stream
(scalaz.Zipper), and Tree
(scalaz.TreeLoc). It turns out that the structure of the zipper is automatically derivable from the original data structure, in a manner that resembles symbolic differentiation of an algebraic expression.
But how does this help you with your Scala case classes? Well, Lukas Rytz recently prototyped an extension to scalac that would automatically create zippers for annotated case classes. I'll reproduce his example here:
scala> @zip case class Pacman(lives: Int = 3, superMode: Boolean = false)
scala> @zip case class Game(state: String = "pause", pacman: Pacman = Pacman())
scala> val g = Game()
g: Game = Game("pause",Pacman(3,false))
// Changing the game state to "run" is simple using the copy method:
scala> val g1 = g.copy(state = "run")
g1: Game = Game("run",Pacman(3,false))
// However, changing pacman's super mode is much more cumbersome (and it gets worse for deeper structures):
scala> val g2 = g1.copy(pacman = g1.pacman.copy(superMode = true))
g2: Game = Game("run",Pacman(3,true))
// Using the compiler-generated location classes this gets much easier:
scala> val g3 = g1.loc.pacman.superMode set true
g3: Game = Game("run",Pacman(3,true)
So the community needs to persuade the Scala team that this effort should be continued and integrated into the compiler.
Incidentally, Lukas recently published a version of Pacman, user programmable through a DSL. Doesn't look like he used the modified compiler, though, as I can't see any @zip
annotations.
Tree Rewriting
In other circumstances, you might like to apply some transformation across the entire data structure, according to some strategy (top-down, bottom-up), and based on rules that match against the value at some point in the structure. The classical example is transforming an AST for a language, perhaps to evaluate, simplify, or collect information. Kiama supports Rewriting, see the examples in RewriterTests, and watch this video. Here's a snippet to whet your appetite:
// Test expression
val e = Mul (Num (1), Add (Sub (Var ("hello"), Num (2)), Var ("harold")))
// Increment every double
val incint = everywheretd (rule { case d : Double => d + 1 })
val r1 = Mul (Num (2), Add (Sub (Var ("hello"), Num (3)), Var ("harold")))
expect (r1) (rewrite (incint) (e))
Note that Kiama steps outside the type system to achieve this.
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
}