prev up next

6) Functions

Aldwych allows a style of programming close to that of functional programming, with functions passed as arguments, and curried functions (functions which take only some of the arguments of a "full" function call and return a function which can take the rest).

A procedure which is intended to be used in a functional style is written as the procedures we have seen previously, but its argument list is enclosed in square brackets and the arguments are separated by white space, not commas. For example, here is the procedure we saw previously to square a number:

#square(n) <==> n*n;
but here is the functional version:
#square[n] <==> n*n;
A function is set up from its name by following it with (). So to get the square of a number stored in variable m, we use square() m. Alternatively, a variable could be set to refer to the function: Square<-square() followed by Square m. Note that Aldwych treats the function as an object.

Using this, the following Aldwych program reads in a sequence of numbers, stores them in a list, maps the square function to the list, and prints out the result:

#main ==
   Screen<-aio\stdio(),
   writeNumsList(Screen,map(square(),list)),
   Screen.fwrite("Enter some numbers terminated by zero: "),
   list<-readNums(Screen);

#readNums(Screen)<
<(n<-Screen.readInt)
{
 n=0 ||>$;
  :  ||>n:readNums(Screen);
}

#writeNumsList(Screen,list)=
{
 list$  ||
     Screen.nl;
 list=head:tail ||
     Screen.writeInt(head).putc(' '),
     writeNumsList(Screen,tail)
}

#map(Func,list)<
{
 list$ ||>$;
 list=head:tail ||>Func head:map(Func,tail)
}

#square[n] <==> n*n;
The procedure map takes a function and a list and returns the list that consists of the result of applying the function to each element of the original list. Of course, map could be written in a functional style as well, making the program (omitting the reading and writing procedures, which are unchanged):
#main ==
   aio\stdio()->Screen,
   writeNumsList(Screen,map() square() list),
   Screen.fwrite("Enter some numbers terminated by zero: "),
   readNums(Screen)->list;

#map[Func list]<
{
 list$ ||>$;
 list=head:tail ||>Func head:map() Func tail
}

#square[n] <==> n*n;
In Aldwych, juxtaposition (with white space separating symbols) means function application. It is left-associative (as in standard functional programming), so A B C is interpreted as (A B) C rather than A (B C), and has higher precedence than the cons operator : or the arithmetic operators, but not higher than the message send operators.

Precedence of message send against function application is demonstrated in the example below:

#main ==
   Screen<-aio\stdio(),
   writeNumsList(Screen,map() square() List),
   Screen.fwrite("Enter some numbers terminated by zero: "),
   List<-listobj(readNums(Screen));

#map[Func ListObj] <==
  ?ListObj.more
  :>Func ListObj.next : map() Func ListObj;
  :>$;

#square[n] <==> n*n;

#listobj(list)~
{
 more-[
       list$ |>#;
         :   |>=
      ]
 list=head:tail,next-|>head,list<-tail
}
This involves an object with a mutable state, as described in part 2. Here the object type is listobj. A listobj object has a list as its internal state, and takes two messages, next which returns the head of the list and sets the state variable to the tail of the list, and more which returns a boolean value saying whether the list is empty or not. It can be considered a sort of reader in which more says whether there are more items to read, and next gives the next item.

In map here, Func ListObj.next : map() Func ListObj is parsed as (Func (ListObj.next)) : ((map() Func) ListObj) due to the precedence and associativity of the operations. The code for listobj shows the Aldwych symbols for boolean literals, # for "false" and = for "true".

This example shows how in Aldwych it is possible to mix a functional style with mutable objects. A listobj object is mutable. Whenever a next message is sent to it, it changes its state, so that the next next message returns the next item in the list. For example, the following code:

#main ==
   Screen<-aio\stdio(),
   list1 <- map() square() List,
   list2 <- map() (add() 100) List,
   writeNumsList(Screen,list1),
   writeNumsList(Screen,list2),
   Screen.fwrite("Enter some numbers terminated by zero: "),
   List<-listobj(readNums(Screen));

#map[Func ListObj] <==
  ?ListObj.more
  :>Func ListObj.next : map() Func ListObj;
  :>$;

#square[n] <==> n*n;

#add[m n] <==> m+n;

#listobj(list)~
{
 more-[
       list$ |>#;
         :   |>=
      ]
 next-[
       list=head:tail |>head,list<-tail;
       :              |>0
      ]
}
will not create two lists, one from mapping square onto the original sequence of numbers, the other from mapping add() 100 to it (i.e., the function which takes a number and returns the number 100 greater). This is because when map from either mapping sends a next message to the shared listobj object, it causes the state of that object to change. The state change is global for all computations sharing the listobj object.

It is also necessary here to alter the code for listobj so that it is able to handle next messages when the internal state holds the empty list. Here it returns 0. In the first example of listobj the rule for next also had the condition that the internal list variable store a value with the pattern head:tail. Aldwych does not have a way of dealing with messages where none of the rules matches. Although the code for map would seem to guard against receiving a next message when the internal state holds an empty list, it is possible. This is because a next message from a concurrent computation can come between the more message sent by map and the next message sent if the more message returns "true".

Note that it is possible to mix the functional and procedural style. For example, the add function could have been written:

#add(m)[n] <==> m+n;
This results in an add function that can be curried to abstract out the first argument, but not the second. The line calling add above would have to be replaced by list2 <- map() add(100) List.

Here is a version of map defined in the way suggested by John Hughes in his well-known paper Why Functional Programming Matters:

#reduce[F x l]<
{
 l?y |>F y <;
  : ||>x;
}

#dot[F G x] <==> F (G x);

#cons[h t]<==> h:t;

#map[F]~ ==<= reduce() (dot() cons() F) [];

prev up next