代码之家  ›  专栏  ›  技术社区  ›  missingfaktor Kevin Wright

更新嵌套结构的更简洁方法

  •  120
  • missingfaktor Kevin Wright  · 技术社区  · 16 年前

    假设我有以下两个 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))
    

    现在如果我想更新 zipCode raj 那我就得做:

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

    随着嵌套层次的增加,这会变得更加难看。有更干净的方法吗(像Clojure的 update-in )更新这样的嵌套结构?

    7 回复  |  直到 16 年前
        1
  •  94
  •   om-nom-nom Dennis Williamson    14 年前

    拉链

    Huet's Zipper 为不可变数据结构提供方便的遍历和“变异”。Scalaz提供拉链 Stream scalaz.Zipper ),和 Tree ( scalaz.TreeLoc ). 事实证明,zipper的结构是从原始数据结构自动派生的,其方式类似于代数表达式的符号微分。

    但是这对你的Scala case类有什么帮助呢?嗯,卢卡斯·瑞兹最近 prototyped

    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团队继续这项工作并将其集成到编译器中。

    published Pacman的一个版本,用户可以通过DSL进行编程。不过,我看不出他使用了修改过的编译器 @zip 注解。

    树重写

    在其他情况下,您可能希望根据某种策略(自上而下、自下而上)并基于与结构中某个点的值匹配的规则,在整个数据结构中应用某种转换。经典的例子是转换一种语言的AST,可能是为了评估、简化或收集信息。 Kiama 支架 Rewriting ,请参见中的示例 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))
    

    注意Kiama steps outside 实现这一点的类型系统。

        2
  •  186
  •   Community Mohan Dere    9 年前

    有趣的是,没有人添加镜头,因为他们是为这种东西。所以, here 是CS的背景文件, here 是一个关于Scala使用镜头的博客, here 是Scalaz和 here 是一些使用它的代码,这看起来很像你的问题。而且,为了减少锅炉板, here's

    here's 另一个涉及镜头的S.O.问题,以及 paper 作者: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)
    

    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
    }
    
        3
  •  11
  •   Mike Morearty NiCk Newman    12 年前

    使用镜头的有用工具:

    只是想补充一下 Macrocosm 和 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))
    

    使用宏空间:

    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
    
        4
  •  9
  •   Johan S    12 年前

    我一直在寻找哪一个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"))
    

    这些都是非常好的,有很多方法来组合镜头。例如,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
    )
    
        5
  •  7
  •   simbo1905    11 年前

    不成形的技巧是:

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

    请注意,虽然这里的一些其他答案可以让您组合镜头深入到给定的结构中,但这些无形状镜头(和其他库/宏)可以让您组合两个不相关的镜头,这样您就可以将任意数量的参数设置到结构中的任意位置。对于复杂的数据结构,额外的组合非常有用。

        6
  •  7
  •   bluenote10    9 年前

    我所做的就是简单地写一些 modify... 顶层结构中的helper函数,用于处理难看的嵌套副本。例如:

    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)
    

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

        7
  •  4
  •   Erik van Oosten    9 年前

    也许 QuickLens 更符合你的问题。QuickLens使用宏将IDE友好的表达式转换为接近原始copy语句的内容。

    给出两个实例类:

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

    您可以使用以下命令更新raj的zipCode:

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