Quantcast
Channel: 檜山正幸のキマイラ飼育記 (はてなBlog)
Viewing all 1207 articles
Browse latest View live

[雑記/備忘]「関数型プログラミングはオブジェクト指向の正当な後継」なの?

$
0
0

オブジェクト指向を知っている人々に、「関数型もオブジェクト指向と大差ないよ、大丈夫だよ」とお誘いする記事は大いに存在意義があると思います。

上記の記事は、そういう目的を持って書かれたのでしょう。その内容(目次)は次のようです(僕のこの記事の目次じゃないよ)。

  1. 対象読者
  2. なぜこの記事を書こうと思ったのか?
  3. なぜ関数型プログラミングはわかりにくいのか?
  4. オブジェクト指向の負の遺産を捨てよう
  5. 関数型プログラミングの概要
  6. 「阿吽の呼吸」とも言うべき使いやすさの拡張
  7. 型にまつわる考察
  8. まとめ

最初のほうを読むと、言ってることはまっとうで好感を持てます。が、「5. 関数型プログラミングの概要」の節あたりから雲行きが怪しくなって、ちょっと何言ってるかわかんない((c)サンドウィッチマン)。

檜山のこの記事の内容:

  1. 真面目なポエム
  2. モナドっておいしいの?
  3. オブジェクトは仮想機械
  4. そもそも関数とは何?
  5. 制御構造不要なら楽ちん!
  6. それで結局、関数型プログラミングはオブジェクト指向の後継なの?

真面目なポエム

「んんんーー??」と思って「はてなブックマーク」コメントをチェックしたら、id:kmizushimaさんが「ポ、ポエムだー!!」と叫んでいました。

僕の言葉で言えば、retemoさんによる当該Qiita記事は「具体的じゃない」ものです。僕の「具体的/抽象的」の用法は、たぶん標準的日本語とズレていて、「具体的じゃない」は、「掴み所がない」とか「あやふや過ぎる」といった意味で使います(抽象的かどうかとは無関係)。詳細は次の記事:

retemoさん記事は、何か言いたいことがあるのは確かだし、それがひどくトンチンカンだとも思いません。具体性に欠けるので、“ちょっと何言ってるかわかんない”(kmizushimaさんの言う“ポエム”)になっているのでしょう。“何言ってるかわかんない”けど、「真面目さ」が感じられて、僕の好感はさほど損なわれませんでした。

意味不明な記述を僕なりに解釈・注釈し(それが無理な所もあります)、多少の補足をしてみます。解釈に推測が入るので、「たぶん…らしい/でしょう」の表現が多用されます。歯切れ悪いけど、推測だからね。

このテの話題は、当ブログ「キマイラ飼育記」で10年以上に渡って扱ってきたものですから、掘り返せば、たいていのことが過去記事にあります。再度説明する代わりに適宜リンクを挿入します。大部分の説明を参照にしても、それなりに長い記事になってしまいました。

混乱が起きないように注意しておくと、引用はすべてretemoさん記事からです。自分の記事は引用してません。参照のみです。それでも参照と引用が混乱しそうなところには横線を入れて区切ってます。

モナドっておいしいの?

関数型プログラミングはオブジェクト指向の正当な後継である - Qiita 3.なぜ関数型プログラミングはわかりにくいのか?》

関数だけをとっても理解しづらいのですが、さらに高度な概念で構成されるモナドについては聞けば聞くほど混乱が増すばかりです。

この問題をなんとか改善しようと言葉を選んでいるケースもあるのですが、それはそれで「詩的」というか「俳句的」というか、”ふんわり"し過ぎな傾向があります。

同感です。“ふんわり”し過ぎを避けて、でも厳密過ぎもしないレベルの説明を書いたことがあります(10年以上前)。

同様な趣旨、具体例で(9年以上前):

圏論に慣れたきたら、「モナドとは自己関手圏のモノイドなり」と理解するのが良いでしょう。

「圏論? 全然わからん!」なら、シリトリから始めましょう(これも10年たった)。

オブジェクトは仮想機械

関数型プログラミングはオブジェクト指向の正当な後継である - Qiita 4.オブジェクト指向の負の遺産を捨てよう》

オブジェクト指向のオブジェクトは[...snip...]古き良きチューリングマシンから今日の仮想化技術までを含む「抽象的な概念」としてのコンピュータです。

オブジェクトを仮想機械とみなすのも共感・賛同します

ここで言う仮想機械は、あくまで概念的なステートマシンです。

ステートマシンは「状態」と「遷移」、そして遷移を引き起こす「イベント」で構成されます。イベントにはトリガーやガード条件を含まれていて、「制御」と言いなおすことができます。同様に状態と遷移も「変数」及び「演算」と言い換えることができます。この「変数、演算、制御」がプログラミング言語や抽象的コンピュータの3大要素です。

僕は「言い換え」はしないほうがいいと思いますが、まー、ここは好みの問題としましょう(突っ込まない)。「イベント」はラベル付き状態遷移系(labeled transition system)のラベル、オートマトンの入力記号のようなものだろう、と、たぶん。「制御」が何を意味するか? ハッキリとは理解できませんでした。ガード条件を引き合いに出しているので、状態点に対する述語(状態空間上で定義された真偽値関数)をp、遷移を引き起こす関数(状態空間上の自己写像)をaとして、

  • if (p(x)) then a(x)

のような条件付き実行を「制御」と呼んでいるのでしょう。

実際、クリーネ代数の演算(順次結合、非決定性選択、非決定性繰り返し)に、if (p) then a というスタイルの「制御」(ガード付きの実行)を入れると、whileプログラムをシミュレートできます。

よって、「順次結合、非決定性選択、非決定性繰り返し」を暗黙に前提した上で、仮想マシンとしてのオブジェクトが「変数(状態)」「演算(遷移)」「制御(ガード条件)」から構成される、とするのは、悪くない定式化だと思います。

これら4つがオブジェクト指向を代表する技術トピックでしょう。[...sinp...]この4大トピックが1つ目の「定性的な物差し」です。

「これら4つ」とは、次のことらしいです。

  1. 継承と多態性の技術
  2. 独立したオブジェクト間をメッセージングでつなぐ技術
  3. 問題領域の区割りと仮想機械の区割りを関連付ける技術
  4. 仮想機械間の役割分担

僕は、原則だのトピックが4つだ5つだ、という数え上げには興味がないので、どうでもいいですが、「定性的な物差し」ということですから留意しておきましょう。「物差し」と言っているのは、これら4項目において、オブジェクト指向と関数型を比較しようじゃないか、という意図(らしい)です。

そもそも関数とは何?

関数型プログラミングはオブジェクト指向の正当な後継である - Qiita 5. 関数型プログラミングの概要》

関数型プログラミングの関数の特徴を簡潔に理解しようとすると、注目すべきは「参照透過性、関数合成、部分適用」の3つです。これらを順に見ていきましょう。

参照透過性は「入力が同じなら必ず同じ答えを返す」と書いてあるので、関数の純粋性と同じ意味ですね。あいにく僕は、純粋があまり好きじゃないです。

好き嫌いの話なので、別にいいとしましょう。関数合成の話も簡単だからいいとしましょう。んで、部分適用の話:

「部分適用」というのは「複数の引数を受け取る関数」に対して部分的に引数を渡すと、関数からは「足りない分の引数を取る関数」が返ってくる機能(というか技術)です。つまりたくさんの引数を持つ関数を段階的に実行することができます。

これはいい説明だと思います。でも、この説明の前後がなんか変。

[部分適用は]名称から「関数合成」と関係してそうなことは想像ができると思います。

名称から言ったら「関数適用」と関係するでしょ。「たくさんの引数を持つ関数を段階的に実行する」のは合成(composition)を繰り返してるんじゃなくて、適用(application, evaluation)を繰り返してます。

部分適用の説明の後で、「他にも」として、カリー化や高階関数が紹介されてますが、部分適用が出来るのはカリー化された高階関数だからであって、別な機能じゃないです。もっとも、部分適用を構文的利便性と解釈するなら、それを実現しているメカニズムがカリー化/高階関数だとは言えるので、「縁の下の力持ち」という文言を考慮すれば、これもまーいいか。

制御構造不要なら楽ちん!

「5. 関数型プログラミングの概要」のサブセクション「モナドのない関数型なんて」になると、だいぶ解釈が苦しくなります。retemoさんはモナドの雰囲気を伝えたいらしいのですが、僕は理解できません。

モナドは多様だということで、次のような記述があります(太字強調は檜山)。

  1. IOモナドはインターフェース役であることが一目瞭然
  2. ArrayモナドやDictionaryモナドあるいはリスト・モナドが構造役であることも疑う余地はない
  3. モナドは合成関数を作ってサービス役にもなれる
  4. モナドの主な使い方は「保持役(状態役)に暗黙の制御構造を付与すること」になります

この「ナントカ役」に関する知識が僕はないので分からないのかも知れません。センテンスレベル/パラグラフレベルでは“ちょっと何言ってるかわかんない”のですが、全体として「明示的な制御構造が不要になるから便利!」と訴えているのは伝わります

例えば、Optionalモナド(Maybeモナド)なら、次のような明示的if文が不要になる、ということでしょう。

if (isUndefined(x)) {
 // xの値が未定義のときの処理
} else {
 // xの値が存在するときの処理
}


それ[Optionalモナド]が本領を発揮するのは関数をOptionalチェーンで繋げる時です。

「Optionalチェーン」と言っているのは、Optionalモナドのクライスリ結合のことでしょう。条件分岐しながらの関数合成がスッキリするから便利だ、と。実際、そのとおりです

Listモナドのmap関数であれば、List.map(f)(x) と書けば、次のforループを不要にしてくれます。

  var y = [];
  for (var i = 0; i < x.length; i++) {
    y.push(f(x[i]));
  }
  return y;

制御構造要らん、あー、便利だ。と、たぶん、そんなことを言いたいのでしょう。

関数型プログラミングはオブジェクト指向の正当な後継である - Qiita 6.「阿吽の呼吸」とも言うべき使いやすさの拡張》

「継承と多態性の技術」は、1つのメソッド名で型に応じた別個のメソッドを自動的に呼び分けることができます。つまりは型をパラメータとしたswitch文的な条件分岐が暗に含まれているわけです。

「継承と多態性」はswitch分岐(型case)を不要にしてくれた、モナドもif分岐やfor繰り返しを不要にしてくれる、どっちも制御構造を不要にするから似てるよね、という話だと思います。

分かりきった手続きについての言及を省略した、言い換えれば「阿吽の呼吸」とも言うべき、そのユーザーフレンドリーな「暗黙の制御構造」の応用範囲をモナドは広げてくれるわけです。実際、多態性とモナドの使用感はよく似ています。

確かに、抽象化により煩雑な記述が省略できる点で多態性とモナドは似てなくもないですが、くくりが大雑把過ぎるかな、とは思います。このくくりなら、オーバーロードも入れちゃってもいい気がします。

それで結局、関数型プログラミングはオブジェクト指向の後継なの?

疲れてきた。残りは急ぎ足。

残りの4つの節も、“ちょっと何言ってるかわかんない”表現が多いですが、特に難儀だったのは:

関数型プログラミングはオブジェクト指向の正当な後継である - Qiita 7. 型にまつわる考察》

「構造を抽象化した型」は「ジェネリック(C++ではテンプレート、Haskellでは型コンストラクタ)」で、「扱いを抽象化した型」は「インターフェース(Swiftではプロトコル、Haskellでは型クラス)」であり、オブジェクト指向と関数型プログラミングのいずれにも存在しています。

「構造」と「扱い」という言葉が分からなくて、一見では意味不明なんです…

「ジェネリック」は型パラメータを具体化して型を返す、という意味らしいので、「構造を抽象化」するとは、具体型から型パラメータへの置き換え、かな、たぶん。「インターフェイス」が「扱いを抽象化」と言っているのは、具体的なメソッド群から実装を剥ぎとって名前だけにした、ってこと、かな、たぶん。

このへんは、ソート、指標、モデルといった概念で理解するのがいいと思いますよ。

できればインスティチューションで分析したいところ。割と圏論バッキバキだったりするんだけど:


Haskellでは型同士の継承をサポートしていませんが、型クラスから型インスタンスへの継承はできるため、それを利用した多態性の実現が可能です。

「型クラスから型インスタンス」は文字通りインスタンス化であって継承とは言わないでしょ。言ってもいいのかな? …… やっぱり、区別したほうがいいと思います。

型クラスの話題だとCoqになってしまうけど、多少は参考になるかも↓

さて、「オブジェクトは仮想機械」で挙げた「オブジェクト指向の4大技術トピック」に関しては、若干の説明の後で:

つまり実はオブジェクト指向の4大技術トピックは全て関数型プログラミングに継承されているわけです。

プログラミング言語AあるいはパラダイムAのメカニズムは、プログラミング言語BあるいはパラダイムBでシミュレートできる、逆のシミュレートもできる、という状況は多いです。そうなると、BがAの後継者とか進化形というよりは、A, Bお互いがメタ・ポジションを取りあう(実際は互角、割と不毛な)議論になるんじゃなかろうか。


[...snip...]どう見てもオブジェクト指向と関数型プログラミングは直系ではありませんが、[...snip...]

歴史的経緯や、言語設計者の意図の観点からは、「関数型プログラミングはオブジェクト指向の正当な後継である」と主張するには無理がありますよね。

こうしてみると表面上はかなり違って見えるオブジェクト指向と関数型プログラミングが深い部分で繋がっていることが感じられます。

それを言うと、何だって深い部分では繋がってます。

関数型プログラミングがオブジェクト指向の正当な後継であるというのはもはや極論ではないと思われます。

retemoさんの言いたいことは、おそらく、

  1. オブジェクト指向から関数型への移行には、それほど大きな障壁はないよ。
  2. 高水準の設計の技法・スキルは流用できるよ。

こんなことかな。そうなら異論はありません。ただ、それならそうと直截に言えばいいわけで、背景に無理筋な主張をあえて持ってくる必要があったんでしょうか? -- という疑問は残ります。

