Previous Home Contents Next

Chapter 5: Expressions

5.1 Names

5.2 Literals

5.3 Definitions

5.4 Assignments

5.5 Functions

5.6 Function Calls

5.7 Imperatives

5.8 Multiple Values

5.9 Sequences

5.10 Exits

5.11 If

5.12 Logical Expressions

5.13 Loops

5.14 Generators

5.15 Collections

5.16 General Branching

5.17 Never

Aldor is an expression-based language: every construct in the language produces zero, one or more values. This chapter describes the structure of expressions in Aldor and the rules for expression evaluation.



5.1 : Names

The evaluation of a name in Aldor causes the value of a variable or constant to be retrieved.

If a name refers to a variable or constant defined in the same scope or in an enclosing scope then retrieving its value is a very inexpensive operation. If a name refers to an imported constant, then there may be a cost associated with the look up, depending on the degree of optimization used to compile the program.

Consider the following example:

In the last line, the name "n" is defined in the current scope and the name "f" is defined in an enclosing scope, so their use is very inexpensive. The names "*", "-" and "1" are imported from SingleInteger. If this program is compiled with optimization, then there is no cost in using the values from SingleInteger. Without optimization, these values would have to be dynamically retrieved from SingleInteger at a modest cost.

A name may represent a variable, which may take on different values at different points in a computation, or a constant, which always refers to the same value every time it is used.

Names for constants may be overloaded in Aldor. That is, it is possible to have more than one constant with a given name, visible at the same point in a program. Names for variables cannot be overloaded. A name cannot represent both a variable and a constant in the same scope.



5.2 : Literals

Literal constants are expressions which represent the explicit data values which appear in a program. There are three styles of literal constants in Aldor: quoted strings, integers and floating-point numbers:

The meaning of a constant in Aldor is determined by the environment in which it is used. For example, the constant 1.234e56 might be a value of type SingleFloat, DoubleFloat or Float, depending on the context.

When a literal expression is encountered in a program, it is treated as an application of a corresponding ``literal accepting'' operation:

(where T represents the type of the value being formed).

Each of these operations takes a single argument of type Literal and constructs a value of the appropriate type. When programs are compiled against the base Aldor library, the constant-folding optimization will immediately convert constants of the following types to their machine representations: String, SingleInteger, Integer, SingleFloat, DoubleFloat.

New types may provide their own interpretation of literal constants by exporting a literal forming operation with the corresponding signature. As a consequence, if operations for creating string literals, for example, are available from several types, it may be necessary to provide a declaration to indicate which kind of literal is intended. When implementing literal-forming operations for new types, it is often useful to use the string literal-forming operation from String.

Some types may accept some literal values and not others. For example, a fixed-precision integer type might reject values which lie outside the range of values representable by the type.

String literals

String-style literals are enclosed in quotation marks. Inside a string-style literal, an underscore character "_" is used as an escape character to modify the meaning of the characters which follow:

Examples:

When using #include "aldor" or #include "axiom", the type String provides string-style literals.

Integer literals

Integer-style literals provide a syntax for whole numbers. These are in base ten unless otherwise indicated.

An integer-style literal is either

In Aldor the numerals "0" and "1" are not literal constants - they are treated as names so that various mathematical structures which export 0 or 1 can do so, without being required to support general integer constants.

An underscore appearing in the middle of an integer-style literal is ignored together with any following white space. An underscore which appears at the very beginning of a word causes the whole word to be treated as an identifier, rather than as a literal constant.

Examples:

When using #include "aldor", the types SingleInteger and Integer provide integer-style literals. When using #include "axiom", the types SingleInteger, Integer, NonNegativeInteger and PositiveIntegerprovide integer-style literals.

None of the above types support the baservalue radix notation described above.

Floating-point literals

Float-style literals are numbers with a decimal point, an exponent, or both.

Examples:

An underscore appearing in the middle of a float-style literal is ignored together with any following white space.

When using #include "aldor", the types SingleFloat, DoubleFloat and Float provide float-style literals. When using #include "axiom", the types DoubleFloat and Float provide float-style literals.

None of the above types support the baservaluer radix notation.



5.3 : Definitions

A constant in Aldor denotes a particular value which cannot be changed. The general syntax for a constant definition is:

x is an identifier giving the name of the constant.

T is an expression giving the type of the constant. The type declaration is optional. If the type is declared, then the type of E must satisfy it. Otherwise the type is inferred from the type of the expression E.

E is an expression which computes the value of the constant.

Examples:

A function definition is a special case of a constant definition. Function definitions are described in more detail in section 6.1.

Once a value has been given to a named constant, it cannot be changed to refer to another value.




5.4 : Assignments

A variable in Aldor denotes a value which may change during the evaluation of a program. A variable is given a value by an assignment of the form:

x is an identifier giving the name of the variable.

T is an expression giving the type of the variable. The type declaration is optional. If the type is declared, then the type of E must satisfy it. Otherwise the type is inferred from the type of the expression E.

E is an expression which computes the value of the variable.

Several variables may be assigned a value at the same time:

Any or all of the type declarations may be omitted, in which case the ith variable would read "xi", rather than "xi: Ti", and the type of xi is inferred from the type of the expression E.

Examples:

The value of an assignment expression is the same as the value of E.

A special form of assignment expression is used to provide a general kind of updating operation:

Typically, x is an expression which evaluates to a structured data value, such as an array or a list, and the expressions ai taken together specify some component of x. An assignment expression of this form is treated as an application of the operation "set!" of the form:

For example, for lists, the "set!" function (see section 25.19) takes as its second (component specifying) parameter either "first" or "rest", so we could have:

which would result in L having the value list(4, 2, 3).

The value of this form is the return value of the function "set!".



5.5 : Functions

Function expressions are the primitive form for building functions in Aldor. An example of a function expression is:

See section 6.5 for a complete description.



5.6 : Functions calls

Typical expressions consist mostly of function calls. For example, consider the expression

