An Intro to Scheme Macros
In this post I want to go a little deeper into the macro system of Scheme (particularly Chicken), and share a couple of macros I've created.
First of all, macros are what make Scheme and Lisp so exciting. Macros offer the ability to evaluate code at compile time, and modify the structure of a program before it even runs. You can think of it as a really really powerful #define
, but where #define lets you modify text, a Lisp macro lets you modify the code structure itself.
There are a couple of different kinds of macro available in Scheme, and as I'm somewhat of a young schemer I'm not completely sure which are available across all implementations and which are only available in Chicken. But I will assume that the macros here can be ported with relative ease.
The first kind, looks a lot like a regular ol' function. It is even called like a function, only when this macro is read it leaves behind whatever it returns. Here's an annotated example:
; Here we are going to create a new macro called "return-me"
; that will simply return in a list whatever is passed to it
(define-syntax return-me
(syntax-rules () ; Any symbols placed in this list will be ignored
[(return-me body ...) ; This is what the arguments should look like
(list body ...)])) ; And this is how they should be transformed
; Example
(return-me "Hello" "World") => ("Hello" "World")
As you can see, it's nothing terribly complicated, but it opens up a world of possibilities. Extending on this previous example, I will show you a "run" macro, which gives a nice interface to launching applications through SSH and receiving the output:
; Firstly, we need a couple of extra units loaded
(use posix regex regex-literals)
; This is a shell function which calls the supplied argument and
; returns the result in a list of lines
(define (shell args) (call-with-input-pipe args read-lines))
; The run macro itself
(define-syntax run
(syntax-rules (as on) ; We want to ignore the words "as" and "on"
[(run command as username on machine) ; Here is the full macro
; This line extracts the host and port from the "machine" part
(let ([matches (string-match #/(.*?):(.+)/ machine)])
(when (list? matches) ; Got some matches
(shell (format "ssh -C -p ~a ~a@~a ~a"
(third matches) ; Port
username
(second matches) ; Machine
command))))] ; Cool, return the output
; Here we have a smaller version of the macro with some defaults
[(run command on machine)
(run command as (getenv "USERNAME") on machine)]
; And same again here, but avoid the SSH step in this case
[(run command)
(shell command)]))
; Example
(run "uname -a" as "zanea" on "pr0d:2222") => ("Linux pr0d 2.6 etc..")
So as you can see we have already created our very own (albeit small) Domain Specific Language used to describe how to run a command on another machine.
The second kind of macro I'm going to introduce is the read macro. A read macro gives you the ability to control how the Scheme reader understands syntax. A very small example follows:
; This example simply makes !sym become (not sym)
; The lambda expression is triggered whenever a "!" is encountered
(set-read-syntax #\! (lambda (port) `(not ,(read port))))
A very small example indeed, but it shows something useful.
So I hope you've enjoyed this small intro to Scheme Macros.. In the next episode I'll be showing another rather simple macro that allows you to use infix expressions (ie. 1 + 2 + 3) inside your programs… stay tuned..