web-dev-qa-db-ja.com

ネストされた構造を更新するよりクリーンな方法

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

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

121
missingfaktor

ジッパー

HuetのZipper は、不変のデータ構造の便利なトラバースと「突然変異」を提供します。 Scalazは、Streamscalaz.Zipper )、およびTreescalaz.TreeLoc )。ジッパーの構造は、代数的表現の象徴的な微分に似た方法で、元のデータ構造から自動的に導出できることがわかりました。

しかし、これはあなたのScalaケースクラスでどのように役立ちますか?さて、Lukas Rytzは最近 prototyped 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 サポート RewritingRewriterTests の例を参照し、これを見る 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 は、これを達成するために型システムの外に出る ことに注意してください。

94
retronym

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

ボーナスポイントの場合、 here's another S.O.レンズに触れる質問、および 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
}
182

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

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
11

私は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
)
9
Johan S

構成可能な性質により、レンズは、ネスト構造の問題に対する非常に優れたソリューションを提供します。ただし、ネストのレベルが低いと、レンズが少し多すぎると感じることがあり、ネストされた更新がある場所が少ない場合、レンズ全体のアプローチを導入したくありません。完全を期すために、この場合の非常にシンプルで実用的なソリューションを以下に示します。

私がしていることは、単にいくつかのmodify... nestedいネストされたコピーを処理するトップレベル構造のヘルパー関数。例えば:

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
bluenote10

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

ここで他のいくつかの答えは、レンズを構成して特定の構造に深く入り込むことができることに注意してくださいあなたの構造に。複雑なデータ構造の場合、追加の構成が非常に役立ちます。

7
simbo1905

多分 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)
4
Erik van Oosten