This has six explicit function calls (i.e. to the arithmetic functions ``+'', ``-'', ``*'', ``^'', to the n-ary function ``list'', and to the type constructor function ``List''. This expression also has eight implicit function calls to the literal forming operation ``integer''.

See
section 4.6 for a complete description of the syntax of function calls.



5.7 : Imperatives

The do expression evaluates E and discards the computed value, so that the do expression returns no value.



5.8 : Multiple values

A series of comma-separated expressions is used in Aldor to produce multiple values. The expressions are evaluated one by one, and the results are taken together as the (multiple) value of the whole expression:

This expression produces three values, all of type Integer. In general, an expression in Aldor produces zero, one or more values, each having its own type. For convenience, nevertheless, we often speak of the value and the type of an expression, even if it produces multiple values, when the intended meaning is clear from the context.

See
section 4.7 for a discussion of the use of parentheses in comma expressions.

Functions may be declared to accept or return multiple values. The example below shows how to declare, define and use a function which involves multiple values.

The call to f returns (193, 7), which are passed as arguments to the function "divide" from Integer. This returns

which are assigned to q and r respectively.

Comma-separated expressions are not necessarily evaluated in any particular order; furthermore, the evaluation of their subexpression may be interleaved. Thus, the program:

could print any of "2 3", "3 2", "2 2" or "3 3". Programs which depend on the order of evaluation of expressions to be used as arguments to a function should use a sequence to make the order explicit. (See section 5.9.)



5.9 : Sequences

A series of expressions to be evaluated one after another is called a sequence. A sequence is written as a semicolon-separated series of subexpressions:

The expressions (that is, the subexpressions of the sequence) are evaluated one by one, in the order of their occurrence, and the value of the last expression evaluated is used as the value of the sequence. A sequence may also contain one or more exit expressions, as described in section5.10, which prevent the evaluation of any expressions later in the sequence and so provide a way to return a value other than that of the last expression in the sequence.

Because semicolon has a relatively low precedence, it is usually necessary to enclose a sequence in braces ("{ }") or parentheses ("( )") to get the desired result. (See section 4.7 for details.)

Examples:

The meaning of a sequence is the same whether braces or parentheses are used. Braces are normally used, especially to enclose a longer expression split over several lines. Parentheses are occasionally used to enclose shorter sequences as part of other expressions. An implicit semicolon is assumed after a closing brace but not after a closing parenthesis.

It is also possible to use indentation to construct sequences by enclosing lines between the directives "#pile" and "#endpile". In this context, a group of consecutive lines indented by the same amount is called a pile and is treated as a sequence. The precise rules for forming piles are described in section 24.3.



5.10 : Exits

An exit expression has the form:

When an exit expression appears as one of the elements of a sequence, the condition is evaluated. If the condition evaluates to true then the value of the sequence is the value of the expression E, and no further components of the sequences are evaluated. If the condition evaluates to false then evaluation continues with the next expression in the sequence. So the expression:

is equivalent to

In a sequence which contains an exit expression, the type of the expression E must be compatible with the type of the sequence, that is, with the type of the final element of the sequence. If a sequence contains several exit expressions, the types of the possible exit values must all be compatible.

If the condition is not of type Boolean, then an implicit application of the function test is performed to convert the condition to the type Boolean. (See
section 11.2.)

An exit expression transfers control; it does not, itself, produce a value. As a result, the type of the exit expression is (), and so an exit expression can only be used in a context which does not require a value. Examples:

Note that all of the exit values (i.e. 0 and 100) have type Integer, and the final element of the sequence also has type Integer.

A series of exit expressions is often a compact way to enumerate a list of alternative cases:

Since any expression can appear after the =>, another sequence, which may contain other exit expressions, can appear there:

If an exit expression appears as a strict subexpression of an expression other than a sequence, the exit expression is treated as a sequence of length one:

This example is treated as equivalent to:



5.11 : If

Conditional branching in Aldor is provided by the if expression.

When an if expression is evaluated, the condition is evaluated. If the condition evaluates to true then the value of the if expression is the value of the expression T. If the condition evaluates to false and the else clause is present, then the value of the if expression is the value of the expression E. If the else clause is not present, then the if expression returns no value.

An if expression used in a context which requires a value must have both a then and an else branch. The types of the two branches must be compatible, and the type of the expression is the type which is satisfied by both branches.

In contexts which do not require a value, the types of the then and else branch are independent. In this case, the type of the if expression is taken to be the type of the empty expression, and it is possible to omit the else branch altogether.

If condition is not of type Boolean, then an implicit application of the function test is performed to convert condition to type Boolean. (See
section 11.2)

Example:

Note that if the else clause is not present, then the if expression cannot be used in a context which requires a value:



5.12 : Logical expressions

Logical expressions in Aldor are provided using the following forms:

An "and" expression is true if both E1 and E2 are true. If the first expression evaluates to false, then the second expression is not evaluated.

An "or" expression is true if E1 or E2 or both are true. If the first expression evaluates to true, then the second expression is not evaluated.

A "not" expression is true if E is false. If any of E1, E2 or E is not of type Boolean, then an implicit application of the function test is performed to convert the value to type Boolean. (See
section 11.2) The type of a logical expression is Boolean.

Examples:

The logical connectives "and", "or" and "not" are syntactic elements of the language that should not be confused with similar functions exported from the type Boolean (/\, \/ and ~). The functional versions from Boolean evaluate all of their arguments before computing their result, and denote function values. The logical connectives cannot be used as functions:

but you cannot say: map(not, [false, false, true]), since "not" is not a function. (The Aldor function "map", defined for all aggregate types (see section 25.3 applies its first argument to each component of its second argument.)



5.13 : Loops

The Aldor language provdes a set of constructs to handle loops in a way which is both elegant and efficient. The concept lying at the base of Aldor loops is that of generator, discussed in depth in chapter9. Generators provide an abstract way to traverse values belonging to certain domains without violating the principle of encapsulation (see section 7.8). Generators can be considered as autonomous entities producing values. This section shows how values they produce can be used to control loops.

The general form of a loop expression is:

Iterators repeat Body

The iterators control how many times the body is evaluated. Any number of iterators may be used on a loop, and each is either a "while" or a "for" iterator.

A loop with no iterator repeats forever, unless terminated by an error or some other event. For example:

Two forms, "break" and "iterate," may be used withing the loop body to control the loop evaluation, as described later on.

While-iterators

A "while" iterator, allows a loop to continue so long as a condition remains true. The syntax of a "while" iterator is

If a loop has a "while" iterator, then at the beginning of each iteration condition is evaluated. The result is then tested:

Just as with "if" and other expressions having tests for control flow, if the condition of a"while" is not a Boolean value, then an appropriate "test" function is applied to determine the sense of the condition.

The following example shows a repeat loop using a while iterator:

This loop counts the number of times the body has to be evaluated in order for n to reach one. When n reaches 1, the evaluation of the loop is terminated, and the count is printed. (It is a well-known conjecture that this process will terminate for all integer n > 0. This is sometimes called the "3n + 1 problem".)

In a case such as this it is important to give k an initial value, since the loop may be iterated zero times! In fact, if the while condition is false the first time that it is evaluated, then the repeat body will not be executed at all:

For-iterators

Very often, loops are used to traverse certain kinds of data structures, such as lists, arrays or tables, or to execute some operations for all the numerical values in a defined range.

"for" iterators make this sort of loop convenient to write. Take, for instance, the following examples:

The "for" iterators in these examples all have the form

The lhs part specifies a varable which will take on the successive values over the course of the iteration. The lhs has the form

The type declaration is optional. If it is missing, the type of the variable is inferred from the source of the values, Expr

The "free" part of the lhs determines the scope of the variable. Without it, a new variable of the given name is made local to the loop. If the word "free" is present, then the loop uses an existing variable from the context. In this case, the value of the variable is available after the loop terminates.

The example above can be rewritten useing a free loop variable:

The previous example uses the "break" construct explained later in this section. When a break is evaluated it causes the termination of the loop. In this case, the break is the only exit point of the loop, because we are iterating over an open segment, producing infinitely many values: 1, 2, 3, ...

Now we turn our attention to the expression traversed by the "for" iterator. In the examples, we have seen

In general, the "for" iterator expression must have type Generator T, where T is the type of the "for" variable. If the expression is not of this type, then an implicit type will be used. See chapter 9 for a description of generators and how to create new ones.

The examples we have seen work because the list, array and segment types provided by "libaldor" export generator functions. Other examples of generators provided by "libaldor" are "step", provided by SingleFloat, DoubleFloat and Float, which creates uniformly spaced points on a floting point interval, and "tails" which provides successive tails of a list.

There is a second form of "for" iterator which filters the values used. This has the form:

This kind of "for" iterator skips those values which do not satisfy the condition. For example, the sum example we saw earlier could have been wrtten as:

Multiple interators

A "repeat" loop may have any number of iterators, with the following syntax:

The loop is repreated until one of the iterators terminates it: the first "while" which has false condition or the first "for" which consumes all its values ends the loop.

This is convenient when the termination condition does not relate directly to a "for" variable, or when structures are to be traversed in parallel.

Continuing the example from earlier, we may use a "while" to decide if the end has been reached, and, at the same time, use a "for" to count the number of times we have evaluated the loop body:

Multiple "for" iterators can allow lists to be combined in an efficient way:

These two "for" iterators are used in parallel, like a zipper combining the two lists. The loop stops at the end of the shortest list, in this case giving a sum of three products.

This is not a double loop - to use all pairs with the number from l1 and the second number from ls, you would use two nested loops, each with its own "repeat":

Using more than one iterator is often the most efficient natural way to write a loop. A loop with two "for" iterators is more efficient than

because it does not need to traverse the lists each time to pick off the desired elements. And the code is more concise than

Evaluating a "break" causes a loop to terminate. For example:

"break" is most useful when the condition which quits the loop falls most naturally in the middle or at the end of the loop body. Sometimes it is possible to change a test which appears at the end to a test at the begining. This helps make programs more readable, since one sees immediately what will cause the loop to end. Also, it allows the exit to be controlled by a "while" iterator, rather than an "if" and a "break".

Iterate

Evaluating an "iterate" abandons the current evaluation of the loop body and starts the next iteration. For example:

This example does the same thing as the previous one, but is organized slightly differently. The "iterate" on the second line of the loop body causes the rest of the body to be skipped.

"iterate" can be used instread of placing the remainder of a loop body inside an "if" expression. This can make programs easier to read, by emphasizing that certain conditions are not expected, and by avoiding extra levels of indentation. It is particularly useful when the decision to go on to the next iteration of a loop is buried deep inside some other logic, rather than appearing at the top level of the loop body.

An "iterate" is equivalent to a "goto" branching to the end of the loop body. Thus, the meaning of "iterate" is independent of whether there are any "while" or "for" iterators controlling the loop.

If an "iterate" occurs inside nested loops, it steps the deepest one.

Definition in low-level terms

It is possible to express the loop behavious in terms of gotos and labels. A loop of the form

is equivalent to

Where, if iti is "while condi", then initi is empty and stepi is

if iti is "for lhsi in expri | condi", then initi is

and stepi is

(a "for" without a condition is treated as if the condition were "true").

In this, TOP, DONE and the gi are generated names, and are not accessible to the other parts of the program.



5.14 : Generate expressions

Generate expressions are used to create generators. For a complete description of how to use generate to create a generator, see chapter 9. The general syntax for a generate expression is:

E is an expression which represents code which will be evaluated each time a value is extracted from the generator. Evaluation begins at the start of the expression E and continues until an expression of the form

is processed, where v is the value to be passed back to the context which is collecting values from the generator. Each time a value is requested after the first value is yielded, control resumes after the yield expression which produced the previous value.



5.15 : Collections

A collect expression provides a convenient shorthand for creating generators and aggregate objects. A collect expression has the form:

E iter1 ... itern

where n >= 1. The iteri are iterators, as described in
section 5.13. Consider the following example:

This program creates a list of the squares of the integers from 1 to 10, and then a list of products of integers. Note that while can form an iterator, and can therefore be used in a collect expression.

Note that the square brackets are not part of the collect expression, but are simply a shorthand for a call to the function bracket, with the value of the collection as an argument. The domain List Integer from the Aldor base library exports a function with the signature bracket: Generator Integer -> %, which is called twice in the above example.

As collect expressions produce generators, one would expect that generate expressions and collect expressions are related. The collect expression:

E iter1 ... itern

is equivalent to the generate expression:

generate iter1 ... itern repeat yield E

Thus, x*y for x in 1..9 for y in 9..1 by -1 is the same as:

generate for x in 1..9 for y in 9..1 by -1 repeat yield x*y

Collect expressions provide a convenient notation for creating new aggregates, and require no additional functionality in the language.



5.16 : General branching

Aldor provides unconditional branching using label expressions and goto expressions. A label expression is of the following form:

where L is an identifier known as the label name, and E is any expression. The type of the label expression is the same as the type of E. Names which are used as labels have no relationship with variables or constants of the same name, and a label name may also be used as a variable or constant. A label name obeys the same scope rules as constants or variables, but may not appear in any expressions other than gotos and other label expressions. Since labels are constants, it is an error to bind the same label twice in the same scope.

A goto expression has the following form:

where L is the name of a label. After the evaluation of a goto expression, execution resumes with the expression associated with the label L. The type of the goto expression itself is the type Exit.

The label must appear in the same function body as the goto. In addition, it is an error for a goto to branch into an inner scope of the scope in which it appears (that is, a local function or any repeat or collection including a for, where, add or with expression). A goto may also not branch out of a function or a with or add expression.

Example:

If the first test is successful, then the return expression is skipped and the execution proceeds on the line following ERROR. Labels can also be defined at the top level of a file, since the top level of a file is treated as a sequence:



5.17 : Never

The expression never is a special value, of type Exit, which acts as a programmer-supplied assertion that execution will never reach that point. An exit expression can be useful as it may allow a program to be translated into more efficient code.

The following Aldor code is a possible use of never

With luck, the never at the end of the sequence will not be reached in any execution of the program. (If it is reached, Aldor will complain "User error: Reached a "never"").


Previous Home Contents Next