I spent the first 2+ years in Ruby-land without even knowing about the probably most underused (and underrated) keyword of the language: redo. Even after I came across the first example and liked it immensely, I could not come up with another use for it, so I threw it to the bottom of my toolbox. Then I found another example, and another one – so I came to the conclusion that redo might be a valuable keyword in your Ruby arsenal after all – it is one of those things which you rarely need, but if you need it, it’s a perfect solution which would be cumbersome to replace with other constructs.
Break vs Next vs Redo
I am sure everyone is familiar with redo’s cousins: next and break. While next and break allow you to skip an iteration, resp. skip the rest of the remaining iterations altogether, redo does not skip anything – it ‘restarts’ the same iteration. In other words, while next transfers control to the end of the iteration block, redo ‘jumps’ to the beginning of the block. Let’s see an example, where redo is used to recover from an input error (code from “The Ruby Programming Language”):
puts "Please enter the first word you can think of" words =%w(apple banana cherry) response = words.collect do |word| print word + "> " response = gets.chop if response.size == 0 word.upcase! redo end response end
The trick is that whenever the user enters an empty string, the block is restarted with redo, asking the same ‘question’ until the user enters something that makes sense to the system.
A Real Example – Tail Recursion
As of now, Ruby does not support tail recursion optimization, so you either have to write the function in a non-recursive (i.e. iterative) way yourself, or suffer the consequences of a full stack… or use redo!
I recently came across Magnus Holm’s article on this problem. Magnus provides an elegant solution using redo:
define_method(:acc) do |i, n, result| if i == -1 result else i, n, result = i - 1, n + result, n redo end end def fib(i) acc(i, 1, 0) end
Magnus even worked around a redo perk: “The redo statement restarts the current iteration of a loop or iterator”. That means he couldn’t define a method with “def”, since that neither involves a loop nor an iterator. Maybe a less esoteric way relying on the same fact would be to use a lambda:
def fib(i) acc = lambda do |i, n, result| if i == -1 result else i, n, result = i - 1, n + result, n redo end end.call(i, 1, 0) end
"Generate a random string of length 4 or more consisting of only the following set of characters: a-z, A-Z, 0-9"
Here is the winner, Magnus Holm‘s solution (The name sounds familiar? Right, the previous example was from him too – he understands the power of redo for sure :-)).
The above solution might not be instantly crystal clear – so let’s break it down a bit:
- rand(?z) is equivalent to rand(122) (but shaves off a character!)
- rand(?z).chr turns the random number into a string (equivalent to its ASCII code, i.e. 65.chr == “A” etc)
- the regexp /[^_\W]/ means the negation of ‘everything but underscore or non-word character (digits and numbers). To put it another way, accept word characters but not an underscore. (\w includes underscore, so we need to get rid of that)
- rand(?z).chr[/[^_\W]/] acts as an identity function for letters and digits, but returns nil for everything else. So “a”[/[^_\W]/] == “a” but “@”[/[^_\W]/] == nil. “_”[/[^_\W]/] also returns nil.
- And now comes the redo trick: rand(?z).chr[/[^_\W]/] || redo will restart the block until a random character/letter is not found! w00t!
While one might argue that golfing examples are contrived by definition, I don’t always agree: the [/[^_\W]/] is a neat trick to get numbers/letters from a string only, as well as the use of redo is valid and non-contrived.
As demonstrated above, redo can be used in a variety of situations to produce an elegant solution that would look much more cumbersome without it. Of course all the usual stuff applies: don’t overuse, abuse etc. etc.
If you have any other interesting cases for redo, leave a comment – I’d love to see some more 🙂