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 class
flatMap
メソッドを提供している
Applicative
pure
メソッドを提供している- 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つ
raiseError
handleError
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をネストして呼び出したことで消費されるスタックのスペースを制限するために使われる。