高階プログラミングでは、値として関数を使うことができます。つまり引数として関数を別の関数へ渡すことも、関数を別の関数の返り値にすることもできるのです。この形式のプログラミングは、しばしば関数型プログラミングで使用されますが、「通常」のオブジェクト指向のプログラミングでも非常に有用です。スクリプト言語のRubyは、このよい例です。Rubyは、純粋なオブジェクト指向プログラミングと高階プログラミングのすべての長所を兼ね備えています。しかし残念ながら、ブラウザの上で動作しないためウェブページでRubyを利用することはできません。幸い、JavaScriptはどんなブラウザでも利用できます。またJavaScriptはとても柔軟なため、ウェブページ内のスクリプトにおける有用なツールである高階プログラミングへと拡げることができるのです。
ほとんどの人はJavaScriptを、画像を切り替えたり、邪魔なポップアップウィンドウを表示するスクリプト言語としてしか知らないでしょう。しかしJavaScriptの実装は、配列のソートメソッドを通じて、より高度なプログラミングの可能性を示しています。sort()という、引数のない単純な形式では、ただ配列をソートするだけです。
document.write([2,3,1,4].sort())
このコードは、"1,2,3,4"を出力します。しかしsortメソッドは、オプションの引数として比較関数をとることで、単純な並べ替え以上のことを行なえます。これこそ正に高階プログラミングなのです。例えば、オブジェクトの配列があるとします。各オブジェクトにはdateプロパティがあり、その日付の値でオブジェクトをソートしよう思います。
arrayOfObjects.sort(
function (x,y) {
return x.date-y.date;
}
);
比較関数は、ソート中に定期的に呼び出されます。xがyより小さい場合、比較関数は負の値を返します。そして、xとy同じ場合ゼロを返し、xがyより大きい場合は正の値を返します。数と日付を比べる場合、これは引き算を行うことと同じです。文字列の場合は、比較演算子の < と > が使えます。
もし以前に配列からHTMLを生成したことがあれば、このコードは見覚えがあると思います。
var s='';
for (var i=0;i<arr.length;i++) {
var item=arr[i];
s+=...generate some HTML...
}
document.write(s);
これからコードを読みやすくし、モジュール化を促進するreduceメソッドを作りましょう。先程のコードは以下のようになります。
function prettyTemplate(item) {
return ...generate some HTML...
}
document.write(arr.reduce(prettyTemplate));
reduceメソッドを作るために、最初のサンプルのコードの大部分を使い回せます。配列のプロトタイプを拡張し、すべての配列がreduceメソッドを使えるようにしましょう。
Array.prototype.reduce=function(templateFunction) {
var l=this.length;
var s='';
for (var i=0;i<l;i++) s+=templateFunction(this[i]);
return s;
}
しばしばテンプレート関数は、配列の各アイテムを1つのHTMLエレメントで囲むだけという程度の簡単なものになります。例えば、HTMLのテーブルを生成する場合、テンプレート関数は以下のコードだけです。
return '<TD>' + item + '</TD>';
それでは、このシンプルなエレメントラッパーを生成する関数を作ってみましょう。
function wrap(tag) {
var stag='<'+tag+'>';
var etag='</'+tag.replace(/s.*/,'')+'>';
return function(x) {
return stag+x+etag;
}
}
// サンプル:
document.write(wrap('B')('This is bold.'));
var B=wrap('B');
document.write(B('This is bold too.'));
document.write(
'<TABLE><TR>'+
arr.reduce(wrap('TD class="small"'))+
'</TR></TABLE>'
);
最初のサンプルで、返された関数がすぐに呼び出せることを確認できるでしょう。これは、2つの引数リストが連続するような少し変わった構文として現れています。ここではもう一つ特別なことが起こっています。送り返された関数は、stag変数とetag変数を参照しています。これがwrap関数の外でも正常に動作するのは、JavaScriptの関数がクロージャに似た振る舞いを持つためです。関数を定義したタイミングで、クロージャはカレントスコープのポインタを保存します。そして、関数が呼び出された時に、このスコープが復元されるのです。
先程の最後のサンプルで、配列の各要素を<TD>エレメントで囲みテーブルに変換しました。しかしテーブルのレイアウトを水平ではなく垂直にしたい場合は、<TR>エレメントと<TD>エレメントの両方で各アイテムをラップする必要があります。そこで、また新しい関数を作りましょう。
var TABLE=wrap('TABLE');
var TR=wrap('TR');
var TD=wrap('TD');
document.write(TABLE(arr.reduce(
function (item) {
return TR(TD(item));
}
)));
TR(TD(item))は、「TR o TD」*1と書かれた関数合成のようなもので、「TR after TD」を表しています。そこで、以下のように書きたいと思います。
document.write(TABLE(arr.reduce(TR.after(TD))));
JavaScriptでは関数はオブジェクトでもあるため、メソッドを持つこともできます。関数のプロトタイプを拡張することで、すべての関数がafterメソッドを利用できるようになります。
Function.prototype.after=function(g) {
var f=this;
return function(x) {
return f(g(x));
}
}
更新:Dan Shappirに、これがイベントハンドラとコールバック関数にも役に立つとの指摘を貰いました。詳細は、こちらを参照して下さい。
オブジェクト指向言語を使って高階プログラミングをする際、引数としてメソッドを渡したいと思うかもしれません。しかし、実は少し問題があります。ここで、wrapメソッドを使って、elementクラスを作ってみましょう。
function element(tag) {
this.stag='<'+tag+'>';
this.etag='</'+tag.replace(/s.*/,'')+'>';
this.wrap=function(x) {
return this.stag+x+this.etag;
}
}
P=new element('P');
// これは正常動作:
document.write(P.wrap('This is a paragraph.'));
// これは失敗:
document.write(arr.reduce(P.wrap));
何故、このコードは失敗するのでしょうか?これは、P.wrapをreduce関数に渡す時、関数だけが渡されてしまい、そこでthisが違う意味をもつためです。しかしメソッドを上手く動かす、ちょっとしたこつがあります。
function element(tag) {
this.stag='<'+tag+'>';
this.etag='</'+tag.replace(/s.*/,'')+'>';
var me=this;
this.wrap=function(x) {
return me.stag+x+me.etag;
}
}
P=new element('P');
// 引き続き、正常動作:
document.write(P.wrap('This is a paragraph.'));
// 今度は、こちらも正常動作:
document.write(arr.reduce(P.wrap));
このコードは変わっていないように見えるかもしれません。しかし、thisがすべての関数において特別の意味を持つのに対して、meは単なる変数です。JavaScriptは、wrapメソッドがどこで使われるかに関係なく、meが指し示すものをwrapメソッドが知っているかを確かめます。
一部を除いてほぼ同じコードをもつ2つ(または複数)の関数があれば、これらの関数を1つにまとめてしまおうと思うでしょう。この場合、その異なる部分を、別々の関数への関数呼出しに置き換えてください。個別に分けられた関数は、一般化された新しい関数に引数として渡されます。
forループを書くのに飽きたとき(私がそうだったように)、reduceのようなメソッドはプログラミングスタイルの喜ばしい変化となることでしょう。
高階プログラミングにより、私はJavaScriptがずっと面白くなりました。皆さんも是非、高階プログラミングを楽しんでください!
*1:oはコンビネータ