shapelessというScalaでジェネリックプログラミングできるライブラリを試してみたので紹介してみる。
ジェネリックプログラミングとは
ジェネリック(総称あるいは汎用)プログラミング (generic programming)はデータ形式に依存しないコンピュータプログラミング方式である。
https://ja.wikipedia.org/wiki/ジェネリックプログラミング
データ形式に依存しないらしい。
Scrap your boilerplate
Scrap your boilerplate (通称syb) は、Haskellでジェネリックプログラミングをするためのライブラリの1つで、shapelessはこれを実装しているとのこと。
とにかく例をみてみよう。
import shapeless._
object plus1 extends Poly1 {
implicit val caseString = at[String](_ + "1")
implicit val caseInt = at[Int](_ + 1)
}
val plus1All = everywhere(plus1)
case class User(id: Int, name: String)
plus1All(User(1, "bob"))
// User(2,bob1)
plus1All(Some(1))
// Some(2)
plus1All((List(1, 2), Right("a"), Try(1)))
// (List(2, 3), Right("a1"), Success(2))
case class Manager(id: Option[Int], name: Either[String, User])
plus1All(Manager(Some(1), Right(User(1, "bob"))))
// Manager(Some(2),Right(User(2,bob1)))
ひたすらに中身を舐め回せる( 'ㅂ')
もう一つ、everythingを試してみよう。これは畳み込み関数だ。
object plus extends Poly2 {
implicit val intCase = at[Int, Int](_ + _)
}
object size extends Poly1 {
implicit val intCase = at[Int](x => x) // Intならその値を使い、
implicit def default[T] = at[T](_ => 0) // それ以外は0。
}
def sum[T](t: T)(implicit everything: Everything[size.type, plus.type, T]) = everything(t)
sum((1, 2))
// 3
sum((1, (2, "3", Some(4))))
// 7
case class Foo(a: Int, b: String)
sum(Foo(1, "2") :: Foo(10, "20") :: Nil)
// 11
なんていうか...すごい!
まずplus1の定義で使ってるPolyから見ていこう。
Poly
Poly1はFunction1のPolymorphic版のようなものだ。
複数型を処理できる関数になる。
object plus1 extends Poly1 {
implicit val caseString = at[String](_ + "1")
implicit val caseInt = at[Int](x => Some(x + 1))
}
plus1("a")
// a1
plus1(1)
// Some(2)
関数合成もできる。
object show extends Poly1 {
def f(x: String) = s"result is $x"
implicit val caseInt = at[Int](x => f(x.toString))
implicit val caseString = at[String](f)
}
(show compose plus1)(1)
// "result is 2"
(plus1 andThen show)("1")
// "result is 11"
Polyはとてもおもしろい機能だ。ちなみにPolyは22まである。
ではHListとCoproductなどを見ていこう。これのおかげでcase class等の中身を処理できる。
HList
HListは、ListとTupleを合わせたようなもの。
そして直積だ。(直積は、AとBとCと...のようなAND。例えばcase classやTuple)
import shapeless._
val xs = 1 :: "a" :: HNil
// xs: Int :: String :: HNil = ::(1, ::("a", HNil))
普通のリストと同じようなメソッドを持っている。
val ys = 1d +: xs
// ys: Double :: Int :: String :: HNil = ::(1.0, ::(1, ::("a", HNil)))
val zs = xs ++ xs
// zs: Int :: String :: Int :: String :: HNil = ::(1, ::("a", ::(1, ::("a", HNil))))
xs.head
// Int = 1
xs.tail
// String :: HNil = ::("a", HNil)
xs.take(2)
// Int :: String :: HNil = ::(1, ::("a", HNil))
xs(0)
// Int = 1
object toBoolean extends Poly1 {
implicit val caseString = at[String](_ == "true")
implicit val caseInt = at[Int](_ == 1)
}
xs.map(toBoolean)
// ::(true, ::(false, HNil))
object sum extends Poly2 {
implicit val caseInt = at[Int, Int](_ + _)
implicit val caseString = at[Int, String](_ + _.toInt)
}
(1 :: "2" :: 3 :: HNil).foldLeft(0)(sum)
// 6
他にもflatMapやらfilter等いつもの関数は用意されている。
Coproduct
Eitherのようなもの。
そして直和だ。(直和は、AかBかCか...のようなOR。例えばEither)
本家のWikiの例がそのままで分かりやすかったので貼る...。
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)
object size extends Poly1 {
implicit def caseInt = at[Int](i => (i, i))
implicit def caseString = at[String](s => (s, s.length))
implicit def caseBoolean = at[Boolean](b => (b, 1))
}
scala> isb map size
res2: (Int, Int) :+: (String, Int) :+: (Boolean, Int) :+: CNil = (foo,3)
scala> res2.select[(String, Int)]
res3: Option[(String, Int)] = Some((foo,3))
Generic
具体的な型を、Genericを使いHList
やCoproduct
に相互変換できる。
最初に紹介したcase class等の中身を処理できたのは、これのおかげだ。
例えば以下のような代数的データ型を定義する。
sealed trait Animal
case class Dog(name: String, age: Int) extends Animal
case class Cat(name: String, state: Int) extends Animal
GenericでDogを、HListに変換してまた戻す。
val hachi = Dog("hachi", 2)
val gHachi = Generic[Dog].to(hachi) // hachi :: 2 :: HNil
hachi == Generic[Dog].from(gHachi)
String :: Int :: HNil
という型からDogに戻せたので、型さえあってれば別の具体的な型にできる。
Generic[Cat].from(gHachi) // Cat(hachi,2)
そしてAnimalのGenericは、DogかCatなのでCoproductで表せる。
同じように相互変換してみる。
val animal = Generic[Animal].to(hachi)
// Generic.Aux[Animal, Dog :+: Cat :+: CNil] = Dog(hachi,2)
animal.select[Dog]
// Option[Dog] = Some(Dog(hachi,2))
animal.select[Cat]
// Option[Cat] = None
Generic[Animal].from(animal)
// Animal = Dog(hachi,2)
相互変換を利用してmapしてみる。
object plus1 extends Poly1 {
implicit val caseString = at[String](_ + "1")
implicit val caseInt = at[Int](_ + 1)
}
def f[A, L <: HList](a: A)(implicit gen: Generic.Aux[A, L], mapper: Mapper.Aux[plus1.type, L, L]) =
gen.from(gen.to(a) map plus1)
f(Dog("hachi", 1))
// Dog("hachi1", 2)
f((1, "a"))
// (2, "a1")
f(Some(1))
// Some(2)
うーむすごい...( 'ㅂ')
Monoidを使った例とかあった。
Foo(13, "foo") |+| Foo(23, "bar")
// Foo(36, "foobar")
scalazのMonoidを使えるものも用意されているようだ。
他にも機能を紹介していく。
Record
HListにキーがついたMapみたいなもの。
val mx = "a" ->> 1 :: "b" ->> 2 :: HNil
mx("a")
// 1
mx("b")
// 2
mx("c")
//<console>:39: error: No field String("c") in record shapeless.::[Int with shapeless.labelled.KeyTag[String("a"),Int],shapeless.::[Int with shapeless.labelled.KeyTag[String("b"),Int],shapeless.HNil]]
// mx("c")
mx.keys
// a :: b :: HNil
mx.values
// 1 :: 2 :: HNil
mx + ("c" ->> 3)
// 1 :: 2 :: 3 :: HNil
mx.updated("a", 9)
// 9 :: 2 :: HNil
これ使えばcase classもキー付きで扱える。
例にある通り、存在しないキーで取得するとコンパイルエラーになる。
これは次の仕組みが使われている。
Singleton-typed literals
シングルトンなリテラルの型を扱えるものだ。
例えば1だったらInt(1)という型で、Scalaでは普通ならInt(1)という型は書けない。
shapelessでは、マクロを使ってInt(1)型を扱えるようにしている。
試しにshapelessで1しか受け取れない関数を作ってみる。
import syntax.singleton._
val one = 1.witness
def f(x: one.T) = x
f(1)
// 1
f(2)
/*
<console>:40: error: type mismatch;
found : Int(2)
required: one.T
(which expands to) Int(1)
f(2)
^
*/
単純な例であれば、簡単なマクロを書くだけでInt(1)は取得できる。
import scala.language.experimental.macros
import scala.reflect.macros.Context
trait Gen { type T }
def witness[T](t: T): Gen = macro witness_impl[T]
def witness_impl[T](c: Context)(t: c.Expr[T]) = {
import c.universe._
q"new Gen { type T = ${t.actualType} }"
}
実行すると
val one = witness(1)
// one: AnyRef with Gen{type T = Int(1)} = cmd5$$anon$1@6a1c6870
def f(x: one.T) = x
f(1)
// 1
f(2)
/*
Compilation Failed
Main.scala:45: type mismatch;
found : Int(2)
required: cmd6.One
(which expands to) Int(1)
f(2)
^
*/
このようにマクロを経由することで普通は扱えない型を使えるようにしている...( 'ㅂ')
おわり
元はSprayでHListが使われていたので調べてみたんだが、なんとも面白いライブラリだ...。
他にも紹介してない機能は色々あるので、興味あれば本家のwikiやサンプルコードを見るといいと思う。
shapelessでデータを舐め回しましょう。(^ω^)ペロペロ
(今回使用したshapelessのバージョンは2.2.5)