Personal tools
You are here: Home ブログ 井上 僅か30分で3つのバグ - Rubyの落し穴 -
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Recent entries
Apache2.4のリリース予定は来年(2011年)初め(あくまで予定) inoue 2010-12-23
Herokuの発音 inoue 2010-12-20
雑誌記事「ソフトウェア・テストPRESS Vol.9」の原稿公開 inoue 2010-12-18
IPA未踏のニュース inoue 2010-12-15
労基法とチキンゲーム inoue 2010-12-06
フロントエンドエンジニア inoue 2010-12-03
ASCII.technologies誌にMapReduceの記事を書きました inoue 2010-11-25
技術評論社パーフェクトシリーズ絶賛発売中 inoue 2010-11-24
雑誌連載「Emacsのトラノマキ」の原稿(part8)公開 inoue 2010-11-22
RESTの当惑 inoue 2010-11-22
「プログラマのためのUXチートシート」を作りました inoue 2010-11-19
「ビューティフルコード」を読みました inoue 2010-11-16
Categories
カテゴリなし
 
Document Actions

僅か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の生産性の高さ故でしょうか。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/inoue/ruby-and-bugs/tbping

Re:僅か30分で3つのバグ - Rubyの落し穴 -

Posted by rubikitch at 2008-04-19 05:00
そこでArray#sort_byですよ!
ブロックを呼び出す回数が少ないから効率的です。

Re:僅か30分で3つのバグ - Rubyの落し穴 -

Posted by inoue at 2008-04-20 23:44
nil.to_iがゼロになるとか、[0,1]<=>[0,2]が-1を返すとか、細かな知識の積み上げがあってこそですね。

記事のためにデータ構造を単純化しましたが、実際のコードはソートキーが整数ではなく、Time型でした。可読性とのバランスで、配列を使うsort_byで書き換えました。コードが短くなりました。

ありがとうございました。
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.