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) [];