圏論のモナドとPureScriptのモナドの関係性を知ろう
この記事が目指すところ
この記事は圏論のアレやコレが具体的なプログラミング言語ではどうなっているのかを解説することで、圏論のような数学的概念とプログラミング言語にどういう繋がりがあるのかを知っていただくことを目的としています。
特にみんなが大好きな「モナド」にフォーカスします。
したがってモナドの定義にあらわれる「関手」「自然変換」などにも言及します。
具体的なプログラミング言語としてはPureScriptを用います。
前置き
この記事の基本構成
この記事は圏論の「圏」「関手」「自然変換」「モナド」について以下を繰り返しながら進んでいくという構成になっています。
- 圏論側の定義や概念を示す
- 1とPureScript側がどう対応しているのかを説明する
前提となる知識
圏論、PureScriptともに基礎的な知識を前提としています。
具体的にいうと、圏論においては圏・関手・自然変換あたりの知識で、PureScriptにおいてはFunctor・Monadあたりの知識です。
とはいえ必要な定義は書いていますし、解説・補足などを適宜行っていくので、ざっくり知っているくらいでも大丈夫かと思います。
圏論の諸概念の定義について
圏論の諸概念の定義は可能な限り何らかの圏論のテキストを元にしました。
(記号などは記事内での一貫性を担保するため変えたりしていますが)
こういった定義に関わるような文章に関しては、「~である」「~だ」のような常体で書き、解説・補足に該当するような文章に関しては「です」「ます」のような敬体で書くことで区別できるようにしています。定義と解説・補足の文章を水平線で分割したりもしています。
書いていないこと
記事の目的のみにフォーカスするため次のようなことは書いておりません。
- いわゆる証明
- この記事のモナドの説明に出てこない圏論の概念の説明(例えば双対圏とか、反変関手とか、随伴とか)
- PureScriptの
FunctorやMonadなどの使い方自体(使い方は圏論の学習ではなく、実際モナドを使ったコードを書く、自分でモナドを作るなどして覚えることをおすすめします)
他にもあるかもしれませんが、ただでさえ絞っても分量が多くなる内容なので、基本的に必要最低限のものを書くスタンスです。
さあ前置きはこのへんにして、はじめましょう!
ざっくりとした関係性
このあと一つずつ見ていきますが、まずざっくりと関係性を示した図を示します。
これは圏論のモナドに関わる概念からPureScript側の概念への関連の図です。
圏論側にあって、PureScript側にないものがあるかと思いますが、これは圏論のモナドを定義する上で登場しない概念です。
つまりPureScriptのApplyは圏論におけるモナドの定義には登場しないことを意味します。

一つずつ見ていきましょう。
圏
まず圏論における圏の定義を示します。
【定義】圏
圏
-
対象
の集合{\footnotesize (\mathrm{object})} {\footnotesize \mathrm{ob}(\mathcal{A}) } - 各
について、{\footnotesize A,B\in \mathrm{ob}(\mathcal{A})} から{\footnotesize A} への射({\footnotesize B} )の集合{\footnotesize \mathrm{map}} {\footnotesize \mathcal{A}(A,B) } - 各
について、合成と呼ばれる関数{\footnotesize A,B,C\in \mathrm{ob}(\mathcal{A})}
- 各
について、{\footnotesize A\in \mathrm{ob}(\mathcal{A})} 上の恒等射と呼ばれる{\footnotesize A} の元{\footnotesize \mathcal{A}( A,A)} {\footnotesize 1_{A}}
からなり、以下の公理を満たすもののことである。
- 結合律:任意の
について{\footnotesize f\in \mathcal{A}( A,B), g\in \mathcal{A}( B,C) , h\in \mathcal{A}( C,D)}
が成り立つ。{\footnotesize (h\circ g) \circ f=h\circ ( g\circ f)} - 単位律:任意の
について{\footnotesize f\in \mathcal{A}(A,B)}
が成り立つ。{\footnotesize 1_{A} \circ f=f=f\circ 1_{B}}

