In this chapter we will describe statements and special expressions.
Statements are used to express specific actions. They are never returning values. Statements are recognized as such because of their syntax, such as assignment statements, or they are expressions which are returning nothing.
Special expressions are irregular expressions with distinguishing formats.
In the following we will use the word clause if we do not make a distinction between a statement or an expression. A clause is either a statement or an expression.
Assignment statements are used to assign data items to targets. An assignment statement has the following form:
target := expression
We will start with a very simple example to clarify the differences between a simple assignment statement and a name-definition:
a = 2 * 3;
b:= 3 * 4;
The first line in this example is a name-definition. It says that from now on a is the name of the integer value 6. The meaning of a cannot be changed within its scope. So, where-ever you see a in its scope, it always represents 6.
The second line in the example is an assignment-statement. It states that b is the name of a variable which value may be changed; the initial value of b is 12. The value of b may be changed by subsequent assignment-statements where needed. The only limitation is that the types of the new values assigned to b are the same as the initial type of b. This means in our example that some following assignment-statements are allowed and others not, as will be clear from the following lines:
b:=0; << same type: is allowed >>
b:=a + b; << same type: is allowed >>
b:=3.0; << type-conflict: is not allowed >>
b:='b'; << type-conflict: is not allowed >>
The language makes a clear distinction between an initial assignment, which determines the name, type and initial value of a variable and subsequent assignments which can only change the value of an existing variable.
Let us give an example how assignment-statements may be used. Suppose we want a definition which computes the sum of all numbers from 1 to N. We may write:
Sigma (integer) -> integer; Sigma (N) = [ sum:=0; sum:=sum + 1 .. N; sum ];
Let us discuss this example in more detail. The first statement sum:=0; of the definition rule is an initial assignment-statement, because sum has not been defined before in the definition rule. It introduces the variable sum and it assigns the integer value zero to sum.
To analyze the effects of the second statement sum:=sum + 1..N;, we will rewrite the definition into an equivalent version:
Sigma (integer) -> integer; Sigma (N) = [ sum:=0; [ i = 1 .. N; sum:=sum + i ]; sum ];
In this version there are two blocks. The sum's used in the inner block are referring to the initial sum of the outer block. The inner block is a statement-block because the last element of the block is a statement, in contrast to the outer block which is an expression-block. In the inner block all values of i are assigned to sum until N will be exceeded.
After the second statement of the outer block is finished the final sum value will be returned.
Suppose we want a definition which returns also the intermediate sums. Based on the preceding definition we derive:
Sigmas (integer) -> multi( integer); Sigmas (N) = [ sum:=0; i = 1 .. N; sum:=sum + i; sum ];
If we call this definition with Sigmas(7) we get the stream of integer values (1, 3, 6, 10, 15, 21, 28).
In the previous chapters we used an example of how to compute factorials. Here we give an imperative version:
Factorial (integer) -> integer; Factorial (N) = [ Fact:=1; Fact:=Fact * 1 .. N; Fact ];
Another example we used in previous chapters was the computation of Fibonnaci numbers. Here is an imperative version:
fib (integer) -> integer; fib (n) = [ Fib1:=0; Fib2:=1; [ i = 2 .. n; Fib3:=Fib2 + Fib1; Fib1:=Fib2; Fib2:=Fib3 ]; Fib2 ];
In this version each value in the Fibonnaci series is only computed once.
There are three kinds of variables: local variables, global variables, and parameter variables. Local variables are defined in definition rules as illustrated by the previous examples. Global variables are defined outside definitions.
Global variables are sometimes useful. For example, if we want to know how many times a definition has been called, we can use a global variable as a counter:
Counter:=0; << new global variable >> . . F (real) -> real; F (R) = [Counter:=Counter + 1; ....];
Parameter variables are parameters representing variables. A parameter variable is marked by the var indication. For example, suppose we want to define a procedure to increment the value of a variable with one. We may write this as:
Increment (var( integer)) -> nothing; Increment (x) = [ x:= x + 1];
The var indication specifies that the parameter may also be used as the target of an assignment statement.
With this definition the value of any variable may be incremented by calling the definition. For example, in the following piece of program
i:=0; . . Increment (var( i));
is the value of i increased by one. For parameter variables the corresponding arguments must also contain the var indication, as shown in the example. A variable argument may be a local variable, a global variable, or a parameter variable.
The expression in an assignment statement may represent zero, one or more values. It is clear what happens if the expression represents one value. In that case the value will be assigned to the target. But what will happen in the other cases?
If the expression represents an empty stream, no assignment will be performed. As a consequence, the target will not be changed. If the expression represents a non-empty stream, all the values of the stream are successively assigned to the target.
Let us discuss some examples:
P:= 0; << P:= 0 >>
P:= 1 .. 15; << P:= 1, 2, 3,.... 15 >>
The end value of P is 15. But what in the next example?
P:= 0; << P:= 0 >>
P:= P + 1 .. 15; << P:= 120 >>
The end value of P is 120, being the sum of 1 + 2 + 3 .... + 15.
The last line is equivalent to:
[ i = 1 .. 15; P:= P + i]; << P:= 120 >>
It is often necessary to select a value or an action based on a condition. For example, let us see how the maximum value of two integers is defined:
max (integer, integer) -> integer; max (x, y) = if x > y then x else y;
We have used in this example the if clause to select the x value if x is greater than y, otherwise the y value will be taken.
There are two versions of an if clause: a complete version and an abbreviated version. The syntax of a complete if clause is as follows:
if condition then clause1 else clause2
The condition must be a boolean expression. If the value of the condition is true the clause following the then will be executed, otherwise the clause following the else will be executed.
Both clauses are either statements or are expressions of the same type.
Examples of some complete if clauses are:
if X > 5 then X - 1 else 2 * X << expressions >>
if p == q then s:=p + q * r else s:= p * r << statements >>
if a < b then if b < c then d:=c - a << nested if - >> else d:=a + b << statements >> else d:=b - a
The syntax of an abbreviated if clause is as follows:
if condition then clause
An abbreviated if clause has no else clause. If the value of the boolean condition is true the clause following the then will be executed which may be a statement or an expression.
If the clause is an statement and the condition is false nothing will be done. If the clause is an expression the if clause represents an optional value. This means that if the condition is false an empty stream is the result.
Consider the following example:
Even (integer) -> optional (integer); Even (I) = if mod (I, 2) == 0 then I;
The Even definition tests if an input value is even or odd. If it is even it returns the input value, otherwise it backtracks without returning any value.
In addition to the two versions of the if clause there is the when clause, which has the following form:
clause when condition
The condition must be a boolean expression. If the value of the condition is true the clause preceding the when will be executed. A when clause is equivalent to the following abbreviated if clause:
if condition then clause
Examples of when clauses are:
x when x > 0
x:=x+1 when x < n
print (customer) when city (customer) == "Amsterdam" and sales (customer) > 10000.00
The condition in an if or when clause may represent zero, one or more boolean values. In general, the clause is executed as many times as there are boolean values in the condition. As one of the consequences, if the condition represents an empty stream, the conditional clause will not be executed.
A clause in a conditional form may not be a name-definition nor an initial assignment. For example:
if a > b then x = 5 else y = 7
is not allowed, because name-definitions may not be conditionally executed. The same applies to initial assignment statements as will be clear in:
if a > b then x:=5 else x:=7.0;
In this example, a unique type for x can not be decided.
It is often necessary when writing a definition to specify the repeated execution of an expression or a statement. In previous chapters we discussed already two ways for repetitions: recursive functions and multi-value definitions. In this section we will discuss an explicit form of repetition by means of the repeat clause.
A repeat clause specifies the repeated execution of a clause. Its syntactic form is:
The clause may be a statement or an expression.
If the clause is a simple statement then the repetition continues endlessly. For example, "repeat print(x)" will continue forever.
If the clause is a simple expression then the repeat clause represents a multi-value expression with an infinite number of values. For example, "repeat 1" represents a stream of an endless number of ones.
However, in many practical situations the clause is a block which contains a conditional ready statement to terminate the repetition. For example, to terminate our print loop after 10 repetitions, we may write:
i:=0; repeat [ i:=i+1; if i > 10 then ready; print(x) ];
The example starts with a counting variable i outside the loop. In the repeat block the variable i is incremented and tested for its end value. If the end value is exceeded then the ready statement is executed to indicate that the repeat loop is finished.
In general, the elements of a block will be repeatedly executed until a termination condition occurs. Because the termination condition may be tested anywhere in a loop, the test may be at the entrance of a loop, or at the end of a loop, or somewhere in the middle of a loop. We will show some examples:
repeat [ ready when i > j; i:=i+1; print (i) ] << early ready >>
repeat [ i:=i+1; ready when i > j; print (i) ] << middle ready >>
repeat [ i:=i+1; print (i); ready when i > j ] << late ready >>
If no ready statement will be executed an endless loop occurs.
The last element of the block determines the kind of the repeat clause. If the last element is a statement then the repeat clause is a statement, otherwise it is an expression. If the repeat clause is an expression then it represents a multi-value expression which may generate a stream of values. The number of values is determined by the termination condition. For example, to generate a stream of N successive integers, we may write:
i:=0; j = repeat [ i:=i+1; if i > N then ready; i];
In this example, j is the name of a stream of N successive integers which is generated by the repeat expression.
A ready statement may also be used to terminate the loop caused by a name definition. For example, to print the values of a function F until a value greater than W is encountered, the following block can be used:
[ i = 1..N; V = F (i); ready when V > W; print (V) ]
If loops are nested, the ready statement terminates only the innermost loop in which it occurs.
As we have seen, expressions may represent a stream of data items. But in some cases not all elements of a stream are accepted for further processing. For example, if we are only interested in customers spending above a certain limit, or in integers which are divisible by 7, one or more filters are required to separate the acceptable entities from the unacceptables.
There are two filter statements in the language: an accept statement and a reject statement.
An accept-statement will accept given entities for further processing if specified conditions are satisfied. The syntactic form of an accept-statement is:
accept (condition )
The condition must be a boolean expression. If the value of the condition is true then the clause following the accept-statement will be executed. If the condition is false, backtracking will occur.
A reject-statement will reject given entities for further processing if specified conditions have been satisfied. The syntactic form of a reject-statement is:
reject (condition )
The condition must also be a boolean expression. If the value of the condition is false then the clause following the reject-statement will be executed. If the condition is true, backtracking will occur.
For example, if we are selecting only those integers which are divisible by 7 we may write:
[ i = 1 .. N; accept (mod( i, 7) == 0); i ]
If the remainder of i divided by 7 is unequal to zero, then i will not be accepted and backtracking will occur to obtain a new value for i, otherwise i is accepted.
The same solution can also be written with a reject statement, as in:
[ i = 1 .. N; reject (mod( i, 7) <> 0); i ]
Generally, the condition in a reject statement is the inverse of the condition in a corresponding accept statement.
As we have seen, an accept-statement or reject-statement works as a filter. It selects from a stream those elements which have some given properties. Let us take another example and let us assume that you want to buy a new car. You made already a list of all candidate cars and now you want to select those cars with values below 20000:
[Car = items (AllCars); accept (Value(Car) < 20000); print (Car) ]
In this example the items function produces a stream of cars which are filtered by the accept statement. Only those cars whose values are below 20000 are printed.
We may also write the filter in terms of rejected cars, as in:
[Car = items (AllCars); reject (Value(Car) >=20000); print (Car) ]
This example has the same effect as the preceding one: it prints only those cars whose values are below 20000.
Execution of a program may lead to exceptional situations in which normal program execution cannot continue. For example, an attempt is made in an arithmetic computation, to divide by zero, or to compute the square root of a negative number. In these situations the system will report the exceptional condition and will stop the execution. In some situations, however, there is also a need to program explicitly the effects of exceptional conditions. This can be done by means of an exception statement.
The syntactic form of an exception-statement is:
exception (condition, text)
The first argument of the exception statement represents the exceptional condition. The condition must be a boolean expression. If the value of the condition is true then the execution of the program will be terminated and the system will report the message as stated by the text, which is the second argument of the exception statement.
If the condition is false, the statement following the exception statement will be executed.
exception ( X == 0, "Divide by Zero")
exception (Full (Stack), "Stack Overflow")
The condition in an exception-statement may represent zero, one or more boolean values. In general, the exception-statement is executed as many times as there are boolean values in the condition. As one of the consequences, if the condition represents an empty stream, the exception-statement will not be executed.
Returning results is in many definition rules implicit by simply writing an expression as the last element of a rule. However, in some situations there is a need for explicit returning a result. There are two statements in the language to achieve this: the result statement and the return statement.
The syntactic form of a result statement is:
result ( expression )
The result statement may only be used in definitions with the multi quantifier. It issues the result of the expression to the calling program and continues afterwards with the clause following the result statement.
The syntactic form of a return statement is:
return ( expression )
The return statement may be used in all kinds of definitions. In definitions with the nothing quantifier the parentheses and the expression are missing. In definitions with the single quantifier the parentheses and the expression are required. In definitions with the multi or optional quantifier the parentheses and the expression are specified if a result should be returned, otherwise, the parentheses and the expression are missing.
The return statement terminates the execution of the current definition; it returns to the calling program without coming back. The expression in a return-statement must be a single value expression.
Note that while a ready statement terminates only one loop, a return statement terminates all its embracing loops.
The elements of a block are statements, name-definitions and expressions. An element of a block may also be another block which means that blocks may be nested. The elements of a block are evaluated from left to right.
Block elements are separated by semicolons. The semicolon (;) is used as an element separator and not as an element terminator. This means, however, that the last element of a block cannot be terminated by a semicolon. To relax this punctuation rule we will introduce an empty element which has no effect on the program but which allows the following constructs:
because it is equivalent to:
An example is:
[ a:=x + 1; b:=a + 7; print(b); ]
which is equivalent to:
[ a:=x + 1; b:=a + 7; print(b) ]
This page was last modified on 27-09-2012 13:44:13