scala 更新嵌套结构的更简洁方法
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3900307/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Cleaner way to update nested structures
提问by missingfaktor
Say I have got following two case classes:
假设我有以下两个case classes:
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 Personclass:
以及以下Person类的实例:
val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg",
"Mumbai",
"Maharashtra",
411342))
Now if I want to update zipCodeof rajthen I will have to do:
现在,如果我想更新zipCode的raj话,我将不得不这样做:
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?
随着嵌套级别的增加,这变得更加丑陋。是否有更简洁的方法(类似于 Clojure 的update-in)来更新此类嵌套结构?
采纳答案by retronym
Zippers
拉链
Huet's Zipperprovides 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.
Huet 的 Zipper为不可变数据结构提供了方便的遍历和“变异”。Scalaz 为Stream( scalaz.Zipper) 和Tree( scalaz.TreeLoc)提供了拉链。事实证明,拉链的结构可以自动从原始数据结构推导出来,其方式类似于代数表达式的符号微分。
But how does this help you with your Scala case classes? Well, Lukas Rytz recently prototypedan extension to scalac that would automatically create zippers for annotated case classes. I'll reproduce his example here:
但这对您的 Scala 案例类有何帮助?好吧,Lukas Rytz 最近为 scalac设计了一个扩展原型,它会自动为带注释的案例类创建拉链。我将在这里重现他的例子:
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.
所以社区需要说服 Scala 团队,这项工作应该继续并集成到编译器中。
Incidentally, Lukas recently publisheda version of Pacman, user programmable through a DSL. Doesn't look like he used the modified compiler, though, as I can't see any @zipannotations.
顺便提一下,Lukas 最近发布了一个 Pacman 版本,用户可以通过 DSL 进行编程。不过,他看起来不像使用修改后的编译器,因为我看不到任何@zip注释。
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. Kiamasupports Rewriting, see the examples in RewriterTests, and watch this video. Here's a snippet to whet your appetite:
在其他情况下,您可能希望根据某种策略(自上而下、自下而上)并基于与结构中某个点的值匹配的规则,对整个数据结构应用一些转换。经典示例是将 AST 转换为一种语言,可能是为了评估、简化或收集信息。Kiama支持重写,请参阅RewriterTests 中的示例,并观看此视频。这是一个可以激发您食欲的片段:
// 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 outsidethe type system to achieve this.
请注意,Kiama跳出类型系统来实现这一点。
回答by Daniel C. Sobral
Funny that no one added lenses, since they were MADE for this kind of stuff. So, hereis a CS background paper on it, hereis a blog which touch briefly on lenses use in Scala, hereis a lenses implementation for Scalaz and hereis some code using it, which looks surprisingly like your question. And, to cut down on boiler plate, here'sa plugin that generate Scalaz lenses for case classes.
有趣的是没有人添加镜头,因为它们是为这种东西制造的。所以,这是一篇关于它的 CS 背景论文,这是一篇简要介绍 Scala 镜头使用的博客,这是 Scalaz 的镜头实现,这是一些使用它的代码,这看起来很像你的问题。而且,为了减少样板,这里有一个插件可以为案例类生成 Scalaz 镜头。
For bonus points, here'sanother S.O. question which touches on lenses, and a paperby Tony Morris.
对于加分,这是另一个涉及镜头的 SO 问题,以及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:
最后,使用那个镜头来改变 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:
这是从 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
}
回答by Sebastien Lorber
Useful tools to use Lenses:
使用镜头的有用工具:
Just want to add that the Macrocosmand Rillitprojects, based on Scala 2.10 macros, provides Dynamic Lens Creation.
只想补充一点,基于 Scala 2.10 宏的Macrocosm和Rillit项目提供了动态镜头创建。
Using Rillit:
使用 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
回答by Johan S
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 monoclewhich for me has been really good. An example follows:
我一直在寻找哪个 Scala 库具有最好的语法和最好的功能,而这里没有提到的一个库是monocle,它对我来说非常好。一个例子如下:
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.
这些非常好,有很多方法可以组合镜头。例如,Scalaz 需要大量样板文件,并且编译速度快且运行良好。
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
)
回答by bluenote10
Due to their composable nature, lenses provide a very nice solution to the problem of heavily nested structures. However with a low level of nesting, I sometimes feel lenses are a bit too much, and I don't want to introduce the whole lenses approach if there is only few places with nested updates. For sake of completeness, here is a very simple/pragmatic solution for this case:
由于它们的可组合性,镜头为大量嵌套结构的问题提供了一个很好的解决方案。但是嵌套层次低,有时候会觉得镜头有点多,如果嵌套更新的地方很少,我不想介绍整个镜头的方法。为了完整起见,对于这种情况,这是一个非常简单/实用的解决方案:
What I do is to simply write a few modify...helper functions in the top level structure, which deal with the ugly nested copy. For instance:
我所做的只是modify...在顶层结构中简单地编写一些辅助函数,用于处理丑陋的嵌套副本。例如:
case class Person(firstName: String, lastName: String, address: Address) {
def modifyZipCode(modifier: Int => Int) =
this.copy(address = address.copy(zipCode = modifier(address.zipCode)))
}
My main goal (simplifying the update on client side) is achieved:
我的主要目标(简化客户端的更新)已实现:
val updatedRaj = raj.modifyZipCode(_ => 41).modifyZipCode(_ + 1)
Creating the full set of modify helpers is obviously annoying. But for internal stuff it is often okay to just create them the first time you try to modify a certain nested field.
创建全套修改助手显然很烦人。但是对于内部内容,通常可以在您第一次尝试修改某个嵌套字段时创建它们。
回答by simbo1905
Shapeless does the trick:
无形的诀窍:
"com.chuusai" % "shapeless_2.11" % "2.0.0"
with:
和:
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)
}
}
}
Note that whilst some other answers here let you compose lenses to go deeper into a given structure these shapless lenses (and other libraries/macros) let you combine two unrelated lenses such that you can make lens that sets an arbitrary number of parameters into arbitrary positions in your structure. For complex data structures that additional composition is very helpful.
请注意,虽然此处的其他一些答案可让您组合镜头以更深入地了解给定结构,但这些无形镜头(和其他库/宏)可让您组合两个不相关的镜头,这样您就可以制作将任意数量参数设置为任意位置的镜头在你的结构中。对于复杂的数据结构,额外的组合非常有帮助。
回答by Erik van Oosten
Perhaps QuickLensmatches your question better. QuickLens uses macro's to convert an IDE friendly expression into something that is close to the original copy statement.
也许QuickLens 更符合您的问题。QuickLens 使用宏将 IDE 友好表达式转换为接近原始复制语句的内容。
Given the two example case classes:
鉴于两个示例案例类:
case class Address(street: String, city: String, state: String, zipCode: Int)
case class Person(firstName: String, lastName: String, address: Address)
and the instance of Person class:
和 Person 类的实例:
val raj = Person("Raj", "Shekhar", Address("M Gandhi Marg",
"Mumbai",
"Maharashtra",
411342))
you can update zipCode of raj with:
您可以使用以下命令更新 raj 的邮政编码:
import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)

