Higher Order Functions in Moto
David Hakim <dhakim-Gkm/TONP9n1Wk0Htik3J/[email protected]> Tue, 11 Mar 2003 23:38:58 -0500
| Newsgroups | gmane.comp.lang.moto.devel,gmane.comp.lang.moto.user |
|---|---|
| Message-ID | <[email protected]> |
Here's a rough draft of the documentation I want to add to support
site about HOFs. Comment, criticisms, edits :) Pass em along!
-Dave
Implicitly Valid Types
------------------------
Arrays are the simplest example of implicitly valid types in Moto.
Given a valid type X, X[] is a valid type in Moto. Specifically X[]
represents an array of X's . And X[][] is a type representing a two
dimensional array of X's. This sort of dynamic type definition is
common to many languages including Java.
There is another class of types in Moto that are 'implicitly valid' as
long as their base types are valid. These are called functional types
which can be described as follows:
1) If X is a valid type X() is a valid type - It represents a function
which takes no arguments and returns a value of type X
2) If X and Y are valid types X(Y) is a valid type - It represents a
function which takes an argument of type Y and returns a value of type X
3) If X is a valid type and Z is comma delimited list of valid types
than X(Z) is a valid type - It represents a function which takes
arguments of the types listed in Z and returns a value of type X
Here are some concrete examples of declarations using functional types:
int() f; // f can be assigned a function which takes no arguments and
returns an int
int(int) g; // g can be assigned a function which takes an int and
returns an int
int(int,String) h; // h can be assigned a function which takes an int
and a String and returns an int
int(int()) i; // i can be assigned a function which takes a function
that takes no arguments but returns an int. i must also return an int.
int(Vector(int),String()) j;
Enumeration(StringBuffer(int),String())(String(),float) k;
Object(int[](StringBuffer[][]),String())(String[](),float[]) l;
Functional Typed Variables
------------------------
As you might have guessed by the name (and the above examples)
functionally typed variables can be assigned real functions or methods
who's arguments and return types match. Consider to the function atoi
in cstdlib which takes a String and returns an int. We could define a
functional variable 'myatoi' and assign it atoi:
int(String) myatoi = atoi;
Functional variables can be called just like regular functions and
methods.
int i = myatoi("3");
What is the 'type' of a Function.
------------------------
The type of a function or method in Moto is
- its return type
- followed by '('
- followed by the types of each of the arguments it takes separated by
commas
- followed by ')'
Simple ... no ?
Function Identification
------------------------
The atoi example is simple in that there is only one function in the
system named atoi. Moto however, like C++ and Java allows for multiple
functions to have the same name but take different arguments. A good
example of this is the getValue function from the codex.http library.
There are two variants of getValue, one that takes one argument, and
one that takes two. We can differentiate between the two by using the
'?' or 'unknown argument' token.
String(String) myGetValue = getValue(?);
String(String,String) myGetValueWTwoArgs = getValue(?,?);
We can be even more specific by using casts in addition to the '?'
token. Suppose we have two functions with the same name that take a
single argument (but of two different types) :
void println(int i){ print i+"\n"; }
void println(String s) { print s+"\n"; }
We can specify which one we want to identify by casting the unknown:
void(int) myIntPrintln = println(<int>?);
void(String) myStringPrintln = println(<String>?);
The last thing worth noting here is that you can optionally use the '&'
operator to make clear that you want to identify a function. The
following are examples of fully qualified function identification:
void(int) myIntPrintln = &println(<int>?);
void(String) myStringPrintln = &println(<String>?);
The real reason for the '&' becomes clear in relation to partial
application discussed later. For the sort of regular function
identification that has been described using the '&' or 'function
identification' operator can improve code readability but is generally
optional. The only case where it is necessary is when we have multiple
variants of a function with the same name and we need to identify the
no-arg variant:
void println(){ print "<empty line>\n"; }
void() myPrintln = println; // No good ... this is ambiguous
void() myPrintln = &println() // Perfect ... this is fully qualified
identification
Functions (and Methods) that take Functions as Arguments
------------------------------------------------
Since functional types are no different from other types, creating
functions that take functions as arguments is done just like you might
suspect. The following function takes an array of Strings and a
function to perform on each element of the array:
void each(String[] a,void(String) f){ for (int i=0;i<length(a);i++)
f(a[i]); }
We could pass this function a function println
void println(String s) { print s+"\n"; }
To print out all the elements in an array:
each(<String[]>{"one","two","three"}, println);
The Enumeration class has an each() method built in as of Moto 0.19.0
which calls the function passed to it on each element in the
enumeration. Thus to print out all the keys in a hash you could now
just write
stab.keys().each(<void(Object)>println);
This is as opposed to
Enumeration e = stab.keys();
while(e.hasNext())
println(<String>e.next());
Which is the 'non-functional' (Java) way of doing things.
Notice the explicit cast to type 'void(Object)' in the functional
example . That is needed because that is the type of the function the
each method expects to be passed while println has type 'void(String)'.
Sometimes functions can be cast 'implicitly' but this is not one of
those times.
No example of passing functions to functions would be complete without
the classic example of sorting. As of Moto 0.19.0 Vectors have a sort()
method. This method takes a function which can be used to compare two
Objects in the Vector. Why does it need to take a function you ask ?
Because there is no way to compare two arbitrary objects.
The function must take two Objects and return 0 if they are equal, 1 if
the first one is 'greater' than the second, -1 if the first one is
'less' than the second. What 'less','greater', and 'equal' really mean
in this context is that you want the the first element to appear
before, after, or next to the second element in the final sorted list.
Luckily the is a function that does just that for Strings in the
cstdlib extension called strcmp.
Thus to sort a Vector of Strings in Moto all you need to do is say
v.sort(<int(Object,Object)>strcmp)
Again we need to explicitly cast strcmp to 'int(Object,Object)' from
'int(String,String)' . The reason is that the Vector doesn't know its
holding Strings. For all it knows its holding Dates or any other kind
of Object and so it expects functions that work on 'Object's not
sub-classes of Object. If it were holding dates however you might sort
them as follows:
int dcompare (Object a,Object b){
Date l = <Date>a, r=<Date>b;
return atoi(l.format("%Y%m%d")) - atoi(r.format("%Y%m%d")) ;
}
v.sort(dcompare);
Implicit Casting Rules
------------------------
You don't need to 'explicitly cast' functions (or functionally typed
expressions) when assigning them or passing them to functions when they
can be 'implicitly cast'. The implicit casting rules for functional
types are as follows:
1) Like all reference types (yes, functional types are reference types)
values with functional types can be implicitly cast to 'Object'
2) Any type X(...) can be implicitly cast to void(...) since 'void'
really means we don't care what if anything the function returned
3) Any type X(...) can be implicitly cast to Y(...) as long as X is a
descendent of Y. The reason is if we have a function foo, which for the
sake of argument takes no arguments and returns a String, and we assign
it to a variable of type 'Object()' that assignment should of course be
valid since everything foo could return would also be an Object.
4) Any type X(... Y ...) can be implicitly cast to X(... Z ...) as long
as Y is an ancestor of Z. This may seem counter-intuitive be the
rationale is as follows. Suppose we have a variable of type
'void(String)'. This functional variable must be callable with any
String. If we assigned to it a function of type 'void(Object)' than the
requirement that 'any function assigned to it must handle all values of
type String' is not broken. The function we assigned to it can handle
all Strings and then some.
Explicit Casting Rules
------------------------
The explicit casting rules are like the implicit casting rules and
their mirror image.
1) If X can be implicitly cast to Y then X can be explicitly cast to Y
2) If X can be implicitly cast to Y then Y can be explicitly cast to X
Partial Application
------------------------
In cstdlib there is a function pow
double pow(double x, double y)
which returns x to the power y (x^y). Suppose we wanted to define a
function 'square' which simply squares (x^2) its input. We could say:
double square(double x) { return pow(x,2); }
Or we could do this:
double(double) square = pow(?,2);
What we've done is created a new function by 'partially applying' the
arguments to an existing function. We could partially apply the other
argument to create a function twoToThePow (2^x)
double(double) twoToThePow = pow(2,?);
We could even apply both arguments to give us a function that returns
the same thing all the time:
double() nine = pow(3,2) // No no no ... this won't work
The above actually generates an error saying 'Expression of type double
cannot be assigned to variable of type double()' . Thats because moto
thinks the expression 'pow(3,2)' is a function call, not a function
identifier. To clarify the situation you need to use the '&' operator.
double() nine = &pow(3,2) // Bingo!
Congratulations, we've crated a function that returns 9 all the time :)
Callbacks
------------------------
Suppose we have a Vector v. We can say
void(Object) add = v.add;
The above defines a new function add() which, every time it is called,
adds an Object to v. This is actually a powerful example of partial
application. Behind the scenes all methods secretly take the instance
of the object they are called on as an argument. By identifying 'v.add'
we are saying we want the 'add method on the Vector currently assigned
to v'. If v were to be assigned a different vector later on the add
function we just defined would still add objects to the original Vector.
So what makes this so powerful ? Lets look at some examples
// Cloning a Vector
v1.elements().each(v2.add);
// Pruning a Vector of duplicates
Stringset s1 = new Stringset()
v1.elements().each(<void(Object)>s1.add);
s1.elements().each(v2.add);
// Printing out the elements in a hash (SymbolTable)
void outputNV(Object n,SymbolTable s) { print <String>n +" =>
"+<String>s.get(<String>n)+"\n"; }
stab.keys().each(outputNV(?,stab));
// Inverting a hash
void valueToKey(Object oldkey, SymbolTable src, SymbolTable dest) {
dest.put(<String>src.get(<String>oldkey),oldkey); }
SymbolTable namesur = new SymbolTable();
surname.keys().each(valueToKey(?,surname,namesur));