Edited at
ScalaDay 7

Scalaでshapelessを使ったジェネリックプログラミング

More than 3 years have passed since last update.

shapelessというScalaでジェネリックプログラミングできるライブラリを試してみたので紹介してみる。


ジェネリックプログラミングとは


ジェネリック(総称あるいは汎用)プログラミング (generic programming)はデータ形式に依存しないコンピュータプログラミング方式である。

https://ja.wikipedia.org/wiki/ジェネリックプログラミング


データ形式に依存しないらしい。


Scrap your boilerplate

Scrap your boilerplate (通称syb) は、Haskellでジェネリックプログラミングをするためのライブラリの1つで、shapelessはこれを実装しているとのこと。

とにかく例をみてみよう。


CaseClassの中身を+1する。

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を試してみよう。これは畳み込み関数だ。


Intのみ足し合わせる

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)

関数合成もできる。


plus1してshowするPolyを合成

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


map

object toBoolean extends Poly1 {

implicit val caseString = at[String](_ == "true")
implicit val caseInt = at[Int](_ == 1)
}
xs.map(toBoolean)
// ::(true, ::(false, HNil))


foldLeft

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を使いHListCoproduct相互変換できる。

最初に紹介した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で表せる。

同じように相互変換してみる。


Animalの相互変換

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を使った例とかあった。


caseclassそのままappendする例

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しか受け取れない関数を作ってみる。


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)