いずれにしても、retemoさんには経験と思いがあるのは確かでしょうから、事例、サンプルコード、図などを交えながら経験と思いをケレン味無く語れば、本来の目的である「オブジェクト指向と関数型プログラミングの関係」を伝えることが出来るのではないでしょうか。


[雑記/備忘]一般化されたマイヒル/ネロードの定理 3:オートマトンの振る舞いと観測

$
0
0

ソフトウェアの設計と実装では、サブシステムやコンポネントの内部構造を明かすべきではない、と考えられています。アプリケーションプログラムは、サブシステム/コンポネントの公式のインターフェイスにだけ依拠すべきだ、となります。それを守れば、サブシステム/コンポネントの内部構造が変わっても(インターフェイスが不変なら)アプリケーションはそのまま動き続けるでしょう。

上記のような原則や、それからの帰結を一般化オートマトンの文脈で定式化してみます。

内容:

  1. 隠蔽原則
  2. 復習:構文と意味
  3. 振る舞い
  4. オートマトンの振る舞いと観測
  5. 非退化指標/非退化オートマトン
  6. 振る舞いの反カリー化表現と観測手順
  7. リファクタリング補題と観測の関手性
  8. 今回のまとめ

隠蔽原則

一般化オートマトンの典型的な例は、伝統的オートマトンや特定のインターフェイスに対する実装クラスなどです。これらのオートマトンは内部構造を持ちます。しかし、その内部構造を知ることは出来ないという仮定を置きます。この仮定を情報隠蔽原則(information hiding principle)、または単に隠蔽原則と呼びましょう。「実装の細部を利用者に漏らしてはならない」というアレです。

しかしですね、現実には内部構造(=実装の詳細)が見えちゃうこともあるんですよね。なので、隠蔽原則は、「内部構造を知り得るにしても、それを利用してはならない」という利用者側の規範だと捉えるのが実際的でしょう。

我々の設定において知ってはいけない内部構造(実装の詳細)とは、オートマトンの状態空間のことです。F:Φ→Set がオートマトンのとき、xが隠蔽頂点ならF(x)は型xの状態空間です。そのようなF(x)がどんな集合であるかは何も分からないということです。状態空間F(x)の正体は完全に未知(不可知)なのですから、F(x)からの写像やF(x)への写像の正体もサッパリ分かりません

一方で、xが可視頂点(x∈(S∪D))なら、F(x)は既知の値空間であり、完全に理解できます。既知の値空間(例:整数値の空間、真偽値の空間)のあいだの関数もまた完全に理解できるとします。

徹底した隠蔽原則の支配下において、我々は何が出来るでしょうか? -- この問題意識が、マイヒル/ネロードの定理の源泉です。そして、マイヒル/ネロードの定理が教えてくれる答は、「頑張れば、知りたいことは何でも分かる」です。

「頑張る」とは何をすることでしょう? 「知りたいこと」とは何でしょう? 今後、これらに正確な定義を与えていくことにします。

復習:構文と意味

一般化されたマイヒル/ネロードの定理 2:文化的なギャップを乗り越えるための対訳表」で述べたように、このテの話題で最大の難関は言葉・用語法・記法の問題だと思います。もう一度言葉の一覧表を示しておきます。「くどい」と思われる方はこの節をスキップしてください。以下、Φ = (Φ, S, D), C = FreeCat(Φ), Hid = |Φ|\(S∪D) です。

モノ グラフ理論 圏論 形式的プログラム構文論 説明
|Φ|の要素 頂点 対象 ソート記号 型の名前
Φ(x, y)の要素 (生成系の)射 オペレーション記号 基本手続きの名前
C(x, y)の要素 パス オペレーション記号の列 プログラムコード
Dの要素 開始頂点 開始対象 開始ソート記号 コンストラクタの引数の型の名前
Sの要素 識別頂点 識別対象 識別ソート記号 クエリーの値の型の名前
D∪S の要素 可視頂点 可視対象 可視ソート記号 既知の型の名前
Hidの要素 隠蔽頂点 隠蔽対象 隠蔽ソート記号 不可知な型の名前
s∈S, x∈Hid として Φ(s, x)の要素 コストラクタ辺 コンストラクタ射 コンストラクタ記号 コンストラクタの名前
x∈Hid, d∈D として Φ(x, d)の要素 クエリー辺 クエリー射 クエリー記号 クエリーの名前
x, y∈Hid として Φ(x, y)の要素 コマンド辺 コマンド射 コマンド記号 コマンドの名前

これらは指標グラフΦ、指標圏Cに関連する概念で、すべては構文的存在物です。意味は、関手 F:CSet によって与えられます。意味を与える関手Fがオートマトンそのものです。Fの行き先は集合圏なので、集合と写像の世界です。構文に対応する意味的存在物は次のように呼びます。

モノ 呼び名
x∈(D∪S) として F(x) 値空間
s∈S として F(s) 開始値空間
d∈D として F(d) 識別値空間
y∈Hid として F(y) 状態空間
s∈S, x∈Hid, c∈Φ(s, x) として F(c) コストラクタ写像
d∈D, x∈Hid, q∈Φ(x, d) として F(q) クエリー写像
x, y∈Hid, f∈Φ(x, y) として F(f) コマンド写像

ΦやCに属するモノ(頂点、辺、パス)とSetに属するモノ(集合、写像)は徹底的に区別しましょう。そうでないと、話がワヤクチャになります。

例として、整数スタックの指標グラフを再掲します。(thisがマズかったなー、という話は「整数スタックの例」に追記で強調されてます。マズイけど、そのまま。)

この例において:

  • 開始頂点:{void, int}
  • 識別頂点:{boolean, intError}
  • 可視頂点:{void, int, boolean, intError}
  • 隠蔽頂点:{this}
  • コンストラクタ辺:{emptyStack, singletonStack}
  • クエリー辺:{isEmpty, top}
  • コマンド辺:{pop}∪{push(n) | n∈Z}

可視頂点には、前もって実際の集合が割当てられています。その割当ては固定化関手Kで行われます。

  1. K(void) = 1 = {0}
  2. K(int) = Z = {... -1, 0, 1, 2, ...}
  3. K(boolean) = B = {true, false}
  4. K(intError) = Z∪{error}

この例の指標をΨとして、オートマトン G:Ψ→Set を定義するとは、次のモノを決めることになります。

  1. 集合 G(this)
  2. 写像 G(emptyStack):1→G(this)
  3. 写像 G(singletonStack):Z→G(this)
  4. 写像 G(isEmpty):G(this)→B
  5. 写像 G(top):G(this)→Z∪{error}
  6. 写像 G(pop):G(this)→G(this)
  7. n∈Zごとの写像 G(push(n)):G(this)→G(this)

数学的な記法ではなくて、実在のプログラミング言語でGを書き下してみましょう。TypeScriptを使ってみます。

// IntStack.ts

// intがないのでナンチャッテ定義
// 名前がintになっているだけ
type int = number;

class IntStack {
    private list_: int[]; // G(this) = List(Z) 隠蔽されている
    constructor() {
        this.list_ = [];
    }
    // メイヤー先生のクエリ
    isEmpty() : boolean {
        return (this.list_.length == 0);
    }
    top() : int {
        if (this.isEmpty()) {
            throw new Error("error");
        }
        return this.list_[this.list_.length - 1];
    }

    // メイヤー先生のコマンド
    pop() : void {
        if (this.isEmpty()){
            return; // ちと分かりにくいが、何もしない
        }
        this.list_.pop();
    }
    push(n : int) : void {
        this.list_.push(n);
    }

    // 生成子(コンストラクタ)
    // TypeScriptネイティブのコンストラクタを使って
    // static関数として実装
    static emptyStack() : IntStack {
        return new IntStack();
    }
    static singletonStack(n : int) : IntStack {
        var stk = new IntStack();
        stk.push(n);
        return stk;
    }
}

振る舞い

Φ = (Φ, S, D), K:ΦvisSetとして、部分固定指標Φ/Kに対して、振る舞いと呼ばれるモノを定義します。振る舞い(behavior, behaviour)とは、s∈D, d∈D で添字付けられた写像の族 bs, d:C(s, d)→Set(K(s), K(d)) のことです。ここで、C = FreeCat(Φ) です。振る舞いは英字小文字ボールド体で示すことにします -- a, b などは指標グラフΦの辺、指標圏Cの射を表すことがあるので区別したいのです。

伝統的オートマトンの部分固定指標Φ/Kについて、その振る舞いを見てみます。Φ/Kが伝統的オートマトンの部分固定指標なら、|Φ| = {0, 1, 2}, S = {0}, D = {2}, Φ(0, 1) = {i}, Φ(1, 1) = Γ, Φ(1, 2) = {t}, K(0) = 1, K(2) = B となります。振る舞いは b0, 2 だけで決まるので、b0, 2を単にbと書きます。

振る舞いの定義より、b:C(0, 2)→Set(1, B) という写像です。C(0, 2) ¥stackrel{¥sim}{=} C(1, 1) = Γ* です。Γ = Φ(1, 1) で、Γ* はΓのクリーニスターです。一方、Set(1, B) ¥stackrel{¥sim}{=} B なので、結局 b*B とみなしてかまいません。

b*B に対して、b-1(true) はΓ* の部分集合です。逆にΓ* の部分集合から、写像 Γ*B が決まります。Γ* の部分集合とは、アルファベットΓの形式言語に他なりません。つまり、伝統的オートマトンにおける振る舞いと形式言語は1:1対応するのです。

部分固定指標Φ/Kに対して、その振る舞いの全体からなる集合をBeh[Φ/K]とします。Φ/Kが伝統的オートマトンの部分固定指標の場合は、上で述べたとおり、Beh[Φ/K]はアルファベットΓ上の言語の集合と同じものです。Γ上のすべての形式言語からなる集合をLang(Γ)とすると、

  • 伝統的オートマトンでは、Beh[Φ/K] ¥stackrel{¥sim}{=} Lang(Γ) 。

整数スタックの例では、振る舞いbは次の写像達からなります。

  1. bvoid, boolean:C(void, boolean)→Set(1, B)
  2. bvoid, intError:C(void, intError)→Set(1, Z∪{error})
  3. bint, boolean:C(int, boolean)→Set(Z, B)
  4. bint, intError:C(int, intError)→Set(Z,Z∪{error})

bint, intErrorの値の実例を幾つか出しましょう。次のパス(Cの射はΦのパス)を取ります。

  1. singletonStack;top
  2. singletonStack;push(2);push(3);top
  3. singletonStack;pop;top

これらのパスに対する値は:

  1. bint, intError(singletonStack;top) = λn∈Z.n
  2. bint, intError(singletonStack;push(2);push(3);top) = λn∈Z.3
  3. bint, intError(singletonStack;pop;top) = λn∈Z.error

オートマトンの振る舞いと観測

Φ/Kが任意の部分固定指標として、F:Φ→Set をΦ/K上のオートマトンとします。つまり、F∈|Autom[Φ/K]| 。記号Fは、F:Φ→Set と F:CSet の両方を表します(「プレオートマトンの定義」参照)。関手FのホムセットC(x, y)への制限をFx, yと書くことにします。

オートマトンFの振る舞いbは、簡単に定義できます。

  • bs, d := Fs, d

要するに、関手Fを、SからDへのホムセットに制限したものがFの振る舞いです。オートマトンFにその振る舞いを対応させることを振る舞い観測(behavior/behaviour observation)、または単に観測(observation)と言います。

振る舞い観測は、(オートマトン |→ 振る舞い) という対応なので、Obs:|Autom[Φ/K]|→Beh[Φ/K] という写像になります。この定義からは、Obsは関手にはならないように思えますが、実は関手になります(後述)。

Aが伝統的オートマトンで、Fが対応する関手オートマトンだとします。このとき、Fの振る舞いは、伝統的オートマトンAが受理する形式言語と同じことです。この事実を示すことは良い練習問題です。やってみてください。

一般的関手オートマトンFをその振る舞いから調べることは、伝統的オートマトンを受理言語から調べる方法の一般化になります。伝統的マイヒル/ネロードの定理の一般化も、このラインに沿って行います。

非退化指標/非退化オートマトン

今後、振る舞い全体の集合Beh[Φ/K]を道具に使うのですが、指標グラフ Φ = (Φ, S, D) によっては振る舞いが無意味化することがあります。振る舞いbは、S×Dで添字付けられることになりますので、S×Dが空だと振る舞いは無意味になってしまいます。そこで次の条件を付けます。

  • 開始頂点族Sは空ではない。
  • 識別頂点族Dは空ではない。

Hid = |Φ|\(S∪D) として、Hidが空の場合も面白くありません。次の条件も追加します。

  • 隠蔽頂点族Hidは空ではない。

S, D, Hのすべてが空でない指標を非退化指標(non-degenerate signature)と呼びます。非退化指標Φを定義域とするようなオートマトン F:Φ→Set非退化オートマトン(non-degenerate automaton)と呼ぶことにします。非退化条件を満たさないモノは、退化した指標/オートマトンです。

今後扱う指標/オートマトンは、非退化であると仮定します。退化オートマトンでは観測が出来ません。

振る舞いの反カリー化表現と観測手順

b∈Beh[Φ/K] のとき、s∈, d∈D に対して bs, d:C(s, d)→Set(K(s), K(d)) ですが、少し変形しておくと取り扱いが便利になります。次の形です。

  • bs, d:K(s)×C(s, d)→K(d)

bs, dは、bs, dの反カリー化になっています。

集合圏SetのホムセットSet(K(s), K(d))は、集合の指数でもあるので、

  • bs, d:C(s, d)→K(d)K(s) in Set

と書けます。集合圏はデカルト閉圏なので、反カリー化により指数を直積に直して、

  • (bs, d):K(d)×C(s, d)→K(s) in Set

ここで、(-)は反カリー化する演算子とします。ほんとは (-) にしたかったんですが(そのほうが辻褄が合う)、「s, d」が下付きなので「∨」は上付きにしました。

  • bs, d := (bs, d)

と定義して、bの反カリー化bが出来上がります。

bでもbでも何の変わりもありませんが、オートマトン F∈Autom[Φ/K] の振る舞いを求める行為(つまり観測)は、bに基づいて次のように記述できます。

  1. 開始値(開始値空間の要素) u∈F(s) を選ぶ。
  2. プログラムコード(Cの射) f∈C を選ぶ。
  3. 実行系F上で、uをfに適用する。
  4. 評価結果(実行結果) F(f)(u)∈F(d) を見る。

