Share
Embed
Scala におけるモナドって何だろう?
Scala関西Summit 2019 の登壇資料です。
変更履歴
v1.0.0
v0.1.1 - init
Naoki Aoyama - @aoiroaoino
October 26, 2019
Transcript
70 slides
-
Agenda ➢ Introduction ◦ モナドは身近な存在 ➢ Scala におけるモナドの基本 ◦ モナドって何?実用上の価値とは?
◦ 抽象的なモナドと具体的なモナド ➢ モナドを活用するために ◦ いくつかの具体的なモナドの紹介 ◦ 抽象的なモナドの活用方法 ➢ Conclusion -
モナドは身近な存在 Option("42").flatMap(_.toIntOption) // res25: Option[Int] = Some(42) for { i
<- List(1, 2, 3) j <- List(1, -1) } yield i * j // res26: List[Int] = List(1, -1, 2, -2, 3, -3) -
モナドは身近な存在 ➢ Scala でプログラミングする上で必須の概念ではない ➢ モナドを知らずとも Scala らしいコードは十分書ける ➢ そもそも標準ライブラリに(抽象的な)モナドは存在しない
◦ v2.12 までは FilterMonadic というそれっぽいものが存在していた ◦ 「抽象的なモナド」については後ほど -
モナドって何? // M[_] はモナドの型 def pure[A](a: A): M[A] def flatMap[B](f:
A => M[B]): M[B] // map はデフォルト実装が与えられる def map[B](f: A => B): M[B] = flatMap(a => pure(f(b))) -
モナドって何? // M[_] はモナドの型 def pure[A](a: A): M[A] def flatMap[B](f:
A => M[B]): M[B] // モナド則 pure(a).flatMap(f) == f(a) // 左単位元 fa.flatMap(a => pure(a)) == fa // 右単位元 fa.flatMap(f).flatMap(g) == fa.flatMap(a => f(a).flatMap(g)) // 結合律 -
Scala における実用上のモナドの価値 val xs: Either[String, Int] = Right(1) val ys:
Either[String, Int] = Right(2) for { x <- xs y <- ys } yield x + y // res0: scala.util.Either[String,Int] = Right(3) -
Scala における実用上のモナドの価値 val xs: List[String] = List("foo", "bar") val ys:
List[String] = List("!", "?") for { x <- xs y <- ys } yield x + y // res3: List[String] = List(foo!, foo?, bar!, bar?) -
Scala における実用上のモナドの価値 for { x <- expr1 y <- expr2
} yield { expr3 } expr1.flatMap { x => expr2.map { y => expr3 } } -
抽象的なモナドとは? ❖ (広義の)インターフェース ❖ 抽象的なモナドを実装できるプログラミング言語は少ない ❖ 主に高階型パラメータ、型クラスを用いて実装する ❖ (型クラスによって)既存のデータ型もモナドとして拡張できる ❖
例: Scalaz や Cats の trait Monad[F[_]] { … } -
具体的なモナドとは? ❖ (広義の)インターフェースに対するインスタンス、実装 ❖ 具体的なモナドインスタンスを実装できるプログラミング言語は多い ❖ 「モナドを知らなくても使える/使ってる」は主に具体的なモナドの方 ❖ 例: 標準ライブラリの
Option[A], Either[E, A], List[A], etc… -
【抽象的なモナドの実装】モナド型クラスの定義 trait Monad[F[_]] { def pure[A](a: A): F[A] def flatMap[A,
B](fa: F[A])(f: A => F[B]): F[B] // デフォルト実装 def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(a => pure(f(a)) } object Monad { // implicitly[Monad[F]] のエイリアス def apply[F[_]](implicit M: Monad[F]): Monad[F] = M } -
【抽象的なモナドの実装】データ型の例 sealed abstract class Maybe[A] case class Just[A](a: A) extends
Maybe[A] case class Empty[A]() extends Maybe[A] object Maybe { // smart constructor def just[A](a: A): Maybe[A] = Just(a) def empty[A]: Maybe[A] = Empty() } -
【抽象的なモナドの実装】インスタンスを定義 implicit val maybeInstances: Monad[Maybe] = new Monad[Maybe] { def
pure[A](a: A): Maybe[A] = Just(a) def flatMap[A, B](fa: Maybe[A])(f: A => Maybe[B]): Maybe[B] = fa match { case Just(a) => f(a) case Empty() => Empty() } } -
【抽象的なモナドの実装】動作確認 scala> val n = Monad[Maybe].pure(42) n: Maybe[Int] = Just(42)
scala> Monad[Maybe].flatMap(n) { i => | if (i > 0) Maybe.just(i) else Maybe.empty[Int] | } res7: Maybe[Int] = Just(42) -
【抽象的なモナドの実装】モナド則を満たすか trait MonadLaw[F[_]] { def M: Monad[F] def leftIdentity[A, B](a:
A, f: A => F[B]): Boolean = M.flatMap(M.pure(a))(f) == f(a) def rightIdentity[A](fa: F[A]): Boolean = M.flatMap(fa)(M.pure) == fa def associativity[A, B, C](fa: F[A], f: A => F[B], g: B => F[C]): Boolean = M.flatMap(M.flatMap(fa)(f))(g) == M.flatMap(fa)(a => M.flatMap(f(a))(g)) } -
【抽象的なモナドの実装】モナド則を満たすか class MonadSpec extends Properties("Monad") { def law: Monad.Law[Maybe] =
new Monad.Law[Maybe] { override val M = Maybe.maybeInstances } def odd(i: Int): Maybe[Int] = if (i % 2 != 0) Just(i) else Empty() def fizz(i: Int): Maybe[Int] = if (i % 3 == 0) Just(i) else Empty() def buzz(i: Int): Maybe[Int] = if (i % 5 == 0) Just(i) else Empty() // ... } -
【抽象的なモナドの実装】モナド則を満たすか class MonadSpec extends Properties("Monad") { def law: Monad.Law[Maybe] =
// 左単位元, 右単位元 property("leftIdentity") = Prop.forAll { i: Int => law.leftIdentity(i, fizz) } property("rightIdentity") = Prop.forAll { i: Int => law.rightIdentity(buzz(i)) } // ... } -
【抽象的なモナドの実装】モナド則を満たすか class MonadSpec extends Properties("Monad") { def law: Monad.Law[Maybe] =
// ... // 結合律 property("associativity") = Prop.forAll { i: Int => law.associativity(odd(i), fizz, buzz) } } -
【抽象的なモナドの実装】モナド則を満たすか sbt:fn> test [info] + Monad.associativity: OK, passed 100 tests.
[info] + Monad.rightIdentity: OK, passed 100 tests. [info] + Monad.leftIdentity: OK, passed 100 tests. [info] Passed: Total 3, Failed 0, Errors 0, Passed 3 -
【抽象的なモナドの実装】for 式で合成するために // for 式で合成ができない for { i <- Maybe.just(1)
j <- Maybe.just(2) } yield i + j // i <- Maybe.just(1) // ^ // On line 2: error: value flatMap is not a member of Maybe[Int] // j <- Maybe.just(2) // ^ // On line 3: error: value map is not a member of Maybe[Int] -
【抽象的なモナドの実装】for 式で合成するために // Monad[Maybe].map(fa)(f) を fa.map(f) と書けるようになる trait MonadSyntax {
implicit def toMonadOps[F[_], A](fa: F[A]): MonadOps[F, A] = new MonadOps(fa) } class MonadOps[F[_], A](val fa: F[A]) extends AnyVal { def map[B](f: A => B)(implicit M: Monad[F]): F[B] = M.map(fa)(f) def flatMap[B](f: A => F[B])(implicit M: Monad[F]): F[B] = M.flatMap(fa)(f) } -
【抽象的なモナドの実装】for 式で合成するために // toMonadOps がスコープに定義されてる状態で for 式で合成ができる for { i
<- Maybe.just(1) j <- Maybe.just(2) } yield i + j // res1: Maybe[Int] = Just(3) -
【具体的なモナドの実装】実装方法 sealed abstract class Maybe[A] { def flatMap[B](f: A =>
Maybe[B]): Maybe[B] = this match { case Just(a) => f(a) case Empty() => Empty() } // デフォルト実装 def map[B](f: A => B): Maybe[B] = flatMap(a => Maybe.pure(f(a))) } case class Just[A](a: A) extends Maybe[A] case class Empty[A]() extends Maybe[A] -
【具体的なモナドの実装】動作確認 // 特に暗黙の型変換をせずとも for 式で合成できる for { i <- Maybe.just(1)
j <- Maybe.just(2) } yield i + j // res0: Maybe[Int] = Just(3) -
具体的なモナドの例 ❖ Option[A] ❖ Either[E, A] ❖ List[A] ❖ Reader[R,
A] ❖ Writer[W, A] ❖ State[S, A] ❖ Cont{R, A] ❖ IO[A] ❖ Free[F[_], A] ❖ ...etc -
【具体的なモナド】Reader モナド object Reader { def pure[R, A](a: A): Reader[R,
A] = Reader(_ => a) } final case class Reader[R, A](run: R => A) { def flatMap[B](f: A => Reader[R, B]): Reader[R, B] = Reader(r => f(run(r)).run(r)) def map[B](f: A => B): Reader[R, B] = flatMap(a => Reader.pure(f(a))) } -
【具体的なモナド】Reader モナド // for 式での合成時には計算は行われていない scala> val r = for
{ | i <- Reader[String, Int]{ s => (s.toInt + 1).tap(println) } | j <- Reader[String, Int]{ s => (s.toInt * 10).tap(println) } | } yield i + j r: fn.data.Reader[String,Int] = Reader(...) scala> r.run("1") // .run で引数を与えたタイミングで実行される 2 10 res13: Int = 12 -
【具体的なモナド】Cont モナド(継続モナド) 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))) } -
【具体的なモナド】Cont モナド(継続モナド) 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 } -
【具体的なモナド】Cont モナド(継続モナド) 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 -
【具体的なモナド】IO モナド object IO { def pure[A](a: A): IO[A] =
IO(a) def apply[A](a: => A): IO[A] = new IO[A] { def unsafeRun(): A = a } } sealed abstract class IO[A] { def unsafeRun(): A // 呼び出したタイミングで全ての計算が実行される def flatMap[B](f: A => IO[B]): IO[B] = IO { f(unsafeRun()).unsafeRun() } def map[B](f: A => B): IO[B] = flatMap(a => IO.pure(f(a))) } -
【具体的なモナド】IO モナド def readLine: IO[String] = IO { StdIn.readLine }
def println(s: String): IO[Unit] = IO { Predef.println(s) } scala> val io = println("Hello!") io: fn.data.IO[Unit] = fn.data.IO$$anon$1@30609427 scala> io.unsafeRun() Hello! -
【具体的なモナド】IO モナド scala> val app = for { | _
<- println("Message:") | s <- readLine | _ <- println(s"Your message is $s") | } yield s app: fn.data.IO[String] = fn.data.IO$$anon$1@63a536db scala> app.unsafeRun() Message: Your message is Hello! res15: String = Hello! -
【抽象的なモナド】MonadTransformer ❖ モナドの合成を考える ❖ def readInt: IO[Option[Int]] = IO {
StdIn.readLine().toIntOption } -
【抽象的なモナド】MonadTransformer val prog: IO[Option[Int]] = for { // IO[A] の合成
nOpt <- readInt mOpt <- readInt } yield for { // Option[A] の合成 n <- nOpt m <- mOpt } yield n + m -
【抽象的なモナド】MonadTransformer scala> prog.unsafeRun() // 1 と 2 を入力 res6: Option[Int]
= Some(3) scala> prog.unsafeRun() // 1 と a を入力 res7: Option[Int] = None -
【抽象的なモナド】MonadTransformer ➢ F[_], G[_] がモナドのとき、F[G[_]] もモナドになる...とは限らない ➢ G[_] が特定の条件を満たす(具体的な)モナドの場合は F[G[_]]
もモナドになる ◦ F[Option[A]], F[Either[E, A]], F[Cont[R, A]] など ◦ 特定の条件: F[G[_]] を G[F[_]] にできる ➢ 条件を満たす(具体的な)モナドに対応するデータ型を定義し、 それもまたモナドのインスタンスにしてしまおう ◦ OptionT[F, A], EitherT[F, E, A], ContT[F, R, A] など -
【抽象的なモナド】MonadTransformer object OptionT { def pure[F[_], A](a: A)(implicit M: Monad[F]):
OptionT[F, A] = OptionT(M.pure(Some(a))) } -
【抽象的なモナド】MonadTransformer final case class OptionT[F[_], A](value: F[Option[A]]) { def map[B](f:
A => B)(implicit M: Monad[F]): OptionT[F, B] = OptionT(M.map(value)(_.map(f))) def flatMap[B](f: A => OptionT[F, B])(implicit M: Monad[F]): OptionT[F, B] = OptionT(M.flatMap(value) { case Some(a) => f(a).value case None => M.pure(None) }) } -
【抽象的なモナド】MonadTransformer object IO { // ... // あらかじめ IO[A] のモナドインスタンスは定義されているとする
implicit val ioInstances: Monad[IO] = new Monad[IO] { override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.flatMap(f) override def pure[A](a: A): IO[A] = IO.pure(a) } } -
【抽象的なモナド】MonadTransformer scala> val prog = for { | i <-
OptionT(readInt) | j <- OptionT(readInt) | } yield i + j prog: fn.data.OptionT[fn.data.IO,Int] = OptionT(fn.data.IO$$anon$1@524114c0) scala> prog.value res2: fn.data.IO[Option[Int]] = fn.data.IO$$anon$1@524114c0 -
【抽象的なモナド】Effect Injection ❖ 返り値の型を F[_] に包み、(主に)実装クラスで Effect を差し替える ❖ F[_]
に Monad の制約をかけることで for 式で書けることを約束 ❖ 巷で Tagless-final と呼ばれているパターン -
【抽象的なモナド】Effect Injection // このタイミングでモナドの制約をかける必要は必ずしもない trait UserRepository[F[_]] { def resolveAll(ids: UserId*):
F[Seq[User]] } trait MessageRepository[F[_]] { def resolveAll(ids: MessageId*): F[Seq[Message]] } -
【抽象的なモナド】Effect Injection class GetReplyTargetUsers[F[_]: Monad]( userRepo: UserRepository[F] messageRepo: MessageRepository[F] ){
def run(targetMessageId: MessageId): F[Seq[User]] = for { // 前出の toMonadOps がスコープ内で定義されてること messages <- msessageRepo.resolveAll(targetMessageId) userIds = messages.flatMap(_.replyMessages.targetUser).toSet users <- userRepo.resolveAll(userIds: _*) } yield users } -
【抽象的なモナド】Effect Injection val implByIO: GetReplyTargetUsers[IO] = ??? implByIO.run(messageId) // =>
IO[Seq[User]] type Aync[A] = Reader[ExecutionContext, Future[A]] val implByAsync: GetReplyTargetUsers[Async] = ??? implByAsync.run(messageId) // => Async[Seq[User]] // => Reader[ExecutionContext, Future[Seq[User]]] -
【抽象的なモナド】Effect Injection - メリット ❖ Monad 型クラスのインスタンスが定義されていれば F[_] に当てはめられる =>
実行する際の「文脈」をインターフェースから切りせる => あとで(実装するタイミングで)差し替えられる -
【抽象的なモナド】Effect Injection - 注意点 ❖ あくまで F[_] に包まれたままでも(for 式などで)合成できることだけを保証 =>
例外をハンドリングするか否かは F[_] の時点で保証できない -
まとめ ❖ モナドとは、モナド則を満たすように pure/flatMap を実装したもの ❖ Scala においてデータ型の「文脈」に依存しない汎用的な操作/合成を提供 ❖ 抽象的なモナドと具体的なモナドの紹介と活用例を紹介した
Scala におけるモナドは、知っておくと便利なツールの一つ