Monads

Scala with Catsを読んでいく

What is a Monad?

ざっくり言うと、MonadはコンストラクタとflatMapメソッドを持つもの。

Option[A] flatMap (A => Option[B]) => Option[B]

すべてのMonadFunctorでもある。

flatMapmapメソッドを持っていれば、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メソッドに近い。

handleErrorWithraiseErrorを補足する。

The Eval Monad

cats.Eval は、評価の様々なモデルを抽象化するためのMonadである。

典型的な2つはeagerlazyでそれぞれcall-by-valuecall-by-nameとも呼ばれる。

Evalはまた結果をメモすることができ、call-by-needで評価もできる。

ja.wikipedia.org

また、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をネストして呼び出したことで消費されるスタックのスペースを制限するために使われる。

Monadに関する参考情報

MonadTransformer とは何か · GitHub

合成できるモナド、モナドが合成できる時 - Milestones to EVERPEACE 〜alius via〜