Skip to main content

Ruby: An Interesting Block Pattern

Ruby has blocks, which enable all sorts of interesting idioms. I'm going to show one that will be familiar to Rails enthusiasts, but was new to me.

I was reading some code in a book, and it had the following:
def if_found(obj)
if obj
yield
else
render :text => "Not found.", :status => "404 Not Found"
false
end
end
Here's how you call it:
if_found(obj) do
# We have a valid obj. Render something with it.
end
The code in the block will only execute if the obj was found. If it wasn't found, the response will already have been taken care of.

I've been in the same situation in Python (using Pylons), and I coded something like:
def handle_not_found(obj):
if not obj:
return render_404_page()
return None
Here's how you call it:
response = handle_not_found(obj)
if response:
return response
# Otherwise, continue normally.
Pylons likes to return responses, whereas render in Ruby works as a side effect whose return value isn't important. However, that's not my point.

My point is that the Python code uses "if response:" whereas the Ruby code uses "if_found(obj) do". Python uses an explicit if statement, whereas Ruby hides the actual if statement in a block. Similarly, Rubyists tend to write "my_list.each do |i|..." (even though Ruby has a for statement), whereas Pythonistas use "for i in my_list".

Ok, now that I've totally made a mountain out of a molehill, please note that I'm not saying either is better than the other. I'm just saying it's interesting to note the difference.

Comments

You know, actually, this reminds me of the maybe monad in Haskell. In Haskell, there's this thing called the maybe monad that basically says, "I might have something, or it might be null." When you operate within the context of the maybe monad, if you actually have something, it runs your code. If you don't have something, it just ignores your code. That way you never get an exception for having a NULL pointer when you weren't expecting one.
Anonymous said…
Here is a shorter Version:
def handle_not_found(obj):
return False if obj else render_404_page()
Ian Bicking said…
Actually I'd expect a Python system to use an exception (some form of HTTPNotfound), and to do so at the point of fetching. I.e., "obj = get_obj_from_db(id)" and that method might raise an exception. This is in part because returning None for not-found isn't that common in Python. OTOH, typically a fetching method would raise LookupError or something, which doesn't itself render as a response. Django actually builds that exception in directly, I think (at least for one of its fetching methods)

I've also seen decorators that do this, populating the function signature with the database object and returning 404 if it can't.

There is a funny sort of contract to the Rails one, which is that all your output goes in that block. If you don't put all your output in the block then it looks like you could get a weird response with a mixture of 404 and normal output. Exceptions are more atomic in this sense.
Bill Mill said…
Well I had two points, the first one was that it was a place where the maybe monad is very clean and awesome, and that I would leap before I looked in Python and handle an exception that occurred.

But you and Ian got there before I could. So. Yeah!
Ian, I agree with all your points.
> Actually I'd expect a Python system to use an exception (some form of HTTPNotfound), and to do so at the point of fetching

I've actually been in situations where I wanted the handle_ function to interrupt and take over processing. I was trying to use Paste's exception mechanism to raise an exception with an HTTP 200. That was weird.

> There is a funny sort of contract to the Rails one, which is that all your output goes in that block.

Yep, that's the point of my post.
Anonymous said…
even shorter...

obj and do_something()
This is a maybe monad :-)

If you prefer an OO Syntax, the andand gem lets you write foo.andand do...end as well as a few other forms. Ruby Facets contains a nearly identical implementation called ergo.

I use the and keyword for some of this stuff as well, but only when don't need to recompute an expensive operation. So I do not write:

Person.find(...) and Person.find(...).title

Also of interest:

Writing Self-Confident Code

JM2C.
J Kenneth King said…
i'm not really a fan of the exception stack approach to error handling in languages like python (even though I build a lot of software in it). if you get to the top of that stack there's not a whole lot your program can do. i prefer lisp in this way -- restarts are very handy.

continuations, another lispy feature, seem to be the inspiration of the 'with' protocol being introduced in py3k. that should really clean up this kind of scenario where one is constantly checking for NULL by hiding the exception stack in the objects themselves.

no more dealing with leaky object data in the application!
> i prefer lisp in this way -- restarts are very handy.

Heh, I was talking about this with someone just yesterday; i.e. that Lisp provides a different way of handling errors using continuations that let you start right where you left off when something goes wrong.