この記事は、ドワンゴ Advent Calendar 2019の12日目の記事です。
普段はscalaでバックエンドサーバーを書いているエンジニアです
今回は便利だけどあまり使い方が知られていなさそうなshapelessについて簡単な例を交えて紹介したいと思いま
shapeless とはなんぞや
公式では「generic programming for Scala」と書かれていますが、要は型安全なメタプログラミングをするためのライブラリです[1]
shapelessは汎用的な処理を書くときに便利な道具なので他のライブラリがshaplessに依存していたりします
有名なものだとJSON libraryのcirceやconfファイルの読み込むときに便利なpureconfig等があります
まずはimplicit parameterのおさらい
shapelessを使うにはimplicitをうまく使ったライブラリなのでこの機能をおさらいします。
implicit parameterの機能は一言でいえば、指定した型をコンパイル時に探してコードを自動的に導出してくれる機能です[2]
例えばこんなFooという型が合った時に
case class Foo[A]()
object Foo {
implicit def string: Foo[String] = new Foo[String]()
implicit def int: Foo[Int] = new Foo[Int]()
implicit def bool(implicit foo: Foo[Int]): Foo[Boolean] = new Foo[Boolean]()
}
こんな感じにコードをコンパイル時に埋めてくれます
def findFoo[A]()(implicit foo: F[A]): Unit = ()
findFoo[String]()
findFoo[Int]()
findFoo[Boolean]()
//上の行をコンパイルすると下のような感じのコードになる
findFoo[String]()(Foo.string)
findFoo[Int]()(Foo.int)
findFoo[Boolean]()(Foo.bool(Foo.int))
shapelessを使ってみよう
今回は2つのインスタンスを比較して差分表現を出力するDiffクラスを例に考えてみます
trait Diff[A] {
def show(a: A, b: A): Seq[String]
}
object Diff {
def show[A](a: A, b: A)(implicit diff: Diff[A]): Seq[String] = diff.show(a, b)
}
shapelessを使うと実際の型を気にせずDiffインスタンスを生成させることができます
まずはこのDiff[Bar]
をshapelessのちからを使って生成できるようにしてみましょう
case class Bar(a: Int, b: Boolean, s: Symbol)
shapelessは任意のcase classやsealed traitで記述された型をHList
やCoproduct
という型に変換することができます
HListは異なる型を入れられるリストで
//一部抜粋
sealed trait HList
case class ::[+H, +T <: HList](head : H, tail : T) extends HList
sealed trait HNil extends HList
case object HNil extends HNil
中の構造はタプルに近く
val hlist: String :: Int :: Boolean :: HNil = "hoge" :: 123 :: true :: HNil
val tuple = ("hoge", (123, (true, ()))) //これに構造は近い
Coproductは任意の数の型をから選択できるEitherのような存在で
//一部抜粋
sealed trait Coproduct
sealed trait CNil extends Coproduct
case class Inl[+H, +T <: Coproduct](head : H) extends :+:[H, T]
case class Inr[+H, +T <: Coproduct](tail : T) extends :+:[H, T]
中の構造もEitherに近いです
val cop: String :+: Int :+: Boolean :+: CNil = Inr(Inl(123))
val either: Either[String, Either[Int, Either[Boolean, Nothing]]] = Left(Right(Right(true)) //これに構造は近い
さてshapelessのGenericインスタンスを使うcase classをHListと相互変換できるようになります
これを使ってBarをInt :: Boolean :: Symbol :: HNil
に変換するとこうなります
import shapeless._
def bar2hlist[L <: HList](bar: Bar)(implicit gen: Generic.Aux[Bar, L]): L = gen.to(bar)
bar2hlist(Bar(123, true, 'bar)) // 123 :: true :: 'bar :: HNil
Genericを使って要素毎のDiffからDiffを導出させます
trait ProductGenericInstance {
// Diff[Repr <: HList]からDiff[X]を作成
implicit def deriveProduct[X, Repr <: HList](
implicit
gen: Generic.Aux[X, Repr],
dr: Diff[Repr]
): Diff[X] = (a, b) => dr.diff(gen.to(a), gen.to(b))
// Diff[H] と Diff[T <: Hlist]から Diff[H :: T] を作る
implicit def product[H, T <: HList](implicit head: Diff[H], tail: Diff[T]): Diff[H :: T] = {
//Listのhead tailと似た感じでハンドリングする
case (ah :: at, bh :: bt) => head.diff(ah, bh) ++ tail.diff(at, bt)
}
implicit val hnil: Diff[HNil] = (_, _) => Nil
}
これだけではDiff[H]
にはまるDiff[Int]
, Diff[Boolean]
, Diff[Symbol]
がわからないのでこれらはequalsをつかってDiffをつくります
trait AnyInstance {
implicit def other[A](implicit ev1: A <:!< Coproduct, ev2: A <:!< HList): Diff[A] = {
case (a, b) if a != b =>
s"$a => $b" :: Nil
case _ => Nil
}
}
これらを組み合わせるとBar型の自体の知識なしにDiff.show
を使えるようになります
case class Bar(a: Int, b: Boolean, s: Symbol)
object Diff extends ProductGenericInstance { ... }
trait ProductGenericInstance extends AnyInstance { ... }
trait AnyInstance { ... }
Diff.show(Bar(1, true, 'a), Bar(1, true, 'a)) // Seq.empty
Diff.show(Bar(1, true, 'a), Bar(1, false, 'b)) // Seq("true => false", "'a" => "'b")
shaplessは便利でしょ?
最後に
今回はshapelessを使って各クラスの実装の詳細に立ち入らず、インスタンス間差分を計算するコードを記述できた
以下のことに触れなかったが、これらを実現することは比較的容易にできます
- Coproductを利用してサブタイプに対応したDiffを作る方法
- 差分が出たところのフィールド名や構造がネストしているときにフィールドのパスを差分の表現に含める方法
- 再帰的な型のDiffを作る方法 (eg.
case class Hoge(a: Int, b: Option[Hoge])
) - 独自ルールをDiffに差し込む方法(SeqやMapなどはequalsじゃない方法で比較したいですよね?)
shapelessに関する日本語のドキュメントはあまり多くはありませんが、良質な英語のドキュメントや動画が数多く存在します
またソースコード自体についているコメントもとてもわかりやすいのでぜひ使ってみましょう
参考文献
Generic周りのガイド https://books.underscore.io/shapeless-guide/shapeless-guide.html
機能概要(公式wiki) https://github.com/milessabin/shapeless/wiki/Feature-overview%3A-shapeless-2.0.0
[1] https://github.com/milessabin/shapeless
[2] http://dwango.github.io/scala_text_previews/trait-tut/implicit.html