중첩 된 구조를 업데이트하는보다 깔끔한 방법


124

다음 두 가지 항목이 있다고 가정합니다 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))

내가 업데이트 할 경우 지금 zipCoderaj나는이해야 할 것 :

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

중첩 수준이 높을수록 더보기 흉해집니다. update-in이러한 중첩 구조를 업데이트 하는 더 깨끗한 방법 (Clojure 같은 것 )이 있습니까?


1
불변성을 보존하고 싶다고 가정합니다. 그렇지 않으면 Persons의 주소 선언 앞에 var를 붙이십시오.
GClaramunt 2010

8
@GClaramunt : 예, 불변성을 보존하고 싶습니다.
missingfaktor 2010

답변:


94

지퍼

Huet의 Zipper 는 불변 데이터 구조의 편리한 순회 및 '변이'를 제공합니다. Scalaz는 Stream( scalaz.Zipper ) 및 Tree( scalaz.TreeLoc )에 대한 지퍼를 제공합니다 . 지퍼의 구조는 대수식의 상징적 미분과 유사한 방식으로 원래 데이터 구조에서 자동으로 파생 될 수 있음이 밝혀졌습니다.

하지만 이것이 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 팀을 설득해야합니다.

덧붙여서 Lukas는 최근 DSL을 통해 사용자가 프로그래밍 할 수있는 Pacman 버전을 발표 했습니다. 하지만 @zip주석을 볼 수 없기 때문에 수정 된 컴파일러를 사용한 것처럼 보이지 않습니다 .

트리 재 작성

다른 상황에서는 일부 전략 (하향식, 상향식)에 따라 구조의 특정 지점에서 값과 일치하는 규칙을 기반으로 전체 데이터 구조에 일부 변환을 적용 할 수 있습니다. 고전적인 예는 아마도 정보를 평가, 단순화 또는 수집하기 위해 언어에 대한 AST를 변환하는 것입니다. 키 아마 지원 다시 쓰기는 ,의 예제를 참조 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))

Kiama 는이를 달성하기 위해 유형 시스템을 벗어납니다 .


2
커밋을 찾는 사람들을 위해. 여기 있습니다 : github.com/soundrabbit/scala/commit/… (내 생각에 ..)
IttayD

15
이봐, 렌즈는 어딨어?
Daniel C. Sobral

방금이 문제가 발생했고 @zip 아이디어가 정말 환상적으로 들립니다. 모든 케이스 클래스에 문제가있을 수 있도록 지금까지 가져와야할까요? 왜 이것이 구현되지 않습니까? 렌즈는 멋지지만 크고 많은 클래스 / 케이스 클래스를 사용하면 setter 만 원하고 증 분자처럼 멋진 것은없는 경우 상용구 일뿐입니다.
Johan S

186

렌즈를 추가하지 않았다는 사실이 재밌습니다. 그래서, 여기 그것에 CS 배경 용지이며, 여기에 렌즈에 터치 간단히 스칼라에서 사용하는 블로그입니다, 여기 Scalaz의 렌즈를 구현하고 여기에 의외로 질문처럼 보이는 그것을 사용하는 일부 코드이다. 그리고, 보일러 판을 줄이기 위해 여기 경우 클래스 Scalaz 렌즈를 생성하는 플러그인은.

보너스 포인트를 들어, 여기에 렌즈에 닿는 또 다른 SO 질문 및 종이 토니 모리스가.

렌즈의 큰 문제는 구성이 가능하다는 것입니다. 따라서 처음에는 약간 번거롭지 만 더 많이 사용할수록 계속해서 자리를 잡습니다. 또한 개별 렌즈를 테스트하기 만하면되고 그 구성을 당연하게 받아 들일 수 있기 때문에 테스트 가능성에 매우 좋습니다.

따라서이 답변의 끝에 제공된 구현을 기반으로 렌즈로 수행하는 방법은 다음과 같습니다. 먼저 렌즈를 신고하여 주소의 우편 번호를 변경하고 사람의 주소를 변경합니다.

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
}

1
Gerolf Seitz의 렌즈 플러그인에 대한 설명으로이 답변을 업데이트 할 수 있습니다.
missingfaktor 2011-08-29

@missingfaktor 물론입니다. 링크? 나는 그런 플러그인을 몰랐습니다.
Daniel C. Sobral

1
코드 personZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)는 다음과 같습니다personZipCodeLens mod (raj, _ + 1)
ron

하지만 @ron mod은 렌즈의 기본 요소가 아닙니다.
Daniel C. Sobral

Tony Morris는 이 주제에 대해 훌륭한 논문 을 썼습니다 . 나는 당신의 대답에 그것을 연결해야한다고 생각합니다.
missingfaktor 2012 년

11

렌즈 사용에 유용한 도구 :

Scala 2.10 매크로를 기반으로 하는 MacrocosmRillit 프로젝트가 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))

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

아마 더 나은 Rillit를 놓쳤을 것입니다. :-) github.com/akisaarinen/rillit
missingfaktor

니스, 그것을 확인합니다
세바스티앙 Lorber

1
Btw 나는 Rillit를 포함하도록 내 대답을 편집했지만 Rillit가 더 나은 이유를 정말로 이해하지 못합니다. 첫눈에 동일한 장황함으로 동일한 기능을 제공하는 것 같습니다 @missingfaktor
Sebastien Lorber

@SebastienLorber 재미있는 사실 : Rillit는 핀란드어이고 렌즈를 의미합니다. :)
Kai Sellgren

Macrocosm과 Rillit는 모두 지난 4 년 동안 업데이트되지 않은 것 같습니다.
에릭 반 Oosten

9

나는 가장 멋진 구문과 최고의 기능을 가진 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
)

7

Shapeless는 트릭을 수행합니다.

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

여기에 다른 답변을 사용하면 주어진 구조에 더 깊이 들어가도록 렌즈를 구성 할 수 있지만,이 shapless 렌즈 (및 기타 라이브러리 / 매크로)를 사용하면 관련없는 두 렌즈를 결합하여 임의의 매개 변수를 임의의 위치로 설정하는 렌즈를 만들 수 있습니다. 당신의 구조에서. 복잡한 데이터 구조의 경우 추가 구성이 매우 유용합니다.


결국 LensDaniel C. Sobral의 답변에서 코드를 사용하여 외부 종속성을 추가하는 것을 피했습니다.
simbo1905

7

구성 가능한 특성으로 인해 렌즈는 중첩 된 구조의 문제에 대한 매우 좋은 솔루션을 제공합니다. 그러나 중첩 수준이 낮 으면 가끔 렌즈가 너무 많다고 느끼고 중첩 업데이트가있는 장소가 적다면 전체 렌즈 접근 방식을 도입하고 싶지 않습니다. 완전성을 위해 다음은이 경우에 대한 매우 간단하고 실용적인 솔루션입니다.

내가하는 일은 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)

수정 도우미의 전체 집합을 만드는 것은 분명히 성가신 일입니다. 그러나 내부 항목의 경우 특정 중첩 필드를 처음 수정하려고 할 때 생성하는 것이 좋습니다.


4

아마도 QuickLens은 더 나은 귀하의 질문에 일치합니다. QuickLens는 매크로를 사용하여 IDE 친화적 인 식을 원래 copy 문에 가까운 것으로 변환합니다.

두 가지 예제 케이스 클래스가 주어집니다.

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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.