君のプログラミング言語で、これ、できる?

From The Joel on Software Translation Project

Jump to: navigation, search

Joel Spolsky / 青木靖 訳
2006年8月1日 火曜


ある日、自分のコードを眺めていて、ほとんど同じに見える2つの大きなコードブロックがあるのに気付く。実際、一方が"スパゲッティ"、他方が"チョコレートムース"について言及しているところを別にすれば、どちらもまったく同じだ。

   //簡単な例:
   
   alert("スパゲッティが食べたい!");
   alert("チョコレートムースが食べたい!");

この例ではたまたまJavaScriptを使っているが、この後の話は別にJavaScriptを知らなくとも理解できるはずだ。

同じコードが繰り返し現れるのは、もちろん良くないことだ。だから関数を書くことにしよう:

   function SwedishChef( food )
   {
       alert(food + "が食べたい!");
   }
   
   SwedishChef("スパゲッティ");
   SwedishChef("チョコレートムース");
01BorkBorkBork.PNG

確かにこれは単純な例だが、もっと中身のある場合も想像できるだろう。多くの点でこちらのコードの方が良く、そういった理由については何百万回も聞いていることだろう。保守性、可読性、抽象性 = 良い!

それからまた別な2つのコードブロックがほとんど同じに見えることに気付く。一方はBoomBoomという関数を呼び続けていて、もう一方はPutInPotという関数を呼び続けているところだけが違っている。その他の点ではまったく同じと言っていい。

   alert("ロブスターを取る");
   PutInPot("ロブスター");
   PutInPot("水");
   
   alert("チキンを取る");
   BoomBoom("チキン");
   BoomBoom("ココナツ");

今回は関数の引数として関数を渡す方法が必要になる。これは重要な機能であり、それによって関数の中に隠蔽できる共通なコードを見つけられる可能性が高くなる。

   function Cook( i1, i2, f )
   {
       alert(i1 + "を取る");
       f(i1);
       f(i2);
   }
   
   Cook( "ロブスター", "水", PutInPot );
   Cook( "チキン", "ココナツ", BoomBoom );

ほら見て! 関数を引数で渡している。

君の言語で、これ、できる?

そうだ・・・関数PutInPotやBoomBoomをまだ定義していないものとしてみよう。それをどこかよそで定義するかわりに、インラインで書けたらいいと思わない?

   Cook( "ロブスター", 
         "水", 
         function(x) { alert(x + "を鍋に入れる"); }  );
   Cook( "チキン", 
         "ココナツ", 
         function(x) { alert(x + "を撃つ"); } );

フム、これは便利だ。関数を呼ぶその場で関数を作っていることに注意してほしい。名前を付ける手間さえかけず、ただ耳をつかんで引っ張り出し、関数に放り込んでいるのだ。

引数としての無名関数という考え方でものを見るようになると、配列の各要素に何かしているコードがそこらじゅうにあるのが気になってくる。

   var a = [1,2,3];
   
   for (i=0; i<a.length; i++)
   {
       a[i] = a[i] * 2;
   }
   
   for (i=0; i<a.length; i++)
   {
       alert(a[i]);
   }

配列の各要素に何かするというのはごく一般的なことなので、それをやってくれる関数を書いてみることにしよう。

   function map(fn, a)
   {
       for (i = 0; i < a.length; i++)
       {
           a[i] = fn(a[i]);
       }
   }

そうすると前のコードは以下のように書き直すことができる:

   map( function(x){return x*2;}, a );
   map( alert, a );

配列でよくやるもうひとつのことは、配列の全要素を何らかの仕方で組み合わせるということだ。

   function sum(a)
   {
       var s = 0;
       for (i = 0; i < a.length; i++)
           s += a[i];
       return s;
   }
   
   function join(a)
   {
       var s = "";
       for (i = 0; i < a.length; i++)
           s += a[i];
       return s;
   }
   
   alert(sum([1,2,3]));
   alert(join(["a","b","c"]));

sumjoinはよく似ているので、そのエッセンスを抽出し、配列の要素を組み合わせて1つの値にする汎用的な関数を作りたいと思うかもしれない。

   function reduce(fn, a, init)
   {
       var s = init;
       for (i = 0; i < a.length; i++)
           s = fn( s, a[i] );
       return s;
   }
   
   function sum(a)
   {
       return reduce( function(x, y){ return x + y; }, 
                      a, 0 );
   }
   
   function join(a)
   {
       return reduce( function(x, y){ return x + y; }, 
                      a, "" );
   }

