以前にTypeScriptでモナドを書いてみたことがあるのですが、名前の組織化がイマイチな感じだったので改善してみました。
内容:
モナド
モナドなんて知らないぞ、って方は次の記事を読んでみてください。最近、入門的記事をあんまり書かないので、だいぶ昔の記事ですが。
- 世界で一番か二番くらいにやさしい「モナド入門」 (2006年)
- 圏論やモナドが、どうして文書処理やXMLと関係するのですか? (2007年)
- モナドの定義とか (2006年)
- 圏、関手、モナドはどうしたら分かるの? (2007年)
いや、そもそも圏論が分からん、て方は次の記事からはじめてはいかがでしょう。
- はじめての圏論 その第1歩:しりとりの圏 (2006年)
このブログは、モナドで溢れてます。あまり整理されてませんが、探せば色々な話題が見つかるでしょう。
モナドの定義としては、クライスリ拡張オペレーターを使う“拡張方式”と、自己関手圏内のモノイドとして定義する“モノイド方式”があります。今回使うのは“モノイド方式”のほうです。
Cを圏とします。C上のモナド〈monad〉は3つの構成素を持ちます。
- C上の自己関手 F:C→C
- 自然変換 μ::F*F⇒F:C→C
- 自然変換 η::C^⇒F:C→C
C^は、圏Cの恒等関手IdCのことです。その他、ここで使う記号については次を参照してください。
モナドを(F, μ, η)のように書きます。F:C→C をモナドの台関手〈underlying functor〉、μ::F*F⇒F:C→C をモナドの乗法〈multiplication〉、η::C^⇒F:C→C をモナドの単位〈unit〉と呼びます。3つ組(F, μ, η)がモナドであるためには、次の法則を満たす必要があります。
- [結合律] (μ*F^);μ = (F^*μ);μ
- [左単位律] (η*F^);μ = F^
- [右単位律] (F^*η);μ = F^
単位律における左右の別は、書き方により変わります。反図式順記法で書くなら:
- [結合律] μ(F^・μ) = μ(μ・F^);
- [右単位律] μ(F^・η) = F^
- [左単位律] μ(η・F^) = F^
2つのモナド M = (F, μ, η), N = (G, ν, ε) があるとき、記号を節約するために、M = (FM, μM, ηM), N = (FN, μN, ηN) のような書き方をします。さらには、記号の乱用により、F = (F, μF, ηF), G = (G, μG, ηG) とも書きます。モナド全体とモナドの台関手を同じ文字で表します。FやGがモナドを表しているのか、台関手を表しているのかは文脈で判断してください。
モナドの各部を表す名前
以前次の記事で、TypeScriptでモナドを書いてみました。
上記の過去記事内で:
型構成子と型パラメータを持つ関数があれば、とりあえずモナドは定義できるのですが、モナドを構成する型構成子と総称関数達をうまくまとめる機構が欠けている感じはします。
“うまくまとめる機構”が欠けているとは、名前の組織化/名前の管理がイマイチだということです。次の記事はその点に関する改良案です -- が、満足できる所までいってません。
TypeScriptのモジュールとは内部モジュールのことで、現在は名前空間〈namespace〉と呼ばれています。今回使う道具も名前空間〈内部モジュール〉です。
この記事の目的・目標は、前節で導入した F = (F, μF, ηF) に出来るだけ近い書き方をTypeScriptで実現することです。ただし、関手Fを(F, mapF)のように分解します。この点を以下で説明しましょう。
関手 F:C→C は、対象部分〈object part〉と射部分〈morphism part〉に分解できます。
- Fobj:Obj(C)→Obj(C)
- Fmor:Mor(C)→Mor(C)
対象部分Fobjを単にFと書き、射部分FmorをmapFと書くことにします。mapの由来は圏論ではなくて関数型言語で、ある種の高階関数をmap関数と呼ぶからです。この書き方に従うと、モナドは次のような4つ組で書けます。
- F = (F, mapF, μF, ηF)
モナド全体と台関手の射部分が同じ名前になります(オーバーロードされます)。Gが別なモナドの名前なら、次のように書けます。
- G = (G, mapG, μG, ηG)
TypeScriptへの翻訳
例題にはMaybeモナドを使います。「TypeScriptのモジュール:Maybeモナドの例」と同じ例なので、必要があればソチラを参照してください。
例題のモナドの名前がMaybeですから、次のようになります。
- Maybe = (Maybe, mapMaybe, μMaybe, ηMaybe)
しかしプログラミング言語では、上付き添字やギリシャ文字は使えません。次のようにします。
そうすると:
- Maybe = (Maybe, Maybe.map, Maybe.join, Maybe.unit)
上記モナドの4つの構成素が、プログラミング言語TypeScriptにおいて何になるかと言うと:
- Maybeは、型パラメータを1つ取る型構成子である。
- Maybe.mapは、型パラメータを2つ取る総称高階関数である。Maybe.map<X, Y>:Maybe<X>→Maybe<Y>
- Maybe.joinは、型パラメータを1つ取る総称関数である。Maybe.join<X>:Maybe<Maybe<X>>→Maybe<X>
- Maybe.unitは、型パラメータを1つ取る総称関数である。Maybe.unit<X>:X→Maybe<X>
モナド全体を表す名前Maybeは、名前空間の名前とします。TypeScriptでは、型構成子(型パラメータを持つ型別名)と名前空間の名前が同じになっても問題ありません*1。
TypeScriptコード
早速、前節の翻訳方針にしたがって、MaybeモナドをTypeScriptで書いてみます。
// 補助的なクラス class MaybeValue<X> { defined: boolean; value: X|undefined; constructor(defined: boolean, value: X|undefined) { this.defined = defined; this.value = value; } } // 型構成子=モナドの台関手の対象部分 type Maybe<X> = MaybeValue<X>; // 名前空間=モナドの他の部分をまとめる入れ物 namespace Maybe { // map総称関数=モナドの台関手の射部分 export function map<X, Y>(f: (x:X)=>Y): (mx:Maybe<X>)=>Maybe<Y> { return ( (mx: Maybe<X>)=>{ if (mx.defined) { return new MaybeValue<Y>(true, f(mx.value)); } else { return new MaybeValue<Y>(false, undefined); } }); } // join総称関数=モナドの乗法 export function join<X>(mmx: Maybe<Maybe<X>>): Maybe<X> { if (mmx.defined) { return mmx.value; } else { return new MaybeValue<X>(false, undefined); } } // unit総称関数=モナドの単位 export function unit<X>(x: X): Maybe<X> { return new MaybeValue<X>(true, x); } }
これで、名前はかなりスッキリと編成されました。けっこう満足できます。
機能的にはこれで十分ですが、HaskellのMaybeモナドだと、Just, Nothingというデータ構成子が使えます。これらはあれば便利なので、追加しておきましょう。map, join, unitのなかも少し書き換えました。
// ...省略... // 型構成子=モナドの台関手の対象部分 type Maybe<X> = MaybeValue<X>; // 名前空間=モナドの他の部分をまとめる入れ物 namespace Maybe { // JUST総称関数 確定した値を生成する export function JUST<X>(x: X): Maybe<X> { return new MaybeValue<X>(true, x); } // NOTHING総称関数 値がないことを示す値を生成する export function NOTHING<X>(): Maybe<X> { return new MaybeValue<X>(false, undefined); } // map総称関数=モナドの台関手の射部分 export function map<X, Y>(f: (x:X)=>Y): (mx: Maybe<X>)=>Maybe<Y> { return ( (mx: Maybe<X>)=>{ if (mx.defined) { return new MaybeValue<Y>(true, f(mx.value)); } else { return NOTHING<Y>(); } }); } // join総称関数=モナドの乗法 export function join<X>(mmx: Maybe<Maybe<X>>): Maybe<X> { if (mmx.defined) { return mmx.value; } else { return NOTHING<X>(); } } // unit総称関数=モナドの単位 export function unit<X>(x: X): Maybe<X> { return JUST<X>(x); } }
[追記]unitとJUSTがまったく同じ機能で重複している理由は:
- unitは、どんなモナドでも備えるべき関数の名前です。TypeScriptには、名前空間の仕様を記述する機能はないので、人のあいだのお約束(命名規則)です。
- JUSTは、Maybeモナド特有の名前で、習慣的に使われているので入れてみたものです。他のモナドでは無意味な名前だろうし、必須でもありません。[/追記]
おわりに
関手の射部分、モナドの乗法/単位に関する名前(ローカル名)は、コレダという定番はなくて、けっこうゆらいでいます。
グローバルな合意は難しいですが、ローカル・ルールでいいなら、どれかに決めて守ればいいだけです。
実は、一種のテストとして、モナド法則を部分的に確認してみたのですが、その話は長くなりそうなので割愛しました。別な記事にするかも知れません。
今回の名付け方式を使って、モナドの準同型、ベックの分配法則、テンソル強度などもTypeScriptで書けそうです。これも別記事ですね。
*1:この事実に気付いたことで、今回の名前方式が実現できました。