The classic example of a Lisp closure is the following function that returns a counter:
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
When called it increments its count and returns the result:
CL-USER > (setq up1 (make-up-counter))
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
CL-USER > (funcall up1)
1
CL-USER > (funcall up1)
2
When I showed this to a friend who is unfamiliar with Lisp he asked me how he could copy a counter to create a new, independent counter of the same type. This doesn't work:
CL-USER > (setq up2 up1)
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
because up2 isn't a new counter, it's just a different name for the same counter:
CL-USER > (funcall up2)
3
Here's my best attempt:
(defun make-up-counter ()
(let ((n 0))
#'(lambda (&optional copy)
(if (null copy)
(incf n)
(let ((n 0))
#'(lambda () (incf n)))))))
To return a copy of a counter you call it with the argument t:
(defun copy-counter (counter) (funcall counter t))
It works for a first generation copy:
CL-USER > (setq up2 (copy-counter up1))
#<Closure 1 subfunction of MAKE-UP-COUNTER 200DB722>
CL-USER > (funcall up2)
1
but it obviously won't work if you try to copy up2. I can't see how to get it to work properly, because the definition of make-up-counter needs to have a copy of itself inside its own definition.
Any suggestions?
Not really answering the question. But this way, a copy would be easier...
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
Generally I would avoid more complex versions of such code for production use in maintainable software. It's harder to debug and introspect. It's basic FP lore from the past (using closures to hide mutable state, see for example the early Scheme paper), but for anything more complex it is a pain. It hides the value - which is useful - but at the same time it makes it hard to debug. Minimum is a debugger/inspector able to look into closure bindings. It's convenient, because it is easy to write, but the price is paid later.
Problems:
CL-USER 36 > (make-up-counter)
#<anonymous interpreted function 40600015BC>
What is it? It's a function like all the others. It is not saying anything about its purpose, its arguments, documentation, source, it has no documented interface, no useful printed representation, the code can't be updated while used, ... We could add more functionality to it - internally - but then we can get all that for free from an object system like CLOS.
(defclass counter ()
((value :initarg :start :initform 0 :type integer)))
(defmethod next-value ((c counter))
(with-slots (value) c
(prog1 value
(incf value))))
(defmethod copy-counter ((c counter))
...)
(defmethod reset-counter ((c counter))
...)
...
Then:
CL-USER 44 > (let ((c (make-instance 'counter :start 10)))
(list (next-value c)
(next-value c)
(next-value c)
c))
(10 11 12 #<COUNTER 40200E6F3B>)
CL-USER 45 > (describe (fourth *))
#<COUNTER 40200E6F3B> is a COUNTER
VALUE 13
To solve this, you need to use a recursive function, using labels
:
(defun make-up-counter ()
(labels ((new ()
(let ((n 0))
(lambda (&optional copy)
(if copy
(new)
(incf n))))))
(new)))
You can even make it copy the current counter value when copy
is true:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(if copy
(new n)
(incf n)))))
(new 0)))
For the best of both worlds, you can make it create a counter with a specified value if copy
is numeric, otherwise just copy the counter value if truthy, else increment:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(cond ((numberp copy) (new copy))
(copy (new n))
(t (incf n))))))
(new 0)))
Rainer's answer is right that you should avoid using closures as objects. You can use defclass
as already shown, but for a simple counter you could simply write:
(defstruct counter (value 0))
This defines all you need: (make-counter)
, (make-counter :value x)
and (copy-counter c)
all work as expected. Your objects can also be printed readably.
(let* ((c (make-counter))
(d (copy-counter c)))
(incf (counter-value c))
(values c d))
#S(counter :value 1)
#S(counter :value 0)
You should still export higher-level functions like reset and next so that users of your counter do not need to know how it is implemented.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With