Slide 1

Slide 1 text

Scala における 継続モナドの実装と活用 2019/09/16 Scala秋祭り Naoki Aoyama - @aoiroaoino

Slide 2

Slide 2 text

❖ Naoki Aoyama ❖ Twitter/GitHub: @aoiroaoino ❖ Working at: $ Whoami

Slide 3

Slide 3 text

Agenda ➢ Introduction ○ 継続、継続渡しとは? ➢ 継続モナドの実装 ○ 継続モナドを実装し、合成の意味を考える ➢ 継続モナドの活用 ○ 継続モナドの活用例 ○ 注意点 ➢ Conclusion

Slide 4

Slide 4 text

話さないこと ❖ 継続、継続モナドの歴史や基礎理論 ❖ 継続モナドの発展系について(通称 ContT, IndexedCont など) ❖ Scala 以外の継続、継続モナドについて ❖ Scalaz, Cats などのライブラリ継続モナドの実装や解説

Slide 5

Slide 5 text

Introduction

Slide 6

Slide 6 text

継続とは? ❖ 「その後に実行される計算/処理」

Slide 7

Slide 7 text

継続とは? ❖ 「その後に実行される計算/処理」

Slide 8

Slide 8 text

継続とは? val result = isEven(42) if (result) { println(s"$n is even number") } else { println(s"$n is odd number") }

Slide 9

Slide 9 text

継続とは? val result = isEven(42) if (result) { println(s"$n is even number") // 実行される } else { println(s"$n is odd number") // 実行されない(捨てられる処理) }

Slide 10

Slide 10 text

継続とは? def add(i: Int, j: Int): Int = i + j def mul(i: Int, j: Int): Int = i * j def show(i: Int): String = s"num: $i" val a = add(1, 2) val b = mul(a, 3) val s = show(b) println(s)

Slide 11

Slide 11 text

継続とは? def add(i: Int, j: Int): Int = i + j def mul(i: Int, j: Int): Int = i * j def show(i: Int): String = s"num: $i" val a = add(1, 2) // 計算結果 // その後の計算 val b = mul(a, 3) val s = show(b) println(s)

Slide 12

Slide 12 text

継続とは? def add(i: Int, j: Int): Int = i + j def mul(i: Int, j: Int): Int = i * j def show(i: Int): String = s"num: $i" val a = add(1, 2) val b = mul(a, 3) // 計算結果 // その後の計算 val s = show(b) println(s)

Slide 13

Slide 13 text

継続とは? def add(i: Int, j: Int): Int = i + j def mul(i: Int, j: Int): Int = i * j def show(i: Int): String = s"num: $i" val a = add(1, 2) val b = mul(a, 3) val s = show(b) // 計算結果 // その後の計算 println(s)

Slide 14

Slide 14 text

継続渡しスタイル ❖ 引数に「計算結果を受け取って実行する、その後の計算」を追加 ❖ 暗黙的だった継続を関数で表現 ❖ 継続を明示的に(値として)扱えるようになる ❖ CPS: Continuation-passing style

Slide 15

Slide 15 text

継続渡しスタイル // 通常の計算(直接スタイル) def add(i: Int, j: Int): Int = i + j // 継続渡しスタイル def add[R](i: Int, j: Int)(cont: Int => R): R = { val a = i + j cont(a) }

Slide 16

Slide 16 text

継続渡しスタイル // 通常の計算(直接スタイル) def show(i: Int): String = s"num: $i" // 継続渡しスタイル def show[R](i: Int)(cont: String => R): R = { val a = s"num: $i" cont(a) }

Slide 17

Slide 17 text

継続渡しスタイル // 1 + 2 を実行。その後の計算「 10倍する」 scala> add(1, 2)(a => a * 10) res0: Int = 30 // 1 + 2 を実行。その後の計算「引数をそのまま返す」 scala> add(1, 2)(a => a) res1: Int = 3

Slide 18

Slide 18 text

継続渡しスタイル // 1 を文字列に変換し prefix をつける。その後の計算「 suffix をつける」 scala> show(1)(a => a + " !!") res3: String = num: 1 !! // 1 を文字列に変換し prefix をつける。その後の計算「 Byte 配列を得る」 scala> show(1)(a => a.getBytes(java.nio.charset.StandardCharsets.UTF_8)) res4: Array[Byte] = Array(110, 117, 109, 58, 32, 49)

Slide 19

Slide 19 text

継続渡しスタイル // 直接スタイル(再掲) val a = add(1, 2) val b = mul(a, 3) val s = show(b) println(s)

Slide 20

Slide 20 text

継続渡しスタイル // 直接スタイル(再掲) val a = add(1, 2) val b = mul(a, 3) val s = show(b) println(s) // 継続渡しスタイル add(1, 2){ a => mul(a, 3){ b => show(b){ s => println(s) } } }

Slide 21

Slide 21 text

もしや、モナドでは?

Slide 22

Slide 22 text

継続モナドの実装

Slide 23

Slide 23 text

継続を値として扱う def add[R](i: Int, j: Int)(cont: Int => R): R = { val a = i + j cont(a) } def show[R](i: Int)(cont: String => R): R = { val a = s"num: $i" cont(a) }

Slide 24

Slide 24 text

継続を値として扱う def add[R](i: Int, j: Int)(cont: Int => R): R = { val a = i + j cont(a) } def show[R](i: Int)(cont: String => R): R = { val a = s"num: $i" cont(a) }

Slide 25

Slide 25 text

継続を値として扱う // 継続部分を関数に変更 def add[R](i: Int, j: Int): (Int => R) => R = { cont => ??? } def show[R](i: Int) : (String => R) => R = { cont => ??? }

Slide 26

Slide 26 text

継続を値として扱う // 継続部分を関数に変更 def add[R](i: Int, j: Int): (Int => R) => R = { cont => ??? } def show[R](i: Int) : (String => R) => R = { cont => ??? } // (A => R) => R という関数に Cont[R, A] と名前をつける type Cont[R, A] = (A => R) => R def add[R](i: Int, j: Int): Cont[R, Int] = { cont => ??? } def show[R](i: Int) : Cont[R, String] = { cont => ??? }

Slide 27

Slide 27 text

継続を値として扱う // Scala ではしばしばラップするデータ型を定義 final case class Cont[R, A](run: (A => R) => R) // もちろん、ケースクラス版 Cont[R, A] を用いて add, show が定義できる def add[R](i: Int, j: Int): Cont[R, Int] = Cont { cont => ??? } def show[R](i: Int) : Cont[R, String] = Cont { cont => ??? }

Slide 28

Slide 28 text

モナドってなんだっけ? ❖ モナド則を満たすように pure/flatMap を実装したもの(ざっくり)

Slide 29

Slide 29 text

モナドってなんだっけ? // M[_] はモナドの型 def pure[A](a: A): M[A] def flatMap[B](f: A => M[B]): M[B] // 右単位元 fa.flatMap(a => pure(a)) == fa // 左単位元 pure(a).flatMap(f) == f(a) // 結合律 fa.flatMap(f).flatMap(g) == fa.flatMap(a => f(a).flatMap(g))

Slide 30

Slide 30 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = ??? } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = ??? def map[B](f: A => B): Cont[R, B] = ??? }