このような、開始値/プログラムコードを選んでの個別観測行為をタクサンタクサン繰り返すことが、Fの観測です。観測行為の総体をObsで表すと、Obs(F)が実行系Fの振る舞いとなります。

リファクタリング補題と観測の関手性

次に述べる命題は、とても簡単に示せますが、意外な結果です。人によっては当たり前かも知れませんが、僕は驚きました。この結果を使えば、圏Autom[Φ/K]の構造を探れるな、と期待が持てたのでした。

  • オートマトンの準同型写像(自然変換) α:F→G があるとき、Obs(F) = Obs(G)

Obsの定義から、s∈S, d∈D を任意に選んだとき、Fs, d = Gs, d を示せばいいわけです。αが自然変換であることから、次の図式が可換になります。

F(s) -αs→G(s)
|         |
F(f)       G(f)
↓         ↓
F(d) -αd→G(d)

ところが、オートマトンはS, D上では固定化関手Kと一致するので、F(s) = G(s) = K(s), F(d) = G(d) = K(d), αs = idK(s), αd = idK(d) となり、

  • F(f) = G(f)

が成立します。fは任意なので、「任意の f∈C(s, d) に対して F(f) = G(f)」が成立し、Fs, d = Gs, d が言えます。QED.

F→G または G→F のどちらか一方向でも準同型写像があれば、FとDは同じ振る舞いを持ちます。隠蔽原則の支配下にある我々に出来ることは振る舞い観測だけなので、準同型写像で結ばれた2つのオートマトンF, Gを区別することは出来ません。別な言い方をすると、準同型写像 α:F→G があるなら、振る舞いを一切変えずにインターフェイスΦ/Kの実装をFからGに(あるいはGからFに)取り替えることが出来るのです。準同型写像αはリファクタリングと捉えられます。もっと正確に言うと、準同型写像の存在がリファクタリングの正当性の保証を与えます。

Φ/Kを(一部実装付き)のインターフェイス、Φ/K-オートマトンをインターフェイスの実装、振る舞い観測は網羅的ブラックボックス・テストと解釈できます。FとGの振る舞いが一致することは、「実装FをGに取り替えても気付かれない」という意味でリスコフ置換可能性です。次のように言っていいでしょう。

  • Fの振る舞いとGの振る舞いは一致する ⇔ FとGはリスコフ置換可能である

メイヤーオートマトンに関するマイヒル/ネロードの定理を宣伝する」では次のように説明しています。

メイヤーオートマトンは、Command-Query分離されたインターフェース(メイヤー指標)の実装ですが、「アプリケーションプログラムから見て区別が付かない」とは、どんなに頑張ってテストしても挙動の食い違いを発見できない、ことです。

どんなに頑張ってテストしても挙動の食い違いを発見できないなら、それは異なる実装と考える必要はなくて、(事実はどうあれ)「同じ実装」とみなして差し支えありません。この「同じ」という概念がリスコフ置換可能性としての同値関係です。

この節の冒頭で出した「オートマトンの準同型写像(自然変換) α:F→G があるとき、Obs(F) = Obs(G)」は、「リファクタリングしても振る舞いは変わらない」と読めます。もう少し正確に言うと:

  • Gを構成した。
  • FからG(あるいはGからF)への準同型写像が存在する。
  • このとき、GはFをリファクタリングして作った、と言ってよい。
  • なぜなら、FとGは同じ振る舞いを持つから。

この点を鑑み、「オートマトンの準同型写像(自然変換) α:F→G があるとき、Obs(F) = Obs(G)」という命題をリファクタリング補題として参照します。

リファクタリング補題から、「振る舞い観測は関手と考えてよい」ことが導けます。

  • α:F→G in Autom[Φ/K] に対して Obs(f) = idObs(F) = idObs(G) と定義すると、ObsはAutom[Φ/K]から離散圏とみなしたBeh[Φ/K]への関手となる。

離散圏とは、恒等射だけを射とする圏です。この関手性は、定義を知っていれば簡単に示せるのでやってみてください。

今回のまとめ

隠蔽原則とは次のことです:

  • サブシステム/コンポネントの実装者は、内部構造(実装の詳細)を利用者に知らせてはならない。
  • サブシステム/コンポネントの利用者は、仮に内部構造を知り得るにしても、その情報を利用してはならない。
  • 我々のモデルにおいて、サブシステム/コンポネントとは一般化されたオートマトンであり、内部構造とは状態空間のことである。

隠蔽原則の支配下において可能なことは、ブラックボックス・テストだけです。テスト対象プログラムコード f∈C(s, d) と開始値 u∈K(s) を選んで実行すると、識別値 F(f)(u)∈K(d) が得られます。この行為を網羅的に行うと、K(s)×C(s, d)→K(d) が得られるますが、カリー化すれば、C(s, d)→Set(K(s), K(d)) ともみなせます。

網羅的ブラックボックス・テストの概念を抽象化すると、振る舞いの集合Beh[Φ/K]と観測関手 Obs:Autom[Φ/K]→Beh[Φ/K] として表せます。Obsが(離散圏への)関手であることは、次のリファクタリング補題から従います。

  • オートマトンの準同型写像(自然変換) α:F→G があるとき、Obs(F) = Obs(G)

αの存在は実装者視点からのリファクタリングの正当性保証であり、Obs(F) = Obs(G) は利用者視点で「FとGは区別できない(リスコフ置換可能である)」ことです。

[雑記/備忘]関数型プログラミングとオブジェクト指向について、何か書く、かも

$
0
0

「関数型プログラミングはオブジェクト指向の正当な後継」なの?」を書いていて次のようなことを思いました: テクノロジーに関する思索を語るのは推奨されるべき事だと思いますが、ベーシックな知識に裏打ちされてないと、解釈困難で意味不明になりがちです。せっかくの経験や思いがうまく伝わらないことになります。

知識は大切だな、とは思っているので、ちょっと気になりそうなトピックに関して、ベーシックな知識の入り口を解説する記事を書いてきました。

公開年を見るとわかるように、いずれも10年近く前です。一度書いてしまうと、同じことを繰り返し書く気にはなれないので、このテの入門記事を書くことはなくなってしまいました。

表題の「関数型プログラミングとオブジェクト指向」とかは、今でも興味を持つ人は多いのでしょうが、(僕自身にとっての)新しい切り口がなかなか見つからない。「なんかないかな?」と見渡してみると、型クラスあたりかな、と。

型クラスは重要で面白い話題だと思います。ただね、例によって概念も用語法も混乱していて、それを整理するのがすごく負担。そもそも型クラスって何? の定義を確定するのが容易じゃない。

元祖・型クラスみたいなHaskellの型クラスは、およそ指標(signature)のことです。StandardMLでは素直にsignatureと呼んでます。しかし、StandardMLのsignatureは(structure, functorと共に)モジュール機構を意図しているので、多相な型システムとは微妙に違う気がします。

CoqやIsabelleの型クラスとなると、指標だけではなくて公理を書けます。よって、型クラスは形式セオリー(または形式仕様)のことになります。公理(条件、制約、表明)を書けない型クラスというのは、人間だけが知っている暗黙の思いに支えられている点で不十分で脆弱なものです。

例えば、Haskell型クラスの定番の例であるEqクラス:

class Eq a where
  (==) :: a -> a -> Bool

こんなもんで等号の性質を規定したことには全然なりません。Coq型クラスなら、

Class Setoid A := {
  equiv : relation A ;
  setoid_equiv :> Equivalence equiv
}.

Equivalenceは、反射律(Reflexive)、対称律(Symmetric)、推移律(Transitive)をまとめた記述で、等号(同値関係を表す記号)として満たすべき制約まで記述してます。これなら、二項関係equivが等号にふさわしい関係であることが保証されます。

CoqやIsabelleの型クラスのインスタンス化では、公理(要求される制約)を満たすことの証明を要求されます。通常のプログラミングで、型定義の際に証明を要求されるのは現実的ではありません。しかし、指標だけでは上っ面の構文を規定しただけで、型の内実は何も制約してません。「x == y を x < y と具体化してはいけない」は人間側のモラルに過ぎません。

「指標+公理」という意味の型クラスには、CoqやIsabelleの型クラスがいいのですが、ややこし過ぎる、しかしHaskell型クラスやML指標では構文だけで貧弱過ぎる、という現状。

「指標+公理」の記述には、CafeOBJのような形式仕様記述言語が一番いいような気がするんですが、CafeOBJを知っている人/使っている人が少な過ぎて、実効性がありません。実は、Categorical InformaticsのFQL/OPLも「指標+公理」の説明には好適な素材・道具なんですが、ユーザーの少なさはCafeOBJと同様の問題です。


関数型プログラミングにもオブジェクト指向にも関係があって、今後重要度を増すであろう「型クラス」ですが、今述べた(愚痴った)ような事情で(あと、C++のコンセプトは宙ぶらりんだし)、説明の方針も題材も定まりません。でも、いつか、何か書く、かも。

[日常]ウワーッ! 昭和だぁー

$
0
0

↓の映像がとんでもなく昭和臭い。顔、着てるもの、風景、所作まで昭和の匂いってあるんだなー。曲は、島谷ひとみさんのカバー(2002)で知っている人が多いでしょうが、この映像は1968当時のものでしょう。

D

