関数がファンクターである、とは

Haskell をお勉強中。
『すごいHaskellたのしく学ぼう!』をつらつらと読んでいる。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

ファンクター、アプリカティブファンクター、モナド、あたりの説明を読んでいるものの、「関数 ((->) r) は、ファンクターで、アプリカティブファンクターで、モナドである」というあたりの説明が全くピンと来なかったので、解説を咀嚼する目的で自分なりにまとめてみる。
とりあえず、関数がファンクターである、ということについて。

そもそも (->) って何

(->) は型コンストラクタ。

Prelude> :k (->)
(->) :: * -> * -> *

具体型を2つ取って、具体型を1つ返す型コンストラクタ、ということになる。
返される方の具体型は関数である。
例えば、「Integer 型と Integer 型を取って、Intger -> Integer 型を返す」という動作になる。

の右側は、2引数関数の宣言のように見えるが、これは型の世界の記述なので意味が全く違う…と。まぎらわしい…。


(->) r は、これの部分適用版ということになる。
そのまんま (->) r に :k してもエラーになるが、r の部分を適当な型にしてやると :k できる。

Prelude> :k ((->) Char)
((->) Char) :: * -> *

これは具体型を1つ取って、具体型を1つ返す型コンストラクタ、ということになる。
返される方の具体型は関数である。
例えば、「Integer型を取って、Char -> Integer 型を返す」となる。


先ほどの2引数のバージョン( -> )では、任意の型Xと任意の型Yを取って、XからYへの関数型を返す、という動作だったが、
1引数のバージョン((->) r)では、最初の型が既に指定されているので、任意の型Xを取って、rからXへの関数型を返す、という動作になる。


これが、ファンクターであるそうなのである。

ファンクターとはなんぞや

ファンクターとはなんぞや。
GHCドキュメントの記述。

The Functor class is used for types that can be mapped over.


『すごい』の説明。

Functor は、全体を写せる(mapover)ものの型クラスです。


Functor 型クラスの実装。

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

これまたまぎらわしいが、"f" は、普通の関数ではなく、型コンストラクタである。


『すごい』の説明。

どうやら fmap は、「ある型 a から別の型 b への関数」と、「ある型 a に適用されたファンクター値」を取り、「別の型 b の方に適用されたファンクター値」を返す関数のようです。

…「ファンクター値」という概念が急に出てきた。
この時点では、「Functor は、fmap 関数が適用可能な型クラスで、fmap 関数はファンクター値に関数を適用してファンクター値を返す」みたいな説明になっているのだが、「だからファンクター値って何だよ!」ってなる(翻訳したことでわかりにくくなっている?)。
「ファンクター値」というのは、Functor 型クラスの型の値、ということになるはず。上記の「fmap を適用した時に〜」という性質を満たす型の型クラスが Functor で、その値を「ファンクター値」と呼ぶ、と理解すれば良さそうである。


「ファンクター値」と言っているからアレだが、コードをそのまま読めば、「型コンストラクタに具体型が適用されてできた具体型の値」である。
この、型コンストラクタの部分が fmap の結果失われない、ということが、Functor の性質ということのようである。


例えば、

    fmap :: (a -> b) -> f a -> b

    fmap :: (a -> b) -> f a -> g b

という動作の場合は、型コンストラクタ f の部分が消えてしまっているので、Functor ではない、ということになる。


ファンクターとしての (->) r

fmap の宣言はこうだが、

    fmap :: (a -> b) -> f a -> f b

(->) r をこれに当てはめるとこうなる。

    fmap :: (a -> b) -> (r -> a) -> (r -> b)


『すごい』の説明。

この型は、a から b への関数と、 r から a への関数を引数に取り、r から b への関数を返す、と読めます。何か思い出しませんか? そう! 関数合成です!

確かに! と言いたいところだが、狐につままれたような話である。


リストや Maybe がファンクターである、ということを説明していたあたりで、

ファンクターになれるのは、箱のような働きをする型です。リストというのは、空だったり、ものがいくつか入っていたりする箱だとみなせますね。

とあったわけだが、関数に「箱のような」のイメージを適用しようとすると混乱する。


『すごい』では、ファンクターについての説明を更新し、以下としている。

他のファンクターと同様、関数ファンクターも文脈を持った値だとみなせます。 Maybe との対比で考えてみましょう。 Maybe ファンクターは、「値があるかもしれない」という文脈でした。それに対し、関数ファンクター (->) r (あるいは r -> )は、「 r 型の入力を適用すれば結果が返ってくる」という文脈を表し、それを具体化した (->) r a (あるいは r -> a )は、「 r 型の入力を適用すれば a 型の結果が返ってくる」という文脈を表しています。

…と、いうことで、この後延々と出てくる「文脈」という語が出てくる。


で、この、関数ファンクターの「 r 型の入力を適用すれば結果が返ってくる」という「文脈」は、つまりはどういうことか。
『すごい』では、fmap (*3) (+100) を例にしていたが、Int -> Int で型がそもそも変わらないので、ちと分かりにくい。
型の変換が起こる関数で、ということで、length 関数を使ってみる。


length 関数は、リストを取って整数値を返す。

