Subsections

Procedures

The second way of using routines is to declare them as procedures. We have seen that an operator can be declared and used, have a mode and a value (its routine denotation), but apart from having an operator symbol, it cannot be identified with an identifier in the way that a name or a denotation of a CHAR value can. Procedures are quite different.

Firstly, here are some general remarks on the way procedures differ from operators. The mode of a procedure always starts with the mode constructor PROC. A procedure can have any number of parameters, including none. Two procedures having the same identifier cannot be declared in the same range (so “overloading” is not allowed). When a procedure is used, its parameters, if any, are in a strong context. This means that rowing and widening are available.

Procedures are declared using the mode constructor PROC. Here is a procedure which creates a range of characters:

   PROC(CHAR,CHAR)[]CHAR range =
      (CHAR a,b)[]CHAR:
      BEGIN
         CHAR aa,bb;

         (a<=b|aa:=a; bb:=b|aa:=b; bb:=a);

         [ABS aa:ABS bb]CHAR r;

         FOR i
         FROM LWB r TO UPB r
         DO
            r[i]:=REPR i
         OD;
         r
      END

This procedure identity declaration resembles the declaration for a multiple: much of the mode is repeated on the right-hand side and the formal-declarer on the left-hand side has no identifiers for the modes of the parameters. Notice that the modes of the parameters must be repeated in the formal-declarer, but that the mode of the procedure on the right-hand side can contain the usual abbreviation. Here is the abbreviated header:

   PROC range = (CHAR a,b)[]CHAR:

The formal-declarer is important for creating synonyms:

  PROC(REAL)REAL sine = sin

Two or more procedure declarations can be separated by commas, even if the procedures have different modes. Consider, for example:

   PROC pa = (INT i)INT:  i*i,
        pb = (INT i)CHAR: REPR(i*i),
        pc = (INT i)REAL: (i=0|0|1/i)

Parameterless procedures

Procedures can have no parameters. Suppose the following names have been declared:

   INT i,j

Here is a procedure with mode PROC INT which yields an INT:

   PROC INT p1 = INT: i:=3+j

A procedure can be invoked or called by writing its identifier. For example, the procedure p1 would be called by

   p1

or

   INT a = p1

The right-hand side of this identity declaration requires a value of mode INT, but it has been given a unit of mode PROC INT. This is converted into a value of mode INT by the coercion known as deproceduring. This coercion is available in every context (even soft).

Have you realised that print must be the identifier of a procedure? Well done! However, we cannot talk about its parameters yet because we don't know enough about the language.

Here is another procedure which yields a name of mode REF INT. The mode of the procedure is PROC REF INT:

   PROC p2 = REF INT: IF i < 0 THEN i ELSE j FI

and assumes that the names identified by i and j had already been declared. Here is an identity declaration which uses p2:

   REF INT i or j = p2

Because p2 yields a name, it can be used on the left-hand side of an assignment:

   p2:=4

Here, 4 will be assigned to i or j depending on the value i refers to. The left-hand side of an assignment has a soft context in which only the deproceduring coercion is allowed.

In procedures p1 and p2, the identifier i had been declared globally to the procedures. Assignment to such an identifier is, as already stated, a side-effect. Here is another procedure of mode PROC INT which uses a global identifier, but does not assign to it:

   PROC p3 = REAL:
   (
      [i]REAL a;  read((a,newline));
      REAL sum:=0.0;

      FOR i FROM LWB a TO UPB a
      DO
         sum+:=a[i]
      OD;

      sum
   )

and here is a call of p3:

   print(p3)

In the identity declaration

   REAL r = p2

p2 is deprocedured to yield a name of mode REF INT, dereferenced to yield an INT, and then widened to yield a REAL. All these coercions are available in a strong context (the right-hand side of an identity declaration).

The call of a procedure can appear in a formula without parentheses. Here is an example:

   p2:=p1 * ROUND p3

If we call the procedure p1, declared above, its value does not have to be used. For example, in

   p1;

the value yielded by p1 has been voided by the following semicolon after the procedure had been called.

In the section on routines, we introduced the mode VOID. Here is a procedure yielding VOID:

   PROC p4 = VOID:  print(p3)

and a possible use:

   ; p4;

where the semicolons show that the call stands on its own.

When a parameterless procedure yields a multiple, the call of that procedure can be sliced to get an individual element. For example, suppose we declare

   PROC p5 = [,]REAL:
   (
      [i,j]REAL a;
      read((a,newline));
      a
   )

