連鎖性言語/名前付けの対象は値ではなくコード

スタック型言語では、プログラムはワードとリテラルの並びです。リテラルは、エバリュエータに遭遇するとスタックにプッシュされます。ワードは、プリミティブかサブルーチンで、サブルーチンはそれ自体がワードとリテラルの並びです。

ワード間で受け渡しされる値 (いわゆる「パラメータ」) に名前はなく、言語によって直接参照されることもありません。値は、必要なだけ表現力を持たせた複雑なものにすることができます。Factor は、豊富なコレクション (配列、ハッシュテーブルなど) をサポートしているほか、名前付きスロットを備えたユーザー定義クラスもサポートしています。ただし、値自体に名前は付けられません。Lisp や Java のような言語と同様、Factor では少なくとも次のものには名前を付けます。

  • ワード (いわゆる関数)
  • クラス
  • スロット (クラスのインスタンス変数)
  • グローバル変数
  • ... およびその他

ただし、ワードのパラメータに名前は付けません。

プログラムはワードとリテラルが並んだものに過ぎず、ワードのリテラルのあらゆる並びは、新しいワードに「まとめる (factor out)」ことができるので、結局は値ではなく、操作に名前を付けることになります。そして、どのような繰り返しでも作ることができ、名前を付けることができます。このことの重要性はいくら強調しても足りないくらいです。適用型言語では、一まとまりのステートメントと式を新しい 1 つの関数にまとめることは時として困難です。そのようなことを行うには最低限、目的の式の中で自由変数を特定し、これらの変数を新しい関数に渡す関数呼び出しでその式を置き換える必要があります。しかし、目的のコードの断片がローカル変数への割り当てを行っている場合には、それが不可能なことがあり、その場合は内容の変化した値を何らかのデータ構造の形で戻す操作が必要になります。すべての新しい Java IDE には、こうしたプロセスを自動化する「抽出メソッド (メソッドの抽出)」コマンドが用意されています。スタック型言語では、「抽出メソッド」操作とは単にカット&ペーストに過ぎません。

スタック型言語でプログラミングすると、ほかのプログラミング言語では気付かなかった多くのパターンが明確に見えてきます。たとえば、ワードとワードのつながり、つまりデータフローが明らかになります。そしてワードとワードのつながりを作ることもできます。

次に示すのは、適用型言語でよくあるパターンを C ライクな疑似コードで記述したものです。

var x = ...;
foo(x);
bar(x);

これがパターンであるとは思わないかもしれません。まして名前付けや抽象化などの対象とは考えないでしょう。Factor には、まさに上の操作をする bi というコンビネータ (高階関数) があります。bi は 1 つの値と 2 つのクォーテーション (無名関数) を取り、 1 つの値に各クォーテーションが適用されます。たとえば、値 x がスタックの一番上にあったとします。

[ foo ] [ bar ] bi

これで、foox に適用され、次に barx に適用されます。

値 (x) に名前を付けるのではなく、データフローのパターン (bi) に名前を付けている点に注目してください。

もうひとつ、データフローのパターンを示しましょう。

var x = ...;
bar(x,foo(x));

Factor では、これを dup と呼びます。値 x がスタックの一番上にあったとします。

dup foo bar

これで、foox に適用され、次に barxfoo の結果に適用されます。

今度は、インスタンス変数へのアクセスとメソッド呼び出しに . インフィックス演算子を使う適用型プログラミング言語で記述した次のようなコードを考えてみましょう。

var customer = ...;
var price = customer.orders[0].price;

Factor では、この種のコードはとても自然に記述されます。スタック上の最初のオブジェクトからスタートし、次にメンバにアクセスし (アクセス子はスタック上の値を 1 つ消費して 1 つ残します)、あとはパスの最後に到達するまで同じ操作を繰り返せばよいからです。したがって、customer オブジェクトがスタックの一番上にあったとすると、上のコードは次のように記述できます。

orders>> first price>>

Factor では、名前の付いた slot>> をアクセス子として使います。ほかのさまざまなスタック型言語では異なったアクセス子が使われることがありますが、いずれも考え方は同じです。

では、上の例をさらに利用してみることにしましょう。たとえば、 中間的なオブジェクト (オーダーのリスト、最初のオーダー、価格、さらに customer オブジェクトも含む) がいずれも null でない場合に、同じ操作を行うとしましょう (Factor では null には f を使います。f はブール値の false で、「値がない」ことを示す正式なマーカーです)。さて、操作を 1 行で簡潔に記述することはもはやできなくなります。

var customer = ...;
var orders = (customer == null ? null : customer.orders);
var order = (orders == null ? null : orders[0]);
var price = (order == null ? null : price);

1 行のコードが 4 行になり、新しいローカル変数を 3 つ使うことになりました。各ローカル変数は 2 回参照されていますが、このコードが実行された後で使われる可能性はまずありません。また、上のコードには繰り返しがあり、[javascript{(foo == null ? null : foo.bar)}] が 3 回出現しますが、これを抽象化することはできません。一般的に不完全なデータ、とりわけ null 値の扱いは、主流言語が抱える共通の問題の 1 つで、それゆえに多くの対応策が考案されています。ここでは、よく見られる次の 3 つ対応策を検討してみましょう。

  • 問題をカーペットの下に隠して「真のスコットランド人」ばりの主張をする。つまり、「真のプログラマは、このような機能を必要としない。なぜなら、そうした機能が必要な場合があるとすれば、それはコードの設計が間違っているからだ」と主張する。
  • 実際の null の代わりに「null オブジェクト」を使う。このアプローチは、さまざまなものを実装して、次のようにすることである。すなわち、customer.orders は決して null にならないが、空のリストになることはある、また、customer.orders[0] は決して null にならないが、空の order オブジェクトになることはあり、さらに空の order オブジェクトの価格は null である。この方法は、Java、C#、および C++ でよく使われます。
  • . インフィックス演算子のような新しい構文を追加する。ただし、演算子の左側が null の場合には、null を返す。Groovy スクリプト言語はこの機能を持っています ("Elvis" 演算子 "?")。

問題は、上の対応策のどれも満足のゆくものではない点です。いずれも、開発者に不必要な作業を強いるか、または非常に限られた特殊な問題を解決するためにその場しのぎの構文を追加して言語を複雑にしてしまいます。

Factor では、この例の場合、when というコンビネータを使うことで次のように記述できます。when は、ステートメントに分岐が 1 つある場合に普通に使われるコンビネータです。

dup [ orders>> ] when dup [ first ] when dup [ price>> ] when

もしこのような記述をしばしば使うなら、新しいコンビネータを定義して dup [ ... ] when の反復をなくすことができます。たとえば、新しく定義するコンビネータを (Haskell の Maybe モナドに似ているので) maybe と呼ぶことにしましょう。

MACRO: maybe ( quots -- ) [ '[ dup _ when ] ] map [ ] join ;

このように定義してやると、上のコードは次のようにすっきりと記述できます。

{ [ orders>> ] [ first ] [ price>> ] } maybe

どうですか。「デザインパターン」などといったものを使う必要もなければ、言語に新しい構文を追加することもなく、また不必要なコードを記述する必要もないことを考えれば、かなり優れた解決策ではないでしょうか。これはスタック型言語の優れた面を示すごく基本的な例ですが、こうした例は決して少なくありません。スタック型言語を使うと、不要な作業をせずに済むのです。

This revision created on Tue, 1 Sep 2009 16:23:03 by Ashberk (Added elvis operator for groovy example)