Slide 1

Slide 1 text

dali introduction MakeNowJust @rpscala#252 1

Slide 2

Slide 2 text

⾃⼰紹介 HN: さっき作った ⼤学⽣ プログラミング⾔語・正規表現が好き 最近はGoを書いていた (M社でインターンをしていた) 2

Slide 3

Slide 3 text

アジェンダ 1. Generic Programming⼊⾨ 2. Generic Programming Libiary 3

Slide 4

Slide 4 text

1. Generic Programming⼊⾨ 4

Slide 5

Slide 5 text

Generic Programmingとは? 具体的なデータ型に直接依存しない、抽象的かつ汎⽤的なコード記述を可能にするコン ピュータプログラミング⼿法である。 (from ジェネリックプログラミング - Wikipedia) よく分からない。 5

Slide 6

Slide 6 text

例: Expr 数式を表す trait とその実装たち。 (現在は⾜し算しかない) sealed trait Expr[A] case class Value[A](a: A) extends Expr[A] case class Add[A](l: Expr[A], r: Expr[A]) extends Expr[A] 6

Slide 7

Slide 7 text

例: Expr の⽐較 (⼀般Lv. 0: 愚直) 値の部分を String から Int にする。 def toInt(e: Expr[String]): Expr[Int] = e match { case Value(a) => Value(a.toInt) case Add(l, r) => Add(toInt(l), toInt(r)) } toInt(Add(Value("10"), Value("20"))) // => Add(Value(10), Value(20)) 7

Slide 8

Slide 8 text

例: Expr の変換 (⼀般Lv. 0: 愚直) 値の部分を Int から String にする。 def toString(e: Expr[Int]): Expr[String] = e match { case Value(a) => Value(a.toString) case Add(l, r) => Add(toString(l), toString(r)) } toString(Add(Value(10), Value(20))) // => Add(Value("10"), Value("20")) ほとんど同じ。 8

Slide 9

Slide 9 text

ほとんど同じ → ⼀般化できる 9

Slide 10

Slide 10 text

例: Expr の変換 (⼀般Lv. 1: map ) map 関数を作る。 def map[A, B](e: Expr[A])(f: A => B): Expr[B] = e match { case Value(a) => Value(f(a)) case Add(l, r) => Add(map(l)(f), map(r)(f)) } def toInt(e: Expr[String]): Expr[Int] = map(e, _.toInt) def toString(e: Expr[Int]): Expr[String] = map(e, _.toString) 10

Slide 11

Slide 11 text

例: Expr の変換 (⼀般Lv. 1: map ) いい感じ。 toInt(Add(Value("10"), Value("20"))) // => Add(Value(10), Value(20)) toString(Add(Value(10), Value(20))) // => Add(Value("10"), Value("20")) 11

Slide 12

Slide 12 text

例: Expr の変換 (⼀般Lv. 1: map ) 引き算を表すクラス Sub が増えた。 case class Sub[A](l: Expr[A], r: Expr[A]) extends Expr[A] どうする? 12

Slide 13

Slide 13 text

