ScalaにはHaskellのようなShowやEqをderivingしてくれる機能は無いんですか?と質問をしたところ
Typelevel.scala | Deriving Type Class Instancesを紹介してもらったのでものは試しとやってみることにしました。
本題に入る前に、このページ中のコードで使っているbuild.sbtを貼っておきます。
name := "deriving" version := "1.0" scalaVersion := "2.11.6" resolvers ++= Seq( Resolver.sonatypeRepo("releases") ) libraryDependencies ++= Seq( "com.chuusai" %% "shapeless" % "2.0.0" )
HListについて
とつとつと進めていたのですが、
唐突にHList
にbe familiar withじゃねえやつはこれを見て勉強しな!というお達しが出たので方針転換でこっちを先に進めることにしました。
Shapeless: Exploring Generic Programming in Scala - YouTube
oh, english video online gakusyuu…(´・_・`)
本題
HList
というのはどうやら、Tuple
とList
のいいとこ取りのようなデータ構造のようです。
ここで
Tuple
は長さは固定だが、異なる型を入れられるList
は長さは可変だが、同じ型しか入らない
ということを踏まえると、これのいいとこ取りを考えると
HList
は長さは可変だし、異なる型を入れられる
ということになりそうです。
だいぶあとになって追記: HListは実行時に長さを変えられるわけではなくてコンパイル時に要素をつけたり外したりできるものなので、可変というとちょっと表現が微妙で、変に混乱を生むかもしれません。概ねtupleみたいに色々な型を入れられるけどListのように再帰的な構造になっているので要素をつけたり外したりしやすいといった類のものです。
たぶんHeterogeneous List
の略だと思います。
Heterogeneous
は、異なる
とか異種の
とかそういう意味だったと思うので、
たしかにヘテロジニアスなリストと言えそうです。
ここで、おもむろにScala worksheetを開きます。
そして
import shapeless._ import HList._ val l: Int :: String :: HNil = 1 :: "foo" :: HNil
とすると
l: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: foo :: HNil
が定義できました!これはやばいですね。完全にやばい扉を開けている感じがします。
なお、中身はhead
とかtail
とかするといつものように取得することが出来ます。
appendの導出
ここからは主にTypelevel.scala | Deriving Type Class Instancesの話に戻ります。
HListの話は一旦おいておいて、型クラス
の話をすることにしましょう。
型クラスにはいろいろありますが、今回はSemigroup(半群)
という型クラスを考えることにします。
Semigroup
というのはどういうものかというとこういうものです。
trait Semigroup[S] { def append(s1: S, s2: S): S }
なんやこれ append
があるだけやないか!と思うかもしれませんが、 implicit parameter
を使うと
// Intの足し算 implicit val intInstance = new Semigroup[Int] { def append(s1: Int, s2: Int) = s1 + s2 } // Stringの足し算 implicit val stringInstance = new Semigroup[String] { def append(s1: String, s2: String) = s1 + s2 } // なんでも足し算 def plus[A](a: A, b: A)(implicit semigroup: Semigroup[A]) = { semigroup.append(a, b) } plus(1, 2) // 3 plus("aaa", "bbb") // "aaabbb"
のようにplus
を呼び出した際に勝手に引数に渡されて、append
が統一的に使えるよ。といった効果があります。
うーん。わかるけどわからない。そう思いますか?僕もそう思います(´・_・`)
これならどうでしょうか。
implicit def tupleInstance[A, B](implicit A: Semigroup[A], B: Semigroup[B]) = new Semigroup[(A, B)] { def append(t1: (A, B), t2: (A, B)): (A, B) = (A.append(t1._1, t2._1), B.append(t1._2, t2._2)) } plus((1, "aaa"), (2, "bbb")) // (3, "aaabbb")
なんとSemigroupインスタンスを組み合わせてタプルのSemigroupインスタンスができちゃうんです。
どういうふうに動いているかというと、
plus
にtuple[Int, String]
が渡されているのでそれに合うimplicit parameter
をコンパイラが探し始めるtuple._1
の型はInt
なのでSemigroup[Int]
であるintInstance
がA
に渡されるtuple._2
の型はString
なのでSemigroup[String]
であるstringInstance
がB
に渡される
というような感じです。
ここまでできるとなると、たしかに色々出来そうな気がしてきましたね。
Semigroup
は仮定していることが少ないので、このくらいかもしれませんが(Monoid
とかだともっと色々できる)、
それでも十分に色々なことができそうです。
3次元ベクトルの足し算
ここからいよいよ本題です。
case class Vector3D(x: Int, y: Int, z: Int) {}
のようなclass
があったとします。
これに足し算を追加したいときはどうするかというと
case class Vector3D(x: Int, y: Int, z: Int) { def +(that: Vector3D): Vector3D = Vector3D(this.x + that.x, this.y + that.y, this.z + that.z) } Vector3D(1,2,3) + Vector3D(4,5,6) // Vector3D(5,7,9)
とか、
// 試し終わったら消してください implicit val vector3DInstance = new Semigroup[Vector3D] { def append(v1: Vector3D , v2: Vector3D) = Vector3D(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z) } plus(Vector3D(1,2,3), Vector3D(4,5,6)) // Vector3D(5,7,9)
とかしますよね。
しかし Vector2D
とか Vector5D
とか出てきた時にこれをいちいち書くのはしんどそうです(´・_・`)
なんとか統一的にひょろろーと表現すると、てれれーと足し算ができるようにならないでしょうか。
ということで、先ほどのHList
が登場します。
3次元ベクトルの足し算 with HList
どういうことかというと、
まず、HList
上での足し算を定義してあげます。
implicit val nilInstance = new Semigroup[HNil] { def append(x: HNil, y: HNil) = HNil } implicit def consInstance[H, T <: HList](implicit H: Semigroup[H], T: Semigroup[T]) = new Semigroup[H :: T] { def append(x: H :: T, y: H :: T) = H.append(x.head, y.head) :: T.append(x.tail, y.tail) }
このようにすると
val a = 1 :: 3 :: HNil val b = 2 :: 4 :: HNil println(plus(a, b)) // 3 :: 7 :: HNil
のような演算が可能になります。
そして、
def to(vec: Vector3D): Int :: Int :: Int :: HNil = vec.x :: vec.y :: vec.z :: HNil def from(hlist: Int :: Int :: Int :: HNil): Vector3D = Vector3D(hlist.head, hlist.tail.head, hlist.tail.tail.head) val c = to(Vector3D(1,2,3)) // 1 :: 2 :: 3 :: HNil val d = from(c) // Vector3D(1, 2, 3)
のようなHListへの変換する関数と、HListから元に戻す関数を用意してあげます。
なんとなく見えてきましたね。
あとは、Vector3D
をto
でHList
に変換して、append
で足し算した後に、from
で元に戻してくれるような関数があればよさそうです。
def subst[A, B](to: A => B, from: B => A)(implicit instance: Semigroup[B]) = new Semigroup[A] { def append(a1: A, a2: A) = from(instance.append(to(a1), to(a2))) } implicit val vectorInstance: Semigroup[Vector3D] = subst(to, from) val e = plus(Vector3D(1, 2, 3), Vector3D(1, 2, 3)) // Vector3D(2, 4, 6)
できました!!
まとめ
ということで、駆け足でしたが、from
とto
でHList
の世界を行ったり来たりすることで、
いろいろな型クラスのインスタンスを具体的に定義することを避ける事ができそうです。
ぶっちゃけfrom
とかto
とかを書きたくないんですが、マクロを使うとSemigroup.derive[Vector3D]
のように一気に書くことが出来るらしいです。
今回はここまではやりません。(というかまだ出来てません(´;ω;`))
最後に全部のコードを載せておきます。
import shapeless._ import HList._ object Test { case class Vector3D(x: Int, y: Int, z: Int) {} // Semigroupをimplicit valで渡す trait Semigroup[S] { def append(s1: S, s2: S): S } // appendする関数 def plus[A](a: A, b: A)(implicit semigroup: Semigroup[A]) = { semigroup.append(a, b) } // HListとの変換 def to(vec: Vector3D): Int :: Int :: Int :: HNil = vec.x :: vec.y :: vec.z :: HNil def from(hlist: Int :: Int :: Int :: HNil): Vector3D = Vector3D(hlist.head, hlist.tail.head, hlist.tail.tail.head) // IntのSemigroup implicit val intInstance = new Semigroup[Int] { def append(x: Int, y: Int) = x + y } // HListのSemigroup implicit val nilInstance = new Semigroup[HNil] { def append(x: HNil, y: HNil) = HNil } implicit def consInstance[H, T <: HList](implicit H: Semigroup[H], T: Semigroup[T]) = new Semigroup[H :: T] { def append(x: H :: T, y: H :: T) = H.append(x.head, y.head) :: T.append(x.tail, y.tail) } def subst[A, B](to: A => B, from: B => A)(implicit instance: Semigroup[B]) = new Semigroup[A] { def append(a1: A, a2: A) = from(instance.append(to(a1), to(a2))) } implicit val vectorInstance: Semigroup[Vector3D] = subst(to, from) def main(args: Array[String]) { val a = 1 :: 3 :: HNil val b = 2 :: 4 :: HNil println(plus(a, b)) println(plus(Vector3D(1, 2, 3), Vector3D(1, 2, 3))) } }
今後
Scalaは始めたばかりですが、このようなおもしろいライブラリがいっぱいあるのですね! HListの実装周りは初心者にはちょっときつそうなので、もう少し時間を書けて勉強したいなーと思っています。
あとマクロとかをやらないとfrom
とかto
を書かなければならなくてちゃんと問題が解決しないので、
そのへんもやりたいんですが、この資料パート1で終わってて先が・・・(´・_・`)