前回の記事では、Functorについて軽く触れました。
今回はApplicativeについて言及する前に、Functorのfmapの定義だけでは実現ができない計算があることを見てみます。これを知ることで、Applicativeの有用性がより分かるようになると思います。
Applicativeについてはまた別の記事で書こうと思います。
言語は以前と同様Haskellを用います。
Applicativeに触れる前にFunctorについて、軽くおさらいしておきましょう。
Functorクラスは以下のようにfmapという関数が定義されているのでした。
Functorのおさらい
fmapについて
class Functor f where
fmap :: (a -> b) -> f a -> f b
これにより、「a型の値を引数にとりb型の値を返す関数」をファンクター値である「f a」に写し、ファンクター値「f b」を得ることができるのでした。
例を見てみます。
-- 型が「Int -> Int」である関数fを定義する
Prelude> let f = (+1) :: Int -> Int
-- fmapにfを適用した結果得られる関数の型を表示
Prelude> :t fmap f
--「Functor f => f Int -> f Int」を型にもつ関数が得られる。ここでfはFunctorである
fmap f :: Functor f => f Int -> f Int
-- fmapを使用することにより、関数fをFunctor値である「Just 4 :: Maybe Int」に写すことができる。
Prelude> fmap f $ (Just 4 :: Maybe Int)
Just 5 -- Just
上記で使用した関数fは「Int型の値を引数にとり、Int型の値を返す1引数の関数」でした。
fmapを多引数関数に適用することはできる?
ところで、多引数関数でファンクター値を写すことはできるのでしょうか。
はい、できます。
以下はカインドの話が絡むので、興味がない方はカインドのくだりは読み飛ばしても構いません。
カインドについて説明すると話が長くなってしまうので、これは別の機会で触れることにします。
ここで、改めてfmapの型を見てみましょう。
fmap :: (a -> b) -> f a -> f b
「fmap :: (a -> b) -> f a -> f b」の型aおよび型bのカインドは「*」です。
関数の型コンストラクタ(->)自体のカインドは「* -> * -> *」ですが、
「Int -> Int」のように具体的な型の値が(->)に適用された結果の型のカインドは「*」です。
ざっくり説明すると、「b = (x -> y)」(x, yのカインドは「*」とする)
と置くと、「a -> b」は「(a -> (x -> y))」と置き換えられます。
ここで、型aのカインドは「*」…①
また、x, yのカインドは「*」であるから、具体的な型であるx, yを(->)に対して適用した「(x -> y)」の結果の型のカインドも「*」…②
①、②より「a -> (x -> y)」の結果の型のカインドは「*」。
したがって、多引数関数も適用することができると言うことになります。
fmapに多引数関数を適用するとどうなる?
ここでは、fmapを用いて、多引数の関数 (*)をMaybeのファンクター値に写した結果を見てみます。
:t (*) --関数(*)の型は Num a => a -> a -> a。ここでaはNumクラスのインスタンス。
(*) :: Num a => a -> a -> a
-- 「Just 5」の「5」が(*)に部分適用され、その結果は(Just (5*))と同値となる。
Prelude> :t fmap (*) (Just 5)
fmap (*) (Just 5) :: Num a => Maybe (a -> a) -- Maybeの文脈に(a -> a)という「関数」が入っている
-- (Just (5*))の型を上記の結果と見比べてみる。
Prelude> :t (Just (5*))
(Just (5*)) :: Num a => Maybe (a -> a) --fmap (*) (Just 5)と同一の型
上記の結果得られた値の型は「Num a => Maybe (a -> a)」です。
この値は「(Just (5*)))」と同値です。
「(Just (5*)))」は、
「関数(*)に型a(aはNumクラスのインスタンス)の値「5」が部分適用された結果の関数「(*) 5」がMaybeの箱(文脈)に入っている」
と解釈できます。
さて、関数(*)の主たる目的は、「Numクラス制約をもつ同一の型同士の値の積」を得ることでしょう。
Maybeの箱に入っている「(Just (5*))」に対して、同じくMaybeの箱に入っている「Just 3」を適用し、「Just (5 * 3) = Just 15」のような計算をしたくなる場面はしばしば発生するでしょう。
以下のようにfmapを適用して、これを実現することはできるのでしょうか。
fmap (Just (5*)) (Just 3)
結論から言うと、これはできません。
ghciで上記を実行した結果を見てみましょう。
Prelude> fmap (Just (5*)) (Just 3)
<interactive>:37:7: error:
• Couldn't match expected type ‘Integer -> b’
with actual type ‘Maybe (Integer -> Integer)’
• Possible cause: ‘Just’ is applied to too many arguments
In the first argument of ‘fmap’, namely ‘(Just (5 *))’
In the expression: fmap (Just (5 *)) (Just 1)
In an equation for ‘it’: it = fmap (Just (5 *)) (Just 1)
• Relevant bindings include
it :: Maybe b (bound at <interactive>:37:1)
fmapの型は「(a -> b) -> f a -> f b」です。
ここで、「(a -> b)」の型aと「f a」の型aは同一の型でなければなりません。
上記の例では、「(a -> b)」の部分に、「Integer -> b」の型の値が適用されなければなりませんが、「Maybe (Integer -> Integer)」型の値を適用しているためにエラーが起きています。
まとめ
fmapにより、多引数関数をファンクター値に写したとき、
fmap :: (a -> b) -> f a -> f b
の「f b」のbの部分には関数が入ります。
(例)Maybe (Int -> Int)
※上記の例では、Maybeがfにあたり、(Int -> Int)がbにあたる。
ファンクター値の中身の関数を、別のファンクター値に写そうとしたとき、fmapの定義ではその計算が実現ができません。
では、どうすれば良いのでしょう。
これを解決するためには、以下の型定義をもつ関数が必要です。
(欲しい関数) :: f (a -> b) -> f a -> f b
そして、まさしくこのような関数を定義しているのがApplicativeなのです。
Applicativeであれば、Functorでできなかった上記の計算ができるのです。
長くなってしまいましたので、続きはまた今度。