ツリー変換に向いたプログラミング・パラダイムは?

前の2つの記事(「GCの変換は、更新ではなく、再構成です。」と「PC→SP変換を再構築方式にする条件」)において坂田さんは、ツリー処理に関して「更新」と「再構築」という2つの方式を紹介すると共に、次のように述べています。

複雑なページや大量のページが対象である場合、工期やコスト・品質において、再構築方式に大きなアドバンテージがあります。


ジーンコードが再構築方式であるのは、開発が非常にし易く、自社開発の方法として適していることが挙げられます。

この記事では、「更新」(update)と「再構築」(reconstruction)に関して、少し一般的な文脈 — プログラミングのパラダイムという観点から補足してみることにします。

プログラミングの2つの流儀

プログラミングのパラダイム(考え方、方法論、価値観、流儀)には色々な分類があり得ますが、ここでは次の二種の流儀に分類しましょう。

  1. 状態と更新よるプログラミング
  2. 値と適用によるプログラミング

まずは簡単な例で説明します。「状態と更新によるプログラミング」の例としてカウンター変数とそれをインクリメントする関数(手続きと呼ぶのがふさわしいですが)をJavaScriptで示します。

var count = 0;

function inc() {
  count++;
}

とても簡単なプログラマですから説明は要らないでしょう。カウンター変数countが状態を保持して、inc関数がその状態を更新します。

それに対して、整数値の後者(successor)を求める関数は次のようです。

function succ(n) {
  return (n + 1);
}

こちらも1を加える操作ですが、引数(入力)から戻り値(出力)を、計算により作り出すだけで、環境や状態への影響は一切ありません。

複数の処理単位(関数や手続きと呼ばれます)から共通にアクセス可能な状態を、時々刻々と上書き変更しながら処理を進める方式が「状態と更新よるプログラミング」です。一方、各処理単位は入力から出力を計算するだけの働きを持ち、それらを組み合わせてより大きな処理を構成する方式が「値と適用によるプログラミング」です。
なお、ここでの「適用」(apply)とは、「関数に計算をさせる」という程度の意味です。

破壊的な変更と値データの生成

「1を加える操作」ではあまりに簡単ですから、別な例を:

var theList = [];

function append(data) {
  theList.push(data);
}
function putLast(list, data) {
  var result = [];
  for (var i = 0; i < list.length; i++) {
    result.push(list[i]);
  }
  result.push(data);
  return result;
}

appendもputLastも、どちらもリストの最後にデータを追加する関数です。appendはtheListという状態を直接的に更新します。一方のputLastは新しく作ったリスト(配列)を返します。引数にもらったリストに変更を加えないように、わざわざ戻り値用のデータを新しく作り出しています。(注意:厳密にやるなら、putLast内で配列の項目をディープコピーする必要があります。)

putLastは「なんだか面倒なことをしてるな」と感じたかもしれませんが、その見返りに次のような書き方が自然に(あるいは期待したとおりに)解釈できます。

var x = [1, 2, 3];
var y = putLast(x, 4);

xの値は[1, 2, 3]のままで、yの値は[1, 2, 3, 4]です。次のようなputLastを作ってしまうと、そうはいきません。

function putLast(list, data) {
  list.push(data);
  return list;
}

var y = putLast(x, 4); により引数のxの値も変わってしまうのです。このような現象は、プログラマを混乱させます。
多くのバグが、このような“期待してない副作用”に起因することが知られています。

inc関数やappend関数のような処理は、破壊的変更と呼ばれることがあります。処理の前の状態は壊れて失われてしまうからです。一方、succ関数やputLast関数(最初に定義したほう)は、引数やその他の何かを壊したりはしません。計算結果は、必ず新しいデータを生成してそれを返します。

関数型言語の発想

プログラミング言語の処理単位を「関数」と呼ぶことが多いですが、本来の関数(数学的な関数)は、引数から戻り値を計算するだけで、環境に影響されることもなく、環境を変更することもしません。このような数学的な関数を特に純粋関数(pure function)と呼ぶこともあります。

純粋関数をベースとしたプログラミング言語を関数型言語と呼びます。最近だとHaskellやML、オブジェクト指向と融合したScalaなどでしょうが、関数型言語の歴史は非常に古く、(オリジナルの)Lispは半世紀以上も前に設計実装されています。

関数型言語には、いわゆる副作用がないので、プログラムを明確に書けてバグを減らせる効果があります。利用者数においては関数型言語はメジャーとは言えませんが、常に熱心なファンがい続けたし、最近(数年のスパンで)また関数型言語への注目が高まっているように思えます。

冒頭の話に戻って、ツリー変換における再構築方式は、関数型言語の発想にかなり近いものです。XMLのツリー変換を目的に設計されたXSLTなども関数型言語のフレーバーを持ちます。それに対して、生のDOM APIを使う方法は、状態と更新よるプログラミングスタイルになります。

関数型、つまり値と適用によるプログラミングと、状態と更新よるプログラミングのどちらが良いかは目的や状況次第ですが、変換処理となると、入力データから出力を生成していくやり方が向いているでしょう。このやり方は、非破壊的なものでありバグの混入が少ないことが期待できます。

結び

GC(ジーンコード)の設計時に、特に関数型の流儀を意識したことはなかったと思います。しかし、開発のしやすさや品質への考慮から、結果的に「再構築」と呼んでいる今の方式になったわけです。これは、ツリー変換には非破壊的な方式が向いていることの情況証拠と言えるでしょう。

Page Top