図:圏の例
可換
一般的に図式が可換であるとは、対象
例えば次のように与えられた圏の対象と射について

恒等射と対象の同一視
対象と恒等射は一対一の対応があるため、対象を恒等射と同一視することができる。

例えば上記の対象
このように同一視するモチベーションは、こうした方が便利な場合があるからである。
PureScriptの圏
次にPureScript側のコードを示します。
PureScriptにはまんま圏を表すCategoryという型クラスがあります。
そしてCategoryはSemigroupoidという型クラスを継承したものになっています。
class Category :: forall k. (k -> k -> Type) -> Constraint
class Semigroupoid a <= Category a where
identity :: forall t. a t t
instance categoryFn :: Category (->) where
identity x = x
class Semigroupoid :: forall k. (k -> k -> Type) -> Constraint
class Semigroupoid a where
compose :: forall b c d. a c d -> a b c -> a b d
instance semigroupoidFn :: Semigroupoid (->) where
compose f g x = f (g x)
infixr 9 compose as <<<
まずSemigroupoidの方を見てみると、Semigroupoidにはcomposeという関数が定義されており、Semigroupoidのインスタンスとして->のインスタンスが定義されています。
->は関数なので、ここにはPureScriptの関数の合成が定義されているということになります。
またcomposeのエイリアスとして<<<が定義されていますね。関数合成としてはこちらを使うことがほとんどだと思いますが実はcomposeという関数だったのですね。
一方、Categoryの方を見てみると、こちらにはidentityという関数が定義されています。
圏論においての圏とPureScriptのCategoryの関係性
まず圏論の圏はかなり抽象的な定義となっており、条件さえ満たせば対象と射はどのようなものでも圏を構成することができます。
ではPureScriptのCategoryはどのような圏になるでしょうか?
まずPureScriptにおいての対象はPureScriptの型となります。
そしてそれらの間の関数が射となります。
(このような圏をHaskell界隈ではHaskellの型の圏Haskというようです)
圏における射の合成は、PureScriptでいうとSemigroupoidのcomposeにあたります。
恒等射は、PureScriptでいうとCategoryのidentityになります。
結合律・単位律
PureScriptは関数自体の比較が行えないため、関数自体を用いて結合律や単位律を満たすかどうかを検証することはできません。
しかし具体的な値と関数を用いれば(あくまでその具体化されたものに対しては)検証することができるのでテストコードを書いてみます。
module Test.Main where
import Prelude
import Data.Number (isNaN)
import Data.String (length)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "Categoryは以下の条件を満たす" do
let
f = isNaN
g = show
h = length
a = 100.0
it "結合律を満たす" do
((h <<< g) <<< f) a `shouldEqual` (h <<< (g <<< f)) a
it "左単位律を満たす" do
(identity <<< f) a `shouldEqual` f a
it "右単位律を満たす" do
f a `shouldEqual` (f <<< identity) a
Categoryは以下の条件を満たす
✓︎ 結合律を満たす
✓︎ 左単位律を満たす
✓︎ 右単位律を満たす
まとめ
圏論の圏と、PureScriptのCategoryの対応をまとめてみます。

圏の定義で使った圏の例とともに、PureScriptの圏の例を示します。

関手
圏と同様にまず圏論における関手の定義を示します。
【定義】関手
関手
-
と書かれる関数{\footnotesize X\mapsto F(X)}
{\footnotesize \ \mathrm{ob}(\mathcal{A})\rightarrow \mathrm{ob}(\mathcal{B})} -
について{\footnotesize X,Y\in \mathcal{A} } と書かれる関数{\footnotesize f\mapsto F(f)}
{\footnotesize \mathcal{A}( X,Y)\rightarrow \mathcal{B}( F(X) ,F(Y))}
からなり、以下を満たすものである。
-
で{\footnotesize \mathcal{A}} となるものについて{\footnotesize X\xrightarrow{f} Y\xrightarrow{g} Z}
{\footnotesize F(g\circ f) = F(g) \circ F(f)} -
について{\footnotesize X\in \mathcal{A}}
{\footnotesize F(\mathrm{1}_{X}) = \mathrm{1}_{F(X)}}

