Help us understand the problem. What is going on with this article?

Shapelessはいいぞ

この記事は、ドワンゴ 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で記述された型をHListCoproductという型に変換することができます

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした