➡️

クライスリ圏とプログラムの関係

2024/06/24に公開

はじめに

これは何の記事?

本記事は、クライスリ圏という圏と、プログラムの関係を書いた記事になります。

主に内容は次になります。

  • 圏の定義
  • 定義の解説
  • プログラムとの関係

この記事では実際のプログラミング言語として個人的な嗜好で関数型言語PureScriptを用いますが、Haskellにも読み替えられるはずです。

クライスリ圏とは?の前に

では、まずクライスリ圏の定義を、、、といきたいところなのですが、いきなり圏の話をするよりは、プログラムにクライスリ圏の概念が登場する例を見たほうがよいかなと思います。

ということで圏の話の前に、応用の話を一部先取りして書いてしまいます。

>=>

HaskellやPureScriptなどには>=>という中置演算子があります。
見たことがある、あるいは使ったことがあるかもしれませんね。

関数の定義としては
forall a b c m. Bind m => (a -> m b) -> (b -> m c) -> a -> m c
のようになっています。

詳しくは後述しますが、これはクライスリ圏でいうと「射の合成」にあたります。

クライスリ圏の射はa -> m bという形をしているので、2つの射(a -> m b),(b -> m c)を合成するというわけです。

=<<

>>= (bind)の引数の順序を入れ替えた=<< (bindFlipped)もクライスリ圏と関係があります。

bind :: m a -> a -> m b -> m b
bindFlipped :: a -> m b -> m a -> m b

クライスリ圏を構成する要素として拡張演算子と呼ばれるものがあるのですが、a -> m bという射にこの拡張演算子を適用するとm a -> m bという射になります。
a -> m bを関数fと呼び、拡張演算子を*とするならf*はbindFlippedと一致します。
詳しくは後ほど説明しましょう。


二つ例を挙げましたが、bindなんかはかなり身近な関数ですよね。

というところで、そろそろ本格的にクライスリ圏の話に突入したいと思います。

クライスリ圏

まず、クライスリ圏ですが、定義の仕方がいくつか存在します。
例えばnLabに記載されている定義は、モナドを前提とした定義になっています。
この記事では、クライスリトリプルを用いた定義を使います。

【定義】クライスリトリプル

{\footnotesize \mathcal{C}}上のクライスリトリプルとは

  • 関手{\footnotesize T:\mathcal{C}\rightarrow\mathcal{C}}
  • 自然変換{\footnotesize \eta_{A}:A\rightarrow T(A)} ({\footnotesize A\in\mathrm{ob}(\mathcal{C})})
  • {\footnotesize f:A\rightarrow T(B)}に対して射{\footnotesize f^{*}:T(A)\rightarrow T(B)}を与える拡張演算子{\footnotesize -^{*}}

からなる3つ組{\footnotesize \langle T,\eta,-^{*}\rangle}で、以下を満たします。

  • {\footnotesize f^{*} \circ \eta _{A}=f} ただし {\footnotesize f:A\rightarrow T(B)}
  • {\footnotesize \eta {_{A}^{*}} =1_{T(A)}}
  • {\footnotesize g^{*} \circ f^{*} =\left( g^{*} \circ f\right)^{*}} ただし {\footnotesize f:A\rightarrow T(B)} かつ {\footnotesize g:B\rightarrow T(C)}

【解説】クライスリトリプル

自己関手{\footnotesize T}や、自然変換{\footnotesize \eta}についてですが、どこかで見たことはないでしょうか?
そうモナドの定義にも出てきたものです。
2つの定義を比べてみましょう。

モナドには自然変換{\footnotesize \mu}があり、クライスリトリプルには拡張演算子があるという違いがありますが似た定義になっていますね。
というかモナドとクライスリトリプルはお互いに構成可能なのですが、それは置いておいてモナドとの違いを生んでいる拡張演算子について説明します。

拡張演算子の定義の {\footnotesize -} の部分には任意の射が入ります。
拡張演算子が適用された射は、元の射のdomain {\footnotesize A}の部分を関手{\footnotesize T}が適用された{\footnotesize T(A)}に変えた射になります。

これを踏まえて、定義にあるクライスリトリプルの3つの条件を図解します。

  • {\footnotesize f^{*} \circ \eta _{A}=f}(ただし {\footnotesize f:A\rightarrow T(B)})
  • {\footnotesize \eta {_{A}^{*}} =1_{T(A)}}
  • {\footnotesize g^{*} \circ f^{*} =\left( g^{*} \circ f\right)^{*}}(ただし {\footnotesize f:A\rightarrow T(B)} かつ {\footnotesize g:B\rightarrow T(C)})

また、この拡張演算子において射のdomainが既に関手{\footnotesize T}が適用された{\footnotesize T(A)}だったらどうなるかというと、{\footnotesize T(T(A))}となります。
ということは

{\footnotesize 1_{T(A)}:T(A)\rightarrow T(A)}
に対して拡張演算子を適用させたら
{\footnotesize (1_{T(A)})^{*}:T(T(A))\rightarrow T(A)}
になりますね。
これまたどこかで見たことありませんか?
そう、モナドの3つ組の
{\footnotesize \mu:T^2\rightarrow T}
ですね。
ということはこれを利用してクライスリトリプルからモナドを構成することができそうですし、実際できます(趣旨から外れるため証明は省略します)。

【定義】クライスリ圏

