Scala with Catsを読んでいく
What is a Monad?
ざっくり言うと、MonadはコンストラクタとflatMapメソッドを持つもの。
Option[A] flatMap (A => Option[B]) => Option[B]
すべてのMonadはFunctorでもある。
※ flatMapとmapメソッドを持っていれば、for-comprehension(For式)を使うことができる。
Monad Laws
- Left identity
pure(a).flatMap(func) == func(a)
- Right identity
m.flatMap(pure) == m
- Associativity
m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))
ここらへんの圏論用語がいまいち分からない。
- identity ... 恒等射
- associativity ... 結合律
Monads in Cats
cats.Monadは2つの型クラスを継承している。
FlatMap type classflatMapメソッドを提供している
Applicativepureメソッドを提供している- Applicativeは
Functorを継承しているため、すべてのMonadでmapメソッドを使うことができる
Error Handling
プログラムで発生する可能性のあるエラーを表す代数的データ型を用いたアプローチ
sealed trait LoginError extends Product with Serializeable final case class UserNotFound(username: String) extends LoginError final case class PasswordIncorrect(username: String) extends LoginError case object UnexpectedError extends LoginError case class User(username: String, password: String) type LoginResult = Either[LoginError, User] def handleError(error: LoginError): Unit = error match { case UserNotFound(u) => println(s"User not found: $u") case PasswordIncorrect(u) => println(s"Password Incorrect: $u") case UnexpectedError => println(s"Unexpected error") }
Aside: Error Handling and MonadError
CatsはMonadErrorと呼ばれるエラーハンドリングのために使われるEitherのような抽象化したデータ型を提供する。
package cats trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] { def raiseError[A](e: E): F[A] def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] def handleError[A](fa: F[A])(f: E => A): F[A] def ensure[A](fa: F[A])(f: A => Boolean): F[A] }
F[_]はMonad型EはFに含まれるエラー型
MonadErrorの重要なメソッドは以下の2つ
raiseErrorhandleError
raiseErrorはMonadでいうpureメソッドに近い。
handleErrorWithはraiseErrorを補足する。
The Eval Monad
cats.Eval は、評価の様々なモデルを抽象化するためのMonadである。
典型的な2つはeagerとlazyでそれぞれcall-by-valueとcall-by-nameとも呼ばれる。
Evalはまた結果をメモすることができ、call-by-needで評価もできる。
また、Evalはスタックセーフなのでとても深い再帰処理でもスタックを開放することなく使用できる
評価時の挙動
call-by-value evaluation
val x = { println("Computing X") math.random } // Computing X // val x: Double = 0.0056134621561709785 x // first access // val res0: Double = 0.0056134621561709785 // first access x // second access // val res1: Double = 0.0056134621561709785
- 定義した時点(eager)で計算が評価される
- 計算は一度だけ評価されメモ化される
call-by-name evaluation
def y = { println("Computation Y") math.random } // def y: Double y // first access // Computation Y // val res0: Double = 0.6768399768604834 y // second access // Computation Y // val res1: Double = 0.1400800525943975
- 計算は使用した時点(lazy)で評価される
- 計算は毎回使用されるたびに評価される(メモ化されない)
call-by-need evaluation
lazy val z = { println("Computation Z") math.random } // lazy val z: Double z // first access // Computation Z // val res0: Double = 0.5971338589578261 z // second access val res5: Double = 0.5971338589578261
- 定義時点では評価されず(not eager)、使用した時点で評価される(lazy)
- 一度評価されると結果はキャッシュされる(memoized)
Eval's Models of Evaluation
Evalには3つのサブタイプがあり、上記で記載した評価時の挙動と一致している。
- Now(call-by-value)
- Always(call-by-name)
- Later(call-by-need)
| Scala | Cats | Properties |
|---|---|---|
| val | Now | eager, memoized |
| def | Always | lazy, not memoized |
| lazy | Later | lazy, memoized |
The Writer Monad
cats.data.Writerは計算と一緒にログを運ぶことができるMonadである。
Writer[W, A]は2つの型を運ぶことができる。
Wはログの型でAが結果の型を表す。
The Reader Monad
cats.data.Readerは入力に依存する操作を連続させることができるMonadである。
Readerのインスタンスは引数一つの関数をラップし、それらを合成するための便利なメソッドを提供する。
import cats.data.Reader final case class Cat(name: String, favoriteFood: String) val catName: Reader[Cat, String] = Reader(cat => cat.name) catName.run(Cat("Steve", "tuna")) // res1: cats.ppackage.Id[String] = "Steve"
The State Monad
cats.data.Stateは計算の一部として追加のステートを渡すことができる。
アトミックな状態操作を表すStateを定義し、mapやflatMapを使ってそれらを繋ぎ合わせる。
State[S, A]のインスタンスは S => (S, A)の型を持つ関数を表す。
SがStateの型で、Aが結果の型。
import cats.data.State val a = State[Int, String] { state => (state, s"The state is $state") } val (state, result) = a.run(10).value // state: Int = 10 // result: String = "The state is 10" val justTheState = a.runS(10).value // justTheState: Int = 10 val justTheResult = a.runA(10).value // justTheResult: String = "The state is 10"
Stateの特徴は2つ
- inputの状態をoutputの状態に変化させる
- 結果を計算する
Stateは3つのメソッドを提供する
- run
- runS
- runA
Define Custom Monads
以下の3つのメソッドを実装すればカスタムタイプのMonadを定義できる。
- flatMap
- pure
- tailRecM
tailRecMメソッドはCatsで使われる最適化で、flatMapをネストして呼び出したことで消費されるスタックのスペースを制限するために使われる。