シュワルツ変換とRuby
ソートのときに比較関数が何らかの計算を含む場合、計算に重複が出て処理性能が落ちる。これを避けるために、あらかじめ計算結果と一緒に連想配列のような入れ物を別途用意して並べ直すのが一般的なようだ。1994年にPerlハッカーのランダル・シュワルツがPerlイディオムとして書いたことから広く「シュワルツ変換」と呼ばれている、とWikipediaにある。Lispではもともとasoccを使ったりして同様のことをやっていたので、Perlのイディオムに付いた名前が一般化したということらしい。
ちょっと検索すると、いろんな言語でのシュワルツ変換が出てくるけど、Ruby では何と組み込みだった。Enumerable#sortには、以下の解説が。
As of Ruby 1.8, the method Enumerable#sort_by implements a built-in Schwartzian Transform, useful when key computation or comparison is expensive..
sort_byがシュワルツ変換入りのソート。ただし、いつも使うべきというわけじゃない。enum#sort_by の説明を見ると、sort_byでかえって遅くなる例と高速になる例が出ている。現在のRubyの実装では、sort_byに単純なブロックを渡した場合、ソート対象として一時的に作るタプル(ってRubyじゃ聞かない用語けど、immutableなarrayってことか?)を生成するコストのほうが高くなって、実は遅くなってしまう。でも、ファイルをタイムスタンプ順に並べ替えるようなときにはファイルI/Oを抑えることになるので、高速になるという。なるほど。
array.cには、Array#sott!の定義として、次のような箇所がある。
1756 rb_ary_sort_bang(VALUE ary) 1757 { 1758 rb_ary_modify(ary); 1759 assert(!ARY_SHARED_P(ary)); 1760 if (RARRAY_LEN(ary) > 1) { 1761 VALUE tmp = ary_make_substitution(ary); /* only ary refers tmp */ 1762 struct ary_sort_data data; 1763 1764 RBASIC(tmp)->klass = 0; 1765 data.ary = tmp; 1766 data.opt_methods = 0; 1767 data.opt_inited = 0; 1768 ruby_qsort(RARRAY_PTR(tmp), RARRAY_LEN(tmp), sizeof(VALUE), 1769 rb_block_given_p()?sort_1:sort_2, &data);
1761行目でtmpにオリジナルのaryをコピーしたArrayオブジェクトを作り、これを並べ替えている。このとき、ary_sort_dataという構造体を使ってるけど、ドキュメントにあるタプルというのはこれのことじゃないかという気がする。
1670 struct ary_sort_data { 1671 VALUE ary; 1672 int opt_methods; 1673 int opt_inited; 1674 };
んー、あれ。単にintが2つあるだけか、、、。しかも単純にオプションスイッチ? 分からん。
ともあれ、Rubyでは、シュワルツ変換という言葉や概念を知らなくても、sortで凝ったブロックを渡すときにはsort_byを使うと覚えておけば、それほどひどいことにならない。やっぱりRubyって素敵だなと思った。