2

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?

Óscar López
  • 232,561
  • 37
  • 312
  • 386
SultanLegend
  • 535
  • 3
  • 11
  • 3
    See https://docs.racket-lang.org/guide/eval.html#%28part._.Local_.Scopes%29 – Sorawee Porncharoenwase Jan 27 '19 at 04:04
  • 2
    Why do you use `eval` at all? If you only need specified `next-noun` behavior, then you get it with `(define next-noun (sequence->generator (shuffle nouns)))`. – user4003407 Jan 27 '19 at 09:15
  • @PetSerAl I agree that using `sequence->generator` would have been a nice solution, but how would you reset the generator with a new reshuffled list every time is consumed? `sequence->repeated-generator` just keeps returning the elements in the same order. – Óscar López Jan 27 '19 at 10:36
  • @ÓscarLópez I do not see where OP require that list should be reshuffled when exhausted, but in that case you can use `(define next-noun (infinite-generator (for-each yield (shuffle nouns))))`. – user4003407 Jan 27 '19 at 10:53
  • It's right there in the code: `(if (null? (cdr lst-nouns)) (shuffle nouns)` – Óscar López Jan 27 '19 at 10:54
  • @PetSerAl that's a very nice solution! hope you don't mind, I added it to my post as more idiomatic alternative – Óscar López Jan 27 '19 at 11:14

2 Answers2

4

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
...
Óscar López
  • 232,561
  • 37
  • 312
  • 386
0

You issue is with eval. eval does not have lexical environment from where it is called rather it has at most the top level bindings. Eg.

(define x 12)
(let ((x 10))
  (eval '(+ x x))) ; ==> 24

eval is almost always the wrong solution and can often be replaced with closures and called directly or with apply. Here is what I would have done:

(define (shuffle-generator lst)
  (define shuffled (shuffle lst))
  (define (next-element)
    (when (null? shuffled)
      (set! shuffled (shuffle lst)))
    (begin0
      (car shuffled)
      (set! shuffled (cdr shuffled))))
  next-element)

(define next-int15 (shuffle-generator '(1 2 3 4 5)))
(define random-bool (shuffle-generator '(#t #f)))
(random-bool) ; ==> #f
(next-int15) ; ==> 5
(next-int15) ; ==> 4
(next-int15) ; ==> 2
(next-int15) ; ==> 1
(next-int15) ; ==> 3
(next-int15) ; ==> 3
(random-bool) ; ==> #t
(random-bool) ; ==> #t

The returned values are random so it's just what I got my first round. Instead of naming next-element one could simply just return the lambda, but the name gives information on what it does and the debugger will show the name. eg.:

next-int15 ; ==> #<procedure:next-element>
Sylwester
  • 47,942
  • 4
  • 47
  • 79