{\footnotesize \mathcal{C}}上にクライスリトリプル{\footnotesize \langle T,\eta,-^{*}\rangle}が与えられているとします。
そして圏として成り立つための条件である対象・射・射の合成について次のように定めた圏{\footnotesize \mathcal{C_T}}を圏{\footnotesize \mathcal{C}}上のクライスリ圏と呼びます。

  • 【対象】 {\footnotesize \mathrm{ob}(\mathcal{C_T})=\mathrm{ob}(\mathcal{C})}
  • 【射】 {\footnotesize \mathrm{Hom}_{\mathcal{C_T}}(A,B)=\mathrm{Hom}_{\mathcal{C}}(A,T(B))}
  • 【射の合成】 {\footnotesize f\in \mathrm{Hom}_{\mathcal{C_T}}(A,B),\,\, g\in \mathrm{Hom}_{\mathcal{C_T}}(B,C)}の合成 {\footnotesize g\circ f:A\rightarrow C} は、
           {\footnotesize f\in \mathrm{Hom}_{\mathcal{C}}(A,T(B)),\,\, g\in \mathrm{Hom}_{\mathcal{C}}(B,T(C))}の合成 {\footnotesize g^{*}\circ f:A\rightarrow T(C)}
  • 【恒等射】 {\footnotesize A\in \mathrm{\mathcal{C_T}}}に対し、{\footnotesize 1_{A}=\eta_A}

{\footnotesize \mathcal{C}}における{\footnotesize A\rightarrow T(B)}の形の射をクライスリ射と呼びます。

【解説】クライスリ圏

対象について

対象についての規則は、『{\footnotesize \mathrm{ob}(\mathcal{C_T})=\mathrm{ob}(\mathcal{C})}』というものでした。
これは、圏{\footnotesize \mathcal{C_T}}の対象と圏{\footnotesize \mathcal{C}}の対象は同じものであるといっています。

射について

次に射についての規則は、『{\footnotesize \mathrm{Hom}_{\mathcal{C_T}}(A,B)=\mathrm{Hom}_{\mathcal{C}}(A,T(B))}』となっていました。
これは、圏{\footnotesize \mathcal{C_T}}において射{\footnotesize A\rightarrow B}と書くものは、圏{\footnotesize \mathcal{C}}においては射{\footnotesize A\rightarrow T(B)}であるということをいっています。
{\footnotesize \mathcal{C}}{\footnotesize A\rightarrow T(B)}という射を、圏{\footnotesize \mathcal{C_T}}では{\footnotesize A\rightarrow B}という「より単純な射」として表すということです。

雰囲気を掴むための図
表面の世界と、その世界につながる裏の世界みたいに捉えてみるといいかもしれません。

射の合成について

最後に射の合成についての規則は、
{\footnotesize f\in \mathrm{Hom}_{\mathcal{C_T}}(A,B),\,\, g\in \mathrm{Hom}_{\mathcal{C_T}}(B,C)}の合成 {\footnotesize g\circ f:A\rightarrow C} は、{\footnotesize f\in \mathrm{Hom}_{\mathcal{C}}(A,T(B)),\,\, g\in \mathrm{Hom}_{\mathcal{C}}(B,T(C))}の合成 {\footnotesize g^{*}\circ f:A\rightarrow T(C)}
というものでした。

なぜこのような規則が定められているのでしょうか?
それは、クライスリ射をそのまま合成することができないからです。
例えば{\footnotesize \mathcal{C_T}}において射{\footnotesize A\rightarrow B}{\footnotesize B\rightarrow C}の合成を考えます。
これは
{\footnotesize A\rightarrow B}に対応したクライスリ射{\footnotesize A\rightarrow T(B)}

{\footnotesize B\rightarrow C}に対応したクライスリ射{\footnotesize B\rightarrow T(C)}

合成になるわけですが、{\footnotesize A\rightarrow T(B)}{\footnotesize A\rightarrow T(C)}ではcodomainとdomainが一致しないため合成ができません。

そこで拡張演算子が出てくるわけで、クライスリ射{\footnotesize B\rightarrow T(C)}に拡張演算子を適用するとクライスリ射同士の合成ができるようになるわけです。

恒等射について

{\footnotesize \mathcal{C_T}}においての恒等射{\footnotesize 1_A:A\rightarrow A}{\footnotesize \mathcal{C}}においては{\footnotesize A\rightarrow T(A)}になりますが、これは{\footnotesize \eta_A:A\rightarrow T(A)}と一致するということをいっています。

プログラムの世界での話

クライスリ圏の概念を踏まえつつ、今一度>=>>>=を見ていきます。

>=>

>=>の定義は
forall a b c m. Bind m => (a -> m b) -> (b -> m c) -> a -> m c
でした。
a -> m bb -> m cという形は、クライスリ射{\footnotesize A\rightarrow T(B), B\rightarrow T(C)}の形と一致していますね。
クライスリ射を合成すると{\footnotesize A\rightarrow T(C)}となり、上記の定義のa -> m cとも一致します。
ということで最初の方に書いた通り、>=>はクライスリ射の合成というわけです。

PureScriptでは>=>composedKleisliという関数名になっていますが、元ネタの通りの意味を持つ関数だったんですね。

>>=, =<<

次に>>=(bind)や=<<(bindFlipped)の定義を見てみましょう。

bind :: m a -> a -> m b -> m b
bindFlipped :: a -> m b -> m a -> m b

ここでもa -> m bというクライスリ射にあたるものが出てきましたね。
そしてbindFlippedの方は括弧をつけて(a -> m b) -> (m a -> m b)としてみると、a -> m bをクライスリトリプルの拡張演算子でm a -> m bに写してやっているものと一致しており、拡張演算子*=bindFlippedと解釈できるかと思います。
そして拡張演算子によってクライスリ射の合成を実現できたことを思い出しつつ、PureScriptにおいてのbindFlippedの定義を見てみましょう。

composeKleisli :: forall a b c m. Bind m => (a -> m b) -> (b -> m c) -> a -> m c
composeKleisli f g a = f a >>= g

実装に、>>=が出てきました。これはbindですが、bindFlippedの引数を入れ替えればbindになるので、bindFlippedでも実装可能です。

プログラムの圏としてのクライスリ圏と、モナドへの影響

この節では、「プログラムの圏をモデル化するためのクライスリ圏」の話と、関数型プログラムの構造化におけるモナドの応用の歴史に登場するクライスリ圏の話をします。

まずHaskellの設計者の一人であるPhilip Wadlerが、論文『The essence of functional programming』で、モナドの過去について言及している部分を見てみます(翻訳はDeepLです。参照の部分はリンクなどを追記しています。誤りがあったらご指摘いただけると幸いです)。

5.2 過去
最後に、これらの考え方の起源について述べておこう。
モナドの概念は圏論 [マックレーンの『圏論の基礎』] に由来する。
最初はホモロジー代数の分野で生まれたが、後に(KleisliやEilenbergとMooreの研究によって)もっと広い応用があることが認識された。その重要性は徐々に現れたが、初期には正式な名前すら与えられず、単に「標準構成」や「トリプル」と呼ばれていた。ここで使われている定式化はKleisliによるものである。
Eugenio Moggiは、モナドが意味論に有用な構造化ツールを提供することを提案した[Mog89a, Mog89b]。彼は、ラムダ計算が任意のモナドにおいて、値による呼出しと名前による呼出しのセマンティクスをどのように与えられるか、また、モナドが状態、例外処理、継続などのプログラミング言語のさまざまな機能をどのようにカプセル化できるかを示した。

原文

5.2 The past
Finally, something should be said about the origin of these ideas.
The notion of monad comes from category theory [Mac71, LS86].
It first arose in the area of homological algebra, but later was recognised (due to the work of Kleisli and of Eilenberg and Moore) to have much wider applications.
Its importance emerged slowly: in early days, it was not even given a proper name, but called simply a “standard construction” or a “triple”.
The formulation used here is due to Kleisli.
Eugenio Moggi proposed that monads provide a useful structuring tool for denotational semantics [Mog89a, Mog89b].

更にWadlerはこう書いています。

MoggiとSpiveyに触発されて、私は関数型プログラムを構造化する一般的な手法としてモナドを提案した。
原文:Inspired by Moggi and Spivey, I proposed monads as a general technique for structuring functional programs.

どうやら、WadlerはモナドにおいてMoggi(などの人物)の考え方に影響を受けているようです。
(計算機科学者MoggiについてはWikipediaを参照)

ということで、次はMoggiの『Notions of Computation and Monads』を見てみます。
Moggiはこの「1. A CATEGORICAL SEMANTICS OF COMPUTATIONS」で「プログラムの圏」について言及しています。

以下の圏論的意味論の基本的な考え方は、プログラミング言語を圏{\footnotesize \mathcal{C}}で解釈するために、{\footnotesize A}型の値の対象{\footnotesize A}{\footnotesize A}型の計算の対象{\footnotesize TA}を区別し、{\footnotesize A}型のプログラムの表記を{\footnotesize TA}の要素とする、というものである。
特に、{\footnotesize A}型と{\footnotesize A}型の値の対象を識別し、{\footnotesize A}型に単項の型構成子{\footnotesize T}を適用することで、{\footnotesize A}型の計算の対象を得る。
私たちは{\footnotesize TA}を計算の概念と呼ぶ。これは、計算が生み出す可能性のある値の型を抽象化したものだからである。
{\footnotesize TA}には、さまざまな計算概念に対応する多くの選択肢がある。

原文

The basic idea behind the categorical semantics below is that, in order to interpret a programming language in a category {\footnotesize \mathcal{C}}, we distinguish the object {\footnotesize A} of values (of type {\footnotesize A}) from the object {\footnotesize TA} of computations (of type {\footnotesize TA}), and take as denotations of programs (of type {\footnotesize A}) the elements of {\footnotesize TA}.
In particular, we identify the type {\footnotesize A} with the object of values (of type {\footnotesize A}) and obtain the object of computations (of type {\footnotesize A}) by applying an unary type-constructor {\footnotesize T} to {\footnotesize A}.
We call {\footnotesize T} a notion of computation, since it abstracts away from the type of values computations may produce.
There are many choices for {\footnotesize TA} corresponding to different notions of computations.

次に計算の概念の例示があります。

EXAMPLE 1.1. We give few notions of computation in the category of sets:

ここでは部分性、非決定性、副作用、例外、継続などが計算概念として示されます。
(詳細は後述しますが、具体的にはListモナドやStateモナドなどが計算概念にあたります。)

そして次に続きます。

さらに(cposの圏で)様々なプログラミング言語の意味論に基づいた例を挙げることができる(Schmidt (1986), Gunter and Scott (1989), Mosses (1989)を参照)。
我々は、特定の{\footnotesize T}に焦点を当てるのではなく、計算のすべての概念に共通する一般的な性質を見出したい。
本節の目的は、このような要件が、{\footnotesize T}がクライスリトリプル{\footnotesize (T,\mu,-^*)}の一部であり、プログラムの圏がこのようなトリプルに対するクライスリ圏であることを、一連の非公式な論証によって読者に納得させることである。

