Arantium Maestum

プログラミング、囲碁、読書の話題

Haskell私的メモ FunctorとApplicativeをリストで探る

モナドとはなにか」「Applicativeとはなにか」といった抽象的な共通性の探索からはじめると挫折するのが目に見えているので、いったんそういったことは置いておいて、まずはいくつかの具体的なモナドと戯れて個別に挙動を理解していきたい。そういった具体的な理解からだんだん抽象化が自然と見えてくればしめたものである。

ということで、今回は一番お世話になっている「リストモナド」をFunctor、ApplicativeそしてMonadとして使ってみて理解してみたい。(今回はFunctorとApplicative、次回でMonad

Functorとしてのリスト

HaskellでFunctorとは以下のように定義されている:

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

(実際には他にも$>が定義されているが、fmapを使ったデフォルト定義が存在するので割愛)

さらに、暗黙の了解としてfmapが以下の法則を満たすことが求められる:

fmap id = id
fmap a . fmap b = fmap (a . b)

リストはFunctorとして定義されているし、fmap = mapでFunctorの法則を満たしている。

map id [1,2,3]
= [id 1, id 2, id 3]
= [1, 2, 3]
= id [1, 2, 3]

fmap a $ fmap b [1,2,3]
= fmap a [b 1, b 2, b 3]
= [a (b 1), a (b 2), a (b 3)]
= [(a . b) 1, (a . b) 2, (a . b) 3]
= fmap (a . b) [1,2,3]

実はfmap(<$>)という同義の二項演算子が存在する。これは(+1) <$> [1..5] = [2,3,4,5,6]のように使える。

Applicativeとしてのリスト

ApplicativeというのはFunctorとMonadの中間にある概念で、Haskellに明確に組み込まれるのは他の二つより遅かったらしい。Applicative Functor with Effectsという論文でApplicativeの概念としての有用性が懇切に説かれている。

今回はひたすらリストをApplicativeとしていじるだけにとどめておく。

Applicativeは

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

が主な機能となる。(実際には他にも<**>があるが・・・)

pure

pureは簡単で、ある値をApplicativeのコンテキストに入れる。

x :: [Int]
x = 5

などとすると当然型が合わずエラーになるが、

x :: [Int]
x = pure 5

とすると5がリストというコンテキストに入る、つまりリストの要素となり、x = [5]となる。

<*>

<*>は同一のコンテキストに入っている関数と引数をつないで、結果を同じコンテキストに入れて返す。ごく簡単な例を挙げると:

[(+1)] <*> [1] = [2]

となる。リストに入った関数をリストに入った引数に適用して、リストに入った結果を返す。

引数部分が増えると:

[(+1)] <*> [1..3] = [2,3,4]

となる。これは

(+1) <$> [1..3] = [2,3,4]

と非常に似ている。pureを使って:

pure (+1) <*> [1..3] = [2,3,4]

とするとさらに似ているかもしれない。しかし、ApplicativeがFunctorより強力な理由として、引数が2以上の関数でも簡単に使えるという点がある。

pure (+) <*> [1..3] <*> [10, 20] = [11, 21, 12, 22, 13, 23]

と、引数リストの直積に関数が適用される。

f a b c = a + b + c
pure f <*> [1..2] <*> [1..3] <*> [5,6] = [7, 8, 8, 9, 9, 10, 8, 9, 9, 10, 10, 11]

と、引数が増えても対応できる。理屈としては、一回<*>が使われるごとに:

pure (+) <*> [1..3] = [(1+), (2+), (3+)]

のようにcurryingが起きている。

pure (+) <*> [1..3] <*> [1..3]より(+) <$> [1..3] <*> [1..3]のほうが短いので、実際には後者を使う方が楽。

個人的には複数リストに対する関数適用だと、直積よりもzipWithの拡張版のような挙動が欲しい気がしている。そっちについてもいろいろ調べていて、その話題についてはまた別の記事で書きたい。

次回の記事ではリストをMonadとしていろいろいじってみる。