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:
- mutually recursive definitions – we use
letrec
to defineis-even?
in terms ofis-odd?
, and also to defineis-odd?
in terms ofis-even?
; neither can be completely defined without the other. - Prefix precedence – even operations (like subtraction or
and
) that would involve infix operators in other languages are done with prefix function calls. - 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. - 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.
- 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.
- The
[…]
reduction operator pulls double duty, letting us call infix operators act as prefix ones. - Similarly,
pred n:
uses indirect invocation syntax (the:
) to call a method like a function – that is, in prefix position. - Our stand-in for Lisp's
letrec
is that old standby, the Immediately Invoked Function Expression. We combine the IIFE with Raku's ability to declare optional named arguments to effectively create scope-limited variables, exactly the power that Lisp's variouslet
constructs typically provide. - Many of the parentheses are optional, thrown in for fun. But some of them are functional. In Raku, you need to provide enough info to prevent ambiguity about which arguments go with which functions, but there's no rule that says you need to use the postfix
(…)
syntax to do so. For example, say you want to callf
with1
and2
and then callsay
with bothf
's return value and'foo'
. To do that, most Rakoons would writesay f(1, 2), 'foo'
. That works, unlikesay f 1, 2, 'foo'
– which ends up passing all three arguments tof
. But you're also free to place your parens the way a Lisper might:say (f 1, 2), 'foo'
, which also makes it clear which args go with which function. (Or even(say (f 1, 2), 'foo')
, if you just really love the elegance of a good parenthesis.
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:
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.