Semantics of let
Key point: A let
scope is evaluated immediately (including both bindings and expressions after bindings, i.e., everything within the outer parentheses of the let
), even if the let
is nested inside an inner function that hasn’t been called yet.
Example:
(define (comp x)
(if (> 3 x)
(display "then-clause")
(display "else-clause")
)
#|
(define foo1
(let ((bar1 (/ 2 0))) ; * evaluted immediately
(display "should not be printed")
)
)
|#
(define foo2
(let ((bar2 (/ 5 2))) ; * evaluted immediately
(newline)
(display "let in `foo2`, bar2: ")
(display bar2)
)
)
)
Running examples:
-
foo1
: We can see that thelet
binding is evaluated, otherwise we wouldn’t get a division by zero exception.prompt> (comp 4) else-clause ;Division by zero signalled by /. ;To continue, call RESTART with an option number: ;snip
-
foo2
: We can see that the expressions after thelet
binding are evaluated, otherwise we wouldn’t see the display output.prompt> (comp 2) then-clause let in `foo2`, bar2: 5/2 ;Unspecified return value
Semantics of if
The semantics of if
: It evaluates the condition first, then decides whether to evaluate the then-clause or else-clause based on the result.
A good reference for this is SICP (2nd Edition) Exercise 1.6, where an abstraction is used to define new-if
using cond
:
(define (new-if predicate then-clause else-clause)
(cond (predicate then-clause)
(else else-clause)
)
)
The semantics of function application (applicative order evaluation) requires evaluating arguments first (like then-clause and else-clause here) before applying the function. This is why you can’t write recursive expressions in then-clause or else-clause - they would be evaluated regardless of the predicate’s value, leading to infinite recursion. if
/cond
/… are special forms with different semantics compared to abstractions defined through define
. I like the new-if example because it elegantly demonstrates Lisp’s metaprogramming features. Following the substitution model, predicate, then-clause, and else-clause can be replaced with any expressions you need, where expressions are enclosed in parentheses - the parentheses mark expression boundaries, and you can put parenthesized expressions in any parameter position (as long as they satisfy the implicit type constraints of the abstraction).
An Error Caused by Immediate let
Evaluation
Consider this prime number checking code:
(define (prime? x)
(if (or (= x 1) (= x 2))
#t
test_prime
)
(define (divisible? y)
(= 0 (remainder x y))
)
(define (iter_biggest_divisor y)
(cond ((= y 1) 1)
((divisible? y) y)
(else (iter_biggest_divisor (- y 1)))
)
)
(define test_prime
(let ((biggest_divisor (iter_biggest_divisor (quotient x 2)) )) ; Notice
(display biggest_divisor)
(if (= biggest_divisor 1)
#t
#f
)
)
)
)
The results when running:
prompt> (prime? 1)
;The object 0, passed as the second argument to integer-remainder, is not in the correct range.
prompt> (prime? 2)
1
;Value: #t
According to programmers’ expectations, both (prime? 1)
and (prime? 2)
should directly return #t
, rather than the former throwing an error and the latter showing the behavior of (display biggest_divisor)
. As stated earlier, this occurs because the entire let
scope is evaluated immediately.