関数がファンクターである、とは
Haskell をお勉強中。
『すごいHaskellたのしく学ぼう!』をつらつらと読んでいる。
- 作者: MiranLipovaca
- 出版社/メーカー: オーム社
- 発売日: 2017/07/14
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
ファンクター、アプリカティブファンクター、モナド、あたりの説明を読んでいるものの、「関数 ((->) 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 文脈を保ちつつ関数を足したり引いたりできる、という性質が使われていると意識できるのかなー、というところ。
一旦ここまで。