So far, we have bound variables to values in two ways: at top-level using define
,
and as function parameters. Typed Racket also supports local definitions inside
expressions. Local definitions provide two important features:
-
they provide local names for intermediate values, which is useful for breaking complicated expressions into a series of simpler ones and provides a way to share the results of computations (instead of computing the same expression multiple times).
-
they provide a way to limit the scope (or visibility of variables such as helper functions).
The syntax of a local
definition is
(local { ... defs ... } expr)
where definitions appear between the curly braces and the expr
is evaluated
to produce the result (by convention, use {
}
to enclose the local
definitions, but remember that they are the same as using parentheses.
A simple example of a local definition is the following expression, where
we bind n1
to the value of (+ n 1)
.
(local
{(define n1 (+ n 1))}
(* n1 n1))
Locals can also be used to define helper functions (including their type signatures). For example, here is a tail recursive version of the factorial function written using a helper.
(: fact : Natural -> Natural)
(define (fact n)
(local
{(: fact-aux : Natural Natural -> Natural)
(define (fact-aux i acc)
(if (<= i n)
(fact-aux (+ i 1) (* i acc))
acc))}
(fact-aux 1 1)))
Notice that the fact-aux
function references n
, which is bound as a parameter
to the outer function fact
. This is an example of using lexical scoping to avoid
needing extra parameters to the helper function.
Lexical scoping
Lexical scoping is an important concept in programming languages. It is a mechanism for statically determining for a variable occurrance which binding is being referenced. A closely related notion is the scope of a variable. We have already seen this concept in play with function parameters, which can hide (or shadow) top-level bindings of the same name and which have scope limited to the body of the function. With local definitions, the scoping story becomes more complicated. Consider the following three Typed Racket definitions:
(define x : Boolean #t)
(: f : Integer -> Integer)
(define (f x) (+ x x))
(: g : Any -> Integer)
(define (g x)
(local
{(define x 2)}
x))
(define y : Boolean (not x))
There are four distinct bindings of the variable x
in this code.
-
The top-level binding of
x
to#t
; this variable is referenced in the definition ofy
. -
The parameter to the function
f
, which has typeInteger
. This binding shadows the top-level binding, which is why the body off
passes the typechecker. This binding is referenced in the body off
. -
The parameter to the function
g
, which has typeAny
. This binding also shadows the top-level binding. -
The local binding of
x
inside the body ofg
. This binding shadows the parameter binding and is referenced inside the body of thelocal
expression. Thus, the functiong
will always return2
as a result.
While Racket allows including a |