Prelude Control.Monad.Instances> :t length
length :: [a] -> Int


で、この length 関数が、ファンクター型クラスの型の値だとして、fmap を適用してみる。
とりあえず、2倍にする関数。

Prelude Control.Monad.Instances> :t fmap (*2) length
fmap (*2) length :: [a] -> Int
Prelude Control.Monad.Instances> fmap (*2) length $ "sugoi"
10


次は、文字列とつなげる関数。

Prelude Control.Monad.Instances> :t fmap (\x -> "Length : " ++ (show x)) length
fmap (\x -> "Length : " ++ (show x)) length :: [a] -> [Char]
Prelude Control.Monad.Instances> fmap (\x -> "Length : " ++ (show x)) length $ "sugoi"
"Length : 5"


「 r 型の入力を適用すれば結果が返ってくる」と言われる通り、fmap の結果得られる関数が、[a] 型に適用可能である点は、どんな関数を fmap の第1引数に指定した場合も変わらない。


これは、 fmap の適用を何度行なっても維持される。

Prelude Control.Monad.Instances> (fmap (\x -> "Length : " ++ (show x)) $ fmap (*2) length) $ "sugoi"
"Length : 10"

これにより、元の関数 f について、任意の関数を fmap で適用して関数 g を得た場合、f が適用可能な型に対しては、g も適用可能である、ということになる(ただし、fmap で適用した関数が適切に型を扱う場合)。


つまり、ファンクターとしての関数においては、維持される「文脈」は、「どの型に適用可能か」ということ。
で、関数は、関数合成できる、という性質を持つ故に、Functor とみなせる、ということであろう。

リストがファンクターであるということについての再考

『すごい』では、関数がファンクターであることの前に、リスト、 Maybe をファンクターの例として挙げている。
リスト、Maybe のどちらも、「元の値」を取り出すことができるので、「箱」という比喩からコンテナ的なイメージを持ってしまいそうになる。

Prelude> [10]
[10]
Prelude> head [10]
10
Prelude Data.Maybe> Just 10
Just 10
Prelude Data.Maybe> fromJust (Just 10)
10

だが、関数の例で見た通り、「文脈」というのは値が維持されるとかそういうことでなく、もっと広い意味で見るもののようである。
そこで、リストがファンクターである、ということについて再考してみる。


リストとはそもそも何か。
『すごい』の「再帰的なデータ構造」の節での説明。

[5] というリストで考えてみましょう。これは実は 5:[] の構文糖衣です。

リストは、「空リスト」または「要素とリスト(空でもよい)を : で結合したもの」のいずれかの値を取るデータ構造です。

: という2引数関数を、5 と [] に適用すると [5] になる、ということである。
つまり、リストは、関数適用を連鎖的に行ったもの、とみなせる。

Prelude> :t (:[])
(:[]) :: a -> [a]
Prelude> :t (:1:2:3:[])
(:1:2:3:[]) :: Num a => a -> [a]
Prelude> (:1:2:3:[]) $ 0
[0,1,2,3]


リストに対して fmap を適用した場合の動作は以下である。

Prelude Control.Monad.Instances> fmap (*2) [0,1,2,3]
[0,2,4,6]

これはこう書き直せる。

Prelude Control.Monad.Instances> fmap (*2) (0:1:2:3:[])
(0:2:4:6:[])

さらに、関数適用に注目すれば、こうなる、はず。

Prelude Control.Monad.Instances> fmap (*2) (0:(1:(2:(3:[]))))
(0:(2:(4:(6:[]))))

これは、fmap によって、「 : 関数の適用」という「文脈」を維持しつつ、関数が適用された、と見ることができる。


リストの場合と比較する形で、関数に fmap を適用するケース(=関数合成)を書いてみるとこんな感じになるはずである。(実際には表示できないので手で捏造したもの)

Prelude Control.Monad.Instances> fmap (*2) ((+2) . (*4))
(*2) . ((+2) . (*4))


この対比から、意味付けを整理してみる。


・ リスト
 「リストにつなげる」という行為を再帰的に適用可能で、かつその行為の結果が構造として定着し、構造を残したままで値として扱えるもの。
  また、この構造を維持したまま、関数による操作を適用できるもの。


・ 関数
 「関数として適用する」という行為を再帰的に適用可能で、かつその行為の結果が構造として定着し、構造を残したままで値として扱えるもの。
  (また、その得られた値がすなわち関数なので、別の値に適用できる)


おそらく…こんな感じでいいのではないか。
「適用すること」が構造として定着し、特定の操作(fmap)が行われる限りはそれが維持されること、を「文脈」と呼ぶとすれば、イメージがつかめるような気がしてきた。
※ ただ、「リストにする」行為は普通の関数適用として扱えるが、「リストに関数を適用する」行為とは意味が変わってくるので、もうちょっと上手く整理するべきであるような気もする。


Haskell のプログラム全体を、複数の小さな関数が一定のルールに従って関数合成されることでできた1個の関数、として見る場合に、構造 or 文脈を保ちつつ関数を足したり引いたりできる、という性質が使われていると意識できるのかなー、というところ。


一旦ここまで。