说我有两个 case classES:

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

现在如果我想更新 zipCoderaj 那我将不得不做:

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

随着筑巢的更多水平,这变得更加丑陋。是否有更干净的方式(像Clojure一样 update-in)更新此类嵌套结构?

有帮助吗?

解决方案

拉链

Huet的拉链 提供不变数据结构的方便遍历和“突变”。 Scalaz为拉链提供了拉链 Stream (Scalaz.Zipper), 和 Tree (scalaz.treeleoc)。事实证明,拉链的结构以类似于代数表达的符号分化的方式自动从原始数据结构中自动衍生。

但是,这如何帮助您参加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)

因此,社区需要说服Scala团队,应该继续进行这项工作并将其集成到编译器中。

顺便说一句,卢卡斯最近 出版 PACMAN的版本,用户可以通过DSL编程。不过,看起来他没有使用过修改的编译器,因为我看不到任何 @zip 注释。

树的重写

在其他情况下,您可能希望根据某些策略(自上而下,自下而上)在整个数据结构上应用一些转换,并基于与该结构中某个点相匹配的规则。经典示例是为一种语言转换AST,可能是为了评估,简化或收集信息。 Kiama 支持 重写, ,请参见示例 重新创下, ,看这个 视频. 。这是一个可以激发您食欲的片段:

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

请注意Kiama 户外 实现此目标的类型系统。

其他提示

有趣的是,没有人添加镜片,因为它们是为了这种东西而制作的。所以, 这里 是CS背景纸, 这里 是一个博客,简要介绍了Scala中的镜头使用, 这里 是Scalaz的镜头实施 这里 是使用它的一些代码,看起来像您的问题令人惊讶。并且,要切下锅炉板, 这是 为案例类生成scalaz镜头的插件。

对于奖励积分, 这是 另一个涉及镜头的问题,一个 托尼·莫里斯(Tony Morris)。

关于镜头的最大交易是它们是可以组合的。因此,起初它们有点麻烦,但是您使用的越多,它们就会不断增加。另外,它们非常适合可测试性,因为您只需要测试单个镜头,并且可以理所当然的构图。

因此,基于本答案结束时提供的实现,这是您使用镜头进行操作的方法。首先,宣布镜头在地址中更改邮政编码,并在一个人中更改地址:

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

现在,组成它们以获取改变一个人的Zipcode的镜头:

val personZipCodeLens = personAddressLens andThen addressZipCodeLens

最后,使用镜头改变拉吉:

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
}

使用镜头的有用工具:

只是想补充 宏观rillit 基于Scala 2.10宏的项目提供动态镜头的创建。


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

使用MacRocosm:

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

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

我一直在寻找具有最佳语法和最佳功能的Scala库,此处未提及的一个库是 单片 对我来说,这真的很好。一个示例如下:

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

这些非常好,有很多方法可以结合镜头。例如,Scalaz需要大量的样板,并且可以快速编译并运行良好。

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

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
)

无形的技巧:

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

请注意,虽然这里有其他一些答案,让您组合镜头更深入地进入给定的结构,这些无形的镜头(和其他库/宏)使您可以将两个无关的镜头组合在一起,以便您可以使镜头将任意数量的参数设置为任意位置在您的结构中。对于复杂的数据结构,其他组合非常有用。

由于它们的合成性质,镜片为重度嵌套结构的问题提供了一个很好的解决方案。但是,由于筑巢的水平较低,我有时会觉得镜头有点太多了,而且如果只有很少有嵌套更新的地方,我不想引入整个镜头方法。为了完整,这是针对这种情况的非常简单/务实的解决方案:

我要做的就是简单地写一些 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)

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

也许 Quicklens 更好地匹配您的问题。 Quicklens使用宏将IDE友好表达式转换为接近原始复制语句的东西。

给定两个示例案例类:

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

和人类的实例:

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

您可以使用:

import com.softwaremill.quicklens._
val updatedRaj = raj.modify(_.address.zipCode).using(_ + 1)
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top