Ruby: Implementing progn from Lisp
While hacking on some Ruby code today I started to miss
progn from my Common Lisp programming days. If you’re not familiar with
progn, it’s a special form which evaluates all of the contained expressions and returns the value of the last one.
progn construct is more important in the (more-or-less) functional1 programming world than in the imperative world because it allows us to insert multiple expressions where, syntactically, we could otherwise only insert one.
Let’s start with a trivial example:
(if (> x 0) x 0)
x is greater than zero, return
x, else return 0.
But what if we also wanted to output a message whenever
x is not greater than 0? We can’t just add our printing line in before (or after)
0 because the compiler knows what’s what based on the positions of the subexpressions in the
if expression: it has to be
(if test-form then-form [else-form]) 2.
(if (> x 0) x (progn (print "Returning zero!") 0))
Boom! It works. How very exciting. Ok, maybe not. In fact, I called
progn a “special form” earlier—which basically means that it’s something which defies the basic evaluation rule of the language—but the truth is it’s probably the least special of the special forms. In fact, implicit progns are all over the place in Lisp:
let forms, but most importantly, function bodies.
And now we’re getting to Ruby because it’s basically the same situation: Ruby implicitly returns the value of the last expression evaluated within a method body. Which is to say that Ruby’s…
def foo 5 + 5 # doesn't matter 2 + 2 # doesn't matter "horse" end > foo() => "horse"
…and Common Lisp’s…
(defun foo () (+ 5 5) ; doesn't matter (+ 2 2) ; doesn't matter "horse") > (foo) => "horse"
…are pretty much exactly the same thing. They both implicitly exhibit
At this point you’re likely wondering why the hell I would want
progn in Ruby. And the simple answer is it makes things nice and clean. For instance:
# Bob has 13 points, Wil has 42 points, and anyone else has 0. scores = progn do h = Hash.new(0) h['Bob'] = 13 h['Wil'] = 42 h end
Do some work, build up the value I really want, assign it. Because Ruby has block-scoping,
h (as such) doesn’t exist anymore. That’s not critical here, but in general that means that we can introduce whatever (new) intermediate variables we want and not pollute the enclosing scope. More importantly, though, it’s exceedingly obvious where
scores gets its value—the code is offset in its own little block.
It’s not life-changing, but hey, it’s cool. And it’s cool that Ruby allows us to do this. By blurring the traditional lines between expressions and statements, Ruby allows us to be more Lisp-like when it suits us:
x_or_zero = if x > 0 x else puts "Returning zero!" 0 end
Here’s the cool part. Implementing
progn in Ruby is every bit as simple as:
def progn yield end
Yep, really. We’re exploiting Ruby’s implicit
progn behavior but making it explicit. We
yield to whatever block is passed in (defined by
yield returns the value of its last expression and the
yield itself is the last expression in the
progn method. Even
return works like you’d expect because we’re returning from a ‘raw’ block.
Ruby experts: is there a better way? Am I missing some obvious way to create and evaluate a literal block?
Yes, it can be done with a
Proc3, like so:
foo = Proc.new do puts "doing it" 42 end.call()
But I hardly think that’s an improvement stylistically. It’s a bit more tolerable with Lisp’s minimalistic syntax:
((lambda () (print "doing it") 42))
progn gets its name from the fact that it’s a derivative of the “program feature” (
prog), combined with the fact that it returns the value of the n-th subexpression.
It’s quite easy4 to implement
progn with Common Lisp macros (again, taking advantage of existing implicit
(defmacro my-progn (&rest body) `(let () ,@body))
…but Common Lisp also provides us with the
prog1 form (and, indeed,
prog2), which does rather what you’d expect: it evaluates a series of expressions and returns the value of the first expression:
> (progn 1 2 3) => 3 > (prog1 1 2 3) => 1
In Common Lisp we could implement
prog1 like this5, using macros :
(defmacro my-prog1 (&rest body) `(let ((x ,(first body))) ,@(rest body) x))
We can do this because Lisp allows us to treat the code inside of the macro form as data—a list, to be precise. We take off the first element of the list, evaluate it, save that value away as
x, execute the tail of the list, and return
But as far as I know, the
prog1 form is impossible to write in Ruby because it’s impossible to “open up” a block/Proc/lambda and rewrite its constituent code. I bring this up not as a complaint about the Ruby language, but rather as a point of exploration for understanding.
Correct me if I’m wrong!
Get crazy with the Cheez Whiz
Well…since we’re already so far down the rabbit hole, let’s take it a step further: what if I want
prog42? There’s surely no healthy use for these, but then again I’m sure you’ve had a manager ask you for even more useless things, so indulge me, will you?
Like good coders we’re not going to write distinct
prog42 macros—we’re going to abstract. We’re going to write a
prog variant that takes a value
n, and returns the value of
n-th subexpression. Hell, we’d call it
progn, but it seems that name’s taken…anyway, here goes…
(defmacro progx (x &rest body) (let ((ret-value-name (gensym)) (pos (- x 1))) (setf (nth pos body) `(setf ,ret-value-name ,(nth pos body))) `(let (,ret-value-name) ,@body ,ret-value-name)))
Got that? We replace the expression at index
x - 1 (because of zero indexing) with a
setf call that will store away the expression’s value after it runs. Then we simply splice the modified list of expressions into a
let form that will eventually return the value we saved away.
Now it’s easy to whip up those forms the pointy-haired boss demanded:
(defmacro prog9 (&rest body) `(progx 9 ,@body)) (defmacro prog42 (&rest body) `(progx 42 ,@body))
Note that these need to be macros (not functions), otherwise their arguments will get evaluated before
progx gets a shot at them.
One last thing: for uniformity’s sake let’s go back and update
my-progn to use our newly abstracted code:
(defmacro my-progn (&rest body) `(progx ,(length body) ,@body))
Done and done. If you made it this far, go grab a beer—you deserve it.
(defmacro better-prog1 (&rest body) (let ((car-value-name (gensym))) `(let ((,car-value-name ,(car body))) ,@(cdr body) ,car-value-name)))
It’s interesting to consider why
prognexists because it says something about the nature of Lisp; if Lisp were a truly functional programming language then
prognwould be precisely useless. Since pure functional programming allows no side effects, only the final form (the one which defines the value returned) would be worth evaluating. The only exception to this would be if the earlier forms caused the program to never terminate. ↩︎
(if confused (study lisp)). ↩︎
Or with a
lambda, but it breaks the
returnsemantics such that it only exits from the
lambdaand not from the containing method. ↩︎
Again, assuming I haven’t missed anything subtle, like non-local–exit behavior. ↩︎
This implementation is actually a bit weak but I wanted to keep it simple for learning purposes. It has two issues that I see: (1)
prog1seems to throw an error for empty lists whereas
my-prog1doesn’t, and (2)
xvariable into the execution context. In reality you’d definitely want to use gensym to avoid this leak: ↩︎