Partial application of functions
Partial application is a powerful feature that allows you to prepare a function call in advance of it being used. This makes many pieces of code easier to write, and also allows lazy evaluation, where complex calculations are postponed until the answer is needed.
Partial application underlies Kaya's secure web application state handling, and so this tutorial should be read at least briefly before looking at web applications - understanding at least the syntax if not the reasons for using partial functions will be necessary to make sense of the code examples.
Syntax summary
Int functionOne() {
return 5;
}
Int functionTwo(Int a, Int b) {
return a+b;
}
The two functions above will be used to demonstrate partial function syntax.
a = functionOne;
// 'a' is an Int - since the brackets are optional for parameterless
// functions, it evaluates the function.
b = @functionOne;
// 'b' is a reference to functionOne, and has type Int()
c = b();
// 'c' is an Int
d = functionTwo;
// because functionTwo has required parameters:
// 'd' is a reference to functionTwo
// d = @functionTwo; means the same thing
// 'd' has the type Int(Int,Int)
e = d(1,2);
// 'e' is an Int
So far this is similar to callbacks in C. However, for a function that has parameters, it's also possible to create a reference to the function with some of the parameters already filled in.
f = functionTwo@(5);
// this creates a reference to an anonymous function with one parameter,
// of type Int(Int). functionTwo has been partially applied
g = f(3);
// this finishes applying the function. 'g' is an Int
h = functionTwo@(3,4);
// 'h' has the type Int()
i = f@(-1);
// 'i' also has the type Int() - it's possible to partially apply a
// partially-applied function just as if it were a normal function.
Partial application and 'var' arguments
Currently, there is no mechanism for a partially applied function to modify its arguments if they are declared 'var', although modification of components of array or data type arguments using the normal 'shallow copy' rules can still be done. The following code does nothing:
a = 1;
b = 2;
swapfn = swap@(a,b);
swapfn();
// a = 1; b = 2;
This limitation will be removed in a future Kaya version.
Examples of partial application
import Dict;
Void main() {
mapping = Dict::new();
record = add@(mapping);
record("key1","value1");
// etc...
}
This code defines a record function, which is the add function, with the first argument already filled in as a particular dictionary. The record function can now be called with record(x,y) equivalent to add(mapping,x,y). This is a relatively simple use of partial application, merely to save some typing and have clearer code. The next example allows checks on data and execution of the results of these checks to be carried out in the correct order, while also giving a logical order for the source code.
items = getItems();
processing = [];
for item in items {
if (checkItem(item)) {
push(processing,processItem@(item));
} else {
throw(CheckFailed);
}
}
for processor in processing {
processor();
}
Here, the action if the check succeeds is defined next to the check itself, but none of the actions will be carried out unless all the checks succeed. Note also that in this case, the partial application has applied all of the processItem function's arguments in advance, and so no extra arguments are passed when the function is called. Equally, in some circumstances, it may be useful to partially apply a function with no arguments given.
Bool check(Bool(a) pred, a val) {
return pred(val);
}
Bool long(String str) {
return (length(str) > 100);
}
Void main() {
if (check(long@(),"a string")) {
putStrLn("It's long!");
}
}
An alternative way of writing long@() is @long.
Separating code into logical components
[BigData] getData([Int] ids) {
bigdatas = [];
for id in ids {
push(bigdatas,getThisData(id));
}
return bigdatas;
}
Void processData([BigData] bigdatas) {
for bigdata in bigdatas {
// do something
}
}
Separating the retrieval and processing functions out is essential for good design if the data may need to be processed in different ways. However, there is a disadvantage if the data is large, because the entire data set must be kept in memory at once. An alternative method is:
Void getAndProcessData([Int] ids) {
for id in ids {
bigdata = getThisData(id);
// do something
}
}
This method, however, entangles two separate operations, making code reuse more difficult. On the other hand, memory usage will be significantly smaller. Partial application allows the best of both approaches to be combined.
[BigData()] getData([Int] ids) {
bigdatas = [];
for id in ids {
push(bigdatas,getThisData@(id));
}
return bigdatas;
}
Void processData([BigData()] bigdatas) {
for bigdata in bigdatas {
currentbigdata = bigdata();
// do something with currentbigdata
}
}
The code ordering and reusability is as good as the first code, but because generation of the data is delayed until it is needed, memory usage is optimised.
The Lazy List data type from the Lazy module can be used for lazy generation and evaluation of lists.
data List = nil |
cons(a head, Lazy::List<a> () tail)
Compare this with the standard List from the standard Prelude.
data List<a> = nil |
cons(a head, List<a> tail);
The difference is that in the lazy list declaration, the tail of the list is a function, rather than a list. This function will be executed when the tail of the list is needed, rather than the entire list being built in advance.
Changing types with partial application
If you have a function that takes a function as an argument, e.g. a Bool(Int), then you can use a Bool(...,Int) function instead, by providing the initial arguments in a partial application.
Bool isEqual(Int a, Int b) {
return a==b;
}
Void main() {
vals = [1,2,3,4,5];
if (any(isEqual@(3),vals)) {
putStrLn("A value equals 3");
}
}
This allows functions to be used more generically.
Anonymous (lambda) functions
Anonymous functions can be created with lambda expressions, introduced with a backslash, \, representing a Greek letter lambda, followed by the argument list (optionally with types) in brackets. A simple example is the following, which creates a function then calls it immediately:
Void foo()
{
x = \(a,b) { return a+b; } (4,5);
putStrLn("x = "+x);
}
A slightly shorter syntax, if a function just returns an expression as above, is:
x = \(a,b) -> { a+b } (4,5);
Of course, usually if you construct a function like this, you would not be applying it immediately. A more common use is in applications of higher order functions such as map. For example, you can double every element in a list as follows:
doubled_xs = map(\(a) -> { a*2 }, xs);
You can use any variable which is in scope in the body of the lambda expression. For example, the following is valid (get is defined in the IO module and reads a line from a file):
mult = Int(get(stdin));
multiplied_xs = map(\(a) -> { a*mult }, xs);
From Kaya 0.3.0 onwards, variables that are in scope outside the lambda and used inside the lambda will be modified by the lambda (they are passed into the lambda as 'var'). In Kaya 0.2, variables from outside the lambda would not be modified inside it.
foo = 0;
wibble((x) -> { foo++; stuff(x); });
putStrLn(String(foo));
This code will print the number of times that wibble() called the lambda function in Kaya 0.3.0 onwards. In Kaya 0.2 this would (incorrectly) print zero.