Sage-Code Laboratory
index<--

Eve Functions

A function is a relation between input and output values. A function makes a computation to establish the output. Eve functions are objects of type Function. Eve enable functional programming paradigm (FP).

Page bookmarks:



Function declaration

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.

Pattern:

#function declaration
function name(parameters) => (@result:TypeName):
  ** executable block
  ...
return;

Notes:

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.

Pattern:


#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.

Formal parameters

Function 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.

Example:

# 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;

Closures

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.

Why closures?

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.

Routines

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.

Side Effects

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...

Examples

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;

Coroutines

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.

Asynchronous Execution

Synchronous vs Asynchronous

Shoulder Thread

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.

Multi Threading

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.

Example


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

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.

Example

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;

Lambda signature

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 & states

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.

Example:


# 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;
Warning: Observe we have used "set" to create the initial value for a global variable. Then we alter it using "let". The new value is used by the function to cut several decimals after round(). This is not exactly a side-effect but is an influence, that is not accepted in functional programming.

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