原文

Further examples (in a category of cpos) could be given based on the denotational semantics for various programming languages (see Schmidt (1986), Gunter and Scott (1989), and Mosses (1989)).
Rather than focusing on a specific {\footnotesize T}, we want to find the general properties common to all notions of computation; therefore we impose as the only requirement that programs should form a category.
The aim of this section is to convince the reader, with a sequence of informal argumentations, that such a requirement amounts to saying that {\footnotesize T} is part of a Kleisli triple {\footnotesize (T,\mu,-^*)} and that the category of programs is the Kleisli category for such a triple.

Moggiは「プログラムの圏」は(このようなクライスリトリプルに対する)「クライスリ圏」であると言っているようです。

ではここでいう「プログラム」とは何でしょうか?

MoggiのAn Abstract View of Programming Languagesの「3.2.1 A justification for monad」では、次のように説明されています。

計算の概念をモデル化するためのモナドの使用を正当化するために、我々はプログラムについて次のような直感的な理解を採用する。 プログラムとは値から計算への関数である。

  1. まず、関数のモデルとして圏{\footnotesize \mathcal{C}}を取り上げ、その上に値と計算の一般的な理解を展開する。
    より正確には、{\footnotesize \mathcal{C}}の対象に単項演算{\footnotesize T}を導入し、{\footnotesize \tau}型の値の集合として見た対象{\footnotesize A}を、{\footnotesize \tau}型の計算の集合に対応する対象{\footnotesize T A}に写す。
  2. そして、{\footnotesize A}から{\footnotesize B}へのプログラム、つまり{\footnotesize A}型の値を入力とし、ある計算を実行した後に{\footnotesize B}型の値を返すプログラムは、{\footnotesize \mathcal{C}}における{\footnotesize A}から{\footnotesize T B}への射とみなすことができる。
  3. 最後に、プログラムが適切な圏の射となるように、値と計算に関する最小限の要件を特定する。

これらの3つのステップにより、計算の概念をモデル化するためのクライスリトリプルと、プログラムの圏をモデル化するためのクライスリ圏が導かれる。

原文

In order to justify the use of monads for modelling notions of computations we adopt the following intuitive understanding of programs a program is a function from values to computations.

  1. First we take a category {\footnotesize \mathcal{C}} as a model for functions and develop on top a general understanding of values and computations.
    More precisely we introduce a unary operation {\footnotesize T} on the objects of {\footnotesize \mathcal{C}}, which map an object {\footnotesize A} viewed as the set of values of type {\footnotesize \tau} to an object {\footnotesize T A} corresponding to the set of computations of type {\footnotesize \tau}.
  2. Then a program from {\footnotesize A} to {\footnotesize B}, i.e. which takes as input a value of type {\footnotesize A}
    and after performing a certain computation will return a value of type {\footnotesize B}
    can be identified with a morphism from {\footnotesize A} to {\footnotesize T B} in {\footnotesize \mathcal{C}}
  3. Finally we identify a minimum set of requirements on values and computations so that programs are the morphisms of a suitable category.

These three steps lead to Kleisli triples for modelling notions of computation and Kleisli categories for modelling categories of programs.

どうやらこの文脈での「プログラム」とは「値から計算への関数」のことを指すようです。
具体的には「A型の値を引数とし、計算(何らかの処理)を実行した後B型の値を返すもの」と書かれていますね。
射としては{\footnotesize A\rightarrow T(B)}を({\footnotesize A}から{\footnotesize B}への)プログラムとしています。
そして、この射はクライスリ射と同じ形の射です。

更に「プログラムの圏をモデル化するためのクライスリ圏」とも書かれています。
これがこの節の冒頭に書いた「プログラムの圏をモデル化するためのクライスリ圏」です。

プログラムが、単なる{\footnotesize A}から{\footnotesize B}への射ではなく、計算概念と結果の型を表す{\footnotesize T(B)}への射であることで副作用や例外(を伴う型)を表現できるのですね(実際のプログラムでは{\footnotesize T}はモナドになるでしょう)。

更に重要なのが、これが圏であるということです。
当たり前のことをいうようですが圏であるということは射(この場合はプログラム)を合成できるわけですから、この射(=プログラム)の合成により、より大きなプログラムを構成できます。

プログラマーのための圏論(Bartosz Milewski著)(原文)に「合成はプログラミングの本質」と書かれていましたが、納得です。

合成は圏論の根本にあり、圏そのものの定義の一部だ。そして私は、合成こそプログラミングの本質であると強く主張したい。我々は、偉大なエンジニアがサブルーチンのアイデアを思いつく遥か前から、ずっとものを合成してきた。かつて構造化プログラミングはプログラミングに革命をもたらした。コードのブロックを合成可能にしたからだ。続いてオブジェクト指向プログラミングが登場した。これはオブジェクトを合成することこそすべてだ。関数プログラミングは、関数や代数的データ構造を合成するだけでなく、並行性を合成可能にする。これは他のプログラミングパラダイムでは事実上不可能だ。

以上で、この節は終わりなのですが、関数型プログラムの構造化におけるモナドの応用の歴史においてクライスリ圏が関わっているということが多少なりともおわかりいただけたのではないかと思います。

最後に話の流れに影響しないため「詳細は後述します」としていた計算概念の例の話を書きます。

様々な計算概念

