Unexpected behavior with loop macro and closures
Asked Answered
N

2

4

Why do these forms behave this way?

CL-USER>
(setf *closures*
      (loop for num in (list 1 2 3 4)
            collect (lambda ()
                      num)))
(     
#<COMPILED-LEXICAL-CLOSURE #x302004932E1F>
#<COMPILED-LEXICAL-CLOSURE #x302004932DCF>
#<COMPILED-LEXICAL-CLOSURE #x302004932D7F>
#<COMPILED-LEXICAL-CLOSURE #x302004932D2F>)
CL-USER> 
(funcall (first *closures*))
4
CL-USER> 
(funcall (second *closures*))
4

I would have expected the first funcall to return 1, and the second to return 2, etc. This behavior is consistent with both Clozure Common Lisp and Steel-Bank Common Lisp implementations.

If I rework the loop macro to a version using dolist, what I'd expect is what's returned:

(setf *closures*
      (let ((out))
        (dolist (item (list 1 2 3 4) (reverse out))
          (push (lambda () item) out))))
(
#<COMPILED-LEXICAL-CLOSURE #x302004A12C4F>
#<COMPILED-LEXICAL-CLOSURE #x302004A12BFF>  
#<COMPILED-LEXICAL-CLOSURE #x302004A12BAF>
#<COMPILED-LEXICAL-CLOSURE #x302004A12B5F>)
CL-USER> 
(funcall (first *closures*))
1
CL-USER> 
(funcall (second *closures*))
2

CL-USER>

What's going on with the loop macro version?

Neuroblast answered 30/3, 2013 at 3:12 Comment(0)
B
8

num is same variable shared by all lambdas.

Use

(setf *closures*
  (loop for num in (list 1 2 3 4)
        collect (let ((num1 num))
                  (lambda ()
                    num1))))

num1 is fresh variable for each iteration.

As of dolist, "It is implementation-dependent whether dolist establishes a new binding of var on each iteration or whether it establishes a binding for var once at the beginning and then assigns it on any subsequent iterations." (CLHS, Macro DOLIST). So it may work on one implementation and fail on other.

Bioclimatology answered 30/3, 2013 at 4:20 Comment(2)
BTW, you may name num1 as num ;) (let ((num num)) ...)Bioclimatology
I've used this before: (defmacro rebind (vars &body body) `(let ,(mapcar #'list vars vars) ,@body))Chez
A
4

The name num represents the same binding during the evaluation of LOOP. Maybe you want to write:

(mapcar 'constantly (list 1 2 3 4))

to get what you meant.

Aquarium answered 30/3, 2013 at 10:12 Comment(2)
This would work for the simplified example here, but not for the actual use case, where I need to collect lexical closures in the looping construct.Neuroblast
@ClaytonStanley Hmm, actually you can replace constantly with a (lambda (idx) ...), and replace the (list ...) with (alexandria:iota ...). If there's more iteration variables you may add more lists. Actually this is the FP way, so you always can use this if there's no state changing in the loop...Aquarium

© 2022 - 2024 — McMap. All rights reserved.