Next: , Previous: Lambda Expressions, Up: Special Forms


2.2 Lexical Binding

The three binding constructs let, let*, and letrec, give Scheme block structure. The syntax of the three constructs is identical, but they differ in the regions they establish for their variable bindings. In a let expression, the initial values are computed before any of the variables become bound. In a let* expression, the evaluations and bindings are sequentially interleaved. And in a letrec expression, all the bindings are in effect while the initial values are being computed (thus allowing mutually recursive definitions).

— special form: let ((variable init) ...) expression expression ...

The inits are evaluated in the current environment (in some unspecified order), the variables are bound to fresh locations holding the results, the expressions are evaluated sequentially in the extended environment, and the value of the last expression is returned. Each binding of a variable has the expressions as its region.

MIT/GNU Scheme allows any of the inits to be omitted, in which case the corresponding variables are unassigned.

Note that the following are equivalent:

          (let ((variable init) ...) expression expression ...)
          ((lambda (variable ...) expression expression ...) init ...)

Some examples:

          (let ((x 2) (y 3))
            (* x y))                              ⇒  6
          
          (let ((x 2) (y 3))
            (let ((foo (lambda (z) (+ x y z)))
                  (x 7))
              (foo 4)))                           ⇒  9

See Iteration, for information on “named let”.

— special form: let* ((variable init) ...) expression expression ...

let* is similar to let, but the bindings are performed sequentially from left to right, and the region of a binding is that part of the let* expression to the right of the binding. Thus the second binding is done in an environment in which the first binding is visible, and so on.

Note that the following are equivalent:

          (let* ((variable1 init1)
                 (variable2 init2)
                 ...
                 (variableN initN))
             expression
             expression ...)
          
          (let ((variable1 init1))
            (let ((variable2 init2))
              ...
                (let ((variableN initN))
                  expression
                  expression ...)
              ...))

An example:

          (let ((x 2) (y 3))
            (let* ((x 7)
                   (z (+ x y)))
              (* z x)))                           ⇒  70
— special form: letrec ((variable init) ...) expression expression ...

The variables are bound to fresh locations holding unassigned values, the inits are evaluated in the extended environment (in some unspecified order), each variable is assigned to the result of the corresponding init, the expressions are evaluated sequentially in the extended environment, and the value of the last expression is returned. Each binding of a variable has the entire letrec expression as its region, making it possible to define mutually recursive procedures.

MIT/GNU Scheme allows any of the inits to be omitted, in which case the corresponding variables are unassigned.

          (letrec ((even?
                    (lambda (n)
                      (if (zero? n)
                          #t
                          (odd? (- n 1)))))
                   (odd?
                    (lambda (n)
                      (if (zero? n)
                          #f
                          (even? (- n 1))))))
            (even? 88))                           ⇒  #t

One restriction on letrec is very important: it shall be possible to evaluated each init without assigning or referring to the value of any variable. If this restriction is violated, then it is an error. The restriction is necessary because Scheme passes arguments by value rather than by name. In the most common uses of letrec, all the inits are lambda or delay expressions and the restriction is satisfied automatically.