DEV Community

Masataka Pocke Kuwabara
Masataka Pocke Kuwabara

Posted on • Originally published at pocke.hatenablog.com

Never Escape This Ruby Loop. Also, Here's How.

This is a self-translation of an article I originally wrote in Japanese.

Hello. I'm Pocke. In this article I'll show you how to define a loop in Ruby that you can never escape from.

A loop you can escape

Ruby has the Kernel.#loop method. It's a method that evaluates its block over and over, forever.

# "hello" is printed forever
loop do
  puts "hello"
end
Enter fullscreen mode Exit fullscreen mode

Naturally, you can also break out of loop.

# Prints "hello" once, then leaves the loop
loop do
  puts 'hello'
  break
end

# Likewise, prints "Ruby" once and then leaves the loop.
# Calling `return` inside a method exits the method itself.
def f
  loop do
    puts 'Ruby'
    return
  end
end
f

# Prints "world" once, then leaves the loop by raising an exception
# (the program terminates abnormally)
loop do
  puts 'world'
  raise 'bye!'
end
Enter fullscreen mode Exit fullscreen mode

A loop you can't escape

Now, unlike Kernel.#loop, let's think about a loop that you can't escape even with break, return, or raise. Surprisingly, Ruby lets you define such a method. Calling it infinite_loop, here's what it looks like: 1

def infinite_loop(&block)
  tap do
    yield
  ensure
    redo
  end
end

# "hello" is printed forever
infinite_loop do
  puts 'hello'
  break
end

# "Ruby" is printed forever.
# You can't get out of the method.
def f
  infinite_loop do
    puts 'Ruby'
    return
  end
end
f

# "world" is printed forever.
# The exception never surfaces.
infinite_loop do
  puts 'world'
  raise 'bye!'
end
Enter fullscreen mode Exit fullscreen mode

If you run this code, you'll see that you can't escape the loop even though you're calling break and friends. The part where the same method's code runs again even after you return is especially fun.

One warning before you try it: this program can't even be stopped with Ctrl-C. You'll need kill -9.

How the inescapable loop works

Here's the infinite_loop method again:

def infinite_loop(&block)
  tap do
    yield
  ensure
    redo
  end
end
Enter fullscreen mode Exit fullscreen mode

It's beautiful how everything except tap is made of keywords.

Here's what happens, step by step:

  1. infinite_loop is called.
  2. Kernel.#tap is called.
  3. Kernel.#tap runs the block it received.
  4. The yield inside that block runs, which runs the block passed as the block argument.
    • This is where the caller of infinite_loop runs whatever code they want.
  5. After returning from block, control always moves to the ensure clause.
    • This is the key. ensure exists precisely to write code that always runs, so it is evaluated no matter whether an exception was raised, or break, or return was used.
  6. redo is evaluated in the ensure clause.
    • redo jumps back to the very beginning of the tap block's evaluation. In other words, it goes back to just before step 3, and we loop.

So, by taking advantage of the fact that ensure is always evaluated, we managed to implement a loop with no escape.

How to escape the inescapable loop

Just as there is no shield that can block every spear2, there are ways out of this loop. Let's look at a few.

Kernel.#exit!

Let's start with the boring one. Calling exit! gets you out easily.

def infinite_loop(&block)
  tap do
    yield
  ensure
    redo
  end
end

infinite_loop do
  puts 'hello'
  exit!
end
Enter fullscreen mode Exit fullscreen mode

Running this prints "hello" exactly once and then the program terminates abnormally. That's because exit! terminates the process on the spot.

Note that Kernel.#exit will not get you out of this loop, because exit merely raises an exception.

TracePoint

You can also escape using TracePoint.

The heart of this loop is that the ensure clause always runs. But ensure has a weak spot too: if an exception is raised in the ensure clause, or a return happens there, that same ensure clause is not evaluated again.

def f
  raise 'ex 1'
ensure
  raise 'ex 2'
end

f
Enter fullscreen mode Exit fullscreen mode

When you run this, the ex 1 exception sends us into the ensure clause, but the ex 2 exception does not send us back into that same ensure clause. If it did, you'd get an infinite loop trivially.

We can exploit this: if we raise an exception or return from inside infinite_loop's ensure clause, we can leave the loop. Concretely:

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'
Enter fullscreen mode Exit fullscreen mode

Running this prints "hello" once, escapes the loop, and then prints "world" outside of it.

This works by using TracePoint's line event to return on line 5, where the redo in the ensure clause lives. You could raise instead of return and still escape, but return feels cleaner since no exception is thrown.

Wrapping up

That was an inescapable loop, and some ways to escape it. If you come up with other interesting ways out, please let me know!


  1. My original implementation of infinite_loop used recursion and blew the stack, but tompng came up with a much sturdier version using redo. tompng is amazing. 

  2. There's an old Chinese story about a merchant selling both a spear that pierces any shield and a shield that blocks any spear. When a customer asked what would happen if he threw the one at the other, the merchant had no answer. The Japanese word for "contradiction", mujun, is written with the characters for spear and shield. 

Top comments (0)