[雑記/備忘]入門的ではない型クラスの話:Haskellの型クラスがぁ (´^`;)

$
0
0

タイトルに「入門的ではない」と入れたのは;
先日の「関数型プログラミングとオブジェクト指向について、何か書く、かも」において、「型クラス入門」の記事を書くかもと予告じみたことを言ってしまったので、その入門じゃないぞ、と。でも、型クラスの話だぞ、と。そういう意味合いです。ヨロシク、アシカラズ。

型クラスの元祖はHaskellです。なので、「型クラス = Haskellの型クラス」という前提での解説が多いみたいです。しかし、元祖は“最初の試み”であるがゆえに、使用経験や後発の理論を活かすことが出来ず、むしろ問題をかかえていたりします。Haskellの型クラスも、なんだか残念なところが。

内容:

  1. オーバーロードと人生
  2. 型クラスは何がうれしいのか(オーバーロードなしでも)
  3. 型クラスの実際
  4. 記号の乱用の実装法
  5. Haskellの型クラスは何がマズイのか

オーバーロードと人生

型クラス誕生の動機は、演算子オーバーロードと関係していました。そして、演算子オーバーロードをうまく扱う機能を期待されてもいるでしょう。実際、僕は演算子オーバーロードを欲しているので、その観点から型クラスを眺めることが多かったです。

でもね、オーバーロードと型クラスが分離不能に癒着しているわけではありません。理論的には独立に扱うことが可能です。オーバーロードに寄与しない型クラスもあるし、それはそれで有用だということです。

例えば、Standard MLのモジュールシステムのアイデアは型クラスと同様ですが、特にオーバーロードを意図してません。オブジェクト指向言語のインターフェイスは型クラス類似機能ですが、演算子のオーバーロードには直結しません。

オーバーロードは、名前・記号に対する実体をいつどのように結びつけるかに関わることなので、プログラミング・パラダイム、型システム/モジュールシステムとは一応切り離して議論できるはずです。

僕は、名前・記号が増えるのを非常に嫌っているので、個人的にはオーバーロードを希求してやまないのですが、皆んなにオーバーロードが必要とは限りません。Lisp由来の関数型言語は、あんまりオーバーロードしません。型システムがゆるいので、実質的に多相な関数を、実行時・型ディスパッチで書けるので「まーいいや」ってことでしょう。

強い型システムのもとでは、オーバーロード(非パラメトリックな多相)は難しくなります。OCamlでは、「-」が整数の引き算、「~-」が整数の符号反転、「-.」が浮動小数点数の引き算、「~-.」が浮動小数点数の符号反転です。印字関数も、print_char, print_string, print_int, print_floatなんて型ごとにタクサンあります。その分、確実な型推論という見返りがあるので、ユーザーに強い不満はないでしょう、たぶん。

というわけで、オーバーロードなしでも楽しい人生をおくっている人がいるので、オーバーロードを意図しない型クラスはあってもいいし、説明のときはむしろ、オーバーロードの話をいったん切り離したっていいでしょう。これは、僕がオーバーロードに拘り過ぎだった、という自戒の念から言ってます。

型クラスは何がうれしいのか(オーバーロードなしでも)

オーバーロード機能は無視するとして、それでもなお型クラスは有用なのだ、と言いました。では、型クラスは何を提供してくれるのでしょう? -- 「構造付き集合(structured set)としての型」概念を提供してくれます。構造とは、ブルバキによって提唱された“数学的構造”だと思ってください。ごく短く説明するなら、台集合(underlying set, carrier set)と、その上の演算、定数、関係を一緒に考えたものです。例えばモノイドは、台集合に二項演算と単位元(定数)を一緒に考えたものです*1

圏論の観点から言えば、構造付き集合(structured set)は単に圏Cの対象です。ただし、圏Cは忠実関手 U:CSet を持っているとします。この関手Uは忘却関手と呼ばれることが多いですが、構造的集合にその台集合を対応させる関手です。

上記の圏論的定義では余りにも味気ないので、構造の指標(signature)と(その指標に基づく)構造の公理(axioms)から具体的構造を定義します。モノイドの指標を擬似言語で宣言してみましょう(Coqで書いた指標と公理が「Coqの型クラスの使い方:バンドル方式とアンバンドル方式」にあります)。

signature MonSig := {
  carrier M: Set;
  operation mon_op: M, M -> M;
  constant mon_unit: M;
};

これは記号の使い方を約束してるだけです。曰く:

  1. 台集合をMという名前(文字)で表す。
  2. M上の二項演算をmon_opという名前で表す。
  3. Mのとある要素をmon_unitという名前で表す。

モノイドの公理は次のようなものです。

  1. 結合律: ∀x,y,z:M. mon_op(mon_op(x, y), z) = mon_op(x, mon_op(y, z))
  2. 左単位律: ∀x:M. mon_op(mon_unit, x) = x
  3. 右単位律: ∀x:M. mon_op(x, mon_unit) = x

指標と公理を一緒にしたものを形式セオリー(formal theory)、または単にセオリー(theory)と呼びます。モノイドのセオリーも擬似言語で書いてみます(もちろん、CoqやIsabelleの構文でも書けます)。

theory MonTh := {
  signature := MonSig;
  axiom assoc: ∀x,y,z:M. mon_op(mon_op(x, y), z) = mon_op(x, mon_op(y, z));
  axiom unit_l: ∀x:M. mon_op(mon_unit, x) = x;
  axiom unit_r: ∀x:M. mon_op(x, mon_unit) = x;
};

セオリー内に指標をインライン展開して書くなら:

theory MonTh := {
  carrier M: Set;
  operation mon_op: M, M -> M;
  constant mon_unit: M;
  axiom assoc: ∀x,y,z:M. mon_op(mon_op(x, y), z) = mon_op(x, mon_op(y, z));
  axiom unit_l: ∀x:M. mon_op(mon_unit, x) = x;
  axiom unit_r: ∀x:M. mon_op(x, mon_unit) = x;
};

Haskellの型クラスとは指標のことで、台集合(carrier)の指定をパラメータの形にしたものです。CoqやIsabelleの型クラスは公理も書けるセオリーです。なお、Isabelleで実際に「セオリー」と呼んでいるのは割と大粒度のモジュールのことで、ここで言ったセオリーじゃありません。

指標もセオリーも、お約束事と条件を記述した構文的存在物(つまり、書き物)に過ぎません。実際の構造的集合としてのモノイドは、指標の名前Mに実際の集合を割当て、mon_op, mon_unitにも実際の写像/要素を割当てたもので、公理をほんとに満たすものです。そういう構造的実体をモノイドと呼ぶのです。

型クラスの実際

Standard MLでは指標を素直にsignatureと呼び、型クラスという言葉は使いません。CoqやIsabelleの型クラスはセオリーのことです。指標は、公理をひとつも持たないセオリーで代用できるので、CoqやIsabelleに指標という概念はありません。そして、Haskellの型クラスは単なる指標のことでした。

指標、セオリー、型クラスの区別には神経質にならずに、ほぼ同義語として使います。公理を書けない言語が多い(つうか、普通のプログラミング言語は公理を書けない)ので、以下では公理は出しません。公理(型クラスの法則)はプログラマ/ユーザーの心のなかにある、でヨシとしましょう。

以下はそれぞれ、Coq, Standard ML, Haskellにおける、「モノイドを定義する型クラス」です。型インスタンス、つまり実際のモノイドも1個定義しています。

Class Mon := {
  m: Set;
  mon_op: m -> m -> m;
  mon_unit: m
}.

Require Import ZArith.
Open Scope Z_scope.

Instance IntMulMon : Mon := {
  m := Z;
  mon_op := fun x=>fun y=> x*y;
  mon_unit := 1
}.
signature Mon = sig
  type m
  val mon_op : m -> m -> m
  val mon_unit : m
end

structure IntMulMon : Mon = struct
  type m = int
  val mon_op = fn x=> fn y=> x*y
  val mon_unit = 1
end
class Mon m where
  mon_op :: m -> m -> m
  mon_unit :: m

instance Mon Int where
  mon_op = (*)
  mon_unit = 1

言語ごとのネーミングコンベンションは無視して、名前は揃えてあります。でも、整数型を表す名前は、Z, int, Int とバラバラです。

上記の例では、整数値の集合(Z, int, Int)の上にモノイド構造を載せて実際のモノイド(型クラスMonのインスタンス)を作っています。二項演算は掛け算、単位元は1です。ところで、整数値の集合上には足し算もあり、これもモノイドになります。

Instance IntAddMon : Mon := {
  m := Z;
  mon_op := fun x=>fun y=> x + y;
  mon_unit := 0
}.
structure IntAddMon : Mon = struct
  type m = int
  val mon_op = fn x=> fn y=> x + y
  val mon_unit = 0
end

簡単ですね。あれ、Haskellでは? … 出来ません*2

記号の乱用の実装法

Haskellの場合、演算子記号・関数名のオーバーロードだけではなくて、型名のオーバーロードも許しています。この型名のオーバーロードはとてつもなく便利ですが、弊害もあります。後知恵で言えば、「悪いお薬」だったと思います。服用するとモノ凄く元気になるが、長期的には心身を蝕んでしまうお薬だったと。

かつて、次のような記事を書いたことがあります。

これは、人間がモノイドMについて、「M = (M, m, e) とする」のような言い方を平気でするイイカゲンさを、コンピュータにも実装すべきかも、という内容です。Haskellは、この便利なイイカゲンさを実装しているのです。

M = (M, m, e) のイイカゲンさを解決する解釈は二通りあります。

  1. M = (Carrier(M), mM, eM)
  2. Enhance(M) = (M, mM, eM)

一番目の解釈: Mはモノイド構造に付けられた名前で、右辺のM(正確には Carrier(M))はモノイドMの台集合を表している。

二番目の解釈: Mは集合の名前で、左辺のM(正確には Enhance(M))は、集合Mに対して演算と単位を付与して作ったモノイドを表している。

CoqもStandard MLも、一番目の乱用はサポートしています。特にCoqは、組み込みでCarrier相当の機能(implicit coercion)を持っています。

我々人間にとってより自然で便利なのは、たぶん二番目の解釈です。そしてHaskellは、二番目の解釈を実装しています。Int = (Int, (*), 1) の解釈は、Enhance(Int) = (Int, (*), 1) なのです。Enhanceに相当する対応 Int |→ (Int, (*), 1) がinstance宣言で登録されます。

本来は単なる集合の名前であったIntを、文脈により様々な構造として再解釈を許すのですからこれは便利です。ごく一部を以下に図示します。左側の「集合としての型」を、右側の「構造としての型」としても流用できるのです。

Haskellの型クラスは何がマズイのか

Haskellでは、モノイド自体に名前を付けることが出来ません。CoqやStandard MLにおけるIntMulMonのような名前はないのです。集合としての型であるIntを経由して、Intの標準モノイド構造 Mon Int としてのみ構造にアクセスできます。このことが記号の乱用をうまく扱える根拠になっています。しかし、Int(整数の集合)上のモノイド構造は無数にあるわけで、「台集合ひとつにモノイドもひとつ」という制限は、用途によっては耐え難いものとなります。

またHaskellでは、マルチパラメータ・クラスは色々と細工をしないと実現できません。よく引き合いに出される、要素の型がeであるコレクション型ceの型クラスは:

-- このままではうまくいかない
class Collects e ce where
  empty :: ce
  insert :: e -> ce -> ce
  member :: e -> ce -> Bool

Coqでは、特に問題もなく書けます*3。構造に対する台集合が2つあり、それらをパラメータにしています。

Class Collects (e: Type) (ce: Type) := {
  empty : ce;
  insert : e -> ce -> ce;
  member : e -> ce -> bool
}.

Require Import List.

Instance IntListColl : Collects Z (list Z) := {
  empty := nil;
  insert := fun x=> fun xs=> x::xs;
  member := fun x=> fun xs=> existsb (fun y=> x =? y) xs
}.

コレクションの実装にはリストを使うことに決めて、要素の型をパラメータとする場合をStandard MLで書いてみます*4

signature Eq = sig
  type t
  val eq : t -> t -> bool
end

signature Collects = sig
  type ce
  type e
  val empty : ce
  val insert : e -> ce -> ce
  val member : e -> ce -> bool
end

functor ListColl(X: Eq) : Collects = struct
  type ce = X.t list
  type e = X.t
  val empty = []
  val insert = fn x=> fn xs=> x::xs
  val member = fn x=> fn xs=> exists (fn y=> X.eq x y) xs
end

(* 型インスタンスの生成 *)

structure EqInt : Eq = struct
  type t = int
  val eq = fn x=> fn y=> x = y
end

structure IntListColl : Collects = ListColl(EqInt)

Haskellが実装した記号の乱用はやはり「乱用」だったのです。特定の状況ではうまく動くものの、より複雑な状況/広い範囲に対しては破綻します。綻びを修復する試みはされていますが、不必要な複雑さを導入しているように思えます。

利便性のためには確かに「乱用=オーバーロード」が必要です。しかし、オーバーロードは、どうやっても恣意性/無法則性を免れません。それを基本メカニズムに採用すれば、恣意性/無法則性をシステムに招き入れることになります。オーバーロード機能は、ユーザーの好みにより利用する/しないを制御できるオマケ便利機能にすべきだと思います。



そんなオーバーロードを実現したいだけの人生だった。

*1:Haskellでは、モナドも型クラスとして定義されています: class Monad m where ... パラメータmは台集合じゃないぞ!? 確かにmは集合を表してはいません。ちょっと一般化して、集合とは限らない台対象(underlying/carrier object)の上に構造を載せたものとすれば辻褄はあいます。
例えばモナドは、関手を台対象(underlying/carrier object)とする構造なので、台の圏をSetから自己関手の圏End(C)に切り替えればいいのです。

*2:持って回った方法ならありますが、それは「直接的には出来ない」ことに対するワークアラウンドです。

*3:Coqの型クラスにも困った点があります。クラスが備える関数名であるempty, insert, memberが大域名前空間に漏れ出てしまいます。まったくウンザリです。

*4:Standard MLはすっきりキレイに書けますね。しかし、オーバーロードはサッパリ出来ない(泣)。

[雑記/備忘]忠実ではない忘却関手と左右の随伴パートナー

$
0
0

忘却関手のハッキリした定義はありませんが、忘却関手は忠実関手であると想定されることが多いでしょう。しかし、忘却関手と呼ぶにふさわしそうでも、忠実ではない例もあります。

Catを小さな圏の圏(射は関手)、Setを集合の圏とします。小さな圏Cに対象集合|C|を割当て、関手 F:CD in Cat に関手の対象部分 Fobj:|C|→|D| in Set を対応させる関手をObjとします。

  • Obj(C) = |C|
  • Obj(F:CD in Cat) = (Fobj:Obj(C)→Obj(D) in Set)

この Obj:CatSet は射に関する情報を忘れ去るので忘却関手と呼んでいいでしょう。Objは、一般的には忠実とは言えません(関手全体が対象部分からは決まらない)。対象が1個の圏を考えれば、非忠実性は明らかです。

さて、D:SetCat を、集合に対して離散圏を対応させる関手とします。[追記]Dは関手を表し、斜体のDは圏を表すので注意![/追記]

  • |D(A)| = A
  • D(A)(a, a) = {ida}
  • a≠b のとき、D(A)(a, b) = 空

次が成立します。

  • Cat(D(A), D) ¥stackrel{¥sim}{=} Set(A, Obj(D))

この同型はA, Dに対して関手性を持つので、DとObjは随伴対になります。

  • D -| Obj

このことから、圏D(A)が、集合Aから自由生成した圏だと言えます。

もうひとつ、K:SetCat を、集合に対して完全グラフの形をした圏を対応させる関手とします。

  • |K(A)| = A
  • K(A)(a, b) = {[a, b]}
  • [a, b];[b, c] = [a, c]
  • ida = [a, a]

次が成立します。

  • Set(Obj(C), B) ¥stackrel{¥sim}{=} Cat(C, K(B))

この同型はC, Bに対して関手性を持つので、ObjとKは随伴対になります。

  • Obj -| K

Kは、忘却関手Objの右随伴パートナーです。余自由生成関手と呼べばいいのでしょうかね。

まーともかく、忘却関手Objには左随伴パートナーDと右随伴パートナーKがあって、

  • D -| Obj -| K

です。

[雑記/備忘]オーバーロードは何故にかくも難しいのか:Haskellの成功と失敗

$
0
0

名前や記号の多義的使用をオーバーロードと呼びます。オーバーロードとは「曖昧な表現を使う」ことだ、と言ってもいいでしょう。曖昧さを嫌うコンピュータに、曖昧な表現を理解させるのは難しいことです。コンピュータに関する技術や理論以前に、「我々人間は、曖昧な表現をどう使い/どう解決しているのか?」と問う必要があります。

内容:

  1. Haskellの場合 -- The・構造の仮定
  2. 要素とコレクションの事例
  3. 型クラスの境界線は引けるのか
  4. 名前・記号の問題は難しい

Haskellの場合 -- The・構造の仮定

入門的ではない型クラスの話:Haskellの型クラスがぁ (´^`;)」において:

後知恵で言えば、[注:Haskellのオーバーロード・メカニズムは]「悪いお薬」だったと思います。服用するとモノ凄く元気になるが、長期的には心身を蝕んでしまうお薬だったと。


Haskellが実装した記号の乱用はやはり「乱用」だったのです。特定の状況ではうまく動くものの、より複雑な状況/広い範囲に対しては破綻します。綻びを修復する試みはされていますが、不必要な複雑さを導入しているように思えます。

なんて言いました。Haskellファンはムッとしたかも知れません。

そもそも型クラスはHaskell起源だし、モジュラー化とオーバーロードの手法として大成功を収めたのは確かです。その点は高く評価するのは言うまでもありません。しかし、何ゆえに成功したのか? と考えてみると、特定の(かなり狭い)状況でしか成立しない原理に強く依拠している、と思えるのです。この原理について説明し、さらに、この原理に一般性・汎用性がないことを例示します。

Haskellの型クラスは、演算子記号・関数名のオーバーロードだけでなく型名のオーバーロードも可能とします。そして、型名をオーバーロードすることが必須です。その様子は「入門的ではない型クラスの話:Haskellの型クラスがぁ (´^`;)」に絵で示しました。

例えばIntという型名が、「EqのインスタンスとしてのInt/OrdのインスタンスとしてのInt/NumのインスタンスとしてのInt」と多様な解釈を持ちます。この多様な解釈を可能としている前提は次のものです。

  • 集合(としての型)が決まれば、その上の構造(演算、特定要素、関係など)はひとつだけ決まる。

例えば、「整数の等値性」と言えば、誰でも同じイコールを思い浮かべます。「整数の順序関係」といえば「あの大小関係」で間違いないでしょう。つまり、型Intに対して「The・等値性」「The・順序関係」が決まるので、Eq Int, Ord Int などと書けて、それを曖昧性なく解釈できます。

ところが、「整数の二項演算」となると、「構造(演算、特定要素、関係など)はひとつだけ決まる」という原理があやしくなります。整数に対して「The・二項演算」は一意に決められないからです。

二項演算で既にあやしいとなると、より複雑な構造に対して、与えられた集合上の「The・構造」が決まるのでしょうか? 普通に考えれば、まー無理ですよね。

要素とコレクションの事例

次のコードは、「入門的ではない型クラスの話:Haskellの型クラスがぁ (´^`;)」のサンプルの再掲ですが、一筋縄ではいかない「コレクションを表す型クラス」です。

