はじめに
『Scala Design Patterns』 という本を、会社の輪読会で呼んでいるのですが、モナド周りの説明が難しかったので自分なりに日本語で咀嚼してみた内容になります。具体的には、『Scala Design Patterns』の265ページ、Chapter10-Monads の内容です。
(「モナド何も分からん」から、「モナドちょっと分からん」くらいにはなりました)
Monad の定義
本の中の定義
Monads are functors that have the
unit
andflatMap
methods and follow the monad rules.
ということで、モナドの定義を知るためには Functors とは何かを知る必要がある。
Functorsとは
map
メソッドを持ち、以下の決まりに則ったものを呼ぶ。これらを Functor則 と呼ぶことがある。
- Identity:
map(x) (i => i) == x
- Composition:
map(map(x) (i => y(i))) (i => z(i))
とmap(x) (i => z(y(i)))
は等しい。 map
メソッドはデータ構造自体を変更することはない。データの表現を変えるだけ。
実際には以下のようなコードが Functor となる。
trait Functor[F[_]] { def map[T, Y](l: F[T])(f: T => Y): F[Y] }
map
メソッドが定義されており、F
は保ったまま、その中の T
を Y
へと変換している。
これの具体的な実装は、例えば List
の場合以下のようになる。
val listFunctor: Functor[List] = new Functor[List] { override def map[T, Y](l: List[T])(f: T => Y): List[Y] = l.map(f) }
Monadとは
Monads are functors that have the
unit
andflatMap
methods and follow the monad rules.
flatMap, unit, map
flatMap
とは、map
した後にflatten
すること。unit
はコンストラクタと同義。map
はflatMap
とunit
を用いて表現することができる。f: T => Y
の結果をunit
で包んであげて、その後でflatten
するイメージ。
前節で取り上げた Functor
とは異なり、map
などの関数の引数としてデータを受け取るのではなく、Functor
自体がデータを保持しているとする。モナドの定義から、flatMap
を Monad
側に持たせ、map
の具体的な実装は Monad
内で flatMap
と unit
を用いて表している。
trait Functor[T] { def map[Y](f: T => Y): Functor[Y] } trait Monad[T] extends Functor[T] { def unit[Y](value: Y): Monad[Y] def flatMap[Y](f: T => Monad[Y]): Monad[Y] override def map[Y](f: T => Y): Monad[Y] = flatMap(i => unit(f(i))) }
モナド則
- Identity law: 恒等写像に対して
map
をしてもデータは変わらない。また、unit
メソッドに対してflatMap
をしてもデータは変わらない。unit
はモノイドで言うところの単位元。map(x)(i => i) == x
flatMap(x) (i => unit(i)) == x
The unit law
unit(x).flatMap { y => f(y) } == f(x)
unit(x).map { y => f(y) } == unit(f(x))
Composition: 複数の
map
やflatMap
は合成することができ、順番によって違いは生まれない。(副作用が生まれる場合はこれが満たされないので注意が必要)x.map(i => y(i)).map(i => z(i))
とx.map(i => z(y(i)))
は等しい。x.flatMap(i => y(i)).flatMap(i => z(i))
とx.flatMap(i => y(i).flatMap(j => z(j)))
は等しい
何が嬉しいのか
状態を隠蔽して、重要な部分のみに注目することができるようになると書いてある。具体例がないと実感できないので、本に載っている具体例を見てみる。
Monads help us hide this state from the user and just expost the important parts as well as abstract the way we deal with errors, and so on.
具体例
Optionモナド
まずは基底クラスである Option
を定義する。
sealed trait Option[A] extends Monad[A]
そして、具体的な実装である Some
と None
を実装する。
case class Some[A](a: A) extends Option[A] { override def unit[Y](value: Y): Monad[Y] = Some(value) override def flatMap[Y](f: A => Monad[Y]): Monad[Y] = f(a) }
case class None[A]() extends Option[A] { override def unit[Y](value: Y): Monad[Y] = None() override def flatMap[Y](f: (A) => Monad[Y]): Monad[Y] = None() }
副作用の隠蔽という点では、適していない例のように思う。Some
と None
を同じtraitから定義し、map
, flatMap
が定義してあるおかげで for
文で書けるという嬉しさはある。
IOモナド
ファイルから行を読み取り、その全てを大文字にしてあtらしいファイルに書き出すプログラムを考える。「状態」として今回は FileIOState
の数値をincrementする。
まずはIOの状態を保持する State
を定義する。
sealed trait State { def next: State }
IOAction
は、古い State
から、新しい State
と操作の結果のタプルを返す関数を継承している。unit
, map
, flatMap
を実装しており、モナド則を満たしている。
flatMap
では、
- 新しい
IOAction
を返す。 - その新しい
IOAction
はapply
で古いIOAction
のapply
を呼び出し、実行結果として新しいState
と結果のタプルを受け取る。 - 受け取った結果を引数とし、
flatMap
で受け取った関数f: T => IOAction[Y]
を実行。 - 実行した結果の
IOAction
であるaction2
に新しいState
を渡し、apply
を呼び出す。
ここまで全て、flatMap
実行時には呼び出しがされておらず、IOAction#apply
が呼び出されて初めて実行されることに注意する。つまり副作用は flatMap
時点では発生していない。
sealed abstract class IOAction[T] extends ((State) => (State, T)) { def unit[Y](value: Y): IOAction[Y] = IOAction.unit(value) def flatMap[Y](f: (T) => IOAction[Y]): IOAction[Y] = { val self = this new IOAction[Y] { // 1 override def apply(state: State): (State, Y) = { val (state2, res) = self(state) // 2 val action2 = f(res) // 3 action2(state2) // 4 } } } def map[Y](f: T => Y): IOAction[Y] = flatMap(i => unit(f(i))) }
object IOAction { def apply[T](result: => T): IOAction[T] = new SimpleAction[T](result) def unit[T](value: T): IOAction[T] = new EmptyAction[T](value) // 名前渡しで引数を受け取り、実行時に評価する private class SimpleAction[T](result: => T) extends IOAction[T] { override def apply(state: State): (State, T) = (state.next, result) } // unitを呼び出した時に状態を変えないような実装が必要 private class EmptyAction[T](value: T) extends IOAction[T] { override def apply(state: State): (State, T) = (state, value) } }
具体的な使い方を見る。いずれも IOAction
の引数は名前渡しなので、この時点では実行されず、受け取った IOAction
に対して apply
を呼んだ時に初めて実行される。
package object io { def readFile(path: String): IOAction[Iterator[String]] = IOAction(Source.fromFile(path).getLines()) def writeFile(path: String, lines: Iterator[String]): IOAction[Unit] = IOAction({ val file = new File(path) printToFile(file) { p => lines.foreach(p.println) } }) private def printToFile(file: File)(writeOp: PrintWriter => Unit): Unit = { val writer = new PrintWriter(file) try { writeOp(writer) } finally { writer.close() } } }
そして、FileIOを実行できるクラスを定義する。State
の実装である FileIOState
は privateにすることで、外から状態を作れないようにしている。
run
でやっていることは、次のような意味になる。
runIO
でIOAction
を定義する。IOaction
のapply
を、FileIOState
の初期値を渡して実行する。
action
を実行するまで副作用が発生しておらず、副作用を局所化できているのが嬉しい点。
abstract class FileIO { private class FileIOState(id: Int) extends State { override def next: State = new FileIOState(id + 1) } def run(args: Array[String]): Unit = { val action = runIO(args(0), args(1)) action(new FileIOState(0)) } def runIO(readPath: String, writePath: String): IOAction[_] }
実際の使い方としては次の通り。
object FileIOExample extends FileIO { def main(args: Array[String]): Unit = { run(args) } override def runIO(readPath: String, writePath: String): IOAction[_] = for { lines <- readFile(readPath) _ <- writeFile(writePath, lines.map(_.toUpperCase)) } yield () // 以下と同じ // override def runIO(readPath: String, writePath: String): IOAction[_] = { // readFile(readPath).flatMap { lines => // writeFile(writePath, lines.map(_.toUpperCase)) // } }