Synergy/DE possesses a macro definition syntax that’s very similar to the #define syntax in C, but not quite as robust. It doesn’t support macros that expand to multiple lines of code. An individual parameter passed to a macro cannot be continued to the next line, either. A macro cannot be used as a parameter to itself, nor within its expansion (as of 9.1.5). But despite these limitations, I find macros to be a powerful tool for either simplifying my code or making it hopelessly obscure. The basic Synergy/DE macro syntax is:
.define macroname(parameter, …) expansion
Wherever parameter is encountered within expansion it is replaced with whatever is passed to the macro in that argument position when it’s invoked. In order to be recognized as the desired token within expansion, it must either be syntactically isolated as a token, or escaped with a back-quote (`). Within strings, you use two back-quotes preceding and one following the identifier. If you continue a macro to the next line, its expansion is also continued – which is why macros are limited to a single statement. There is no syntax (yet) for saying, “this is a continuation of my macro, but I want it to be a new statement rather than a continuation of the same statement within the expansion.”
One problem that’s common to any parameterized macro system (be it Lisp’s, C’s, or Synergy/DE’s) is how many times an argument gets evaluated at runtime. Macro invocation syntax looks like functional syntax, but how arguments get evaluated is quite a different ball game. Consider the classic “max” macro:
.define max(val1, val2) fif(val1 > val2, val1, val2)
For a definition of the fif macro, see this post. Now lets try using it:
account.fee = max(5.00, account.CloseQuarter() * 0.01)
Seems innocent enough, we’re going to close out the quarter, which returns the total, from which we’ll compute a fee of 1% of total activity, but not less than $5.00. If max were a function, then each parameter would be evaluated once before invoking it. But the max macro expands out into:
account.fee = fif(5.00 > account.CloseQuarter() * 0.01, 5.00, account.CloseQuarter() * 0.01)
which means that if 1% of activity is not less than 5.00, we’ll call account.CloseQuarter() twice. Actually, because fif is implemented under the hood as a static method, we’ll always call account.CloseQuarter() twice. Even if account.CloseQuarter() had no side-effects, it’s more than we intended to do. I could also devise examples of macros in which an argument may not get evaluated at all. This, too, can cause confusion when you’re looking at the code and thinking of the macro as if it were a function.
One way to solve this problem in Lisp is to use LET. LET exists in many functional languages to allow you to define a scope for local assignments, but it can also be used to force single evaluation of macro parameters:
(defmacro mymacro (param1)
`(let ((,var1 ,param1))
(maybe do things with ,var1)))
The macro generates a LET statement in the code, assigning the parameter param1 to a local variable var1, which only has the scope of the LET statement. That assignment forces param1 to be evaluated once and only once within the generated code. The LET then returns whatever the (maybe do things with var1) statement returns. Even if there is a var1 declared in the surrounding code, the var1 referenced inside the LET will refer to the parameter’s value. Other variables defined in the surrounding code may, however, be referenced and used within the macro (which isn’t hygenic, but is sometimes useful).
I have a dream
How could we do something similar in Synergy/DE? We do have the ability to define a very local scope for variables in version 9, using the DATA statement — but there are at least two reasons why this won’t help us with macros: (1) a DATA statement has to be on a line by itself, and we don’t have multi-line macro expansions (remember?), and (2) even if we did, we don’t have any way to return a value from a multi-statement block. Probably the right way to solve this in the language would be to allow inline anonymous functions with access to the local scope (a la JavaScript and other languages with closures) and also provide a syntax for multiline macros. We could dream, for instance, of something like this:
.define mymacro(param1) function, ^val
.& var1, i
.& proc
.& freturn maybe_do_things_with(var1) ;a macro that might use var1 or not
.& end(param1) ;Invoke it, passing param1
By converting the macro into a function, you’d automatically evaluate arguments only once. But that’s pie-in-the-sky – how about something I can use today?
Let my parameters go
The downloadable code below works with Synergy/DE version 9.1.5 and above, and provides a form of scoped LET. The syntax is a bit more cluttered than I’d like, but it’s functional (pun intended). For instance, you could define our mymacro example as:
.define mymacro(param1) let(assign(“var1”, param1), maybe_do_things_with(valueof(“var1”)))
and we can write a single-evaluation-safe version of the max macro as:
.define max(val1,val2) let(assign("val1",val1) && assign("val2",val2), fif((vv("val1") > vv("val2")), vv("val1"), vv("val2”)))
This version of max takes any two primitives (or Vars) and returns a Var containing the greater of the two values. Both val1 and val2 will be evaluated once and only once, in their respective assigns.
The basic syntax for let is:
return_value = let(assignments, body)
where
return_value is whatever body returns. It will have the same type as what body returns, so you may need to cast it.
assignments are one or more “assign” macros, joined with the logical AND operator (&& or .and.)
body is some expression whose result will be returned.
I lied just a bit when I said that the return value is the same as what body returns. It will be one of the following, whichever is the best match for what body returns:
- i (which covers boolean, int, and ^val functions)
- decimal (which covers d and d.)
- a (which also handles strings)
- @Var (if it’s a Var)
- @* (for all other object classes)
The syntax for the assign macro is:
assign(name, value)
where
name is the name of a let variable
value is the value to assign it
Under the hood
The assign macro expands into a static method named Lets.Set. It returns a Lets object that is passed to let to maintain scope (let actually expands into a static method named Lets.Return). The logical AND between two Lets objects causes the first one to maintain the scope of the second one. Thus, we always use only one argument for all assignments, and the second argument is for the body.
The assignment of let variables is maintained in a static Hash of Stacks. Each member of the hash is the stack of values for a given name. An assign pushes a new value onto the stack for its name, and the let to which that assign belongs removes that value from the stack before it returns. Within either a subsequent assign or within the body of the let, you can reference the topmost value on the stack for a given name via the valueof macro.
Since Stacks can only hold objects, if a primitive type is passed to assign, it is boxed in a Var. The valueof macro (which expands into the static method Lets.Get) always returns an object, so I’ve provided a handy shortcut to cast it as a (Var): the vv macro. vv(name) is equivalent to (Var)valueof(name).
What is It?
Another handy shortcut is the “it” macro. “it” provides a reference to the most recently assigned value, without having to know its name. “it” is also scoped to a let, so in the case of nested lets, it goes back to what it was in the outer let when the inner let returns. To cast “it” as a Var, you can use the itv macro as a shortcut.
Why wouldn’t you know the name of the most recently assigned value? Sometimes you don’t care, especially if you only need one temporary variable. So, to generate a unique name, we have gensym, and as a shortcut for a let with only one assignment that uses gensym for its name, we have the genlet macro:
result = genlet(value, body)
Believe me, “it” will come in handy in that body. In case you do want to inspect the name of whatever is assigned to “it”, there is the “itsname” macro to provide it to you.
I’ve provided quite a few tests in test_let.dbl. These also serve as some good examples of usage.
Even though Synergy/DE doesn’t allow macros to be nested in their own parameters, I’ve worked around that for let and assign. They aren’t actually parametric macros – they’re just text replacements for their static method names. So you can have a let within an assign, and you can have a let within the body of a let. The let variable scoping all works as you would expect. You can also continue a statement to the next line in the middle of a let or an assign.
Back in the 1980s I stretched the DIBOL language (which at that time lacked integers, dynamic memory allocation, and recursion) to implement various DSL interpreters. In the early 90s, I used the newly available xsubr routine to create a form of object-orientation by convention. Ken Lidster and I subsequently designed a couple of prototypes for a true OOP implementation, which has finally come to fruition in Synergy/DE version 9. Now once again, I’m pushing the limits of the language to enable the functional model. Let us see where it goes from here.
UPDATE 2009-07-06: Change the namespace to “ChipsTips”, and included an updated version of Var.
UPDATE 2009-09-13: Updated for Synergy/DE 9.3