Macro trial balloons
I’ve been hacking on and off on s1, my vaporware Awk-like tool that uses Scheme for scripting. The “language” defined by s1 includes a few macros. Strictly speaking, these are redundant, but they do make things shorter, which is important for writing one-liners.
Anyway, I am still kind of new to macros… so I’m wondering if the code I wrote is sound. I’m offering the definitions here for review. (Comments from experienced Schemers are welcome.)
1. inc! is used to easily increase variables. It can take any number of arguments; if none are given, 1 is added by default. For example:
- (inc! x) adds 1 to x
- (inc! x 33) adds 33 to x
- (inc! x 1 2 3 4 5) adds (+ 1 2 3 4 5) to x
As the exclamation mark indicates, inc! changes the variable in-place. Here’s its current definition:
(define-macro (inc! name . args)
(let ((total (gensym)))
`(let ((,total
(if (null? (list ,@args))
1
(apply + (list ,@args)))))
(set! ,name (+ ,name ,total)))))
2. Defining multiple variables does not mix well with one-liners. So I added a macro def that allows one to define them quickly and concisely. Like inc!, def takes any number of arguments. If an argument is a list (name value), then a variable is created with the given name and value. If an argument is a symbol, then a variable is created with that name and a value of 0. (Awk is often used to add numbers, so zero seems the most sensible default, IMHO.)
Examples:
- (def x) — same as (define x 0)
- (def x y) — same as (define x 0) (define y 0)
- (def (a 1) (b “hello”)) — same as (define a 1) (define b “hello”)
- (def q (w 3)) — same as (define q 0) (define w 3)
Here’s the current definition:
(define-macro (def . args)
(if (null? args)
#f
(let* ((a (car args))
(rest-args (cdr args))
(name (if (list? a) (car a) a))
(value (if (list? a) (cadr a) 0)))
`(begin
(define ,name ,value)
(def ,@rest-args)))))
(I’m not sure about the #f; it’s not supposed to return a value anyway.)
In any case, s1‘s auxiliary functions and macros allow for concise code. (Some of it is sloppy, but useful for “scripting”, especially one-liners. Naturally, it’s always possible to write longer scripts using “cleaner” code.)
For example, here’s a one-liner that takes the last words on the given lines and adds them up (assuming they are numbers):
s1 '(B (def s)) (inc! s &$nf) (A (out s))'
(I’m not sure about the & syntax yet; it’s used here as a shortcut for the as-number function, which attempts to convert a string to a number, returning 0 by default.)
B and A are shorthands for BEFORE and AFTER, blocks that are executed before and after the main code (which is executed for each line in the given text). The actual order in which these appear doesn’t matter, but it’s probably more intuitive to do before-main-after.
Print the number of lines, words and characters (like wc):
s1 '(B (def c w)) (inc! w nf) (inc! c (len $0) 1) (A (out nl w c))'
Print names starting with “Ga”:
s1 '(if (~ #/^Ga/) (print $0))' /usr/share/dict/propernames
(I’m using regex literals, and ~ is the same as string-search, except it matches against $0 (the whole line) by default.)
These are just teasers. Actual code is subject to change. I will release this when the “API” is somewhat stable. It’s still mostly a toy, though… :-)