126

次の2つの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))

を更新したい場合zipCodeは、次のrajことを行う必要があります。

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

ネストのレベルが高くなると、これはさらに醜くなります。update-inそのようなネストされた構造を更新するためのよりクリーンな方法(Clojureのようなもの)はありますか?

4

7 に答える 7

188

レンズはこの種のもののために作られたので、誰もレンズを追加しなかったのはおかしいです。これCSの背景紙、Scalaのレンズの使用について簡単に触れたブログ、 Scalazのレンズの実装、そしてそれを使用したコードです。これは驚くほどあなたの質問のように見えますそして、ボイラープレートを削減するために、ケースクラス用のScalazレンズを生成するプラグインがあります

ボーナスポイントについては、レンズに触れる別のSOの質問と、TonyMorrisによる論文があります。

レンズの重要な点は、レンズが構成可能であるということです。そのため、最初は少し面倒ですが、使用するほどに定着していきます。また、個々のレンズをテストするだけでよく、その構成を当然のことと見なすことができるため、テスト容易性にも優れています。

したがって、この回答の最後に提供されている実装に基づいて、レンズを使用してそれを行う方法を次に示します。まず、レンズを宣言して、住所の郵便番号と人の住所を変更します。

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
}
于 2011-04-08T16:02:17.307 に答える
95

ジッパー

Huet の Zipperは、不変データ構造の便利なトラバーサルと「突然変異」を提供します。StreamScalaz は( 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 を変換することです。KiamaはRewritingをサポートしています。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は型システムの外に出ることに注意してください。

于 2010-10-10T13:35:38.107 に答える
12

レンズを使用するための便利なツール:

Scala 2.10 マクロに基づくMacrocosmおよび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
于 2013-05-03T13:16:14.077 に答える
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)

変更ヘルパーの完全なセットを作成するのは明らかに面倒です。しかし、内部的なものについては、特定のネストされたフィールドを最初に変更しようとするときに、それらを作成するだけで問題ないことがよくあります。

于 2014-09-16T14:02:40.837 に答える
4

おそらく、 QuickLensの方があなたの質問により適しています。QuickLens はマクロを使用して、IDE に適した式を元のコピー ステートメントに近いものに変換します。

2 つのケース クラスの例を考えると、次のようになります。

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)
于 2017-04-20T08:09:41.147 に答える