こんにちは。 id:Pocke です。Rubyで抜け出せないループを定義する方法を紹介します。
抜け出せるループ
Ruby にはKernel.#loopメソッドが定義されています。これはブロックの評価を永遠に繰り返すメソッドです。
# 永遠にhelloが出力される loop do puts "hello" end
loopメソッドは当然抜け出すこともできます。
# helloを1回出力した後ループから抜ける loop do puts 'hello' break end # 同様にRubyを1回出力した後ループから抜ける # メソッド内で`return`を呼ぶことで、メソッドごと抜け出すのに使われる def f loop do puts 'Ruby' return end end f # worldを1回出力した後、例外を出してループから抜ける(プログラムは異常終了する) loop do puts 'world' raise 'bye!' end
抜け出せないループ
では、Kernel.#loopとは違い、breakやreturn, raiseを使っても抜け出せないループを考えてみましょう。Rubyでは、なんとそのようなメソッドが定義できてしまいます。それをinfinite_loopメソッドとして定義すると、以下のようになります。1
def infinite_loop(&block) tap do yield ensure redo end end # 永遠にhelloが出力される infinite_loop do puts 'hello' break end # 永遠にRubyが出力される。 # メソッドを抜け出せない。 def f infinite_loop do puts 'Ruby' return end end f # 永遠にworldが出力される。 # 例外は出力されない。 infinite_loop do puts 'world' raise 'bye!' end
このコードを実行すると、breakなどをしているにも関わらず、ループから抜け出せないことが分かると思います。returnしたにも関わらずまた同じメソッドのコードが呼ばれるところなどはとてもおもしろいですね。
なお実行の際はこのプログラムはCtrl-cでも止められず、kill -9で止める必要があることに注意してください。
抜け出せないループの仕組み
infinite_loopメソッドのコードを再掲します。
def infinite_loop(&block) tap do yield ensure redo end end
tapメソッド以外キーワードで構成されていて美しいですね。
このメソッドの挙動を解説すると、以下のようになります。
infinite_loopメソッドが呼ばれるKernel.#tapメソッドが呼ばれるKernel.#tapメソッドは、それが受け取ったブロックを実行する- ブロック内の
yieldが実行され、block引数のブロックが実行される- ここで
infinite_loopメソッド利用側が任意の処理を実行する
- ここで
blockの実行から戻った後、常にensure節に進む- これが肝。
ensure節は常に実行するコードを書くための構文であるため、例外が上がろうがbreakしようがreturnしようが常に評価される
- これが肝。
ensure節でredoが評価されるredoはtapメソッドの評価の一番先頭まで戻る。つまり、3の直前まで戻り、ループする。
このように、ensure節が必ず評価されることを利用して、抜け出せないループを実装することができました。
抜け出せないループを抜け出す方法
どんな矛も防ぐ盾が存在しないように、このループを抜け出す方法は存在します。 その方法をいくつか見てみましょう。
Kernel.#exit!
まずはつまらない方法から。exit!メソッドを呼ぶと簡単に抜け出せます。
def infinite_loop(&block) tap do yield ensure redo end end infinite_loop do puts 'hello' exit! end
このコードを実行すると、helloを1回だけ出力して、プログラムが異常終了します。これはexit!メソッドがその場でプロセスを終了させるためです。
なお、Kernel.#exitメソッドではこのループは脱出できません。exitメソッドは単に例外を投げるメソッドであるためです。
TracePoint
TracePointを使うことでもこのループを抜けだせます。
このループの肝はensure節が必ず実行されることでした。そのensure節にも弱点はあります。ensure節で発生した例外やreturnによって、そのensure節が新たに評価されることが無いことです。
def f raise 'ex 1' ensure raise 'ex 2' end f
上のコードを実行すると、ex 1の例外が起きたことによりensure節に入りますが、ex 2の例外によって再度このensure節に入ることはありません。再度ensure節に入ってしまったら簡単に無限ループが起きてしまいます。
これを応用して、infinite_loopメソッドのensure節の中で例外を起こしたりreturnしたりすればループを抜けられます。具体的には次のようなコードになります。
def infinite_loop(&block) tap do yield ensure redo end end infinite_loop do puts 'hello' TracePoint.trace(:line) { it.binding.eval 'return' if it.lineno == 5 } end puts 'world'
このコードを実行すると、helloを1回だけ出力してループを抜け出し、その後ループの外でworldを出力します。
これはTracePointのlineイベントを使って、ensure節のredoがある5行目でreturnすることで実現しています。returnの代わりにraiseをしてもループを抜け出せますが、例外が上がらないreturnのほうがきれいな感じがしますね。
最後に
抜け出せないループと、それを抜け出す方法の紹介でした。この他にも面白いループの抜け出し方を思いついたらぜひ教えてください。