Chapter 28

7.2 コンテナとしての関手

さのたけと
さのたけと
2021.05.31に更新

これまで,プログラミング言語における関手の例を見てきた.それらは汎用のコンテナとして定義されているか,なにがしかの値を持つオブジェクト(データ)で,その値の型がパラメータ化されているようなものであった. それに対し,データでなく関数をつくる reader 関手はすこし特殊な例にみえる.しかし考えてみれば,純粋な関数はメモ化できるので,関数の実行はルックアップテーブルに置き換えられる.そしてテーブルはデータなのだから,純粋関数をデータと見ることもできるのだ.逆に,Haskell は遅延評価なので,リストのような古くからあるコンテナは(データだが)実際には関数として実装されることもある.たとえば自然数の無限長のリストは次のように短く定義できる.

nats :: [Integer]
nats = [1..]

一行目の大かっこはリストの型コンストラクタで, Haskell にもともとあるものだ.二行目の大かっこは,リストのリテラルを書くのに使われている.このような無限長リストは明らかにメモリに入らないので,コンパイラは必要に応じて, Integer を生成する関数として nats を実装する.Haskell はデータとコードの境界をうまい具合にぼやかしているのだ.リストを関数と思ってもよいし,関数引数と返り値を紐づけるテーブルだ,と思ってもよい.関数の定義域が有限でかつそれほど大きくない場合には,テーブルと見るほうが実用的な一方,strlen をテーブルで実装しようとするのは,とんでもない種類の文字列に対応したテーブルを作ることになるので実用的でない.プログラマとしては無限なんて見たくもないが,圏論では無限なんて朝飯前だ.すべての文字列の集合だろうが,過去現在未来でこの世界がとりうる状態すべてのコレクションだろうが扱うことができるのだ! だから筆者は(自己関手によって生成されるようなタイプの)関手に包まれたオブジェクト[1] を考えるのは楽しい.オブジェクトは,単なる値でもいいし,パラメータをとる型の値でもいいし,そもそもその値が実際には存在しなくてもいいのだ.ひとつの関手の例は C++ の std::future で,これはある時点で値を持っているかもしれないが,それが保証されているわけではないし,値にアクセスしようとしたら他のスレッドの実行が終わるのを待たされるかもしれない.別の例としては,Haskellの IO オブジェクトがある.これはユーザーからの入力を保持するかも知れないし,モニターに "Hello World!" と表示される未来を保持するかもしれない.この解釈においては,関手に包まれたオブジェクトは,値か,パラメータをとる型の値かもしれない. あるいは一連の値を生成するレシピを保持するのかもしれない.値にアクセスできるかどうか,というのは関手の概念の外側の話で,あくまでオマケである.興味があるのは,関手に包まれた値を関数で操作できるかどうかだ.もし値にアクセスできるならば,得られるのは関数を作用させた結果でなければならない.値にアクセスできないのであれば,操作によって射が正しく合成され,恒等射が変化しないか,という点にのみ気を付ければよい.「値にアクセスできるか」に本当に興味がないということを見せるため,引数を完全に無視する型コンストラクタを見てみよう.

data Const c a = Const c

この型コンストラクタ Const は二つの型 c a を引数にとる.アロー型コンストラクタでやったように,部分適用によって関手を作ってみよう.データコンストラクタ(Const とも呼ばれる)は,型 c の値を一つだけとり, a は使わない.この型コンストラクタにおける fmap の型は次のようになる.

fmap :: (a -> b) -> Const c a -> Const c b

この関手 Const は型引数を無視するので,fmap の実装では,与えられた関数を無視する形になる.

instance Functor (Const c) where
  fmap _ (Const v) = Const v

これは C++ のほうがすこしわかりやすいかもしれない.(まさかこんなことを口にするとは思ってもみなかった!)C++ では,コンパイル時の引数の型と,実行時の値とを,より明確に区別できる.

template<class C, class A>
struct Const {
    Const(C v) : _v(v) {}
    C _v;
};

C++ での fmap の実装も,引数の関数を無視し, Const の引数を値を変えずに再キャストする.

template<class C, class A, class B>
Const<C, B> fmap(std::function<B(A)> f, Const<C, A> c) {
    return Const<C, B>{c._v};
}

Const 関手は奇妙なものに見えるかもしれないが, 多くのコンストラクタで重要な役割を果たす.圏論でいうと, Const 関手は前に述べた自己関手におけるブラックホール,つまり定関手 \mathbf{\Delta_c} の特殊な場合に相当する.もう少し詳しく見ていこう.

(和訳:@takase

脚注
  1. 原文では functor object ↩︎