Search code examples
bindingschemelispracket

How to refresh, remake, lexical bindings on a lambda?


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?


Solution

  • 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
    ...