まつもとゆきひろさんの「エッセイのごときプログラム」を読みながら考えた
オライリーの「ビューティフルコード」がずっと自宅の本棚にある。
ビューティフルコード (THEORY/IN/PRACTICE)
- 作者: Brian Kernighan,Jon Bentley,まつもとゆきひろ,Andy Oram,Greg Wilson,久野禎子,久野靖
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/04/23
- メディア: 大型本
- 購入: 30人 クリック: 617回
- この商品を含むブログ (190件) を見る
文中に、例としてJavaと比較されているRubyのコードがある。JavaよりRubyのほうが簡潔に書けるという例なのだが、ふつうに書いた場合の例ではあるが、十分簡潔に書いた例であるようには思えなかった。次のような例だ。(例29-1)
(a)Javaによるもの
class Sample { public static int fact(int n) { if (n == 1) return 1; return n * fact(n - 1); } }
(b)Rubyによるもの
def fact(n) if n == 1 1 else n * fact(n - 1) end end
(c)Rubyによるもの(Ruby 1.9による簡潔な表現)
def fact(n) (1..n).inject(:*) end
(a)や(b)と比べると、どちらも(c)のほうが簡潔に書かれているのはわかる。
しかし、(a)と(b)がそんなに大きく違うだろうか。(a)のJavaでは class Sample { ... } という枠が書いてあるが、これは比較にあたっては本質的には無視しても差し支えないのではないだろうか。
さて、ここで思ったのが三項演算子を使えばもう少し簡潔に書けるだろうにということだった。
JavaにもRubyにも、三項演算子(またの名を条件演算子)というのがある。
これは、a ? b ? c のような式で、条件 a が成り立つときは b、そうでなければ c という値をとる式だ。
この三項演算子を使うと、Javaの(a)とRubyの(b)はどちらももっと簡潔に書ける。
(e)Rubyによる簡潔な表現(三項演算子による)
def fact(n) (n == 1) ? 1 : n * fact(n - 1) end
Ruby(c)のinjectを用いた表現は、1からnまでの数列のあいだに掛け算の演算子 * をはさみこんで計算するというアルゴリズムだ。
この計算方法は、injectを使わずに書けば、JavaでもRubyでも同じように書ける。
(f)Javaによる簡潔な表現(inject方式のアルゴリズム)
public static int fact(int n) { int ans = 1; for (int i = 1; i <= n; ++i) {ans *= i;} return ans; }
(g)Rubyによる簡潔な表現(inject方式のアルゴリズム)[別法]
def fact(n) ans = 1 (1..n).each {|i| ans *= i} ans end
たしかに(c)のほうが(g)や(f)よりは簡潔ではある。(f)と(g)でも(g)のほうが簡潔だろう。
とはいえ、そう大きくは違わないようにも見える。
もっと劇的な例はなかったのだろうか?
そう思って読み進めると、次のような例が出てきた。
例29-2 CとRubyでの抽象度の高さの比較。
(a)Cによるもの
int ary_indext(int n, int *ary, int len) { int i; for (i=0; i
関数名が ary_indext になっているが、ary_index の間違いじゃないかと思う。たぶん。
ptrが突然出現しているがこれは ary の間違いじゃないかと思う。たぶん。
参照しているのが初版第1刷だから誤記もままあるということなんだろうと思う。たぶん。
(b)Rubyによるもの
ary.find_index{|x| n == x}
たしかに簡潔だ。
でもちょっとひっかかる。
find_indexというメソッドが定義済のArrayクラスがライブラリとして組み込まれているのはRubyの長所だろう。
しかし、Cで開発するのならライブラリ関数をまず開発するだろう。たとえば次のようなことはできる。
(c)Cのライブラリ関数を定義して使うための準備(ary.h)
#if defined(__ary_h__) #define __ary_h__ #include/* ary_index を定義する。 */ int ary_index(int n, int * ary, int len, int (*compare)(int, int)); /* n == x の真偽を返す。 */ int equals(int n, int x); /* 配列長を補うマクロ */ #define ARY_INDEX(n, ary, exp) ary_index(n, ary, sizeof(ary)/sizeof(int), exp) #endif /* defined(__ary_h__) */
(d)Cのライブラリ関数を定義して使うための準備(ary.c)
#include/* ary_index を定義する。 */ int ary_index(int n, int * ary, int len, int (*compare)(int, int)) { int i; for (i=0; i
Cでは配列はオブジェクトではない。だから、配列長はsizeof演算子を使って計算するか、別途計算しておいて渡すしかない。しかしそれもint型配列(int型へのポインタではなく)を使うことを前提としてよければマクロで回避できる。(この前提はRubyでArrayオブジェクトを前提とするのと同列ではないだろうか。)
また、コード中に無名の関数を定義する機能はCにはない。だから、Cの場合には、あらかじめ n == x を表現する関数も定義しておく必要もある。
そんなわけでたしかにCではたくさんの準備が必要になるし、制約も多い。
それでも、準備はいちどだけすればよい。そうすれば何回でも使える。ary_indexがもし数多く利用される関数なら、その準備は十分引き合うだろう。
ARY_INDEXを使うところだけ見れば、Ruby版と比べても遜色はないかもしれない。たぶん。
(e)Cでライブラリ関数を使う
#includeint main(int argc, char * argv) { int ary = {5,4,3,2,1}; int n = 2; return ARY_INDEX(n, ary, equals); /* 終了ステータスは 3 になるはず。 */ }
ここでは、もとのC版にしたがって ARY_INDEX(n, ary, equals) の順で引数を指定するようにしてあるが、ほんとうは n を最後の引数にするほうがいいだろう。 ARY_INDEX は ary のための関数であり、その関数の振る舞いを決める(汎関数を関数に変える)ためのパラメータが equals であり、 n はもっとも個別的で具体的な関数の最終形態を決めるためのパラメータだからだ。もっとも、ここではそれは横道だ。気にしなくてもいいか。
けっきょく、C言語でも工夫しだいでRubyに近い表現をある程度までまねることはできる。ただし、そのためには準備が必要だし、受け入れなくてはならない制約も多い。それには同意できる。だからRubyは優れているのだ、と主張してもらうのは、どちらかといえば歓迎だ。進歩は喜ばしい。だからこそ、比較するときの例をもっと劇的に、もっと説得力の大きなものにしてもらいたかったなと思うのだった。