例: Expr の変換 (⼀般Lv. 1: map ) map を修正する。 def map[A, B](e: Expr[A])(f: A => B): Expr[B] = e match { case Value(a) => Value(f(a)) case Add(l, r) => Add(map(l)(f), map(r)(f)) // 追加: case Sub(l, r) => Sub(map(l)(f), map(r)(f)) } 13

Slide 14

Slide 14 text

例: Expr の変換 (拡張) 掛け算を表す Mul と割り算を表す Div も増えた。 case class Mul[A](l: Expr[A], r: Expr[A]) extends Expr[A] case class Div[A](l: Expr[A], r: Expr[A]) extends Expr[A] また map を修正すれば良さそうだけど‥‥ 。 14

Slide 15

Slide 15 text

例: Expr のその他の操作 (少し話題を変えて) Expr の他の操作も考えられる。 等価性の⽐較 ⽂字列化 数式として評価 Add(Value(10), Value(20)) -> 30 これらは map では⼀般化できなさそうなので、個別に実装する必要がありそう。 Mul とか Div が増えたら、これも修正するの‥‥ ? 15

Slide 16

Slide 16 text

つらい! 16

Slide 17

Slide 17 text

例: Expr の変換 (⼀般Lv. 1: map ) ところで、 map の Add と Sub を処理する部分: case Add(l, r) => Add(map(l)(f), map(r)(f)) case Sub(l, r) => Sub(map(l)(f), map(r)(f)) ほとんど同じ。 17

Slide 18

Slide 18 text

ほとんど同じ → ⼀般化できる(?) 18

Slide 19

Slide 19 text

発想の⾶躍 19

Slide 20

Slide 20 text

map →処理を関数によって⼀般化 Mul や Div への対応→データ構造の⼀般化 20

Slide 21

Slide 21 text

よく分からないイメージ1: 21

Slide 22

Slide 22 text

よく分からないイメージ2: 22

Slide 23

Slide 23 text

つまり何が⾔いたいのか どんな構造でも表現できるすごいデータ構造があったとする。 元のデータ構造( Expr )からその構造 への変換と、 その構造 に対する操作( map , equal , show など)があれば、 型を拡張した場合に、その構造 への変換を拡張するだけで、 他の操作も拡張できる。 そんな都合のいいデータ構造があるのか? 23

Slide 24

Slide 24 text

HList と Coproduct HList : いくつでも要素を格納できる Tuple Coproduct : いくつでも型を選択できる Either この2つがあれば⼤体の型は表現できる。 24

Slide 25

Slide 25 text

HList いくつでも要素を格納できる Tuple あるいは、 head と tail で異なる型を格納する List sealed trait HList { def :*:[H](head: H): H :*: this.type = new :*:(head, this) } sealed class HNil extends HList object HNil extends HNil case class :*:[H, T <: HList](head: H, tail: T) extends HList 25

Slide 26

Slide 26 text

HList 例 例: 型 Int :*: String :*: Boolean :*: HNil 1要素⽬が Int 、2要素⽬が String 、3要素⽬が Boolean の HList 例: 値 1 :*: "foo" :*: true :*: HNil どうやって値を取り出すの? →パターンマッチとかプロパティとか、普通の⽅法で取り出せる。 def x(h: Int :*: String :*: Boolean :*: HNil): String = h match { case _ :*: s :*: _ :*: HNil => s } x(1 :*: "foo" :*: true :*: HNil) // => "foo" 26

Slide 27

Slide 27 text

Coproduct sealed trait Coproduct sealed trait CNil extends Coproduct sealed trait :+:[H, T <: Coproduct] extends Coproduct case class Inl[H, T <: Coproduct](head: H) extends :+:[H, T] case class Inr[H, T <: Coproduct](tail: T) extends :+:[H, T] 27

Slide 28

Slide 28 text

Coproduct 例 例: 型 Int :+: String :+: Boolean :+: CNil Int 、 String 、 Boolean のうち、どれかの値を取る型 例: 値 Inl[Int, String :+: Boolean :+: CNil](1) 上の型で、 Int を持つ Coproduct の値 HList と同じく、パターンマッチなどで値を取り出す。 28

Slide 29

Slide 29 text

HList , Coproduct まとめ HList はいくつかの型・値を並べたもの(タプルっぽくてリストっぽい) Coproduct はいくつかの型・値のうち、どれか1つを取るもの( Either っぽい) どちらも定義⾃体は数⾏で書ける(普通の) sealed trait , case class 。 29

Slide 30

Slide 30 text

データ型と HList , Coproduct との変換 型クラスにする。 trait Converter[X, Y] { def from(x: X): Y def to(y: Y): X } 30

Slide 31

Slide 31 text

Expr[A] Expr[A] に対応する型は Value[A] :+: Add[A] :+: Sub[A] :+: CNil 。 ( Mul と Div はまだ追加してないことにする) implicit def exprConverter[A] = new Converter[Expr[A], Value[A] :+: Add[A] :+: Sub[A] :+: CNil] { def from[A](e: Expr[A]): Value[A] :+: Add[A] :+: Sub[A] :+: CNil = e match { case v: Value[A] => Inl(v) case a: Add[A] => Inr(Inl(a)) case s: Sub[A] => Inr(Inr(Inl(s))) } def to[A](e: Value[A] :+: Add[A] :+: Sub[A] :+: CNil): Expr[A] = ??? // 略 } 31

Slide 32

Slide 32 text

Value[A] Value[A] に対応する型は A :*: HNil 。 ( case class Value[A](a: A) なので) implicit def valueConverter[A] = new Converter[Value[A], A :*: HNil] { def from[A](v: Value[A]): A :*: HNil = v.a :*: HNil def to[A](v: A :*: HNil): Value[A] = Value(v.head) } 32

Slide 33

Slide 33 text

Add[A] Add[A] に対応する型は Expr[A] :*: Expr[A] :*: HNil 。 ( case class Add[A](l: Expr[A], r: Expr[A]) なので) implicit def addConverter[A] = new Converter[Add[A], Expr[A] :*: Expr[A] :*: HNil] { def from[A](a: Add[A]): Expr[A] :*: Expr[A] :*: HNil = a.l :*: a.r :*: HNil def to[A](a: Expr[A] :*: Expr[A] :*: HNil): Add[A] = Value(a.head, a.tail.head) } Sub[A] も同じく。 33

Slide 34

Slide 34 text

HList , Coproduct に対する処理の実装 今回は equal を実装する。 やはり型クラスにする。 trait Equal[A] { def equal(x: A, y: A): Boolean } 34

Slide 35

Slide 35 text

HNil に対する実装 HNil のインスタンスは1つしかないので、 HNil 同⼠の⽐較は必ず true になる。 implicit val hnilEqual = new Equal[HNil] { def equal(x: HNil, y: HNil) = true } 35

Slide 36

Slide 36 text

:*: に対する実装 head と tail 両⽅が等しい場合に等しい。 implicit def hconsEqual[H, T <: HList]( implicit heq: Equal[H], teq: Equal[T]) = new Equal[H :*: T] { def equal(x: H :*: T, y: H :*: T): Boolean = heq.equal(x.head, y.head) && teq.equal(x.tail, y.tail) } 36

Slide 37

Slide 37 text

CNil に対する実装 CNil のインスタンスは存在しないはずなので、例外を投げておけばいい。 implicit val cnilEqual = new Equal[CNil] { def equal(x: CNil, y: CNil) = ??? } 37

Slide 38

Slide 38 text

:+: に対する実装 持ってる値の位置が同じで、等しければ等しい。 implicit def cconsEqual[H, T <: Coproduct]( implicit heq: Equal[H], teq: Equal[T]) = new Equal[H :+: T] { def equal(x: H :+: T, y: H :+: T): Boolean = (x, y) match { case (Inl(xh), Inl(yh)) => heq.equal(xh, yh) case (Inr(xt), Inr(yt)) => teq.equal(xt, yt) case _ => false } } 38

Slide 39

Slide 39 text

基本的な型に対する Equal の実装 implicit val intEqual: Equal[Int] = (x, y) => x == y implicit val stringEqual: Equal[String] = (x, y) => x == y 39

Slide 40

Slide 40 text

Converter を使った Equal の実装 implicit def converterEqual[A, B]( implicit AB: Converter[A, B], beq: Equal[B]) = new Equal[A] { def equal(x: A, y: A) = beq.equal(AB.from(x), AB.from(y)) } 40

Slide 41

Slide 41 text

こうすることで、 Expr に対する Equal の実装を⼀切書いていないのにも関わらず、 implicitly[Equal[Expr[Int]]] が取得できる。 →実装を導出する。 implicitly[Equal[Expr[Int]]].equal(Value(10), Value(20)) すごい。 41

Slide 42

Slide 42 text

Mul , Div を追加した場合でも、 Mul , Div に対応する Converter を実装するだけで Equal のインスタンスが導出できる。 また、全く関係ない型でも Converter を⽤意できれば Equal のインスタンスが得られるよ うになる。 sealed class MyOption[A] case class MyNone[A]() extends MyOption[A] case class MySome[A](a: A) extends MyOption[A] // ...MyOption へのConverter の実装... implicitly[Equal[MyOption[Int]]].equal(MySome(1), MySome(2)) とてもすごい。 42

Slide 43

Slide 43 text

ここまでのまとめ HList , Coproduct との変換と、 HList , Coproduct での処理を ⽤意すると、元の型でもその処理が使えるようになる。 変換と処理は疎なので、⼀度 HList , Coproduct で定義した処理は、 変換のある様々な型で使うことができる。 43

Slide 44

Slide 44 text

しかし HList , Coproduct への変換を⼀々書くのが⾯倒。 →それを⾃動で(マクロで)やってくれるのが shapeless や dali などの、 Generic Programming Library。 44

Slide 45

Slide 45 text

2. Generic Programming Libiary 45

Slide 46

Slide 46 text

Generic Programming Libiary shapeless, dali (拙作) HList , Coproduct などのデータ構造 それらの型とデータ構造の変換を⾃動的に作るマクロ 46

Slide 47

Slide 47 text

マクロ Generic.Aux[A, B] が Converter[A, B] に相当。 Generic[A] で⾃動的に変換が定義される。 例: Generic[Expr[Int]] (これはshapelessもdaliも同じ) 47

Slide 48

Slide 48 text

ここからが本題 48

Slide 49

Slide 49 text

⾼階型の処理を導出したい 最初の map の例。 F[_] のような種の型の処理を導出したい。 shapelessには Generic1 というものがある。 → Generic の⾼階版っぽい。 しかし‥‥ 。 49

Slide 50

Slide 50 text

shapeless.Generic1 Generic1.Aux[F[_], FR[_[_]], R[_]] 型引数が3つある→単純な変換ではない? ドキュメントが全くない。 ( FR には導出したい処理の型クラスを⼊れるといいらしい) どうしてこうなったのかは不明。 (型推論などとの兼ね合い?) 50

Slide 51

Slide 51 text

dali.higher.Generic1 Generic1.Aux[F[_], R <: TypeFunction1] F[_] と R (に具体的な型を指定したもの)との変換を提供。 TypeFunction1 の具体的な型には HList1 や Coproduct1 などがある。 (個⼈的には)分かりやすいと思う。 51

Slide 52

Slide 52 text

そもそもdaliとは? MakeNowJust/dali Scala 2.13の機能を前提に作ったGeneric Programming Library 最低限の機能のみを提供(minimalism) catsの⽅クラスの⾃動導出も別ライブラリとして提供 Functor などだけではなく、 Alternative や Monad などの導出にも対応 まだMaven Central Repositoryにはリリースしていない‥‥ (今週中には) Scala 2.13が使えるなら結構良いライブラリだと思う(⾃画⾃賛) 52

Slide 53

Slide 53 text

dali開発の経緯 Scalaの勉強のため既存のライブラリの再発明をしまくっている MakeNowJust-Labo/scala-labo その中でshapelessの再発明をしていると、shapelessがScala 2.13の機能を使いこなし ていないことに気付く (後⽅互換性とか⾊々あるのだと思う) Scala 2.13の機能(by-name implicit, literal types)を使うともっと良いライブラリが作れ ると思って、切り出して開発を始める 53

Slide 54

Slide 54 text

まとめ Generic Programming Libiaryを使うと、処理を型クラスで記述するだけで、⾊々な型で その処理が使えるようにできる HList や Coproduct は普通の型で、マクロを使った魔術的なのはそれらの変換を定義 する Generic のみ shapelessもいいけどdaliも使えよ 54