An interesting web-page THE POWER OF LISP MACROS appeared on Reddit and Hacker News last week. It was written by Dr. Edmund Weitz. Of course I knew for Lisp macros and their "magic" for a long time, but I rarely saw their better use cases, so I couldn't compare them to anything. This article finally gave a chance for that.
Rebol does not have macros (nor a compilation step), but it has very flexible and reflexive run-time capabilities. Code is data / Data is code holds as much for Rebol as it possibly can. In this post, I want to see, how a Rebol doing the same as Lisp macros would look like.
my-ifOne macros use-case that I've heard of many times is the "if". Lisp needs macros to be able to have if, because otherwise it would evaluate true and false blocks. And this is the first case Dr. Weitz shows:
Lisp:
(defmacro my-if (test-form then-form else-form)
;; in "reality" IF is a "special operator" and COND is a macro
(list 'cond
(list test-form then-form)
(list t else-form)))
Because of the way Rebol blocks are evaluated or reduced. Rebol doesn't need macros for this.
If is an function like any other.
Rebol:my-if: func [ cond then-form else-form ]
[ do either cond [ then-form ][ else-form ] ]
BrianH on Rebol chat proposed another solution with eiher:
my-if: func [ cond then-form else-form ]
[ either :cond :then-form :else-form ]
He also showed what he thinks is most effective and robust (with types) solution that uses only basic functions of Rebol:
either: func
[[throw] cond then-form [block!] else-form [block!]]
[ do get pick [else-form then-form] not :cond]
withDr. Weitz continued with the
with- convention. This basically intrigued me to write this post, because the with-* convention is one of things I really like that I can do with Rebol. I learned about it while programming in Factor.
Lisp:;;; Wrap a body of code with a prologue
;;; and a (guaranteed) happy ending
(defun begin-transaction ()
(format t "Starting transaction~%"))
(defun commit-transaction ()
(format t "Committing transaction~%"))
(defun abort-transaction ()
(format t "Aborting transaction~%"))
(defmacro with-transaction (&body body)
`(let (done) ; variable capture - see below
(unwind-protect
(prog2
(begin-transaction)
(progn ,@body)
(setq done t))
(if done
(commit-transaction)
(abort-transaction)))))
I think the Rebol version of this is very elegant:
Rebol:begin-transaction: does [ print "Starting transaction" ]
commit-transaction: does [ print "Committing transaction" ]
abort-transaction: does [ print "Aborting transaction" ]
with-transaction: func [ code ] [
begin-transaction
either error? try [ do code ]
[ abort-transaction ] [ commit-transaction ]
]
We don't use any variable "done" as Lisp uses it in our
with-transaction function, so the next chapter "Ensuring discretion" isn't applicable here. Rebol has many ways to limit the effects of words inside a block to it's outside.
Unit test frameworkThis is a little larger example. I steered away of somewhat following the example in Rebol and just wrote what I would write to do the same. If my code doesn't do something that the author's code does please let me know.
Lisp:(defvar *test-thunks* (make-hash-table)) ; <http://en.wikipedia.org/wiki/Thunk>
(defvar *test-sources* (make-hash-table))
(defmacro define-test (name (&optional condition) &body body)
(with-unique-names (c)
`(setf (gethash ',name *test-sources*) '(progn ,@body)
(gethash ',name *test-thunks*)
(lambda ()
(handler-case
(format t "Test ~A ~:[FAILED~;passed~].~%"
',name (progn ,@body))
,@(when condition
`((,condition () (format t "Test ~A passed.~%"
',name))))
(error (,c)
(format t "Test ~A FAILED, ~
condition ~S was signalled.~%"
',name ,c)))))))
(defun run-tests ()
(dolist (test-name (sort (loop for name being the hash-keys of *test-thunks*
collect name)
#'string-lessp))
(format t "~%~%Starting test ~A...~%" test-name)
(let ((*print-pretty* t))
(format t "~S~%~%" (gethash test-name *test-sources*)))
(funcall (gethash test-name *test-thunks*)))
(values))
; let's do some testing
(define-test simple-plus-test ()
(= (+ 1 1) 2))
(define-test wake-up-after-sleep ()
(sleep 1) t)
(define-test division-by-zero (division-by-zero)
(let ((a 42) (b 41))
(incf b)
(/ 1 (- a b))))
(define-test unknown-file ()
;; this is a Windows laptop...
(open "/etc/passwd"))
(run-tests)
Why I steered away?
- I don't want to use globals for this
- I don't like (imperative) feel of: first *setup-state*, then *run*
- Lisp example must return true/false/exception for correct/wrong/wrong . I prefer unit test to return result/exception where result is compared to a expected value. So you can show "Test A FAILED. expected 100 got: 101" in case of a failure.
Rebol:
run-tests: func [ ts /local str res err exp ] [
forskip ts 3 [ print rejoin [ "--" newline "Test " first ts " "
either not error? err: try [
res: either equal? got: do second ts exp: third ts
[ "PASSED :)" ]
[ rejoin [ " FAILED, expected: " exp ", got: " got ]]
] [ res ] [ join "Failed with ERROR: " disarm err ]]]]
; let's do some testing
run-tests [
sum [ + 1 1 ] 2
notsum [ + 100 1 ] 200
zerodiv [ / 100 0 ] 1
file [ read %somefile ] false ]
---
Disclaimer: I am not a REBOL guru, nor a CS. If anyone has any comments about what I did wrong, he/she is welcome to correct me.
---
Update: I talked to the Lisp-er colleague Simon and he said that article "The power of Lisp macros" isn't too good at showing the *power of* Lisp macros. He also said rebol examples here are very clean and cool. On the other side a Rebol-er said that he feels sorry for me if this is how I format the Rebol code ;))