camdez.com

Rule #1: There are no rules.

Ruby: Implementing Progn From Lisp

| Comments

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.

Background

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

1
2
3
(if (> x 0)
  x
  0)

If 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.

Enter progn:

1
2
3
4
5
(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: case forms, catch forms, when forms, 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…

1
2
3
4
5
6
7
8
def foo
  5 + 5 # doesn't matter
  2 + 2 # doesn't matter
  "horse"
end

> foo()
=> "horse"

…and Common Lisp’s…

1
2
3
4
5
6
7
(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 progn behavior.

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:

1
2
3
4
5
6
7
# 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:

1
2
3
4
5
6
x_or_zero = if x > 0
  x
else
  puts "Returning zero!"
  0
end

Implementing

Here’s the cool part. Implementing progn in Ruby is every bit as simple as:

1
2
3
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 do...end), 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:

1
2
3
4
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:

1
2
3
((lambda ()
  (print "doing it")
  42))

…and beyond

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 progns)…

1
2
3
(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:

1
2
3
4
> (progn 1 2 3)
=> 3
> (prog1 1 2 3)
=> 1

In Common Lisp we could implement prog1 like this5, using macros :

1
2
3
4
(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 x.

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 prog9? Or 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 prog1, prog2, prog9, and 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…

1
2
3
4
5
6
7
(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:

1
2
3
4
5
(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:

1
2
(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.


Footnotes

  1. It’s interesting to consider why progn exists because it says something about the nature of Lisp; if Lisp were a truly functional programming language then progn would 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.
  2. The else-form is optional. (if confused (study lisp)).
  3. Or with a lambda, but it breaks the return semantics such that it only exits from the lambda and not from the containing method.
  4. Again, assuming I haven’t missed anything subtle, like non-local—exit behavior.
  5. 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) prog1 seems to throw an error for empty lists whereas my-prog1 doesn’t, and (2) my-prog1 leaks the x variable into the execution context. In reality you’d definitely want to use gensym to avoid this leak:
1
2
3
4
5
(defmacro better-prog1 (&rest body)
  (let ((car-value-name (gensym)))
    `(let ((,car-value-name ,(car body)))
       ,@(cdr body)
       ,car-value-name)))

Comments