shapelessのLensを試してみる。
試す
簡単なLensの例
scala> import shapeless._
//import shapeless._
scala> case class Foo(a: Int, b: Int)
//defined class Foo
scala> val aLens = lens[Foo].a
//aLens: shapeless.Lens[Foo,Int] = shapeless.Lens$$anon$7@57039e80
scala> aLens.get(Foo(1,2))
//res1: Int = 1
scala> aLens.set(Foo(1,2))(3)
//res2: Foo = Foo(3,2)
FooのaにアクセスするLensを作って、Fooのインスタンスを渡すとaが取れる。セットもできる。
もちろんネストしてても。
scala> case class Bar(foo: Foo)
//defined class Bar
scala> val aLens = lens[Bar].foo.a
//aLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@389f9397
scala> aLens.get(Bar(Foo(1,2)))
//res3: Int = 1
scala> aLens.set(Bar(Foo(1,2)))(3)
//res4: Bar = Bar(Foo(3,2))
存在しないキーを指定してLensを作ろうとするとコンパイルエラー。
scala> lens[Bar].fo
//<console>:18: error: could not find implicit value for parameter mkLens: shapeless.MkSelectDynamicOptic[shapeless.Lens[Bar,Bar],Bar,shapeless.tag.@@[Symbol,String("fo")],B]
// lens[Bar].fo
その他の細かな機能
複数繋げてタプルで取得やセットができる。
scala> val aLens = lens[Bar].foo.a
//aLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@6da49f8e
scala> val bLens = lens[Bar].foo.b
//bLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@3f8bb4da
scala> val abLens = aLens ~ bLens
//abLens: shapeless.ProductLensBuilder[Bar,(Int, Int)] = shapeless.Lens$$anon$1@32d63616
scala> abLens.get(Bar(Foo(1,2)))
//res19: (Int, Int) = (1,2)
scala> abLens.set(Bar(Foo(1,2)))((3,4))
//res20: Bar = Bar(Foo(3,4))
パスを定義することも。
scala> val aLens = lens[Bar](^.foo.a)
//aLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@6e72e7a2
scala> aLens.get(Bar(Foo(1,2)))
//res8: Int = 1
>>
の記号でキーを指定したりインデックスで番号を指定することも。
scala> (lens[Bar] >> 'foo).get(Bar(Foo(1,2)))
//res26: Foo = Foo(1,2)
scala> (lens[Bar] >> 0).get(Bar(Foo(1,2)))
//res27: Foo = Foo(1,2)
パターンマッチも。
scala> val aLens = lens[Bar].foo.a
//aLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@6da49f8e
scala> val aLens(a) = Bar(Foo(1,2))
//a: Int = 1
合成も。
scala> val aLens = lens[Foo].a compose lens[Bar].foo
//aLens: shapeless.Lens[Bar,Int] = shapeless.Lens$$anon$7@6e3b3bf5
scala> aLens.get(Bar(Foo(1,2)))
//res28: Int = 1
Prism
Lensは取得するとそのまま値が返るが、PrismはOptionで返る。
sealed trait等(Coproduct)を指定した場合、取得できないかもしれないのでPrismが使われる。
sealed trait Tree[T]
case class Leaf[T](t: T) extends Tree[T]
case class Node[T](l: Tree[T], r: Tree[T]) extends Tree[T]
scala> val leafLens = lens[Tree[Int]][Leaf[Int]].t
//leafLens: shapeless.Prism[Tree[Int],Int] = shapeless.Lens$$anon$8@76a3f102
scala> leafLens.get(Leaf(1))
//res48: Option[Int] = Some(1)
scala> leafLens.get(Node(Leaf(1), Leaf(2)))
//res49: Option[Int] = None
lens
の他にもprism
やoptic
でも同じもののようだ。
仕組み
例えばlens[Foo].a
であれば、Fooをキー付きのHList(Recordと呼ばれてる)に変換して、そこから指定したキーaで値を取得・更新するLensを作っている。
本体のコードはこちら
おわり
~
で繋げてタプルでまとめて取得できるLensBuilderは便利だなと思いました(小並感)
SelectDynamicが使われてるので補完がきかなくてダルい...。Monocleは補完効くのでいいなぁ。