読者です 読者をやめる 読者になる 読者になる

Pseudo Engineer

ソフトウェアの話とか書いてくよ

Haskell道 その5

前回はファンクターでした。
今回はアプリカティブファンクターです。

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

ファンクターは文脈を持った値を入れる箱でした。値は整数、浮動小数点数、文字列などです。ところでHaskellは関数がファーストクラスでした。つまり関数を値のように扱えるということです。となると、ファンクターに関数を入れてもいいのでは? 答えはYes。ファンクターに関数も入れられるようになったのがアプリカティブファンクターです。
言い換えると、アプリカティブファンクターは文脈を持った関数も入れられる箱です。なお、アプリカティブファンクターでは長くて呼びづらいので単にアプリカティブと呼んだりします。

アプリカティブに値を適用

箱の中に関数があるのですから、関数に値を適用したいですよね。それが <*> です。
試しにMaybeのアプリカティブに値を適用してみましょう。

Prelude> let a = Just (+2)
Prelude> a <*> Just 3
Just 5

「2を足すかもしれない」ものに対して「3かもしれない」ものを適用すれば、「5かもしれない」ものになりました。ここでもMaybeの文脈が維持されていますね。

pure と <*>

ここでアプリカティブの定義を見てみます。

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

f (a -> b) -> f a という定義からアプリカティブに値を適用するには、同じ文脈に入れたファンクター値で与えなければなりません。上述のMaybeの例だと、a に適用するのに Just 3 でわざわざファンクター値にしていました。ここで a はアプリカティブであれば何でもいいような抽象的なコードの場合、具体的な型がわかりません。そこで pure の登場です。pure :: a -> f a の定義の通り、与えられた値を文脈にいれて返すだけです。
Maybeの例をpure で書きなおしてみましょう。

Prelude> let a = Just (+2)
Prelude> a <*> pure 3
Just 5

pureの活用方法は他にもあります。例えばアプリカティブではない関数にファンクター値を適用する場合です。まずは fmap でやってみます。

Prelude> fmap (+) (Just 3) <*> Just 4
Just 7

それならば、最初から(+)をアプリカティブにして <*> を連鎖させて部分適用していっても良さそうです。

Prelude> pure (+) <*> Just 3 <*> Just 4
Just 7

これでも十分そうですが便利な中置演算子 <$> を利用することもできます。

Prelude> (+) <$> Just 3 <*> Just 4
Just 7

<$> の実体は fmap のエイリアスなので、ファンクターがアプリカティブを実装していなくても使うことはできます。

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

Prelude> (+3) <$> Just 3
Just 6

アプリカティブ則

第一法則「idのアプリカティブで写してもファンクター値は同じである」
pure id <*> v = v
第二法則「アプリカティブの連鎖でファンクター値を写したものと、アプリカティブで関数合成してからファンクター値を写したものは同じである」
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
第三法則「文脈に入れてから関数に適用したものと、関数に適用してから文脈に入れたものは同じである」
pure f <*> pure x = pure (f x)
第四法則「アプリカティブでファンクター値を写したものと、ファンクター値を$演算子で関数化してアプリカティブで写してものは同じである」
u <*> pure y = pure ($ y) <*> u

まとめると、ファンクター則と同様に与えられた関数や値を無視したり、関数適用以外の余計なことはしないということですね。箱はあくまで箱であり、入れ物にすぎないということです。



補足:
$演算子は優先度の低い演算子であるため引数を右結合させたい場合(かっこを減らしたい場合)に有用という記述をわりと見かけますが、それだけでは少々説明不足です。

($) :: (a -> b) -> a -> b
f $ x = f x

$は関数本体と引数を受け取り引数を関数本体に適用する優先度の低い演算子です。これは関数適用を右結合させる以外のもう1つの活用方法として、関数が引数を待つのではなく、引数が関数を待つ関数を作ることができます。

Prelude> let f = \x -> x + 1
Prelude> :info f
f :: Num a => a -> a
Prelude> let p = ($ 1)
Prelude> :info p
p :: Num a => (a -> b) -> b 
Prelude> p f
2

$は演算子なため中置記法(f $ x)が可能でした。なので($ 1)とすれば右引数を部分適用した関数が作れます。なお、すごいHaskellたのしく学ぼう!の5.6章にも詳しい解説があります。