Scala初学者のためのテクニック集、今回は Lens を紹介します。
Lensは、getterやsetterを関数型言語的に抽象化した概念で、関数合成することでネストしたデータ構造に対するアクセスを容易にしてくれます。
今回はそのLensの初歩的な使い方を紹介したいと思います。
Lensの具体例
解決したいこと
例えば次のような社員と所属する会社とその住所を表現したクラスがあったとします。
これは、Employee - Company - Address - Streetという具合にネストしたクラス構成となっています。
case class Employee(name: String, company: Company)
case class Company(name: String, address: Address)
case class Address(city: String, street: Street)
case class Street(number: Int, name: String)
このクラス構成をもとに社員のTaroさんを表現したのが以下になります。
Employee("Taro", Company("Darkside Company", Address("Washington", Street(1000, "Pennsylvania"))))
今回はTaroさんの所属する会社の住所の番地(number)に誤りがあることに気づいたので、1000
から 1230
へ不変性を保ったまま変更したいと思います。
Lens適用前
いきなりネストが深い状態で考えると分かりづらいので、末端のStreetクラスに対する変更から1階層ずつネストを深くして見ていきましょうか。
まずは末端のStreetを直接変更します。
object Example extends App {
// 元の状態
val street = Street(1000, "Pennsylvania")
// 番地を変更
val fixedStreet = street.copy(number = 1230)
// => Street(1230,Pennsylvania)
}
不変性を保つため、case classのcopyメソッドで一部のフィールドを書き換えたうえでコピーしています。まだクラスがネストしていないので分かりやすいですね。
次にAddressクラスに内包されている場合を考えます。
object Example extends App {
// 元の状態
val address = Address("Washington", Street(1000, "Pennsylvania"))
// 番地を変更
val fixedAddress = address.copy(
street = address.street.copy(
number = 1230
)
) // => Address(Washington,Street(1230,Pennsylvania))
}
もとの状態がネストしているので、copyもその分ネストしなければなりません。しかしまだまだ読める。
更にCompanyクラスに内包されている場合を見てみましょう。
object Example extends App {
// 元の状態
val company = Company("Darkside Company", Address("Washington", Street(1000, "Pennsylvania")))
// 番地を変更
val fixedCompany = company.copy(
address = company.address.copy(
street = company.address.street.copy(
number = 1230
)
)
) // => Company(Darkside Company,Address(Washington,Street(1230,Pennsylvania)))
}
この辺から段々心がやられそうになりますね。
最後にRootであるEmployeeに内包されている場合。
object Example extends App {
// 元の状態
val employee = Employee("Taro", Company("Darkside Company", Address("Washington", Street(1000, "Pennsylvania"))))
// 番地を変更
val fixedEmployee = employee.copy(
company = employee.company.copy(
address = employee.company.address.copy(
street = employee.company.address.street.copy(
number = 1230
)
)
)
) // => Employee(Taro,Company(Darkside Company,Address(Washington,Street(1230,Pennsylvania))))
}
うお、ネストしまくりでとてもつらい。
Lensはこんなネストしまくりなデータ構造に対して不変性を保ったまま値を変更する場合などに効果的です。
Lensの適用
今回は Scalaz で提供されているLensを使ってみましょう。
build.sbtに以下を追加してください。
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.7"
そして、Lensを適用した場合のコードはこんな感じになります。
// Employeeのコンパニオンオブジェクト
object Employee {
// EmployeeのCompanyに対するsetter/getter
val employeeCompany = Lens.lensu[Employee, Company](
(x, value) => x.copy(company = value), // setter
_.company // getter
)
// CompanyのAddressに対するsetter/getter
val companyAddress = Lens.lensu[Company, Address](
(x, value) => x.copy(address = value),
_.address
)
// AddressのStreetに対するsetter/getter
val addressStreet = Lens.lensu[Address, Street](
(x, value) => x.copy(street = value),
_.street
)
// Streetのnumberに対するsetter/getter
val streetNumber = Lens.lensu[Street, Int](
(x, value) => x.copy(number = value),
_.number
)
// 上記の定義を >=> (andThenのエイリアス)で関数合成して、
// Employeeから末端のnumberに対するアクセスを可能にする
val emploeeCompanyAddressStreetNumber = employeeCompany >=> companyAddress >=> addressStreet >=> streetNumber
}
object Example extends App {
import Employee._
// 元の状態
val taro = Employee("Taro", Company("Darkside Company", Address("Washington", Street(1000, "Pennsylvania"))))
// コンパニオンオブジェクトで定義したsetterを使って番地を変更
val fixedTaro = emploeeCompanyAddressStreetNumber.set(taro, 1230)
// => Employee(Taro,Company(Darkside Company,Address(Washington,Street(1230,Pennsylvania))))
}
コンパニオンオブジェクトに定義しているのは、それぞれのクラスで内包しているフィールドに対するgetter/setterです。そして個々の定義を >=>
で関数合成したものをemploeeCompanyAddressStreetNumber
として定義しています。
この関数合成した定義を使うことで、Employeeから末端のnumberへ簡略的な記述でアクセスできるようになるのです。
個々のgetter/setterを色々定義しないといけないのはアレですが、一度定義さえしておけばネストが深いデータ構造でもシンプルに、しかも不変性を保ったままアクセスすることができるのです。
ちなみにLensはScalazの他にも Monocle などもあります。Monocleはマクロを使って、さっきのコンパニオンオブジェクトで書いたような定義を簡略化することができるようです。
というわけで、今回はLensの紹介でした。