class Collects e ce where
  empty :: ce
  insert :: e -> ce -> ce
  member :: e -> ce -> Bool

Collects e ce において、eは要素の型で、ceはコレクション(要素の集まり)の型です。型eと型ceが決まれば、その上のコレクション構造が決まるわけです。

それって、ほんとでしょうか? Collects Int Int とか Collects Int Bool とかを見て、「The・コレクション構造」をイメージできますか? 無理ですよね。2つの型を並べて、それをコレクション構造の名前として使用するのは現実離れしています。

想定される反論は以下のものです; 勝手な型e, ceに対してコレクション構造が決まるわけではない。eとceは無関係ではなく、どちらかを決めればもう一方も決まる。例えば、要素の型がInt、コレクションの型は[Int](Intのリスト)のように。

この反論を信じるなら、e→ce または ce→e の方向に「The・対応」があることになります。「Int型とIntのリスト型」なら「The・対応」があるでしょうが、典型例だけを見て一般化するのは危険です。

型クラスの境界線は引けるのか

Haskellでは、要素の型eとコレクションの型ceをパラメータにしていますが、一般的なコレクション構造のインターフェイスを記述する目的なら、パラメータである必然性はありません。Coqでパラメータを使わない形に書いてみます。[追記]Coqはシンタックスハイライトされない。悲しい。[/追記]

Class Collects :=
  {
    e: Type;
    ce: Type;
    empty : ce;
    insert : e -> ce -> ce;
    member : e -> ce -> bool
  }.

ちなみにStandard MLでも同じように書けます。

signature Collects = sig
    type e
    type ce
    val empty : ce
    val insert : e -> ce -> ce
    val member : e -> ce -> bool
end

まずは典型的な型インスタンスを定義しましょう。

(* インスタンスの実装に使うライブラリ *)
Require Import Bool.
Require Import ZArith.
Open Scope Z_scope.
Require Import List.

Instance IntListColl : Collects :=
  {
    e := Z;
    ce := list Z;
    empty := nil;
    insert := fun x=> fun xs=> x::xs;
    member := fun x=> fun xs=> existsb (fun y=> x =? y) xs
  }.

見慣れないかも知れないCoqの構文を表にして説明しておきます。↓

Type すべての型からなる“型の型”
Z 整数型
list リスト型構築子
fun ラムダ抽象
nil 空リスト
:: consの中置演算子
existsb p xs リストxsの要素で述語pを満たすものがあればtrue, なければfalse
=? 整数(Z)のイコール(スコープ依存)

次に、固定サイズの配列とか長さが有限のキューのように、格納限界があるコレクションを考えてみます。以下は3個までの要素を保存できるコレクションです。

Instance IntSize3Coll : Collects :=
  {
    e := Z;
    ce := list Z;
    empty := nil;
    insert := fun x=> fun xs=> if leb (length xs) 2 then x::xs else xs;
    member := fun x=> fun xs=> existsb (fun y=> x =? y) xs
  }.

lebは自然数の不等号≦のことです。リストの長さが3以上(3にしかならないけど)だと、insertが黙って失敗します。

挿入すべき要素を黙って捨てるのはヒドイので、新しい要素はちゃんと保存して、代わりに古い要素を捨てることにしましょう。そして、格納限界を要素1個にします。

…… ん? それって、単に上書き更新する変数と同じことじゃん。

(* 上書き更新する整数変数のモデル *)
Instance IntIntColl : Collects :=
  {
    e := Z;
    ce := Z; (* 変数が保持する値 *)
    empty := 0; (* 初期値 *)
    insert := fun x=> fun y=> x; (* 上書き=破壊的代入 *)
    member := fun x=> fun y=> x =? y (* 現在値の検査 *)
  }.

同じようにして、「上書き更新する真偽値変数のモデル」BoolBoolCollも定義できます。次は、真偽値変数に整数を代入するモデルです。そのままでは代入できないので、0を偽、それ以外を真に変換して代入します。

(* 上書き更新する真偽値変数と
   整数値を無理やり代入するモデル *)
Instance IntBoolColl : Collects :=
  {
    e := Z;
    ce := bool; (* 変数が保持する値 *)
    empty := false; (* 初期値 *)
    insert := fun x=> fun y=> negb (x =? 0); (* 整数を真偽値に直して代入 *)
    member := fun x=> fun y=> eqb y (negb (x =? 0)) (* 現在値の検査 *)
  }.

negbは真偽値の否定(NOT)、eqbは真偽値のイコールです。

ここまでで、「要素の型, コレクションの型」として、「Z, list Z」、「Z, list Z(長さ3まで)」、「Z, Z」、「bool, bool」(ソースコードは省略)、「Z, bool」の例を挙げました。「要素の型, コレクションの型」も多様だし、コレクション型の実装もまた多様です。どれかを決めたら残りは一意に決まるなんて事はありません

次の反論がありそうです; 「Z, Z」(Haskellなら「Int, Int」)とか「Z, bool」(「Int, Bool」)とか変なモノはコレクションとは呼ばない。コレクションの実例(型インスタンス)をもっと制限すれば関連性は一意に決まる。

では、どんな実装(型インスタンス)なら「コレクション」と呼んでいいのでしょうか? 境界線を確定できるでしょうか。構造の指標を定義しても、公理を書けないHaskellでは(ほとんどのプログラミング言語では)フォーマルに「コレクション」を規定するのは無理です。結局は人間同士の合意であり、その合意された暗黙のセマンティクスを、型クラス名、型名、関数名などの名前に込めているのです。

名前に込めた思いを理解しないコンピュータは、次の型クラス定義だって、まったく同じ扱いをします。「要素(element)」「コレクション(collection)」という言葉から、勝手に色々と連想するのは人間だけです。

Class CC :=
  {
    ss: Type;
    tt: Type;
    cc : tt;
    ff1 : ss -> tt -> tt;
    ff2 : ss -> tt -> bool
  }.

[追記]オマケ: コレクションの性質を公理にするなら、例えば次のようかな。∀と¬は論理記号の全称と否定。

  1. ∀x:e. ¬(member x empty) (emptyは要素をまったく含まない)
  2. ∀x:e.∀y:ce. (member x (insert x y)) (挿入した要素は入っている)

この公理だと、満杯になるとinsertが何もしない有界バッファは失格。上書き変数のモデルはOK。←いや、ダメだ。初期値が入るから公理1が満たされない。コレクションの型をオプション型(Maybe型)にして初期値は未定義にすればいいかな。[/追記]

名前・記号の問題は難しい

「名前に込めた思い」の「思い」は、「一人の思い」ではなくて、複数の人々で共有された思い、別言すれば「共通認識、暗黙の合意、言語的習慣」などです。また、「その場の空気、話の流れ」などもコミュニケーションの前提として共有されるものでしょう。多義語/乱用された記号の意味は、共有された思いにより解決されます。しかも、その解決法さえも暗黙に共有された知恵によります。

Haskellの型クラスの場合は、「集合(基本的な型)の名前を、構造(より複雑な型)の名前に流用する」という知恵を曖昧性解決原理に採用したのです。実際、この知恵(記号の乱用法)は人間同士で広く使われていて、記号・名前の節約に絶大な効果を発揮します。

しかし、記号の乱用をやり過ぎると混乱するので、明示的な記法に切り替えたり、文脈を固定してから略記を使うなどの方法が併用されます。我々は、無意識にけっこう色々うまくやっています。単一の原理に頼っているわけではありません。

人間が膨大な名前を正確に憶えて使い分けられるなら、そもそもオーバーロードなんて不要なんです。オーバーロードは人間のための機能なので、我々が「曖昧な表現」にどう対処しているかを定式化する必要があります。コンピュータだけでなく、人間の理解・認識に関わるので、どうしたって話は難しくなりますね。

[日常]イケダハヤトさんの記事


[雑記/備忘]Haskellの型クラスに関わるワークアラウンド

$
0
0

次の2つの記事で、Haskellの型クラス機構に、場合によっては不都合が生じることを指摘しました。

具体的には次のような問題があります。

  • Collects e ce のようなマルチパラメータの型クラスが素直には定義できない。
  • 整数を台集合とする掛け算モノイドと足し算モノイドの2つのモノイドを同時には定義できない。

これらに対するワークアラウンド(回避策)を示します。2番めの問題への対策は、トリックを駆使した挙句、本末転倒なことになっています(泣)。

内容:

  1. コレクション型の型クラスを定義する
  2. 整数の集合上に2つのモノイド構造を定義する(本末転倒)

コレクション型の型クラスを定義する

お馴染みになったであろうコレクション型の型クラスを再掲します。

class Collects e ce where
  empty :: ce
  insert :: e -> ce -> ce
  member :: e -> ce -> Bool

ここで、eは要素の型、ceはコレクション(要素の集まり)の型です。

僕は、「要素の型とコレクションの型から一意にコレクション構造が決まるなんて前提がオカシイでしょ」と言ったわけですが、想定される反論も記しておきました。「オーバーロードは何故にかくも難しいのか:Haskellの成功と失敗」より:

想定される反論は以下のものです; 勝手な型e, ceに対してコレクション構造が決まるわけではない。eとceは無関係ではなく、どちらかを決めればもう一方も決まる。例えば、要素の型がInt、コレクションの型は[Int](Intのリスト)のように。

この反論のアイデアに沿って考えます。コレクションの型ceに対して、要素の型eが一意に決まるなら、ce→e という対応があるので、それをElemとします。すると、e = Elem ce という等式が成立します。

e = Elem ce であるなら、型クラスCollectsの型パラメータは2つではなくて、ceただ1つになります。つまり、次のようです。

class Collects ce where
  type e = Elem ce
  empty :: ce
  insert :: e -> ce -> ce
  member :: e -> ce -> Bool

このままでは構文的事情でコンパイラを通らないので、中間の型変数eを消去して次のように書き換えました。型インスタンスを2つ定義しています。

