quicklensでネストしたケースクラスを簡単にコピーする

Scalaのケースクラスはイミュータブルなため、値を変更する場合はcopy()メソッドで一部のプロパティを書き換えた新しいインスタンスを生成する必要があります。

しかし、ネストしたケースクラスの場合、copy()メソッドの呼び出しもネストさせる必要があり、かなり冗長なコードになってしまいます。

case class Street(name: String)
case class Address(street: Street)
case class Person(address: Address, age: Int)

val person = Person(Address(Street("1 Functional Rd.")), 35)

// ネストしたプロパティを変更したインスタンスを生成
val p2 = person.copy(
  address = person.address.copy(
    street = person.address.street.copy(
      name = "3 OO Ln."
    )
  )
)

こんな場合はLensを使うと簡単にコピーすることができます。たとえばMonocleというLensライブラリだと以下のような感じになります。

val p2 = (_address ^|-> _street ^|-> _name).modify(_ =>"3 OO Ln.")(person)

LensはScalazやShapelessといったScalaの関数型ライブラリにも含まれていますが、このようなコードを書くためにはケースクラスに対応したLensを自前で定義しなくてはなりません(Monocleの場合はアノテーションやマクロで自動生成することもできますが)。そこで登場するのがquicklensというライブラリです。

github.com

このライブラリはimport文を1行追加するだけで、以下のような感じでケースクラスのコピーを行うコードを簡単に書くことができるようになります。

import com.softwaremill.quicklens._

val p2 = person.modify(_.address.street.name).setTo("3 OO Ln.")

元の値を加工したい場合はsetTo()メソッドの代わりにusing()メソッドを使います。また、modify()メソッドはチェーンさせることもできます。

// 元の値を加工したい場合はusingを使う
val p3 = person.modify(_.address.street.name).using(_.toUpperCase)

// modifyをチェーンさせて複数のプロパティを変更
val p4 = person
  .modify(_.address.street.name).using(_.toUpperCase)
  .modify(_.age).using(_ - 1)

他のLensライブラリと比べると非常に手軽に使えるので便利なのですが、実はこのquicklensはLensを生成しているわけではなく、modify()メソッドの呼び出しをマクロでcopy()メソッドを呼び出すコードに展開しているだけです。コードも割とシンプルなのでマクロの参考としてもよいのではないかと思います。