ruby 1.9 を日常的に使うぼくが 1.9 の新機能を寸評する

なんか偉そうな見出しですが、ruby 1.9 を主に使うようになって 1 年ちょっと経ったので、1.9 の新機能に思うところや注意点などを書き残そうと思うのです。さらに 1 年後に見たとき、「あのころはあんなふうに考えてたなあ」などと感慨にひたる予定です。
あらかじめ断っておくと、ぼくの ruby 1.9 経験はすべて趣味範囲なので、エンタープライズとかシステム運用の問題とかは知りません。あとぼくは ruby のコミッタなので、色眼鏡もあると思います。あしからず。

YARV

VM 実行になったという話。一般的には「速い」という文脈で語られます。1.8 と比べると確かに速いです。でも、1.9 ばかり使い出すとなんとも思わなくなるはずです。速さなんて相対的な価値ですから、当然ですけどね。好意的に考えれば、「なんとも思わない程度に、遅くて困ることが減った」のかもしれない。
コンパイルフェーズを挟むため、eval や require が遅くなったことには注意が必要です。とくに、「いっぱい require してすぐに終わるプログラム」は割りを食います (例: gem や rake のようなコマンド、生の cgi など) *1
まあ、速さなんて飾りですよね *2 。個人的には、1.8 のなんかごちゃごちゃしたソースコードが、それなりに教科書的な、整理されたソースコードになったことの方が嬉しい *3

M17N

なぜか「幸せ Ruby ライフをじゃまする面倒な奴」という先入観があるけど、実際に困ったことはあまりないです。

  • Q. magic comment の見た目がダサすぎ。
  • A. ぼくの主観では、ダサいのは magic comment 自体ではなく emacs スタイル 。主に -*- キラッ -*- の部分。Ruby としては # coding: UTF-8 だけでいいので、慣れればぎりぎり許容範囲です。
  • Q. magic comment を書くこと自体が面倒くさい。
  • A. 意外にも慣れます *4 。あと、コメントやリテラルに日本語を書かなければ不要なので、ワンライナーなんかの邪魔にはならない。ちなみに、ライブラリのソースではむしろ書くべきでないらしい *5
  • Q. 文字列処理が遅くなるという噂。
  • A. 実際に困ったことはないです。何度か「M17N がボトルネックじゃないか」と疑ったことがあるけど、今のところ全部濡れ衣だった。
  • Q. エンコードが異なる文字列を連結・比較すると例外になるんだっけ?
  • A. 今のところ UTF-8EUC-JP とバイナリ (ASCII-8BIT) しか使ってないけど、妙な例外に遭遇したことはない。ASCII-8BIT や US-ASCII はよきに計らってくれるみたい。
  • Q. 仕様がなんかよくわからない。
  • A. これはぼくもわからない。けど、Ruby の仕様なんて、わかってる気になってもわかってないことばっかですよ。

やっぱり、"あいうえお"[0] #=> "あ" は便利だし、"あいうえお".encode("EUC-JP", "UTF-8") も便利ですね。
ただ、異国で書かれたコードが M17N のせいで自分の環境で動かないとか、M17N に対応できてない古いライブラリではまるとかは今後も発生するかも。

Fiber

Coroutine や SemiCoroutine より名前がかっこいい。でも本気で使ったことはない。
Fiber と言えばゲームの状態管理だけど、1.9 の Fiber は 1.8 のスレッドと同じ仕組みで動いている (= 遅い) ので、あんまり向いてなさそう。外部イテレータ化には Enumerator#next があるので Fiber を使わなくてよい。なので、普通の人が Fiber を直接使うことは事例はあまり想像できない *6
Fiber のスタック長の制限がきついのは注意が必要かも *7 。でも Fiber が 1.8 のスレッドより軽量なのはこの制限のおかげなので、むずかしいところ。
ちなみに、知る人ぞ知る Fiber::Core は黒魔術なので使ってはいけない (ruby-dev:31601) 。使うなよ、絶対使うなよ!

Enumerator (遅延配列的なもの)

1.9 最大の便利機能。イテレータっぽいメソッドをブロックなしで呼び出すと Enumerator を返す」という convention (慣習、しきたり) が導入された。

a = []
[1, 2, 3].each_cons(2) do |x, y|
  a << [x, y]
end
a.map {|x, y| ... }...

a = [1, 2, 3].each_cons(2).map {|x, y| ... } ...

などと書ける。素直にメソッドチェインがつなげられるので、とってもうれしい。

