将来書かれるかも知れない(書かれないかも知れない)記事から参照される具体例を先に書いておきます。内容としては、関数達のタプル構成とコタプル構成です。タプル構成とコタプル構成がひとつの設定内で出てくる例題になっています。
タプル構成とは、キー付きタプル型データを返す関数を組み立てる方法、コタプル構成とは、タグ付きユニオン型データを受け取る関数を組み立てる方法です。
[追記]本題の記事を書きました。→「タプル・コタプルとVΣ計算」[/追記]
内容:
- 製品IDから製品情報を出力
- フィールドごとの素材関数と組み立て
- 素材関数を単一の関数データに
- 抽象的セッティング: タプル構成
- 人を表すデータ
- 価格を計算する関数
- 抽象的セッティング: コタプル構成
- おわりに
製品IDから製品情報を出力
製品情報の型を、次のように定義します。
type ProductInfo = { "id" : ProductId, // 製品ID "name" : ProductName, // 製品名 "price" : Price // 価格 }
これは、TypeScriptによるデータ型定義です。個々のデータはJSON形式で書くことを意識して、型定義もJSONぽい構文にしています。このようなデータ型を、ここではキー付きタプル型〈keyed tuple type〉と呼ぶことにします。次のような呼び名のほうが一般的かも知れません。
- 名前付きタプル型
- 構造体型
- レコード型
- ディクショナリ型
- 連想配列型
ときに、オブジェクト型とかkey/value pairs型ともいいます。依存型理論では、パイ型〈Pi type〉と呼びます*1。
ProductInfo型のフィールドの型である ProductId, Name, Price の正体は明かさないことにします。気になるなら、次のように考えておいてください。
- ProductId型は、文字列型の部分集合型。すべての文字列が製品IDになるわけではない。
- ProductName型も、文字列型の部分集合型。すべての文字列が製品名になるわけではない。
- Price型は、整数型の部分集合型。負の数や、べらぼーに大きな整数は価格(の値)にはならない。
- 現実のプログラミング言語は部分集合型が苦手なので、文字列型/整数型で妥協するしかないかも知れない。
キー付きタプル型 ProductInfo のフィールドのキー(である文字列)を列挙した型を ProductInfoKey型とします。
type ProductInfoKey = "id" | "name" | "price"
TypeScriptにはもっといい方法があって、次のようにも書けます(が、内容は同じです)。
type ProductInfoKey = keyof ProductInfo
今、製品の価格が買う人により変わるとします。例えば、お得意さんなら値引きするとか。製品IDと買う人を渡すと製品情報(の名前付きタプル)を返すような関数を考えます。その関数を makeProductInfo としましょう。
function makeProductInfo(id : ProductId, per : Person) : ProductInfo { // ... }
Person は人を表すデータ型です。Person については後で詳しく述べます。
フィールドごとの素材関数と組み立て
ProductInfo型のデータは、キー "id", "name", "price" で識別できる3つのフィールド〈メンバー | 属性 | プロパティ | 成分 | 項目〉があります。それぞれのフィールドごとに、フィールド値を出力する関数を準備しましょう。
- idフィールド用: makeId関数
- nameフィールド用: makeName関数
- priceフィールド用: makePrice関数
function makeId(id : ProductId, per : Person) : ProductId { return id; } function makeName(id : ProductId, per : Person) : ProductName { return getProductNameFromId(id); } function makePrice(id : ProductId, per : Person) : Price { return calcPrice(id, per); }
makeId関数とmakeName関数では、第二引数 per を使ってません。コンパイラに警告されますが、別に悪いことしてるわけじゃないです。渡された引数を、必ず使う義務はありません。
makeId関数はgetProductNameFromId関数を呼んでいるだけですが、getProductNameFromId関数はデータベース・テーブルを引くとかして製品IDから製品名を取得します。makePrice関数から呼んでいるcalcPrice関数については後で述べます。
フィールドごとの素材関数が準備できたので、これらから目的の関数 makeProductInfo を組み立てることができます。
function makeProductInfo(id : ProductId, per : Person) : ProductInfo { return { "id" : makeId(id, per), "name" : makeName(id, per), "price" : makePrice(id, per) }; }
素材関数を単一の関数データに
前節では、3つのフィールドに対して素材関数 makeId, makeName, makePrice を準備しました。これらの関数達をキー付きタプル型データとしてまとめましょう。まず型定義から。
type ProductInfoFieldFunction = { "id" : (id : ProductId, per : Person) => ProductId, "name" : (id : ProductId, per : Person) => ProductName, "price" : (id : ProductId, per : Person) => Price }
3つの関数をまとめたProductInfoFieldFunction型データは次のようです。フィールドの値として関数(アロー関数式〈ラムダ式〉)をセットしています。
const MakeField : ProductInfoFieldFunction = { "id" : (id, per) => id, "name" : (id, per) => getProductNameFromId(id), "price" : (id, per) => calcPrice(id, per) }
MakeField を使ったmakeProductInfo関数の定義は次のようになります。
function makeProductInfo(id : ProductId, per : Person) : ProductInfo { return { "id" : MakeField["id"](id, per), "name" : MakeField["name"](id, per), "price" : MakeField["price"](id, per) }; }
抽象的セッティング: タプル構成
話が具体的過ぎて、かえって何を言いたいかの趣旨が伝わらないかも知れないので、抽象的セッティングを述べておきます。名前・記号を次のように置きます。
抽象的セッティング | 具体的セッティング |
$`X`$ | ProductId |
$`Y`$ | Person |
$`I`$ | ProductInfoKey |
$`\prod_{i\in I}A_i`$ | ProductInfo |
$`F_i`$ | MakeField[key] |
$`f`$ | makeProductInfo |
具体例におけるキーの集合 ProductInfoKey = {"id", "name", "price"}
の3つの名前文字列の代わりに番号を使うことにします。つまり、$`I = \{1, 2, 3\}`$ 。キー付きタプル型に相当する $`\prod_{i\in I}A_i`$ は次のように解釈できます。
抽象的セッティング | 具体的セッティング |
$`A_1`$ | ProductId |
$`A_2`$ | ProductName |
$`A_3`$ | Price |
$`A_1\times A_2 \times A_3`$ | ProductInfo |
関数 $`f`$(makeProductInfo
に相当)のプロファイル(域と余域)は次のようになります。
$`\quad f:X \times Y \to \prod_{i\in I}A_i = A_1\times A_2 \times A_3`$
関数 $`f`$ を組み立てる素材であるところの $`F_i\;(i = 1, 2, 3)`$ は:
$`\quad F_1 : X \times Y \to A_1\\
\quad F_2 : X \times Y \to A_2\\
\quad F_3 : X \times Y \to A_2
`$
$`F_i`$ 達から組み立てた $`f`$ 、あるいは組み立てる工程をデカルト・タプル(デカルト・タプリング)といいます。このことを一行で書けば:
$`\quad f = \langle F_i\rangle_{i\in I} = \langle F_1, F_2, F_3\rangle`$
山形括弧はデカルト・タプルを表します。通常、$`f`$ と $`F`$ はオーバーロードされるので、
$`\quad f = \langle f_i\rangle_{i\in I}`$
と書きます。
人を表すデータ
製品の価格は、買う人により変わるのですが、人を表すデータを三種類に分けます。
- 従業員番号
- 顧客番号
- 顧客メールアドレス
Person型はこれら三種のどれかになります。どの種類のデータであるかを“タグ”で識別します。タグは次のように決めましょう。
- 従業員番号であることを示すタグ: emp (employeeから)
- 顧客番号であることを示すタグ: cust (customerから)
- 顧客のEメールアドレスであることを示すタグ: email
タグ付きデータを表すには、タグと値をペア(TypeScriptでは長さ2の配列)で表すことにします。すると、次のようなデータ型定義になります。
type Person = ["emp", EmpNumber] | ["cust", CustNumber] | ["email", EMail]
EmpNumber, CustNumber, EMail の正体も明かさないので、適当に想定してください。
Person型で使っているタグを列挙した型も定義しておきます。
type PersonTag = "emp" | "cust" | "email"
タグと値を組み合わせてタグ付きデータを作れば、やり方はなんだっていいのですが、上の方法よりは多少“映える”方法を示しておきます。
type PersonTag = "emp" | "cust" | "email" interface PersonData { "tag" : PersonTag, "value" : any } interface PersonEmp extends PersonData { "tag" : "emp", "value" : EmpNumber } interface PersonCust extends PersonData { "tag" : "cust", "value" : CustNumber } interface PersonEMail extends PersonData { "tag" : "email", "value" : EMail } type Person = PersonEmp | PersonCust | PersonEMail
[/補足]
タグ付きデータで構成されるデータ型をタグ付きユニオン型〈tagged union type〉と呼びます。次のような呼び名もあります。
- 判別可能ユニオン型〈discriminated union type〉
- 直和型
- 無共通部分ユニオン型〈disjoint union type〉
- バリアント型
依存型理論ではシグマ型〈Sigma type〉と呼びます。
というわけで、Person型はタグ付きユニオン型です。
価格を計算する関数
製品IDと人のデータを渡されて価格を計算する関数は次のようです。
function calcPrice(id : ProductId, per : Person) : Price { // ... }
入力のひとつであるPerson型はタグ付きユニオン型なので、場合分けをしなくてはなりません。この“場合”をケース〈case〉ともいいます。ケースごとの処理をする関数が素材関数になります。
今度は最初から、CalcPriceCaseFunction という関数データを作ってしまいましょう。
type CalcPriceCaseFunction = { "emp" : (id : ProductId, en : EmpNumber) => Price, "cust" : (id : ProductId, cn: CustNumber) => Price, "email" : (id : ProductId, mail : EMail) => Price } const CalcPrice : CalcPriceCaseFunction = { "emp" : (id : ProductId, en : EmpNumber) => adjust(getPriceFromId(id) * 0.8), "cust" : (id : ProductId, cn: CustNumber) => adjust(getPriceFromId(id) * getDiscountRateFromNumber(cn)), "email" : (id : ProductId, mail : EMail) => adjust(getPriceFromId(id) * getDiscountRateFromEmail(mail)) }
ここで出現する関数は:
- adjust関数: 必要があれば、価格の値を調整する。
- getPriceFromId関数: 製品IDから定価を取得する。
- getDiscountRateFromNumber関数: 顧客番号から、顧客の割引率を取得する。
- getDiscountRateFromEmail関数: 顧客のEメールアドレスから、顧客の割引率を取得する。
これらの素材関数を組み合わせて、価格を計算する関数 calcPrice を構成できます*2。
function calcPrice(id : ProductId, per : Person) : Price { let tag : PersonTag = per[0]; switch (tag) { case "emp" : let empNum = per[1] as EmpNumber; return CalcPrice["emp"](id, empNum); case "cust" : let custNum = per[1] as CustNumber; return CalcPrice["cust"](id, custNum); case "email" : let mail = per[1] as EMail; return CalcPrice["email"](id, mail); } }
抽象的セッティング: コタプル構成
価格を計算する関数の抽象的セッティングも述べておきます。名前・記号を次のように置きます。
抽象的セッティング | 具体的セッティング |
$`X`$ | ProductId |
$`J`$ | PersonTag |
$`\sum_{j\in J}B_j`$ | Person |
$`Z`$ | Price |
$`G_j`$ | CalcPrice[tag] |
$`g`$ | calcPrice |
タプル構成のセッティングと一緒に考えれば $`Y = \sum_{j\in J}B_j`$ 、$`A_3 = Z`$ の関係があります。しかし、それは無視してください。タプル構成とは独立にコタプル構成を考えることができるので。
具体例におけるタグの集合 PersonTag = {"emp", "cust", "email"}
の3つの名前文字列も番号に置き換えます。つまり、$`J = \{1, 2, 3\}`$ 。タグ付きユニオン型に相当する $`\sum_{j\in J}B_j`$ の具体的な定義は:
$`\quad \sum_{j\in J}B_j \\
= B_1 + B_2 + B_3 \\
= \bigcup_{j\in J}(\{j\}\times B_j) \\
= (\{1\}\times B_1)\cup(\{2\}\times B_2)\cup(\{3\}\times B_3)`$
$`\sum_{j\in J}B_j`$ は次のように解釈できます。
抽象的セッティング | 具体的セッティング |
$`B_1`$ | EmpNumber |
$`B_2`$ | CustNumber |
$`B_3`$ | EMail |
$`B_1 + B_2 + B_3`$ | Person |
関数 $`g`$(calcPrice
に相当)のプロファイル(域と余域)は次のようになります。
$`\quad g:X \times \sum_{j\in J}B_j \to Z`$
関数 $`g`$ を組み立てる素材であるところの $`G_j\;(j = 1, 2, 3)`$ は:
$`\quad G_1 : X\times B_1 \to Z\\
\quad G_2 : X\times B_2 \to Z\\
\quad G_3 : X\times B_3 \to Z
`$
$`G_j`$ 達から組み立てた $`g`$ 、あるいは組み立てる工程を余デカルト・コタプル(余デカルト・コタプリング)といいます。このことを一行で書けば:
$`\quad g = (\!| G_j |\!)_{j\in J} = (\!| G_1 \mid G_2 \mid G_3 |\!)`$
凸レンズ形括弧は余デカルト・コタプルを表します*3。通常、$`g`$ と $`G`$ はオーバーロードされるので、
$`\quad g = (\!| g_j |\!)_{j\in J}`$
と書きます。
おわりに
デカルト・タプルと余デカルト・コタプルは、圏論的双対性の一例で、極限と余極限の特殊なケースになっています。$` \langle f_i\rangle_{i\in I}`$ や $`(\!| g_j |\!)_{j\in J}`$ と書いたときのインデックスの集合 $`I, J`$ は有限集合に限定する必要はなくて、(概念的には)無限集合でもかまいません。インデックス集合を任意の集合としたデカルト・タプル/余デカルト・コタプルは、カリー/ハワード/ランベック対応を通じて、論理の全称限量子/存在限量子に対応します。
製品情報の出力だの、割引した価格の計算だのが、圏論的双対性/極限・余極限、全称限量子/存在限量子に繋がっているのは楽しいですね。その楽しい話は将来書かれるかも知れない(書かれないかも知れない*4)。
[追記]本題の記事を書きました。→「タプル・コタプルとVΣ計算」[/追記]