Haskellのファンクター、アプリカティブファンクター、ちょっとだけモナドについて

それぞれの関係性を意識しながら、今の理解をまとめてみました。

ファンクター

ファンクターとは、関数で写せる値を表す便利な概念のことです。 ファンクターに所属する型は、「a -> b型の関数と、f aというデータ型をとって、f bというデータ型を返す」というふるまいを持ちます。

これだけだとなかなか理解しづらいので、Functor型クラスの実装と、ファンクターに所属するMaybeの実装から、理解を試みてみました。

まず、Functor型クラスの実装です。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

ここから、任意の型をFunctor型クラスに所属させるためには、fmapという関数を実装する必要があることが分かります。 そのほかの制約は特に設けられていないので、適切にfmapを実装することができればその型はFunctor型クラスに所属していると言えそうです(「適切に」とは、Functor則に則っているという意味ですが、長くなってしまうので今回は割愛します)

それでは、fmapのふるまいはどのようなものになるのでしょうか? 型注釈から挙動を確認すると、fmapは、a -> b型の関数とf a型の値を引数にとり、f b型の値を返すことが分かります。言い換えれば、関数をファンクター値の中の値に適用し、その結果をファンクター値として返すという挙動をとる、ということです。

次に、ファンクターに所属するMaybeの実装を確認してみます。

instance Functor Maybe where
  fmap f (Just x) = Just (f x)
  fmap f Nothing  = Nothing

引数がJust xの場合は、関数fをファンクター値の中身であるxに適用してファンクター値を返し、引数がNothingの場合は、そのままNothingを返すことが分かります。つまりどちらの場合も、中身は値かもしれないし何も入っていないかもしれない、というMaybeの文脈を保存したまま、関数適用の結果を得ることができていることが分かります。

アプリカティブファンクター

アプリカティブファンクターは、ファンクターの拡張です。そのため、ある型がアプリカティブファンクター型クラスに属するためには、必然的にファンクター型クラスに属している必要があります。

アプリカティブファンクターでは、ファンクターの中に入っている関数を取り出して、ファンクターの中に入っている値に適用することができます。 この説明を初めて読んだとき、「ファンクターの中に関数を入れなければこんなもの必要ないのではないか?」と思ったのですが、複数の引数を取る関数をfmapでファンクター値に適用する場合を考えると、必要性が理解できます。

> :t fmap (*) (Just 3)
fmap (*) (Just 3) :: Num a => Maybe (a -> a)

2引数関数(*)がファンクターの中の値に部分適用された結果、返り値のファンクターの中には、Num型をとってNum型を返す関数が入っています。 ここで、ファンクター値の中の関数を取り出して、ファンクター値の中の値に適用することができれば便利そうです。(そして、これこそがアプリカティブファンクターの関心ごとです) 通常のファンクターは、「通常の関数で」「ファンクターの中の値を写すこと」しかできないため、「ファンクターの中の関数で」「ファンクターの中の値を写すこと」ができるアプリカティブファンクターが必要になってきます。

必要性を確認したところで、アプリカティブファンクターの実装を見てみます。

class (Functor f) => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

アプリカティブファンクターとなるために必要な関数は2つです。 - pure - 任意の型の引数を受け取り、それをアプリカティブ値の中に入れて返します。(値を引数にとり、その値を何らかのデフォルトの文脈に置くために利用されます) - <*> - 関数の入っているファンクター値と値の入っているファンクター値を引数にとって、1つ目のファンクターの中身である関数を2つ目のファンクターの中身である値に適用します。(まさにアプリカティブファンクターの関心そのものですね)

上記2つの関数を、Maybeでは以下のように実装しています。(Maybeはファンクターであり、アプリカティブファンクターでもあります) ファンクター値の中の関数を取り出して、fmapでファンクター値を写していることが分かります。

instance Applicative Maybe where
  pure = Just
  Nothing <*> _ = Nothing
  (Just f) <*> something = fmap f something

また、アプリカティブファンクターは、「ファンクターの中に入っている関数を取り出して、ファンクターの中に入っている値に適用することができる」という特性から、1つの関数で、複数のファンクター値を続けざまに写すことができます。

つまりこういうこと。

-- `<*>`によって関数の部分適用を行うことによって、(うまく書けば)幾つでもアプリカティブ値をチェインすることができる
> pure (+) <*> Just 3 <*> Just 5
Just 8

-- 順を追って見ていく(前提として、<*>は左結合)
-- この段階で、ファンクターの中に関数をもつファンクターができる
> :t (pure (+) <*> Just 3)
(pure (+) <*> Just 3) :: Num a => Maybe (a -> a)

-- 上でできた、関数をくるむファンクターが、ファンクターの中の値に適用される
> Just ((+) 3) <*> Just 5
Just 8

また、<*>によって関数の部分適用を行うことによって(うまく書けば)幾つでもアプリカティブ値をチェインできることがアプリカティブの嬉しさなので、そのための頻出テクニックをまとめておきます。

まず、以下は等しいです。

-- 通常の関数fをアプリカティブの中に入れ、そのアプリカティブ値の中の関数を、アプリカティブ値xの中の値に適用して、アプリカティブ値を返す
pure f <*> x
-- 通常の関数fを、アプリカティブ値xの中の値に適用して、アプリカティブ値を返す
fmap f x

つまり、こうできます。

-- こう書く代わりに
pure f <*> x <*> y <*> ...

-- こう書ける
fmap f x <*> y <*> ...

上記パターンはしょっちゅう使うので、fmapと等価な中値演算子<$>が用意されており

(<$>) :: (Functor f) => (a -> b) -> f a -> f b
f <$> x = fmap f x

つまり、こう書けます。

-- 以下は全て等価
pure f <*> x <*> y <*> ...

fmap f x <*> y <*> ...

f <$> x <*> y <*> ...

アプリカティブファンクターは、文脈のついた値に、文脈を保ったまま、普通の関数を適用させてくれる、と表現することもできます。

// (*)は普通の乗算関数, Just 2, Just 8は文脈のついた値
> (*) <$> Just 2 <*> Just 8
> 16

> (++) <$> Just "exdeath" <*> Nothing
> Nothing

> (-) <$> [3, 4] <*> [1, 2, 3]
> [2,1,0,3,2,1]

モナド

モナドは、「普通の値aを取って文脈付きの値を返す関数に、文脈付きの値m aを渡したい」という願いを叶えるための、アプリカティブ値の自然な拡張です。 言い換えると、「a -> m b型の関数を、m a型の値に適用したい」というのが、モナドの関心ごとです。

--- これがモナドの関心ごと
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

つまり、モナドでは>>=演算子を追加し、「モナド値」と「普通の値を取る関数」を引数にとり、なんとかしてその関数をモナド値に適用してモナド値を得るように実装する必要があります。

モナドを使うと、>>=が関数を値に適用するにあたって、値が持っている文脈を保存できるようになります。 これによって、巨大で分岐の多いコードを>>=によるモナド適用の連鎖で書くことができるため、コードが簡潔になることがモナドの嬉しさの一つのようですが、まだまだ理解できてないので別の機会に詳しくまとめたいと思っています、、、

やっていこう

参考

https://www.amazon.co.jp/dp/B009RO80XY https://www.lambdanote.com/products/haskell