Functor(関手)ってなんですか?
どうもこんばんは、south37です。今日も引き続きHaskellネタでいこうと思います。Functorって概念を知ったので、ちょろっとまとめてみたいと思います。
そもそも、Functorってなんですか?
さて、Functorって聞いて、ピンと来ますか?
僕は、全然ピンと来ませんでした。Wikipedia先生の定義を見ても、
関手(かんしゅ、functor)とは、圏の間の対応付けのことである。関手は対象関数と射関数の組からなる。
などと、何だかよくわからない感じで書かれています。
じゃあ一体なんなんだって話ですが、ポイントはどうやら「対応付け」って点のようです。「圏(と呼ばれるよく分からないモノ)」から「圏」への対応付けをしてくれるのが、Functorです。そうすると、今度は「圏」がなんなんだって話になる訳ですが、これもWikipedia先生によれば
数学における圏(けん、category)とは数学的構造とその変形を取り扱うための枠組みであり、数学的対象をあらわす「対象」とそれらの間の関係を表す「射」の集まりによって与えられる。
などと説明されています。じゃあ、その「対象」とか「射」って何やねんって話になる訳ですが、これ...キリが無いですね?一応、「圏」の例を挙げておくと(これもWikipedia先生のコピペですが)、
Set: 集合を「対象」とし、集合間の写像を「射」とする。
のように、「対象」と「射」をまとめたものが「圏」であると説明されています。「関手(Functor)」は、この「圏」から「圏」への対応付けを行います。
さて、ここまでゴニョゴニョ述べてきた訳ですが、圏や関手(Functor)について全然分からなかったと思います。それもそのはずで、これらは究めて抽象的な概念である為、抽象的なふわふわした話をされても全然イメージがつきません。
そこで、次からはいよいよ、HaskellにおいてFunctorがどういった形で実装されているのかを見ていきたいと思います。
HaskellにおけるFunctor
Haskellにおいては、FunctorはFunctor型クラスとして実装されています。「型クラス」が何なのか、覚えているでしょうか?
そう、型クラスは、型の振る舞いを指定する為の仕組みでしたね。具体的には、特定の型クラスに属する型は、特定の関数が定義されている事が保証されています。Functor型クラスにおいては、fmapと呼ばれる関数が定義されている事が保証されます。
class Functor f where fmap :: (a -> b) -> f a -> f b
これが、Functor型クラスの実装です。Haskellの文法では、class 型クラス名 型名 where 定義されるべき関数と書く事で、型クラスの実装を表します。上記のコードは、「Functorであるfは(a -> b) -> f a -> f bという型を持つfmapと呼ばれる関数を持つ」と解釈されます。
ここで一つ、注意点があります。それは、上記の例で出て来たfは、実際の値の型となる「具体型」ではなく、型引数を一つとる「型コンストラクタ」であると言う事です。
例えば、ListやMaybeがその一例です。
[1, 2, 3] -- [Int] 型。 [] Int 型とも表記可能。 Just 'say' -- Mayby String 型。 Nothing -- Maybe a 型。aは任意の型。
リストは何のリストであるかによって違う型を持つため、その中身に応じて[] (中身) 型を持ちます。Maybe aは値としてNothingも表せる型であり、やはり中身の値の型に応じてMaybe (中身) 型という異なった型を持ちます。このように、様々な中身に応じた型を作る為の仕組みが、「型コンストラクタ」です。
では、fmapという関数はなんなんでしょうか?実は、リストにおいてはこれはmap関数そのものです。すなわち、
map (*3) [1, 2, 3] --> [3, 6, 9]
のように、リストの個々の要素に対して関数を適用し、結果をリストにして返すような関数mapです。
ここで、Functorやfmapを一度忘れて、mapの型を見てみましょう。例えば、Intのリストに対して適用されるmapは、引数として(*3)のような「Intを受け取ってIntを返すような関数」を受け取る場合には、
map :: (Int -> Int) -> [] Int -> [] Int
のような型を持つ事が分かります。すなわち、(*3) :: (Int -> Int)を第一引数に、[1, 2, 3] :: [] Intを第二引数にとって、[3, 6, 9] :: [] Intを返すような関数という事です。
(ちなみに、値 :: 型という表記は、Hakellにおいてよく使われる文法です。)
mapの型を、Functor型クラスの定義で出てきたfmapの型と見比べてみましょう。a,bをInt、fを[]で置き換えれば同じ型になる事が分かるでしょうか?これこそ、fmapがmapと(リストにおいては)同一であるという何よりの証拠です。
さて、fmapがmapという形で実装されている事から、リストはFunctor型クラスであるという事が分かりました。では、リストにおいては、冒頭で述べていた、Functorの「圏」から「圏」への対応付けという性質はどのように現れているのでしょうか?
Haskellにおける「圏」
この疑問に答える為に、Haskellにおいて何を「圏」として捉えたら良いのか考えてみましょう。実は、しばしば「Haskellの型」を「対象」とし、「Haskellの関数」を「射」とするようなHaskと呼ばれる「圏」が、議論の対象とされます。すなわち、IntやCharなどの型を「対象」とし、f :: Int -> Stringのような関数を射として捉えた「圏」という事です。
また、Haskの部分圏であるLstと呼ばれる圏を考える事も出来ます。これは、Haskellにおける全ての型ではなく「リスト型」を「対象」とし、「リスト型」から「リスト型」への関数を「射」とするような「圏」です。すなわち、[Int]や[Char]が「対象」で、f :: [Int] -> [Bool]などが「射」であるような圏です。
勘の良い人はお気づきかもですが、[]という型コンストラクタは、HaskとLstの「対象」を対応付ける事が出来ます。すなわち、Haskに属するどんな「対象(型)」であっても、[]という型コンストラクタを適用させる事で[a]のようなリスト型になる為、Haskの「対象」となる訳です。
では、「射」の対応付けはどうしたら良いでしょうか?実は、ここでfmapが活躍します。リストにおけるfmapの型は、
fmap :: (a -> b) -> [a] -> [b]
でした。これは、「関数とリストを受け取ってリストを返す関数」と見なす事も出来ますが、「関数を受け取って、『リストを引数にとってリストを返す関数』を返す関数」とみなす事も出来ます。すなわち、Haskの射であるf :: a -> bをfmapに適用させる事で、(fmap f) :: [a] -> [b]というLstの射を作る事が出来る訳です。HaskからLstへの対応付けを完全に行える事が分かりました。
この意味で、リストの型コンストラクタである[]はFunctorです。また、fmapが定義されていれば「射」の対応付けを行える為、他の型コンストラクタもFunctorとなれる事が分かります。その意味で、fmapが定義されていればFunctor型クラスに属する事が出来る訳です。
まとめ
Functorは概念はややこしいけど、実装はシンプル。Haskellは抽象的な概念を綺麗に実装に落とし込んでてすごい。
注意点とか
途中で型コンストラクタである[]をFunctorと呼びましたが、「圏」同士の対応付けにはfmapの存在がかかせない為、型コンストラクタをFunctorと呼ぶのは本来は正しく無いと思います。型コンストラクタ、fmapをひっくるめた「対応付け」そのものがFunctorです。それが、Haskellにおいては型コンストラクタとfmapという形で表現されているだけだと思います。
あと、本当はFunctorと呼ぶ為には「Functor則」というものを満たさなければならないそうです。ちょろっとだけでも念頭に置いておいてもらえると嬉しいです。
おすすめの文献
Functor型クラスの話については、「すごいHaskell楽しく学ぼう」という本にもっと詳しくそして面白く載っています。抽象的な話をするにも必ず具体例を欠かさず、また楽しく読める工夫が随所にちりばめられている為、かなりオススメです。
また、HaskellにおけるFunctor型クラスの実装と、圏論における関手(Functor)という概念の対応については、このHaskell/圏論というページが大変参考になりました。

- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (52件) を見る