何の記事か
こちらの『Scala Design Patterns』の11章前半です。
Lensパターンの実装方法の一つとして scalaz.Lens
が紹介されていましたが、他に紹介されていた Monocle を使って書き換えてみようというメモです。
Lensパターンとは
Lensパターンは、ネストが深くなっているオブジェクトの一部だけプロパティを書き換えたい、その一方でイミュータブルなオブジェクトを使用したいときなどに使われます。例えば以下のようなオブジェクトで、User
から user.company.address.city.country.code
を書き換えようとすると大量のコードを書く必要が出てきます。
classDiagram class User { +name: String +company: Company +address: Address } class Company { +name: String +address: Address } class Address { +number: Int +street: String +city: City } class City { +name: String +country: Country } class Country { +name: String +code: String } User *-- Company User *-- Address Company *-- Address Address *-- City City *-- Country
例えばこんな感じにプロパティを一つ書き換えただけなのに、 Country
を含むオブジェクトを全てcopyして作り直す必要があります。
val userFixed = user.copy(
company = user.company.copy(
address = user.company.address.copy(
city = user.company.address.city.copy(
country = user.company.address.city.country.copy(
code = user.company.address.city.country.code.toUpperCase
)
)
)
)
)
そこで役に立つのがLensパターンのようです。変更したいプロパティにレンズを通してフォーカスするイメージらしいのですが、具体的なLensパターンの実装自体は未調査です。
scalaz.Lens
本の中でLensパターンを実現しているライブラリとしてまず紹介されているのは、scalaz.Lens
です。Lens[A, B]
は LensFamily[A, A, B, B]
のエイリアスとなっていました。 LensFamily[A1, A2, B1, B2]
は更新前後で型が変わる場合に対応しているのですが、変わらない場合は基本的に Lens[A, B]
を使うのが良さそうです。
/** * A Lens Family, offering a purely functional means to access and retrieve * a field transitioning from type `B1` to type `B2` in a record simultaneously * transitioning from type `A1` to type `A2`. [[scalaz.Lens]] is a convenient * alias for when `A1 === A2`, and `B1 === B2`. * * The term ''field'' should not be interpreted restrictively to mean a member of a class. For example, a lens * family can address membership of a `Set`. * * @see [[scalaz.PLens]] * * @tparam A1 The initial type of the record * @tparam A2 The final type of the record * @tparam B1 The initial type of the field * @tparam B2 The final type of the field */ sealed abstract class LensFamily[A1, A2, B1, B2]
type Lens[A, B] = LensFamily[A, A, B, B]
Lensのgetterとsetterを Lens.lensu(setter, getter)
で定義してあげます。lensu
の関数定義は以下の通りです。
def lensu[A, B](set: (A, B) => A, get: A => B): Lens[A, B]
使用する側ではUser -> Company
のように、クラス -> クラスのプロパティ
という感じで順にアクセスする方法を定義します。
object User { val userCompany: Lens[User, Company] = Lens.lensu[User, Company]( (u, company) => u.copy(company = company), _.company ) val userAddress: Lens[User, Address] = Lens.lensu[User, Address]( (u, address) => u.copy(address = address), _.address ) val companyAddress: Lens[Company, Address] = Lens.lensu[Company, Address]( (c, address) => c.copy(address = address), _.address ) val addressCity: Lens[Address, City] = Lens.lensu[Address, City]( (a, city) => a.copy(city = city), _.city ) val cityCountry: Lens[City, Country] = Lens.lensu[City, Country]( (c, country) => c.copy(country = country), _.country ) val countryCode: Lens[Country, String] = Lens.lensu[Country, String]( (c, code) => c.copy(code = code), _.code ) }
そして、andThen
のエイリアスである >=>
を使って繋げてます。こうすることで、User
から Country.code
までたどることができます。
val userCompanyCountryCode: Lens[User, String] = userCompany >=> companyAddress >=> addressCity >=> cityCountry >=> countryCode // こっちは compose を使ったもの。表記が逆になるだけでやってることは同じ。 val userCompanyCountryCodeCompose: Lens[User, String] = countryCode <=< cityCountry <=< addressCity <=< companyAddress <=< userCompany
Monocle
本の中で具体的に紹介されていたのは scalaz.Lens
でしたが、より便利なライブラリとしてお勧めされていた Monocle · Access and transform immutable data を使って書き換えてみようと思います。
まず、Monocleはサイトを見ると Scala 2.13 以降に対応しているようなので、サンプルプロジェクトのScalaのバージョンを2.12から上げます。ScalaTestやMockitoなどのプロジェクトに含まれている他のライブラリのバージョンも、Scalaのバージョンに追従して上げる必要がありました。
scalaVersion := "2.13.8" libraryDependencies ++= { Seq( ... "dev.optics" %% "monocle-core" % "3.1.0", "dev.optics" %% "monocle-macro" % "3.1.0", ... ) }
基本的にはサイトに書いてある通りで、以下のような流れになります。
foo.focus
でfoo
オブジェクトの持っているプロパティ内部に注目focus
の中で指定したプロパティを、modify
によって変更- 変更後のオブジェクトを取得
簡単ですね。
val ivanFixed = ivan.focus(_.company.address.city.country.code).modify(_.toUpperCase)
暗黙的に AppliedFocusOps
に変換しているらしいのですが、中身はマクロで書かれていて理解はまだできませんでした。
出力結果を見ると確かに UK
と大文字になっていることが確認できます。
User(Ivan,Company(Castle Builders,Address(1,Buckingham Palace Road,City(London,Country(United Kingdom,uk)))),Address(1,Geneva Lake,City(geneva,Country(Switzerland,CH)))) Capitalize UK code... User(Ivan,Company(Castle Builders,Address(1,Buckingham Palace Road,City(London,Country(United Kingdom,UK)))),Address(1,Geneva Lake,City(geneva,Country(Switzerland,CH))))