図:関手の例
関手の条件における
また関手において、対象についての関数
射についての関数
私が読んだ書籍『ベーシック圏論』ではこの言葉は使われていませんでしたが、Web上では使われている例をしばしば見かけました。
便利なので本記事では以後この言葉を使っていきます。
【定義】関手の合成
関手
に対して、関手の合成
を以下で定める。
{\footnotesize G\circ F:\ X\rightarrow G( F( X)) ( \ X\in \mathrm{ob}(\mathcal{A}) ,\ G( F( X)) \in \mathrm{ob}(\mathcal{C}) \ )} {\footnotesize G\circ F:\ f\rightarrow G( F( f)) ( f\in \mathcal{A}( X,\ Y) ,\ G( F( f)) \in \mathcal{C}( \ G( F( X)) ,\ G( F( Y)) \ )}

図:関手の合成
【定義】恒等関手
恒等関手
{\footnotesize 1_{\mathcal{A}} :X\rightarrow X ( X\in \mathrm{ob}(\mathcal{A}))} {\footnotesize 1_{\mathcal{A}} :\ f\rightarrow f ( f\in \mathcal{A}( X,\ Y) \ )}

図:恒等関手はすべての対象と射をそれ自身へと写す
【定義】自己関手
圏
すなわち恒等関手は、自己関手である。
PureScriptの関手
PureScriptの関手は射関数と対象関数に分けて見ていきます。
射関数
PureScriptにはFunctorという型クラスが存在します。
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
このFunctorにはmapという関数が定義されていますが、これは圏論の関手でいうと射関数にあたるものです。
次の図は、対象や関手の名前を上記のmapの定義に合わせた形で描いたものです。

赤字のところに着目してください。
map関数の定義forall a b. (a -> b) -> f a -> f bと一致していますね。
括弧をつけてやり(a -> b) -> (f a -> f b)、つまりmap関数を、関数(a -> b)を関数(f a -> f b)に写すものと考えるとより圏論の定義に近い形に見えるでしょう。
対象関数
PureScriptでは型構築子が関手の対象関数と考えられます。
例えばMaybeの型構築子は、型aを型Maybe aに写します。
MaybeはFunctorのインスタンスになっているので射関数も備えています。
Maybeを例に対象関数と射関数を図にしてみます。

Functor則
圏論の関手には、射の合成を保つこと、恒等射を保つことという条件がありました。
PureScriptのFunctorにおいてはこの条件をFunctor則と呼びます。
Categoryのときと同様に、具体的な型Maybe Numberを使ってFunctor則を満たすか検証するコードを書いてみます。
module Test.Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Number (isNaN)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "Maybe NumberはFunctor則を満たす" do
it "射の合成を保つ" do
let
a = Just 1.0
f = isNaN
g = show
(map (g <<< f) a) `shouldEqual` ((map g <<< map f) a)
it "恒等射を保つ" do
let a = Just 1.0
map identity a `shouldEqual` identity a
Maybe NumberはFunctor則を満たす
✓︎ 射の合成を保つ
✓︎ 恒等射を保つ
自己関手
自己関手とは、圏
そしてPureScriptの関手は、PureScriptの型の圏からPureScriptの型の圏への関手なので、自己関手ということになります。
まとめ
あらためて圏論の関手とPureScriptのFunctorを比べてみます。


自然変換
続いて、重要な概念である自然変換を見ていきます。
【定義】自然変換
自然変換

が可換になるものをいう。
また
上記の図式を対象とその間の射まで書いたもの


【定義】恒等変換
射に対して恒等射が定められ、関手に対して恒等関手が定められたように、自然変換に対して恒等変換(恒等自然変換ともいう)が定められる。


恒等変換の図式
対象とその間の射まで書いた図式

自然変換の合成
自然変換は射の一種なので、合成できる。
そして自然変換の合成には次のような合成の仕方がある。
- 垂直合成
- 水平合成
- 関手との水平合成
- 恒等変換との水平合成
順番にこれらの合成を見ていこう。
垂直合成
与えられた自然変換

について、合成された自然変換

が、
この合成を垂直合成という。

図式
対象とその間の射まで書いた図式

水平合成
垂直合成に対して水平合成もある。それは自然変換

を受け取り、自然変換

を作り出す。これを

の対角線として定義される。
言い換えると、

図:上記の水平合成を色分けして圏と対象まで描いた図式
対象とその間の射まで書いた図式

恒等変換との水平合成
水平合成の特別な場合に、自然変換と恒等変換の合成がある。
これはとりわけ重要で、それ専用の記法がある。たとえば
- 自然変換
{\footnotesize \alpha:G\rightarrow H} - 恒等変換
{\footnotesize 1_{F}:F\rightarrow F} - 関手
{\footnotesize F:\mathcal{A}\rightarrow \mathcal{B} } - 関手
{\footnotesize G:\mathcal{B}\rightarrow \mathcal{C} } - 関手
{\footnotesize H:\mathcal{B}\rightarrow \mathcal{C} }
に対して

ここで
これらの水平合成を図式にしてみます。
まず

図式
対象とその間の射まで書いた図式

次は

図式
対象とその間の射まで書いた図式

こちらも
関手との水平合成
自然変換は関手と合成することもできる。
- 自然変換
{\footnotesize \alpha:G\rightarrow H} - 関手
{\footnotesize F:\mathcal{A}\rightarrow \mathcal{B} } - 関手
{\footnotesize G:\mathcal{B}\rightarrow \mathcal{C} } - 関手
{\footnotesize H:\mathcal{B}\rightarrow \mathcal{C} }
があったとして、自然変換


図式
対象とその間の射まで書いた図式

また、


図式
対象とその間の射まで書いた図式

このような関手と自然変換の合成はwhiskeringと呼ばれる。
関手を恒等変換と同一視するならば、whiskeringは水平合成の特殊な場合である。
PureScriptの自然変換
PureScriptにおいては自然変換は次のように定義されています。
type NaturalTransformation :: forall k. (k -> Type) -> (k -> Type) -> Type
type NaturalTransformation f g = forall a. f a -> g a
infixr 4 type NaturalTransformation as ~>
fからgへの自然変換はf ~> gと表すことができます。
ここで着目したいのは上記の定義のどこにも圏論の関手に対応するFunctorが出てこないことです。
なぜか説明しましょう。
まず関手には対象に対する対象関数と射に対する射関数がありました。
例えば関手
そして
では、いま書いた例を
PureScriptの自然変換NaturalTransformationの定義f a -> g aと一致しますね。
更にf aやg aという定義には見覚えがあります。
そう、型構築子です。MaybeではMaybe aという定義になっていました。
この型構築子は対象関数に対応するものなので、意味合い的にPureScriptのf a -> g aの定義が自然変換だというのは一定納得できるものなのではないでしょうか。
恐らくFunctorが出てこないのは、Functorに定義されているmap関数は射関数にあたるもので、このNaturalTransformationを定義する上で制約が必須ではなかったからでしょう。
可換性の検証
具体的なFunctorとしてArrayとMaybe、関数として次の2つを用いて可換性を検証してみます。
head :: forall a. Array a -> Maybe anull :: String -> Boolean
headの定義を見ると、これは自然変換とみなせることがわかるでしょう。
nullは射関数map nullとして使います。
図式としてはこんな感じです。処理としては文字列の配列の先頭が空文字かどうか判定するような処理ですね。

検証するテストコードとして次のようなコードを書いてみました。
module Test.Main where
import Prelude
import Data.Array (head)
import Data.String (null)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "自然変換は可換である" do
let
nt_map = head <<< map null -- mapした後に自然変換
map_nt = map null <<< head -- 自然変換した後にmap
it "先頭が空文字の場合" do
nt_map ["", "any"] `shouldEqual` map_nt ["", "any"]
it "先頭が空文字でない場合" do
nt_map ["any", ""] `shouldEqual` map_nt ["any", ""]
自然変換は可換である
✓︎ 先頭が空文字の場合
✓︎ 先頭が空文字でない場合
モナド
長い道のりでしたが、ようやくモナドまでやってまいりました。
【定義】モナド
- 自己関手
{\footnotesize T:\mathcal{C}\rightarrow \mathcal{C}} - 自然変換
{\footnotesize \eta :1_{\mathcal{C}}\rightarrow T} - 自然変換
({\footnotesize \mu :T^{2}\rightarrow T} は{\footnotesize T^{2}} を表す){\footnotesize T\circ T:\mathcal{C}\rightarrow \mathcal{C} }
の3つ組
{\footnotesize \mu \circ T \mu=\mu \circ \mu T} {\footnotesize \mu \circ \eta T = 1_{T} = \mu \circ T \eta}

図式
ここで
(「関手との水平合成」の項で説明した通り、関手とその関手に対応する恒等変換は同一視できるので恒等変換との合成と考えてもいいのですが、よりシンプルに図式が書けるのでここでは関手との合成としました)
自然変換と関手の水平合成は自然変換になので、
さて、自然変換
ここから上記の条件と図式は次のように表すことができます。
{\footnotesize \mu_{A} \circ T(\mu_{A}) = \mu_{A} \circ \mu_{T( A)}} -
{\footnotesize \mu_{A} \circ \eta_{T(A)} = 1_{T(A)} = \mu_{A} \circ T(\eta_{A})}

図式
次に冗長かもしれませんが、いくつか補足しておきます。
【補足】モナドの定義に登場する自然変換について
{\footnotesize T( \mu _{A})} と {\footnotesize \mu _{T( A)}} の違い
これはどちらも
-
:{\footnotesize T(\mu _{A})}
自然変換 の{\footnotesize \mu} 成分である射{\footnotesize A} を関手{\footnotesize \mu_{A}:T(T(A))\rightarrow T(A)} で写してやると以下の射が得られます。{\footnotesize T}
( は関数なのでこの場合の{\footnotesize \mu_{A}} は射関数である){\footnotesize T}
{\footnotesize T( \mu _{A}) :T( T( T( A)))\rightarrow T( T( A))}

図式 -
:{\footnotesize \mu _{T( A)}}
これは を一つの塊として考えた自然変換{\footnotesize T(A)} の{\footnotesize \mu} 成分です。{\footnotesize T(A)}
つまり の{\footnotesize T(T(A)) \rightarrow T(A)} を{\footnotesize A} で置き換えてやると以下が得られます。{\footnotesize T(A)}
{\footnotesize \mu _{T( A)} :T( T( T( A)))\rightarrow T( T( A))}

図式
{\footnotesize T(\eta_{A})} と {\footnotesize \eta_{T( A)}} の違い
これはどちらも
-
:{\footnotesize T(\eta_{A})}
自然変換 の{\footnotesize \eta} 成分である射{\footnotesize A} を関手{\footnotesize \eta_{A}:A\rightarrow T(A)} で写してやると以下の射が得られます。{\footnotesize T}
{\footnotesize T(\eta_{A}):T(A)\rightarrow T(T(A))}

図式 -
{\footnotesize \eta_{T(A)}}
これは を一つの塊と考えた自然変換{\footnotesize T(A)} の{\footnotesize \eta} 成分です。{\footnotesize T(A)}
つまり の{\footnotesize A \rightarrow T(A)} を{\footnotesize A} で置き換えてやると以下が得られます。{\footnotesize T(A)}
{\footnotesize \eta _{T( A)} :T( A)\rightarrow T( T( A))}

図式
【補足】単位元律
条件2とは
が成り立つこと、でした。
この条件が意味しているのは、
PureScriptのモナド
最後にPureScriptのモナドを見ていきましょう。
PureScriptのモナドの定義
PureScriptのモナドである型クラスMonadは次の定義になっています。
class (Applicative m, Bind m) <= Monad m
Monadは型クラスApplicativeとBindを継承していますので合わせてこれらを載せておきます。
class Apply f <= Applicative f where
pure :: forall a. a -> f a
class Apply m <= Bind m where
bind :: forall a b. m a -> (a -> m b) -> m b
infixl 1 bind as >>=
join :: forall a m. Bind m => m (m a) -> m a
join m = m >>= identity
(joinは後ほど説明で使うので一緒に載せてあります)
自然変換との対応
圏論のモナドには、自己関手
これからこの自己関手および2つの自然変換と、PureScriptとの対応を見ていきます。
まず自然変換の方を見ていきます。
自然変換 {\footnotesize \eta}
自然変換
これは恒等関手
上記の関手の名前を
ここでPureScriptの型クラス
Applicativeの関数pureの定義と見比べてみましょう。
class Apply f <= Applicative f where
pure :: forall a. a -> f a
a -> f aとなっており、形が一致していますね。
つまり自然変換pureというわけですね。
自然変換 {\footnotesize \mu}
自然変換
適当な対象
上記の関手の名前を
ここでPureScriptの型クラス
Bindを見てみます。
class Apply m <= Bind m where
bind :: forall a b. m a -> (a -> m b) -> m b
bindという関数が定義されていますが、
join関数です。
こちらを
join :: forall a m. Bind m => m (m a) -> m a
join m = m >>= identity
形が一致していますね。
joinの実装を見るとbindとの関係性がわかります。
join m = m >>= identity
>>=は次のように演算子のエイリアスとして宣言されています。つまりjoinはbindを使って実装されています。
infixl 1 bind as >>=
このようにjoinはbindを利用して実装されていますが、逆にjoinを使ってbindを実装することができます。
joinを使ってbindを実装する例
まず型クラスBindにbindではなくjoinを定義します。
class Apply m <= Bind m where
join :: forall a. m (m a) -> m a
joinを使ってbindを実装するにはこのようにすればよいでしょう。
bind :: forall a b m. Bind m => m a -> (a -> m b) -> m b
bind m f = join (f <$> m)
<$>を使っているのでFunctorである必要はありますが、joinを使ってbindが実装できることがわかると思います。
まとめると
- 自然変換
に対応するのは{\footnotesize \mu} join関数である。 -
join関数とbind関数はいずれか一方だけ定義すればもう一方を実装することができる。
という関係になっています。
自己関手Tとの対応
最後に自己関手です。
関手には対象関数と射関数があるので、それぞれ対応を見ていきます。
対象関数との対応
自己関手pure関数におけるfやjoin関数におけるmに対応しています。
まずpureの方を確認します。
pure :: forall a. a -> f a
自然変換 pureの定義と比較してみると
というように形が一致しました。
次にjoinを確認します。
join :: forall a. m (m a) -> m a
自然変換 joinの定義と比較してみると
というように形が一致しました。
射関数との対応
関手map関数でした。
ということは map pureやmap joinに対応することになります。
まず
replでmap pureの型を見るとこのように表示されます(見づらいので改行しています)。
> :t map pure
forall (f8 :: Type -> Type) (t11 :: Type -> Type) (t12 :: Type).
Functor f8 => Applicative t11 => f8 t12 -> f8 (t11 t12)
もうちょっと見やすくするため関手の型をMaybeとしてみます。
> import Data.Maybe
> t = map@Maybe
> η = pure@Maybe
> :t t η
forall (t5 :: Type). Maybe t5 -> Maybe (Maybe t5)
というように形が一致しました。
次に
pureと同様に関手の型をMaybeとしreplでmap joinの型を見ると次のように表示されます。
> import Data.Maybe
> t = map@Maybe
> μ = join :: forall a. Maybe (Maybe a) -> Maybe a
> :t t μ
forall (t7 :: Type). Maybe (Maybe (Maybe t7)) -> Maybe (Maybe t7)
というように形が一致しました。
結合律の検証
結合律
この条件において自然変換の成分を
となります。
検証のために具体的な型を与えます。
Intとし、関手Arrayとしますね。
すなわちArray Intで、Array (Array (Array Int))となります。
そして join関数であることを踏まえると Array Intに対するjoinなので、Array (Array (Array Int))をArray (Array Int)に写すというjoinになります。つまり単なるjoinです。
一方 map joinになります。
これで材料が揃いました。
結合律:
に対するPureScriptの擬似コードはjoin <<< map join = join <<< joinとなります。
(=で比較はできませんし、そもそも関数自体の比較ができないのでこれはあくまで擬似コード)
結合律の検証は次の単体テストで行います。具体的な値を与えています。
module Test.Main where
import Prelude
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "Array Intはモナドの条件を満たす" do
it "結合律を満たす" do
let
t3 = [
[
[1, 2],
[3, 4]
], [
[5, 6],
[7, 8]
]
]
(join <<< map join) t3 `shouldEqual` (join <<< join) t3
結果は次の通りです。
Array Intはモナドの条件を満たす
✓︎ 結合律を満たす
単位元律の検証
単位元律
この条件において自然変換の成分を
となります。
結合律の検証と同じく具体的な型を与えます。
対象Int
関手Array
とします。
つまりArray Intとなります。
自然変換join
自然変換pure
となります。
したがって
単位元律:
に対するPureScriptの擬似コードはjoin <<< pure = identity = join <<< map pureとなります。
検証のテストコードは次になります。
module Test.Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "Array Intはモナドの条件を満たす" do
it "単位元律を満たす" do
let
ta = [10, 20, 30]
(join <<< pure) ta `shouldEqual` identity ta
(join <<< map pure) ta `shouldEqual` identity ta
結果は次の通りです。
Array Intはモナドの条件を満たす
✓︎ 単位元律を満たす
おまけ:モナド則の検証
ところでプログラミングにおけるモナドには、モナド則という条件があります。
プログラマーとしてはこちらの方が馴染みがあるかもしれませんね。
このモナド則をPureScriptで表すとこうなるでしょう。
- 左単位元律:
pure a >>= h = h a - 右単位元律:
m >>= pure = m - 結合律:
(m >>= g) >>= h = m >>= (\x -> g x >>= h)
このモナド則を検証するテストを書いてみます。
モナドはMaybeとしました。
module Test.Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (launchAff_)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)
main :: Effect Unit
main = launchAff_ $ runSpec [consoleReporter] do
describe "Maybeはモナド則を満たす" do
it "左単位元律を満たす" do
-- `pure a >>= h = h a`
let h = Just
(pure 10 >>= h) `shouldEqual` h 10
it "右単位元律を満たす" do
-- `m >>= pure = m`
let m = Just 10
(m >>= pure) `shouldEqual` m
it "結合律を満たす" do
-- (m >>= g) >>= h = m >>= (\x -> g x >>= h)
let
m = Just 1
g = \x -> pure (x + 10)
h = \x -> pure (show x)
((m >>= g) >>= h) `shouldEqual` (m >>= (\x -> g x >>= h))
まとめ


