Getting Imperative
A few weeks ago I was messing around with CLOG when I noticed a peculiar pattern. In several the excellent examples that ship with CLOG, the let*
form is used to emulate an imperative style of programming. A good example of this can be found here. The faint putrescence of a code smell wafted up from that example, and, I thought, “Darn it, it should be better. It can be better!”
Isn't Common Lisp a multi-paradigm language? Why shouldn't this imperative pattern be expressed more naturally?
The result of my curiosity is a rough draft of a macro, called imperative
.
The IMPERATIVE
Macro
The imperative
macro allows for the interleaving of binding forms with arbitrary non-binding forms without deep nesting. Here is how it might be used:
(imperative
(:= x 10
y (+ x 20)
z (+ x 100))
(print (list x y z))
(setf x 100)
(print (list :x x))
(:= x 101)
(print (list :x x))
(list x y z))
;; which would print
;; (10 30 110)
;; (:X 100)
;; (:X 101)
;; then return
;; (101 30 110)
Under the hood, the macro just nests LET*
forms. For example, a macroexpand-1
of the above yeilds:
(BLOCK NIL
(LET* ((X 10) (Y (+ X 20)) (Z (+ X 100)))
(PRINT (LIST X Y Z))
(SETF X 100)
(PRINT (LIST :X X))
(LET* ((X 101))
(PRINT (LIST :X X))
(LIST X Y Z))))
The whole thing is inside of a BLOCK
so that you can return early if you wish by calling (return &optional value)
anywhere inside.
The macro
It's not perfect, but here's the macro in its current form:
(defmacro imperative (&body body)
"Evaluate expressins in BODY in sequence. Expressions that look
like (:= VAR1 EXPR1 ...) will expand into a LET* form whose bindings
are valid for the rest of the BODY
E.g.
(imperative
(format t \"Welcome to IMPERATIVE\")
(terpri)
(:= x 10 z (+ x 20))
(format t \"X = ~a, Z = ~a~%\" x z)
(:= y (+ z 20))
(format t \"Y = ~a~%\" y)
(list x y z))
would evaluate to:
Welcome to IMPERATIVE ;; <-- printed to stdout
X = 10, Z = 30
Y = 50
(10 50 30) ;; <-- return value
IMPERATIVE introduces an implicit, anonymous BLOCK, and hence can be
returned from.
"
(labels ((binding-form-p (form)
(and (consp form)
(keywordp (first form))
(eql := (first form))))
(collect-bindings (bindings)
(loop for (var expr . more) on bindings by #'cddr
collect (list var expr)))
(expander (body)
(cond
((null body) body)
((binding-form-p (first body))
(list (list* 'let* (collect-bindings (rest (first body)))
(expander (rest body)))))
(t
(cons (first body)
(expander (rest body)))))))
`(block () ,@(expander body))))
Good enough as a first draft.
Deodorizing?
A partial re-write of the smelly example is here:
(imperative
(:= last-tab nil
t1 (create-button body :content "Tab1")
t2 (create-button body :content "Tab2")
t3 (create-button body :content "Tab3"))
(create-br body)
(:= p1 (create-div body)
p2 (create-div body)
p3 (create-div body :content "Panel3 - Type here")
f1 (create-form p1)
fe1 (create-form-element
f1 :text
:label (create-label f1 :content "Fill in blank:")))
(create-br f1)
(:= fe2 (create-form-element
f1 :color
:value "#ffffff"
:label (create-label f1 :content "Pick a color:")))
(create-br f1)
(create-form-element f1 :submit :value "OK")
(create-form-element f1 :reset :value "Start Again")
;; .. this is a long one, but you get the idea ...
)
I can't tell whether or not the deodorizor smells any better.