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] を実装できるか確かめてみよう。