じゃんけんごときにクラスなんて要るのか
CodeIQというプログラミング課題に挑戦するサイトに、Rubyでジャンケンクラスを作れという問題があったのでやってみた。すでに問題は読めなくなっているけど、こんな感じのJankenクラスを作れという。
$ irb > require './janken.rb' > left = Janken.new > right = Janken.new > left.versus(right) 左の人が勝ました。右「チョキ」左「グー」 > left.versus(right) 右の人が勝ちました。右「チョキ」左「パー」 : :
繰り返しirbで実行できるように、とある。
問題を見た瞬間、これは問題自体がおかしいのではないかと思ったけど、やってみた。
# -*- coding: utf-8 -*- class Janken attr_reader :hand NAME = { goo: "グー", choki: "チョキ", pah: "パー" } def initialize generate_new_hand end def versus(other) generate_new_hand other.generate_new_hand case match(other) when :win puts "左の人が勝ちました。右「#{other}」左「#{self}」" when :lose puts "右の人が勝ちました。右「#{other}」左「#{self}」" when :even puts "あいこでした。#{self}" end end protected def generate_new_hand @hand = [:goo, :choki, :pah].shuffle.pop end def match(other) match = [@hand, other.hand] case when @hand == other.hand; :even when match == [:goo, :choki] || match == [:choki, :pah] || match == [:pah, :goo]; :win else :lose end end def to_s NAME[@hand] end end
勝敗メッセージの出力をJankenクラスでやるのは、いまいち。Jankenクラスは勝ち負けの判定ロジックを持つべきではあっても、その結果に基づくレポート機能まで含むのは抱え込みすぎだと思う。もう1つ、Jankenインスタンス自身は自分が右なのか左なのか分からないはずなので、右や左といった現実世界との結びつけは呼び出し側でやるべきこと。「右」とか「左」といった文字列がJankenクラスに含まれるのはおかしい。
ということを考えると、以下のほうが良い。
# -*- coding: utf-8 -*- class Janken attr_reader :hand NAME = { goo: "グー", choki: "チョキ", pah: "パー" } def initialize generate_new_hand end def versus(other) generate_new_hand other.generate_new_hand match(other) end protected def generate_new_hand @hand = [:goo, :choki, :pah].shuffle.pop end def match(other) match = [@hand, other.hand] case when @hand == other.hand; :even when match == [:goo, :choki] || match == [:choki, :pah] || match == [:pah, :goo]; :win else :lose end end def to_s NAME[@hand] end end if __FILE__ == $0 left = Janken.new right = Janken.new 10.times { case left.versus(right) when :win puts "左の人が勝ちました。右「#{right}」左「#{left}」" when :lose puts "右の人が勝ちました。右「#{right}」左「#{left}」" when :even puts "あいこでした。#{left}" end } end
さらに疑問が。
Jankenクラスがversusを呼び出すたびに内部の状態を変えていくというのは違和感を覚える。Jankenクラスは手の状態を持っているべきではあっても、同じインスタンスがどんどん状態を変えていくのが良いAPIとは思えない。ある人がグーを出し、次にチョキを出した時、この2つが同じオブジェクトではマズイ。
むしろ設問にあるJankenクラスというのは、Playerクラスであるべきで、Janken#versusというのは、Player#jankenとでもしたほうがジャンケンの自然な表現。だから、2つのクラスに分けて、
# -*- coding: utf-8 -*- class Player attr_accessor :hand def initialize @hand = Janken.new end def janken(other) case @hand.match(other.hand) when :win puts "私の勝ちです。私「#{@hand}」相手「#{other.hand}」" when :lose puts "私の負けです。私「#{@hand}」相手「#{other.hand}」" when :even puts "あいこでした。#{@hand}" end end end class Janken attr_reader :hand NAME = { goo: "グー", choki: "チョキ", pah: "パー" } def initialize @hand = [:goo, :choki, :pah].shuffle.pop end def match(other) match = [@hand, other.hand] case when @hand == other.hand; :even when match == [:goo, :choki] || match == [:choki, :pah] || match == [:pah, :goo]; :win else :lose end end def to_s NAME[@hand] end end if __FILE__ == $0 me = Player.new opponent = Player.new 5.times { me.janken opponent me.hand = Janken.new opponent.hand = Janken.new } end
とでもしたほうがいい。
ただ、2手目以降で外部からJanken.newをPlayerインスタンスに突っ込むのはちょっとヘン。Playerクラスが常にJanken.newをするほうが良い。出す手を考えるのはPlayerだし。考えてみるとジャンケンというのは一連の手のシーケンスなので、以下のようにEnumeratorを使ったほうが、より現実のジャンケンの表現として適切かもしれない。
# -*- coding: utf-8 -*- class Player def hand_seq @hand_seq ||= Enumerator.new do |y| loop { y << Janken.new } end end def janken(other) hand = hand_seq.peek other_hand = other.hand_seq.peek case hand.match(other_hand) when :win puts "私の勝ちです。私「#{hand}」相手「#{other_hand}」" when :lose puts "私の負けです。私「#{hand}」相手「#{other_hand}」" when :even puts "あいこでした。#{hand}" end end end class Janken attr_reader :hand NAME = { goo: "グー", choki: "チョキ", pah: "パー" } def initialize @hand = [:goo, :choki, :pah].shuffle.first end def match(other) match = [@hand, other.hand] case when @hand == other.hand; :even when match == [:goo, :choki] || match == [:choki, :pah] || match == [:pah, :goo]; :win else :lose end end def to_s NAME[@hand] end end if __FILE__ == $0 me = Player.new opponent = Player.new 5.times { me.janken opponent me.hand_seq.next opponent.hand_seq.next } end
Enumeratorを使うのはいいとしても、外部にそのまま見せるのはマズイ。peekって何だよという感じ。こういうときは必要なメソッドにだけ適当な名前を付けてデリゲート。
# -*- coding: utf-8 -*- require 'forwardable' class Player extend Forwardable def initialize @hand_seq = Enumerator.new do |y| loop { y << Janken.new } end end def_delegator :@hand_seq, :peek, :hand def_delegator :@hand_seq, :next, :next! def janken(other) case hand.match(other.hand) when :win puts "私の勝ちです。私「#{hand}」相手「#{other.hand}」" when :lose puts "私の負けです。私「#{hand}」相手「#{other.hand}」" when :even puts "あいこでした。#{hand}" end end end class Janken attr_reader :hand NAME = { goo: "グー", choki: "チョキ", pah: "パー" } def initialize @hand = [:goo, :choki, :pah].shuffle.first end def match(other) match = [@hand, other.hand] case when @hand == other.hand; :even when match == [:goo, :choki] || match == [:choki, :pah] || match == [:pah, :goo]; :win else :lose end end def to_s NAME[@hand] end end if __FILE__ == $0 me = Player.new opponent = Player.new 5.times { me.janken opponent me.next! opponent.next! } end
ここまで来ると滅茶苦茶という気がしなくもない。
たかがジャンケンプログラムごときで大げさ。グーチョキパーを [0, 1, 2] で表現して、それを比較するif文をずらっと並べればいい話で、クラスなんて1つも作ることなんてないという気もする。いや、必要なのはハッシュテーブル1つと、2つのジャンケンを受け取って評価する関数の1つだけか。このあいだYouTubeで見たPythonな人のトークで、データ構造で済むものにイチイチ名前を付けたりクラスにしたりするなよという主張に、それはあるかもなと思ったりしている。PythonとRubyで文化の違いがありそう。しかし、慣れてみたらクラスは便利で、別にコード行数が増えすぎるとか読みづらくなるというわけでもない。実行効率も多くの場合、全くどうでもいい話で、むしろ大事なのは人間が認識するインターフェースはどうなっているのかというところ。問題の捉え方をモジュールに分けることで読み手に示し、関連するコードをまとめる場としてクラスという容れ物を使うのは、すごく良いことに思えるのだよな。
寄り道してたらWikipediaに迷い込む
ちゃんと1行ずつ読もう、分からないところは飛ばさず読もうと思ってRailsのサンプルアプリを読み始めた。
1.year.from_nowって意味は自明だけど、実際のところ何だろうか? とか立ち止まりつつ。こういうのってActiveSupportの、確かDurationオブジェクトで。でも、from_nowってなんだ? ふむ、sinceのaliasかなるほど。
ユーザーのセッション管理はヘルパーにしてApplicationControllerにincludeしてしまうのが定石か、ところで待てよ、cookies.permanentのpermanentってなんだ、と思って、まず、ActionDispatchのディレクトリに移動。lsすると、何だか興味深いファイル名がたくさんあるので less * とかして読み始める。なるほど、ActionDispatch::BestStandardSupportなんていうRackミドルウェアがあるのか、こういう構造でミドルウェアって作るのか。なるほど、これはIEの互換性関連ぽいな、IE=Edge,chrome=1ってなんだ? なるほど、Chrome Frameがあれば、それをIEにロードしろというディレクティブなのか、げ、それってRailsのデフォルト? まあいいや、で、えーと、なんだっけ。ああ、cookie、cookie、これこれ、ActionDispatch::Request::Cookiesだわ。なるほど、permanentで20年先にexpireする指定ができるのか。options[:expire] = 20.years.from_now とかしてるのか。そういえば、cookieって無限遠の未来って指定できたっけな。どれどれWikipedia。こういうときはRFCをいきなり読むより、Wikipediaでctrl-fしたほうが分かるよな。expir...そうそう、これこれ。絶対日時か秒数か。ふーん、cookieのセキュリティとかプライバシーって議論色々あるのなぁ。へぇぇ。ところでcookieの例として数値の配列なんかを入れてるけど、実際にRailsが吐くヘッダってどうなるんだろ。ていうか、ヘッダってどういう文字列だっけな。そういえば4096バイトという制限があったような。やっぱりRFC見てみるか。どれどれ、RFCってどれが最新なんや。IETFのサイトって分かりづらいな。これは古いバージョンのRFCか……、古いRFCのページは、もっと紙がかすれて腐りかけてるような画面デザインにしてほしいな。で、cookieの最新はRFC6265だ。ふむふむ、ブラウザ実装は最低限でも1つのcookieあたり4096バイト保存可能であるべきで、ドメインあたり50個のcookieに対応できて、全部で3000個のcookieを扱えないといけないのか。あれれ、ちょっと待てよ、Wikipediaの記述と違うような……。うわっ、やっぱり。RFCにリンクが貼ってあるのに、古いRFCの値を参照してるよ、これ。ドメイン単位で20、全部で300個のcookieと書いてあるのは情報が古いんだな、きっと。RFC2965のほうにはそう書いてあるし。コンピュータ関連の記述では、かなり信用している英語版Wikipediaだけど、こんな間違いがあるとはなぁ。よーし、パパ、Wikipedia編集提案しちゃうぞ、初めてのWikipedia編集だ。まずはアカウントを取って、うーん、キャプチャが読めん。よし、それで……、wikiかぁ、このtalkとeditっていうのの違いはなんだ? talkかな? ほぉ、なんか最新のブラウザ実装に基づいた議論してるんだな。それで……、それで……、それでオレは今朝は何をしていたんだっけな。そうそう、Railsアプリを読もうと思ってたんだった。げ、3行しか読んでないやん……。
CRubyで末尾最適化を使った再帰
Schemeなんかと違って、言語としてのRubyは末尾最適化(Tail Call Optimization)の実装は必須ではないけど、処理系としてのCRubyは2.0.0からオプション扱いで入っている、という話。2012年の6月ごろにはMatzさんはTCOをデフォルトにするという考えもあったようだけど、ここにある議論によれば、Ruby 2.0系のマイナーバージョンまで先延ばしになった模様。性急にTCOを入れなかった理由は、
- バックトレースを失うので一般的なRuby利用者に影響が大きい
- set_trace_func()のサポートが大変
- 文法。ちゃんとドキュメントに落としこむのが難しい。これは半分冗談、半分本気
ということらしい。JRubyでも、JVMがサポートしない限り実装が難しいという(あれ? Clojureは明示的な末尾呼び出しの最適化をやってるように思うけど)。
Rubyはイテレータでeachもmapもfoldもあるので、forを使わないのと同じくらい再帰を使う場面がないように思う。リスト処理という意味でいえば、RubyのイテレータはTCO入りの再帰と言える気がする。トランポリン的な相互再帰とかでもない限り、これで困らない。と、思うのはSchemerじゃないからなのか。
でも、例えば、Euler Project 160なんかは再帰を使ったほうがスンナリ書けて、しかも、TCOがないとスタックがあっという間に溢れるような問題だ。問題は、
For any N, let f(N) be the last five digits before the trailing zeroes in N!. For example, 9! = 362880 so f(9)=36288 10! = 3628800 so f(10)=36288 20! = 2432902008176640000 so f(20)=17664 Find f(1,000,000,000,000)
という感じ。これを次のように書いた。
def f(n, res) if n == 1 return res end res = res * n res = res.to_s.sub(/0+$/, '') res = res[-5..-1] if res.size > 5 res = res.to_i f(n - 1, res) end p f(100, 1) #p f(1_000_000_000_000, 1)
ぼくの環境ではnが1000ではオッケーで、10000にするとスタックは溢れた。Ruby 1.9.3-p286ではなく、Ruby 2.0.0系のheadを使って以下のように書いた。RubyVMでTCOをオンにできる。ついでに文字列と数字の変換が遅いので、全部数値としてやるように変更。
RubyVM::InstructionSequence.compile_option = { :tailcall_optimization => true, :trace_instruction => false } RubyVM::InstructionSequence.new(<<-EOF).eval def f(n, res) if n == 1 return res end res = res * n while (res % 10) == 0 res /= 10 end res %= 100000 f(n - 1, res) end p f(1_000_000_00, 1) # 18 sec # p f(1_000_000_000_000, 1) EOF
$ time ruby -v p160tco.rb ruby 2.0.0dev (2012-10-12 trunk 37163) [x86_64-darwin11.4.2] 23616 ruby -v p160tco.rb 17.73s user 0.04s system 94% cpu 18.731 total
設問より4桁少なくして18秒ということは、答えがでるのに18万秒かかるということで、これは丸二日以上かかる計算。無理。メソッド呼び出しをやめて素直にfor的な何かを使うと、もしかしたら速くなるかな。
ともあれ、いくらループを回してもスタックは溢れなくなった。
find.rbにみるRubyのthrow/catchの大域脱出の例
Rubyのthrow/catchの大域脱出って、あまり見かけない気がしているけど、どういうときに使うのかなとぼんやり思っていたら、標準添付の lib/find.rbになるほどと思う例が見つかった。
# # find.rb: the Find module for processing all files under a given directory. # # # The +Find+ module supports the top-down traversal of a set of file paths. # # For example, to total the size of all files under your home directory, # ignoring anything in a "dot" directory (e.g. $HOME/.ssh): # # require 'find' # # total_size = 0 # # Find.find(ENV["HOME"]) do |path| # if FileTest.directory?(path) # if File.basename(path)[0] == ?. # Find.prune # Don't look any further into this directory. # else # next # end # else # total_size += FileTest.size(path) # end # end # module Find # # Calls the associated block with the name of every file and directory listed # as arguments, then recursively on their subdirectories, and so on. # # Returns an enumerator if no block is given. # # See the +Find+ module documentation for an example. # def find(*paths) # :yield: path block_given? or return enum_for(__method__, *paths) paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); d.dup} while file = paths.shift catch(:prune) do yield file.dup.taint begin s = File.lstat(file) rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG next end if s.directory? then begin fs = Dir.entries(file) rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG next end fs.sort! fs.reverse_each {|f| next if f == "." or f == ".." f = File.join(file, f) paths.unshift f.untaint } end end end end # # Skips the current file or directory, restarting the loop with the next # entry. If the current file is a directory, that directory will not be # recursively entered. Meaningful only within the block associated with # Find::find. # # See the +Find+ module documentation for an example. # def prune throw :prune end module_function :find, :prune end
Find.findでpathsを順繰りにyieldしている最中に途中でプツンと処理を切って、次のpathを使ってブロックを起動するには、Find.pruneを使う。これはクラスメソッドというかモジュール関数だけど、呼び先のfind.rbでは throw :prune として大域脱出してる。グローバルなクラス定数のメソッドで処理の流れをゴリッと変えるっていうのは、やや荒っぽい印象も受けるけど、スクリプト言語っぽいなと思った。ていうか、それが大域脱出というものか。あれ、じゃあ、Find.findしてないときにFind.pruneしたら? と思ったら、
> Find.prune ArgumentError: uncaught throw :prune
となるだけのことだった。
それより、? の単項演算子に戸惑った。
# if File.basename(path)[0] == ?.
文脈からすると、右辺はカレントディレクトリを示すピリオドのことかと思ったけど、実際、これは1文字からなる文字リテラルだった。そんなもんあったっけな……。Emacs Lisp由来なのかな。メタ文字も書ける。でも素直に "." と書くほうが分かりやすいと思う。
$ irb [1] pry(main)> ?. => "." [2] pry(main)> ?a => "a" [3] pry(main)> ?1 => "1" [4] pry(main)> ?abc SyntaxError: unexpected '?', expecting $end [4] pry(main)> ?\s => " " [5] pry(main)> ?\t => "\t" [6] pry(main)> ?\x20 => " "
Rubyで自前の例外クラスを作るときExceptionではなくStandardErrorを継承する理由
Rubyの例外について少し調べたので、まとめてみる。
多くのモダンな言語同様にRubyでは例外処理機構が組み込まれている。
- ファイルを開こうと思ったらファイルが存在しなかった
- ネットワーク先のサーバが反応しなくてタイムアウトした
- 定義されていない(存在しない)メソッドを呼んだ
- 0で割り算をしてしまった
など想定外の問題に遭遇したときに、その問題を無視せずプログラマが何らかの対応処理をするための枠組みを提供する。
C言語など古い言語では、関数からの戻り値でエラーコードを返し、それによって呼び出し側がエラー処理をその場で記述する。例えば、fopen(3)が失敗すると戻り値としてNULLが戻ってきてグローバル変数のerrnoに失敗の理由を示すエラーコードが設定される。
#include <stdio.h> #include <string.h> #include <errno.h> int main(void) { FILE *fp; if ((fp = fopen("some_file_that_does_not_exist.txt", "r")) != NULL) { fputs(fp); fclose(fp); } else { printf("Error: %s\n", strerror(errno)); } return 0; }
$ cc error.c && ./a.out Error: No such file or directory
C言語のような戻り値によるエラー処理に比べて、モダンな言語に備わる例外処理のメリットは、
- エラー処理のコードを本体のコードと分けてまとめて記述できるので見通しが良くなる
- 例外が発生すると基本的に処理系は止まる。いかにプログラマがずぼらであっても例外処理を記述することになるので、想定外の状況に対応できる堅牢なプログラムになる
というところか。
で、Rubyでは、QA@ITにあるこのQAにあるとおり例外クラスは以下のような階層になっている。Ruby 1.9.3の例。
Exception NoMemoryError ScriptError LoadError Gem::LoadError NotImplementedError SyntaxError SecurityError SignalException Interrupt StandardError ArgumentError EncodingError Encoding::CompatibilityError Encoding::ConverterNotFoundError Encoding::InvalidByteSequenceError Encoding::UndefinedConversionError FiberError IOError EOFError IndexError KeyError StopIteration LocalJumpError Math::DomainError NameError NoMethodError RangeError FloatDomainError RegexpError RuntimeError Gem::Exception Gem::CommandLineError Gem::DependencyError Gem::DependencyRemovalException Gem::DocumentError Gem::EndOfYAMLException Gem::FilePermissionError Gem::FormatException Gem::GemNotFoundException Gem::GemNotInHomeException Gem::InstallError Gem::InvalidSpecificationException Gem::OperationNotSupportedError Gem::RemoteError Gem::RemoteInstallationCancelled Gem::RemoteInstallationSkipped Gem::RemoteSourceException Gem::VerificationError SystemCallError ThreadError TypeError ZeroDivisionError SystemExit Gem::SystemExitException SystemStackError fatal
大事そうなのを抜き出すと、
Exception NoMemoryError ScriptError LoadError NotImplementedError SyntaxError SecurityError SignalException Interrupt StandardError ArgumentError EncodingError IOError EOFError IndexError KeyError StopIteration NameError NoMethodError RangeError FloatDomainError RegexpError RuntimeError Gem::Exception TypeError ZeroDivisionError SystemExit SystemStackError fatal
という感じかしら。
Rubyでは例外は自分でも定義できる。ただこのとき、ふつうは全ての例外クラスのスーパークラスであるExceptionクラスを継承しない。以下のようなケースで困ったことになるからだ。
class MyException < Exception; end begin puts "hello" raise MyException rescue puts "exception handled" end
上の例で MyExceptionは補足されない。なぜなら、rescue は第1引数で指定した例外クラスの下の階層にある例外だけを補足するけど、引数を省略すると StandardErrorクラスを指定したものとみなすからだ。MyExceptionはException直下の子クラスなので、rescue されない。
Exceptionを直接継承した自前定義の例外クラスを複数作って、それらを補足しようと思うと、いちばん上にあるExceptionごと補足するしかない。そうすると今度は、すべての例外を補足することになってしまう。例えば、exitを呼ぶと発生するSystemExitも補足されてしまう。これは恐らく望ましい動作ではない。
class MyException < Exception; end class MyException2 < Exception; end begin puts "hello" exit rescue Exception => ex p ex puts "exception handled" end
$ ruby system-exceptions.rb hello #<SystemExit: exit> exception handled $
というわけで、自前の例外クラスを作るときには、システム関連の例外まで含むExceptionではなく、アプリケーションレベルのトップレベルとも言える例外クラスのStandardErrorを継承するのがいい。
class MyError < StandardError; end class MyError2 < StandardError; end begin puts "hello" raise MyError rescue puts "exception handled" # 実行される end
RuntimeErrorを継承するケースも良く見る。名前付けの習慣も、MyExceptionより、MyErrorのほうが多い。名前としては、ほかには InvalidHoge とか、NoHogeFoundとかもある。RuntimeErrorは、動作中の停止なので停止理由を動詞の過去形で書くのが多いようだ。例えばHerokuのCLIのgemを見たら、
class AppCrashed < RuntimeError; end class CommandFailed < RuntimeError; end
とRuntimeErrorを継承するシンプルな例外クラスが2つあるだけだった。シンプル。
自前で例外クラスを定義するとき、どういう風に階層化するのかという疑問もあり得る。Rubyじゃないけど、例えばこのC++のコーディングガイドには、注意深く例外クラスの階層を設計したところで利用者は誰も気にしないんだから時間の無駄だと書いてある。1つのライブラリもしくは名前空間に付いて1つの例外を定義すれば十分だ、と。Rubyでもmoduleの名前空間の階層に合わせてStandardErrorを継承することが多いようだ。
gemのトップディレクトリから以下のように調べてみた。
$ find . -iname '*rb' |xargs grep -h '^ *rescue ' | sed 's/^ *//' |sort | uniq -c |sort -nr 718 rescue LoadError 604 rescue Sass::SyntaxError => e 445 rescue Exception => e 235 rescue Sass::SyntaxError => err 218 rescue LoadError => e 206 rescue => e 183 rescue Exception 145 rescue NameError 128 rescue ArgumentError 125 rescue Errno::ENOENT 89 rescue NameError => e 60 rescue ArgumentError, TypeError 58 rescue Interrupt 57 rescue Exception => exception 51 rescue NoMethodError => e 50 rescue NoMethodError 47 rescue Timeout::Error 46 rescue ArgumentError => e 36 rescue TypeError 35 rescue Exception => ex 33 rescue MemCache::MemCacheError => e 32 rescue RestClient::RequestFailed => e 32 rescue EOFError 31 rescue LoadError => boom 30 rescue RuntimeError => e 29 rescue SyntaxError => e 29 rescue NotImplementedError 28 rescue StandardError => e 27 rescue URI::InvalidURIError => e 27 rescue SystemExit 27 rescue RuntimeError 27 rescue Errno::ECONNREFUSED 25 rescue StandardError, ScriptError => e 25 rescue => ex 25 rescue ::Exception 24 rescue TypeError, NoMethodError 24 rescue Haml::Error => e 22 rescue Test::Unit::AssertionFailedError => e 22 rescue MemCache::MemCacheError 22 rescue ::Test::Unit::AssertionFailedError => e 21 rescue ThrowResult 21 rescue ActionView::MissingTemplate => e 20 rescue TZInfo::InvalidTimezoneIdentifier 20 rescue Sass::UnitConversionError 20 rescue EncodingError 20 rescue ::Sass::SyntaxError => e 19 rescue Gem::LoadError 19 rescue => boom 18 rescue RestClient::ResourceNotFound => e 18 rescue InvalidNumberError => e 18 rescue I18n::ArgumentError => e 18 rescue Exception => database_transaction_rollback 18 rescue Encoding::UndefinedConversionError => e 17 rescue StandardError 17 rescue Mysql::Error 17 rescue LoadError => err 16 rescue SystemCallError 16 rescue PGError 16 rescue Errno::ENOENT => e 15 rescue Timeout::Error => e 15 rescue Errno::ESRCH 15 rescue Errno::EPIPE 15 rescue ChildProcess::TimeoutError 15 rescue ActiveRecord::StatementInvalid 14 rescue SetupError 14 rescue Errno::EACCES 13 rescue IndexError 12 rescue RuntimeError; end 12 rescue LoadError => load_error 12 rescue FSSM::FileNotRealError => e 12 rescue Exception => e # errors from template code 12 rescue Errno::EINPROGRESS 12 rescue ActiveRecord::RecordNotFound 11 rescue StandardError # JRuby 11 rescue OpenSSLCipherError, TypeError 11 rescue Object => e 11 rescue NoMethodError then raise 11 rescue NoMethodError # rescue NoMethodError 11 rescue Mocha::ExpectationError => e 11 rescue LocalJumpError 11 rescue Exception => exception # errors from loading file 11 rescue ArgumentError => argument_error 11 rescue ActiveRecord::StatementInvalid => exception 11 rescue => frozen_object_error 11 rescue => error 11 rescue ::TZInfo::PeriodNotFound 11 rescue ::NoMethodError 10 rescue java.io.IOException => ex 10 rescue TypeError, LoadError => e 10 rescue ServerError => e 10 rescue Sass::SyntaxError 10 rescue Mysql::Error => e 10 rescue MemCache::MemCacheError, Errno::ECONNREFUSED 10 rescue Heroku::OkJson::Error 10 rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError 10 rescue EOFError, TypeError, ArgumentError, LoadError => e 10 rescue EOFError, TypeError, ArgumentError => e 10 rescue ArgumentError # Encoding charset + endianness doesn't exist 10 rescue *NOT_CONNECTED_ERRORS 10 rescue 9 rescue ZipError 9 rescue UnauthorizedAccess => e 9 rescue Thor::Error, LoadError, Errno::ENOENT => e 9 rescue ResourceInvalid => error 9 rescue RescuableException => e 9 rescue RescuableException 9 rescue OpenSSL::SSL::SSLError => e 9 rescue MissingSourceFile => e 9 rescue LoadError, NameError => const_error 9 rescue IOError 9 rescue FixtureClassNotFound 9 rescue Exception => failsafe_error 9 rescue Exception => err 9 rescue Exception => e # YAML, XML or Ruby code block errors 9 rescue Errno::EWOULDBLOCK, Errno::EAGAIN 9 rescue Errno::EEXIST 9 rescue ActiveSupport::MessageVerifier::InvalidSignature 9 rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone 9 rescue ActiveResource::ResourceNotFound 9 rescue ActiveRecord::RecordInvalid 9 rescue *RESCUE_ERRORS => error 8 rescue TypeError => e 8 rescue TimeoutError 8 rescue SocketError, Errno::EADDRNOTAVAIL 8 rescue RSpec::Mocks::MockExpectationError => e 8 rescue Pry::RescuableException 8 rescue NoMethodError, TypeError 8 rescue LoadError, NameError 8 rescue ImportError => e 8 rescue I18n::ArgumentError 8 rescue Heroku::API::Errors::RequestFailed => e 8 rescue ::Timeout::Error => e 8 rescue ::Exception => e 8 rescue *PASSTHROUGH_EXCEPTIONS => e 7 rescue SyntaxError 7 rescue SwiftError => e 7 rescue Sprockets::FileOutsidePaths 7 rescue RestClient::Unauthorized 7 rescue RestClient::BadRequest => e 7 rescue Racc::ParseError => e 7 rescue PGError => e 7 rescue OptionParser::InvalidOption => ex 7 rescue OpenURI::HTTPError 7 rescue Netrc::Error 7 rescue LoadError; end 7 rescue Exception => error 7 rescue Errno::ENOTEMPTY 7 rescue Errno::ECHILD, Errno::ESRCH 7 rescue Encoding::ConverterNotFound => _ 7 rescue ActionView::MissingTemplate 7 rescue ::MultiJson::DecodeError => e 7 rescue *RESCUE_ERRORS 6 rescue ZeroDivisionError 6 rescue URI::InvalidURIError 6 rescue Thor::Error => e 6 rescue SystemCallError => exception 6 rescue RestClient::RequestTimeout 6 rescue Rack::Test::Error 6 rescue OpenSSL::SSL::SSLError => error 6 rescue LoadError => ignore 6 rescue Journey::Router::RoutingError 6 rescue Johnson::Error => e 6 rescue JSON::ParserError => e 6 rescue JSON::NestingError 6 rescue IOError, Errno::EPIPE 6 rescue I18n::MissingTranslationData => exception 6 rescue Heroku::Client::AppCrashed => e 6 rescue Haml::SyntaxError => e 6 rescue Exception # errors from Marshal or YAML 6 rescue Errno::EPERM => e 6 rescue Errno::ENOSPC 6 rescue Errno::ENOENT, IOError 6 rescue Errno::ENOENT, Errno::ELOOP 6 rescue Errno::EINVAL 6 rescue Errno::ECHILD 6 rescue Errno::EAGAIN 6 rescue Errno::EADDRINUSE 6 rescue Dalli::DalliError 6 rescue DRb::DRbConnError 6 rescue CallbackError => e 6 rescue ArgumentError # if Date.new raises an exception on an invalid date 6 rescue ::V8::JSError => e 5 rescue java.lang.IllegalThreadStateException 5 rescue Win32::Registry::Error 5 rescue Timeout::Error, EOFError 5 rescue SystemCallError => ex 5 rescue SocketError, Errno::EADDRINUSE, Errno::EBADF => ex 5 rescue SocketError, Errno::EADDRINUSE 5 rescue ScriptError, StandardError => e 5 rescue RestClient::SSLCertificateNotVerified => ex 5 rescue RestClient::Locked => ex 5 rescue RestClient::BadGateway => e 5 rescue RSpec::Expectations::ExpectationNotMetError => last_error 5 rescue ProtocolError => why 5 rescue OptionParser::InvalidOption => e 5 rescue Object 5 rescue Nokogiri::XML::XPath::SyntaxError => e 5 rescue NameError, ArgumentError => e 5 rescue Interrupt => interrupt 5 rescue Heroku::Command::CommandFailed 5 rescue Gem::LoadError => e 5 rescue Errno::EISCONN 5 rescue Errno::EIO 5 rescue Errno::ECONNREFUSED, Errno::ECONNRESET, OpenSSL::SSL::SSLError 5 rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EADDRINUSE 5 rescue Dalli::DalliError => e 5 rescue ConnectionError 5 rescue CommandFailed => e 5 rescue ChildProcess::MissingPlatformError => ex 5 rescue ChildProcess::Error 5 rescue Bundler::BundlerError => e 5 rescue => err 5 rescue ::OAuth::Unauthorized => e 5 rescue *exceptions => ex 5 rescue *QUIT_ERRORS 5 rescue *CONNECTED_ERRORS 4 rescue Utf8Error 4 rescue Undefined => e 4 rescue TypeError, ArgumentError 4 rescue SystemCallError, SocketError => e 4 rescue SignalException => e 4 rescue Sequel::DatabaseDisconnectError 4 rescue RuntimeException => e 4 rescue RSpec::Mocks::MockExpectationError => error 4 rescue RSpec::Expectations::ExpectationNotMetError 4 rescue PostTooBigException 4 rescue Pending::PendingDeclaredInExample => e 4 rescue OAuth::Unauthorized => e 4 rescue Nokogiri::XML::XPath::SyntaxError 4 rescue Nokogiri::SyntaxError, RuntimeError 4 rescue NoMethodError, ArgumentError 4 rescue NewRelic::Command::CommandFailure => e 4 rescue NewRelic::Agent::ForceRestartException => e 4 rescue Net::LDAP::LdapError 4 rescue NameError => predicate_missing_error 4 rescue MultiJson::DecodeError => de 4 rescue Memcached::NotFound 4 rescue Mail::Field::ParseError => e 4 rescue LoadError # 1.8 support 4 rescue Iconv::InvalidEncoding => e 4 rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL 4 rescue Iconv::IllegalSequence => e 4 rescue I18n::ArgumentError => exception 4 rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e 4 rescue Heroku::API::Errors::NotFound => e 4 rescue Heroku::API::Errors::ErrorWithResponse => e 4 rescue GemNotFound => e 4 rescue Gem::RemoteFetcher::FetchError 4 rescue Gem::GemNotFoundException 4 rescue FFI::NotFoundError 4 rescue Exception => raised 4 rescue Exception => e # Net::SMTP errors or sendmail pipe errors 4 rescue Exception => @actual_error 4 rescue Error::NoSuchElementError => last_error 4 rescue Errno::EWOULDBLOCK 4 rescue Errno::ESRCH => e 4 rescue Errno::EHOSTDOWN 4 rescue Errno::EAGAIN, Errno::EINTR 4 rescue Errno::EACCES => e 4 rescue EncodingFound => e 4 rescue CmdException, OptionParser::ParseError => e 4 rescue Capistrano::ConnectionError => e 4 rescue Capistrano::CommandError => e 4 rescue Bundler::GemNotFound => e 4 rescue @expected_exception => @rescued_exception 4 rescue @expected_error => @actual_error 4 rescue => other_exception 4 rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e 4 rescue ::Rhino::JavascriptError => e 4 rescue ::Net::HTTPFatalError => e 4 rescue ::Memcached::NotFound 4 rescue ::Kramdown::Error 4 rescue ::Exception # work around a bug in ruby 1.9 4 rescue ::Exception # for example on EPERM (process exists but does not belong to us) 3 rescue get_exception(arg) 3 rescue get_exception(0), NameError 3 rescue Zlib::BufError 3 rescue ValueError, msg: 3 rescue V8::JSError => e 3 rescue SystemExit => se 3 rescue StandardError, ScriptError 3 rescue StandardError, LoadError, SyntaxError => e 3 rescue Sprockets::FileNotFound, Sprockets::ContentTypeMismatch 3 rescue Spec::Expectations::ExpectationNotMetError => e 3 rescue SignalException 3 rescue ServerError => why 3 rescue Sequel::NotImplemented 3 rescue SecurityError 3 rescue RuntimeError => why 3 rescue Rhino::JSError => e 3 rescue RestClient::Locked => e 3 rescue Rack::Mount::RoutingError 3 rescue RSpec::Expectations::ExpectationNotMetError => e 3 rescue OptionParser::ParseError => ex 3 rescue OptionParser::ParseError => err 3 rescue OptionParser::ParseError => e 3 rescue NotFoundError => e 3 rescue NoMethodError, Haml::Util.av_template_class(:Error) 3 rescue Net::SSH::AuthenticationFailed 3 rescue NativeException, JavaSQL::SQLException => e 3 rescue NamespaceMissingError 3 rescue NameError, LoadError => e 3 rescue MissingTranslationData 3 rescue Message::KeyNotFound => why 3 rescue Magick::ImageMagickError 3 rescue LoadError; end # RCov doesn't see this, but it is run 3 rescue LoadError, SyntaxError => ex 3 rescue LoadError, NameError => error 3 rescue LoadError, NameError => e 3 rescue LoadError => ignore_if_database_cleaner_not_present 3 rescue LoadError => error 3 rescue LoadError => cannot_require 3 rescue LoadError # => rubygems_not_installed 3 rescue LoadError # => gem_not_installed 3 rescue InvalidValue 3 rescue InvalidURIError, TypeError 3 rescue Interrupt => e 3 rescue I18n::MissingTranslationData 3 rescue Haml::Util.av_template_class(:Error) => e 3 rescue ForwardRequest => req 3 rescue FileNotFound 3 rescue ExecutionError => e 3 rescue Excon::Errors::NotFound 3 rescue Exception => exp 3 rescue Exception => e # errors from Marshal or YAML 3 rescue Exception => e 3 rescue Errno::ETIMEDOUT 3 rescue Errno::ESPIPE 3 rescue Errno::EOPNOTSUPP 3 rescue Errno::ENOENT, Errno::ENOTDIR 3 rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable 3 rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable 3 rescue Encoding::ConverterNotFoundError => _ 3 rescue Bundler::RubyVersionMismatch => e 3 rescue ArgumentError, NameError 3 rescue ArgumentError => ex # if Date.new raises an exception on an invalid date 3 rescue AgentProxyException => e 3 rescue => socket_error 3 rescue => request_error 3 rescue => details 3 rescue ::OptionParser::ParseError => pe 3 rescue ::Haml::Error => e 3 rescue ::Excon::Errors::SocketError 3 rescue ::Exception => boom 3 rescue *exceptions => @rescued_exception 3 rescue # else leave as is 2 rescue parser.parse_error => error 2 rescue engine::ParseError => exception 2 rescue adapter::ParseError => exception 2 rescue YARD::Parser::UndocumentableError => err 2 rescue UnserializeError 2 rescue Timeout::Error, StandardError => e 2 rescue Timeout::Error, StandardError 2 rescue Timeout::Error, NewRelic::Agent::ServerConnectionException 2 rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, 2 rescue Timeout::Error => err 2 rescue TemplateOperatorAbortedError 2 rescue Taps::CorruptedData => e 2 rescue Tags::TagFormatError 2 rescue SystemExit, NoMemoryError, SignalException 2 rescue SystemExit => ex 2 rescue SystemExit => e 2 rescue SystemCallError => er 2 rescue SyntaxError, RegexpError 2 rescue SyntaxError, NoMethodError 2 rescue SyntaxError, NameError 2 rescue SyntaxError => ex 2 rescue StandardError, Timeout::Error 2 rescue StandardError => ex 2 rescue StandardError => err 2 rescue Spork::TestFramework::NoFrameworksAvailable => e 2 rescue Spork::TestFramework::FactoryException => e 2 rescue SocketError 2 rescue Server::ProtocolError => err 2 rescue Sequel::InvalidValue => e 2 rescue ScriptError, StandardError => error 2 rescue ScriptError, RegexpError, NameError, ArgumentError => e 2 rescue Sass::SyntaxError => keyword_exception 2 rescue RuntimeError => err 2 rescue RunnerError => e 2 rescue RubyPython::PythonError => exc 2 rescue RestClient::Unauthorized, Heroku::API::Errors::Unauthorized 2 rescue RestClient::RequestTimeout, Heroku::API::Errors::Timeout 2 rescue RestClient::Request::Unauthorized => e 2 rescue RestClient::PaymentRequired, Heroku::API::Errors::VerificationRequired => e 2 rescue RestClient::Locked, Heroku::API::Errors::Locked => e 2 rescue RestClient::Exception 2 rescue RemoteError => error 2 rescue RbSupport::NilWorld => e 2 rescue Rack::AdapterNotFound => e 2 rescue Racc::ParseError, Regin::Parser::ScanError 2 rescue QuestionError 2 rescue Question::NoAutoCompleteMatch 2 rescue PythonError, NoMethodError 2 rescue ProfilesNotDefinedError, YmlLoadError, ProfileNotFound => e 2 rescue PluginHost::PluginNotFound 2 rescue Pending => e 2 rescue PathError, GitError 2 rescue PGError, NoMethodError 2 rescue OptionParser::ParseError 2 rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e 2 rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError, OpenSSL::PKey::ECError => e 2 rescue OpenSSL::PKey::ECError => e 2 rescue OpenID::DiscoveryFailure => e 2 rescue Object # Ignore since ObjectSpace might not be loaded on JRuby 2 rescue NotSupportedByDriverError 2 rescue NotImplementedError => ex 2 rescue NoMethodError => pre_cucumber_0_4 # REMOVE WHEN SUPPORT FOR PRE-0.4 IS DROPPED 2 rescue NilWorld => e 2 rescue NewRelic::Command::CommandFailure => c 2 rescue NewRelic::Command::CommandFailure 2 rescue NewRelic::Agent::ServerConnectionException => e 2 rescue NewRelic::Agent::Sampler::Unsupported => e 2 rescue NewRelic::Agent::LicenseException => e 2 rescue NewRelic::Agent::ForceRestartException, NewRelic::Agent::ForceDisconnectException 2 rescue NewRelic::Agent::ForceDisconnectException => e 2 rescue Net::HTTPBadResponse => e 2 rescue Net::HTTP::Persistent::Error => error 2 rescue NameError => name_error 2 rescue LoadError, NotImplementedError 2 rescue LoadError => try_rspec_1_2_4_or_higher 2 rescue LoadError => try_rspec_1 2 rescue LoadError => loadError 2 rescue LoadError => give_up 2 rescue LoadError => ex 2 rescue LoadError # If we're not on Windows try... 2 rescue LoadError 2 rescue InvalidRequest => e 2 rescue Interrupt, StandardError, SystemExit => error 2 rescue Interrupt, IOError 2 rescue IOError, EOFError, Timeout::Error, 2 rescue IOError => e 2 rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e 2 rescue Heroku::API::Errors::Unauthorized, RestClient::Unauthorized 2 rescue Heroku::API::Errors::Timeout, RestClient::RequestTimeout 2 rescue Heroku::API::Errors::Locked => e 2 rescue HTTParty::RedirectionTooDeep, Timeout::Error => e 2 rescue HTTPError, TypeError => e 2 rescue GitError 2 rescue Gherkin::Lexer::LexingError, Gherkin::Parser::ParseError => e 2 rescue GemNotFound, VersionConflict 2 rescue Gem::Package::FormatError 2 rescue Gem::InvalidSpecificationException => e 2 rescue ForkDiedException, EOFError 2 rescue FetchingError => why 2 rescue Faraday::Error::ClientError 2 rescue Excon::Errors::StubNotFound, Excon::Errors::Timeout => error 2 rescue Excon::Errors::HTTPStatusError => error 2 rescue Errno::EWOULDBLOCK, Errno::EAGAIN => e 2 rescue Errno::ESRCH # No such process 2 rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED 2 rescue Errno::EPIPE => e 2 rescue Errno::EPERM 2 rescue Errno::EMFILE 2 rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL 2 rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG 2 rescue Errno::ECONNRESET => e 2 rescue Errno::ECONNREFUSED => exception 2 rescue Errno::EBADF 2 rescue Errno::EAGAIN, Errno::EWOULDBLOCK 2 rescue Errno::EADDRINUSE => e 2 rescue Errno::EACCES, Errno::ENOENT 2 rescue EOFError => e 2 rescue EOFError # HighLine throws this if @input.eof? 2 rescue DiscoveryFailure => why 2 rescue DalliError, NetworkError => e 2 rescue Dalli::NetworkError 2 rescue DRbClientError => e 2 rescue DRb::DRbConnError => e 2 rescue Cucumber::Ast::Table::Different => e 2 rescue Cucumber::Ast::Table::Different 2 rescue Cucumber::ArityMismatchError => e 2 rescue ConnectionError => error 2 rescue Compass::Error 2 rescue CommandOptionError => ex 2 rescue Capybara::TimeoutError; end 2 rescue Capybara::ExpectationNotMet 2 rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error 2 rescue Capistrano::Error => error 2 rescue Capistrano::CommandError 2 rescue BundlerError 2 rescue Bundler::VersionConflict => e 2 rescue Bundler::GitError => e 2 rescue Bundler::GemNotFound 2 rescue ArgumentError, TypeError => e 2 rescue ArgumentError, SyntaxError, Gem::EndOfYAMLException, Gem::Exception 2 rescue ArgumentError, NameError => error 2 rescue ArgumentError => why 2 rescue ArgumentError => boom 2 rescue Ambiguous => e 2 rescue ActiveRecord::StatementInvalid => e 2 rescue ActiveRecord::ActiveRecordError => e 2 rescue => foo 2 rescue ::WIN32OLERuntimeError => e 2 rescue ::SocketError => e 2 rescue ::Rhino::JSError => e 2 rescue ::Patron::TimeoutError => err 2 rescue ::Object 2 rescue ::ODBC::Error, ArgumentError => e 2 rescue ::OAuth2::HTTPError, ::OAuth2::AccessDenied, CallbackError => e 2 rescue ::OAuth2::HTTPError => e 2 rescue ::OAuth2::Error, CallbackError => e 2 rescue ::NoMethodError, ::MultiJson::DecodeError => e 2 rescue ::Net::HTTPFatalError, ::OpenSSL::SSL::SSLError => e 2 rescue ::Memcached::NotStored 2 rescue ::GrowlNotify::GrowlNotFound 2 rescue ::Errno::ETIMEDOUT 2 rescue ::DataObjects::Error => e 2 rescue *NET_HTTP_EXCEPTIONS 2 rescue *HTTP_ERRORS => e 2 rescue *EXCEPTIONS => e 1 rescue stub_exception_class 1 rescue exception => @rescued_exception 1 rescue error => ex 1 rescue Zlib::GzipFile::Error 1 rescue Zlib::Error 1 rescue Zlib::DataError 1 rescue Yadis::XRDSError => why 1 rescue Yadis::XRDSError 1 rescue XRDSError => err 1 rescue WebSocketError => e 1 rescue UndocumentableError => err 1 rescue URIClassifier::RegistrationError => e 1 rescue URI::InvalidURIError => why 1 rescue URI::InvalidURIError => err 1 rescue URI::Error => why 1 rescue URI::Error => e 1 rescue TypeURIMismatch 1 rescue TypeError 1 rescue Timeout::Error => why 1 rescue Test::Unit::AssertionFailedError => err 1 rescue TerminateLineInput 1 rescue Taps::DuplicatePrimaryKeyError => e 1 rescue TagFormatError 1 rescue SystemStackError 1 rescue SystemCallError; end 1 rescue SystemCallError, TypeError 1 rescue SystemCallError, Timeout::Error, EOFError, SocketError 1 rescue SystemCallError, Timeout::Error, EOFError 1 rescue SystemCallError, Timeout::Error 1 rescue SyntaxError => e2 1 rescue StopServer 1 rescue Sprockets::FileNotFound 1 rescue SocketError => err 1 rescue Slim::Parser::SyntaxError => ex 1 rescue Server::UntrustedReturnURL => err 1 rescue Server::MalformedTrustRoot => why 1 rescue Sequel::UndefinedAssociation 1 rescue Sequel::Rollback 1 rescue Sequel::DatabaseError=>e 1 rescue Sequel::DatabaseError => e 1 rescue Sequel::DatabaseError 1 rescue Sequel::DatabaseConnectionError 1 rescue Selenium::WebDriver::Error::WebDriverError 1 rescue Selenium::WebDriver::Error::UnhandledError => e 1 rescue SQLite3::Exception => e 1 rescue RuntimeError => exc 1 rescue RuntimeError => ex 1 rescue RubyVersionMismatch => e 1 rescue RubyPython::PythonError => e 1 rescue RestClient::PaymentRequired => e 1 rescue RestClient::Exception, Taps::BaseError => e 1 rescue ResponseError => error 1 rescue Redis::CannotConnectError 1 rescue RealmVerificationRedirected => err 1 rescue RangeError 1 rescue Rack::Lint::LintError => e 1 rescue RDoc::RubyLex::Error 1 rescue RDoc::Markup::Parser::Error => e 1 rescue RDoc::Error 1 rescue PythonError => exc 1 rescue PythonError => e 1 rescue Psych::SyntaxError 1 rescue ProtocolError => e 1 rescue Polyglot::NestedLoadError => e 1 rescue ParserSyntaxError => e 1 rescue ParserError 1 rescue Parser::UndocumentableError => undocerr 1 rescue Parser::ParserSyntaxError 1 rescue PGError =>e 1 rescue OptionParser::InvalidOption 1 rescue OpenURI::HTTPRedirect => e 1 rescue OpenURI::HTTPError => e 1 rescue OpenSSL::SSL::SSLError => why 1 rescue OpenSSL::PKey::PKeyError 1 rescue OpenIDError => why 1 rescue OpenID::OpenIDError => e 1 rescue OpenID::FetchingError => why 1 rescue OkJson::Parser 1 rescue Object; end } 1 rescue Object => rails_error 1 rescue Object => exc 1 rescue Object => boom 1 rescue Object => anything 1 rescue Object # Ignore since obj.class can sometimes take parameters 1 rescue OCIInvalidHandle 1 rescue OCIException => e 1 rescue NotImplementedError, NameError 1 rescue NotFoundError 1 rescue NotFound => boom 1 rescue NonMethodContextError => err 1 rescue NoSuchKey 1 rescue NoMethodError => no_method_error 1 rescue NoMethodError => ex 1 rescue NetworkError => e 1 rescue Net::SSH::Authentication::DisallowedMethod 1 rescue NativeException => e 1 rescue NamespaceMissingError => missingerr 1 rescue NamespaceAliasRegistrationError => e 1 rescue NameError, NoMethodError 1 rescue NameError => ne 1 rescue NameError => ex 1 rescue Mongo::ConnectionFailure 1 rescue MissingTranslationData => exception 1 rescue Message::KeyNotFound, ArgumentError => why 1 rescue LoadError, RuntimeError => e 1 rescue LoadError, RuntimeError 1 rescue LoadError, ArgumentError => error 1 rescue LoadError => load_exception 1 rescue LoadError => library_not_installed 1 rescue LoadError => e2 1 rescue LoadError => e 1 rescue LoadError # try rspec 1 1 rescue LoadError # If our first choice fails, try using ffi-ncurses. 1 rescue LoadError # Finally, if all else fails, use stty 1 rescue LoadError # If the ffi-ncurses choice fails, try using stty 1 rescue LoadError # If our first choice fails, try using JLine 1 rescue LoadError 1 rescue LibraryNotPreparedError 1 rescue Less::ParseError => e 1 rescue KVFormError => err 1 rescue JSON::ParserError 1 rescue InvalidOpenIDNamespace => e 1 rescue InvalidOpenIDNamespace 1 rescue Interrupt, StandardError, RDoc::Error, SystemStackError => e 1 rescue InternalError, RequestTimeout 1 rescue IncompleteExpression 1 rescue ImageMagickError 1 rescue IOError => e 1 rescue HttpParserError => e 1 rescue Heroku::Plugin::ErrorUpdatingSymlinkPlugin 1 rescue Heroku::API::Errors::Error 1 rescue HandshakeError => e 1 rescue HTTPStatusError => why 1 rescue HTTPRedirectLimitReached => e 1 rescue HTMLTokenizerError # just stop parsing if there's an error 1 rescue Gem::LoadError, LoadError, RuntimeError 1 rescue Gem::LoadError => load_error 1 rescue Gem::Exception 1 rescue Gem::DocumentError => e 1 rescue Foreman::Export::Exception => ex 1 rescue FloatDomainError 1 rescue FinishRequest 1 rescue FilterNotFound 1 rescue FSSM::CallbackError => e 1 rescue Excon::Errors::StubNotFound => stub_not_found 1 rescue Excon::Errors::SocketError => error 1 rescue Excon::Errors::Error => error 1 rescue Exception=>exception 1 rescue Exception, OpenSSL::OpenSSLError => e 1 rescue Exception => why 1 rescue Exception => e # errors from ActiveRecord setup 1 rescue Exception # just stop parsing if there's an error 1 rescue EventMachine::ConnectionError => e 1 rescue Error => e 1 rescue Error 1 rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError 1 rescue Errno::EPIPE, RestClient::RequestFailed, RestClient::RequestTimeout 1 rescue Errno::EPIPE, Errno::ECONNRESET 1 rescue Errno::EPIPE => boom 1 rescue Errno::ENOTCONN => e 1 rescue Errno::ENOENT => e 1 rescue Errno::ENETUNREACH 1 rescue Errno::EISDIR, Errno::ENOENT 1 rescue Errno::EINVAL, Errno::EBADF 1 rescue Errno::EDOM 1 rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e 1 rescue Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError 1 rescue Errno::ECONNRESET, EOFError 1 rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH 1 rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, SystemCallError, OpenURI::HTTPError, Timeout::Error => error 1 rescue Errno::ECONNREFUSED, Errno::EBADF 1 rescue Errno::ECONNREFUSED => ex 1 rescue Errno::ECONNREFUSED => e 1 rescue Errno::ECONNABORTED 1 rescue Errno::EADDRNOTAVAIL 1 rescue Errno::EACCES; end 1 rescue Errno::EACCES => error 1 rescue Errno::EACCES # unreadble file 1 rescue EncodingFoundException => e 1 rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF 1 rescue EOFError, Errno::ESPIPE 1 rescue EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED 1 rescue EOFError, Errno::ECONNRESET 1 rescue DatabaseError 1 rescue Dalli::MarshalError => ex 1 rescue Dalli::DalliError => ex 1 rescue Dalli::DalliError # SASL auth failure 1 rescue Conversion::UnsupportedConversion => exc 1 rescue ContextMiss 1 rescue ConnectionError => e 1 rescue Compass::Error => e 1 rescue CommandError, Slop::InvalidOptionError => e 1 rescue CommandError 1 rescue ChannelRequestFailed 1 rescue ChannelOpenFailed => err 1 rescue BeforeHookFailed 1 rescue BSON::InvalidObjectId, ::Mongoid::Errors::DocumentNotFound 1 rescue BSON::InvalidObjectId 1 rescue ArgumentError, TypeError, RangeError => error 1 rescue ArgumentError, NotImplementedError => e 1 rescue ArgumentError => err 1 rescue ArgumentError # for ruby < 1.9 compat 1 rescue ArgumentError # bad timestamp 1 rescue ArgumentError # If HOME is relative 1 rescue AgentNotAvailable 1 rescue @container::ResponseError => e 1 rescue @container::AccessDenied => e 1 rescue => e2 1 rescue => detail 1 rescue => @e 1 rescue ::StandardError => e 1 rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e 1 rescue ::OpenID::OpenIDError, Timeout::Error => e 1 rescue ::OmniAuth::NoSessionError => e 1 rescue ::ODBC::Error => e 1 rescue ::Mysql2::Error => e 1 rescue ::LoadError 1 rescue ::Hpricot::Error, RuntimeError, ArgumentError 1 rescue ::Amalgalite::Error, ::Amalgalite::SQLite3::Error => e 1 rescue ::ActionView::MissingTemplate => exception 1 rescue ::AWS::S3::NoSuchBucket 1 rescue *parse_error 1 rescue *@ignored => last_error 1 rescue # ignore failed gsub, for instance when non-utf8 1 rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError 1 rescue # Defend against inspect not taking a parameter
LoadErrorが多いのはgemの性質から来るものかしら。
例外オブジェクトに自前メッセージや自前スタックトレースを付け加えることもできるけど、例外クラス名そのものがズバリのエラーの原因を示している、という例も多い。というか、それが多数派。
以下のようにRSpecを調べてみたら、「RSpec::Configuration::MustBeConfiguredBeforeExampleGroupsError < StandardError」なんていうのもある。なるほど。
$ find gems/rspec-*11* -name '*rb' |xargs grep -hi -C10 'class.*<.*Error' # c.before(:suite) { establish_connection } # c.before(:each) { log_in_as :authorized } # c.around(:each) { |ex| Database.transaction(&ex) } # end # # @see RSpec.configure # @see Hooks class Configuration include RSpec::Core::Hooks class MustBeConfiguredBeforeExampleGroupsError < StandardError; end # @private def self.define_reader(name) eval <<-CODE def #{name} value_for(#{name.inspect}, defined?(@#{name}) ? @#{name} : nil) end CODE end -- module RSpec module Core module Pending class PendingDeclaredInExample < StandardError; end # If Test::Unit is loaed, we'll use its error as baseclass, so that Test::Unit # will report unmet RSpec expectations as failures rather than errors. begin class PendingExampleFixedError < Test::Unit::AssertionFailedError; end rescue class PendingExampleFixedError < StandardError; end end class PendingExampleFixedError def pending_fixed?; true; end end NO_REASON_GIVEN = 'No reason given' NOT_YET_IMPLEMENTED = 'Not yet implemented' # @overload pending() -- # c.before(:suite) { establish_connection } # c.before(:each) { log_in_as :authorized } # c.around(:each) { |ex| Database.transaction(&ex) } # end # # @see RSpec.configure # @see Hooks class Configuration include RSpec::Core::Hooks class MustBeConfiguredBeforeExampleGroupsError < StandardError; end # @private def self.define_reader(name) eval <<-CODE def #{name} value_for(#{name.inspect}, defined?(@#{name}) ? @#{name} : nil) end CODE end -- module RSpec module Core module Pending class PendingDeclaredInExample < StandardError; end # If Test::Unit is loaed, we'll use its error as baseclass, so that Test::Unit # will report unmet RSpec expectations as failures rather than errors. begin class PendingExampleFixedError < Test::Unit::AssertionFailedError; end rescue class PendingExampleFixedError < StandardError; end end class PendingExampleFixedError def pending_fixed?; true; end end NO_REASON_GIVEN = 'No reason given' NOT_YET_IMPLEMENTED = 'Not yet implemented' # @overload pending() -- module RSpec module Expectations if defined?(Test::Unit::AssertionFailedError) class ExpectationNotMetError < Test::Unit::AssertionFailedError; end else class ExpectationNotMetError < ::StandardError; end end end end -- require 'spec_helper' class UnexpectedError < StandardError; end module MatcherHelperModule def self.included(base) base.module_eval do def included_method; end end end def self.extended(base) base.instance_eval do def extended_method; end -- module RSpec module Expectations if defined?(Test::Unit::AssertionFailedError) class ExpectationNotMetError < Test::Unit::AssertionFailedError; end else class ExpectationNotMetError < ::StandardError; end end end end -- require 'spec_helper' class UnexpectedError < StandardError; end module MatcherHelperModule def self.included(base) base.module_eval do def included_method; end end end def self.extended(base) base.instance_eval do def extended_method; end -- module RSpec module Mocks # @private class MockExpectationError < Exception end # @private class AmbiguousReturnError < StandardError end end end -- require 'spec_helper' module RSpec module Mocks describe "#any_instance" do class CustomErrorForAnyInstanceSpec < StandardError;end let(:klass) do Class.new do def existing_method; :existing_method_return_value; end def existing_method_with_arguments(arg_one, arg_two = nil); :existing_method_with_arguments_return_value; end def another_existing_method; end private def private_method; :private_method_return_value; end end end -- it "raises instance of submitted ArgumentError" do error = ArgumentError.new("error message") @double.should_receive(:something).and_raise(error) lambda { @double.something }.should raise_error(ArgumentError, "error message") end it "fails with helpful message if submitted Exception requires constructor arguments" do class ErrorWithNonZeroArgConstructor < RuntimeError def initialize(i_take_an_argument) end end @double.stub(:something).and_raise(ErrorWithNonZeroArgConstructor) lambda { @double.something }.should raise_error(ArgumentError, /^'and_raise' can only accept an Exception class if an instance/) end -- module RSpec module Mocks # @private class MockExpectationError < Exception end # @private class AmbiguousReturnError < StandardError end end end -- require 'spec_helper' module RSpec module Mocks describe "#any_instance" do class CustomErrorForAnyInstanceSpec < StandardError;end let(:klass) do Class.new do def existing_method; :existing_method_return_value; end def existing_method_with_arguments(arg_one, arg_two = nil); :existing_method_with_arguments_return_value; end def another_existing_method; end private def private_method; :private_method_return_value; end end end -- it "raises instance of submitted ArgumentError" do error = ArgumentError.new("error message") @double.should_receive(:something).and_raise(error) lambda { @double.something }.should raise_error(ArgumentError, "error message") end it "fails with helpful message if submitted Exception requires constructor arguments" do class ErrorWithNonZeroArgConstructor < RuntimeError def initialize(i_take_an_argument) end end @double.stub(:something).and_raise(ErrorWithNonZeroArgConstructor) lambda { @double.something }.should raise_error(ArgumentError, /^'and_raise' can only accept an Exception class if an instance/) end -- module RSpec::Rails module Matchers end end begin require 'test/unit/assertionfailederror' rescue LoadError module Test module Unit class AssertionFailedError < StandardError end end end end require 'rspec/rails/matchers/have_rendered' require 'rspec/rails/matchers/redirect_to' require 'rspec/rails/matchers/routing_matchers' require 'rspec/rails/matchers/be_new_record' require 'rspec/rails/matchers/be_a_new' -- require 'active_support/core_ext' require 'active_model' module RSpec module Rails class IllegalDataAccessException < StandardError; end module Mocks module ActiveModelInstanceMethods # Stubs `persisted?` to return false and `id` to return nil # @return self def as_new_record self.stub(:persisted?) { false } self.stub(:id) { nil } self
method_missing の中で gsub 呼んだら無限ループに
method_missing って自分で試したことがなかったので、Eloquent Rubyの21章を参考に、オレオレ定義を書いてみた。
Eloquent Rubyのサンプルは英語で単語間の距離を発音の近さで判定するSoundexというものを使っていたけど、全く同じでもつまらないし、日本人が苦手な「LとR」、「VとB」の区別をなくして、String#capitarize とlとrを間違えて呼んだら、「もしかしてcapitalizeのこと?」と提示する、というのを書いてみた。
class Object def method_missing(method, *args) msg = "No such method: #{method}" if meth = similar_method(method) msg = "Did you mean?: #{meth}" else raise NoMethodError, msg end end def similar_method(name) method = public_methods.find do |m| accented(name.to_s) == accented(m.to_s) end end private def accented(word) word.tr('lv', 'rb') end end puts "hello".capitarize puts :world.pubric_methods puts 123.eben? puts "method".there_really_is_no_such_method
実行すると、これで確かにうまく行く。
$ ruby engrish_method.rb Did you mean?: capitalize Did you mean?: public_methods Did you mean?: even? engrish_method.rb:8:in `method_missing': No such method: there_really_is_no_such_method (NoMethodError) from engrish_method.rb:28:in `<main>' $
最初、thとsの音も同一視しようと思って以下のように書いたら、すごくよく分からない stack level too deep なエラーが出て、ちょっと悩んだ。
module Engrish THE_SAME = [ ['th', 's'], ['r', 'l'], ['b', 'v'] ] def accented(word) THE_SAME.inject(word) do |word, pair| word.gsub(*pair) end end module_function :accented end class String def method_missing(method, *args) msg = "No such method: #{method}" if meth = similar_method(method) msg = "Did you mean?: #{meth}" else raise NoMethodError, msg end end def similar_method(name) method = public_methods.find do |m| Engrish.accented(name.to_s) == Engrish.accented(m.to_s) end end end puts "hello".capitarize puts "method".pubric_mesod
何が起こってるのか分からなくて、あちこちに p を入れてみて気付いたのは、無限ループになっているのは、gsub に2つの引数を渡しているところだということ。
gsubは、
"hoge".gsub('o', 'a') => "hage"
のように文字列を2つ引数に取れるので、Engrish.accented はちゃんと動くように思える。実際、メソッドを単体で切り出して文字列を渡すと、メソッドの実装自体に問題はない。ただ、問題なのは gsub の暗黙の動作。
gsubには2つ目の引数としてハッシュを渡す用法もある。
"hoge".gsub(/[eo]/, 'e' => 'u', 'o' => 'a') => "hagu"
というように。
String#gsub に引数を2つ渡すと、CRubyは、まず2つ目の引数に対してハッシュとして振る舞うかどうかをチェックする。このとき、2つ目のオブジェクトに to_hash を投げているのが無限ループになった原因だったようだ。String#to_hash は存在しないので、ここで再び method_missig が呼ばれてしまう。
CRubyのstring.cでは、
3788 static VALUE 3789 str_gsub(int argc, VALUE *argv, VALUE str, int bang) 3790 { 3791 VALUE pat, val, repl, match, dest, hash = Qnil; 3792 struct re_registers *regs; 3793 long beg, n; 3794 long beg0, end0; 3795 long offset, blen, slen, len, last; 3796 int iter = 0; 3797 char *sp, *cp; 3798 int tainted = 0; 3799 rb_encoding *str_enc; 3800 3801 switch (argc) { 3802 case 1: 3803 RETURN_ENUMERATOR(str, argc, argv); 3804 iter = 1; 3805 break; 3806 case 2: 3807 repl = argv[1]; 3808 hash = rb_check_hash_type(argv[1]); 3809 if (NIL_P(hash)) { 3810 StringValue(repl); 3811 } 3812 if (OBJ_TAINTED(repl)) tainted = 1; 3813 break; 3814 default: 3815 rb_check_arity(argc, 1, 2); 3816 } 3817
となっている。3808行目の rb_check_hash_type の先で、convertを試みて、to_hash が呼ばれ、method_missingに再び戻るということが起こっている、ような気がする。単純な method_missing 遊びのつもりだったけど、やっぱり処理系の動作に手を入れるような手法って、なめたらアカンなーと思った。ちょっとした間違いでも原因特定が難しくなる。
しかし、良く分からん。
class String def method_missing(method, *args) msg = "No such method: #{method}" if meth = similar_method(method) msg = "Did you mean?: #{meth}" end raise NoMethodError, msg end def similar_method(name) puts name.to_s.respond_to?(:to_hash) # this is ok name.to_s.reverse end end "hoge".foobar
という風に respond_to? を投げれば問題はない。だけど、
puts name.to_s.to_hash
とやると stack level too deep で落ちる。
ということは、CRubyだって、respond_to? で to_hash が反応するかどうかを見ればいいだけのような気がする。どうしてそうなっていないんだろうか。rb_check_hash_type -> rb_check_convert_type -> convert_type -> rb_check_funcall -> check_funcall と見てみたけど、良く分からん。check_funcall って、respond_to? で呼ばれてる関数じゃないのかな。いや、Kernel#respond_to? は vm_method.c の obj_respond_to にバインドされていて、うーん……。と、ぼくにはとても追えない。
Rubinius のString#gsub実装はどうなってるんだろうかと思って kernel/common/string.rb を見てみたら、そもそもハッシュは引数として受け取れないことになっていた。そういうものなのか。
Rubyではメソッドの中で定数を変更できないのはflymakeもお見通し
Rubyではメソッドの中で定数を変更できない(リフレクション系のメソッドを使わない限り、という意味で)。これは動的にエラーや例外となっているのではなくて、パーズした段階で吐き出されるエラーのようだ。
MyConst = 2 def change_const MyConst = 3 end change_const
$ ruby constchange.rb constchange.rb:4: dynamic constant assignment MyConst = 3 ^
で、よく見ると、オレのEmacsのflymakeはちゃんと赤のアンダーラインを出してエラーを認識してた。
flymakeは裏で ruby -c を実行して文法をチェックしている。以下と同様。-cを付けるとRubyは実行をせずに文法エラーがないかだけをチェックする。
$ ruby -c constchange.rb constchange.rb:4: dynamic constant assignment MyConst = 3 ^
シンタックスだけで判定できるエラーだから当たり前か。
以下のようにするとシンタックスはオッケーでも実行するとビミョウに怒られる。このことからも、最初の例で単に定数を変更しようとしたから怒られたわけではなくて、Rubyでは文法としてメソッド内での定数の変更を許していないことが分かる。
MyConst = 1 MyConst = 2 def change_const MyConst == 3 end change_const
$ ruby -c constchange.rb Syntax OK $ ruby constchange.rb constchange.rb:2: warning: already initialized constant MyConst