Prev  

Scope, Visibility and Modules [Chapter 8]

Scope

When a binding is made, it rarely affects the entire piece of software. The scope of a binding is that part of a program where the binding is in effect.

Scope within a single definition

Most languages use the lexical scope rule, which relies on a definition of a notion of a block. Definitions made in a block are visible only in that block.

In C++, a block is a compound statement {...} or a function definition or a for-loop. Figure out the scopes of bindings in the following.

 void sort(int a[], int n)
 {
   int i = 1;
   while(i < n) {
     int j = i;
     while(j > 0 && a[j] < a[j-1]) {
       int t = a[i];
       a[i] = a[j];
       a[j] = t;
       j = j - 1;
     }
     i = i + 1;
   }
 }

Shadowing

The scope of a binding can contains holes where it is not visible. These occur where another binding of the same identifier has precedence, and shadows the other binding. In the following, one binding of x shadows the other.

  {
    int x = 3;
    int y;
    ...
    {
      int x;
      ...
      x = 2;
      ...
    }
    cout << x;
  }

Scope at the global level

Some older languages, such as Algol 60, tried to use the local scope rule on the scale of an entire program. You hide a function definition (limiting its scope) by placing it inside another function that needs to use it.

  integer procedure outer(x);
    integer x;
    begin
      integer procedure inner(integer y)
        integer y;
        begin
          ...
        end;
      
      ... (Use function inner here)
    end;

That does not work well, though. Here is a typical scenario. You are implementing a hash table module. Several functions (insert, lookup, delete) need to share a function locate that finds where a thing should go in a hash table. You want to make locate available to those functions, but not to other functions. But you cannot put it inside insert, or it becomes invisible to lookup and delete. Using the local scope rule, all you can do is make locate visible to all functions.

A different approach is based on modules. A module is a collection of things (think of it as a collection of identifier bindings) that can be used to control the scope of bindings.

Export control

A module controls which bindings it exports to other modules. Typically, some of the bindings are marked private, visible only within this module, while others are marked public, visible in other modules as well as this one.

Import control

A module can usually control which bindings exported by other modules are visible here. It imports the other modules. Some languages allow a module to be selective, and to import only some of the bindings made in other modules. In Modula2, for example, you write

  FROM InOut IMPORT Writeln;
to import only the binding of identifier Writeln from the module called InOut.

Naming on a large scale

Software can be built by combining modules from various sources, coming from all over the world. When you are selecting names for things, you need to think on a global scale. What if two different modules, produced in different countries, each define a function called frog, taking an integer parameter and producing an integer. Then you cannot put those two modules into the same piece of software. You cannot use overloading to decide which one you want, since they have the same type, and type information is usually the means of disambiguating overloaded names.

A solution is to use very long names. A function defined by the Amphibian division of the Animal.com corporation might call their function com.Animal.Amphibian.frog.

Well chosen long names solve the name conflict problem. But they are certainly inconvenient. It is awkward to write

  x = com.Animal.Amphibian.from(y);
To compute a square root in Ada, you write
 S = 
   Ada.Numerics.GenericElementaryFunctions.Sqrt(X);
A solution to this inconvience is a way of shortening names. Typically, you allow writing short names for a collection of long names. In Ada, writing
  use Ada.Numerics.GenericElementaryFunctions;
allows you to write
  S = Sqrt(X);
You do this kind of thing when name conflicts are not a problem, and you avoid it when name conflicts rear their heads.

Long names in C++

C++ uses namespaces to deal with long names. Definition

  namespace Amphibian {
    int frog(int x)
    {
      ...
    }
  }
defines identifier Amphibian::frog. You can write
  x = Amphibian::frog(y);
or, to shorten this name to frog, write
  using Amphibian::frog;
or, to shorten all names beginning Amphibian::, just write
  using namespace Amphibian;


Prev