Chapter 8: Name spaces
8.1 Scopes
8.2 Constants
8.3 Disambiguators
8.4 Import from
8.5 Inline from
8.6 Variables
8.7 Functions
8.8 Where
8.9 For iterators
8.10 Add
8.11 With
8.12 Application
8.13 Declarations
8.14 Fluid variables
For a computer, or a human being for that matter, to understand an Aldor program it is necessary to establish the context which gives the meanings of symbols. This is perhaps more important than in other familiar programming environments, since Aldor has fewer built-in assumptions. Much of what is built-in with other programming languages is provided by libraries in Aldor. For example, Aldor libraries define the types "Boolean", "Integer", and "DoubleFloat" and their operations. Typically, the interface to these libraries is through an include file which imports the library so that it may be used in the current program.
This chapter describes how symbols in a program are associated with particular meanings, and how a meaning is selected when several are applicable.
8.1 : Scopes
A symbol's meaning is given by the context in which it appears. A particular meaning (for example: ``n is the second parameter in the definition of the `+' function in the domain Integer'') has a visibility, or scope, governed by the constructs in which it is introduced. In common with most programming languages, Aldor mainly uses lexical scoping.
New scopes in Aldor are introduced by the following expressions:
These forms may be nested to any depth. Note that the last two bind names in particular positions in the expression, and do not form general scope levels.
Lexical scoping implies that the only variables visible at a given point in a program are those that have been created locally or imported into scopes surrounding the current point.
In Aldor, there are two types of meaning for a given symbol --- it must either be a constant or a variable. Constants are created in the following ways
A constant may not be assigned to, and therefore holds the same value throughout its lifetime. If two constants have identical names in the same scope (the name is overloaded), then an assigned variable's type or a qualification (using $) is used to disambiguate the uses of the constants.
A variable may be created explicitly, by a declaration, or implicitly, when it appears on the left of the assignment operator, ``:=''. Variables may be re-assigned; they may not be used in type-forming expressions.
When a scope forming expression is used in Aldor, all definitions and declarations directly within that scope are visible throughout the scope --- sequences have no effect on what names are in scope. In the example
... { x: Integer := 4; y: Integer := 3; } adds(a: Integer): Integer == x + a + z; z: Integer := 3 + y ...
the current scope is extended with the variables x, y and z along with the constant adds. Note that adds uses z before it is defined in the outer sequence, and that x and y are defined in a subsequence, but the definitions are at the same scoping level as the others in the example.
A type may be viewed as an environment, mapping constant names and
types into value bindings from a particular type. In Aldor,
object files and libraries are values which map names and types
into the values defined inside the file or library. This idea allows
types, object files and libraries to be treated uniformly.
8.2 : Constants
A constant definition may appear at almost any point in a program. If its outer defining scope is a with or add then it will be treated as an export of that type. If the type is used in a context not requiring the export, then the export will not be visible when the type is imported. A definition returns the type of the new variable.
A particular name in a scope may be overloaded with several constant values, either defined in the local scope or imported into it.
#include "aldor" x: Integer == 3; x: String == "hello"; print << (x+x) << newline; -- uses x: Integer print << concat(x,x) << newline; -- uses x: String
Here the name x refers to both a constant of type String
and a constant of type Integer. In the two print statements, the
constant to be used is selected according to context --- the first
requires an Integer, while the second requires that x must
have a ``#'' operation, which implies that the String constant is
used.
8.3 : Disambiguators
Occasionally, it is not possible to tell which constant to use given a particular name. For example
x: String == "Hi"; x: List Integer == [1,2,3]; a: SingleInteger := 1; print << x.a << newline;In this case, "x.a" is ambiguous, as it may refer to either the apply operation from List Integer or the apply from String. In this case, we can specify which is required by restricting the result to be of an appropriate type. If the first Character of the string is wanted then the print line should read
print << (x.a)@Character << newline;
Sometimes a constant with the same name and type may be imported from different domains. In this case the package call operator, $, can be used to disambiguate the constants. For example, both Boolean and SingleInteger export a constant ``#'' which is an integer storing the number of possible values of the domain. Thus
print << #$Boolean << newline; print << #$SingleInteger << newline;
will print 2, and then a number 2n, for some n depending on the
host machine.
8.4 : Import from
The ``import'' statement brings constants which are exported from types into scope. The simplest form of an import statement is
import from D1,..., Dn;
with n &<= 1. This form imports all of the exported constants from the given domains (these are treated as type-forming expressions, see chapter 13). The importations are made in sequence, so that later domains can depend on exports from earlier ones. The precise exports of types are given by the category of the type, as described in section 7.9.
The complete form of the import statement is:
import restrictions from D1,..., Dn;
This form is used less often than the previous one, typically when a small number of operations are required from a group of types. Importing the ``<<'' operations from a type is a common example.
This form introduces the exported constants from D1,..., Dn into the current scope. In this case, the restrictions are either a category expression or a sequence of declarations.
import { +: (%, %) -> %; -: %-> % } from Integer, String, Float; import AbelianGroup from DoubleFloat, Ratio Integer;
The first statement will import the constants for addition and negation from Integer and Float into the current scope. Nothing will be imported from String as this type does not export either operation.
The second will import the operations necessary to satisfy AbelianGroup from the given domains.
Values created by programs written in other languages can be made visible
using an import from a ``Foreign'' type.
This is described in section 13.15.
8.5 : Inline from
The ``inline'' keyword is similar to the ``import'' keyword, but instead of importing names into a scope, it allows the result of compiling the current scope to depend on the compiled values of the constants indicated. This dependency information may then be used by the compiler to determine if a particular function may be inlined (for optimization) instead of called in the normal way. Allowing this form of optimization can give very high quality code. The cost is that, if a file is recompiled, then every file which contains an inline statement mentioning that file should also be recompiled to ensure consistency, slowing down the development cycle.
#library Aldor "aldor" import from Aldor; import from Integer; import from DoubleFloat; import from TextWriter; -- For `print' and ... import from Character; -- for `newline' and `<<' -- (all normally imported -- when `aldor' included). inline from Integer; print << 3 + 2 << newline; -- 3 + 2 gets optimized. print << 3.0 + 2.0 << newline; -- 3.0 + 2.0 doesn't.
(See section section 16.2 and section section 16.3 for an explanation of ``#library''.) This code, when compiled with optimization on, converts the function calls from the Integer domain into machine-level operations, but leaves the other operations as ordinary function calls.
One consequence of granting permission to inline from a domain is that permission is also granted to inline from all types exported by the domain. So using inline from Aldor, for example, grants permission to inline from {\em all} the domains in the above example (and the rest of the Aldor base library). The standard include file ``aldor.as'' includes this statement, so all files compiled using it automatically have a dependency on the base library.
As in the import statement, there may also be restrictions on
the particular constants allowed to be inlined.
8.6 : Variables
New variables are created by declaration statements (described below), or implicitly by the first assignment to a variable inside a scope. In the implicit case, the variable is lexical and local to the scope.
It is an error to have two variables with the same name in the same scope---thus
x: Integer := 3; x: String := "hello";
will give a compile-time error ``Variables cannot be overloaded''. In addition, it is an error to define a constant with the same name as a variable within a scope. An assignment will create a new variable hiding any names not explicitly imported. For example:
import from Integer; +: Integer := 3;
defines a new variable of type Integer. The ``+'' function from the domain Integer is hidden by this statement --- it can still be accessed by using the qualified name: +$Integer.
Aldor will generate a warning if an implicitly local variable in
a new scope shadows a similarly named variable in an outer scope.
8.7 : Functions
A function introduces a new scope level which includes the parameters to the function: As you might expect, parameters to the function are visible inside the function. A function expression has the following form:
(s1: S1 == v1, ..., sn: Sn == vn) : (t1: T1, ..., tm: Tm) +-> E
where the expression E is treated as being in a new scope, with s1,..., sn being introduced into that scope. The value of such an expression is a function which, when called with arguments ai, of appropriate types, will return the result of evaluating E with the the actual argument values ai substituted for the formal parameters si. See the rules described in section 6.5 for how this expression is evaluated.
The resulting function is sometimes known as a closure, as it closes over (i.e. gathers up, and places somewhere safe) the lexical variables (not the values of the variables) that it references.
For example,
... import from List Integer; n := 2; m := 3; if cond() then f := (a: Integer): Integer +-> n+a; else f := (a: Integer): Integer +-> m*a + n; m := 22; return map(f, lst);
When the function-valued variable f is passed into the function map, the value of m used is 22 --- not the value 3 which was in effect when f was defined.
Parameters
A function parameter may be assigned to, in the right hand side of a ``+->'' expression, where it is an implicit local; the right hand side of the ``+->'' expression is a fresh scope.
Parameters may be updated as variables. However, if they are not
modified within the scope of the function, then they may be used in
type-forming expressions (e.g., expressions used in import statements).
8.8 : Where
A ``where'' expression is of the form:
Expr where Defns
in which Defns is a sequence of declarations and definitions, used in the evaluation of Expr. For example
x+y where { import from Integer; x := 2; y := 3}
evaluates to 5. This can be useful when an expression has many repeated parts which can be factored out as a sequence of definitions. The names introduced in the declarations are visible in the expression part and also in the declarations (note that this expression does not import bindings from Integer into the outer scope). However, names introduced in the Expr are treated as if they are declared at the outer scope level, so
x: Integer == y where { import from Integer; y := 2}
adds a variable ``x'' to the outer scope, the definition of which
references ``y'' which will not be visible in the outer scope.
8.9 : For iterators
A ``for'' iterator introduces a new local name, unless that name
is declared free (see section 5.13).
The name is local to the ``repeat'' loop or collect form,
and is treated as a constant. That is, it may not be updated within the
body of the loop or collect expression.
8.10 : Add
The ``add'' operator has the following syntax:
Add-domain add declarations
It combines a group of declarations (see section 7.8). Declarations on the right hand side of the add are marked as being exports of the new type, provided that they are not explicitly defined as local.
An add expression also introduces a binding for the constant
%, which is a reference to the domain formed by the add
expression.
8.11 : With
A ``with'' expression forms a new category, and has the following syntax:
L with R
This is equivalent to
with { L ; R }
where L evaluates to a category and R is a sequence of either declarations or other category expressions. These form a new scope, which also contains a binding for %, which refers to the domain over which the category is built (that is, the domain under consideration in the category) and whose type is the value of the with expression. (For more details on categories, see section 7.9.)
For example,
#include "aldor" define BitAggregate: Category == Join(LinearAggregate(Boolean), Logic) with { default { ~(barr: %): % == [not bit for bit in barr]; (a: %) /\ (b: %): % == [b1 and b2 for b1 in a for b2 in b]; } } -- No additional exports are necessary, but we redefine -- the output operations. DumbBitArray: BitAggregate == Array Boolean add { (out: TextWriter) << (a: %): TextWriter == { out << "Boolean["; for bit in a repeat print << bit << " "; print << "]"; } }
Defines a new category (the define keyword is explained in section 8.13) called BitAggregate. The type DumbBitArray is a simple domain satisfying BitAggregate.
The % in the body of the with statement refers to a domain of type BitAggregate.
A with expression also defines a constant named ``%%'' for
each category from which the with expression inherits. The
type of %% is the inherited category, and the value is the
domain viewed as a member of that category. In the example, %%
bindings are in scope for
BitAggregate,
Logic,
LinearAggregate Boolean,
Aggregate Boolean and
BasicType. The %% bindings are generally most useful for
checking conditions.
8.12 : Application
Applications allow arguments which are declarations or definitions. The identifiers which are declared or defined in arguments are then local to the application form. For example, in
F(3.2, 5.8, tolerance == 0.02) G(T: Type, List T),
the identifier tolerance is local to the application of F, and the identifier T is local to the application of G.
A comma expression may declare identifiers to be used in later elements. In the expression (e1,..., en), if any ei is a declaration or definition, then the name is visible in the expressions ej, i < j <= n. The declarations place names in the current scope --- the comma expression of itself does not create a new scope. So, in the case where a comma expression provides arguments to a function, declared identifiers are local to the application.
This allows dependent function types to be created: the -> operator is simply a function which is applied to two tuples of types. For the expression (S1,...,Sn) -> T1,...,Tm) any identifiers declared in Si are visible in any of the Tj. In addition, both the left and right hand sides are comma expressions, so the above rules apply.
The following is a possible example of the use of dependent types to form a new function type:
f: (T: Type, t: T) -> (LT: Aggregate T, lt: LT)
Note that ``T'' is used on both sides of the arrow.
It is similarly possible to create dependent product types: the Cross operator is a function which accepts some number of types as arguments and produces the product type. The argument types may have declarations which induce dependency. For instance:
Cross(T: Type, List T)
While the built-in functions ``->'' and ``Cross'' allow dependency inducing declarations as their arguments, the language does not currently provide a mechanism for creating new functions that support this. Thus, although
HashTable(n: Integer, IntegerMod n)
is a legal call, HashTable cannot support this form of dependency, and
an error may be signalled by the compiler when this type is used.
8.13 : Declarations
Declarations associate a type with a name. A declaration is of the form:
modifier idlist: Type.
The modifier is one of:
Either (but not both) of the modifier and the ``:Type'' may be omitted. A declaration may appear in any context not requiring a value, and remains in force throughout the enclosing scope. If the modifier is omitted, the compiler assumes that local is meant and issues a warning that default may be intended.
Some modifiers allow definitions or assignments in a declaration. In this case, the type part is optional and the declaration has one of the forms
Modifier id [: Type ] := E
Modifier id [: Type ] == E
If the type information is omitted, then the type is inferred or taken from default declarations. When the type information is present, say declaring id: T, the declaration also imports type T into the current scope. If this is not desired, then the declaration may use ``:*'' in place of ``:'' to avoid the import.
Finally, it should be noted that it is also possible to give a sequence of assignments or definitions in these statements. For example,
local { a: Integer := 1; b: Integer := 2; c: Integer := 3 }
Default
The ``default'' modifier declares that any instances of the names specified will have the type indicated. This does not create any new bindings.
default n: Integer; n := 23; f(n): Integer == n + 1; print << n^10 << newline where { local n := 2 }; print << n << newline;
This example creates 3 integer variables named ``n''. The type of these does not need to be specified as it is given in the default statement. This example prints 1024, and then 23. The ``local'' declaration in the ``where'' statement is included to avoid a warning about the n in the outer scope. A warning is given if a binding has a default type and there is a declaration in scope with a second type. A default statement around a definition or definition sequence inside a ``with'' or (not in the current release of Aldor) ``add'' scope modifies the way that exports from the current domain are interpreted. This allows generic methods to be defined which employ definitions found in inheriting types. This is further explained in section 7.9.
Define
A ``define'' modifier allows the definition of a value to be visible as well as its type. This is especially useful in category-forming definitions, because without the define it is impossible to decide what signatures are exported by the category.
define Monoid: Category == BasicType with { 1: %; ++ Identity for multiplication. *: (%, %) -> %; ++ Multiplication. }
The above example defines the category Monoid . Without the ``define'' keyword, uses of this definition would only have the type of the category (Category in this case) available.
Local
The ``local'' modifier declares that the given identifiers are local to the current lexical scope. For example local x declares that the ``x'' will be a local, and does not specify a type, so this will be deduced at the first assignment to the variable. A local declaration may also include an initial assignment or definition of the names it introduces.
Names which are assigned using ``:='' and not otherwise declared are treated as local.
Fluid
The ``fluid'' declaration declares that the given identifiers should be treated as having dynamic, as opposed to lexical scope. The declaration is enforced within the lexical scope containing the declaration. Refer to section section 8.14 for more details.
Free
A ``free'' declaration indicates to the compiler that the given name references a variable in an outer scope, and that the initial assignment to the variable should be interpreted as an assignment to the outer variable, rather than an initialization of a new variable.
callCount := 0; f(n: Integer): () == { free callCount; callCount := callCount + 1; n + 1; ...
The code above counts the number of times the function ``f'' is called. Without the free declaration for callCount, callCount inside the function would refer to a new local variable shadowing the outer variable. The free declaration may refer to either a parameter or a (possibly fluid) variable.
Export
An ``export'' modifier may be used to declare that certain names are to be made visible outside the scope in which they are defined. This is the effect when export is used at the top level of an ``add'', ``with'' or file scope. In other contexts, export has the same meaning as local.
An export declaration may be followed by an optional ``to'' part. This is used to make Aldor values visible to programs written in other programming languages. See section 13.15 for details.
An export declaration occurring in a with-expression may be followed by an optional ``from'' part. This indicates the source of the items to be exported, in the same way the ``from'' part of an import or inline statement indicates the source of the items to be imported or inlined. This is described in section 7.9.
Names which are defined using ``=='' and
not otherwise declared are treated as exports.
8.14 : Fluid varaibles
Fluid variables are not often needed but can be useful when a large amount of dynamic state is needed, or a routine is parameterized by a very large number of variables.
A fluid variable exists throughout the lifetime of a program, and its value is always the most recent extant binding of the variable. The extant bindings are the bindings of the variable inside fluid declarations from scopes which have not yet been exited.
When a variable is declared fluid, all references to that name inside the declaration's scope are assumed to be fluid.
An example might help:
#include "aldor" fluid n: Integer := 2; f(): () == print << "The value of n is " << n << newline; g(): () == { fluid n := 3; f() } f(); g();
The fluid variable ``n'' is bound at the top-level and given the value 2. This is the value printed by the top-level call to f. In the next call, the function g re-binds n giving it the value 3. Then when f is called, it is the new value, 3, that is printed. On exit from g, this binding of n is removed and n assumes the value it had in the outer scope.
As usual, an inner declaration (e.g. local, export) may locally over-ride an outer declaration (e.g. fluid). Note that without the ``fluid'' declaration in g, the inner n would be treated as having an implicit ``local'' declaration. This would then behave as in the example below. The inner occurrence of n is a new local variable, unrelated to the outer n, and the call to g results in 2 being printed.
#include "aldor" fluid n: Integer := 2; f(): () == print << "The value of n is " << n << newline; g(): () == { local n := 3; f() } f(); g();
If no initialization is given in the fluid declaration, then the variable is taken to exist in an outer dynamic scope. In the example below, the function ``g'' provides a binding for the variable ``n''. Then, when the function f is called from g, the uses of ``n'' in f refer to the binding in g.
#include "aldor" f(): () == { fluid n: Integer; print << "The value of n is " << n << newline; } g(): () == { fluid n: Integer := 3; f(); } g()
If an assignment to an existing fluid variable occurs in a context other than a fluid declaration, it will modify the current value of the variable, rather than creating a new one:
#include "aldor" fluid n: Integer := 2; f(): () == print << "The value of n is " << n << newline; g(): () == { fluid n: Integer; n := n + 1; f() } g(); g();
A file may use fluid variables which have been bound in other files, but no type information regarding these variable is known, so it is the programmer's responsibility to ensure that the types match.
In the current implementation of Aldor, a fluid variable must have a binding point at the top level of some file. It is an error to have two fluid variables of the same name and different types in a program.
The current syntax for rebinding fluids --- using assignment inside a fluid declaration --- may be changed in future.