おわりに
圏論とPureScriptとの繋がりを見てきましたが、いかがでしたか。
私自身はこのようにバックグラウンドにある理論との繋がりが見える言語は面白いなと感じました。
圏論との繋がりという意味では、まだ他にも面白そうなテーマがあるので、また機会があれば書いてみたいと思います。
参考資料
参考にさせていただいた書籍およびWeb上の資料です。
-
ベーシック圏論 普遍性からの速習コース - Tom Leinster (著), 斎藤 恭司 (監修), 土岡 俊介 (翻訳)
定義は書籍を参照したかったため購入 -
ゆる圏 YouTube - @yuruken
書籍『ベーシック圏論』を読む際、参考にさせていただきました。 -
圏と関手 【 圏論とモナド #1 / 数学 解説 】 - 豊穣ミノリ
豊穣ミノリ / Hojo Minori さんの一連の動画です(この続きもあります)。視覚的にわかりやすく、ざっくり理解するのに助かりました。 -
自然変換・関手圏 - alg-d
壱大整域で有名なalg-dさんのテキストです。動画も投稿されており、拝見しました。 -
関手と自然変換の計算に出てくる演算子記号とか - 檜山正幸
これまた有名な檜山正幸さんが圏論の演算子記号について書かれた記事です。
Discussion