{-# LANGUAGE TypeFamilies, FlexibleInstances #-}
import Data.Maybe

class Collects ce where
  type Elem ce
  empty :: ce
  insert :: Elem ce -> ce -> ce
  member :: Elem ce -> ce -> Bool

instance Collects [Int] where
  type Elem [Int] = Int
  empty = []
  insert x y = if member x y then y else (x : y)
  member x y = any (== x) y

instance Collects (Maybe Int) where
  type Elem (Maybe Int) = Int
  empty = Nothing
  insert x y = Just x
  member x y = if isJust y then x == fromJust y else False

先頭にあるゴニョゴニョは、Haskellの言語拡張を利用可能にするオマジナイです。type Elem ce が、コレクションの型ceをもらって要素の型を返すような対応(型から型への関数)Elemの存在を宣言しています。

前提への疑問は棚上げしてるし、「なんだよ、マルチパラメータじゃねーじゃん」と言われそうですが、とりあえずうまくいくようです。

整数の集合上に2つのモノイド構造を定義する(本末転倒)

モノイドの型クラスと型インスタンスを再掲します。

class Mon m where
  mon_op :: m -> m -> m
  mon_unit :: m

instance Mon Int where
  mon_op = (*)
  mon_unit = 1

こうすると、足し算のモノイド(↓)を型インスタンスとして定義できなくなるのでした。

instance Mon Int where
  mon_op = (+)
  mon_unit = 0

うまくいかない理由は、モノイド構造(台集合+演算+定数)に名前を付けられないからです。トリックを使って無理やりに名前を付けることにします*1

{-# LANGUAGE TypeFamilies, FlexibleInstances #-}

class Mon m where
  type Carr m
  mon_op :: m -> Carr m -> Carr m -> Carr m
  mon_unit :: m -> Carr m

data IntMulMon = IntMulMon
instance Mon IntMulMon where
  type Carr IntMulMon = Int
  mon_op _ = (*)
  mon_unit _ = 1

data IntAddMon = IntAddMon
instance Mon IntAddMon where
  type Carr IntAddMon = Int
  mon_op _ = (+)
  mon_unit _ = 0

CoqやStandard MLでモノイドを定義するときは、台集合(carrier set, underlying set)とモノイド構造は別物なので、モノイド構造→台集合 という対応を定義できます。圏論では忘却関手と呼ぶものです。

Haskellにおける型クラスMonへのパラメータmは台集合を意味するのですが、「イヤッ、mはモノイド構造の名前なのだ」と無理やり思い込むようにして、台集合は Carr m (the carrier of m)で取り出すことにします(そう思い込む)。

data IntMulMon = IntMulMon が名前の準備です。ダミーのデータ型で名前を作っているわけです。type Carr IntMulMon = Int は、「モノイド構造IntMulMonの台集合は集合Intだ」と解釈されます。別な名前を用意してあげれば、別なモノイド構造を定義できます。

実際、足し算を演算とするモノイド構造のためにIntAddMonという別な名前も準備しています。本来は構文的機構でサポートすべきことを、意味領域側に名前オブジェクトを作って代用していて、いかにもバッドノウハウ。

mon_opとmon_unitの引数が増えてしまってますが、第1引数はダミーです。mon_op 2 3 のように書いても、2 * 3 か 2 + 3 かの区別が付かないので、第1引数を使って mon_op IntAddMon 2 3 のようにします。

…… ……

フエーッ、こんなん、僕の欲しいオーバーロードじゃない!*2

*1:Haskellでの標準的な方法は、型クラスNumを経由して足し算モノイドと掛け算モノイドを定義するものです。

*2:例えば、掛け算でも足し算でも通用する話をするとき、演算子記号を◇とかして、(◇) = (*) か (◇) = (+) かの区別は、「話の文脈」として与えるべきなんでしょう。ここらへんをキチンと分析するなら、インスティチューション理論を使うべきかと思います。

[雑記/備忘]形容詞「複」「多」と箙〈えびら〉

$
0
0

圏には、様々な一般化があります。モノイド圏、豊饒圏、内部圏、高次圏、…。一般化のひとつの方向として、input/outputのアリティ(項数)を増やすことが考えられます。複圏(multicategory)と多圏(polycategory)がその方向での一般化です。圏は 1-in 1-out、複圏は n-in 1-out、多圏は n-in m-out です。

「圏 → 複圏 → 多圏」という一般化は単純な原理に貫かれていて分かりやすいです。形容詞の「複(マルチ)」と「多(ポリ)」の語感も僕は好きです。なので、形容詞「複」「多」を全面的に採用した用語法を使いたいと思うのですよ。

Xを対象(object)の集合とします。X上の圏をC、複圏をM、多圏をPとします。X*(星はクリーネスター)の要素(リスト、タプル)を [A1, ..., An] と書くことにします。

まずはホムセット(homset)に関して:

  • Cのホムセットは C(A, B)。
  • 複圏Mの複ホムセットは M([A1, ..., An], B)。
  • 多圏Pの多ホムセットは P([A1, ..., An], [B1, ..., Bm])。

「圏のホムセット」「複圏の複ホムセット」「多圏の多ホムセット」で辻褄があってます。気持ちいいです。では、ホムセットの要素はどう呼ぶでしょう。

  • ホムセットC(A, B)の要素を射と呼ぶ。
  • 複ホムセットM([A1, ..., An], B)の要素を複射と呼ぶ。
  • 多ホムセットP([A1, ..., An], [B1, ..., Bm])の要素を多射と呼ぶ。

これでよさそうですが、英語だと:

  • 射: morphism
  • 複射: multimorphism
  • 多射: polymorphism

polymorphismは「多相」とか「多態」とか訳す言葉で、オブジェクト指向の人が大好きな言葉ですから、厄介事を避けたいなら使うべきじゃないです。polymapとかpolyarrowとか言い換えているようです。でも、日本語の「多射」は違う語なので大丈夫そう。

ところで、複圏はオペラッド(operad)と呼ぶことも多いです。もともとは、対象が1個だけの対称複圏をオペラッドと呼んでいたのですが、最近は「オペラッド=複圏」として使うことが多く、「オペラッド」のほうが多数派の印象もあります。

圏から結合(composition)と恒等(identity)を忘れてしまうとグラフ(多重辺、自己ループ辺を許す有向グラフ)になります。グラフから自由生成して圏を作れます。この状況を複圏と多圏にも拡張したいですね。

  • 圏から結合と恒等を忘れるとグラフ。
  • 複圏から複結合と恒等を忘れると複グラフ。
  • 多圏から多結合と恒等を忘れると多グラフ。

これも英語だと問題があります。multigraphは、多重辺を許すグラフの意味で使われます(Wikipedia: Multigraph*1。そして、polygraphは嘘発見器です。

そもそも「グラフ」という言葉は曖昧で、無向か有向かもハッキリしないし、多重辺・自己ループ辺の扱いもその場その場で決めることになります。多重辺・自己ループ辺を許す有向グラフを意味する言葉にquiverがあります(Wikipedia: Quiver)。quiverは矢筒のことで、日本語では〈えびら〉という訳語があります。日本の箙の写真↓

箙という言葉を使うなら、辺はやっぱりアロー(矢)と呼ぶべきでしょう。

  • 圏から結合と恒等を忘れると箙。箙はアローの集合。
  • 複圏から複結合と恒等を忘れると複箙。複箙は複アローの集合。
  • 多圏から多結合と恒等を忘れると多箙。多箙は多アローの集合。

あー、用語法の辻褄を合わせるのは難しい。それに、辻褄を合わせると世間の習慣からズレてしまうこともあるし。頭が痛い。

*1:ちなみに、多重辺・自己ループ辺を許さず向きもないグラフは単純グラフです(mathworld: Simple Graph)。

[雑記/備忘]JavaScriptで説明するオーバーロード解決

$
0
0

型クラスの話をしました。

Haskellの型クラスは元祖・型クラス*1なんですが、なんか残念な仕様です。整合性が歪んでいる理由は、オーバーロード機能を優先しているからです。その分、構造としての型を記述する機能は犠牲になっています。トレードオフだから仕方ないけど。

ところで、オーバーロードの解決(多義性の解決)って、どうやるんでしょうか? そのメカニズムをJavaScriptのサンプルコードを使って説明します。

なお、「多相とオーバーロードはどう違うか」とかの話は、どうでもいい割に議論すると消耗してバカバカしいので一切触れません。(ちなみに、「並列と並行の違い」なんて議論も時間の無駄。暇つぶし以上の意味はない。)

内容:

  1. オーバーロード
  2. C++の仮想関数テーブル
  3. 型情報を引数として渡す
  4. 実装関数のまとめ方を変えてみる
  5. Haskellの型クラス
  6. 最後に

オーバーロード

オーバーロード(overload(ing))とは、単一の記号・名前を異なる複数の意味で使用することです。異なる複数の意味を持つ記号・名前を多義的記号・名前(overloaded symbol/identifier)と呼びます。この記事で扱う記号・名前は関数名だけです。話題は多義的関数名ですね。

JavaScriptでは、次のようにして名前と意味(関数の実体)を結びつけます。

var times = function(n, m) {return n*m};

次の例は、timesという名前に異なる2つの関数を結びつけようとしています。

// 数の掛け算
var times = function(n, m) {return n*m};
// 文字列の繰り返し連接
var times = function(s, m) {
              var r = "";
              for (var i = 0; i < m; i++) {r += s};
              return r;
            };

しかし、最初の定義は二番目の定義で上書きされてしまい、同時に2つのtimesは存在できません。timesという1つの名前に2つの機能を持たせたいなら、次のように書けます。

var times = function(x, m) {
  // 数の掛け算
  if (typeof x === 'number') {return x*m}
  // 文字列の繰り返し連接
  if (typeof x === 'string') {
    var r = "";
    for (var i = 0; i < m; i++) {r += x};
    return r;
  }
  throw "Type Error";
};

実行時の型振り分け(type case)が出来ないプログラミング言語や、「引数の型は、数か文字列かハッキリしないとダメ」と融通が効かないプログラミング言語では、こうはいきません。

そもそも、人間がif文/case文で振り分けるのはひど過ぎます。もっとマシな方法が欲しいですよね。オーバーロード機能は、1つの名前に対する異なる複数の意味を上手に管理するメカニズムを提供します。多義的記号・名前(ここでは多義的関数名)に対して、適切な意味を決定します。この「意味の決定」がオーバーロードの解決です。

C++の仮想関数テーブル

まず、C++や同系統の言語で使われているvtbl仮想関数テーブル)方式を見てみましょう。

// overload-sample.cpp
#include <iostream>

class Base {
public:
    void hello() {
        std::cout << "Hello, world.\n";
    }
    virtual void introduceMyself() {
        std::cout << "Hi, my name is Base.\n";
    }
};

class Derived : public Base {
public:
    // 明示的に宣言しなくても、このメソッドはvirtual
    void introduceMyself() {
        std::cout << "Yo, I'm Derived.\n";
    }
    void morning() {
        std::cout << "Good morning, world.\n";
    }
};

int main() {
    Base *p1 = new Base();
    Base *p2 = new Derived();
    Derived *p3 = new Derived();
    p1->hello();
    p1->introduceMyself();
    // p1->morning(); // Baseはmorning()を持たないのでコンパイルエラー
    p2->hello();
    p2->introduceMyself();
    // p2->morning(); // Baseはmorning()を持たないのでコンパイルエラー
    p3->hello();
    p3->introduceMyself();
    p3->morning();
    return 0;
}

コンパイルして実行すると:

$ g++ overload-sample.cpp

$ ./a.exe
Hello, world.
Hi, my name is Base.
Hello, world.
Yo, I'm Derived.
Hello, world.
Yo, I'm Derived.
Good morning, world.

$

上記のC++コードをJavaScriptにコンパイルするなら、次のようになるでしょう。

/*
 * class Base
 */
function Base_hello(_this) {
  console.log("Hello, world.\n");
}

var vtblBase = {
  "introduceMyself" : function(_this) {
    console.log("Hi, my name is Base.\n");
  }
};

// Baseのコンストラクタ
function Base() {
  this.vptr = vtblBase;
}

/*
 * class Derived
 */
var vtblDerived = {
  "introduceMyself" : function(_this) {
    console.log("Yo, I'm Derived.\n");
  }
};

function Derived_morning(_this) {
  console.log("Good morning, world.\n");
}

// Derivedのコンストラクタ
function Derived() {
  this.vptr = vtblDerived;
}

/*
 * main
 */
function main() {
  var p1 = new Base();
  var p2 = new Derived();
  var p3 = new Derived();
  Base_hello(p1);
  p1.vptr["introduceMyself"](p1);

  Base_hello(p2);
  p2.vptr["introduceMyself"](p2);

  Base_hello(p3);
  p3.vptr["introduceMyself"](p3);
  Derived_morning(p3);
  return 0;
}

ブラウザのJavaScriptコンソールで実行すると:

> main()
  Hello, world.
  Hi, my name is Base.
  Hello, world.
  Yo, I'm Derived.
  Hello, world.
  Yo, I'm Derived.
  Good morning, world.
  0

コンパイルの要点をまとめると:

  1. 仮想でないメソッドは大域関数にコンパイルする。クラス名を接頭辞にすることによりオーバーロードは(コンパイル時に静的に)解決される。
  2. メソッドに対応する大域関数の最初の引数にはthisオブジェクトが入る。今回の例では第1引数が使われてないが、インスタンスオブジェクトのデータメンバー(フィールド、プロパティ)にアクセスする際に第1引数が必要。
  3. 仮想メソッドの実装関数は、クラスごとの仮想関数テーブル(vtblBaseとvtblDerived)に格納される。単一の仮想メソッド名に対する複数の実装が、複数のテーブルに分散されて格納される。
  4. オブジェクトには、vptrというデータメンバーがあり、所属するクラスの仮想関数テーブルを指すように初期化される。
  5. 仮想でないメソッド呼び出しは、対応する大域関数を呼び出す。
  6. 仮想メソッド呼び出しは、インスタンスオブジェクトのvptrからvtbl(仮想関数テーブル)を特定し、関数名をキーにして実装関数を引いて実行する。こうして仮想メソッドのオーバーロードが(実行時に動的に)解決される。

仮想メソッドの呼び出しは、p.vptr["methodName"](p) という形ですが、C言語で書くなら (p->vptr)[METHOD_NAME](p) のようになります。METHOD_NAMEは文字列名前ではなくて番号です。p->vptrが仮想関数テーブルの先頭になるので、テーブル(配列)へのインデックスアクセスです。

多重継承があるともっとややこしくなりますが、同じ名前に複数の実装があるときの決定方式の基本はこんな感じです。

型情報を引数として渡す

先に例に出したtimesの例をもう一度考えてみます。

// 数の掛け算
function times(x:number, m:number) {return x*m}

// 文字列の繰り返し連接
function times(x:string, m:number) {
  var r = "";
  for (var i = 0; i < m; i++) {r += x};
  return r;
}

前と少し違っているのは、関数の引数に型宣言が付いていることです(TypeScriptならこう書けます)。呼び出すときに第1引数の型が分かれば、どちらの実装を動かすかを判断できます。

もし第1引数がvptrを持つオブジェクトであるなら、C++のvtbl(仮想関数テーブル)方式がそのまま使えます。次のようにコンパイルすればいいでしょう。

var vtblNumber = {
  "times": function (x, m) {return x*m}
};

var vtblString = {
  "times": function (x, m) {
    var r = "";
    for (var i = 0; i < m; i++) {r += x};
    return r;
  }
};

function times(x, m) {
  return x.vptr["times"](x, m);
}

しかし、数値データはvptrのようなメタ情報を持ってないのが普通です。vptrがなければ、第1引数からvtblを知ることは出来ません。もしコンパイラや実行環境が第1引数の型を知っているなら、その型のvtblを、呼び出す関数に渡してあげればいいでしょう。関数は、vtblを受け取る引数を余分に持つことになります。

var vtblNumber = {
  "times": function (x, m) {return x*m}
};

var vtblString = {
  "times": function (x, m) {
    var r = "";
    for (var i = 0; i < m; i++) {r += x};
    return r;
  }
};

function times(vptr, x, m) {
  return vptr["times"](x, m);
}

> times(vtblString, "A", 4)
  "AAAA"
> times(vtblNumber, 3, 4)
  12

この方式だと、timesという名前の関数は実際に存在します。次の特徴があります。

  1. 元の第1引数の型情報(この場合はvtbl)を渡すための引数が追加されている。
  2. この関数は実際の仕事はしない。型情報を使って実装関数を特定して仕事を振り分けるだけ。

実装関数のまとめ方を変えてみる

前の節の例で、オーバーロードされた関数(多義的名前を持つ関数)timesの実装関数(実際の仕事を行う関数)は、vtblString["times"] と vtblNumber["times"] でした。引数の型ごとにvtblを作ってました。実装関数のまとめ方、つまりvtblの編成方法を変えてみみます。

var vtblTimes = {
  "number": function (x, m) {return x*m},
  "string": function (x, m) {
    var r = "";
    for (var i = 0; i < m; i++) {r += x};
    return r;
  }
};

function times(typeId, x, m) {
  return vtblTimes[typeId](x, m);
}

> times("string", "A", 4)
  "AAAA"
> times("number", 3, 4)
  12

型ごとにvtblを作るのではなくて、多義的関数名に対してひとつのvtblを作っています。この場合、vtblから特定の実装関数を選び出すキーは型名になります*2

オブジェクト指向だと、メソッドは型(クラス)に“所属する”イメージがあります。実際、型ごとにvtblが作られます。しかし、多義的関数ごとにvtblを持ってもかまいません。なんだったら、単一のvtblにすべての実装関数を詰め込んだっていいです。

var vtbl = {
  "times" : {
    "number": function (x, m) {return x*m},
    "string": function (x, m) {
      var r = "";
      for (var i = 0; i < m; i++) {r += x};
      return r;
    }
  },
  "addOne" : {
    "number": function (x) {return x + 1},
    "string": function (x) {return x + "One"}
  }
};

function times(typeId, x,  m) {
  return vtbl["times"][typeId](x, m);
}
function addOne(typeId, x,  m) {
  return vtbl["addOne"][typeId](x, m);
}

> times("number", 3, 4)
  12
> times("string", "A", 4)
  "AAAA"
> addOne("number", 5)
  6
> addOne("string", "hello")
  "helloOne"

いずれの場合でも、timesの第1引数(型情報)は、コンパイラ/インタプリタが自動的に計算し埋めるので*3、ユーザー(プログラマ)からは見えなくなります。

Haskellの型クラス

さて、Haskellの型クラスです。次を例題とします。

import Data.Maybe

class Eql a where
  eql :: a -> a -> Bool

class (Eql a) => TotOrd a where
  lt :: a -> a -> Bool

instance Eql Int where
  eql n m = n == m

instance TotOrd Int where
  lt n m = n < m

member :: (Eql a) => a -> [a] -> Bool
member x [] = False
member x (y:ys) = if eql x y then True else member x ys

findGe :: (TotOrd a) => a -> [a] -> Maybe a
findGe x []  = Nothing
findGe x (y:ys) = if eql x y || lt x y then Just y else findGe x ys

既に定義済みのEq, Ordとかぶらないように、例題の型クラスはEql, TotOrd(total order)としています。話を簡単にするために、関数の最初の引数*4の型だけを見てオーバーロードを解決することにします。また、リスト型のような型パラメータを持つ型(型構成子)は扱いが面倒なので割愛します。

Haskellとオブジェクト指向の用語法にズレがあるので注意しましょう。

Haskell オブジェクト指向言語
型クラス インターフェイス
型インスタンス インターフェイスを実装したクラス
データ値 オブジェクト

Haskellの場合もvtblと同様なテーブルを使いますが、テーブルを辞書(dictionary)と呼びます。多義的関数は、辞書を引数にもらって仕事の振り分けをします。型クラスで宣言されたメソッド名(多義的関数名)ごとに、振り分け関数を作ります。JavaScriptにしてみると:

/*
class Eql a where
  eql :: a -> a -> Bool
*/
function eql(dic, x, y) {
  return dic["eql"](x, y);
}

/*
class (Eql a) => TotOrd a where
  lt :: a -> a -> Bool
*/
function lt(dic, x, y) {
  return dic["lt"](x, y);
}

型インスタンス宣言ごとに1つの辞書を作ります。dicXxxという名前で辞書を作ることにします。ただし、「EqlクラスのInt」と「TotOrdクラスのInt」のように、同じデータ型が複数の型クラスに属するので、辞書のネーミングパターンは、dic〈型クラス名〉$〈データ型名〉にします。

/*
instance Eql Int where
  eql n m = n == m
*/
var dicEql$Int = {
  "eql": function(n, m) {return n == m}
}

/*
instance TotOrd Int where
  lt n m = n < m
*/
var dicTotOrd$Int = {
  "lt": function(n, m) {return n < m}
}

これで、多義的関数を使う準備は出来ました。コンパイラは、多義的関数の名前とその多義的関数の所属するクラスを記憶してます。

コンパイラが、多義的関数を内部で使っているユーザー定義関数を見つけると、その関数を次の方針で書き換え(プログラム変換し)ます。

  • ユーザー定義関数に、辞書を渡す引数を追加する。
  • 多義的関数の呼び出しに対して、辞書を渡すようにする。

member関数は内部でeqlを使用してるいるので、Eql型クラスの辞書を必要とします。次のようになります。

/*
member :: (Eql a) => a -> [a] -> Bool
member x [] = False
member x (y:ys) = if eql x y then True else member x ys
*/
function member(dicEql, x, ys) {
  if (ys.length === 0) {return false}
  var y = ys.shift();
  if (eql(dicEql, x, y)) {
    return true;
  } else {
    return member(dicEql, x, ys);
  }
}

> member(dicEql$Int, 3, [1, 2, 3, 4])
  true
> member(dicEql$Int, 3, [1, 2, 0, 4])
  false

ユーザー定義関数が、色々な多義的関数を使っていて、それらが複数の型クラスに属するとき、複数の辞書を渡す必要があります。findGe関数は、Eql型クラスのeql多義的関数と、TotOrd型クラスのlt多義的関数を使っています。したがって、findGe関数のコンパイル結果は2つの追加引数を持ちます。

/*
findGe :: (TotOrd a) => a -> [a] -> Maybe a
findGe x []  = Nothing
findGe x (y:ys) = if eql x y || lt x y then Just y else findGe x ys
*/
function findGe(dicEql, dicTotOrd, x, ys) {
  if (ys.length === 0) {return null}
  var y = ys.shift();
  if (eql(dicEql, x, y) || lt(dicTotOrd, x, y)) {
    return y;
  } else {
    return findGe(dicEql, dicTotOrd, x, ys);
  }
}

> findGe(dicEql$Int, dicTotOrd$Int, 3, [1, 2, 3, 4])
  3
> findGe(dicEql$Int, dicTotOrd$Int, 3, [1, 2, 0, 4])
  4
> findGe(dicEql$Int, dicTotOrd$Int, 3, [1, 2, 0, 1])
  null

実験では手動で辞書を渡していますが、コンパイラの型推論機能によりどの辞書を渡すかは決定できるので、ユーザー(プログラマ)からは辞書のやり取りは見えなくなります。

最後に

オーバーロード機能を使うために、そのメカニズムを知っている必要はありません。しかし、日常的比喩や衒学的ウンチクで解釈しようとすると、無意味な思弁的考察や不毛な議論に陥ったりします。僕は、そのテの似非哲学的な能書きをだいぶ嫌っているので、ドライなメカニズムから理解するアプローチになります。

メカニズムの観点から見れば、「多相」と呼ぼうが「オーバーロード」と呼ぼうが、オブジェクト指向だろうが関数型だろうが、そんな区分は別にどうでもいいじゃん、と思います。

*1:"How to make ad-hoc polymorphism less ad hoc" Philip Wadler and Stephen Blott (1988) で提唱されてすぐに実装されたので、既に四半世紀を越える使用実績です。

*2:CLOS(Common Lisp Object System) の総称関数では、実装関数へのディスパッチを高速化するため、総称関数ごとにメモ化テーブル(キャッシュ)を持たせてました。(今でもそうかな?)

*3:Coqでは、暗黙の引数を自動的に埋めるか、ユーザーが手で指定するかを制御できます。@funcのようにアットマークを付けると、手動の引数渡しになります。

*4:Haskellの関数はカリー化されているため、引数と言えば最初の引数だとも言えます。

[日常]いいかげんにしろ! 檜山でも怒る

$
0
0

愚痴と小言はしょっちゅうだが、怒ることは滅多にない。だが、

この発言、というかメンタリティには怒りが湧き上がる。何を言ってるんだ、コイツは!

「残業100時間超で自殺は情けない」 -- たった一言だが、その一言でアウトだ。長谷川教授の経歴をみるに優秀だろうし実績もあるのだろう。だが、この一言だけでクズだ。

僕がこの種の話題に反応してしまう理由は過去に書いているから、新たには書かない。参照と引用だけ。

2006年 「悟らないでも分かりたい (過去編)」:

僕は元来、根性論や精神論を忌み嫌っている。「死ぬ気でやれ!」って、ほんとに死んでしまったらどう責任とるのだ?

[...]「鍛える」とか「成長のために」とかは、たいていは無知、誤解、口実だ。

[...]僕が(そしてたぶん多くの人が)経験した過負荷状態とは、そのようなもの[きちんと計画・管理されたトレーニング]では全くなかった。闇雲にガムシャラにひたすらに、しかし先が見えない強行軍に過ぎなかった。そんなことはやるべきではない。絶対に経験すべき/させるべきではない。

[...]たまに、「死ぬ思い」を武勇伝として語る輩がいる(信じられない行為だ!)が、オトギ話くらいに思ったほうがいい。

[...]「鍛える」とか「育てる」とかの名目で、根性論や精神論を持ち出すことには、生理的ともいえる嫌悪感を感じる(ありていに言って、吐き気をもよおすってこと)。

2007年 「みんながとても頑健なわけじゃない

根性主義のわずかな匂いにも吐き気をもよおす。

[...]僕が非常に腹立たしく感じる事態は、「死ぬ気でやってみろよ、死なないから」ってタイプの発言が、「とても頑健な人」の口から出ること; あんまり頑健でない人までも、そんな言葉にあこがれてしまうこと。

2013年 「タチの悪いエリートとは

エリートは何もビジネスに限らず、スポーツでもエリートはいますよね。フィジカル・エリートであるコーチは、常人の限界を超えるトレーニングを「普通に」課したりしかねない、怖いですね。

[...]昨今の体罰・暴力問題にも同様な構造があるような気がします。[...]体罰をイヤな体験として捉えていたスポーツ選手がコーチになれば、体罰を繰り返すことはないでしょう。しかし、「自分は体罰で鍛えられた、あれで良かった」という、ある種の成功体験を持っているなら、同じ方法を採用する可能性が高いでしょう。

過酷な環境での成功体験を、美談や英雄譚として語るのは、もういいかげん、ほんとにやめよう。頑健・強靭な人の特殊事例を、一般的に通用する教訓や法則に仕立てて語るのも、ほんとにほんとにやめよう。語る本人は気持ちいいかも知れないが、それには弊害や犠牲が伴うのだから。

[追記]

続報によると、「処分検討」とのことです。一般的には、「舌禍が仕事に影響するのは可哀想」だと思いますが、この件では是非に処分してくれ! 許されるべき行為ではない。

[/追記]

[日常]苦しみを濾過した美しき武勇伝を語るな!

$
0
0

ムカ…ムカ…  ムカつく感情がおさまらない。単なる私憤を吐き出します。「いいかげんにしろ! 檜山でも怒る」の続き。僕がこのテの件に過敏・過剰であることは承知の上です。

最初の投稿における「残業100時間超で自殺は情けない」はもちろん論外もいいところだが、謝罪の文面がさらに僕を憤〈いきどお〉らせる。

  1. 言葉の選び方が乱暴で済みませんでした。
  2. とてもつらい長時間労働を乗り切らないと、会社が危なくなる過去の経験のみで判断し、今の時代にその働き方が今の時代に適合かの考慮が欠けていました。

1番目の「言葉の問題」へのすり替えは、別な長谷川さんも使っていた論法だが、そんな所が問題じゃない。誰も国語力の欠如を責めたりはしてない。どんなに慎重に言葉を選んだところで、考え方が狂っているんだから、ダメに決まってんだろう。言葉選び/表現に慎重になるのなら、2番目の「今の時代」の繰り返しは何だ? 確かに国語力がないのは確認できるが、そもそも国語力はどうでもいい事なので、まったく謝罪になってない。

2番目、これが酷い。「過去の経験のみで判断し」-- これだよ、コレ。これが僕が忌み嫌い憎んでいる発想。過去の経験が、現在の判断基準となっている; それは当たり前のことで、それ自体は何ら問題ではない。判断基準を形成している経験が「とてもつらい長時間労働」を乗り切った経験であること、その経験を判断基準に選んでいること、それがダメダメで、時代性云々じゃない。

「とてもつらい」と書いているのだから、実際に過酷だったのだろう。だけど、その苦労が報われてしまうと、その辛さが「いい経験だった」に転換してしまうんだよね。挙句に「思えばあれも楽しかった」とか。

それで、「楽しかった」があながち嘘でもない所がまた厄介。経験がある人もいると思うが、切羽詰まった強行軍、度を超えたハードワークには高揚感がある。あえて高いストスレを求める人や組織を「アドレナリン・ジャンキー」と呼んだりするくらいで、過酷な環境に耐性があり、かつ快楽を感じる人が一定数いるのも事実。

アドレナリン・ジャンキー、ストレス・エリートがその才能により、過酷な状況を乗り切ったとき、快楽と成功が結びつくのだから、その体験に強くポジティブな価値を与えるのは、まー必然で、ジャンキーのグッドトリップが判断基準に組み込まれてしまう。だけどその判断基準は間違っている。合理性/科学性がないジャンキー理論だ。

苦しみを濾過しないで正確に思い起こして欲しい。「とてもつらい」ことは「とてもつらい」のだ。ジャンキーでさえ消耗し疲弊する。ジャンキーならぬ回りの人々はさらに深く傷を負い、回復不能に損壊する。そんな惨状のドコが自慢げに語るに値する?

長谷川教授は不謹慎過ぎた。死者に鞭打つ言動で糾弾された。しかし、おそらく彼は、生者に鞭打っている。無自覚的だろうが、自分と同じ「とてもつらい」思いを周囲にも強いているはずだ。そうでなければ、あんな言葉は出ない。そのジャンキーな思考法を変えられないなら、若者達と接触すべきではない。

[雑記/備忘]型付きラムダ計算と4つの圏

$
0
0

型付きラムダ計算に対する“デカルト閉圏による意味論”、あるいは“カリー/ハワード対応”においては、型付きラムダ項の計算(書き換え、変換)だけではなくて、シーケント計算が必要になります。型理論では、シーケントを型判断(type judgement)と呼ぶことが多いですが、内実はシーケントです。

シーケントは、その形からいくつかの種類に分類できます。そして、シーケントの種類ごとに対応する圏も別な種類になります。これらの圏は、よく似ているので同一視することもありますが、区別したほうがいいと思います。

内容:

  • モノイド閉圏
  • シーケントの分類
  • 4つの圏
  • 微妙な気持ち悪さ

モノイド閉圏

型付きラムダ計算の圏的意味論(categorical semantics)はデカルト閉圏を舞台として行います。これについては、次の記事を参照してください。

ここでは、デカルト積(直積)とは限らないモノイド積を持つ閉圏、つまりモノイド閉圏(monoidal closed category)Cを考えます。モノイド閉圏Cでは、次の随伴性(adjunction)が存在します。

  • C(A¥otimesB, C) ¥stackrel{¥sim}{=} C(A, [C←B])
  • C(A¥otimesB, C) ¥stackrel{¥sim}{=} C(B, [A→C])

[A←B]は右指数、[A→C]は左指数で、それぞれ右からの¥otimes掛け算、左からの¥otimes掛け算の随伴パートナーです。この同型を与えるホムセット間の写像をΛr, Λlとしましょう。

  • Λr: C(A¥otimesB, C)→C(A, [C←B])
  • Λl: C(A¥otimesB, C)→C(B, [A→C])

ΛrとΛrは、右カリー化(右ラムダ抽象)と左カリー化(左ラムダ抽象)を与えます。随伴の余単位として、右評価射(right eval)と左評価射(left eval)が決まるので、これらを使ってラムダ計算が出来ます。

Cが対称モノイド圏のときは、左右を入れ替えることが出来るので、単一のΛ(カリー化、ラムダ抽象)とev(評価射、適用射)で十分で、デカルト閉圏の場合とよく似た計算ができます。デカルト閉圏ではない対称モノイド閉圏の例として、有限次元ベクトル空間の圏 (FdVectK, ¥otimes, K) があります。Kはスカラー体(係数体)で、¥otimesはベクトル空間のテンソル積です。FdVectK内の指数は、[W←V] := W¥otimesV*, [V→W] := V*¥otimesW で与えられます*1

非対称な例は、テンパリー/リーブ圏やその変種のなかに見いだせるでしょう。アブラムスキーの論説あたりが参考になるかな。

  • Title: Temperley-Lieb Algebra: From Knot Theory to Logic and Computation via Quantum Mechanics
  • Author: Samson Abramsky
  • Pages: 45p
  • URL: https://arxiv.org/abs/0910.2737

以下では対称性を仮定します。非対称だと話が面倒になるからです。しかし、非対称でも通用する話がけっこうあります。

対称モノイド閉圏におけるラムダ計算は、だいぶ昔から「弱いラムダ計算」と呼んでいたものです。

シーケントの分類

対称モノイド閉圏Cを選んで、その上で解釈できる(型付き)ラムダ項の構文を決めます。ラムダ項の作り方はここでは省略します(「型付きラムダ計算のモデルの作り方」、「セマンティック駆動な圏的ラムダ計算とシーケント」参照)。

A1, ..., A1, Bを型(Cの対象)、x1, ..., xnを変数、Eをラムダ項とします。ただし、Eは{x1, ..., xn}以外の自由変数を含まないとします。このとき、次の形をシーケントと呼びます。

  • x1:A1, ..., xn:An ⇒ E:B

この形のシーケント(型理論の用語では型判断)の“意味”は、A1¥otimes ...¥otimesAn→B in C という射になります。

「⇒」の左側の型宣言が1つだけ(n = 1)のシーケントを単純シーケントと呼ぶことにします。また、左側が空(n = 0)のシーケントを片側シーケントと呼びます。

左辺の型宣言の個数nによる分類をまとめると:

n 名称 形式
n = 1 単純シーケント x:A ⇒ E:B
n = 0 片側シーケント ⇒ E:B
nは任意 一般のシーケント x1:A1, ..., xn:An ⇒ E:B

4つの圏

前節の3種のシーケント、それと型付きラムダ項に対して、それぞれに対応する4つの圏があります。

シーケントの分類 対応する圏 圏の種類
単純シーケント C 対称モノイド閉圏
片側シーケント singleC 対称モノイド圏
一般のシーケント multiC 対称モノイド複圏
型付きラムダ項のみ typeC C-豊饒圏

モノイド閉圏Cをもとに、singleC, multiC, typeCを作ることが出来ます。

S := singleC, M := multiC, T := typeC として、それぞれの圏の概略を記述してみます。ΛAB,C:C(A¥otimesB, C)→C(B, [A→B]) は左カリー化、evA,B:A¥otimes[A→C]→B in C を左評価射とします。

S := singleC

Sの対象類とホムセット族は次のとおりです。

  • |S| = |C|
  • S(A, B) = C(I, [A→B])

IはCの単位対象、[A→B]はCの左指数(左掛け算の随伴パートナー)です。

Cの結合/恒等とは区別して、Sの図式順結合を#、恒等をjAとします。a:A→B, b:B→C in S とは、a:I→[A→B], b:I→[B→C] in C のことです。C内で考えて、a#b は次の式で与えられます。(δ:I→I¥otimesIは、λ、ρを左右の単位律子*2として、δ := λI = ρI です。)

  • a#b := ΛAI,C((A¥otimesδ);(A¥otimesa¥otimesb);(evA,B¥otimes[B→C]);evB,C)
  • jA := ΛAI,AA)
M := multiC

Mは普通の圏ではなくて複圏(multicategory)です。複圏をオペラッド(operad)とも呼びます(「形容詞「複」「多」と箙〈えびら〉」を参照)。

複圏の対象類と複ホムセット族は次のとおりです。

  • |M| = |C|
  • M([A1, ...,An], B) = C(A1¥otimes...¥otimesAn, B)

対称モノイド圏から対称複圏(対称オペラッド)を作るやり方はここでは述べませんが、やり方は決まっています。例えば、次の論説などを参考にしてください。前半(20ページまで)がオペラッドの丁寧な解説になっています。

T := typeC

Tは通常の圏ではなくて、対称モノイド圏Cで豊穣化された圏(enriched category)です。

  • |T| = |C|
  • T(A, B) = [A→B]

T(A, B)はホムセットではなくてホム対象です。C-豊饒圏としての結合γと恒等ιは次のように与えられます。

  • γA,B,C:[A→B]¥otimes[B→C]→[A→C] in C であり、γA,B,C := ΛA[A→B],[B→C]((evA,B¥otimes[B→C]);evB,C)
  • ιA:I→[A→A] in C であり、ιA := ΛAI,AA)

