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件) を見る