A long time back I had written a small blog about a ‘letn’ macro in Common Lisp (check it out here). Of late, I have been venturing deeper and deeper into Racket, and I am starting to like the language, and more importantly, the ecosystem more and more, especially with the express purpose of implementing languages and expanding my understanding of programming language theory.
This is why I decided to give the letn macro a go in Racket this time. Of course, this may by no means be the best way to implement it, but it was an enjoyable exercise all the same. The basic idea is to transform a form like:
(letn [a 1 b 2 c 3]
(+ a b c))
into the corresponding syntactic form:
(let ([a 1]
[b 2]
[c 3])
(+ a b c))
The only difference between this version and the Common Lisp version, functionally speaking, is that in this version, I expect the input to be well-formed pairs of variables and values. This makes more sense now since we cannot possibly substitute a sane value for a variable with no associated value (null? 0? void?), and expect a sane result.
Anyway, so here’s how it looks:
;;; a letn macro in Racket.
(module letn racket
(provide letn)
;;; (letn [a 1 b 2 c 3 d 4 e 5 f 6] (+ a b c d e f)) ->
;;;
;;; (let ([a 1]
;;; [b 2]
;;; [c 3]
;;; [d 4]
;;; [e 5]
;;; [f 6])
;;; (+ a b c d e f))
(require (for-syntax racket/list)) ;; for empty
(begin-for-syntax
(define (process-args-into-pairs lst)
(letrec ([f (lambda (pairs lst)
(if (null? lst)
(reverse pairs)
(f (cons (list (car lst) (cadr lst)) pairs) (cddr lst))))])
(f empty lst))))
(define-syntax letn
(lambda (stx)
(syntax-case stx ()
[(_ (params ...) body0 body ...)
(let ([pairs (process-args-into-pairs (syntax->datum #'(params ...)))])
(with-syntax ([arg-pairs (datum->syntax stx pairs)])
#'(let arg-pairs
body0 body ...)))]))))
Some notes
: Okay, so begin by defining the macro in a module of its own. Then we come to this interesting snippet of code”
(require (for-syntax racket/list)) ;; for empty
What this code means is that we wish to use the racket/list
module during compilation-time (since macro-expansion is part of the compilation phase). The reason for this is that we use empty
, which denotes an empty list, in our program. Of course it would be easier to simply use the literal form, '()
and eschew requiring racket/list
altogether, but this is simply to demonstrate how we can require modules whose functions and symbols we would need at compile time.
Next up, we have the following code block:
(begin-for-syntax
(define (process-args-into-pairs lst)
(letrec ([f (lambda (pairs lst)
(if (null? lst)
(reverse pairs)
(f (cons (list (car lst) (cadr lst)) pairs) (cddr lst))))])
(f empty lst))))
The begin-for-syntax
starts off a new block where we can define functions that we need during compile time itself. In this case, we need a helper function called process-args-into-pairs
which simply takes an input of the form
'(a 1 b 2 c 3 d 4 e 5)
and transforms those into a list of lists:
'((a 1) (b 2) (c 3) (d 4) (e 5))
This is precisely what the actual macro needs during its expansion. Note that since we only have one helper function in this case, we could have used the simpler version, define-for-syntax
to define our helper function like so:
(define-for-syntax (process-args-into-pairs lst)
(letrec ([f (lambda (pairs lst)
(if (null? lst)
(reverse pairs)
(f (cons (list (car lst) (cadr lst)) pairs)
(cddr lst))))])
(f empty lst))))
The process-args-into-pairs
helper function itself is extremely straightforward – we simply accumulate lists of pairs of objects from the input list, and then return them to the caller. This code works on the understanding (as mentioned before) that we expect the input to consist of well-formed pairs).
Now let’s get down to the meat of the business, the letn
macro itself. Here is the code:
(define-syntax letn
(lambda (stx)
(syntax-case stx ()
[(_ (params ...) body0 body ...)
(let ([pairs (process-args-into-pairs (syntax->datum #'(params ...)))])
(with-syntax ([arg-pairs (datum->syntax stx pairs)])
#'(let arg-pairs
body0 body ...)))]))))
There are various ways of defining macros in Racket – define-syntax-rule
, define-syntax
with syntax-rules
,, define-syntax
with syntax-case
, define-syntax
with custom transformer functions, syntax-parse
,etc.
In this case, I have decided to use syntax-case
since it suits the requirements quite nicely – built-in pattern matching is quite nifty!
So here’s how it works – we pattern-match on the supplied syntax (object), and we expect the pattern to be of the form: (letn [*] *)
. Note that in Racket, square brackets are essentially equivalent to (and are converted to, internally) parentheses. As a side-note, when entering code, however, mixing square brackets and parentheses for the same form is an error.
In the template (the right-hand-side of this case), we bind pairs
to the output of the process-args-into-pairs
helper function. Remember that all this happens during compile-time itself. Also note that we need to pass a plain list to the helper function. This is the reason why we need to convert the syntax object into a proper datum (done using (syntax->datum #'(params ...)
).
Next, we need to construct the actual form that the invocation of the macro will expand into. In Common Lisp, we do all that using quasi-quoting and unquoting (with splicing if needed). However, in Racket, we have a different set of forms that deal with this business. The helper function returns a list of lists of variable-value pairs, and this list needs to be inserted into the let
form that we use for the actual body of the template. This is why we need to convert pairs
into a syntax object (since Racket works with syntax objects directly almost throughout). This is why we have
(datum->syntax stx pairs)
.
Finally, we now return the syntax
from the template. Note the reader macro, #'
which is shorthand for syntax
.
Well, that’s about it! In macro, defining recursive macros is pretty easy using ellipses (...
). This is used throughout for both the parameters and the body forms of the macro invocation. This also conveniently ensures that we can nest arbitrary forms inside out stonking new letn
macro.
All right, let’s take it for a quick spin:
Here is the test code:
#lang racket
(require "letn.rkt")
(define (test-case-1)
(letn [a 1 b 2]
(displayln (+ a b))))
(define (test-case-2)
(letn (a "hello" b "world")
(displayln (string-append a ", " b))))
(define (test-case-3)
(letn (a 1 b 2 c 3)
(displayln "Adding three numbers")
(displayln (+ a b c))))
(define (test-case-4)
(letn (a 1 b 2 c 3)
(letn (d 4 e 5)
(displayln "Adding nested variables")
(displayln (+ a b c d e)))))
(define (test-case-5)
(letn (a 1 b 2 c 3)
(let ([d 4]
[e 5])
(letn (f 6 g 7)
(displayln "Nested letnS and letS")
(displayln (+ a b c d e f g))))))
(define (run-all-tests)
(test-case-1)
(newline)
(test-case-2)
(newline)
(test-case-3)
(newline)
(test-case-4)
(newline)
(test-case-5))
And here is the output of a test run:
letn-test.rkt> (run-all-tests)
3
hello, world
Adding three numbers
6
Adding nested variables
15
Nested letnS and letS
28
Excellent! This was quite a satisfying little exercise. Now time to up the ante and start off with real languages!