一年くらい前に、このしきたりの一貫性のなさや非互換性に対して文句たれまくったんですが、多くの場合ではとても便利なので使いまくっています *8 。こういう設計のバランス感覚は matz magic ですね *9

あと、「Enumerator は Fiber で実装されている (ゆえに遅い)」と言う噂は、次に挙げる Enumerator#next だけの話。遅延配列は別に特に遅くはないので、じゃんじゃん使いましょう。

Enumerator#next (外部イテレータ)

Enumerator を each するのではなく、yield される値を一つずつ取り出せる。

g = [1, 2, 3].each
p g.next  #=> 1
p g.next  #=> 2

最初に見たときは使い手がありそうだと思ったけど、1 年の間にまともに使った覚えが 1 度もない。Python からの移民は使うのかな。
内部実装は Fiber なので、とっても遅いことに注意。300,000 回の next に 2 秒くらいかかる。

Array の拡張

Array#permutationArray#product はとてもよく使う。探索や総当たりをしたいときとか、テーブルを作るときとか。
Array#flatten に引数が追加されたのもとても便利です (参考) 。
あとは、Array#dropArray#takeArray#shuffleArray#sample も便利。

配列や文字列の省メモリ化

長さ 3 以下の配列や、長さ 11 以下の文字列 *10 などがいちいち malloc されなくなったので、小さな配列や小さな文字列を多用するときのメモリ効率がよくなっている (参考) 。
機能というより最適化なので、ユーザが直接意識をすることは基本的にはないはず。でも、関数型プログラミングでよくある「タプル」や「ペア」の代わりに配列を使う人は結構いるはずで、そういう人は喜んでいいと思う。ぼくはウォーキングの路線図の生成が 10% くらい速くなった (2 分弱から 1 分強くらい) ので、そのくらいうれしい。

ところで、1.8 で動いていた拡張ライブラリが 1.9 で