where i and j were declared above, and then call p5 in the formula

   REAL x = p5[i-3,j] * 2

When p5 is called, it yields a two-dimensional multiple of mode [,]REAL which is then sliced using the two subscripts (assuming that i-3 is within the bounds of the first dimension) to yield a value of mode REAL, which is then used in the formula.

Procedure p2, declared above, yielded a name declared globally to the procedure. As explained in the sections on routines, a procedure cannot yield a locally-generated name. However, if the name is generated using HEAP, then the name can be yielded as in p6:

   PROC p6 = REF INT: (HEAP INT i:=3;  i)

Here is a call of p6 where the yielded name is captured with an identity declaration:

   REF INT global int = p6

Then print(global int) will display 3.

The yield of a procedure can be another procedure. Consider this program fragment:

   PROC q2 = INT: max int % 2,
        q3 = INT: max int % 3,
        q4 = INT: max int % 4,
        q5 = INT: max int % 5;

   INT i;  read((i,newline));

   PROC q = PROC INT:
      CASE i+1 IN q2,q3,q4 OUT q5 ESAC

Procedure q will yield one of the predeclared procedures depending on the value of i. Here, the yielded procedure will not be deprocedured because the mode required is a procedure.

One parameterless procedure is provided in the standard prelude. Its identifier is random, and when called returns the next pseudo-random real number of a series. If called a large number of times, the numbers yielded are uniformly distributed in the range [0,1].


Exercises

6.19
Write a procedure which assigns a value to a name declared globally to the procedure. Ans[*]
6.20
Write a procedure which reads an integer from the keyboard, then declares a dynamic name of a multiple of one dimension, and reads that number of integers from the keyboard. Now compute the sum of all the integers, and yield its value as the yield of the procedure. Ans[*]
6.21
Write a procedure which yields the name of a two dimensional multiple containing characters read from the keyboard. The mode of the multiple should be REF[,]CHAR. Ans[*]


Procedures with parameters

Parameters of procedures can have any mode (including procedures). Unlike operators, procedures can have any number of parameters. The parameters are written as a parameter list which consists of one parameter, or two or more separated by commas.

Here is a procedure with a single parameter:

   PROC(INT)CHAR p7 = (INT i)CHAR: REPR(i>0|i|0)

This is a full identity declaration for p7. It can be abbreviated to

   PROC p7 = (INT i)CHAR: REPR(i>0|i|0)

The mode of p7 is PROC(INT)CHAR. That is, p7 is a procedure with a single integer parameter and yielding a character. Here is a call of p7:

   CHAR c = p7(-3)

Note that the single parameter is written between parentheses. Since the context of an actual parameter of a procedure is strong, a name of mode REF INT could be used:

   CHAR c = p7(i)

or

   CHAR c = p7(ai[j])

where ai has mode REF[]INT and j has mode INT or REF INT or PROC INT (or even PROC REF INT).

Here is a procedure which takes three parameters:

   PROC char in string =
      (CHAR c,REF INT p,STRING s)BOOL:
   (
      BOOL found:= FALSE;
      FOR k FROM LWB s TO UPB s
      WHILE NOT found
      DO
         (c = s[k] | i:=k; found:= TRUE)
      OD;
      found
   )

The procedure (which is in the standard prelude) tests whether a character is in a string, and if it is, returns its position in the parameter p. The procedure yields TRUE if the character is in the string, and FALSE if not. Here is a possible call of the procedure:

   IF INT p;  char in string(char,p,"abcde")
   THEN ...

where char was declared in an outer range. Notice that the REF INT parameter of char in string is not assigned a new value if the character is not found in the string.

When calling a procedure, the call must supply the same number of actual parameters, and in the same order, as there are formal parameters in the procedure declaration.

If a multiple is one of the formal parameters, a row-display can be supplied as an actual parameter (remember that a row-display can only occur in a strong context). In this case, the row-display counts as a single parameter, but the number of elements in the row-display can differ in successive calls since the bounds of the multiple can be determined by the procedure using the bounds interrogation operators. Here is an example:

   PROC pb = ([]INT m)INT:
   (INT sum:=0;
    FOR i FROM LWB m TO UPB m DO sum+:= m[i] OD;
    sum)

and here are some calls of pb:

   pb((1,2,3))   pb((2,3,5,7,11,13))

Again, procedures with parameters can assign to, or use, globally declared names and other values, but it is better to include the name in the header of the procedure. Here is a procedure which reads data into a globally declared multiple using that multiple as a parameter:

   PROC rm = (REF[]REAL a)VOID:
      read((a,newline))

