読者です 読者をやめる 読者になる 読者になる

Functor(関手)ってなんですか?

Haskell

どうもこんばんは、south37です。今日も引き続きHaskellネタでいこうと思います。Functorって概念を知ったので、ちょろっとまとめてみたいと思います。

そもそも、Functorってなんですか?

さて、Functorって聞いて、ピンと来ますか?

僕は、全然ピンと来ませんでした。Wikipedia先生の定義を見ても、

関手(かんしゅ、functor)とは、圏の間の対応付けのことである。関手は対象関数と射関数の組からなる。

などと、何だかよくわからない感じで書かれています。

じゃあ一体なんなんだって話ですが、ポイントはどうやら「対応付け」って点のようです。「圏(と呼ばれるよく分からないモノ)」から「圏」への対応付けをしてくれるのが、Functorです。そうすると、今度は「圏」がなんなんだって話になる訳ですが、これもWikipedia先生によれば

数学における圏(けん、category)とは数学的構造とその変形を取り扱うための枠組みであり、数学的対象をあらわす「対象」とそれらの間の関係を表す「射」の集まりによって与えられる。

などと説明されています。じゃあ、その「対象」とか「射」って何やねんって話になる訳ですが、これ...キリが無いですね?一応、「圏」の例を挙げておくと(これもWikipedia先生のコピペですが)、

Set: 集合を「対象」とし、集合間の写像を「射」とする。

Grp: 群を「対象」とし、群の準同型を「射」とする。

のように、「対象」と「射」をまとめたものが「圏」であると説明されています。「関手(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は、実際の値の型となる「具体型」ではなく、型引数を一つとる「型コンストラクタ」であると言う事です。

例えば、ListMaybeがその一例です。

[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です。

ここで、Functorfmapを一度忘れて、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,bIntf[]で置き換えれば同じ型になる事が分かるでしょうか?これこそ、fmapmapと(リストにおいては)同一であるという何よりの証拠です。

さて、fmapmapという形で実装されている事から、リストはFunctor型クラスであるという事が分かりました。では、リストにおいては、冒頭で述べていた、Functorの「圏」から「圏」への対応付けという性質はどのように現れているのでしょうか?

Haskellにおける「圏」

この疑問に答える為に、Haskellにおいて何を「圏」として捉えたら良いのか考えてみましょう。実は、しばしば「Haskellの型」を「対象」とし、「Haskellの関数」を「射」とするようなHaskと呼ばれる「圏」が、議論の対象とされます。すなわち、IntCharなどの型を「対象」とし、f :: Int -> Stringのような関数をとして捉えた「圏」という事です。

また、Haskの部分圏であるLstと呼ばれる圏を考える事も出来ます。これは、Haskellにおける全ての型ではなく「リスト型」を「対象」とし、「リスト型」から「リスト型」への関数を「射」とするような「圏」です。すなわち、[Int][Char]が「対象」で、f :: [Int] -> [Bool]などが「射」であるような圏です。

勘の良い人はお気づきかもですが、[]という型コンストラクタは、HaskLstの「対象」を対応付ける事が出来ます。すなわち、Haskに属するどんな「対象(型)」であっても、[]という型コンストラクタを適用させる事で[a]のようなリスト型になる為、Haskの「対象」となる訳です。

では、「射」の対応付けはどうしたら良いでしょうか?実は、ここでfmapが活躍します。リストにおけるfmapの型は、

fmap :: (a -> b) -> [a] -> [b]

でした。これは、「関数とリストを受け取ってリストを返す関数」と見なす事も出来ますが、「関数を受け取って、『リストを引数にとってリストを返す関数』を返す関数」とみなす事も出来ます。すなわち、Haskの射であるf :: a -> bfmapに適用させる事で、(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/圏論というページが大変参考になりました。

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

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