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

bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

関手ー圏の圏

プログラマーのための圏論 (圏論とHaskell)

6.4 圏の圏

圏と圏とをつなぐ射は関手という特別な名前がついているが射であることに変わりはない。従って、二つの関手を合成することもできるはずだ。

図のように、三つの圏\({\mathcal C}\),\({\mathcal D}\),\({\mathcal E}\)を考えてみよう。\({\mathcal C}\)から\({\mathcal D}\)へは関手\(F\)で、\({\mathcal D}\)から\({\mathcal E}\)へは関手\(G\)で写像されているとする。そして、関手の合成\(G \circ F\)が存在するとしよう。
f:id:bitterharvest:20170221160425p:plain

また、\({\mathcal C}\),\({\mathcal D}\),\({\mathcal E}\)には自身を自身に写像する恒等な関手\(id_{\mathcal C}\),\(id_{\mathcal D}\),\(id_{\mathcal E}\)があるとしよう。

さらに結合律、単位律が成り立っているとする。

このような関手を射とし圏を対象にすると、次に示すように圏の条件を満たしていることが分かる。

1) 対象\({\mathcal C}\),\({\mathcal D}\),\({\mathcal E}\)を有する。
2) 射\(F,G\)を有する。
3) 射はドメインとコドメインを有する。即ち、\(F: {\mathcal C} \rightarrow {\mathcal D},G: {\mathcal D} \rightarrow {\mathcal E}\)
4) すべての対象に対して恒等射\(id_{\mathcal C}\),\(id_{\mathcal D}\),\(id_{\mathcal E}\)が存在する。
5) コドメインドメインが一致している射は合成でき、それも射となる。即ち、\(G \circ F\)

前回の記事で、リストを定義した。そこでは、リストはConsが何度も繰り返し現れ、リストの要素が見にくかった。Haskellでは構成要素とその順番がすぐにわかるように、リストを[a,b,c,… ]と表している。これは次のように定義されている。

data [ ] a = [ ] | a : [a]
instance Functor [ ] where
  fmap :: (a -> b) -> [a] -> [b]

このリストに対しては、最初の要素を取り出すheadと最初の要素を取り除いたリストを返すtailという関数が用意されている。

これを利用してみよう。

Prelude> let a = [1,2,3]
Prelude> head a 
1
Prelude> tail a
[2,3]
Prelude> tail . tail $ a
[3]
Prelude> tail . tail . tail $ a
[]
Prelude> tail . tail . tail . tail $ a
*** Exception: Prelude.tail: empty list
Prelude> head . tail . tail $ a
3
Prelude> head . tail . tail . tail $ a
*** Exception: Prelude.head: empty list

空のリストにheadとtailを施すとエラーとなる。これを避けるために、次の関数を用意しよう。

safeTail :: [a] -> Maybe [a]
safeTail [] = Nothing
safeTail (x:xs) = Just xs

safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x

この関数は、関手[ ]の後に、関手Maybeを施したもので、関手を結合したものである。
それでは利用してみよう。

*Main> let a = [1,2,3]
*Main> tail a 
[2,3]
*Main> tail .tail $ a 
[3]
*Main> tail .tail . tail $ a 
[]
*Main> tail .tail . tail . tail $ a 
*** Exception: Prelude.tail: empty list
*Main> safeTail .tail . tail . tail $ a 
Nothing
*Main> head a
1
*Main> head . tail $ a
2
*Main> head . tail . tail $ a
3
*Main> head . tail . tail . tail $ a
*** Exception: Prelude.head: empty list
*Main> safeHead . tail . tail . tail $ a
Nothing
*Main> 

\({\mathcal C}\)の射\(f\)は\(F(f)\)で\({\mathcal D}\)の射\(g\)に写像されているとする。さらに、\(g\)は\(G(g)\)で\({\mathcal E}\)の射\(h\)に写像されているものとする。

この時、\(G \ circ F (f) = G (F (f))\)となる。

それでは、Haskellでのfmapはどのようになるであろうか。下図を参考にしながら求めてみよう。
f:id:bitterharvest:20170221160859p:plain

g=fmap f
h=fmap g

より、

h=fmap (fmap f)
 = (fmap . fmap) f

となる。

それでは、少し実行してみよう。fを(+3)にして確認する。

Prelude> f = (*3)
Prelude> h = (fmap .fmap) f
Prelude> m = Just [1,2,3]
Prelude> h m
Just [3,6,9]

それでは、fを2乗する関数にして確認してみよう。

Prelude> f x = x * x
Prelude> h m
Just [3,6,9]
Prelude> h = (fmap . fmap) f
Prelude> m = Just [1,2,3]
Prelude> h m
Just [1,4,9]

次は双関手(Bifunctor)について述べる。