I am trying to see how to rebind a lexical binding, or redefine the closure of a lambda. The expected usage of next-noun is just to call it as many times as desired with no arguments. It should return a random noun from the list, but one that has not been returned yet until the list is exhausted.
Here is the toy example I am using:
#lang racket
(define nouns `(time
year
people
way
day
man))
(define (next-noun)
(let* ([lst-nouns (shuffle nouns)]
[func-syn
`(λ ()
(let* ([n (car lst-nouns)]
[lst-nouns (if (null? (cdr lst-nouns))
(shuffle nouns)
(cdr lst-nouns))])
(set! next-noun (eval func-syn))
n))])
((eval func-syn))))
When trying to run it I get this error:
main.rkt>
main.rkt> (next-noun)
; lst-nouns: undefined;
; cannot reference an identifier before its definition
; in module: "/home/joel/projects/racket/ad_lib/main.rkt"
Which confuses me since there should be a binding for lst-nouns any time (eval func-syn) is run. What's going on?
You don't need to use eval
here, at all. It's making the solution more complex (and insecure) than needed. Besides, the "looping" logic is incorrect, because you're not updating the position in lst-nouns
, and anyway it gets redefined every time the procedure is called. Also, see the link shared by Sorawee to understand why eval
can't see local bindings.
In Scheme we try to avoid mutating state whenever possible, but for this procedure I think it's justified. The trick is to keep the state that needs to be updated inside a closure; this is one way to do it:
(define nouns '(time
year
people
way
day
man))
; notice that `next-noun` gets bound to a `lambda`
; and that `lst-nouns` was defined outside of it
; so it's the same for all procedure invocations
(define next-noun
; store list position in a closure outside lambda
(let ((lst-nouns '()))
; define `next-noun` as a no-args procedure
(λ ()
; if list is empty, reset with shuffled original
(when (null? lst-nouns)
(set! lst-nouns (shuffle nouns)))
; obtain current element
(let ((noun (car lst-nouns)))
; advance to next element
(set! lst-nouns (cdr lst-nouns))
; return current element
noun))))
@PetSerAl proposed a more idiomatic solution in the comments. My guess is that you want to implement this from scratch, for learning purposes - but in real-life we would do something like this, using Racket's generators:
(require racket/generator)
(define next-noun
(infinite-generator
(for-each yield (shuffle nouns))))
Either way it works as expected - repeatedly calling next-noun
will return all the elements in nouns
until exhausted, at that point the list will be reshuffled and the iteration will restart:
(next-noun)
=> 'day
(next-noun)
=> 'time
...