Ruby の例外クラスは分類が粗すぎる or 細かすぎる

と思いません?

def foo(x)
end
foo(1, 2)  #=> wrong number of arguments (2 for 1) (ArgumentError)
1.step(10, 0) { }  #=> step can't be 0 (ArgumentError)
a = []; a << a
a.flatten  #=> tried to flatten recursive array (ArgumentError)

確かにどれも Argument に関する Error ではあるんだけど *1 、全部同じ例外クラスというのは粗すぎですよね。メッセージ読めば意味はわかるからデバッグには困りませんが、ArgumentError の中の特定の例外だけ拾いたいときに困ります。

具体的には、テストです *2 。例えば foo(1, 2) で wrong number of arguments が投げられることをテストしたいとします。以下のテストだと、wrong number of arguments 以外の ArgumentError が投げられる場合でも合格になってしまいます。

assert_raise(ArgumentError) { foo(1, 2) }

ちゃんとやりたければ、例えばこんな感じのコードを書かないとだめかな。

flag = false
begin
  foo(1, 2)
rescue ArgumentError => e
  raise unless ex.message[/\Awrong number of arguments \(\d+ for \d+\)\z/]
  flag = true
end
assert(flag)

もちろん実際には assert_raise_with_message みたいなメソッドにくくりだすとしても、メッセージを文字列比較や正規表現で判定しないといけないのはダサいです。

もしも ArgumentError ではなく、wrong number of arguments 専用の例外クラス (ArgumentWrongNumberError とする) を投げてくれれば、こんな風に書けそうです。

assert_raise(ArgumentWrongNumberError) { foo(1, 2) }

引数の数についてもチェックしたいときは、以下のようにかけたらかっこいいよなあ。Test::Unit の修正も必要そうだけど。

# wrong number of arguments (2 for 1) が投げられるテスト
assert_raise(ArgumentWrongNumberError.new(2, 1)) { foo(1, 2) }

極端な話、例外にメッセージ文字列を持たせるのではなく、メッセージの種類の数だけ例外クラスがあるべきではないかな *3


もし「テストなんかしないから今ので十分」という見解だとすると、それにしては分類が細かすぎると思います。ArgumentError と TypeError とか NameError とか NoMethodError とか、いちいち区別する理由はないような気がします。だって、これらを意識的に rescue し分ける場合ってほとんどなさそうです。実際、TypeError と ArgumentError の区別ってかなりいい加減に決められているように感じます。

# TypeError の方が自然そうな例
Rational([], 0)  #=> not an integer (ArgumentError)
ObjectSpace.define_finalizer(1, "foo")
  #=> wrong type argument String (should be callable) (ArgumentError)

# ArgumentError でもいい気がする例
1.instance_of?(1)  #=> class or module required (TypeError)
(1...5.0).max #=> cannot exclude non Integer end value (TypeError)


まあたぶん歴史的経緯 *4 なんだと思いますが、これってどうなんでしょうね。現実問題、逐一例外クラスを設計・命名していくのはめんどすぎかなあ。「提案自体はいいんだけど、例外クラスの名前がしっくりこないからその提案は保留」とか matz に言われたら悲しそうではあります。なにより、作業量のわりにメリットがなさすぎか。うーん。

*1:flatten の場合、レシーバは Argument なのか?という疑問は置いとく。

*2:他にも、リフレクションで怪しいことやりたいときに困ったことがある気がします。が、ぱっと例を出せない。

*3:インタプリタ起動時に大量の例外クラスが定義されるので、起動が遅くなったりメモリを食ったりするという問題はあるかも。本当にやるなら、例外クラスの定義を遅延させる枠組みとかが必要かもしれない。

*4:昔は例外クラスが存在せず、文字列を raise していた、と卜部さんに聞いたような気がする。