後置型プログラム言語と記号論と生成文法の思考実験

ふと、ノーム・チョムスキーの生成文法とウンベルト・エーコの記号論を思い出したので、後置型プログラム言語を再考してみる。

たいていのプログラム言語は前置定義型だ

「全ての」と書きたいところなのだが、C# の拡張メソッドや Ruby の後付けメソッド、Javascript のオブジェクトへのメソッドの追加を考えると、後置型とも言えなくもないので「たいていの」にしておく。要は、あらかじめ定義したものを使う、というスタイルでプログラムコードを書いていくのが「前置定義型」だ。

class class A {
public:
  void method() { ... }
};
void main() {
 A a ;
 a.method();
}

な感じでクラスやメソッドを定義する。

記述場所は、main 関数の後からもしれないし、別ファイルかもしれない。ただし「A」として使う時点で、A クラスは定義してあるという前提になる。これは一見、当たり前のように見えるけど、あらかじめ定義されているクラス A がアプリオリであることが前提になる。コードとして記述する、あるいはプログラムが実行される時点で、A から生成される a は、何も A に既定されなければいけない、という法はない。法を作った上で、A を使って a を生成しているというルール的な意味になる。

ただし、コンパイラや補完機能の絡みからすれば、これは便利だ。A から a を生成した時点で、a の型は既定されているので、method というメソッドを持っていることが分かる。あるいは、別のクラス B を使って b を生成したときに、a != b が明確になる。事前に決まっているからこそ曖昧さがなくなり、型推論が可能だし厳密さを保ったところでコードを書けるというメリットを享受できる。

となれば、仮定として「プログラムコードとしての安全性を取り払ったどうなるのか?」という疑問を呈してみよう。厳密に動かすためのプログラムなのに、厳密さを取り除くという考え自体が矛盾しているような感じがするか、思考実験としては妥当だろう。

自然言語的に定義を補完する

例えば、A x としたときに、x の型は決まらないし、A というクラスの内容は定義しない。ただ、A というクラスができて、x というモノ(オブジェクト)ができるという意味だけを取り出してみる。

x is a A;

ってことだ。いわゆるオブジェクト指向の is a だから、A というクラスあるいはカテゴリというものがあることが分かる。かつ。A であるものと A でないものを分離するという役割を担う。しかし、ただそれだけの意味しか持たない。

これをいきなり、A というクラスは func というメソッドを持つということを考えてみよう。その方法は2つ考えられる。

  1. class A { func() { … } };
  2. x.func( … );

ひとつは、1 のようにクラス定義をして func メソッドがあることを知らせる。x は A クラスなので、x が A クラスであるという三段論法から x は func というメソッドを持つことが分かる。これは普通のプログラミングのスタイルだ。

もうひとつ、2 のようにいきなり、func というメソッドを使う方法がある。通常は A クラスは定義されていないのだから、func というメソッドを持つかどうかわからないのだが、いやいや、それ狭いコードの範囲しかみていなくて、コード制作者(コード自身の外の世界)の意図としては、すでに A には func というメソッドがあることを知っているので、コードだけの世界にいきなり func を持ってきても不思議ではない、というスタイルだ。つまりは、コードの外部にあるコード制作者が常に正しとしてコードを書くというスタイルをとる。

しかし、実行時には、func というメソッドがコードという世界で定義されていたら動きようがないので、それは「何事もなく動く」のでもよいし「実行時例外を発生する」方法でもよい。コードの厳密性を探るのであれば「実行時例外」が正しいように見えるが、コード制作者が常に正しいという視点に立ては「何事もなく動」いてもいいような気がする。ただし、コードを実行する世界では、func メソッドの動作は定義されていないので、「何事もなく動く」ということはイコール「何も動かない」ということに等しい。この何も動かないということは例外さえも動かないということだ。

A a ;
a.func();

そう、後置定義型を許せば、上の2行はそのまま動く≒何も動かない、ということになる。

コード外のアプリオリを追加する

もう少し具体的に考えみよう。なんとく変数 a に名前をつけておいて、プリントアウトすることを考える。

A a("masuda");
a.print();

これだけで十分だ。プリントアウトするという意味っぽい print というメソッドを使うだけ、名前を付けるという意味で “masuda” を引数として定義するだけで完成する。本当のとこは、名前付けがコンストラクタの指定なのか、(あるかもしれない)Name プロパティへの指定なのかはどうでもいいことだ。コードを書いた人は、コードの世界よりもすべてを知っているので、コードが動作する枠を超えていても構わない。プリントアウトするという print メソッドは習慣的にプリントアウトするという動作を定義するだけで、それが画面なのか紙なのか3Dディスプレイなのか、それとも他のプロセスへの通知をプリントアウトするのかはわからない。脳内にプリントアウトしたってかまわないはずだ。それらの動きを既定するのはコードの世界なので、それはコード内の世界の「アプリオリな」ライブラリに寄るという答えになる。