Slide 31

Slide 31 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = ??? } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = ??? def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) // モナドのデフォルト実装 }

Slide 32

Slide 32 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = Cont(ar => ar(a)) // a をそのまま継続に渡すだけ } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = ??? def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) }

Slide 33

Slide 33 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = Cont(ar => ar(a)) } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont(br => ???) // 継続 br に対する計算結果を考える def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) }

Slide 34

Slide 34 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = Cont(ar => ar(a)) } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont(br => run(a => f(a)/* Cont[R, B] */)) // 自身の継続に引数 f の結果を渡す def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) }

Slide 35

Slide 35 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = Cont(ar => ar(a)) } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont(br => run(a => f(a).run(br))) // f から得られた継続に外側の継続を渡して実行 def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) }

Slide 36

Slide 36 text

継続モナドの実装 object Cont { def pure[R, A](a: A): Cont[R, A] = Cont(ar => ar(a)) } final case class Cont[R, A](run: (A => R) => R) { def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont(br => run(a => f(a).run(br)) def map[B](f: A => B): Cont[R, B] = flatMap(a => Cont.pure(f(a))) }

Slide 37

Slide 37 text

実装した継続モナドはモナド則を満たすか? // ScalaCheck v1.14.0 を使用 class ContMonadSpec extends Properties("Monad[Cont[R, ?]]") { def inc(i: Int): Cont[Int, Int] = Cont(_(i + 1)) property("rightIdentity") = Prop.forAll { i: Int => inc(i).flatMap(Cont.pure).run(identity) == inc(i).run(identity) } property("leftIdentity") = Prop.forAll { i: Int => Cont.pure[Int, Int](i).flatMap(inc).run(identity) == inc(i).run(identity) } // ... }

Slide 38

Slide 38 text

実装した継続モナドはモナド則を満たすか? // ScalaCheck v1.14.0 を使用 class ContMonadSpec extends Properties("Monad[Cont[R, ?]]") { // ... def add_![R](s: String): Cont[String, String] = Cont(_(s + "!")) def add_?[R](s: String): Cont[String, String] = Cont(_(s + "?")) property("associativity") = Prop.forAll { s: String => Cont.pure(s).flatMap(add_!).flatMap(add_?).run(identity) == Cont.pure(s).flatMap(a => add_!(a).flatMap(add_?)).run(identity) } }

Slide 39

Slide 39 text

実装した継続モナドはモナド則を満たすか? sbt:check_cont_monad_law> test [info] + Monad[Cont[R, ?]].rightIdentity: OK, passed 100 tests. [info] + Monad[Cont[R, ?]].leftIdentity: OK, passed 100 tests. [info] + Monad[Cont[R, ?]].associativity: OK, passed 100 tests. [info] Passed: Total 3, Failed 0, Errors 0, Passed 3

Slide 40

Slide 40 text

継続モナドを for 式で合成する def add[R](i: Int, j: Int): Cont[R, Int] = Cont(ar => ar(i + j)) def mul[R](i: Int, j: Int): Cont[R, Int] = Cont(ar => ar(i * j)) def show[R](i: Int): Cont[R, String] = Cont(ar => ar(s"num: $i")) def prog[R]: Cont[R, String] = for { a <- add(1, 2) b <- mul(a, 3) s <- show(b) } yield { s.toUpperCase }

Slide 41

Slide 41 text

継続モナドを for 式で合成する scala> prog.run(s => s.toList) res18: List[Char] = List(N, U, M, :, , 9) scala> prog.run(s => s.length) res19: Int = 6 scala> prog.run(s => s) res20: String = NUM: 9

Slide 42

Slide 42 text

継続モナドを for 式で合成する def prog[R]: Cont[R, String] = for { a <- add(1, 2) b <- mul(a, 3) s <- show(b) } yield { s.toUpperCase } def prog[R]: Cont[R, String] = add(1, 2).flatMap { a => mul(a, 3).flatMap { b => show(b).map { s => s.toUpperCase } } }

Slide 43

Slide 43 text

継続モナドを for 式で合成する def prog[R]: Cont[R, String] = for { a <- add(1, 2) b <- mul(a, 3) s <- show(b) } yield { s.toUpperCase } def prog[R]: Cont[R, String] = add(1, 2).flatMap { a => mul(a, 3).flatMap { b => show(b).map { s => s.toUpperCase } } } :Cont[R, A] { ar => ??? } の ar に渡される関数  ※厳密には ar には run の引数も含まれる

Slide 44

Slide 44 text

継続モナドを for 式で合成する def prog[R]: Cont[R, String] = for { a <- add(1, 2) b <- mul(a, 3) s <- show(b) } yield { s.toUpperCase } def prog[R]: Cont[R, String] = add(1, 2).flatMap { a => mul(a, 3).flatMap { b => show(b).map { s => s.toUpperCase } } } :Cont[R, A] { ar => ??? } の ar に渡される関数  ※厳密には ar には run の引数も含まれる

Slide 45

Slide 45 text

継続モナドを for 式で合成する def prog[R]: Cont[R, String] = for { a <- add(1, 2) b <- mul(a, 3) s <- show(b) } yield { s.toUpperCase } def prog[R]: Cont[R, String] = add(1, 2).flatMap { a => mul(a, 3).flatMap { b => show(b).map { s => s.toUpperCase } } } :Cont[R, A] { ar => ??? } の ar に渡される関数  ※厳密には ar には run の引数も含まれる

Slide 46

Slide 46 text

継続モナドの「文脈」 各種モナドはそれぞれ何かしらの「文脈」を持つ。 ➢ Option モナド ○ 値の有無、失敗しうる可能性のある計算 ➢ List モナド ○ 複数の解があるような、非決定計算 ➢ State モナド ○ 状態を持つような計算 では、継続モナドは?

Slide 47

Slide 47 text

継続モナドの「文脈」 ❖ 継続を値として扱える計算

Slide 48

Slide 48 text

継続モナドの活用

Slide 49

Slide 49 text

継続モナドの活用例 ❖ 計算結果による分岐 ❖ エラーハンドリング ❖ リソースの管理 ❖ タイムアウト ❖ リトライ ❖ … etc ※ ここに列挙しているものは継続モナドでなくても実現可能です。

Slide 50

Slide 50 text

【活用例】計算結果による分岐 def fizzCont(i: Int): Cont[String, Int] = Cont { cont => if (i % 3 == 0) { "Fizz" // 継続(cont)を実行しないので計算が "Fizz" で終了する } else { cont(i) // 継続(cont)に i を渡し、後続の処理を実行する } } // 以下、同様の実装 def buzzCont(i: Int): Cont[String, Int] = ... def fizzBuzzCont(i: Int): Cont[String, Int] = ...

Slide 51

Slide 51 text

【活用例】計算結果による分岐 def fizzBuzz(i: Int): Cont[String, Int] = for { a <- fizzBuzzCont(i) b <- fizzCont(a) c <- buzzCont(b) } yield c scala> LazyList.from(1).map(fizzBuzz(_).run(_.toString)).take(15).toList res54: List[String] = List(1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz)

Slide 52

Slide 52 text

【活用例】エラーハンドリング // - Some(a) の場合、継続に値(a)を渡して実行 // - None の場合、継続を破棄して ifNone の結果を返す def someValueOr[R, A](fa: Option[A])(ifNone: => R): Cont[R, A] = Cont(ar => fa.fold(ifNone)(ar)) // - Success(a) の場合、継続に値(a)を渡して実行 // - Failure(e) の場合、継続を破棄して ifFailure の結果を返す def successValueOr[R, A](fa: Try[A])(ifFailure: Throwable => R): Cont[R, A] = Cont(ar => fa.fold(ifFailure, ar))

Slide 53

Slide 53 text

【活用例】エラーハンドリング // input より、指定された key に対応する値を取り出し、数値型に変換する def parseInt(input: Map[String, String], key: String): Cont[String, Int] = for { s <- someValueOr(input.get(key))(s"not found: $key") i <- successValueOr(Try(s.toInt))(_.toString) } yield i

Slide 54

Slide 54 text

【活用例】エラーハンドリング // 例えば下記のような input を仮定すると val input = Map("name" -> "John", "age" -> "17") scala> parseInt(input, "address").run(i => s"result: $i") res99: String = not found: address scala> parseInt(input, "name").run(i => s"result: $i") res100: String = java.lang.NumberFormatException: For input string: "John" scala> parseInt(input, "age").run(i => s"result: $i") res101: String = result: 17

Slide 55

Slide 55 text

【活用例】リソースの管理 // Loan パターンの例 def using[A <: AutoCloseable, B](a: => A, n: String)(f: A => B): B = try f(a) finally a.close() // Scala 2.13.0 から scala.util.Using が入った(話簡略化のため例外投げる版を使用 ) def resource[R, A](resource: R)(body: (R) => A)(implicit r: Releasable[R]): A // resource メソッドを継続モナドでラップ def resource[R, A](a: => A)(implicit r: Releasable[A]): Cont[R, A] = Cont(ar => Using.resource(a)(ar))

Slide 56

Slide 56 text

【活用例】リソースの管理 def lines(reader: BufferedReader): Iterator[String] = ... // Using.Manager を使わず、しかも for 式で合成できる val prog: Cont[List[String], List[String]] = for { a <- resource(new BufferedReader(new FileReader("/tmp/file1.txt"))) b <- resource(new BufferedReader(new FileReader("/tmp/file2.txt"))) c <- resource(new BufferedReader(new FileReader("/tmp/file3.txt"))) } yield (lines(a) ++ lines(b) ++ lines(c)).toList scala> prog.run(identity) res140: List[String] = List(Hello, 1, Hello, 2, Hello, 3)

Slide 57

Slide 57 text

【活用例】リソースの管理 // close() 実行時に println する独自 Releasable を定義 def r(n: String): Using.Releasable[AutoCloseable] = _.close().tap(_ => println(s"call close() for $n")) // Releasable を明示的に渡して実行 val prog: Cont[List[String], List[String]] = for { a <- resource(new BufferedReader(new FileReader("/tmp/file1.txt")))(r("f1")) b <- resource(new BufferedReader(new FileReader("/tmp/file2.txt")))(r("f2")) c <- resource(new BufferedReader(new FileReader("/tmp/file3.txt")))(r("f3")) } yield (lines(a) ++ lines(b) ++ lines(c)).toList

Slide 58

Slide 58 text

【活用例】リソースの管理 // きちんとリソースの評価順と逆順に close() が実行されている scala> prog.run(identity) call close() for f3 call close() for f2 call close() for f1 res145: List[String] = List(Hello, 1, Hello, 2, Hello, 3)

Slide 59

Slide 59 text

活用する際の注意点 - 例外処理 type User = String type Result = String def findAll[R](): Cont[Result, List[User]] = Cont { ar => try { ar(List("foo", "bar")) // DB への問い合わせは成功したとする } catch { case _: Throwable => "query execution error" } }

Slide 60

Slide 60 text

活用する際の注意点 - 例外処理 // 全て成功する場合 val prog: Cont[Result, List[User]] = for { users <- findAll() names <- Cont.pure(users.map(n => s"[$n]")) } yield names scala> prog.run(_.mkString(", ")) res132: Result = [foo], [bar]

Slide 61

Slide 61 text

活用する際の注意点 - 例外処理 // 継続の深いところで例外が投げられた場合 val prog: Cont[Result, List[User]] = for { users <- findAll() names <- Cont.pure(users.map(n => s"[$n]")) _ = 1 / 0 // java.lang.ArithmeticException: / by zero } yield names // 全く関係のないメッセージが ... scala> prog.run(_.mkString(", ")) res133: Result = query execution error

Slide 62

Slide 62 text

活用する際の注意点 - 実行回数 // 後続の処理をなんとなく二回実行するやつ def twice: Cont[Unit, Unit] = Cont { ar => ar(()); ar(()) } // 絶対に一回しか実行して欲しくない DB への書き込み def insert(s: String): Cont[Unit, Unit] = Cont { ar => println("insert data to database") ar(()) }

Slide 63

Slide 63 text

活用する際の注意点 - 実行回数 val prog: Cont[Unit, Unit] = for { _ <- twice _ <- insert("some data") } yield () // めでたく二回実行されてしまいました scala> prog.run(identity) insert data to database insert data to database

Slide 64

Slide 64 text

Conclusion

Slide 65

Slide 65 text

継続モナドを使うメリット ➢ 様々な活用方法がある ○ 「継続を値として扱える」という性質は想像以上に便利 ○ 関数を用いた抽象的な概念なので、どのレイヤーでも使える ➢ 処理の流れを合成によって組み立てられる ○ 継続モナドに限らないが、モナドであることのメリットの一つ ○ for 式での合成は可読性向上にも寄与 ➢ 再利用性が高い ○ 小さく作って、組み合わせて使える

Slide 66

Slide 66 text

継続モナドを使うデメリット(注意点) ➢ 処理に適切に名前をつけないと、何をしているか分からなくなる ○ 継続をどう扱ってるか、実装を読む必要が出てくる ○ 複雑なことをすると実装を読んでも分からない場合もある ➢ 気をつけないと意図しない動作になる ○ 実行回数例外処理などが自分の外側で管理される 複数回実行、意図しない箇所での例外 catch など ➢ for 式の順序と処理の流れ(見た目)が必ずしも一致しない ○ かえって処理の流れの理解を妨げることも

Slide 67

Slide 67 text

まとめ ➢ 継続の概念と継続モナドの基本的な実装/動作を解説した ➢ エラーハンドリングやリソース管理など様々な場面で活用できる ○ レイヤー問わず使える便利な道具。再利用性も高い。 ➢ 上手に活用するとシンプルに記述でき、可読性も向上する ○ ただし直感的でない名前や複雑な実装は可読性が下がるだけでなく、 意図しない動作に悩まされることもある。 薬も過ぎれば毒になるので、用法・用量を守って正しくお使いください

Slide 68

Slide 68 text

Appendix ➢ 補足、参考資料、ソースコードメモなど ○ https://gist.github.com/aoiroaoino/f017458c29d0b98eeaf239529f04af22