やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

数学全然わからないけどElixirでMonadを作ってみる

前回までのあらすじ📖

「入門Haskellプログラミング」を読み進めながら、理解を深めるためにElixirを使ってHaskellFunctorApplicativeを再現してみました。

www.okb-shelf.work

www.okb-shelf.work

Functorは上手く再現出来ましたが、Applicativeはデフォルトで部分適応されるというHaskellの仕様がなければ、完全再現することが出来ず、カリー化した関数で近しいものを再現しました。

FunctorApplicativeを用いることでがコンテナ、コンテキスト内の値(例: リスト)に対して以下の操作をすることが出来ます。

  • 引数1つの関数をコンテナ、コンテキスト内の値に対して適応する(Functor : fmap)
  • コンテナ、コンテキスト内の部分適応された関数をコンテナ、コンテキスト内の値に対して適応する(Applicative : app)
    • 引数2つ以上の関数を適応することが出来る
  • コンテナ、コンテキスト外の値をコンテナ、コンテキスト内に格納する(Applicative : pure)


さて、これだけの操作がコンテナ、コンテキスト内に対して可能になりましたが、現状ではまだ出来ないことがあります。その問題を解決するためにMonadが必要なのです。

Monadとは🤔

HaskellではMonadFunctorApplicativeと同様にクラス(型)として定義されており、関数を定義しています。

よく言われるカタカナ表記の「モナド」とは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’
:

qiita.com

またFunctorApplicativeスーパークラスであり、ApplicativeMonadスーパークラスです。関係性としてはFunctor < Applicative < Monadとなっています。

class Applicative m => Monad m where...
class Functor f => Applicative f where...
class Functor f where...

Monadが解決する問題

FuntorApplicativeでは対応出来ない問題は「関数を適応した結果がコンテナ、コンテキスト内の値となる場合」です。言葉で説明するのも理解するのも難しいので、まずは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関数を使用します。
(bindListから値を順に取り出して関数を適応させていきます)

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]

wiki.haskell.org

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を連続で呼び出すことも可能です。Monadbindの真の力は連結することで発揮されます。

*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]

無事に再現出来ました🎉

まとめ📖

  • MonadFunctorApplicativeでは解決できない問題を解決するために必要
    • 問題: 適応する関数がMonad(コンテナ、コンテキスト内の値)を返す場合に適応できない
  • bindm a -> (a -> m b) -> m bというように定義され、上記の問題を解決するための関数である
  • HaskellではListに対してbindFoldable t => (a -> [b]) -> t a -> [b]と定義している
  • bindMonadクラス(型)を返すため連結することが可能で非常にパワフルである

ようやく「入門Haskellプログラミング / UNIT5 コンテキストでの型の操作」にて取り扱われているFunctor, Applicative, Monadの章が終了しました。読み始める前はMonadについて全く分かっていなかった、分かったつもりになっていましたが、何とか誰かに説明出来るようになりました。

数学全然わからない自分でも読み進められる、「入門Haskellプログラミング」は非常におすすめです。少々、分厚いのが難です😓