僅か30分で3つのバグ - Rubyの落し穴 -
Rubyでソートキーがふたつあるソート処理を書こうとした時の話です。
実際のデータ構造は違うのですが、簡単のために次のようなデータを用意します。
arr = [] arr.push({:x=>1,:y=>1}).push({:x=>2,:y=>2}).push({:x=>1,:y=>9}).push({:x=>5,:y=>0}).push({:x=>1,:y=>3})
x要素がプライマリなソートキーで、y要素がセカンダリなソートキーだとして、次のようにソート処理を書きました。
arr.sort{|a,b| cmp = a[:x]<=>b[:x]; cmp != 0 ? cmp : a[:y]<=>b[:y]} => [{:x=>1, :y=>1}, {:x=>1, :y=>3}, {:x=>1, :y=>9}, {:x=>2, :y=>2}, {:x=>5, :y=>0}]
一瞬で作業終了、と思ったのですが、次のようにnil要素があると、これはエラーになります。
arr.push({:x=>nil,:y=>0}).push({:x=>1,:y=>nil}) arr.sort{|a,b| cmp = a[:x]<=>b[:x]; cmp != 0 ? cmp : a[:y]<=>b[:y]} ArgumentError: comparison of Hash with Hash failed
nilのソート順は最後にすることに決めて、だいぶ長くなってしまいますが、次のように書き直しました。ブロックがdoに変わったのは気分の問題です。
arr.sort do |a,b| return 1 if !a[:x] return -1 if !b[:x] cmp = a[:x]<=>b[:x] if cmp != 0 cmp else return 1 if !a[:y] return -1 if !b[:y] a[:y]<=>b[:y] end end
Rubyに慣れた人ならすぐにバグに気づくと思いますが、これは期待通りに動作しません。irb上で実行すると、LocalJumpError: unexpected return のエラーがでて、すぐに return が悪いことに気づきますが、メソッド内に書くと、普通にメソッドが抜けるだけでエラーにはなりません。
ブロックの途中で値を返すには次のようにnextを使う必要があります。知っていたのにミスりました。
arr.sort do |a,b| next 1 if !a[:x] next -1 if !b[:x] cmp = a[:x]<=>b[:x] if cmp != 0 cmp else next 1 if !a[:y] next -1 if !b[:y] a[:y]<=>b[:y] end end
ちなみに上のコードで cmp != 0 を cmp とだけ書くのもありがちな落し穴ですが(Rubyは数値0を真偽値として評価すると真になります)、流石にこれはミスりませんでした。
ここまででふたつのバグです。
残りひとつは別の実験コードを書いていて作ったバグです。injectメソッドの動作確認のために、次のようなコードを書きました(このコード自体はたいした意味はありません。説明のためのコードです。このコードと同じことをするならmapを使う方が自然です)。
arr.inject([]){|arr,e| arr << e[:x].to_s}
実コードであれば、こんないい加減な変数名を使わないのですが、実験コードなので安易に配列はarr、要素はeと無意識に書いていました。
問題があると言われて見ればすぐに分かるかもしれませんが、上のコードは意図通りの動作をしません。正確に言えば、結果は意図通りですが、意図しない副作用が起こっています。ブロック内のスコープのつもりで使っているブロック変数arrが、外側のarrを破壊するのです。
もっとも、このバグはRuby1.9では仕様変更により起きなくなっているようです(未確認)。
僅か30分ほどの間に3つもバグを生みました。これもRubyの生産性の高さ故でしょうか。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/ruby-and-bugs/tbping
Re:僅か30分で3つのバグ - Rubyの落し穴 -
ブロックを呼び出す回数が少ないから効率的です。
Re:僅か30分で3つのバグ - Rubyの落し穴 -
記事のためにデータ構造を単純化しましたが、実際のコードはソートキーが整数ではなく、Time型でした。可読性とのバランスで、配列を使うsort_byで書き換えました。コードが短くなりました。
ありがとうございました。