Raku's surprisingly good Lisp impression

Lisp1 is famous for having pretty much no syntax. Structure and Interpretation of Computer Programs – arguably the most well known intro to programming in Lisp – presents pretty much the entirety of the syntax in its first hour. And that's by no means the only thing SICP does in that hour.

Raku, on the other hand, has a bit more syntax.

Ok, that's an understatement. Raku is syntactically maximalist to exactly the same degree that Lisps are syntactically minimalist. Forget “syntax that fits on a postcard”; Raku's syntax struggles to fit on an A4 sheet of paper. Raku has the type of syntactic riches that inspire Rakoons to classify its operators into beautiful (though now sadly dated) Periodic Tables.

I'm not saying that Raku has too much syntax; I think it has an amount perfectly suited to its design goals. Raku embraces the power of syntax; it's learned from natural languages that the ability to chose between different ways of articulating the same basic thought gives languages expressive clarity – and no small amount of beauty. Raku's syntactic profusion is a big driver of its flexibility: Raku has been described as “multi-paradigm, maybe omni-paradigm”, and its syntax sure helps make it so.

And, though I don't make a secret of how powerful I find Raku's maximalist approach, I also wouldn't say that Lisps have too little syntax. There's something wonderful about working through The Little Schemer and growing a language one step at a time. More practically, starting from a minimal base makes the practice of building your own language on top – aka, Language Oriented Programming – all the more tractable.

So I'm not really arguing for Lisp's syntactic minimalism or for Raku's maximalism. I'm just pointing out that they're very different aesthetics. If there's a spectrum, Lisp and Raku are on pretty much opposite ends of it.


And yet.

And yet it's possible to write Raku in a style that looks tremendously like Lisp – much more like Lisp than most languages could manage. To see what I mean, lets start with some especially Lispy code. Here's a toy program (pulled from the Racket docs) that recursively calculates whether a positive integer is odd.

(letrec ([is-even? (lambda (n)
                       (or (zero? n)
                           (is-odd? (sub1 n))))]
         [is-odd? (lambda (n)
                      (and (not (zero? n))
                           (is-even? (sub1 n))))])
    (is-odd? 11))
#t

What makes this code so distinctively Lispy? Lets count:

  1. mutually recursive definitions – we use letrec to define is-even? in terms of is-odd?, and also to define is-odd? in terms of is-even?; neither can be completely defined without the other.
  2. Prefix precedence – even operations (like subtraction or and) that would involve infix operators in other languages are done with prefix function calls.
  3. Recursion all the way down – this code never defines "even" with anything as simple as "is divisible by 2"; it just walks our n down to 0, and then resolves the nested recursion to get an answer.
  4. Semantic indentation – lines aren't indented by a fixed amount (like 4 spaces per block), but rather are indented to line up in semantically meaningful ways with the line above.
  5. Last but not least, parentheses. So many parentheses. (Admittedly, in Racket some of them are square, but that doesn't actually matter; […] is interchangeable with (…).)

So, let's see how Raku looks if it embraces that style. This is about matching the aesthetic, so we're not interested in nearly built-in "solutions" like 11 !%% 2. Just to have everything together, here's the Lisp again:

(letrec ([is-even? (lambda (n)
                       (or (zero? n)
                           (is-odd? (sub1 n))))]
         [is-odd? (lambda (n)
                      (and (not (zero? n))
                           (is-even? (sub1 n))))])
    (is-odd? 11))
#t

And here's the Raku:

(sub (:&is-even = (sub (\n)
                    {([or] ([==] 0, n),
                           (is-odd (pred n:)))}),
      :&is-odd =  (sub (\n)
                    {([and] (not ([==] 0, n)),
                            (is-even (pred n:)))}))
   {is-odd 11})()
# returns «True»

Now, I'm not sure about you, but to my eyes, that code looks quite a bit more like Lisp than like the Raku code I'm used to reading. I don't want to get bogged down in every trick that code uses, but here are a few highlights, along with links for the curious.

So what?

Is the bottom line that Rakoons should all switch to Lisp syntax? No, not at all. In fact, by my own lights, I'd probably toss out the code above in favor of something like the below (assuming we want to keep the same recursive implementation):

sub (:&is-even = sub ($n) {$n == 0 or $n.pred.&is-odd  },
     :&is-odd =  sub ($n) {$n ≠ 0 and $n.pred.&is-even })
{ is-odd 11}()

This not only throws out many of the Lispy features we had above, it actually inverts some of them: $n.pred.&is-odd leans strongly into postfix notation.

And, in the end, that's exactly my point and exactly Raku's strength. With a Lisp, you'll always have prefix notation, which lends your code predictability. With Raku, you'll always have choices, which lets your code better express your intent. Do you want to say is-odd $n.pred, or (pred $n:).&is-odd, or $n.pred.&is-odd, or (with $n {is-odd .pred}), or something else altogether? To the computer, they all mean the same thing, but each shifts the emphasis. And, depending on what you want to say, a different choice might be a better fit.

Rakoons don't need to imitate the syntax of Lisp, APL, or C – even though the language is up to the task. But it is helpful to remember that we have the option to embrace a wide variety of styles and to make a conscious choice about how to best express ourselves.


Notes:

1

I’m using “Lisp” broadly in this post, and am including the whole Lisp family of languages, from the most minimal of schemes all the way to the baroque majesty of Common Lisp.