It could now be called by

   rm(multiple)

where multiple had been previously declared as having mode REF[]REAL.

As described in section 6.1.3, a flexible name can be used as an actual parameter provided that the formal parameter has also been declared as being flexible. For example, here is a procedure which takes a single parameter of mode REF STRING and which yields an INT:

   PROC read line = (REF STRING s)INT:
   (
      read((s,newline));
      UPB s #LWB is 1#
   )

read line reads the next line of characters from the keyboard, assigns it to its parameter, which is a flexible name, and yields the length of the line.


Exercises

6.22
Write a procedure which takes a REF REAL parameter, divides the value it refers to by $ \pi$, multiplies it by 180, assigns the final value to its parameter, and yields the parameter (that is, its name). Ans[*]
6.23
Write a procedure which takes two parameters: the first should have mode STRING and the second mode INT. Display the string on the screen the number of times given by the integer. If the integer is negative, display a newline first and then use the absolute value (use the operator ABS) of the integer. Yield the mode VOID. Ans[*]
6.24
Write a procedure, identified as num in multiple, which does for an integer what char in string does for a character. Ans[*]


Procedures as parameters

Here is a procedure which takes a procedure as a parameter:

   PROC sum = (INT n,PROC(INT)REAL p)REAL:
   (
      REAL s:=0;
      FOR i TO n DO s+:=p(i) OD;
      s
   )

Notice that the mode of the procedure parameter is a formal mode so no identifier is required for its INT parameter in the header of the procedure sum. In the loop clause, the procedure is called with an actual parameter.

When a parameter must be a procedure, there are two ways in which it can be supplied. Firstly, a predeclared procedure identifier can be supplied, as in

   PROC pa = (INT a)REAL: 1/a;
   sum(34,pa)

Secondly, a routine denotation can be supplied:

   sum(34,(INT a)REAL: 1/a)

A routine denotation is a unit. In this case, the routine denotation has the mode PROC(INT)REAL, so it can be used in the call of sum. Notice also that, because the routine denotation is an actual parameter, its header includes the identifier a. In fact, routine denotations can be used wherever a procedure is required, so long as the denotation has the required mode. The routine denotation given in the call is on the right-hand side of the implied identity declaration of the elaboration of the parameter. It is an example of an anonymous routine denotation.


Exercises

6.25
Given the declaration of sum in the text, what is the value of: Ans[*]
(a)
sum(4,(INT a)REAL: a)

(b)
sum(2,(INT b)REAL: 1/(5*b))

(c)
sum(0,pa) (pa is declared in the text)


Recursion

One of the fun aspects of using procedures is that a procedure can call itself. This is known as recursion. For example, here is a simplistic way of calculating a factorial:

   PROC factorial = (INT n)INT:
      (n=1|1|n*factorial(n-1))

Try it with the call

   factorial(7)

Here is another recursively defined procedure which displays an integer on the screen in minimum space:

   PROC ai = (INT i)VOID:
   IF   i < 0  THEN print("-");  ai(ABS i)
   ELIF i < 10 THEN print(REPR(i+ABS"0"))
   ELSE ai(i%10);  ai(i MOD 10)
   FI

In each of these two cases, the procedure includes a test which chooses between a recursive call and phrases which do not result in a recursive call. This is necessary because, otherwise, the procedure would never complete. Neither of these procedures uses a locally declared value. Here is one which does:

   PROC new fact = (INT i)INT:
   IF INT n:=i-1;   n = 1
   THEN 2
   ELSE i*new fact(n)
   FI

The example is somewhat artificial, but illustrates the point. If new fact is called by, for example, new fact(3), then in the first call, n will have the value 2, and new fact will be called again with the parameter equal to 2. In the second call, n will be 1, but this n this time round will be a new n, with the first n inaccessible (it being declared in an enclosing range). new fact will yield 2, and this value will be used in the formula on line 4 of the procedure. The first call to new fact will then exit with the value 6.

Apart from being fun, recursive procedures can be an efficient way of programming a particular problem. Chapter 11 deals with, amongst other topics, recursive modes, and there, recursive programming comes into its own.

A different form of recursion, known as mutual recursion, is exemplified by two procedures which call each other. You have to ensure there is no circularity. The principal difficulty of how to use a procedure before it has been declared is overcome by first declaring a procedure name and then assigning a routine denotation to the procedure name after the other procedure has been declared. Here is a simple example:7.3

   PROC(INT)INT pb;
   PROC pa = (INT i)INT: (i>0|pb(i-1)|i);
   pb:=(INT i)INT: (i<0|pa(i+1)|i);

