Hatena::ブログ(Diary)

oct inaodu

 | 

2005-10-04

JavaScriptにおける高階プログラミング



前提知識
JavaScriptを使ったオブジェクト指向プログラミングの知識が必要となります。以下のWebreferenceの記事を読み、よく理解しておいてください。
最終更新
2004/3/28

はじめに

高階プログラミングでは、値として関数を使うことができます。つまり引数として関数を別の関数へ渡すことも、関数を別の関数の返り値にすることもできるのです。この形式のプログラミングは、しばしば関数プログラミングで使用されますが、「通常」のオブジェクト指向プログラミングでも非常に有用です。スクリプト言語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の生成

引数として関数を利用する

もし以前に配列から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はコンビネータ

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

 |