error: ‘struct RString’ has no member named `ptr'

みたいなビルドエラーになったら、この最適化が原因です。そういう時は落ち着いて

  • RARRAY(...)->ptr を RARRAY_PTR(...) に書き換える
  • RARRAY(...)->len を RARRAY_LEN(...) に書き換える
  • RSTRING(...)->ptr を RSTRING_PTR(...) に書き換える
  • RSTRING(...)->len を RSTRING_LEN(...) に書き換える

とすればいいです。

rubygems や rake の組み込み化

ぼくは ruby 1.8 のころは gem を使っていなかった *11 んですが、1.9 では使うようになりました。gem install でいろんなライブラリを遊べるのは楽しいですね。
ただ、世の多くの gem は 1.9 に対応していないので、本当に楽しくなるには時間が必要そうです。よく見る問題は、

  • 前述の RARRAY_PTR の話
  • when 1: のようなコロンが書けなくなったこと
  • String が Enumerable ではなくなったこと (String#each が消えた)

ですかね。
ちなみに、1.9 で使える素晴らしい gem でぼくが日ごろお世話になっているのは、cairo と ramaze 。
rake はあんまり使ってないけど、こないだちょっとウォーキングのデータ生成・管理スクリプトとして使ってみたら、とても便利だった。今後もっと使おう。

正規表現の後方参照に名前を使えること

正規表現エンジンが鬼車になって、一番使いそうな機能。

"Yusuke Endoh"[/(?<first_name>\w+)\s+(?<last_name>\w+)/]
p $~[:first_name] #=> Yusuke
p $~[:last_name]  #=> Endoh

いかにも便利そうなんですが、実際に使ったことはないです。やっぱり正規表現が煩雑になるからかなあ。
正規表現って基本的に使いたくないんですよね。簡単なパースには牛刀割鶏だと思うし、複雑なパースには使えないし、普通のパースでもソースのメンテナンス性が下がるし。書き捨てプログラムでしか使いたくないのでした。そういう意味では、String#start_with? みたいのがうれしい。

Hash に順序がついた

Hash を each したとき、新規代入した順に列挙される。

h = {}
%w(foo bar baz).each {|k| h[k] = true }
h.each_key {|k| p k }

などとやると、1.8 では "foo" "bar" "baz" の出てくる順序は不定だったけれど、1.9 ではこのとおりに出てくることが保証されます *12
経験上、これに依存したコードは読みにくいです。でも、ハッシュと配列の組で管理する必要がなくなって、幸せなのは幸せなんですよね。諸刃の剣。
ぼくのベストプラクティスとしては、なにかのテーブルみたいに、「いったん初期化した後で追加・更新しないハッシュ」にだけ使うのがいいと思ってます。初期化したあとに freeze すると、より安全。
代入順だけでなく、もっと細かく順序を制御できたらいいんじゃないかと思うけど、API の設計も効率的な実装も難しそう。

ブロック引数中のブロック引数

f = lambda {|&block| block.call(1) }
f.call {|x| p x }  #=> 1

と書けるようになった。今のところ、本気で使ったことはない機能。
define_method でブロックを受け取るメソッドが定義できるようになったのは、リフレクションマニアにはうれしいかも。

define_method(:foo) do |&block|
  block.call
end

上のコードは def foo; yield; end と同等です (細かいことを気にしなければ) 。

->(args) { ... }

lambda {|args| ... } の別表記。半年前くらいまでは lambda {|&block| ... } が syntax error であり、->(&block) { ... } と書くしかなかったので、-> は必須の存在だった。でも今では lambda {|&block| ... } と書けるので、存在意義を失ったという噂もある。
実際に使うかどうかは、個人の好み次第だと思う。proc や lambda を使うかどうか自体が、個人の趣味に大きく依存するよね。
どうでもいい話としては、Ruby 1.9 を記号だけでチューリング完全にするために重要 (記号だけで brainfuck インタプリタ記号だけで任意の文字列を eval) 。

post argument

def f(a, b, c, *rest, x, y, z)
  ...
end

と書けるようになった。ぼんやりとソースをいじってると、無意識のうちに使っていることがある機能。今となっては、1.8 で書けなかったことが不自然に感じる。
あと配列の先頭と終端を取り出すとき

first, *, last = ary

と書くことがしばしばある。

どこでも何度でも splat

post argument に似てるけど、

ary1 = [1, 2, 3]
ary2 = [4, 5, 6]
foo(*ary1, *ary2)

と書けるようになった。これもよく無意識のうちに使ってる。あと、テーブルの作成とかで、配列リテラルの中でよく使う。

chars = [*"A".."Z", *"a".."z", *"0".."9"]

まとまらないまとめ

ニュースではやっぱり YARV とか M17N みたいな大きい機能が取りざたされがちで、マネージャーとかシステム管理者とかのような立場の人に訴えるにはいいんだろうけど、実際にプログラムを書く人にとってはそんなの空気ですよねー。わはは。
というわけでぼくが好きな新機能ベスト 3 。

  • Enumerator (遅延配列的なもの)
  • Array の拡張
  • 配列や文字列の省メモリ化

これらのおかげで、だいぶ関数型プログラミングがしやすくなったと感じてます。というか、ぼくが知ってる言語の中では「Haskell の次にリスト処理がしやすい言語」です *13

こんな記事より見るべき 1.9 の新機能紹介文献

追記
"あいうえお".encode("EUC-JP", "UTF-8") が encoding になってたところなどを修正。

*1:バイトコード出力・読み込みができるようになれば改善するかもしれない。

*2:Project Euler ではとてもお世話になったけど。

*3:ただし dfp[-2] のような、YARV のデータ構造を知らないと意味不明な magic number がちりばめられたので、リファクタリングは必要だと思う。

*4:怠惰さのパラメータがかなり高いぼくが言うんだから間違いないです。

*5:ライブラリはいろんなエンコーディングのソースから require されるため、特定のエンコーディングに特化すべきでない。ただし YAML パーサみたいに、特定のエンコードに強い関係があるライブラリでは指定するのもあり。

*6:個人的には Fiber の全機能を Enumerator に入れてしまい、Fiber は表に見せないでもよかったのではないかと思っている (ruby-dev:31798) 。

*7:トップレベルなら再帰呼び出しが 8000 回くらいいけるけど、Fiber の中だと 300 回も行かずに SystemStackError になる。

*8:自分でも気がつかないうちに心変わりしてるんですね。この記事を書こうと思ったのは、これがきっかけ。

*9:今でも一貫性のなさが気にならないわけではないんですが。ary.inject.with_index と書けないのは悲しいし、ary.each_cons(2).to_a.compact のようにいちいち .to_a をはさまないといけないことがあるのも面倒。この辺は今後 2.0 の lazy array に向かってちょっとずつ解決していくのだろう。

*10:32 bit 環境の場合。実際には (sizeof(void*) * 3 / sizeof(char) - 1) 以下の文字列が対象なので、64 bit 環境の場合は長さ 23 以下の文字列かな。たぶん。

*11:Debian (というか FHS) との相性が悪いんですよねえ。

*12:ただし Ruby の言語仕様というわけではなく、ruby 1.9 の実装上の仕様という扱いだった気がする。

*13:Haskell ほどのカッコよさはないんですよね。でも OCaml よりは格好いい。