Defining New Operators

CS2130 Programming Language Concepts
Unit 11 More on Function and Procedure Abstractions
Defining New Operators
Some languages, for example Ada and C++, allow new overloadings of existing operator
symbols to be defined. For example in Ada:
TYPE Vector is ARRAY(1 .. 3) OF Float;
FUNCTION "+"(Left, Right : Vector) RETurn Vector IS
-- vector addition
Result : Vector;
BEGIN
FOR I IN 1 .. 3 LOOP
Result(I) := Left(I) + Right(I);
END LOOP;
RETURN Result;
END "+";
FUNCTION "*"(Left : Float; Right : Vector) RETURN Vector IS
-- scalar multiplication
Result : Vector;
BEGIN
FOR I IN 1 .. 3 LOOP
Result(I) := Left * Right(I);
END LOOP;
RETURN Result;
END "*";
Then given suitable declarations
A, B, C, D, E : Vector;
we can now write
C := A + B;
D := 2.0*C;
E := A*2.0;
Float*Vector
-- !? illegal as "*" above only permits
Exercise Define a second new overloading of "*" to allow expressions such as A*2.0 where
A is of type Vector, to be written legally.
In Ada and C++ only existing operators can be given new overloadings; the number of
operands and the precedence level of the operator cannot be changed. In some languages
such as PL1 (and some languages for algebraic computing such as Reduce) it is possible to
define new operators and to set their precedence level. However, as this feature considerably
complicates the parsing of the language by the compiler or interpreter, such extensions are
not normally available in main stream compiled imperative languages.
Aliasing
The advantage of reference parameters is efficiency as the copying of values is avoided; this
is particularly important for values of large composite types. The main disadvantage of
reference parameters is the danger of aliasing.
© A Barnes, 2006
1
CS2130/Unit 11
It is possible to have two or more names bound to the same entity. In this case we say that the
entity is aliased. We have seen an example of this with arrays in Java in Unit 8 where,
because of the reference semantics, after the declarations:
int[] a = new int[100];
int[] b = a;
Now a and b refer to the same underlying array asand so for example an assignment to a[1]
also alters b[1].
In a language using value semantics, if a variable is aliased, the situation can be visualised as
Name2
Value Cell
Reference
Value
Name1
Reference
Note that this exactly the situation that occurs during a subprogram call when parameter
passing by reference is used; the formal and actual parameters are aliases for the same storage
location for the duration of the subprogram call. This aliasing itself does not normally cause
confusion as we would normally only use the formal parameter name to refer to the storage
location in the subprogram body.
However consider the following situation (known as parameter-induced aliasing) when the
same actual parameter is passed by reference1 twice as in the C++ procedure swap:
void swap(int& x, y)
{
x = x + y;
y = x - y;
// y now holds the original value of x
x = x - y;
// x now holds the original value of y
}
.......
swap(a, b);
// works OK
swap(a, a);
// sets a to zero
The problem in the second call is that the formal parameters x and y are both aliases for the
actual parameter a and the 'clever' trick of swapping without the need for a temporary
variable fails in this case.
Note that the analogue of the above code works correctly in Ada for values of non-composite
types as IN OUT parameters are guaranteed to use value-result (copy-in/copy-out) semantics:
PROCEDURE Swap(X, Y : IN OUT Integer) IS
BEGIN
X := X + Y;
Y := X - Y;
-- Y now holds the original value of X
X := X - Y;
-- X now holds the original value of Y
END Swap;
.......
Swap(A, B);
-- works OK
1
Similar problems arise in Java for parameters of composite type because of the reference
semantics.
© A Barnes, 2006
2
CS2130/Unit 11
Swap(A, A);
-- works OK
However analogous code for composite types (such as the array type Vector above) is not
guaranteed to work in the presence of aliasing. It would work correctly if value-result
parameter mechanism is used but not if the reference parameter mechanism is used.
PROCEDURE Swap(X, Y : IN OUT Vector) IS
BEGIN
X := X + Y;
Y := X - Y;
-- Y now holds the original value of X
X := X - Y;
-- X now holds the original value of Y
END Swap;
.......
Swap(A, B);
-- works OK
Swap(A, A);
-- fails if arrays are passed by reference
The above Swap procedure is somewhat artificial, but parameter-induced aliasing can occur
in more practical situations. Consider the following array-based implementation of a set data
structure in Java:
public class ArraySet {
private static int MAXSIZE = 100;
private SomeType[] elems;
private int size;
//say
// construct a new empty set
public ArraySet() {
elems = new SomeType[MAXSIZE];
size = 0;
}
public boolean isIn(Sometype e) {
for (int i = 0; i < size; i++) {
if (e.equals(elems[i])
return true;
}
return false;
}
// add an element to a set
public void addElem(SomeType e) {
if (!isIn(e)) {
elems[size] = e;
size++;
}
}
// other accessor and update methods
.......
public static void intersection(ArraySet s1, s2, result) {
result.size = 0;
for (int i = 0; i< s1.size; i++) {
for (int j = 0; j < s2.size; j++) {
if (s1.elems[i].equals(s2.elems[j])) {
// if element is in both S1 and S2,
//add it to result set
result.size++;
result.elems[result.size] = S1.elems[i];
break;
// exit for j loop
© A Barnes, 2006
3
CS2130/Unit 11
}
}
}
}
// end of for j loop
// end of for i loop
// end of intersection method;
// other set operations: union, difference etc..
}
As all Java implementations uses reference semantics for the reference type ArraySet,
intersection does not behave correctly due to parameter-induced aliasing in calls such as
ArraySet A, B;
.............
ArraySet.intersection(A, B, B);
The first step in the procedure sets result.size to zero and because of the aliasing of
result and S2 (both of which refer to the actual parameter B) this sets S2.size to zero and
so the body of the for(int j...) loop is never executed and the method always terminates
with an empty result set.
The correct way to code intersection is to code it as a function returning the result. We
use a local variable result (say) to build up the result:
public static ArraySet intersection(ArraySet S1, S2) {
ArraySet result = new ArraySet();
// new empty set
for (int i = 0; i< s1.size; i++) {
for (int j = 0; j < s2.size; j++) {
if (s1.elems[i].equals(s2.elems[j]))
// if element is in both S1 a
//add it to result set
result.addElem(S1.elems[i]);
break;
// exit for j loop
}
}
// end of for j loop
}
// end of for i loop
return result;
}
// end of intersection method;
This avoids the problems with aliasing as changes to result do not, of course, affect S2
(nor the actual parameter B).
To summarise: Parameter induced aliasing is a potential problem in languages where
parameters are passed by reference or in languages (like Java) which use reference
semantics. It does NOT occur if parameters are passed by copying (value parameters,
result parameters or value-result parameters).
In languages such as Ada which support OUT mode parameters, Intersection could be
coded as a procedure returning the result via an OUT parameter
MaxSize : CONSTANT Integer := 100;
-- say
TYPE ElemArray IS ARRAY(1 .. MaxSize) OF SomeType;
TYPE ArraySet IS RECORD
Size : Integer RANGE 0 .. MaxSize;
Elems : ElemArray;
END RECORD;
PROCEDURE Intersection(S1, S2 : IN ArraySet;
Result : OUT ArraySet) IS
TmpRes : ArraySet; -- local variable
© A Barnes, 2006
4
CS2130/Unit 11
BEGIN
TmpRes.Size := 0;
FOR I IN 1 .. S1.Size LOOP
FOR J IN 1 .. S2.Size LOOP
IF S1.Elems(I) = S2.Elems(J) THEN
-- if element is in both S1 & S2,
-- add it to TmpRes set
TmpRes.Size := TmpRes.Size + 1;
TmpRes.Set(TmpRes.Size) := S1.Elems(I);
EXIT;
-- exit FOR J loop
END IF;
END LOOP;
END LOOP;
Result := TmpRes;
-- copy result to OUT parameter
END Intersection;
However to avoid potential problems with aliasing in calls such as
A, B : Set;
.............
Intersection(A, B, B);
the result set needs to be built up in a local variable TmpRes and then assigned to the OUT
parameter just before the procedure returns rather than being built up directly in the Result
parameter. This avoids the problems with aliasing as assignments to TmpRes do not, of
course, affect S2 (nor B) and the final assignment to Result can do no harm as it is the last
command in the procedure. However, this technique of using a local variable does not work
in Java (see the end of the Unit 10).
In the design of the language Ada, the danger of parameter-induced aliasing was recognized
and the language designers sought to alleviate its effects as follows. The constant semantics
restriction on IN mode parameters was introduced so that if IN mode parameters behaved in
exactly the same way whether they were passed by reference or by value. The original
version of Ada also sought to make OUT parameter behave in the same way whether they
were passed by result or by reference; in Ada83 the value of an OUT parameter could not be
used in a procedure body; OUT parameters could only be appear on the left-hand side of
assignments (or be passed as OUT mode parameters to other procedures). Thus an Ada83
programmer would be forced by the language to introduce a temporary local variable TmpRes
when coding the Intersection procedure and so would be protected by the language
syntax from making parameter-induced aliasing errors.
However this restriction on OUT parameters was removed in Ada95 as it meant that an Ada83
programmer often needed to introduce an extra local variable (even when there was no
possibility of aliasing) and then assign the value of the local variable to the OUT parameter
immediately before the procedure returned. For example:
Ada95
PROCEDURE AddUp(V : IN Vector; T : OUT Float) IS
BEGIN
T := 0.0;
FOR I IN V'Range LOOP
T := T + V(I);
END LOOP;
END AddUp;
© A Barnes, 2006
5
CS2130/Unit 11
Ada83
PROCEDURE AddUp(V : IN Vector; T : OUT Float) IS
Temp : Float := 0;
BEGIN
FOR I IN V'Range LOOP
Temp := Temp + V(I);
END LOOP;
T := Temp;
-- set OUT parameter
END AddUp;
There is no practical way of making IN OUT mode parameters behave the same way whether
they are passed by reference or by value-result (copy-in copy out). Thus a programmer needs
to be aware of the potential pitfalls of parameter-induced aliasing and avoid writing
procedures where the parameter-passing method can affect the values returned by the
procedure.
Functions with Side Effects
Functions can have local variables and employ commands, but provided the latter have no
effect on program state outside the function, the correspondence between expressions and
functions is maintained and we say the function is free of side-effects. However if a function,
as well as returning a value, modifies program state (outside the function body) either by
altering a (non-local) program variable or by performing I/O operations the function is said to
have side-effects. Side-effect functions thus confuse two notions: namely expression
evaluation to produce a value and commands to modify program state.
In Pascal it is possible to define functions with VAR parameters and in C++ functions may
have reference parameters. Similarly because Java uses reference semantics for objects, a
function can alter the state of an object passed to it as a parameter. All such functions can
modify program state by altering variables passed to them as actual parameters.
Functions with side effect can lead to code which is hard to understand or even to undefined
program behaviour. For example in C++
int funny(int& x) {
x = x+1;
return 2*x;
}
int i = 1;
a[i]= funny(i);
// does this set a[2] or a[1] to 4 ?
If i on the left hand side of the assignment is evaluated first then the target of the assignment
is a[1]. Then when the right hand side of the assignment is evaluated, i is incremented to 2
by the function funny which then returns the value 4 which then gets assigned to a[1].
However, if the right hand side of the assignment is evaluated first, then i is incremented to 2
by the funny which again returns the value 4 which then gets assigned to a[2].
In C and C++ the order of evaluation is not defined by the language standard but is up to the
particular compiler. Such undefined program behaviour is clearly undesirable. Even if the
language specifies the order of evaluation of the sides of an assignment the program code is
still obscure and so functions such as funny with side-effects are best avoided.
Note a similar effect can be achieved in Java (and most other languages) by allowing a
function to modify a non-local variable:
© A Barnes, 2006
6
CS2130/Unit 11
int i = 1;;
int[] a = new int[10];
static int RumDo() {
i = i+1;
return 2*i;
}
......
a[i] = RumDo();
// does this set a[2] or a[1] to 4 ?
Ada prevents some of the problems associated with functions such as funny above since Ada
functions are not allowed to have OUT or IN OUT mode parameters and this prevents one
important mechanism for producing side effects. However in Ada it is also possible define
functions like RumDo which modify non-local variables. The language occam2 is more strict
then Ada: a function cannot have OUT or IN OUT mode parameters and moreover a function
body is not allowed to change non-local variables and thus side-effects are avoided
completely.
Functions with I/O side-effects can similarly give rise to obscure code and possibly undefined
program behaviour. Consider the following Ada function which inputs a value typed at the
keyboard and returns it
FUNCTION GetInt RETURN Integer IS
I : Integer;
BEGIN
Ada.Integer_Text_IO.Get(I);
RETURN I;
END GetInt;
J : Integer := GetInt/(1+GetInt);
Suppose that the user types the integers 10 and 4. If the numerator is evaluated first, then J is
set to 2 = 10/(1+4) . However if the denominator is evaluated first then J is set to zero =
4/(1+10).
Thus it is best (if possible) to avoid writing functions with I/O side effects even if the
language permits it. In Ada GetInt should be coded as a procedure which returns value
input via an OUT mode parameter2. In languages such as C and Java without OUT parameters,
it is sometimes necessary to define functions with I/O side-effects; how else could the input
value be returned to the caller? To avoid undefined or obscure program behaviour, never call
a function with I/O side-effects twice in the same expression.
Commands and Expressions Revisited
Recall that an expression is anything that can be evaluated to produce a value whereas a
command modifies program state (the internal state which includes the current values of all
program variables and the external state which includes the state of all its input and output
streams). In a programming language which maintains a clear distinction between
expressions and commands
evaluating an expression does NOT modify program state,
2
Actually the library procedure Get from Ada.Integer_Text_IO does just thiat!
© A Barnes, 2006
7
CS2130/Unit 11
and executing a command does NOT produce a value.
In some languages for example Ada, Modula-2 and occam2, there is a clear distinction
between commands (which change program state but do not have a value) and expressions
(which are evaluated to produce a value and do not change program state). In such languages:
commands cannot be used as expressions
and
expressions cannot be used as commands.
In many languages this clear distinction is blurred; for example in C, C++ and Java all
assignment commands have a value namely the value of the right-hand side of the command.
This allows an assignment to be used as part of an expression; for example in multiple
assignments of the form
i = j = k = 0;
which is equivalent to
i = 0; j = 0; k = 0;
Note here that = is right associative, that is i=j=k=0 is interpreted as i=(j=(k=0)). The
value of the assignment k=0 has the value zero which is then assigned to j and the value of
this assignment is then assigned to i.
Similarly in the following C code character value are repeatedly input and processed until end
of file is reached. The loop control condition using and assigns the result of to a variable
getchar to a variable ch and the value of this assignment is then tested against the special
end-of-file value EOF:
int ch;
......
while((ch = getchar()) != EOF) {
process this ch
}
Without this 'trick' the loop would need to be coded with an ugly repeated call to getchar:
ch = getchar();
while(ch != EOF) {
process this ch
ch = getchar();
}
The latter style though clumsier is however arguably clearer than the first version. Similarly
the auto-increment operations can be used as stand-alone commands or as part of an
expression. For example
a[i++] = 0;
is equivalent in effect to
a[i] = 0; i++;
In the latter case the value returned by the operator call i++ is ignored; the operator call is
being performed solely for its side-effect of incrementing i. In the latter case we could
equally well have used the pre-increment form ++i, but of course we could not write the first
assignment as
a[++i] = 0;
which is equivalent in effect to
i++; a[i] = 0;
© A Barnes, 2006
8
CS2130/Unit 11
As a general rule such 'tricks' are acceptable if used sparingly, if they are overused code
becomes difficult to understand and therefore error-prone. In particular one should avoid
using the operators ++ and –– more than once on the same variable in a single expression, for
example
a = 1;
res = f(++a, ++a);
The results in C and C++ are implementation dependent as the order in which the arguments
in a function call are evaluated is not defined by the language. If the first argument is
evaluated first the function f is called as f(2,3) whereas if the second argument is evaluated
first the call f(3, 2) is performed. In Java left to right evaluation is guaranteed so the
function call f(2, 3) is always performed, but for the sake of clarity such obscurities in
coding should be avoided. As a general rule in any language one should avoid defining new
functions (or operators) with side-effects or if one does define such functions one should
never call them twice in the same expression.
In C, C++ and Java one can also ignore the value returned by a function. Thus, in effect, we
are using a function call as a command. For example in C many I/O functions such as scanf
and printf return a value (the number of data values successfully processed); however in
normal usage the return values is ignored:
printf("%d %d\n", i, j);
/* output 2 integers in base 10 */
The function here is being called for its I/O side-effects rather than its value.
In Ada the return value of a function cannot simply be ignored; a function call step is never a
complete command.
In Lisp (and some scripting languages) all commands have a value (although in some cases
such as PROG the value of a command is the trivial value NIL) and thus there is no distinction
between commands and expressions. For example in RLisp one can write
RLisp
Java
Max := IF A > B THEN
A;
ELSE
B;
if (A > B)
Max = A;
else
Max = B;
the value of the IF being either the value of the THEN or ELSE part. Thus the above code has
the same effect as the Java fragment shown above.
Note the RLisp fragment is actually equivalent to the Lisp
(SETQ Max (COND
((GT A
B) A) (T, B)))
and could also be written in this way. In general RLisp is simply Lisp with some ‘syntactic
sugar’ which enables Lisp to be written in a very similar way to procedural languages such as
Ada or C. The RLisp interpreter converts a command to ‘pure’ Lisp before evaluating it.
© A Barnes, 2006
9
CS2130/Unit 11
Normal-Order and Eager Evaluation
In most imperative languages, actual parameters are all evaluated at the start of a subprogram
call, before execution of the subprogram body begins. This is known as eager evaluation.
Some functional languages (Miranda and Haskell, but NOT Lisp) use normal order
evaluation. In this the actual parameter is not immediately evaluated at the time of a
function call, but in effect the actual parameter is substituted for each occurrence of the
formal parameter in the function body. The actual parameter is not actually evaluated until
control reaches a step that references the corresponding formal parameter. If control never
reaches such a step, the actual parameter is never evaluated. Parameters which use a normalorder evaluation mechanism are often referred to as name parameters and are said to be
passed by name.
A function is said to be strict, if it can only be evaluated if all its arguments (actual
parameters) can be evaluated whereas it is said to be non-strict if it can sometimes be
evaluated even when some of its arguments cannot be successfully evaluated. With eager
evaluation all functions are strict whereas with normal-order evaluation it is possible to define
non-strict functions.
Eager evaluation has the advantage of efficiency whereas normal-order evaluation (in its
basic form) is less efficient since if a formal parameter is referenced several times during
function execution the actual parameter is evaluated several times.
In Pascal, C, C++ and Java (and most other imperative languages) the logical operators AND
(or &&) and OR (or ||) use normal-order (or short-circuit) evaluation and are non-strict in
their second argument. For example
float x, y;
.....
if (x != 0.0 && y/x > 0.0)
// the condition can be evaluated when x == 0.0 even though
// the second argument can't be evaluated (division by zero)
Ada is unusual in that AND and OR are strict operators; however the operators AND THEN and
OR ELSE are non-strict in their second argument as they use short-circuit evaluation.
X, Y : Float;
.....
IF X /= 0.0 AND Y/X > 0.0 THEN ....
-- error if X = 0.0
IF X /= 0.0 AND THEN Y/X > 0.0 THEN .... -- OK if X = 0.0
Referential Transparency
In pure functional languages there are no commands and thus no explicit means of modifying
program state. Such languages have the property of referential transparency. This means
that if an expression is evaluated several times (in the context of the same definitions), the
result will always be the same. Thus if two expressions E1 and E2 have the same value now,
they will always have the same value in the future and so can be freely interchanged. Thus, if
a function f has been defined and evaluation of f(1) yields the value 8, say, then f(1) and 8
can be freely interchanged without affecting the meaning of the program. Referential
transparency thus makes it easier to reason formally about the correctness of a program.
Imperative languages are referentially opaque, that is they lack referential transparency and
therefore formal reasoning about such languages is more difficult. The use of (for example)
the assignment command to a variable X, say which modifies the internal program state
© A Barnes, 2006
10
CS2130/Unit 11
makes the evaluation of an expression (involving X) time-dependent, that is the value
obtained depends on the previous execution history of the program. Functions that refer to
non-local updateable values are referentially opaque3. For example:
boolean b;
.......
static int refOpaque(int n) {
if (b)
return n/2;
else
return 3*n + 1;
}
Functions with side effects represent an extreme form of referential opacity. For example the
function funny defined above breaks even the basic rule that addition is commutative:
funny(i) + i
does not produce the same result as
i + funny(i)
Pure functional languages possess the Church-Rosser property, namely:
given an expression, if any evaluation order terminates, then so does normal-order
evaluation.
if different orders of evaluation of an expression terminate, then they all produce
the same result (note that in general some orders of evaluation may not terminate
even if other orders do).
Consequently programmers in a purely functional languages do not need to be concerned with
exactly HOW their programs are obeyed, only on WHAT computation their programs
specify. Unlike programmers in an imperative language they can work at a higher level of
abstraction further removed from the underlying machine-level considerations.
Lazy Evaluation
If the Church-Rosser property holds then the efficiency problems of normal-order evaluation
can be overcome by using a lazy evaluation technique. This means that when a formal
parameter is referenced for the first time in a function body the corresponding actual
parameter is evaluated and the value is stored. The stored value is used subsequently when
the formal parameter is referenced (rather than re-evaluating the actual parameter). However,
if the formal parameter is never referenced, the actual parameter is not evaluated.
Historical Note: Algol-60 allowed programmers to choose between eager evaluation (value
parameters) and normal-order evaluation (so called name parameters). The combination of
name parameters and side-effects gave rise to a number of bizarre effects as different
evaluations of an actual parameter could produce different values.
3
This is the main reason why it is considered poor style for subprograms to refer to non-local
variables, but not for them to refer to non-local constants.
© A Barnes, 2006
11
CS2130/Unit 11