ここでは計算概念として様々な\footnotesize T(A)が登場します。
簡素・簡潔に書かれているため最初読んだとき自分はよくわからなかったので、調べてわかったことなどを解説として加えています。
また、計算概念が実際のプログラムではどうなるかを、プログラミング言語PureScriptを用いて説明していきたいと思います。

計算概念については、それぞれ次の3つについて定義が行われます。

  • \footnotesize T(A)
  • \footnotesize \eta:A\rightarrow T(A)
  • \footnotesize fに対しての\footnotesize f^*

定義では、具体の\footnotesize T(A)として、何らかの計算概念\footnotesize X(A)を考えたとき、\footnotesize \eta\footnotesize f^*がどうなるか、という形式で行われます。

PureScriptを用いた説明では、\footnotesize \etaに対応するpure関数、\footnotesize -^*bind(>>=)として説明します。
\footnotesize -^*は前述の説明では=<<に対応するものでしたが、>>==<<の引数の順序が異なるだけで実質同じものとして扱える上、こちらを使うほうがコードとして読みやすいと考えたためです(個人の主観です)。
また、PureScriptのコードとして説明が難しい計算概念については説明を書きません(いきなり↓のPartialityがそれにあたるのですが)。

partiality (部分性)

  • {\footnotesize T(A)=A_{\bot}} (すなわち、{\footnotesize A+\{\bot \}})、ここで{\footnotesize \bot}は発散計算である。
  • {\footnotesize \eta _{A} :A\rightarrow A_{\bot}} これは{\footnotesize A}{\footnotesize A_{\bot}}への包含。
  • もし{\footnotesize f:A \rightarrow T(B)}ならば{\footnotesize f^*(\bot)=\bot}かつ{\footnotesize f^*(a)=f(a)} ({\footnotesize a\in A}の場合)

解説

コンピューター・サイエンスでは、計算が終了しないか、例外的な状態で終了する場合、計算は発散するといわれます。それ以外の場合は収束すると言われます。プロセス計算など、計算が無限であると予想される領域では、生産的ではない (つまり、有限時間内にアクションを生成し続ける) ことができない場合、計算は発散するといわれます。

ちなみに{\footnotesize \bot}ボトムといいます。
この場合は、発散、つまり呼び出し側に結果を返さないことを知らせるために使われます。(これは必ずしもプログラムが終了に失敗することを意味するわけではありません。サブルーチンは呼び出し側へ戻ることなく終了したり、継続のような他の手段によって脱出する可能性があります)

具体的な{\footnotesize A,B}が与えられたとき、{\footnotesize T(A),T(B)}がどうなるかというと
{\footnotesize A=\{x,y\}} のときは {\footnotesize T(A)=\{x,y,\bot\}}
{\footnotesize B=\{1,2\}} のときは {\footnotesize T(B)=\{1,2,\bot\}}
となります。

上記の最初の例を見ると、{\footnotesize \eta_{A}(x)}{\footnotesize \eta_{A}(y)}は、{\footnotesize T(A)}のいずれかに写るので{\footnotesize \bot}に写る可能性があります。

{\footnotesize f^*}についても上記の例を用いて説明しておきましょう。
{\footnotesize f^*:T(A)\rightarrow T(B)} なので {\footnotesize \{x,y,\bot\} \rightarrow \{1,2,\bot\}} になりますね。
そして{\footnotesize T(A)}においての{\footnotesize \bot}{\footnotesize f^*(\bot)}で、{\footnotesize T(B)}{\footnotesize \bot}に写ります。
{\footnotesize \bot}以外の場合は{\footnotesize f^*(a)=f(a)}なので{\footnotesize \bot}以外に写りますね。
この{\footnotesize f^*}が何を意味するかというと、{\footnotesize f^*}は射の合成に用いられるので、計算の途中で発散したら射を合成した後も変わらず発散したままになるということを意味します。

PureScriptではではどうなるか

この定義のまんまに当てはまるようなものは見つけられませんでした。
Wikipediaのボトム型の記事によると、

理論色の強いプログラミング言語で見られる。産業色の強いプログラミング言語では、一般的にオプション型(タグ付きポインタを含む)や例外処理などの他の手法を使う。

ということのようなので、PureScriptはサポートしていないのかも。
Partiality Monadというモナドがあるらしいので、別の形ではあるが自前で実装はできるかもしれない)

nondeterminism (非決定性)

  • \footnotesize T( A) =\mathcal{P}_{\mathrm{fin}}( A)
  • \footnotesize \eta _{A} :A\rightarrow \mathcal{P}_{\mathrm{fin}}( A) は singleton map \displaystyle a\mapsto \{a\}
  • もし\footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)ならば、\footnotesize f^{*}( c) =\cup _{x\in c} f( x)

解説

\footnotesize T( A) =\mathcal{P}_{\mathrm{fin}}( A)について】
まず\footnotesize \mathcal{P}_{\mathrm{fin}}(A)の添字\footnotesize \mathrm{fin}は有限であることを表します。
次に\footnotesize \mathcal{P}( A)\footnotesize Aの冪集合を表します。
つまり \footnotesize \mathcal{P}_{\mathrm{fin}}( A)\footnotesize Aの有限冪集合となります。

次は具体です。
具体的な{\footnotesize A,B}が与えられたとき、{\footnotesize T(A),T(B)}がどうなるかというと
{\footnotesize A=\{1,2\}} のときは {\footnotesize T(A)=\{\phi ,\{1\} ,\{2\} ,\{1,2\}\}}
{\footnotesize B=\{x,y\}} のときは {\footnotesize T(B)=\{\phi ,\{x\} ,\{y\} ,\{x,y\}\}}
という感じになります。

