2

In the paper Fixing Letrec: A Faithful Yet Efficient Implementation of Scheme’s Recursive Binding Construct by Dybvig et al. it is said that (emphasis mine):

A theoretical solution to these problems is to restrict letrec so that its left-hand sides are unassigned and right-hand sides are lambda expressions. We refer to this form of letrec as fix, since it amounts to a generalized form of fixpoint operator. The compiler can handle fix expressions efficiently, and there can be no violations of the letrec restriction with fix. Unfortunately, restricting letrec in this manner is not an option for the implementor and would in any case reduce the generality and convenience of the construct.

I have not scrutinized the R5RS report, but I have used letrec and the equivalent "named let" in Scheme programs and the unfortunate consequences mentioned in the paper are not clear to me, can someone enlighten me ?

duplode
  • 31,361
  • 7
  • 69
  • 130
Rhangaun
  • 1,262
  • 2
  • 14
  • 34

2 Answers2

3

The R5RS letrec restriction says something like these are in violation:

(let ((x 10))
  (letrec ((x x))
    x))

(letrec ((y (+ x 5)) 
         (x 5)) 
  (list x y))

Thus, it's not specified what would happen and it would certainly not be portable Scheme. It may evaluate to 10 and (5 10), the implementation might signal an error or you get an undefined value that might result in an error getting signaled. I have tested Racket, Gambit, Chicken and Ikarus and not one of them signal anything in the first case and they all evaluate to an unspecified value. Ikarus is the only one that returned (5 10) in the latter while the others all got contract errors since an unspecified value as argument violates +'s contract. (Ikarus always evaluates operands right to left)

The R[567]RS reports all state that if all expressions are lambda expressions you have nothing to worry about and I think that is the clue. Another is that you should not try to shadow like you would do with (named) let.

There is a follow up on the original paper that is entitled Fixing letrec (reloaded) that has macros that implements the "fix".

Sylwester
  • 44,544
  • 4
  • 42
  • 70
1

With equational syntax,

letrec x = init-x
       y = init-y
  body....

the restriction is that no RHS init... expression can cause evaluation of (or assignment to) any of the LHS variables, because all init...s are evaluated while all the variables are still unassigned. IOW no init... should reference any of the variables directly and immediately. It is OK of course for any of the init...s to contain lambda-expressions which can indeed reference any of the variables (that's the purpose of letrec after all). When these lambda-expressions will be evaluated, the variables will be already assigned the values of the evaluated init... expressions.

The authors say, to require all the RHSes to be lambda-expressions would simplify the implementation, because there's no chance for misbehaving code causing premature evaluation of LHS variables inside some RHS. But unfortunately, this changes letrec's semantics and thus is not an option. It would also prohibit simple use of outer variables in RHSes and thus this new cut-down letrec would also be less general and less convenient.


You also mention named let but it is not equivalent to letrec: its variables are bound as-if by let, only the looping function itself is bound via letrec:

(let ((x 1)(y 2))
  (let g ((x x) (y x))
    (if (> x 0) (g (- x y) y) (display x)))
  (display x))
01
;Unspecified return value

(let ((x 1)(y 2))
  (letrec ((g (lambda (x y) 
                (if (> x 0) (g (- x y) y) (display x)))))
     (g x x))  ; no 'x' in letrec's frame, so refer to outer frame
  (display x))
01
;Unspecified return value
Will Ness
  • 62,652
  • 8
  • 86
  • 167
  • So, by generality/convenience loss, it simply meant that if you restrict yourself to lambdas on the RHS, you cannot do something like : (letrec ((x 3) (y (f 4)) (z (lambda (a b) ...)) (z x y)) – Rhangaun Jul 15 '13 at 15:50
  • @Skeptic yes, exactly, because you reference an undefined (here) variable `f`, which thus must be defined at some outer scope; and it is not inside any lambda expression. And even as simple a value as `3`. – Will Ness Jul 15 '13 at 16:03
  • But that is only because the standard requires to check that the RHS variables are not used before entering the body, if we forfeit that ability, we can have both lambdas and simple expressions in a letrec implemented without set! right ? – Rhangaun Jul 15 '13 at 18:01
  • @Skeptic LHS vars can't be used in init (RHS) expressions unless behind `lambda`. But other vars and direct values can be used. That's what we have right now anyway. – Will Ness Jul 15 '13 at 18:11
  • @Skeptic but the article says, the implementation leads to non-optimized code. *Were* only lambdas allowed, the implementation would be optimized. But disallowing other vars and simple values (only allowing lambda-exprs) changes semantics (and is inconvenient - instead of `3` we'd have to write `(lambda() 3)` and call it accordingly). So they propose analyzing the bindings and sorting them out into different categories. The lambda-exprs then can be implemented in optimized way, without setting any temporaries, since there's no danger apriori. – Will Ness Jul 15 '13 at 18:19
  • Oops, I meant the LHS variables. What I was asking myself is : is it only because we want to check that the variables are properly used that we can't optimize all cases (inluding constants and free variables). – Rhangaun Jul 15 '13 at 19:34
  • @Skeptic AFAICT, "optimize" means directly plug the RHS expressions into letrec's env.frame bindings' value slots. No temporaries, no intervening `set!`s. But then if there's an invalid use of an LHS var in some RHS expr, we're screwed. And if by chance we aren't, we shouldn't allow it anyway. That's all. – Will Ness Jul 15 '13 at 19:52