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.