古い言語の多くには、単にこの手のことをやる方法がない。言語によってはやれなくもないが難しい(たとえばCには関数ポインタがあるが、関数をどこか別なところで宣言し、定義する必要がある)。オブジェクト指向言語は関数だけで何かやれるようにすべきだということにあまり納得していないようだ。

Javaの場合、関数をファーストクラスのオブジェクトとして扱いたければ、メソッドを1つ持つファンクターと呼ばれるクラスをまるまる作ってやる必要がある。オブジェクト指向言語の多くではクラスごとにファイルを別にする必要があるということと組み合わせると、状況はあっという間にひどいことになってしまうだろう。あなたのプログラミング言語がファンクターの使用を強要しているのなら、あなたは現代的プログラミング環境の利点を十分享受していないことになる。お金の払い戻しが受けられないか確認してみるといい。

配列をイテレートして各要素に何かするというだけのちっぽけな関数を書くことで得られる利点は、いったい何なのだろう?

ちょっとmap関数に戻ってみよう。配列の各要素に何かする必要があるとき、多くの場合それをどういう順序でやるかは問題とならない。配列を前向きにたどっても後向きにたどっても結果は同じになる。もし2つのCPUが使えるなら、それぞれのCPUに要素を半分ずつ処理させるコードを書くこともでき、mapは突如として2倍高速になる。

あるいは、仮定の話として、世界中のいくつものデータセンタに何十万というサーバを持っていて、それから非常に大きな配列があり、再び仮定としてだが、その配列にはインターネットの全コンテンツが入っているものとしよう。あなたはmap関数を何千というコンピュータを使って実行することができ、それぞれのコンピュータは問題の小さな一部分を解くことになる。

そうすると、たとえばインターネットのコンテンツ全体を高速に検索するコードを書くというのは、基本的な文字列検索関数を引数としてmap関数を呼び出すだけという単純な話になる。

ここで気付いてもらいたい本当に興味深い点は、mapreduceが誰でも使える関数として考えられるようになるとみんなそれを使うようになり、地球規模の大並列コンピュータ上でmapreduceを実行する難しいコードは誰か頭のいい人間に書いてもらえば、今まで単独のループで動かしていた古いコードはそのまま動くが遥かに高速化され、巨大な問題が即座に解けるようになるということだ。

繰り返しておこう。ループという概念を抽象化すれば、ループは好きなように実装できるようになり、ハードウェアを余分に使ってよくスケールするように実装することもできる。

しばらく前に、Javaしか学んでいないコンピュータサイエンスの学生について文句を言ったときに私が書いていたことを、理解してもらえたのではないかと思う。

関数プログラミングを理解していなければ、GoogleをあれほどスケーラブルにしているMapReduceは発明できない。MapとReduceという用語はLispと関数プログラミングから来ている。純関数プログラムは副作用がなく容易に並列化できるということを6.001に相当するプログラミングの授業で聞いて覚えている人には、MapReduceは容易に理解できる。GoogleがMapReduceを発明し、Microsoftが発明しなかったという事実は、Microsoftが基本的な検索機能についてキャッチアップの途上にあり、一方Googleは次なる問題へと進んでいることを示している。Skynet世界最大の並列スーパーコンピュータを構築しているのだ。この流れの中でいかに遅れを取っているかを、Microsoftがちゃんと理解しているとは思わない。

OK。今やあなたもファーストクラスの関数を持つプログラミング言語がより多くの抽象化の機会を与えてくれ、コードをより小さく、堅固で、再利用しやすく、スケーラブルにできるということを納得してもらえたのではないかと思う。Googleの多くのアプリケーションはMapReduceを使っており、誰かがそれを最適化したりバグを直したりするときには、MapReduceを使っているすべての人が恩恵を受けられる。

もっとも生産的なプログラミング環境は、異なる抽象レベルで作業させてくれる環境であると主張しよう。ぼろな古いFORTRANでは関数を書くことさえできなかった。Cは関数ポインタを持つが、それは醜く、無名にできず、使うのとは別な場所で定義する必要がある。Javaはファンクターを使うことを要求し、これはさらに醜い。Steve Yeggeが言っているように、Javaは名詞の王国なのだ。


訂正: 私がFORTRANを最後に使ったのは27年の昔になる。FORTRANには明らかに関数がある。私はたぶんGW-BASICのこと考えていたのだと思う。


(オリジナル: Can Your Programming Language Do This?)

戻る

Personal tools