Ruby’s Most Underused Keyword

redo.pngI 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

Golf Time

Here is a Ruby golf riddle, which I have no idea how could be made this short (42 characters) without redo (via coderrr):

"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 :-) ).

(0..9).map{rand(?z).chr[/[^_\W]/]||redo}*""

The above solution might not be instantly crystal clear – so let’s break it down a bit:

  1. rand(?z) is equivalent to rand(122) (but shaves off a character!)
  2. rand(?z).chr turns the random number into a string (equivalent to its ASCII code, i.e. 65.chr == “A” etc)
  3. 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)
  4. 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.
  5. 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.

Conclusion

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 :-)

43 thoughts on “Ruby’s Most Underused Keyword

  1. I recently needed to do a complex iterate/delete sequence: given an array of chains, delete all the chains that are contained in other chains. Normally I’d attack this with recursion, but I had just seen the redo tail-recursion article and wondered if it was possible to do using redo.

    If you delete an element while iterating, say at step 4, element 5 will move up to step 4′s place. When you iterate to the next number, 5, you will have skipped the element that just moved up to 4. My final code ended up with this nugget in it:

    chains.delete_at(i)
    chain = chains[i]
    redo unless chain.nil?

    It works quite nicely. :-)

  2. Your code sample appears to have a minor transcription error. The final character should be replaced with two quotation marks (single or double will work).

  3. Thanks Jeff, fixed. In the code it was correct btw (two single quotation marks) but the code highlighter decided to mess it up ;) Replacing the single quotes with double ones helped…

  4. Redo would be about 5x more awesome if it took arguments that would become the new block parameters. The “assign, then redo” idiom is a bit ugly.

    Still, a good trick to be aware of.

  5. I do in C style languages like

    for (var i=1;i<10;i++)
    {
    snibble();
    snout(i);
    if(youwantto_redo==true)
    {i=i-1;}
    }

  6. The fib method (both Magnus’s and yours) return wrong results. It gives me fib(10) => 89, but fib(10) == 55. I see this error quite a lot when people try to write Fibonacci number functions, not sure why. Chart with results for verification: http://en.wikipedia.org/wiki/Fibonacci_number

    Anyway, Magnus noted that the performance difference between the redo version and the “iterative” version is almost nothing. That’s because the redo version is an iterative solution.. it’s just using a different tactic (a bit like using a while loop to count instead of a for loop.. same result, just some different keywords).

    If you strip out all the cruft, you end up with this:

    def fib(i)
    n, result = 1, 0
    while i != -1
    i, n, result = i – 1, n + result, n
    end
    result
    end

    .. which is clearly iterative (note: I’ve implemented this to match the results of your methods).

    While redo is pretty cool, the whole Fibonacci example seems bogus to me. redo is not a third way, in this situation it’s just a crufty way of producing an iterative solution.

  7. PeterC: Hehe, it’s so funny when one is blogging about some obscure advanced feature and he doesn’t get fibonacci right ;-) even if its me in this case :-) (though I just copied Magnus’s solution and wrapped into a lambda – I ran it, but since I don’t know fib(1000) by heart, it was not apparent that it’s not working :-) ).

    But yeah, I get your point and you are right to some extent, but I still think the example makes sense if you are viewing the progress how we get there – Magnus’s article is much better in this regard, since it tries to simulate tail recursion optimization and arrives at redo. If the original question would have been ‘is it possible to implement fibonacci numbers iteratively’, no one would even think about reaching after redo. However, if the question is to write ‘tail optimizable recursion’ (ok, I dig it that this was kind of a non-sense :-) I think the solution is valid.

    Anyway, AFAIK 1.9 does tail recursion optimization, so it doesn’t matter any more.

  8. PeterC: Frankly, I don’t care if it’s broken, that wasn’t my point :-)

    From an interpreter’s point of view, you are completely right. That’s why it’s so fast. However, from a user’s POV it’s not an iterative solution. The redo simply says “run this block (aka method) again”; exactly like acc() would do.

  9. [..] from a user’s POV it’s not an iterative solution. The redo simply says “run this block (aka method) again” [..]

    So redo is being used to implement a loop (i.e. run this [whatever] again). That’s iterative, right? :) It’s a kinda cool experiment to do loops that way, but, of course, there are already nicer/faster ways to loop, such as while..end, loop..end, iterators, and so forth. Using redo in this way is a modern equivalent of “goto”.

    Or are you suggesting the average coder would look at that implementation with redo and mistakenly believe it was recursive? I’d really like to hope they wouldn’t, but you never know!

    Anyway, AFAIK 1.9 does tail recursion optimization, so it doesn’t matter any more.

    Unfortunately not. From vm_opts.h on Ruby 1.9.1-p0:

    define OPTTAILCALLOPTIMIZATION 0

    However, a comment in the source claims you can turn it on where necessary (although it is claimed odd bugs can emerge) using VM::CompileOption, but I am not familiar with this.

  10. Huy: not at all – following this logic you can say the same about next or break. Yeah, as PeterC stated above, it can be used as a kind of goto in some cases, but in general, that’s not true.

  11. Huy: I agree with peter on this one. All jumps or branches or looping or subroutine/function/method calls and returns (and many other language idioms) can scream “goto,” if you squint hard enough.

    The main historically recognized problem with goto was that it was too generic. Language designers come up with richer, more expressive, and more specific ways to indicate the flow of execution, and gotos become unnecessary and counterproductive. Redo is way more specific and expressive than goto, and as peter mentions, is essentially just as specific as next and break.

  12. Also, I wanted to thank Peter Szinek for mentioning this specific Ruby golf problem. I had a great deal of fun playing with it this morning, and posted about it over at coderrr.

  13. No, it’s better than that ;-) The equivalent of continue is next; there is no redo() equivalent in C.

  14. I think all 3 of these keywords are bad and here’s why: It makes it hard to refactor your code. Look at this pseudo code:

    for (…)

    if done

    break
    end if
    if goToNext

    next
    end if
    if restartBlock

    redo
    end if

    }

    Now, let’s say the “if done” block gets so big that you want to extract a method out of it:

    myIfDoneMethod

    break
    end method

    Oops, this won’t work… “break” makes no sense in a method without a loop. So now you’ve gotta change the loop and the method to have a condition that simulates “break”. If you wrote your loop with the condition in the first place, you wouldn’t have to deal with this.

  15. Pingback: Ennuyer.net » Blog Archive » 2009-02-08- Today’s Ruby/Rails Reading

  16. Here’s how I once used redo. To generate a feed of user’s activity, I wanted to have an item like “posted 3 videos: x y z”. In order to achieve that, I’d loop over a mixed up array of all kinds of activities… Once I begin catching videos, I’d detect hitting a non-video item, render the partial for them and redo.

  17. Pingback: citizen428.blog()

  18. Pingback: Ruby, Rails, Web2.0 » Blog Archive » Ruby’s Most Underused Keyword | Web2.0时代

  19. Pingback: Bookmarks for marzo 11th from 02:00 to 02:00 | Bloggerman

  20. Evaluate your eating habits regularly to ensure you following the plan correctly. VLCD requires following a very strict diet plan with a limited list of foods in the protein, vegetable, and fruit groups.

  21. Firstly, let me commend your lucidity on this topic. I am not expert on this topic, but after reading your write-up, my understanding has developed nicely. Please allow me to snap up your nourish remain in contact with any long term revisions. Achieved career and can extend it on to pals and my followers.

  22. Opposite to some notions, good researched articles still fetch in reviewers like me. You paraded wide understanding of the theme issue and my sights are now total after reading this post. Please sustain up the efficient work and i’ll subscribe to your rss or atom nourish to be enlightened of any long term postings.

  23. Tremendous post you’ve scored proper here! The web is overflowing of horrid composing and I used to be grabbed by your limpidity. Your closings are exact and I’ll straightaway subscribe to your rss or atom feed stay up to date alongside with your up future day postings. Yes! I admit it, your authorship style is unique and i work harder on mine.

  24. I won’t debate with your decisions because I think you are right on your money! You’ve place together a consistent situation for your sentiments and now I know much more about this unusual topic. Gives Thank you for distinctive publish and i restarted for more.

  25. Whole Handle Advertising Review- Excellent piece of particulars that you?ve received on this web page post. Hope I could perhaps get some a lot a lot more on the things on your personal web website. I will arrive once more.

  26. Grrr, I will print this one with regard to offline reading. Didn’t finish reading this article one due to time restriction but, I commented the following to let you know that you delivered a new straight to the point writing.

  27. I am sure this article has touched alll the internet users, its really really fastidious artucle on building up new
    web site.

    my weblog … iphone 5 bumper case (Anthony)

  28. Microsoft has long believed that it has the ability to
    turn the télécharger émulateur xbox 360 (ufodigest.com)
    into a the ultimate entertainment center and this has been witnessed with a slew of TV,
    movie, and music streaming services making their way onto the current
    console. The Microsoft connect is a fantastic piece of hardware
    and one that should be a lot of fun for games but for a large part of
    the computer world when they see a piece of technology like the Microsoft Kinect the question is not what will developers do with it, but what can I do with it.
    Notable examples of this behavior include Square Enix’s re-release of several
    older Final Fantasy titles on the Play – Station, Game Boy Advance, and DS; Sega’s collections of Sonic the Hedgehog games.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>