追記: Shapeless入門を記事にまとめました
ScalaにはShapelessというジェネリックプログラミングを推進するためのライブラリがあって、同じ型の構造を持つCase Class間を変換するといった操作ができるのだけれど、あまり詳しいことは知らなかったので、公式のガイドブックを読んで勉強することにした。
https://books.underscore.io/shapeless-guide/shapeless-guide.pdf
無料でこういう本が読めるのはありがたすぎる。
早速2章の終わりまで読んだので、備忘録として記録を残しておきたい。コードサンプルはだいたい↑からの引用。
Chapter 1
Chapter 1はこれからどういう構成で行くよ、という紹介パートなので、割愛。
Chapter 2
Chapter 2は、ADTsの復習と、HList
・Generic
・Coproduct
の紹介に充てられている。
ADTs
まず、ADTsについての復習から。他の関数型言語と同様、Scalaには
おおまかには、ADTsは、AndとOrの構造を持ったデータ構造だといえる。例えば、次のようなコードを考える。
sealed trait Shape final case class Rectangle(width: Double, height: Double) extends Shape final case class Circle(radius: Double) extends Shape
この場合、Shape
は、Rectangle
Circle
であり、Rectangle
は Double
Double
から成り、 Circle
は Double
から成っている。
専門用語を使って言い換えると、Andを使ってデータ構造を組み立てることをProductと呼び、Orを使ってデータ構造を組み立てることをCoproductと呼ぶ。 今後はこの用語を使っていく。
ADTsのencoding
さて、ADTsをsealed trait
とcase class
とを使って表現したけれど、ADTsをコードに落とし込む(encode)方法はそれだけではない。
例えば、Tupleを使ってProductし、Eitherを使ってCoproductしてもよい。
type Rectangle2 = (Double, Double) type Circle2 = Double type Shape2 = Either[Rectangle2, Circle2]
表現方法が異なるだけで、全く同じことができる。プロパティの名称などが剥がされているので、Tuple/Eitherを使った方が、より一般化されている。
Shapelessは独自の方法でADTsをencodeする
Shapelessは、前掲した2つの方法のいずれも取らず、独自の方法でADTsをencodeする。
理由はいくつかある。
- Tupleは個数制限がある
Tuple1
,Tuple2
, ... といったそれぞれの型同士には関連性がなく、深さが変化するような場合に使いにくい- 0-sizedなTupleがない
- Unitがしばしばこの目的に使われるが、UnitとTupleNとのupper boundをとると
Any
になってしまうので、全然使えない
- Unitがしばしばこの目的に使われるが、UnitとTupleNとのupper boundをとると
ADTsをencodeするためには、ProductとCoproductそれぞれについてencode方法を考えることになる。まずはProductから見ていく。
HList for Product
まずProductをencodeするために、shapelessはshapeless.HList
を導入する。
HListはHeterogeneous Listの略。
HListはListと異なり、それぞれの要素が独自の型を持つことができ、型レベルで長さがわかっている。
型コンストラクタはshapeless.::
であり、shapeless.HNil
が通常のListのNil
に相当する。
import shapeless.{HList, ::, HNil} val product: String :: Int :: Boolean :: HNil = "Sunday" :: 1 :: false :: HNil
これだけだとTupleを使うのとあまり変わり映えしないが、そのうち強さが分かってくる、ということらしい。
型クラス登場: Generic
ここで、case classとHListとの相互変換を行ってくれる便利型クラスのshapeless.Generic
が登場する。
型クラスなので別の型を取ってなんらかの働きをしてくれるのだが、同名のマクロによってこれは自動導出される。
例えば、val fooGen = Generic[FooCaseClass]
のように書くと、 マクロによってGeneric[FooCaseClass]
のインスタンスが自動導出される。
Generic[A]
のインスタンスは、内部にRepl
という型を持っていて、A
をHList
にencodeした型が格納される。
import shapeless.Generic case class IceCream(name: String, numCherries: Int, inCone: Boolean) val iceCreamGen = Generic[IceCream] // iceCreamGen: shapeless.Generic[IceCream]{type Repr = String :: Int :: Boolean :: shapeless.HNil} = anon$macro$4$1@6b9323fe
そして、to
/ from
メソッドを呼び出すことで、case classとHListとを相互に変換できる。
val iceCream = IceCream("Sundae", 1, false) // iceCream: IceCream = IceCream(Sundae,1,false) val repr = iceCreamGen.to(iceCream) // repr: iceCreamGen.Repr = Sundae :: 1 :: false :: HNil val iceCream2 = iceCreamGen.from(repr) // iceCream2: IceCream = IceCream(Sundae,1,false)
ところで、genericな方に向けるために使うのがto
で、concreteな方に向けるために使うのがfrom
なのは若干分かりづらい。genericな方向に向かっていくからto、genericな方から戻ってくるからfrom、と考えると良さそう。
Reprが同じなら構造が一緒なので、to
してfrom
することで、型安全にcase class間を変換できる。
case class Employee(name: String, number: Int, manager: Boolean) // Create an employee from an ice cream: val employee = Generic[Employee].from(Generic[IceCream].to(iceCream)) // employee: Employee = Employee(Sundae,1,false)
今のところそんなに便利じゃない。ちなみにTupleに対してもGenericは導出できるようになっている。
Coproduct for Coproduct
さて、HList
がProductを実装した。ではCoproductを実装するのは誰?かというと、そのままshapeless.Coproduct
である。
型コンストラクタは:+:
であり、CNil
がNilに相当する。CiNiiっぽいね。たぶんCoproductのNilだからCNil。
type FooBarBuzz = Foo :+: Bar :+: Buzz :+: CNil
という具合で型を定義できる。最後のCNil
を忘れないように。これも、そんなに特別な事はしていなくて、:+:
はEither
であり、CNil
はNothing
にだいたい対応する。
さて、実際にFooBarBuzz
型の値を作るにはどうしたら良いかというと、まずは:+:
の2つのサブタイプについて知らなければならない。
:+:
--Either
にだいたい対応するInl
--Left
にだいたい対応するInr
--Right
にだいたい対応する
どうしてこんなサブタイプがあるかというと、型合わせのために必要なのである。
type FooBarBuzz = Foo :+: Bar :+: Buzz :+: CNil val x: FooBarBuzz = Bar(...) // 型が合わない!!! val x: FooBarBuzz = Inr(Inl(Bar(...))) // ヨシ!
不思議に感じるかもしれないが、ふだんEither
を扱うときにLeft()
とかRight()
と書いていることを思い出せば、それほど奇怪ではない。Either
が多段になり、いくつかある型スロットのうちどのスロットを選択するかを、明示的に指定しているのである。ふだんsealed trait
を使っているときは、型ごとに名前がついているから、これが自動的に隠蔽されているにすぎない。
型の合わせ方は、左から順にずらしたい数だけInr
を置き、一番内側にInl
を置けばよい。
val x: FooBarBuzz = Inl(Foo) // 一番左 val x: FooBarBuzz = Inr(Inl(Bar)) // 左から1つめ val x: FooBarBuzz = Inr(Inr(Inl(Buzz))) // 左から2つめ
Listのメタファで考えるとわかりやすいだろう。
Inl
やInr
を省略できないのかと思ったが、この場合順序が大事なので、構造的に同じ型が登場する場合は適当に導出ができないことはすぐわかるはず。
Int :: Int :+: Int :: Int :+: CNil
にInt :: Int
を入れたいとき、どうする?
Generic
ふたたび
HList
に対してやってくれたように、Coproductに対してもGeneric
は変換をサポートしてくれる。Generic
マクロは、sealedなcase classを認識してGeneric
のインスタンスを導出し、Coproduct
の形に変換する。
import shapeless.Generic sealed trait Shape final case class Rectangle(width: Double, height: Double) extends Shape final case class Circle(radius: Double) extends Shape val gen = Generic[Shape] // gen: shapeless.Generic[Shape]{type Repr = Rectangle :+: Circle :+: shapeless.CNil} = anon$macro$1$1@1a28fc61
Reprが:+:
でつながった形式になっているのが分かるはず。
そして、これもto
/ from
による変換をサポートしている。ただし、何番目の型なのかを標識するために、自動的にInl
やInr
がひっついてくる。
gen.to(Rectangle(3.0, 4.0)) // res3: gen.Repr = Inl(Rectangle(3.0,4.0)) gen.to(Circle(1.0)) // res4: gen.Repr = Inr(Inl(Circle(1.0)))
今のところたいして魅力的ではないけれど、本によれば再帰的構造が登場したときに力を発揮するらしい。
今日はここまで。
まとめ
- ADTsを復習した
HList
がProductに、Coproduct
がCoproductに対応するGeneric
型クラスが具体的なADTsとgenericな形式(HList + Coproduct)とを相互変換する- 同名の
Generic
マクロがcase class / sealed traitからGeneric
型クラスのインスタンスを自動導出する - 名前で識別できないかわりに、
Coproduct
はInl
/Inr
で型の特殊化を表現する
Chapter3では、型クラスの自動導出についてやっていくらしい。