6.3 一般化
\(Maybe\)を利用して関手を説明したが、その他にもいろいろな関手を考えることができる。それらは(\(Maybe\)で説明した)\(fmap\)の実装が異なるだけだ。Javaを知っている人であればインターフェースを用いて実装したらと考えるだろう。Haskellにもオブジェクト指向と同じ概念のものが用意されている。Haskellでは、関手はクラスで一般化し、個々の実体はそのインスタンスで表現できる。
それでは実際に示してみよう。次のようになる。
Class Functor f where fmap :: (a -> b) -> (f a -> f b)
上記のプログラムで、\(f\)が関手である。Haskellでは型コンストラクタとなる。
なお、GHCでの定義を見ると次のようになっている。
Prelude> :i Functor class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b
\(f \ a\)と\(f \ b\)がカッコでくくられていないが、これをカーリー化すると\( (f \ a \rightarrow f \ b)\)になるので、表現は異なるが内容は同じである。
これを用いると、\(Maybe\)はクラスをインスタンス化し、次のように実装できる。
data Maybe a = Nothing | Just a deriving (Eq, Show, Read) instance Functor Maybe where fmap _ Nothing = Nothing fmap f x = Just (f x)
これまで、\(fmap \ f \ Nothing = Nothing\)と書いてきたが、どのような\(f\)が来たとしても\(Nothing\)を出力するので、ここでは\(f\)の代わりに\(\_\)を用いた。
プログラムを実行してみよう(実装されている\(Maybe\)とぶつかるようであれば、\(Maybe’, Just’, Nothing’\)のように’をつけて実装し、実行して欲しい)。
*Main> fmap (+3) (Just 5) Just 8 *Main> fmap (+3) Nothing Nothing
それではリストはどうであろうか。\(Maybe\)を参考にしながら実装しよう。
data List a = Nil | Cons a (List a) deriving (Eq, Show, Read) instance Functor List where fmap _ Nil = Nil fmap f (Cons h t) = Cons (f h) (fmap f t)
上のプログラムで、\(Cons \ h \ t\)と書いたが、\(h\)はリストの最初の要素、\(t\)はこれを取り除いたリストである。
プログラムを実行してみよう。
*Main> fmap (+3) (Cons 3 (Cons 4 (Cons 5 Nil))) Cons 6 (Cons 7 (Cons 8 Nil))
Haskellが用意している\(Functor\)のインスタンスを調べてみよう(厳密にはGHCが用意しているインスタンス)。
Prelude> :i Functor class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b (<$) :: a -> f b -> f a {-# MINIMAL fmap #-} -- Defined in ・eGHC.Base’ instance Functor (Either a) -- Defined in ‘Data.Either’ instance Functor [] -- Defined in ‘GHC.Base’ instance Functor Maybe -- Defined in ‘GHC.Base’ instance Functor IO -- Defined in ‘GHC.Base’ instance Functor ((->) r) -- Defined in ‘GHC.Base’ instance Functor ((,) a) -- Defined in ‘GHC.Base’
上記に示すような結果を得たことと思う。
ところで下から2行目に見慣れないものがある。\(((\rightarrow) \ r)\)なのだが、これはリーダーと呼ばれる。それではこのリーダーについて説明しよう。
Haskellでは、関数は\(a \rightarrow b\)と記述される。\(a\)が入力で\(b\)が出力である。ところで、\( \rightarrow \)を\(+\)や\(\times\)などと同じように中置関数と考えると、これを前置関数に変えることで\( (\rightarrow) \ a \ b \)と表すことができる。
そこで、\( (\rightarrow) \ a \ b \)は、\(a\)を\(b\)に変換してくれる型コンストラクタと解釈することが可能となる。従って、
type Reader a = (->) a
と表すことが可能となる。
あるいは、両辺から\(a\)を取り除いて
type Reader = (->)
と表しても構わない。これで、\( (\rightarrow) \)は型コンストラクタとなった。ここでは、それを\(Reader\)というシノニムで置き換えている。
ところで、次の図を考えよう。
図で左側から右側への関手は\(Reader \ r\)である。左側にある関数\(f\)でそのドメインは\(a\)でありコドメインは\(b\)である。今、これら\(a,b\)を、右側に示すように、\(r\)をドメインにし、そのコドメインに持ち上げてしまおう。\(a\)を\(g:r \rightarrow a\)に写像する関数は、\(Reader \ r \ a\)と表すことができる。
それでは、\(b\)を\(r \rightarrow b\)に移す関数はどうなるであろうか。\(b\)を関手\(Reader \ r\)を用いて、\(r \rightarrow b\)に写像する。そして、\(r \rightarrow b\)は射である。関手を射に写像するときは\(fmap\)を用いていた。そこで、\(fmap \ f \ g = fmap \ f \ (Reader \ r \ a) \)と考えられる。
これでよさそうなのだが、何となくすっきりとしない。そこで、Haskellでの可換図式ではなく、圏論での可換図式で表してみることにしよう。
上記の可換図式で、\({\rm Hom}(R,A)\)は、対象\(R\)から\(A\)への射の集合である。
次に、\(R\)から\(B\)への写像を考えてみよう。これは、\(\mathcal {D}\)のところの可換図式を用いればよい。(上側の)\(R\)から\(A\)を経由して\(B\)にいたる射の集合を求めると\({\rm Hom}(R,A) \times {\rm Hom}(A,B)\)となる。同じように、(上側の)\(R\)から(下側の)\(R\)を経由して\(B\)にいたる射の集合を求めると\({\rm Hom}(R,R) \times {\rm Hom}(R,B)={\rm Hom}(R,B)\)となる。これが先に求めたものと等しいので、\({\rm Hom}(R,B)={\rm Hom}(R,A) \times {\rm Hom}(A,B)\)となる。
従って、\(fmap \ f \ (Reader \ r \ a) \)でよいことが判明した。
そこで、\(fmap\)のリーダーに対する実装は次のようになる。
type Reader = (->) instance Functor (Reader r) where fmap :: (a -> b) -> (r -> a) -> (r -> b) fmap f g = f . g
ここで、f.g は(.) f gと表すことができ左右の辺からf,gを消去すると最終的には次のようになる。
type Reader = (->) instance Functor (Reader r) where fmap :: (a -> b) -> (r -> a) -> (r -> b) fmap= ( . )
上記のプログラムをロードすると\(Use \ TypeSynonymInstances\)とエラーが出る。そこで、これを用いて再度ロードするとファンクタが二重定義であるというエラーが出る。これは、Haskellが\( (\rightarrow)\)のファンクタを用意しているためである。従って、Haskellがあらかじめ用意しているものをここでは用いる。Haskellではどのようになっているかをまず、見てみよう。データ型の定義は次のようになっている。
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\footnotesize, frame=single, breaklines=true]
Prelude> :i (->)
data (->) t1 t2 -- Defined in ‘GHC.Prim’
\end{lstlisting}
しかし、GHC.Primを参照してもこのデータ型の定義は見当たらない。ファイクコードだそうだ。これは表示のためだけに使われているそうである。
しかし、ファンクタの方はちゃんと定義されている。\(Data.Functor\)を見ると次のようになっている。
instance Functor ((->) r) where fmap = (.)
システムが用意しているものを実際に使うとこんな感じになる。あまり面白くはない。
Prelude> ((+3) . (*4)) 5 23 Prelude> ((+3) . (+4)) 6 13
次回は、圏から圏を作ることを説明する。