\footnotesize \eta _{A} :A\rightarrow \mathcal{P}_{\mathrm{fin}}( A)について】
singleton map とは、一元集合への写像 \footnotesize f:A\rightarrow P(A)\footnotesize f(a)=\{a\}と定めたもののことです。
singletonは単一集合のことを指し、その射なのでsingleton mapということですね。

そして{\footnotesize \eta_{A}}についてですが、この場合の\footnotesize \eta_{A}は、{\footnotesize a\in A}について、\footnotesize \eta _{A}( a) =\{a\}となり、これは当然べき集合\footnotesize \mathcal{P}_{\mathrm{fin}}( A)の要素になります。

上記の例の{\footnotesize A}を用いて具体的にどうなるか見てみます。
{\footnotesize \eta _{A}(1) =\{1\}}
{\footnotesize \eta _{A}(2) =\{2\}}

【もし\footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)ならば、\footnotesize f^{*}( c) =\cup _{x\in c} f( x)について】
この場合の\footnotesize c\in T( A)\footnotesize c\in \mathcal{P}( A)であり、\footnotesize \mathcal{P}( A)は集合の集合なので、\footnotesize c\footnotesize Aの要素からなる何かしらの集合です。
ということは、\footnotesize x\in c\footnotesize Aです。\footnotesize Aの集合\footnotesize cのすべての\footnotesize xについて\footnotesize f( x)を適用させた結果(つまり\footnotesize \mathcal{P}( B))の和集合が\footnotesize \cup _{x\in c} f( x)です。

これも具体的な値を用いて説明してみます。
上記の\footnotesize A,Bに対し\footnotesize f:A\rightarrow T(B)が次のような射だったとします。

\footnotesize f(1)=\{x\}, \footnotesize f(2)=\{y\}

すると各\footnotesize c\in T( A)に対しての\footnotesize f^*は次のようになるでしょう。

\footnotesize \begin{array}{ l c l c l } f^{*}( \phi ) & = & \phi & = & \phi\\ f^{*}(\{1\}) & = & f(1) & = & \{x\}\\ f^{*}(\{2\}) & = & f(2) & = & \{y\}\\ f^{*}(\{1,2\}) & = & f(1)\cup f(2) & = & \{x,y\} \end{array}

PureScriptではどうなるか

非決定性はPureScriptでいうと、ArrayListなどになります。
つまり\footnotesize T( A) =\mathcal{P}_{\mathrm{fin}}( A)Array AList Aですね。

\footnotesize \etaに対応するpureの動作を見てみましょう。
\footnotesize T(A)Array Stringとします。
つまり\footnotesize \eta:\mathrm{String \rightarrow Array\,String}となりますね。
replで確認してみると、定義どおりになっていますね。

> import Prelude 
> pure@Array "x"
["x"]

次に\footnotesize f:\mathrm{String \rightarrow Array\,Int}として、\footnotesize f^*:\mathrm{Array\,String \rightarrow Array\,Int}を考えます。
つまりこの場合の\footnotesize -^*に対応する
=<<\footnotesize \mathrm{String \rightarrow Array\,Int\rightarrow Array\,String \rightarrow Array\,Int}なので、
>>=\footnotesize \mathrm{Array\,String\rightarrow String \rightarrow Array\,Int \rightarrow Array\,Int}ですね。
適当なString -> Array Int型の関数を使って、replで確認してみましょう。

> import Prelude 
> import Data.String                    
> ["ab", "c"] >>= \s -> pure $ length s 
[2,1]
> [] >>= \s -> pure $ length s         
[]

こちらも計算概念に示された通りになってますね。

side-effect (副作用)

  • \footnotesize T( A) =( A\times S)^{S}
  • \footnotesize \eta _{A}\footnotesize a\mapsto ( \lambda s :S.\langle a,s\rangle )
  • \footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)であれば\footnotesize f^{*}( c) =\lambda s :S.(\mathrm{let} \ \langle a,s'\rangle =c( s) \ \mathrm{in} \ f( a)( s'))

解説

\footnotesize T( A) =( A\times S)^{S}について】
まず{\footnotesize S}は状態の集合であり、例えばStoreの集合\footnotesize U^{L}や入出力シーケンスの集合\footnotesize U^{*}です。

次に\footnotesize (A\times S)^{S}ですが、これは配置集合といい集合\footnotesize A,Bに対して\footnotesize B^{A}は2つの集合に対する演算で、\footnotesize Aから\footnotesize Bへの写像全体からなる集合を割り当てるものです。
ですのでこれは\footnotesize f:S\rightarrow A\times Sという射すべての集合になります。

\footnotesize Aが型だとしたら、状態\footnotesize Sから型\footnotesize Aと状態\footnotesize Sの直積への射の集合ということですね。

\footnotesize \eta _{A}\footnotesize a\mapsto ( \lambda s :S.\langle a,s\rangle ) について】
\footnotesize \eta _{A} :A\rightarrow T( A) =A\rightarrow ( A\times S)^{S}だから、これは\footnotesize A\footnotesize S\rightarrow A\times Sという射に写すことになります。
この射を{\footnotesize \lambda s :S.\langle a,s\rangle}と表しているのですが、この表記は型つきラムダ計算の表記(より正確にはラムダ計算でいうところのラムダ抽象)で、\footnotesize s:S\footnotesize sが型\footnotesize Sに属していることを表しています。

