はじめに
今回の記事では、Scala の generic ライブラリである shapeless について調べてみたことをアウトプットしていきます。
誤っている点がございましたらコメントいただけると嬉しいです。
shapeless とは
shapelessは、Scalaの型クラスと従属型に基づいた汎用プログラミングライブラリです。
https://github.com/milessabin/shapeless
この説明だと想像がつきづらいので、次項から主要概念に触れていきたいと思います。
scala プロジェクトに導入する際は、scala のバージョンを 2.11.x ~ 2.13.x に設定し、 build.sbt に下記を追加する必要があるようです。
scalaVersion := "2.13.5"
libraryDependencies ++= Seq(
"com.chuusai" %% "shapeless" % "2.3.3"
)
Poly1
Shapelessは多相関数を表現するためにPolyという型を提供している。Polyはパラメータの型に依存して結果の型が決まる。
https://books.underscore.io/shapeless-guide/shapeless-guide.html#polymorphic-functions
ポリモーフィズムという考え方が適用されていて、変換するという意味では scala の Tuple や Function と似ているが、扱い方は異なるそう。
型固有のケースを捉えることができるため、ジェネリックプログラミングに最適な変換が可能となるみたいです。
object size extends Poly1 {
implicit def caseInt = at[Int](x => 1)
implicit def caseString = at[String](_.length)
implicit def caseTuple[T, U]
(implicit st : Case.Aux[T, Int], su : Case.Aux[U, Int]) =
at[(T, U)](t => size(t._1)+size(t._2))
}
scala> size(23)
res4: Int = 1
scala> size("foo")
res5: Int = 3
scala> size((23, "foo"))
res6: Int = 4
scala> size(((23, "foo"), 13))
res7: Int = 5
また、Poly 型には、Poly1~Poly22 のサブタイプがあるようです。
Coproduct
shapelessはScalaのEitherを一般化したCoproduct型を持ち、任意の数の選択肢を持つことができる。現在のところ、Coproductはマッピング、選択、単一化をサポートしている。
https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#coproducts-and-discriminated-unions
scala の Either に似ているようで、こちらも概念的には別物で、unionと言われる共用体の方が意味合い的には近いようです。
https://ja.wikipedia.org/wiki/%E5%85%B1%E7%94%A8%E4%BD%93
scala> type ISB = Int :+: String :+: Boolean :+: CNil
defined type alias ISB
scala> val isb = Coproduct[ISB]("foo")
isb: ISB = foo
scala> isb.select[Int]
res0: Option[Int] = None
scala> isb.select[String]
res1: Option[String] = Some(foo)
一般的に、 A :+: B :+: C :+: CNi
の形をとり、「A または B または C」を意味します。
Generic
ShapelessはGenericという型クラスを提供し、具象 ADT とその総称表現の間を行き来することを可能にしている。裏技的なマクロを使えば、定型文なしでGenericのインスタンスを呼び出すことができる。
https://books.underscore.io/shapeless-guide/shapeless-guide.html#switching-representations-using-generic
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
インスタンスは、Repr がつきまとう。
おわりに
ここまで shapeless の Poly1, Coproduct, Generic について触れてみましたが、実装に落とし込めるほど理解できていないので、これからも学習を続けていきます。