OK, so now we’ve defined our domain-specific language (DSL), and we’ve written a function to retrieve symbols in a tree. Now, we need a few more functions.
First, we will need something that tells us which variables are used in the forms, so that we know how to pop the stack. So, we locate the deepest stack variable used, and declare that all variables up to and including that one are used. That function is here:
(defun get-vars-used (rules-list varnames) (let ((symbols-used (get-symbols-in-list rules-list)) (vlen (length varnames))) (dotimes (i vlen) (let ((check (nth (- vlen i 1) varnames))) (when (member check symbols-used) (return-from get-vars-used (subseq varnames 0 (- vlen i))))))))
with sample output:
CL-USER> (get-vars-used '(X <- (sqrt (+ (* X X) (* Y Y))) Y <- (atan Y X)) '(X Y Z W)) (X Y)
Next, we’ll need a list of variables assigned. We look for the special ‘<- symbol in all depths of the tree and collect the variables assigned. If there are no <- symbols, it’s an implicit assignment to X, so we return that variable name alone. This function is here:
(defun get-vars-assigned (rules-list varnames) (let ((rv '())) (labels ((worker (rl) (do ((v rl (cdr v))) ((not v) rv) (cond ((listp (first v)) (setf rv (append rv (worker (first v))))) ((and (symbolp (first v)) (eq (second v) '<-)) (push (first v) rv)))))) (setf rv (worker rules-list)) (remove-if #'(lambda (x) (not (member x varnames))) rv) (if (not rv) (list (first varnames)) (delete-duplicates rv)))))
with sample output:
CL-USER> (get-vars-assigned '(X <- (sqrt (+ (* X X) (* Y Y))) Y <- (atan Y X)) '(X Y Z W)) (Y X)
Our third helpful function will convert <- assignments to proper setf forms, and in so doing will convert the syntax back to proper Lisp. The following function will do that, but it also changes the names of the targets of the setf. That is, assignment is made to a different symbol, so that the values of X, Y, Z, W are not overwritten. This is necessary if you look at one of the forms we want to be able to process:
X <- (sqrt (+ (* X X) (* Y Y))) Y <- (atan Y X)
If these assignments were done serially, the changed value of X would be used to compute Y, and the wrong answer would be produced. If we try to do it with a psetf, we will be severely limiting our permitted structures, as we will require exactly one assignment execution, happening simultaneously, and the programmer may not want to be bound by that. So, by assigning the new values to new symbols, and pushing those back onto the stack, we can ensure that we have full flexibility of syntax. Here, then, is the function that converts to a list of setf forms:
(defun convert-to-setf-forms (rules-list vars-used output-varnames) (let (rv) (do ((pos rules-list (cdr pos))) ((not pos) rv) (cond ((and (member (first pos) vars-used) (eq (second pos) '<-) (third pos)) (setf rv (append rv `((setf ,(nth (position (first pos) vars-used) output-varnames) ,(third pos))))) (setf pos (cddr pos))) ((listp (first pos)) (setf rv (append rv (list (convert-to-setf-forms (first pos) vars-used output-varnames))))) (t (setf rv (append rv (list (first pos)))))))))
with sample output:
CL-USER> (convert-to-setf-forms '(X <- (sqrt (+ (* X X) (* Y Y))) Y <- (atan Y X)) '(X Y) '(X-OUT Y-OUT)) ((SETF X-OUT (SQRT (+ (* X X) (* Y Y)))) (SETF Y-OUT (ATAN Y X)))
You’ll notice the use of a backtick in the convert-to-setf-forms function. If you are new to Lisp, you can be forgiven for thinking that backticks are “the things that make macros”, but that’s not what they are. The backtick is the intermediate construct between single-quoted lists and lists built with the list function. Single-quoted lists are literal collections of symbols, no interpretation of the internal structure beyond the list layout itself is performed: no functions are called and no variables are replaced. The list function is a function, and as such it evaluates all of its arguments. Bare symbols are converted to their value, or called as functions, depending on the syntax in which they appear. The backtick allows the programmer to build lists where some elements are evaluated, and some are not. It’s possible to do the same with the list function, but it’s much less convenient, and much less readable. Consider, for instance, these two forms, which do the same thing:
CL-USER> (let ((aval 20) (bval 10)) `(let ((a ,aval) (b ,bval)) (when (> a b) (format t "A is greater than B~%")))) (LET ((A 20) (B 10)) (WHEN (> A B) (FORMAT T "A is greater than B~%")))
CL-USER> (let ((aval 20) (bval 10)) (list 'let (list (list 'a aval) (list 'b bval)) (list 'when (list '> 'a 'b) (list 'format t "A is greater than B~%")))) (LET ((A 20) (B 10)) (WHEN (> A B) (FORMAT T "A is greater than B~%")))
The form built from the backticks is much easier to interpret and modify. There is nothing about backticks that declares that they must appear in macros, it’s merely that they are almost always necessary for readable macros, but the function contexts where backticks are helpful are less common. The programmer will find the backtick of considerable use when building Lisp code in a function, as we are doing here.