They are different in the order in which variables are bound. Consider this for example:
> (let ((a 1)(b (+ a 2))) b)
This code will FAIL because b
requires a
, which has not been defined before. It is defined, in the same let
, but Scheme will take all your let
definitions as only one statement and not allow them to reference each other. In Gambit Scheme, it raises:
*** ERROR IN ##raise-unbound-global-exception -- Unbound variable: a
Conversely, let*
will bind the first variable of the let
, then the second, etc... so:
> (let* ((a 1)(b (+ a 2))) b)
3
Works as expected.
A third form which is of interest is letrec
which lets not only variables in the let
reference other variables, but also let them reference themselves (e.g. for recursion). This lets you write code like:
> (letrec ((f (lambda(n) ;; Takes the binary log2 recursively
(cond
((= n 1) 0)
(else (+ 1 (f (/ n 2))))))))
(f 256)) ;; 2^8 = 256
8
If you try to define a recursive function with let
or let*
, it will tell you the variable is unbound.
All of this can be achieved via clever rearranging/nesting of the let statements, but let*
and letrec
can be more convenient and readable in some cases like these.