つまり{\footnotesize \lambda s}{\footnotesize S}型の{\footnotesize s}{\footnotesize \langle a,s\rangle}に写す射ということです。

なので\footnotesize \eta _{A}\footnotesize a\in Aを受け取って、\footnotesize \lambda sという射を返すわけですね。

\footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)であれば\footnotesize f^{*}( c) =\lambda s :S.(\mathrm{let} \ \langle a,s'\rangle =c( s) \ \mathrm{in} \ f( a)( s')) について】
まず\footnotesize f^{*} :T( A)\rightarrow T( B) なので \footnotesize f^{*} :\left( S\rightarrow A\times S\right)\rightarrow \left( S\rightarrow B\times S\right)ということになりますね。

続いて内容を見ていきましょう。
\footnotesize c\in T( A)は定義から{\footnotesize s\rightarrow \langle a,s\rangle}という射です。
なので{\footnotesize f^{*}( c)}はこの射{\footnotesize c}{\footnotesize \lambda _{s}}という射に写すわけです。

{\footnotesize \lambda _{s}}ですが、

  1. まず状態{\footnotesize s\in S}に上記の射{\footnotesize c}を適用させ結果{\footnotesize \langle a,s'\rangle}を得ます。
  2. 次に{\footnotesize \langle a,s'\rangle}\footnotesize aを使って射\footnotesize f(a)を適用させ{\footnotesize s\rightarrow \langle b,s''\rangle}という結果(射)を得ます。
  3. 最後に{\footnotesize \langle a,s'\rangle}\footnotesize s'に対して上記の射{\footnotesize s\rightarrow \langle b,s''\rangle}を適用します。

これが\footnotesize \lambda _{s}という射です。

PureScriptではどうなるか

PureScriptだとこれはStateモナドになります。
\footnotesize T( A) =( A\times S)^{S}s -> (Tuple a s)という感じになるでしょう。
型構築子まで含めると次のようになります。

newtype State s a = State (s -> (Tuple a s))

「なるでしょう」というのは、いまのPureScriptのライブラリのStateモナドは、モナド変換子を用いた定義になっており、↑のようなシンプルな定義にはなっていないからです。

newtype StateT s m a = StateT (s -> m (Tuple a s))

説明では、自前のもので恐縮ですが最初のシンプルなコードの方を使っていきます。

では次に\footnotesize \etaに対応するpureを見てみましょう。
\footnotesize \eta _{A}\footnotesize a\mapsto ( \lambda s :S.\langle a,s\rangle )でしたが、pureの実装例はこんな感じになります。

instance applicativeState :: Applicative (State s) where
  pure :: forall s a. a -> State s a
  pure a = State $ \s -> (Tuple a s)

Stateで包まれてはいますが、\footnotesize \eta _{A}の定義と同じように実装できることがわかります。

続いて\footnotesize f^{*}( c) =\lambda s :S.(\mathrm{let} \ \langle a,s'\rangle =c( s) \ \mathrm{in} \ f( a)( s'))
を見つつbindの実装例を見てみます。

instance bindState :: Bind (State s) where
  bind :: forall s a b. State s a -> (a -> State s b) -> State s b
  bind m f = State $ \s -> let
    (Tuple a s') = runState m s
    in runState (f a) s'

runState :: forall s a. State s a -> (s -> (Tuple a s))
runState (State f) = f

runStateはunwrapsして関数を取り出す関数ですが、ここを除いて見ると\footnotesize f^*の定義と同じように実装できます。

exceptions (例外)

  • \footnotesize T( A) =( A+E)
    ここで\footnotesize Eは例外の集合である。
  • \footnotesize \eta _{A}は入射 \footnotesize a\mapsto \mathrm{inl}( a)
  • \footnotesize f:A\rightarrow T( B)ならば
    \footnotesize e\in Eに対して\footnotesize f^{*}(\mathrm{inr}( e)) =e
    かつ
    \footnotesize a\in Aに対して\footnotesize f^{*}(\mathrm{inl}( a)) =f( a)

解説

\footnotesize ( A+E){\footnotesize +}{\footnotesize \oplus}と同じで、一般的には直和を表します。すなわち{\footnotesize A\cap E=\phi}
ただこれは圏論の文脈なので圏論的直和である余積と考えます。

つまり

\footnotesize A\xrightarrow{inl} A+E\xleftarrow{inr} B

ということです。

\footnotesize f^{*}(\mathrm{inr}( e)) =eは、\footnotesize fの適用がされないということです。\footnotesize e\in Eなので当然\footnotesize e\in B+Eが成り立ちます。
\footnotesize f^{*}(\mathrm{inl}( a)) =f( a)\footnotesize fが適用されます。

射を合成したとき、例外ならずっと例外のままってことですね。

PureScriptではどうなるか

\footnotesize T( A) =( A+E) は余積なので次のようにEitherになるでしょう。
このPureScriptの実装例ではE,Aの順に定義しています。したがってinl,inrも逆になっていると考えてください。

newtype Except e a = Except (Either e a)

\footnotesize \eta _{A}である入射 \footnotesize a\mapsto \mathrm{inl}( a) はこうなります。

instance applicativeExcept :: Applicative (Except e) where
  pure = Except <<< Right

bindの実装例もみてみます。

instance bindExcept :: Bind (Except e) where
  bind (Except x) k =
    Except (either Left (\a -> case k a of Except b -> b) x)

continuations (継続)

  • \footnotesize T( A) =R^{(R^{A})}
    ここで\footnotesize Rは結果の空でない集合である。直観的には、計算は継続を取り、結果を返す。
  • \footnotesize \eta _{A}は射 \footnotesize a\mapsto ( \lambda k) :R^{A} .( k( a))
  • \footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)ならば、\footnotesize f^{*}( c) =\left( \lambda k:\ R^{B} .c( \lambda a:A.f( a)( k))\right)

解説

\footnotesize R^{\left( R^{A}\right)}\footnotesize \left( A\rightarrow R\right)\rightarrow Rの集合です。
\footnotesize A\rightarrow Rは継続を表します。
ですので継続を取り、結果を返す射の集合ということですね。

\footnotesize \eta _{A}は射 \footnotesize a\mapsto ( \lambda k) :R^{A} .( k( a))について】
\footnotesize T( A) =R^{\left( R^{A}\right)} なので \footnotesize \eta _{A}\footnotesize \eta _{A} :A\rightarrow \left(\left( A\rightarrow R\right)\rightarrow R\right) になります。

次に \footnotesize ( \lambda k) :R^{A} .( k( a)) の部分ですが、副作用のときと同じく型付きラムダ計算として書かれています。
\footnotesize k:R^{A} ということは {\footnotesize k\in ( A\rightarrow R)} ですね。
なので、これは単に\footnotesize a \in A{\footnotesize k\in ( A\rightarrow R)} を適用させるということです。

\footnotesize f^{*}( c) =\left( \lambda k:\ R^{B} .c( \lambda a:A.f( a)( k))\right)について】
まず
\footnotesize f:A\rightarrow T(B)
だから
\footnotesize f^*:T(A)\rightarrow T(B)=R^{R^A}\rightarrow R^{R^B}=((A \rightarrow R) \rightarrow R) \rightarrow ((B\rightarrow R)\rightarrow R)
です。

また
\footnotesize c \in T(A)
だから
\footnotesize c \in (A \rightarrow R)\rightarrow R
です。

ここまでを前提として、\footnotesize \lambda k:\ R^{B} .c( \lambda a:A.f( a)( k)) を説明します。

ラムダ抽象の部分を見ると、\footnotesize \lambda k:R^{B} だから \footnotesize k \in (B\rightarrow R)ということがわかりますね。
更に\footnotesize \lambda a:A だから \footnotesize a \in A です。

上記から\footnotesize \lambda a:A.f( a)( k)

  1. \footnotesize f(a) の結果 \footnotesize (B\rightarrow R)\rightarrow R を得る。
  2. \footnotesize k\in (B\rightarrow R) に対し1を適用させ、結果\footnotesize Rを得る。

なので \footnotesize \lambda a\footnotesize A\rightarrow R ですね。

\footnotesize c を思い出してみると \footnotesize c \in (A \rightarrow R)\rightarrow R だったので、\footnotesize \lambda a に対して \footnotesize c を適用させられます。その結果は \footnotesize R です。
ということは \footnotesize \lambda k\footnotesize (B\rightarrow R)\rightarrow R ですね。

\footnotesize f^*\footnotesize (A \rightarrow R)\rightarrow R を上記の \footnotesize \lambda k に写すので定義どおりですね。

PureScriptではどうなるか

\footnotesize T( A) =R^{(R^{A})} は次のようになります。

newtype Cont r a = Cont ((a -> r) -> r)

続いて \footnotesize \eta _{A}は射 \footnotesize a\mapsto ( \lambda k) :R^{A} .( k( a)) に対応するpureです。定義のまんまですね。

instance applicativeCont :: Applicative (Cont r) where
  pure a = Cont (\k -> k a)

最後に「\footnotesize f:A\rightarrow T( B)かつ\footnotesize c\in T( A)ならば、\footnotesize f^{*}( c) =\left( \lambda k:\ R^{B} .c( \lambda a:A.f( a)( k))\right)」に対応するbindです。型注釈を多めにつけてます。これも定義どおりに実装できてますね。

instance bindCont :: Bind (Cont r) where
  bind :: forall a b. Cont r a -> (a -> (Cont r b)) -> Cont r b
  bind (Cont c) f = Cont 
    (\(k :: (b -> r)) -> 
      c (\a -> case f a of 
        (Cont (x :: ((b -> r) -> r))) -> x k))

後書き

クライスリというキーワードは圏論を知る以前から、モナドについて調べていたり、ライブラリのコードを読んでいたりしたときに見かけていました。
例えばPureScriptでは>=>の関数名はcomposeKleisliなのですが、「何がクライスリなんだ?」「クライスリってどういう意味?」と思っていました。
ググっても、クライスリトリプルなんかが出てきてよくわからかったので最初はスルーしました。

圏論でモナドを学び始めた頃、「そろそろいけるだろ」とWikipediaの記事を読んでみたのですが、正直簡単には定義を理解できませんでした。
Moggiが挙げている計算概念の例も同様で、なんかわかりそうでわからないという状態でした。

そんな状態だったのですが、クライスリ圏はここしばらく強い関心をもっていたモナドとの関わりが深そうだということわかってきて、面白みを感じたんですね。
そこから色々調べたり、図式を書いてみたり、関係しそうなプログラムを動かしたり読んだり書いてみたりしていたのですが、実際これがなかなか面白かったので記事にしてみました。

みなさまも楽しんでいただけたら幸いです。

Discussion