How can I copy a counter made using a Lisp closure?
Asked Answered
D

6

6

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?

Dugaid answered 10/2, 2016 at 15:23 Comment(0)
A
7

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)))
Armoured answered 10/2, 2016 at 16:11 Comment(0)
L
10

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
Letdown answered 10/2, 2016 at 20:18 Comment(1)
For simple objects such as this, defstruct is appropriate.Monogyny
A
7

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)))
Armoured answered 10/2, 2016 at 16:11 Comment(0)
M
3

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.

Monogyny answered 11/2, 2016 at 9:58 Comment(0)
C
0

Here is another solution, in which copy-counter gets a counter as argument and returns a new counter starting from the current value of the parameter, but using another variable:

(defun new-counter(&optional (n 0))
  (lambda (&optional noincrement)
    (if noincrement n (incf n))))

(defun copy-counter(c)
  (new-counter (funcall c t)))

This is a test:

CL-USER> (let* ((up1 (new-counter))
                (up2 (progn (funcall up1) (funcall up1) (copy-counter up1))))
           (print (funcall up2))
           (print (funcall up2))
           (print (funcall up2))
           (print (funcall up1))
           "end test")

3 
4 
5 
3 
"end test"
Conventionalism answered 10/2, 2016 at 17:35 Comment(0)
C
0

The easiest way to approach this problem would be for make-up-counter to take a number from which to count up.

(defun make-up-counter (&optional (initial-count 0))
  (let ((count initial-count))
    #'(lambda () (incf count))))

However this doesn't solve the problem as we have no way to inspect the value in the counter, so if we try to use the counter to seed the value we will modify the current counter.

(defun copy-counter (counter)
  (make-up-counter (funcall counter)))

To provide a way to obtain the value we make the closure take an 'operation' parameter, so we can either inspect or increment the value

(defun make-up-counter (&optional (initial-count 0))
  (let ((count initial-count))
    #'(lambda (&optional (operation :increment))
        (ecase operation
          (:inspect count)
          (:increment (incf count))))))

(defun copy-counter (counter)
  (make-up-counter (funcall counter :inspect)))

Now when we run the following code

(let ((1st-counter (make-up-counter)))
  (loop :repeat 3 :do (funcall 1st-counter))
  (let ((2nd-counter (copy-counter 1st-counter)))
    (loop :repeat 3 :do (funcall 1st-counter))
    (format t "1st counter: ~A~%2nd counter: ~A~%"
            (funcall 1st-counter :inspect)
            (funcall 2nd-counter :inspect))))

it prints

1st counter: 6

2nd counter: 3

Caracalla answered 10/2, 2016 at 19:46 Comment(0)
D
0

What we can do is define an API for constructing a counter such that:

(make-counter <integer>) --> yields new counter starting at <integer>
(make-counter <counter>) --> yields a clone of counter

The trick is that we give the counting function (the counter itself) an optional argument also. If that argument is nil, then it just counts. Otherwise it clones itself. This is just basic OOP: the counter is an object, and it has two methods. When the make-counter API is asked to clone the counter, it delegates to the copy "method".

(defun make-counter (&optional (constructor-arg 0))
  (etypecase constructor-arg
    (integer 
      (lambda (&optional opcode) ;; opcode selects method
        (ecase opcode ;; method dispatch
          ((nil) (prog1 constructor-arg (incf constructor-arg)))
          (copy-self (make-counter constructor-arg)))))
    (function
      (funcall constructor-arg 'copy-self))))

Test run:

[1]> (defvar x (make-counter 3))
X
[2]> (funcall x)
3
[3]> (funcall x)
4
[4]> (defvar y (make-counter x))
Y
[5]> (funcall x)
5
[6]> (funcall x)
6
[7]> (funcall x)
7
[8]> (funcall y)
5
[9]> (funcall y)
6
[10]> (funcall x)
8
[11]> (funcall y)
7

Rather than overloading the make-counter constructor in this way, we can split it into a two-function API:

(defun make-counter (&optional (constructor-arg 0))
  (lambda (&optional opcode)
    (ecase opcode
      ((nil) (prog1 constructor-arg (incf constructor-arg)))
      (copy-self (make-counter constructor-arg)))))

(defun copy-counter (counter)
  (funcall constructor-arg 'copy-self))

copy-counter is nothing more than a wrapper for the lower-level opcode-based dispatch API on the object.

Denticle answered 13/2, 2016 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.