Monad, ついでにFunctor, Applicativeについて

Monad がほしいとき

型クラスのインスタンス m a, m b について,次のような関数があるとする.

  • 「普通の関数」 f :: a -> b
  • 「構造 m 付きの結果を返す関数」 f' :: a -> m b

「普通の関数」は例えば g :: b -> c と合成して a -> c 型の関数型を作ったりできるのに対して,「構造 m 付きの結果を返す関数」は扱いづらそうにみえる.たとえば mOption とすると,

// 「普通の関数」であれば,
val f = (a: Int) => a + 1
val g = (b: Int) => b + 2
val h = (c: Int) => c + 3

// 簡単に合成できるけど.
(f andThen g andThen h)(100)  // => 106

// Option を返すような関数は,
val f_ = (a: Int) => Option(a + 1)
val g_ = (b: Int) => Option(b + 2) 
val h_ = (c: Int) => Option(c + 3)

// 合成が難しい.
f_(100) match {
  case Some(b) => g_(b) match {
    case Some(c) => h_(c)
    case None => None
  }
  case None => None
}.get // => 106

構造 m には,例えば複数の値を持ちうる (scala.List) とか,失敗しうる操作の結果 (scala.Option) とか,実行順序を保ちつつ遅延評価した結果 (cats.effect.IO) だとかがあり,それは型クラスであり,「文脈」や「箱」と表現される.

仮に,このような構造を扱う関数を普通の関数のように適用したり合成できるとしたら,その型クラス m にはどのような性質が求められるか? 結論としてそれがだいたい Monad という性質になる.

Monad を追いかける前に,似たように構造を扱う型クラスを単純なものからたどってみる.

Functor であれば…

  • fmap が利用できる.
fmap :: (a -> b) -> m a -> m b

「普通の関数」 f :: a -> b を適用して m a -> m b の変換ができる.scala.List であれば,各要素に f を適用した結果の配列が手に入り,scala.Option であれば,None の場合は None で, Some の場合にのみ値に f を適用した結果が得られる.また,cats.effect.IO であれば IO action を最後に追加した結果の IO が値として入る.

// List[Int] -> List[String]
val list: List[Int] = List(1,2,3)
list.map(_.toString) // List(1!, 2!, 3!)

// Option[String] -> Option[Int]
val just: Option[String] = Some[String]("hoge")
val nothing: Option[String] = None
just.map(_.length)  // Some(4)
nothing.map(_.length)   // None

// IO[Unit] -> IO[Int]
val io: IO[Unit] = IO.pure()
io.map(_ => readInt) // IO[Int]

Functor 則

ただし,fmap が正しい型付けで実装されているだけでは Functor とは言えず,以下の Functor 則を満たしている必要がある.これはコンパイラがチェックしてくれないので,ユーザが保証する必要がある.

  • fmap id = id

    • 恒等射で変換をかけても変わらないこと
  • fmap (f . g) = fmap(f) . fmap(g)

Applicative であれば…

  • pure, <*> が利用できる.
pure :: a -> f a
(<*>) :: m (a -> b) -> m a -> m b

Applicative な構造に包まれた関数 m (a -> b) を,同じく Applicative な構造に包まれた m a に適用して,Applicative な構造に包まれた結果 m b を得る関数 <*> を利用できる.これにより,構造から値や関数を取り出すことなく適用することが可能になる.一方,pure は値や関数を構造に包むだけの演算. pure<*> があれば Functor の fmap を自然につくることができるので, Applicative は Functor の拡張.

Applicative 則

Applicative にも Functor 同様,満たしていないといけない規則がある.省略.

Monad であれば…

  • return, >>= が利用できる.
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

return は Applicative の pure と同じ. >>= は bind 関数と呼ばれ, f :: a -> m a のような関数を m a に適用して m b を得る.さらにこれは他の関数と合成してつなげることができる.

つまり,「 構造 m 付きの結果を返す関数」は合成できなくて不便だなあーと最初に書いた問題は,構造 mモナドとして振る舞ってくれるなら合成することができる.

Scala では合成に forflatMap を用いる.

// List[String] => List[Int] => List[Double]
val f = (a: String) => List((a + "1").toInt)
val g = (b: Int) => List((b + 0.1).toDouble) 

for {
  elem <- List("100", "200", "300")
  i <- f(elem)
  j <- g(i)
} yield j      // List(1001.1, 2001.1, 3001.1)

List("100", "200", "300").flatMap(f).flatMap(g) // 等価

// Option[String] => Option[Int] => Option[Double]
val f = (a: String) => Option((a + "1").toInt)
val g = (b: Int) => None
val h = (c: Int) => Option((c + 0.1).toDouble)

val just: Option[String] = Some("100")
val nothing: Option[String] = None

for {
  elem <- just
  i <- f(elem)
  j <- g(i)
  h <- h(j)
} yield h      // None

for {
  elem <- just
  i <- f(elem)
  j <- h(i)
} yield j      // Some(1001.1)

Monad

例によって規則もある.

  • return x >>= f = f x

    • return してから >>= した結果は, return せずに関数適用した結果と一致
    • 左恒等性
  • m >>= return = m

    • return>>= しても変わらない
    • 右恒等性
  • (m >>= f) >>= g = m >>= (\x -> f x >>= g)

refs

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!