前回までのあらすじ📖
「入門Haskellプログラミング」を読み進めながら、理解を深めるためにElixir
を使ってHaskell
のFunctor
とApplicative
を再現してみました。
Functor
は上手く再現出来ましたが、Applicative
はデフォルトで部分適応されるというHaskell
の仕様がなければ、完全再現することが出来ず、カリー化した関数で近しいものを再現しました。
Functor
とApplicative
を用いることでがコンテナ、コンテキスト内の値(例: リスト)に対して以下の操作をすることが出来ます。
- 引数1つの関数をコンテナ、コンテキスト内の値に対して適応する(
Functor : fmap
) - コンテナ、コンテキスト内の部分適応された関数をコンテナ、コンテキスト内の値に対して適応する(
Applicative : app
)- 引数2つ以上の関数を適応することが出来る
- コンテナ、コンテキスト外の値をコンテナ、コンテキスト内に格納する(
Applicative : pure
)
さて、これだけの操作がコンテナ、コンテキスト内に対して可能になりましたが、現状ではまだ出来ないことがあります。その問題を解決するためにMonad
が必要なのです。
Monadとは🤔
Haskell
ではMonad
はFunctor
やApplicative
と同様にクラス(型)として定義されており、関数を定義しています。
よく言われるカタカナ表記の「モナド」とはHaskell
においてはMonad
クラス(型)のインスタンスに持つクラス(型)の総称です。試しにMaybe
クラス(型)についての情報を見てみると、Monad
をインスタンスに持つ事が分かります。Maybe
クラス(型)はモナドの1種です。
*Main> :info Maybe type Maybe :: * -> * data Maybe a = Nothing | Just a -- Defined in ‘GHC.Maybe’ instance Applicative Maybe -- Defined in ‘GHC.Base’ instance Eq a => Eq (Maybe a) -- Defined in ‘GHC.Maybe’ instance Functor Maybe -- Defined in ‘GHC.Base’ :
またFunctor
はApplicative
のスーパークラスであり、Applicative
はMonad
のスーパークラスです。関係性としてはFunctor < Applicative < Monad
となっています。
class Applicative m => Monad m where... class Functor f => Applicative f where... class Functor f where...
Monadが解決する問題
Funtor
とApplicative
では対応出来ない問題は「関数を適応した結果がコンテナ、コンテキスト内の値となる場合」です。言葉で説明するのも理解するのも難しいので、まずはMonad
の定義を見てみます。
class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a
Monad
は3つの関数を定義していますが、今回は簡単のため>>=
(bind
)についてのみ説明をします。 bind
関数の定義を見てみると以下のようになっています。
Monad
クラス(型)の値(m a
)を第1引数で受け取る- 第2引数では第1引数で受け取ったコンテナ、コンテキスト内の値を取り出して受け取り、新たなコンテナ、コンテキスト内の値を返す関数(
(a -> m b)
)を受け取る - 第2引数の関数の実行結果(
m b
)を返す
関数を適応した結果がコンテナ、コンテキスト内の値となる場合
(a -> m b)
からも明らかですが、m a -> (a -> m b) -> m b
という定義が上記の通りになっています。
コードサンプル
次にbind
を用いた具体的なコードを見てみます。Haskell
をあまり知らなくても理解出来るようにList
を用います。
copyInt :: Int -> [Int] copyInt n = [n, n]
copyInt
は受け取った数値を2つList
に格納して返すシンプルな関数です。
*Main> copyInt 1 [1,1] *Main> copyInt 2 [2,2]
この関数を1から5の要素を持つList
に適応させるためにbind
関数を使用します。
(bind
はList
から値を順に取り出して関数を適応させていきます)
applyBind = [1 .. 5] >>= copyInt -- [1,1,2,2,3,3,4,4,5,5]
[[1,1], [2,2] ... [5,5]]
という戻り値を想定していましたが、[1,1,2,2...5,5]
という想定外の戻り値が得られました。自分もかなり驚いたのですが、Haskell
ではList
に対してのbind
は以下のように定義されています。
instance Monad [] where m >>= f = concatMap f m
*Main> :info concatMap concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
Foldable
クラス(型)はList
をインスタンスに持つので、concatMap
は使用可能です。
*Main> concatMap copyInt [1 .. 5] [1,1,2,2,3,3,4,4,5,5]
関数を適応した各要素にconcat
関数を適応するのがconcatMap
です。
*Main> concat [[1,2],[2,2],[3,3],[4,4],[5,5]] [1,2,2,2,3,3,4,4,5,5]
戻り値の謎が解けたところで、applyBind = [1 .. 5] >>= copyInt
について考えてみます。bind
の定義にあったように第1引数の値をコンテナ、コンテキスト内から取り出して、第2引数の関数へ受け渡し(今回はcopyInt
)、その戻り値であるMonad
クラス(型)(List
)を受け取っています。
さらに戻り値がMonad
クラス(型)であるため、bind
を連続で呼び出すことも可能です。Monad
のbind
の真の力は連結することで発揮されます。
*Main> [1, 2] >>= copyInt >>= copyInt [1,1,1,1,2,2,2,2]
*Main> [1, 2] >>= copyInt >>= copyInt >>= copyInt [1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2]
繰り返しコンテナ、コンテキスト内に対して処理を適応出来るというのは非常にパワフルです。
いざElixirで作ってみる🧪
前回のApplicative
とは異なり、今回は言語の制約を受けなくて済んだのであっさりと実装出来ました。
defmodule Monad do def return(v), do: [v] def bind(lst, func) do Enum.map(lst, func) |> List.flatten() end end defmodule Func do def copyInt(n), do: [n, n] end
実態はconcatMap
の実装をElixir
に書き換えただけです。
concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
さっそく実行してみます。
[1,2,3,4,5] |> Monad.bind(fn n -> Func.copyInt(n) end) |> IO.inspect() # [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
同じように連結することも出来ます。
[1,2,3,4,5] |> Monad.bind(fn n -> Func.copyInt(n) end) |> Monad.bind(fn n -> Func.copyInt(n) end) |> IO.inspect() # [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5]
無事に再現出来ました🎉
まとめ📖
Monad
はFunctor
やApplicative
では解決できない問題を解決するために必要- 問題: 適応する関数が
Monad
(コンテナ、コンテキスト内の値)を返す場合に適応できない
- 問題: 適応する関数が
bind
はm a -> (a -> m b) -> m b
というように定義され、上記の問題を解決するための関数であるHaskell
ではList
に対してbind
をFoldable t => (a -> [b]) -> t a -> [b]
と定義しているbind
はMonad
クラス(型)を返すため連結することが可能で非常にパワフルである
ようやく「入門Haskellプログラミング / UNIT5 コンテキストでの型の操作」にて取り扱われているFunctor
, Applicative
, Monad
の章が終了しました。読み始める前はMonad
について全く分かっていなかった、分かったつもりになっていましたが、何とか誰かに説明出来るようになりました。
数学全然わからない自分でも読み進められる、「入門Haskellプログラミング」は非常におすすめです。少々、分厚いのが難です😓