更清晰的方式来更新嵌套结构

假设我有以下两个case class

 case class Address(street: String, city: String, state: String, zipCode: Int) case class Person(firstName: String, lastName: String, address: Address) 

Person类的以下实例:

 val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", "Mumbai", "Maharashtra", 411342)) 

现在,如果我想更新raj zipCode ,那么我将不得不这样做:

 val updatedRaj = raj.copy(address = raj.address.copy(zipCode = raj.address.zipCode + 1)) 

随着更多的嵌套层次,这变得更加丑陋。 有没有一个更清洁的方式(像Clojure的update-in )来更新这样的嵌套结构?

拉链

Huet的Zipper提供了不变数据结构的便捷遍历和“变异”。 斯卡拉斯提供Stream拉链( scalaz.Zipper )和Tree ( scalaz.TreeLoc )。 事实certificate,拉链的结构可以从原始的数据结构自动导出,其方式类似于代数expression式的符号区分。

但是,这对Scala case类有什么帮助呢? 那么,Lukas Rytz最近为scalac创build了一个扩展,可以自动为注释的case类创build拉链。 我将在这里重现他的例子:

 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) 

所以社区需要说服Scala团队,这个努力应该继续下去,并且整合到编译器中。

顺便说一下,Lukas最近发布了一个Pacman版本,用户可以通过DSL进行编程。 看起来他不像使用修改过的编译器,因为我看不到任何@zip注释。

树重写

在其他情况下,您可能希望根据某种策略(自上而下,自下而上)在整个数据结构上应用某种转换,并根据与结构中某个点的值匹配的规则。 经典的例子是将语言转换为AST,也许是为了评估,简化或收集信息。 Kiama支持重写 ,请参阅RewriterTests中的示例,并观看此video 。 这里有一个片段来激发你的胃口:

 // 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)) 

请注意,凯马在types系统之外的步骤来实现这一点。

有趣的是,没有人增加镜头,因为他们是这样的东西的MADE。 所以,下面是一个CS背景文件,下面是一个在Scala中使用镜头简单接触的博客, 这是一个用于Scalaz的镜头实现,下面是一些使用它的代码,看起来很像你的问题。 而且,为了减less锅炉板, 这里有一个插件,可以生成案例类的斯卡拉兹镜头。

对于奖励点, 这是另一个涉及镜头的SO问题,以及Tony Morris撰写的一篇论文 。

关于镜头的一大问题是它们是可组合的。 所以起初他们有点累赘,但是他们用的越多,他们也越来越多。 而且,它们非常适合testing,因为您只需要testing单个镜头,并且可以将其构图视为理所当然。

所以,根据这个答案的结尾处提供的一个实现,以下是你如何使用镜头来做到这一点。 首先,申报镜头更改地址中的邮政编码,并在一个人的地址:

 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)) 

现在,组成他们得到一个镜头,改变一个人的邮政编码:

 val personZipCodeLens = personAddressLens andThen addressZipCodeLens 

最后,用这个镜头来改变raj:

 val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1) 

或者,使用一些语法糖:

 val updatedRaj = personZipCodeLens.set(raj, personZipCodeLens(raj) + 1) 

甚至:

 val updatedRaj = personZipCodeLens.mod(raj, zip => zip + 1) 

下面是从Scalaz中取得的简单实现,用于这个例子:

 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 } 

有用的工具来使用镜头:

只需要补充一下,基于Scala 2.10macros的Macrocosm和Rillit项目提供了Dynamic Lens Creation。


使用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)) 

使用macros观:

这甚至适用于当前编译运行中定义的case类。

 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 

无形的技巧:

 "com.chuusai" % "shapeless_2.11" % "2.0.0" 

有:

 case class Address(street: String, city: String, state: String, zipCode: Int) case class Person(firstName: String, lastName: String, address: Address) object LensSpec { import shapeless._ val zipLens = lens[Person] >> 'address >> 'zipCode val surnameLens = lens[Person] >> 'firstName val surnameZipLens = surnameLens ~ zipLens } class LensSpec extends WordSpecLike with Matchers { import LensSpec._ "Shapless Lens" should { "do the trick" in { // given some values to recreate val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", "Mumbai", "Maharashtra", 411342)) val updatedRaj = raj.copy(address = raj.address.copy(zipCode = raj.address.zipCode + 1)) // when we use a lens val lensUpdatedRaj = zipLens.set(raj)(raj.address.zipCode + 1) // then it matches the explicit copy assert(lensUpdatedRaj == updatedRaj) } "better yet chain them together as a template of values to set" in { // given some values to recreate val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", "Mumbai", "Maharashtra", 411342)) val updatedRaj = raj.copy(firstName="Rajendra", address = raj.address.copy(zipCode = raj.address.zipCode + 1)) // when we use a compound lens val lensUpdatedRaj = surnameZipLens.set(raj)("Rajendra", raj.address.zipCode+1) // then it matches the explicit copy assert(lensUpdatedRaj == updatedRaj) } } } 

请注意,虽然这里有一些其他的答案让你组成镜头深入到一个给定的结构这些无耻的镜头(和其他图书馆/macros),让你结合两个无关的镜头,这样你可以使镜头,设置任意数量的参数到任意位置在你的结构中。 对于复杂的数据结构,额外的组合是非常有用的。

我一直在寻找具有最好的语法和最好的function的斯卡拉图书馆,以及这里没有提到的一个图书馆是单片 ,这对我来说是非常好的。 一个例子如下:

 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")) 

这些都非常好,有很多方法来结合镜头。 例如,斯卡拉斯需要大量的样板,这个编译速度快,运行速度很快。

要在你的项目中使用它们,只需将它添加到你的依赖项中:

 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 ) 

由于其可组合的性质,镜头为嵌套严重的结构提供了一个非常好的解决scheme。 但是,如果嵌套层次较低,我有时会觉得镜头有点太多,如果只有less数嵌套更新的地方,我不想引入整个镜头的方法。 为了完整起见,这里是一个非常简单/实用的解决scheme:

我所做的只是在顶层结构中编写一些modify...辅助函数,它处理丑陋的嵌套副本。 例如:

 case class Person(firstName: String, lastName: String, address: Address) { def modifyZipCode(modifier: Int => Int) = this.copy(address = address.copy(zipCode = modifier(address.zipCode))) } 

我的主要目标(简化客户端的更新)已经实现:

 val updatedRaj = raj.modifyZipCode(_ => 41).modifyZipCode(_ + 1) 

创build完整的修改助手显然是烦人的。 但是对于内部的东西,在第一次尝试修改某个嵌套字段时创build它们通常是可以的。

也许QuickLens更好地匹配你的问题。 QuickLens使用macros来将IDE友好的expression式转换成与原始复制语句接近的东西。

给定两个例子的例子:

 case class Address(street: String, city: String, state: String, zipCode: Int) case class Person(firstName: String, lastName: String, address: Address) 

和Person类的实例:

 val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg", "Mumbai", "Maharashtra", 411342)) 

你可以更新raj的zipCode:

 import com.softwaremill.quicklens._ val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)