schemeletrec

Is letrec only meant for defining procedures?


Is Scheme's letrec only meant for defining procedures, especially recursive ones? I am asking because it appears to be possible to bind non-procedures using letrec. For example, (letrec ((x 1) (y 2)) (+ x y)). If Scheme's letrec is only meant for procedures, then why isn't its syntax constrained to allow procedures only?

Consider this use of letrec to define two mutually recursive procedures:

(letrec ((is-even? (lambda (n)
                     (let ((n (abs n)))
                       (if (= n 0)
                           #t
                           (is-odd? (- n 1))))))
         (is-odd? (lambda (n)
                    (let ((n (abs n)))
                      (if (= n 0)
                          #f
                          (is-even? (- n 1)))))))
  (is-even? 123))

If we use Common Lisp's LABELS instead of Scheme's letrec, these two mutually recursive procedures would be defined like this instead:

(labels ((even-p (n)
           (let ((n (abs n)))
             (if (= n 0)
                 t
                 (odd-p (- n 1)))))
         (odd-p (n)
           (let ((n (abs n)))
             (if (= n 0)
                 nil
                 (even-p (- n 1))))))
  (even-p 123))

If letrec is only useful for defining procedures, then why isn't its syntax constrained like that of LABELS to allow only procedures in its initialization forms?


Solution

  • It is mostly useful for binding procedures, yes: the variables bound by letrec can't refer to their own bindings until afterwards, so something like

    (letrec ((x (list x)))
      x)
    

    Does not work: see my other answer.

    However first of all it would be a strange restriction, in a Lisp-1 like Scheme, to insist that letrec can only be used for procedures. It's also not enforcable other than dynamically:

    (letrec ((x (if <something>
                    (λ ... (x ...))
                    1)))
      ...)
    

    More importantly, letrec can be used in places like this:

    (letrec ((x (cons 1 (delay x))))
      ...)
    

    which is fine, because the reference to the binding of x is delayed. So if you have streams, you can then do this:

    ;;; For Racket
    (require racket/stream)
    
    (letrec ((ones (stream-cons 1 ones)))
      (stream-ref ones 100))