定義を見れば分かるように、C-豊饒圏Tと通常の圏(Set-豊饒圏)Sは非常に近い関係にあります。このことについては、「豊饒圏(ピノキオ)が圏(人間)になる物語」に詳しく書いています。Cの指数を内部ホムとして作ったC-豊饒圏がTで、Tから作った通常の圏(ピノキオの人間化)がSになっています。

微妙な気持ち悪さ

型付きラムダ計算の圏的意味論において、単一の構文と単一の圏を使おうとすると、微妙な気持ち悪さが残ります。何かがズレているような感覚です。この気持ち悪さは、複数の圏を無理に同一視することが原因ではないかと思えます。前節で概略だけを示した4つの圏とその相互関係を、もっと精密に調べてみる価値はありそうです。

*1:有限次元ベクトル空間の圏はコンパクト閉圏になります。

*2:律子(りつし)に関しては「律子からカタストロフへ」を参照してください。

[日常]viXraのトンデモとarXivのトンデモ

$
0
0

arXiv.orgのオルタナティブ!? viXra.org」で紹介したviXraは、案の定トンデモの巣窟ですね。だいぶ香ばしい例を紹介すると:

Part I: The P vs NP は7行のテキスト、Part II: Riemann Hypothesis は4行のテキストと4枚の画面キャプチャ画像からなります。この2つのミレニアム懸賞問題について“何か”書いてあります。「解いた」つもりなんでしょうか? 意味不明です。

