The evolve
Macro
It sounds like you may be using the CS 61A Scheme implementation.
In principle, what you should do is move the work out of the macro to a helper procedure which is then called from the macro.
The helper procedure should use an accumulator to store the results so that recursive calls are in tail position, providing an iterative process; each time the helper procedure is called, a new list of symbols is append
ed to the end of the accumulator, but there is no more work to do when the procedure returns.
(define (evolve-helper xs acc)
(if (null? xs)
acc
(let ((x (car xs)))
(evolve-helper (cdr xs)
(append acc
(cond ((eq? x 'A) '(A A))
((eq? x 'B) '(A < B > B))
(else (list x))))))))
The macro that calls this procedure is variadic. Since macros are used to create forms to be evaluated, this macro must create not a list, but a form that evaluates to a list. Quasiquotation is used to create the final form which evaluates to a list.
(define-macro (evolve . xs)
`(quote ,(evolve-helper xs '())))
All of this should work in the CS 61A implementation, according to the documentation. But, if you prefer:
(define-macro (evolve-2 (variadic xs))
(quasiquote (quote (unquote (evolve-helper xs '())))))
Example REPL interaction:
scheme@(guile-user)> (evolve lsys A A < A < B > B > A < B > B)
$8 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)
scheme@(guile-user)> (evolve-2 lsys A A < A < B > B > A < B > B)
$9 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)
The evolve-n
Macro
A helper procedure should also be used with evolve-n
. This helper does not need to call the evolve
macro, but instead calls the evolve-helper
procedure. The recursive call to evolve-n
is in tail position, so this is an iterative process; each call simply passes an evolved list of arguments to the next.
(define (evolve-n-helper n xs)
(if (zero? n)
xs
(evolve-n-helper (- n 1)
(evolve-helper xs '()))))
(define-macro (evolve-n n . xs)
`(quote ,(evolve-n-helper n xs)))
You could avoid the helper procedure by using eval
in the evolve-n
macro, but it is bad style to use eval
when a procedure will do as well.
(define-macro (evolve-n-2 n . xs)
(if (zero? n)
`(quote ,xs)
`(evolve-n-2 ,(- n 1) ,@(eval `(evolve ,@xs) (interaction-environment)))))
Example REPL interaction:
scheme@(guile-user)> (evolve-n 0 lsys A A < A < B > B > A < B > B)
$6 = (lsys A A < A < B > B > A < B > B)
scheme@(guile-user)> (evolve-n 1 lsys A A < A < B > B > A < B > B)
$7 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)
scheme@(guile-user)> (evolve-n 2 lsys A A < A < B > B > A < B > B)
$8 = (lsys A A A A A A A A < A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B > A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)
scheme@(guile-user)> (evolve-n-2 2 lsys A A < A < B > B > A < B > B)
$9 = (lsys A A A A A A A A < A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B > A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)
Performance Issues
The append
procedure is expensive, and with the definition of evolve-helper
above, this expensive procedure is called on every recursive call. This adds up when input gets large. In particular, with ten evolutions of OP example input in (evolve-n 10 lsys A A < A < B > B > A < B > B)
there is a significant delay before results arrive. Since OP states that "I need to evolve this perhaps tens of times," this delay is a real issue.
The situation can be improved by moving append
out of the iterative process. Since cons
is much cheaper, we can use cons
to add sublists to the front of the accumulator instead of appending them to the end. When the base case is reached, the accumulator must then be reversed before append
ing all of the sublists together for the final result. In informal tests this improved procedure led to a speedup of approximately 10x when compared with the original definition; the result from (evolve-n 10 lsys A A < A < B > B > A < B > B)
arrived in a fraction of one second. This also illustrates why it is better to move as much work as possible out of macros and into functions; macros are more difficult to write, and more fragile than regular functions. Making the necessary optimizations here was simplified by using macros only where needed, and functions elsewhere.
(define (evolve-helper xs acc)
(if (null? xs)
(apply append (reverse acc))
(let ((x (car xs)))
(evolve-helper (cdr xs)
(cons (cond ((eq? x 'A) '(A A))
((eq? x 'B) '(A < B > B))
(else (list x)))
acc)))))
A Macro-Only Solution
The evolve
macro can avoid custom external helper functions and explicit recursion altogether by mapping a procedure that applies the transformation rules over the input.
(define-macro (evolve . xs)
`(quote ,(apply append (map (lambda (x) (cond ((eq? x 'A) '(A A))
((eq? x 'B) '(A < B > B))
(else (list x))))
xs))))
This seems like a nice solution, but at the moment I don't see a straightforward way to use this definition in the evolve-n
macro, unless eval
is employed.
(define-macro (evolve-n n . xs)
(if (zero? n)
`(quote ,xs)
`(evolve-n ,(- n 1) ,@(eval `(evolve ,@xs) (interaction-environment)))))
Several informal tests using (evolve-n 15 lsys A A < A < B > B > A < B > B)
showed that both versions, the one using helper functions and the one using only macro definitions, had indistinguishable performance; both took between 20 and 24 seconds to complete the task. In the absence of any significant performance advantage, I would stick with the simpler helper-function version of evolve-n
. Or I would reconsider what inputs and outputs should look like; why not just take a list of symbols directly for input?