Functions are declared with keyword: "function". A function can be public or private. You can define public functions in modules and private functions in other scripts.
#function declaration
function name(parameters) => (@result:TypeName):
** executable block
...
return;
Execute function Function is executed in expressions using the name of the function followed by semicolumn, and list of arguments in round brackets (). Brackets are mandatory even if there are no arguments.
Function arguments The arguments can be pass by position or by name. The names of arguments is useful when when a function has optional parameters.
#demo function execiton
driver function_ex:
** declare functions
function name1(param :Type,...) => (@result:Type):
let result := expression;
return;
function name2(param := value,...) => (@result:Type):
let result := expression;
return;
process
** execute using argument list
new result1 := name1(arguments);
** execute using parameter names with pair operator (:)
new result2 := name2(param:arg,....);
return;
Note: Argument value can be an expression or a variable that translate to a value of expected type. For clarity of application we encourage creation of local variables for arguments.
There is a difference between the parameter and the argument. The parameter is a local variable in the function scope while arguments are values assigned to these parameters in a function call. Arguments can be literals, constants, variables or expressions.
# demo function with parameters
driver function_params:
function sum(a, b: Integer) => (@result:Integer):
let result := (a + b);
return;
process
print sum(1,2); -- 3
print sum(2,4); -- 6
return;
A closure is a function created by another function. The closure has read/write access to context variables. The context is the scope of parent function, that is called high order function. The context is replicated and bound to every closure instance.
Closures are enabled in Eve to offer a convenient way to create generators. These are functions that can return a different result for every call. A closure is a light weight object.
driver test_closure:
# declare a high order function
function generator(start:Integer) => (@closure:Function):
new current := start;
** define a closure
let closure := Function() => (@result:Integer):
let current += 1;
let result := current;
return;
return;
process
** instantiate two closures
new index1 := generator(0);
new index2 := generator(10);
** interlace the two calls
print index1(), index1(); -- 1, 2
print index2(), index2(); -- 11, 12
print index1(), index1(); -- 3, 4
return;
Note: Closures are stochastic functions, that is oposite to deterministic. The compiler will make no effort to optimize this kind of functions.
A routine is a subprogram that has a name and parameters. Unlike a function, a routine do not have a result. Basicly a routine is a void function. A routine is used for its side-effects. Routines can be executed syncrhonously or asynchronously.
driver routine_demo:
** mandatory arguments
routine first_routine(param1, param2: Type):
print param1, param2;
return;
** optional arguments
routine second_routine(param1:=default1,
param2:=default2,
param3:=default3):
print param1, param2, param3;
return;
process
** execute routine synchronously with arguments
call first_routine(value1, value2);
** execure routine synchronously with no arguments
call second_routine;
** use only 2 arguments from a list of 3 possible
call secon_routine(param1:value1, param3:value3);
return;
Node: Asynchronous call is executed with keyword "call". But a routine need to yield otherwise this is the only method to execute the routine. The routine is executed in same thread as the main thread and is part of the parent process.
A process can use states. When a state is changed by a routine or method this is called side-effect. This may be good but potential unsafe. Some side effects may be harder to debug.
these are side effects...
Next routine: "add_numbers" has side effects:
driver side_effect:
set test :Integer;
set p1, p2 :Integer;
** subroutine with side effects
routine add_numbers():
let test := p1 + p2; -- first side-effect
print test; -- second side-effect
return;
** define main process
process
let p1 := 10;
let p2 := 20;
call add_numbers; -- simpel subroutine call
expect test == 30;
return;
A coroutine is a subprogram that has a name and can start a secondary thread. Coroutines can be suspended using "yield" keyword, and later wake-up with a signal and the thread continue with the next statement. Also the main thread can yield for a coroutine.
Synchronous vs Asynchronous
In next example we try to demonstrate a thread that can generate a a bunch of numbers every time the main thred is yielding for it. When the buffer is full, you can process the batch and then yield for a new.
driver shoulder_thread:
** coroutine producer, make 100 numbers
routine generator(cap:Integer, @result:()Float):
new count := 0;
loop
for i in (1..100) loop
let result += (0..1).random();
repeat;
yield; -- continue the main thread
let count += 1;
repeat if count < cap;
return;
process
new batch :()Float;
new total :Double;
** initialize the generator and produce first batch
run generator(1000, result:batch):
while batch.count() > 0 loop
** process current batch
for x in batch loop
let total += x;
repeat
** read the next batch
yield generator; -- resume generator
repeat;
return;
Time-out: Is easy to create a wrong shoulder thread that can runs forever. If a process takes too much time you can have a time-out. If one process time-out, the entire application crash. You can set $timeout system variable to control this time.
This design pattern enable you do create two coroutines that wait for each other. It is common practice to use this pattern to create a multi-thread applications. The threads communicate using one or more channels.
driver parallel_threading:
** coroutine producer, make 1000 numbers
routine producer(@pipeline: ()Integer, count = 1000: Integer):
cycle:
new i: Integer;
for i in (1..count) loop
let pipeline += random(100);
if (i % 100) == 0 then
** find one free consumer and resume it
yield consumer;
done;
repeat;
return;
** consume 100 numbers
routine consumer(@pipeline, @partials: ()Integer):
loop
cycle:
new element: Integer;
for element in pipeline loop
let pipeline := pipeline -> element;
wait 10ms;
let partials += element;
let i += 1;
repeat;
** send signal to producer to wake-up
yield producer;
repeat if pipeline.count() > 0;
return;
process
** execute routine with arguments
new pipeline: ()Integer;
new partials: ()Integer;
** execute producer/consumer
run producer(pipeline, 10000);
for i in [1..32] loop
run consumer(pipeline,partials);
wait 3ms;
repeat;
** wait fo all threads to finish
yield all;
** reduce the partials into one result
new result := sum(partials);
print result;
return;
Notes: a yielding routine is suspended in memory, waiting for a wake-up signal. Another coroutine must signal the wake-up. If the coroutine receiving the signal is not running then it can't wake-up. In this case, the control is given back to the main thread. If the main thread is yielding it will resume.
Lambda expressions are simpler functions that respect arithmetic rules. These can receive several arguments and can produce one result. A lambda expression is created with keywords "set" or "new". Lambda expressions do not have local variables or states and do not have side effects.
Lambda expressions can be declared in global region but alos in the process region. You can define more than one global regions in a script. Lamda expressions are associated to a variable, sometimes can be associated to a member in a collection.
In next example we define two lambda expressions.
global
set sum = (p1, p2 :Integer) => (p1 + p2):Integer;
set sqr = (p1, p2 :Integer) => (p1 * p2):Integer;
Labda expression can be declared as data type using keyword "class". Later, this type can be used to enforce a specific signature for a call-back argument, varible or collection element.
# define a lambda signature
driver lam_sig
** Sefine callback signature
class BinEx = (p1, p2 :Integer):Integer <: Lambda;
** Use lambda signature as parameter type
routine useLambda(test :BinEx):
print test(); -- execute callback
return;
** Execute a process with lambda argument
process
** use anonymous lambda argument
call useLambda((x, y) => (x + y));
return;
Note: Lambda expressions can be created by a function or method as a result. Lambda expressions can't modify values of variable declared in the parent scope.
Lambda expressions can read global or parent scope states, but can not modify them. However, if external values change after expression is defined, the result may be influenced by the new values.
# verify shared state influence
driver shared_states:
** define global settings
set decimals = 2: Integer;
global
set trunc = (x:Double) => floor((x)* 10^decimal)/10^decimal) :Double;
process
new x := 10/3;
** call the lambda expression
print trunc(x) -- expected 3.33
** overvrite the initial settings
let decimals := 4;
print trunc(x) -- expected 3.3333
return;
Disclaim: We aknowledge, Eve is not a pure functional language. This behaviour is present in many other modern languages. Eve is not different. We seek for a balance between the theory and pragmatism.
Read next: Collections