🔀

HaskellのFunctorが圏論の関手であるというのはどういうこと?

に公開3

最近圏論面白いと思っていて、人と話したり Gemini などに聞いたりしながら学んでいるので、その備忘録です。

一応 ChatGPT 及び Gemini に読んでもらっているのですが、浅学の身による理解なので間違ってるかもしれません。気になりましたらコメントお願いします。

また、この記事は Haskell をある程度触ったことがあることと高階関数とは何かを理解していることを前提として書いています。数学は前提としていないつもりですが分かりづらかったらすみません。

fmap

Haskellには fmap と呼ばれる関数がある。以下のように定義されている。

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

これは多くの言語にある配列の写像操作([1,2,3]->map(x -> x * 2) => [2,4,6]のような)を一般化したようなインターフェースに見え、実際そのように使える。先述した配列操作を記述するとこのようになる。

ghci> fmap (\n -> n * 2) [1,2,3]
[2,4,6]

fmap を使うには定義した型を Functor というクラスのインスタンスとして宣言する必要がある。Haskell の組み込み型では、リストや Maybe がそのように宣言されており、これらの型では fmap を使える。この Functor は圏論と呼ばれる数学の理論から持ち込まれた概念で、関手と言う。

手続き的なプログラミング言語に慣れていると、上記の操作は配列の例だと「配列の中身を一旦取り出して与えた関数を全ての値に適用して配列に詰め直す」と認識できるだろう。

しかし、この認識からは関手には辿り着けないと感じたため、自分なりの理解を書いておく。

圏論

まず、圏論とはなんぞやという所から解説する必要がある。

圏論は、数学的構造とその関係について抽象的に扱う理論で、ありとあらゆる概念を対象、対象と対象の関係である射、それらから成る体系である圏として扱う。

Haskellの例で言うと ab のような型は圏論の世界では対象、a -> b のような関数は圏論では対象と対象を結び付ける射として扱われる。そして Haskell の型と関数からなる集合を Hask 圏と言ったりする。

圏論において、関手というのはある圏 C とある圏 D の対応ないしは関係性と言われるものだが、概念が理解できていないと「は?」となることだろう。私も実際そうなった。

部分適用はいいよね

もう一度 Haskell の世界に戻ろう。Haskell では関数はカリー化されており常に1つの引数を取るように定義される。[1]

fmap はカリー化を前提とすると (a -> b) という型の引数を取り f a -> f b という型の返り値を返す関数と考えられる。この操作は型だけ見ると、生の値の世界(圏)の関数を対象 Functor の世界(圏)に持ち上げている(lift という概念としてあちこちに出てくる)と言える。[2]

この型だけ見るというのが重要で、まさに圏論の言う「構造と関係について抽象的に扱う」ということそのものである。

そして、持ち上げに関係のない ab の対応は f af b になっても破壊されていない。[3]

これは生の値の圏 C から 対象 Functor の圏 D への写像操作と見なせ、更に(圏論にとっての)本質でない生の値の圏と対象 Functor の圏、それから写像操作のことを省くと圏 C と圏 D の対応と言える。

まとめと感想

ここまでで Functor が関手を Haskell の世界に落とし込んだものということを分かって頂けたと思います。分かったなら嬉しいな。

wikibooksの圏論のページにある図が分かりやすかったので言葉じゃわからんって人はそれを見るのもいいかもしれないです。

普段プログラミングをしていると具象(値を取り出すだとか Maybe だとか map だとか)を気にしがちですが、圏論ではそういうのを一切気にせず本当に対象の関係性しか考えてないんだなというのを理解しました。

数学は難しい言葉で解説されることが多く、日本語が苦手な筆者には知らない国の言葉のようでした。しかし LLM の手助けを借りて平易な言葉に変換してもらいながら学んでいたら、普段扱っているものが理路整然とした美しい概念として記述されたものであることを理解しました。

別に Haskell でコードを書く上で圏論について理解しないといけないということは無いと思いますが、知っていると理解にあたり大幅な手助けになると思います。(実際 Hask 圏の構造は圏論の影響を強く受けている)綺麗な概念だと思いますし、興味が湧いたら学んでみてもらえると幸いです。

脚注
  1. なぜそんなことをしているかというと部分適用と関数合成を扱いやすくするためだと思われる ↩︎

  2. Hask圏で考えると同じ圏の中で構造を被せる自己関手に近いらしいのだけど今のとこよくわからんので後で調べます ↩︎

  3. より厳密に言うと恒等射及び合成において構造が保存されることを関手則として定義している ↩︎

GitHubで編集を提案

Discussion

kz6809kz6809

凄いH本で勉強した時に Functor は型クラスで fmap という関数を一つだけ持っていると書いてあっりました。単純に Functor のインスタンスな型に fmap を使えば Functor (関手) なんだと能天気に考えてにコードを書いていました。

圏論を少し齧り始めて少し経っての事ですが、ふと「Haskellのコードのどの部分が関手に相当するんだろうか?」と自分が実は良く理解できていない事に気が付いてしまいました。

書籍に当たってみると、マックレーン氏の「圏論入門」には「関手を圏のモルフィズム」と書かれており雪田修一氏の「『圏論入門』Haskell で計算する具体例から」には「圏と圏との間の『準同型写像』」と説明されています。プログラミングで使う圏はほぼ間違いなく自己関手圏なので Functor と呼ばれているものが実は Endofunctor だそうです。その為書籍で説明されている関手とは少し性格が異なっている様です。

考え始めてみると、自分には以下の Functor コードのどこに「自分自身へのモルフィズム」や「自身への順同型写像」に相当する記述が有るのかが全く分かりませんでした。

fmap (+1) [0 .. 4] -- => [1,2,3,4,5]
fmap (+1) (Just 2) -- => Just 3

[0 .. 4] や Just 2 は対象ですし (+1) は射でしか有りません。そこで、関手の説明の中関数を関手を使って写像を作ると

f : A -> B から F(f) : F(A) -> F(B) という写像が出来る

という説明が有ったのを思い出し (+1) から F(+1) の写像作成が関手の作用によって引き起こされると考えました。ですが何がそれを引き起こしているか全く分かりませんでした。

最終的に ChatGPT に質問をし、(+1) をリフティングしているのは [] と Maybe だという回答を得ました。この2つの kind を調べると * -> * と成っています。Functor で在る為には少なくとも 1 つ以上の -> が必要な事を考えると Functor のインスタンス定義を行い fmap の定義を行った型コンストラクタが関手に相当するという事が理解できます。

こうしてみると自分が如何に何も理解せずにコードを書いていたかを痛感させられました。Functor 意外と手強かったです。(過去形で書いて良いのか不安です)