今回は、関数型プログラミングにおけるApplicative Functorの有用性について軽く触れたいと思います。プログラミング言語は前回同様、Haskellを用います。
Functorでできない計算
前回の記事で見た通り、ファンクター値の中身の関数を、別のファンクター値に写そうとしたとき、fmapの定義ではその計算が実現できないのでした。※ここは前回の記事と一部内容が重複しているため、読み飛ばしても構いません。
例えば、以下のようなコードはfmapの型定義と矛盾が発生するため、コンパイルエラーとなります。
fmap (Just (5*)) (Just 3) --このコードはコンパイルエラーとなる
なぜ、上記のコードがエラーになるかを改めて振り返っておきます。
まず、Functorクラスのfmapの定義を改めて見てみましょう。
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmapの型定義に存在する「(a -> b)」のaと「f a」のaは同一の型でなければなりません。
上記のエラーとなるソースコードにおいて、
・「(a -> b)」に該当するのは、「Just (5*)」
・「f a」に該当するのは、「Just 3」
です。
「Just 3」の型は「Num a => Maybe a」ですので、「f a」のaの型は「Num a => a」となります。
したがって、「(a -> b)」に適用する型は「Num a => a -> b」でなければなりません。
上記のソースコードでは「(a -> b)」に対して、「Just (5*)」を適用していますが、
「Just (5*)」の型は「Num a => Maybe (a -> a)」です。
(Num a => a) と、 (Num a => Maybe (a -> a))は型が一致しないのでコンパイルエラーとなります。
Applicativeの登場
上記の計算を実現するのがApplicativeです。
前置きが長くなりましたが、Applicativeの定義(の一部)を見てみましょう。
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
上記のコードを一つずつ見ていきましょう。
Applicativeのクラス宣言部(1行目)
Applicativeのクラス宣言部には以下の記述があります。
class Functor f => Applicative f where
これは、Applicative型クラスはFunctor型クラスの型制約を持つことを示しています。
Applicative型クラスのインスタンスは、同時にFunctor型クラスのインスタンスでもあるので、文字通り「ApplicativeはFunctorよりもできることが多い(強力)」のです。
Applicative型クラスのインスタンスは、pureや(<*>)といったApplicative特有の関数が使用できるのはもちろんのこと、Functor型クラスで定義されているfmapも使用することができます。
Functorよりもできる処理が増えるわけですから、(Functorができなかったことを実現できるという意味では)これは嬉しいですね。
Applicativeの(<*>)について
pureの前に、(<\*>)の定義を見てみます。
(<*>) :: f (a -> b) -> f a -> f b
「<\*>」は関数が入っているFunctor値「f (a -> b)」をf aに適用し、f b型の値を返す関数です。これにより、「Functorでできない計算」で実現したかった計算ができるようになります。
--fmapの代わりに、<*>を適用する。
Prelude> (Just (5*)) <*> (Just 3) -- (<*>) (Just (5*)) (Just 3)と書いても同じ
Just 15 --コンパイルエラーにならず、適用できる
fmapで実現できなかった計算ができていますね。
ここで、次はpureを見てみましょう。
Applicativeのpureについて
pure :: a -> f a
pureは任意の型aの値を引数にとり、それをApplicativeの文脈に持ち上げます。
以下でpureの動きを少し見てみます。
Prelude> :t pure (1 :: Int) --pureをInt型の値「1」に適用する
pure (1 :: Int) :: Applicative f => f Int --Int型の値がApplicativeの文脈に持ち上げられる
上記のコードの「pure (1 :: Int)」の結果得られる値の型は「Applicative f => f Int」です。
この型変数fはApplicativeに対して多相です。
Prelude> :t (pure 1 :: Maybe Int)
(pure 1 :: Maybe Int) :: Maybe Int
Prelude> :t (pure 1 :: IO Int)
(pure 1 :: IO Int) :: IO Int
Haskellでは型推論が働きますので、pureを使って、ある値を適切なApplicative型クラスのインスタンスに持ち上げることができます。
--型推論が働くため、「pure (3 :: Int)」の型は「Maybe Int」に持ち上げられる
Prelude> Just (5*) <*> pure (3 :: Int)
Just 15
--型推論が働くため、「pure (3 :: Int)」の型は「Either a Int」に持ち上げられる
Prelude> Right (5*) <*> pure (3 :: Int)
Right 15
以上のように、pureを使って特定のApplicative型クラスのインスタンスに依存しないコードを書くことができます。上記の例では、文脈に応じて「pure 3」がMaybeアプリカティブと(Either a)アプリカティブに持ち上げられていますね。
Applicativeの嬉しいところ
(<\*>)を連続して適用し、複数のApplicative値を逐次写していくことができます。
fmapでは実現できなかった、多引数関数を任意のFunctor(Applicative)の文脈に持ち上げて計算を継続させるという操作が可能となります。
(<\*>)を複数回使用するケースを見てみましょう。今まで見て来たコード、
Just (5*) <*> pure (3 :: Int)
の、Maybeの文脈に入っている「(5*)」という関数は、「(*)という関数に、「5」という値が部分適用されていました。
プログラミングの過程において、関数(*)は、部分適用していない形で使用するケースの方が(多分)多いでしょう。要は、「Just (5*)」ではなく、裸の「(*)」をApplicative値に適用したい場面があるってことですね。
そんな時、Maybeの文脈で(<\*>)を使用したければ、以下のように書けば動きます。
Prelude> pure (*) <*> Just 5 <*> Just (3 :: Int) --pureを使って、(*)をApplicative(ここではMaybe)の文脈に持ち上げ
Just 15 --結果は、今まで見てきた例の「Just (5*) <*> pure (3 :: Int)」と同じになる
このコードが動く理由は、ghciを使って型の情報を見ると分かりやすいです。
Prelude> :t (*) --まず、(*)の型を見ておく。
(*) :: Num a => a -> a -> a --(*)は、Num型クラス制約をもつ型変数aを2つ取り、型変数aを返す関数
Prelude> :t pure (*) --pureで「(*)」をApplicativeの文脈にもちあげ
pure (*) :: (Applicative f, Num a) => f (a -> a -> a) --この段階では、特定のApplicativeに依存していない
Prelude> :t pure (*) <*> Just 5 -- Maybeの文脈で<*>を適用
pure (*) <*> Just 5 :: Num a => Maybe (a -> a) --Just 5の適用から、Maybeアプリカティブの文脈であることがわかるので、結果の値の型は「Num a => Maybe (a -> a)」となる
Prelude> :t pure (*) <*> Just 5 <*> Just (3 :: Int)
pure (*) <*> Just 5 <*> Just (3 :: Int) :: Maybe Int
ちなみに、
pure (*) <*> Just 5 <*> Just (3 :: Int)
は、以下のように書くこともできます。
(*) <$> Just 5 <*> Just (3 :: Int) -- <$>はfmapの中置演算子バージョン
これはhttps://en.wikibooks.org/wiki/Haskell/Applicative_functors#Applicative_functor_lawsに記載があるように、Applicativeは以下の法則を満たしているためです。
fmap f x = pure f <*> x -- f <$> x = pure f <*> x
また、(<\*>)の逐次適用によるコードの書き方を「Applicativeスタイル」と言ったりします。
これの利点については、山本先生によるApplicativeのススメという記事が非常に分かりやすいです。
まとめ
・Applicative型クラスのインスタンスはFunctor型クラス制約を持つので、fmapも使用できる(Functorよりもできることが多い)
・Functorでは実現できなかった、「Functor値の中身の関数を、別のFunctor値に写す」という計算ができる
・(<\*>)により、多引数関数が入ったApplicative(Functor)値と、別のApplicative(Functor)値を逐次適用することで、文脈(計算効果)を維持して処理を書ける。
・Applicativeスタイルの適用により、コードが簡潔・安全になる(これは、山本先生のApplicativeのススメが非常に分かりやすい)