Scheme Programming/Local Scope

Scheme Programming
 ← Mutability Local Scope Object Orientation → 

This section defines local scope and variable scoping in general.

Variable scope concepts

edit

Scheme uses lexical scoping, which means that the variable scope can be immediately seen from the program text and that the variable scope is defined at compile time.

Scheme is block structured. A block here means a stretch of code lines that have start and end points. Block defines one scope and a scope is where a variable is defined (or bound). Blocks can be nested. There are outer and inner blocks.

Top level scope is a special scope, since by definition is has no outer scope. Top level scope is also called global scope. Any other scope is nested, and all of them are affected by the outer scope or scopes. Local scope is the same as the current scope.


Top level scope

edit

Top level scope is the outmost scope (block) in the program's scopes.

We define here a variable in the top level scope and evaluate its value:

> (define x 1)
> x
1


Formally we can say that variable x is bound to value 1. In general, top level scope contains global bindings which can be procedures or data values.


Local scope

edit

New scope (i.e. inner scope to the current) can be created with let expression:

> (let ((y 2))
>   y)
2

The value of a let expression is the last evaluated expression within the body of let. In this case we get the value of y, which is 2.

Variable y exists only in the inner scope:

> y
;; Error: unbound variable: y

However, variables from the top level are visible to all inner scopes:

> (define x 1)
> (let ((y 2))
>   (+ x y))
3

It is possible to use the same variable name in the inner scope. Even when the variable in the inner scope has the same name, as in the outer scope, they are different bindings and they are stored into different locations. Scheme uses variable bindings from the innermost scope where the variable identifier is found.

Thus we can shadow variables in the outer scope:

> (define x 1)
> (let ((x 7))
>   x)
7
> x
1

The inner x exists only within the let expression, but the outer x exists before and after the let expression.

There are no limits in the nesting depth of blocks:

> (define x 1)
> (let ((x 7))
>   (let ((x 13))
>     x))
13
> x
1

Note that, the code above is only demonstrating nesting of scopes and it does not make much sense in general.


Closures

edit

So far we have referred variables from normal expressions that produce data values. However, we can refer variables from outer scope also from procedures. The lexical scoping rules apply the same way to procedures as for other expressions. In fact let can be expressed as syntax extension of lambda, so actually we have referenced variables from procedures all the time.

When a procedure refers to a variable that is not bound within the procedure itself (i.e. bound in the outer scope), a closure is created. The external variables become part of the closure. This connection is so strong that even if the scope of a variable is outlived, it still exists in the closure. In that sense closures break the normal block scope of a variable. This happens when a closure is the passed value from an inner scope to the outer scope.

Here is a very simple example of a closure:

> (define x 0)
> (define inc-x (lambda ()
                  (set! x (+ x 1))
                  x))
> (inc-x)
1
> (inc-x)
2

inc-x increments x by 1 each time it is evaluated. The procedure does not take any arguments. inc-x refers to the external variable x. Also, it might be worth noting that it refers to + as well. + is actually a variable in Scheme that just happens to be bound to a procedure that adds numbers together.

Closures are more useful when lambda reference variables that can't be accessed outside. You can do this with let over lambda:

> (define inc (let ((x 0))
                (lambda ()
                  (set! x (+ x 1))
                  x)))
> (inc)
1
> (inc)
2
> x
;; x is unbound variable

Here same as in previous example lambda is closed over x variable, but this time it's inside let so it's not accessible outside.

Closures can be used in many ways. Higher order functions take other functions as arguments and apply them, and an argument can be a closure. Closures can also be used to emulate objects. They maintain state in an external variable, and update the state in each application.

Here is an example, where closure is used along with high-order function map:

> (define x 1)
> (map (lambda (num)
         (+ num x))
       '(10 11 12))
(11 12 13)

map takes two arguments: a procedure and a list. The procedure is applied to each element in the list and a new list is created from results of the procedure application.

There are two scopes involved in this example: the top level scope and the inner scope created by lambda. num exists in the local scope of lambda, but x does not. x is hence part of the closure created by lambda. map is part of a normal expression, and it does not start a new scope.