本家arXivは、トンデモ野放し状態ではないので、ある程度の質は担保されていると言われます。しかし、次のような論文をみつけました。

「リーマン予想を証明した(これが普通のパターン)」じゃなくて「リーマン予想は間違っている」です。

大手商業出版社であるシュプリンガーから出版された本に、「フェルマーの最終定理」と「ゴールドバッハ予想」の初等的な“証明”が載っていた、なんて例もありますからね。

情報の真偽を、媒体の評判(世間的認知度)から判断するのも難しい。もう何を信頼していいやら -- 中身を自分で判断するしかないのか、、、疲れる。


[雑記/備忘]真面目に「arXiv vs. viXra」、そしてトンデモ判定器

$
0
0

昨日、viXraはもとよりarXivにもトンデモは混じってるよ、と書きました。もちろん、平均的な質はarXivのほうが高いのは間違いありません。

arXivとviXraの比較を真面目にやっている人達がいました。その報告がarXivに載っています。

  • Title: A Scienceographic Comparison of Physics Papers from the arXiv and viXra Archives (5 Nov 2012)
  • Authors: David Kelk, David Devine
  • Pages: 9p
  • URL: https://arxiv.org/abs/1211.1036

タイトルにあるscienceographicはscienceography(サイエンセオグラフィー)の形容詞で、scienceographyは、科学の文献(書かれたもの)を科学する分野だそうです。ここで使われている手法は、比較対象である論文の意味内容には立ち入らず、ページ数、著者の人数、図版や定理の数、参照している文献数のような外形的特徴(メトリック)だけで論文群を比較するものです。

上記Comparison報告では、arXivとviXraの意義やら優劣やらには一切言及せずに、淡々と比較結果が報告されています。有意と思える差が出てますが、「そりゃそうだろな」という感じで驚くような結果はありません。例えば、viXraは単独著者が多いとか、文献の引用・参照の形式がイイカゲンだとか。

このComparisonの調査は何のために行われたのでしょうか? 単にscienceographyを適用する対象データとして適当だった、というだけかも知れません。でも、別な意図があったようにも思えます。うがった見方をすれば、(Comparison報告の著者達はおくびにも出してないが)トンデモ論文を意味解析なしに判定する関数を探しているんじゃないだろうか。

Comparison報告内に"Metrics Identifying Source Archive"という節があるのですが、source archiveとは「arXivかviXraのどちらか」ということで、サンプリングした論文をうまいメトリック関数に渡すと、「arXivかviXraのどちらか」を判定できるといいな、という背景(隠れた意図)が感じられます(僕の感覚では)。

二値の判断はどうせ無理ですが、「かなり香ばしい」とか「まともである可能性が高い」といった“傾向としての判断”は出来そうな気がします。

[雑記/備忘]Ring, Rng, Rig

$
0
0

環(ring)から単位元必須の要求を外した代数系をRngと呼ぶことがあるんですね。知らなかった。

  • Wikipedia項目: Rng

語源は「a ring without "i"」というダジャレです。ルングと発音すると書いてあるけど、(カタカナじゃない)日本語訳をどうするか?困りますね。別名として出されているnon-unital ringなら、「非単位的環」と直訳できるし、意味もすぐに分かってイイような気がします、洒落てないけど。

ダジャレと言えば、Rig(リグ)というのもあります。

  • nLab項目: Rig

こっちは「a ring without "n"egatives」で、負の元を持たない、あるいは引き算が出来ないかも知れない環です。このダジャレを言い出しのはローヴェル(Lawvere)らしいです。「半環」(semiring)という普通の言い方があり、Rigより普及しているでしょう。

駄洒落たネーミングって、あんまり一般化しないみたい。

[日常]最近のチェン先生

$
0
0

“元気のいい圏論・モナド・オネエさん”として紹介したことがあるシェフィールド大学のユージェニア・チェン (Eugenia Cheng)先生

最近どうしてるのかな?と思ってYouTubeを探してみたら、相変わらず元気ですね。2015年に"How to Bake Pi"という本を出版したとのことで、その宣伝をかねてでしょうか、テレビ番組に出演なさってます。テンション高すぎの平野レミさんと僕が好きなたかまつななさんを足した(で割り算はしない)ようなキャラクターです。

D

チェン先生、芸人ですわ。

チェン先生は音楽家(ピアノ)でもあるんですが、演奏の動画は見あたらなかったです。が、歌声は聞けます。上記のテレビ番組のときに撮影したようです。

D

[雑記/備忘]量子と古典の物理と幾何

$
0
0

というイベントが来年(2017年)あります。

  • 日時: 2017/02/11 (土) 10:00 ~ 2017/02/12 (日) 17:30
  • 会場: 名古屋大学情報科学棟1階 第1講義室(名古屋大学東山キャンパス)
  • 登録URL: https://atnd.org/events/82626

二日目に僕もしゃべります。谷村省吾先生の「物理学者のための圏論入門」というチュートリアルを受けて、圏の事例を追加し、n-圏(高次圏)にも少し触れよう、という心づもりです。

以前、「再考: クリーネスター・圏論・計算の離散力学」などの記事で、人前で話す話題/事例/ストーリーやらをどうしようか? と二転三転の様子も含めた経緯をここに書いたことがあるので、今回もそんな感じにします。

このエントリーはこれで終わりですが、何か書いたらそこへのリンクを以下に追加して、この記事をハブにします。


ここにリンクが追加される予定。

[日常]電話

$
0
0

1986年リリースの「レイニーブルー」(徳永英明)の歌詞は、

  • 電話ボックスの外は雨、かけなれたダイアル回しかけて、ふと指を止める。

もはや「電話ボックス」を見ることはないし、「ダイアル回す」こともないですね。

屋外の電話ボックス以外に、飲食店などに設置されたピンク電話という公衆電話もありました。

*1*2

なんでそんなことを思い出したかというと、さきほど大通り沿いのカフェでコーヒーを飲んでるとき、少しあわてた感じのおじいさんが入ってきて「電話はありませんか?」と店員さんに聞いてきたのです。もちろん返事は「電話はありません」。

昔は僕も、電話ボックスが見あたらないときに喫茶店に入り、ピンク電話を使わせてもらったことがあります。あのおじいさんも、過去のそんな記憶から「電話はありませんか?」と聞いたのでしょう。

ピンク電話だと、104番の電話番号案内サービスが使えないんですよね。でも、なんとかする裏技があったんです。その方法は、飲食店で働いているときに店長から教わりました。

長く生きていれば、公衆電話にまつわる思い出のひとつやふたつはあるでしょう。電話ボックスでイタズラしようとした話は「「バカなこと」は難しい」に書いたことがあります。

彼が彼女に電話をかけるのに、彼女のアパートのすぐそばの電話ボックスまで行って、部屋の窓にうつる彼女の影を見ながら話していた、なんて思い出を聞いたことがあります。切なく微笑ましく感じるのは、固定電話と公衆電話という状況だからでしょう。そう、今ではあり得ない昔話だね。

Viewing all 1207 articles
Browse latest View live