Then pa(4) would yield 3 and pa(-4) would yield -4. Similarly, pb(4) would yield 4 and pb(-4) would yield -3. Notice that the right-hand side of the assignment is an anonymous routine denotation.


Exercises

6.26
Write a recursive procedure to reverse the order of letters in a value of mode []CHAR. It should yield a value also of mode []CHAR. Ans[*]
6.27
Write two mutually recursive procedures which take an integer parameter and which yield an INT. The first should call the second if the parameter is odd, and the second should call the first if the parameter is even. The alternative option should yield the square of the parameter for the first procedure and the cube of the parameter for the second procedure. Use square and cube as the procedure identifiers. Ans[*]


Standard procedures

The standard prelude contains the declarations of more than 60 procedures, most of them concerned with transput (see chapter 9). A number of procedures, all having the mode

   PROC(REAL)REAL

are declared in the standard prelude and yield the values of common mathematical functions. These are sqrt, exp, ln, cos, sin, tan, arctan, arcsin and arccos. Naturally, you must be careful to ensure that the actual parameter for sqrt is non-negative, and that the actual parameter for ln is greater than zero. The procedures cos, sin and tan expect their REAL parameter to be in radians.

New procedures using these predeclared procedures can be declared:

   PROC sinh =
      (REAL x)REAL: (exp(x) + exp(-x))/2

A variety of pseudo-random numbers can be produced using random int. The mode of the procedure random int is

   PROC(INT)INT

and yields a pseudo-random integer greater than or equal to one, and less than or equal to its integer parameter. For example, here is a procedure which will compute the percentage of each possible die throw in 10 000 such throws:

   PROC percentage = []REAL:
   (
      PROC throw = INT: random int(6);

      [6]REAL result:=(0,0,0,0,0,0);

      TO 10 000 DO result[throw]+:=1 OD;

      FOR i FROM LWB result TO UPB result
      DO result[i] /:= 10 000 OD;

      result
   )

Notice that percentage has another procedure (throw) declared within it. There is no limit to such nesting.

Other features of procedures

Since a procedure is a value, it is possible to declare values whose modes include a procedure mode. For example, here is a multiple of procedures:

   []PROC(REAL)REAL pr = (sin,cos,tan)

and here is a possible call:

   pr[2](2)

which yields -0⋅416 146 836 5. We could also declare a procedure which could be called with the expression

   pr(2)[2]

but this is left as an exercise.

Similarly, names of procedures can be declared and can be quite useful. Instead of declaring

   PROC pc = (INT i)PROC(REAL)REAL: pr[i]

using pr declared above, with a possible call of pc(2) we could write

   PROC(REAL)REAL pn:=pr[i]

and then use pn instead of pc. The advantage of this would be that pr would be subscripted only once instead of whenever pc is elaborated. Furthermore, another procedure could be assigned to pn and the procedure it refers to again called. Using pn would usually involve dereferencing.

There are scoping problems involved with procedure names. Although the scope of a denotation is global, procedure denotations may include an identifier whose range is not global. For this reason, the scope of a procedure denotation is limited to the smallest enclosing clause containing a declaration of an identifier or mode or operator indicant which is used in the procedure denotation.

For example, in this program fragment

   PROC REAL pp;  REAL y;
   BEGIN
      REAL x:=3.0;
      PROC p = REAL:  x:=4.0;
      print(p);
      pp:=p; CO wrong CO
      print(x)
   END;
   print(("pp=",pp)) #wrong#

the assignment in line 6 is wrong because the scope of the right-hand side is less than the scope of the left-hand side. Unfortunately, the a68toc compiler does not perform scope checking and so will not flag the incorrect assignment.

There are times when SKIP is useful in a procedure declaration:

   PROC p = REAL:
   IF x<0
   THEN print("Negative parameter"); stop; SKIP
   ELSE sqrt(x)
   FI

The yield of the procedure is REAL, so each part of the conditional clause must yield a value of mode REAL. The construct stop yields VOID, and even in a strong context, VOID cannot be coerced to REAL. However, SKIP will yield an undefined value of any required mode. In this case, SKIP yields a value of mode REAL, but the value is never used, because the program is terminated just before.

Grouping your program into procedures helps to keep the logic simple at each level. Nesting procedures makes sense when the nested procedures are used only within the outer procedures. This topic is covered in greater depth in chapter 12.

Sian Mountbatten 2012-01-19