dali introduction

dali introduction

slide for my talk in rpscala#252 https://rpscala.connpass.com/event/147143/

Db1b7e3c6fe6f6a0e9752b2c19bf5473?s=128

TSUYUSATO Kitsune

September 17, 2019
Tweet

Transcript

  1. dali introduction MakeNowJust @rpscala#252 1

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

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

  4. 1. Generic Programming⼊⾨ 4

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

  6. 例: 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
  7. 例: 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
  8. 例: 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
  9. ほとんど同じ → ⼀般化できる 9

  10. 例: 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
  11. 例: 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
  12. 例: Expr の変換 (⼀般Lv. 1: map ) 引き算を表すクラス Sub が増えた。

    case class Sub[A](l: Expr[A], r: Expr[A]) extends Expr[A] どうする? 12
  13. 例: 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
  14. 例: 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
  15. 例: Expr のその他の操作 (少し話題を変えて) Expr の他の操作も考えられる。 等価性の⽐較 ⽂字列化 数式として評価 Add(Value(10),

    Value(20)) -> 30 これらは map では⼀般化できなさそうなので、個別に実装する必要がありそう。 Mul とか Div が増えたら、これも修正するの‥‥ ? 15
  16. つらい! 16

  17. 例: 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
  18. ほとんど同じ → ⼀般化できる(?) 18

  19. 発想の⾶躍 19

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

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

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

  23. つまり何が⾔いたいのか どんな構造でも表現できるすごいデータ構造があったとする。 元のデータ構造( Expr )からその構造 への変換と、 その構造 に対する操作( map ,

    equal , show など)があれば、 型を拡張した場合に、その構造 への変換を拡張するだけで、 他の操作も拡張できる。 そんな都合のいいデータ構造があるのか? 23
  24. HList と Coproduct HList : いくつでも要素を格納できる Tuple Coproduct : いくつでも型を選択できる

    Either この2つがあれば⼤体の型は表現できる。 24
  25. 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
  26. 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
  27. 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
  28. Coproduct 例 例: 型 Int :+: String :+: Boolean :+:

    CNil Int 、 String 、 Boolean のうち、どれかの値を取る型 例: 値 Inl[Int, String :+: Boolean :+: CNil](1) 上の型で、 Int を持つ Coproduct の値 HList と同じく、パターンマッチなどで値を取り出す。 28
  29. HList , Coproduct まとめ HList はいくつかの型・値を並べたもの(タプルっぽくてリストっぽい) Coproduct はいくつかの型・値のうち、どれか1つを取るもの( Either っぽい)

    どちらも定義⾃体は数⾏で書ける(普通の) sealed trait , case class 。 29
  30. データ型と HList , Coproduct との変換 型クラスにする。 trait Converter[X, Y] {

    def from(x: X): Y def to(y: Y): X } 30
  31. 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
  32. 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
  33. 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
  34. HList , Coproduct に対する処理の実装 今回は equal を実装する。 やはり型クラスにする。 trait Equal[A]

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

    hnilEqual = new Equal[HNil] { def equal(x: HNil, y: HNil) = true } 35
  36. :*: に対する実装 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
  37. CNil に対する実装 CNil のインスタンスは存在しないはずなので、例外を投げておけばいい。 implicit val cnilEqual = new Equal[CNil]

    { def equal(x: CNil, y: CNil) = ??? } 37
  38. :+: に対する実装 持ってる値の位置が同じで、等しければ等しい。 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
  39. 基本的な型に対する Equal の実装 implicit val intEqual: Equal[Int] = (x, y)

    => x == y implicit val stringEqual: Equal[String] = (x, y) => x == y 39
  40. 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
  41. こうすることで、 Expr に対する Equal の実装を⼀切書いていないのにも関わらず、 implicitly[Equal[Expr[Int]]] が取得できる。 →実装を導出する。 implicitly[Equal[Expr[Int]]].equal(Value(10), Value(20))

    すごい。 41
  42. 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
  43. ここまでのまとめ HList , Coproduct との変換と、 HList , Coproduct での処理を ⽤意すると、元の型でもその処理が使えるようになる。

    変換と処理は疎なので、⼀度 HList , Coproduct で定義した処理は、 変換のある様々な型で使うことができる。 43
  44. しかし HList , Coproduct への変換を⼀々書くのが⾯倒。 →それを⾃動で(マクロで)やってくれるのが shapeless や dali などの、

    Generic Programming Library。 44
  45. 2. Generic Programming Libiary 45

  46. Generic Programming Libiary shapeless, dali (拙作) HList , Coproduct などのデータ構造

    それらの型とデータ構造の変換を⾃動的に作るマクロ 46
  47. マクロ Generic.Aux[A, B] が Converter[A, B] に相当。 Generic[A] で⾃動的に変換が定義される。 例:

    Generic[Expr[Int]] (これはshapelessもdaliも同じ) 47
  48. ここからが本題 48

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

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

    (型推論などとの兼ね合い?) 50
  51. dali.higher.Generic1 Generic1.Aux[F[_], R <: TypeFunction1] F[_] と R (に具体的な型を指定したもの)との変換を提供。 TypeFunction1

    の具体的な型には HList1 や Coproduct1 などがある。 (個⼈的には)分かりやすいと思う。 51
  52. そもそもdaliとは? MakeNowJust/dali Scala 2.13の機能を前提に作ったGeneric Programming Library 最低限の機能のみを提供(minimalism) catsの⽅クラスの⾃動導出も別ライブラリとして提供 Functor などだけではなく、

    Alternative や Monad などの導出にも対応 まだMaven Central Repositoryにはリリースしていない‥‥ (今週中には) Scala 2.13が使えるなら結構良いライブラリだと思う(⾃画⾃賛) 52
  53. dali開発の経緯 Scalaの勉強のため既存のライブラリの再発明をしまくっている MakeNowJust-Labo/scala-labo その中でshapelessの再発明をしていると、shapelessがScala 2.13の機能を使いこなし ていないことに気付く (後⽅互換性とか⾊々あるのだと思う) Scala 2.13の機能(by-name implicit,

    literal types)を使うともっと良いライブラリが作れ ると思って、切り出して開発を始める 53
  54. まとめ Generic Programming Libiaryを使うと、処理を型クラスで記述するだけで、⾊々な型で その処理が使えるようにできる HList や Coproduct は普通の型で、マクロを使った魔術的なのはそれらの変換を定義 する

    Generic のみ shapelessもいいけどdaliも使えよ 54