ここで、コードを書く人とコードを動かす人(実行環境)とのずれが発生する。一般的なコミュニケーションの伝達の問題であって、プログラムコードではそれらを「厳密さ」で補おうとするけど、人とのコミュニケーションであればそれらの厳密さあまりあてにならない。文化の違いもあれば育ちもあれば宗教的な違いや政治的な違いがある。勿論、学問としてひとつのルール(閉鎖的なルール)で議論することも可能なのだが、日常生活においてはあまり厳密に伝えることは少ない。例えば、居酒屋にて「とりあえずビール」と言ったときに、ビンなのか生なのかピッチャーなのかは問わない場合もあれば問う場合もあれば、盆に注がれたりどんぶりだったり氷入りだったりコーラを一緒に持って来たりといパターンも考えられるけど、まあ大体において「とりあえずビール」で通じることが多い。常連であればなおさらだ。

ならば、コード制作者がコードに対して、いちいち厳密に説明することはすくなくて「とりあえずビール」感覚で「とりあえずプリントアウト」でもいいですよね、って感覚でコードを書くこともできるのではないだろうか。実際、Outlook でメールを印刷しようとすると「とりあえずメモ形式で印刷しますよ」みたいな感じになる。

じゃあ、プリントアウトする用紙を A4横にしたいときは、こんな感じで補完する。

A a("masuda");
a.print()
print() is A4横.

いきなり自然文法になってしまうが、 こんな感じで後付け書く。「プリントアウトしておいて、ええと、A4横でね」みたいな感覚だ。英語文法/文章法でいうところの、結論を先に書いて後から詳細を足す感覚で書けばいい。普段は「A4縦」で印刷するのだから、普通のデフォルトは print だけでいいのだが、ちょっと変わったことをしたいときは後から追加する。

A a("masuda");
a.paper = "A4横"
a.print()

普通のプログラム言語であれば、こんな風に print の前に書く。なぜかというと、print する時点でプリントアウト自体が実行されてしまうので、後から「ちょっと待った」ができないからだ。 インタープリタだったら、逐一実行するためにこう書かないと駄目なのだ。あらかじめ「これは A4横 で印刷するんだぞ」と初心者的にオーダーするわけだ。だが、コンパイラ言語であって、コードを解析するときとコードを実行するときでは時間的なずれがあるのだから、ほどよく プリントアウトに対する設定変更が終わったころにおもむろにプリントアウトを実行する熟練者がいても不思議ではないはずだ。

また、print 自体の動作も、印刷する先は実行時に決まってもよい。実際、コードを動かして Windows で動かすのか、Linux で動かすのか、何かの組み込みデバイスで動かすのか(小さな液晶に出すのか)という違いが出てくる。それでも、print というコード制作者の意図が達成されれば、それでよいし、逆に実行する環境で print という実行メソッドがなければ「何もしない」ことを実行すればよい。そのあたりの、コード外の実行環境は、コード外のアプリオリとして存在するのであって、コード制作者は未来のコード実行環境を想像しながら作るしかない。逆に、想像するしかないから大ざっぱにプリントアウトするという動作だけ決めてしまう。

記号論と生成文法を加味する

厳密に生成文法を使うかどうかは別として、記号論/現象論的な、シニフィエ/シニフィアンの問題を取り扱ってみる。実際の print の動作は、実行環境のアプリオリなライブラリ(ひらたく言えば既存のライブラリ)により実行されるが、先のコードに、コード制作者の意図を加えてもよいだろう。

A a("masuda");
a.print()

print() is "A4横"
print() {
	name <- name.upper()
}

こんな風に、プリントアウトするときに name プロパティを大文字にすることを考えてみよう。print メソッドの中身は print メソッドを使う時点ではなく、実行時に決まるので、コードの記述上 print メソッドの中身はいつでも変えられる。この場合は、”masuda” が指定されたら “MASUDA” のように大文字に変換すること示している(当然、upper 自体が「大文字にする」という動作がになっていれば、という限定がある)。

つまり、print というメソッドは、記号としての意味しかなくて、それはコード制作時と実行時の意味をつなげるという意味しか持たない。解釈する順序は、現実的な時間制約として、

  1. アプリオリな print メソッドが存在する
  2. コード制作者が print メソッドを使う
  3. 場合によっては、コード制作者が print メソッドを補足する
  4. 実行時に print メソッドが実行される。

の順序しかない。場合によっては、1 のアプリオリな print メソッドのライブラリがない場合もある。コード制作者の頭にしかない print メソッドを、なんとなくコード制作者が 3 で補足して、コードを実行する段階において(コード制作者とは異なる) print メソッドが実行される。コード制作者の「print」と、コード実行時の「print」とは確実に異なる。異なるので「同じ動作をする」という保証はない。保証はないが「だいたい同じ動作をする」という現実的な解を持つという訳だ。

このコードには「厳密さ」はないし、厳密に書くことは不可能だ。コード制作者は、あくまで過去においてコードを制作するので、未来において実行される実行コードを制御はできない。大体、想像して作るだけだし、そこにはとある「約束」や「ルール」があるだけだ。

そういう緩い結合で「後置型プログラム言語」を実装できないかな、と思考実験してみる。文法的に英語の自然言語っぽい形で書ければ(しかし、決して自然言語ではない)なんとなく動くというスタイル。

カテゴリー: 雑談 パーマリンク