Functions compared to procedure are pure. A pure function is a function that has the following properties:
- Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
- Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).
Thus a pure function is a computational analogue of a mathematical function. Pure functions are declared with fun[]
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }
var[] add = [fun(el1, el2: int): int]{ result = el1 + el2 }
Functions in FOL are lazy-initialized.
So it is an evaluation strategy which delays the evaluation of the function until its value is needed. You call a function passing it some arguments that were expensive to calculate and then the function don’t need all of them due to some other arguments.
Consider a function that logs a message:
print.debug("Called foo() passing it " + .to_string(argument_a) + " and " + .to_string(argument_b));
The log library has various log levels like “debug”, “warning”, “error” etc. This allows you to control how much is actually logged; the above message will only be visible if the log level is set to the “debug” level. However, even when it is not shown the string will still be constructed and then discarded, which is wasteful.
Since Fol supports first class functions, it allows functions to be assigned to variables, passed as arguments to other functions and returned from other functions.
Anonymous functoins
Anonymous function is a function definition that is not bound to an identifier. These are a form of nested function, in allowing access to variables in the scope of the containing function (non-local functions).
Staring by assigning a anonymous function to a vriable:
var f = [fun(a, b: int)]{ // variable assignmet to function
return a + b
}
.echo(f(5,6)) // prints 11
It is also possible to call a anonymous function without assigning it to a variable.
[fun(a, b: int)]{ `define anonymous function`
.echo(a + b)
}(5, 6) `calling anonymous function`
Higer-order functions
A higher-order function is a function that takes a function as an argument. This is commonly used to customize the behavior of a generically defined function, often a looping construct or recursion scheme.
They are functions which do at least one of the following:
- takes one or more functions as arguments
- returns a function as its result
//function as parameter
fun[] add1(adder: rut[(x: int): int]): int = {
return adder(x + n)
}
//function as return
fun[] add2(): rut[(x: int): int] = {
var f = [fun(a, b: int): int]{
return a + b
}
return f
}
rut
representr a routine (it can be: fun
, pro
, or log
).
Closures
Functions can appear at the top level in a module as well as inside other scopes, in which case they are called nested functions. A nested function can access local variables from its enclosing scope and if it does so it becomes a closure. Any captured variables are stored in a hidden additional argument to the closure (its environment) and they are accessed by reference by both the closure and its enclosing scope (i.e. any modifications made to them are visible in both places). The closure environment may be allocated on the heap or on the stack if the compiler determines that this would be safe.
There are two types of closures:
- anonymous
- named
Anonymus closures automatically capture variables, while named closures need to be specified what to capture. For capture we use the []
just before the type declaration.
fun[] add(n: int): int = {
fun added(x: int)[n]: int = { // we make a named closure
return x + n // variable $n can be accesed because we have captured ti
}
return adder(5)
}
fun[] add(n: int): int = {
return [fun(x: int): int]{ // we make a anonymous closure
return x + n // variable $n can be accesed from within the nested function
}(5)
}
Currying
Currying is converting a single function of “n” arguments into “n” functions with a “single” argument each. Given the following function:
fun f(x,y,z) = { z(x(y));}
When curried, becomes:
fun f(x) = { [fun(y)]{ [fun(z)]{ z(x(y)) } } }
And calling it woud be like:
f(x)(y)(z)
However, the more iportant thing is taht, currying is a way of constructing functions that allows partial application of a function’s arguments. What this means is that you can pass all of the arguments a function is expecting and get the result, or pass a subset of those arguments and get a function back that’s waiting for the rest of the arguments.
fun calc(x) = {
return [fun(y)]{
return [fun (z): int]{
return x + y + z
}
}
}
var value = calc(5)(6) // this is okay, return value is a function
var another int = value(8) // this completes the function by calling the function above
var allIn: int = calc(5)(6)(8) // or this as alternative
A more verbose way of the function above is:
fun calc(x): [fun(y): [fun (z): int]] = {
return [fun(y): [fun (z): int]]{
return [fun (z): int]{
return x + y + z
}
}
}
Generators
A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.
For a function to be a generator (thus to make the keyword yeild
accesable), it needs to return a type of container: arr, vec, seq, mat
but not set, any
.
fun someIter: vec[int] = {
var curInt = 0;
loop(){
yeild curInt.inc(1)
}
}