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:

  1. 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).

  2. 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.

  1. The top-level binding of x to #t; this variable is referenced in the definition of y.

  2. The parameter to the function f, which has type Integer. This binding shadows the top-level binding, which is why the body of f passes the typechecker. This binding is referenced in the body of f.

  3. The parameter to the function g, which has type Any. This binding also shadows the top-level binding.

  4. The local binding of x inside the body of g. This binding shadows the parameter binding and is referenced inside the body of the local expression. Thus, the function g will always return 2 as a result.

While Racket allows including a define in certain contexts where an expression is required, we use the local declaration form to clearly define the scope of the definitions in this course.

Last updated 2020-01-24 15:55:22 -0600
Table of Contents | Back to CS151 Home