Introduction to newLISP/Macros
Macros
[edit | edit source]Introducing macros
[edit | edit source]We've covered the basics of newLISP, but there are plenty of powerful features left to discover. Once you've grasped the main rules of the language, you can decide which of the more advanced tools you want to add. One feature that you might want to explore is newLISP's provision of macros.
A macro is a special type of function that you can use to change the way your code is evaluated by newLISP. For example, you can create new types of control functions, such as your own version of if or case.
With macros, you can start to make newLISP work exactly the way you want it to.
Strictly speaking, newLISP's macros are fexprs, not macros. In newLISP, fexprs are called macros partly because it's much easier to say 'macros' than 'fexprs' but mainly because they serve a similar purpose to macros in other LISP dialects: they allow you to define special forms such as your own control functions.
When do things get evaluated
[edit | edit source]To understand macros, let's jump back to one of the very first examples in this introduction. Consider the way this expression is evaluated:
(* (+ 1 2) (+ 3 4)) ;-> (* 3 7) ;-> 21
The * function doesn't see the + expressions at all, only their results. newLISP has enthusiastically evaluated the addition expressions and then handed just the results to the multiplication function. This is usually what you want, but there are times when you don't want every expression evaluated immediately.
Consider the operation of the built-in function if:
(if (<= x 0) (exit))
If x is greater than 0, the test returns nil so the (exit) function isn't evaluated. Now suppose that you want to define your own version of the if function. It ought to be easy:
(define (my-if test true-action false-action)
(if test true-action false-action))
> (my-if (> 3 2) (println "yes it is" ) (exit)) yes it is $
But this doesn't work. If the comparison returns true, newLISP prints a message and then exits. Even if the comparison returned false, newLISP would still exit, without printing a message. The problem is that (exit) is evaluated before the my-if function is called, even when you don't want it to be. For ordinary functions, expressions in arguments are evaluated first.
Macros are similar to functions, but they let you control when - and if - arguments are evaluated. You use the define-macro function to define macros, in the same way that you define functions using define. Both these defining functions let you make your own functions that accept arguments. The important difference is that, with the ordinary define, arguments are evaluated before the function runs. But when you call a macro function defined with define-macro, the arguments are passed to the definition in their raw and unevaluated form. You decide when the arguments are evaluated.
A macro version of the my-if function looks like this:
(define-macro (my-if test true-action false-action)
(if (eval test) (eval true-action) (eval false-action)))
(my-if (> 3 2) (println "yes it is" ) (exit))
"yes it is"
The test and action arguments aren't evaluated immediately, only when you want them to be, using eval. And this means that the (exit) isn't evaluated before the test has been made.
This ability to postpone evaluation gives you the ability to write your own control structures and add powerful new forms to the language.
Tools for building macros
[edit | edit source]newLISP provides a number of useful tools for building macros. As well as define-macro and eval, there's letex, which gives you a way of expanding local symbols into an expression before evaluating it, and args, which returns all the arguments that are passed to your macro.
Symbol confusion
[edit | edit source]One problem to be aware of when you're writing macros is the way that symbol names in macros can be confused with symbol names in the code that calls the macro. Here's a simple macro which adds a new looping construct to the language that combines dolist and do-while. A loop variable steps through a list while a condition is true:
(define-macro (dolist-while)
(letex (var (args 0 0) ; loop variable
lst (args 0 1) ; list
cnd (args 0 2) ; condition
body (cons 'begin (1 (args)))) ; body
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
It's called like this:
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1)))
x is 19 x is 18 x is 17 x is 16 x is 15 x is 14 x is 13 x is 12 x is 11 x is 10
And it appears to work well. But there's a subtle problem: you can't use a symbol called y as the loop variable, even though you can use x or anything else. Put a (println y) statement in the loop to see why:
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1))
(println {y is } y))
x is 19 y is true x is 18 y is true x is 17 y is true
If you try to use y, it won't work:
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is value expected in function dec : y
The problem is that y is used by the macro to hold the condition value, even though it's in its own let expression. It appears as a true/nil value, so it can't be decremented. To fix this problem, enclose the macro inside a context, and make the macro the default function in that context:
(context 'dolist-while)
(define-macro (dolist-while:dolist-while)
(letex (var (args 0 0)
lst (args 0 1)
cnd (args 0 2)
body (cons 'begin (1 (args))))
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
(context MAIN)
This can be used in the same way, but without any problems:
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is 19 y is 18 y is 17
Other ideas for macros
[edit | edit source]newLISP users find many different reasons to use macros. Here are a couple of macro definitions I've found on the newLISP user forums.
Here's a version of case, called ecase (evaluated-case) that really does evaluate the tests:
(define-macro (ecase _v)
(eval (append
(list 'case _v)
(map (fn (_i) (cons (eval (_i 0)) (rest _i)))
(args)))))
(define (test n)
(ecase n
((/ 4 4) (println "n was 1"))
((- 12 10) (println "n was 2"))))
(set 'n 2)
(test n)
n was 2
You can see that the divisions (/ 4 4) and (- 12 10) were both evaluated. They wouldn't have been with the standard version of case.
Here's a macro that creates functions:
(define-macro (create-functions group-name)
(letex
((f1 (sym (append (term group-name) "1")))
(f2 (sym (append (term group-name) "2"))))
(define (f1 arg) (+ arg 1))
(define (f2 arg) (+ arg 2))))
(create-functions foo)
; this creates two functions starting with 'foo'
(foo1 10)
;-> 11
(foo2 10)
;-> 12
(create-functions bar)
; and this creates two functions starting with 'bar'
(bar1 12)
;-> 13
(bar2 12)
;-> 14
A tracer macro
[edit | edit source]The following code changes the operation of newLISP so that every function defined using define will, when evaluated, add its name and details of its arguments to a log file. When you run a script, the log file will contain a record of the functions and arguments that were evaluated.
(context 'tracer)
(define-macro (tracer:tracer farg)
(set (farg 0)
(letex (func (farg 0)
arg (rest farg)
arg-p (cons 'list (map (fn (x) (if (list? x) (first x) x))
(rest farg)))
body (cons 'begin (args)))
(lambda
arg
(append-file
(string (env "HOME") "/trace.log")
(string 'func { } arg-p "\n"))
body))))
(context MAIN)
(constant (global 'newLISP-define) define)
; redefine the built-in define:
(constant (global 'define) tracer)
To run a script with this simple tracer, load the context before you run:
(load {tracer.lsp})
The log file generated contains a list of every function that was called, and the arguments it received:
Time:Time (1211760000 0) Time:Time (1230163200 0) Time:Time (1219686599 0) show ((Time 1211760000 0)) show ((Time 1230163200 0)) get-hours ((Time 1219686599 0)) get-day ((Time 1219686599 0)) days-between ((Time 1219686599 0) (Time 1230163200 0)) leap-year? ((Time 1211760000 0)) adjust-days ((Time 1230163200 0) 3) show ((Time 1230422400 0)) Time:Time (1219686599 0) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) period-to-string ((Duration 124.256956)) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) Time:print ((Time 1211760000 0)) Time:string ((Time 1211760000 0)) Duration:print ((Duration 124.256956)) Duration:string ((Duration 124.256956))
It will slow execution quite a lot.