Implementing Go's defer in Common Lisp

2023-06-16

I have been going over the basics of Go and I found the defer statement really interesting.

Given that Common Lisp is my favorite language, I started thinking about how to implement defer in Common Lisp and if it's really needed at all.

I don't really want to extend the Common Lisp standard by changing the defun to allow defering funcalls and since I don't see an easy way to do this without establishing some point where the deferred funcalls should be made, I decided in the spirit of Common Lisp to create a macro that allows the user to select where the deferred funcalls will be executed.


(defvar *deferred-functions* :unbound)

(defmacro defer ((function-name &rest args))
    (let ((args-names (mapcar #'(lambda (arg)
                                  (declare (ignore arg))
                                  (gensym))
                              args)))
    `(let (,@(mapcar (lambda (arg name)
                        `(,name ,arg))
                     args
                     args-names))
        (push (lambda ()
                (,function-name ,@args-names))
              *deferred-functions*))))

(defmacro with-deferred-calls (() &body body)
  `(let ((*deferred-functions* nil))
      (unwind-protect (progn ,@body)
        (mapc #'funcall *deferred-functions*))))
            

So with-deferred-calls establishes a context where function calls can be deferred. At the end of it, all deferred calls will be executed. We could probably just use local macros and lexical binding instead of dynamic binding to make it all cleaner (we don't want someone to call defer outside the context of a with-deferred-call) but let's see an example of how it works.


(defun print-stacked-integers ()
  (with-deferred-calls ()
    (dotimes (i 5)
      (defer (print i)))))

CL-USER> (print-stacked-integers)

4 
3 
2 
1 
0 
NIL
CL-USER> 
            

An advantage of using with-deferred-calls is that this can be used while defining methods, macros, etc. Of course this does very minimal syntax verifications but respects defer's basic properties. We can see in the next example how it evaluates arguments while executing the defer statement.


(defun print-deferred-side-effects ()
  (with-deferred-calls ()
    (let ((i 0))
      (format t "first i: ~a~%" i)
      (defer (print (incf i)))
      (format t "second i: ~a~%" (incf i))
      (format t "function ends.~%"))))

CL-USER> (print-deferred-side-effects)
first i: 0
second i: 2
function ends.
  
1 
NIL
CL-USER>               
            

Given that incf is a macro and not a function call and this works as expected should give us pause. Maybe this doesn't work as expected for all macros and special forms. What's a defer of a cond? Something to thing about and work the details later.