ネストされた構造を更新するためのより明確な方法


124

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

更新したい場合はzipCoderaj次のようにする必要があります。

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

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


1
私はあなたが不変性を維持したいと思います、そうでなければ、単に人のアドレス宣言の前に変数を貼り付けます。
GClaramunt、

8
@GClaramunt:はい、不変性を保持したいのですが。
missingfaktor

回答:


94

ジッパー

Huetのジッパーは、不変のデータ構造の便利なトラバーサルと「変異」を提供します。Scalazはジッパーを提供しますStreamScalazscalaz.Zipper)とTreescalaz.TreeLoc)のZipperを。ジッパーの構造は、代数式の記号による区別に似た方法で、元のデータ構造から自動的に導出できることがわかります。

しかし、これはあなたのScalaケースクラスでどのように役立ちますか?さて、ルーカスリッツは最近、注釈付きのケースクラスのジッパーを自動的に作成する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チームに説得する必要があります。

ちなみにルーカスは最近 、DSLを介してユーザーがプログラム可能なバージョンのパックマンを公開しました。しかし、私は@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
ねえ、レンズはどこ?
ダニエルC.ソブラル

私はこの問題に遭遇したばかりで、@ zipのアイデアは本当に素晴らしいようです。なぜこれが実装されていないのですか?レンズは素晴らしいですが、クラスが多く、クラスが多いため、セッターが必要なだけで、インクリメンターのような豪華なものは不要です。
ヨハンS

186

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

ボーナスポイントについては、レンズに関するその他のSOの質問と、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))

次に、それらを構成して、人の郵便番号を変更するレンズを取得します。

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

@missingfaktorはい。リンク?私はそのようなプラグインを知りませんでした。
ダニエルC.ソブラル2011

1
コードpersonZipCodeLens.set(raj, personZipCodeLens.get(raj) + 1)は同じですpersonZipCodeLens mod (raj, _ + 1)
ron

ただし、@ ron modはレンズのプリミティブではありません。
ダニエルC.ソブラル2012年

トニー・モリスはこの問題について素晴らしい論文を書いています。私はあなたの答えにそれをリンクするべきだと思います。
missingfaktor 2012年

11

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

Scala 2.10マクロに基づく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))

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

あなたはおそらくリリットを見逃したでしょう。:-) github.com/akisaarinen/rillit
missingfaktor

いいですね、チェックします
Sebastien Lorber 2013年

1
ところで、Rillitを含めるように回答を編集しましたが、Rillitの方が優れている理由がよくわかりません。一見すると同じ冗長性で同じ機能を提供しているようです@missingfaktor
Sebastien Lorber

@SebastienLorberおもしろい事実:Rillitはフィンランド語でレンズを意味します:)
Kai Sellgren 2014

MacrocosmとRillitはどちらも過去4年間更新されていないようです。
エリックファンオステン2017

9

私は、最も優れた構文と最高の機能を備えたScalaライブラリーを探していました。ここで言及されていない1つのライブラリーは、私にとって本当に優れたモノクルです。次に例を示します。

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

ここで他のいくつかの回答では、レンズを構成して特定の構造にさらに深く入れることができますが、これらのシャープレスレンズ(および他のライブラリ/マクロ)は、任意の数のパラメーターを任意の位置に設定するレンズを作成できるように、2つの無関係なレンズを組み合わせることができますあなたの構造で。複雑なデータ構造の場合、追加の構成が非常に役立ちます。


最終的にはLensダニエルC.ソブラルの回答のコードを使用することになり、外部依存関係の追加を回避したことに注意してください。
simbo1905 2017

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フレンドリーな式を元のコピーステートメントに近いものに変換します。

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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.