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