LoginSignup
4
2

More than 3 years have passed since last update.

関数型プログラミング -Applicativeについて知る前に、Functorでできないことを知っておこう-

Posted at

前回の記事では、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の型
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でできなかった上記の計算ができるのです。
長くなってしまいましたので、続きはまた今度。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2