5日目に綱渡りの例を使って得られた直観は、
>>=
を使ったモナディックなチェインはある演算から次の演算へとコンテキストを引き渡すということだった。
中間値に 1つでも None
があっただけで、チェイン全体が島流しとなる。
引き渡されるコンテキストはモナドのインスタンスによって異なる。
例えば、7日目にみた State
データ型は、>>=
によって状態オブジェクトの明示的な引き渡しを自動化する。
これはモナドを Functor
、Apply
や Applicative
と比較したときに有用な直観だけどもストーリーとしては全体像を語らない。
モナド (正確には FlatMap
) に関するもう1つの直観は、
これらがシェルピンスキーの三角形のようなフラクタルであることだ。
フラクタルの個々の部分が全体の形の自己相似となっている。
例えば、List
を例にとる。複数の List
の List
は、単一のフラットな List
として取り扱うことができる。
val xss = List(List(1), List(2, 3), List(4))
// xss: List[List[Int]] = List(List(1), List(2, 3), List(4))
xss.flatten
// res0: List[Int] = List(1, 2, 3, 4)
この flatten
関数は List
データ構造の押し潰しを体現する。
型シグネチャで考えると、これは F[F[A]] => F[A]
だと言える。
平坦化を foldLeft
を使って再実装することで、より良い理解を得ることができる:
xss.foldLeft(List(): List[Int]) { _ ++ _ }
// res1: List[Int] = List(1, 2, 3, 4)
これによって List
は ++
に関してモナドを形成すると言うことができる。
次に、どの演算に関して Option
はモナドを形成しているのが考えてみる:
val o1 = Some(None: Option[Int]): Option[Option[Int]]
// o1: Option[Option[Int]] = Some(value = None)
val o2 = Some(Some(1): Option[Int]): Option[Option[Int]]
// o2: Option[Option[Int]] = Some(value = Some(value = 1))
val o3 = None: Option[Option[Int]]
// o3: Option[Option[Int]] = None
foldLeft
で書いてみる:
o1.foldLeft(None: Option[Int]) { (_, _)._2 }
// res2: Option[Int] = None
o2.foldLeft(None: Option[Int]) { (_, _)._2 }
// res3: Option[Int] = Some(value = 1)
o3.foldLeft(None: Option[Int]) { (_, _)._2 }
// res4: Option[Int] = None
Option
は (_, _)._2
に関してモナドを形成しているみたいだ。
フラクタルという視点から State
データ型に関してもう一度考えてみると、
State
の State
がやはり State
であることは明らかだ。
この特性を利用することで、pop
や push
といったミニ・プログラムを書いて、
それらを for
内包表記を用いてより大きな State
に合成するといったことが可能となる:
def stackManip: State[Stack, Int] = for {
_ <- push(3)
a <- pop
b <- pop
} yield(b)
このような合成は自由モナドでもみた。
つまり、同じモナド・インスタンスの中ではモナディック値は合成することができる。
独自のモナドを発見したいと思ったら、フラクタル構造に気をつけるべきだ。
見つけたら flatten
関数 F[F[A]] => F[A]
を実装できるか確かめてみよう。