Skip to content

Functional Programming

Giancarlo Niccolai edited this page Jul 17, 2018 · 14 revisions

Falcon2 is based on a purely functional engine, with extensions for OOP/AOP and C-Rel paradigms. As such, every instruction is an evaluation. The basic evaluation unit is the expression, which can be parametric, if it has local symbols which are given a value during the evaluation process, or direct if it doesn't.

Expression sequences are themselves expressions, whose evaluation consists in evaluating each expression contained in them.

Functions are special kind of expressions, which evaluation forms a function frame, which can be manipulated (i.e. exited) specifically.

Basic evaluation

Expressions written directly are immediately evaluated. Parenthesis at the right of an expression express an explicit evaluation, and the expressions within the parenthesis are parameters for the evaluation

1+1          // Evaluates to 2
a = 10       // Evaluates to "assign 10 to a"
x            // Evaluates the symbol 'x', i.e. take its value
x()          // Evaluates the value in x
x()()        // Evaluates the result of the evaluation of the value of x
x(1,2)       // Evaluates the value of x passing 1 and 2 as parameters.

Indirect expressions

The symbol {...} declares a list of expressions to be evaluated in sequence; it's value is the value of the last evaluated expression:

val = {
    printl("I will have the value 2")
    1+1
}
printl(val)   // 2

The symbol {...} is equivalent to the function :run(...).

The symbol {() ...} declares a simple expression, which evaluates to the expression contained between the brackets.

e = {() printl("Hello world")}   // e is the expression `printl("Hello world")`
e()                              // now "Hello world" gets printed

{() ...} expands to :expr( ...).

Parametric Expressions

expr = {(a) a+1}
expr(1)           // evaluates to 2 

Closed Expressions

c = 100
d = 0
expr = {(a)[c,d=10] a+c+d}  // closes "c" and creates a local "d"
c = 0
expr(1)         // evaluates to 111

Referring to the current evaluation

The symbol selfie refers to the current evaluation frame, and can be used to recurse in expressions that can't access their own symbol. For example:

print( "Sum of 10 numbers = ",  {(n)
   if n <= 1 1
   else n + selfie(n-1)
   }(10)
)

Functions

Functions are named parametric/closed expressions; also, anonymous functions are parametric expressions.

function NAME {(...)[...]
    a = 1
    b = 2
    expr1()
    expr2()
}

NAME() 

The function syntax construct is equivalent to the expression name = :defun(name,...) function, and if NAME=nil, it's equivalent to :expr(). More about parameter declarations in the functional model later.

The function name is a metadata available for debugging purpose only, and it doesn't identifies a function, which is usually invoked (and known in a stack trace) through the symbol to which it was assigned at the moment of the call. This also implies that a function can't necessarily rely on the name parameter to call itself, as it is known to the system through the symbols or concept properties to which it is assigned.

Parameter Declaration

Parameter declaration syntax is common for parametric expressions an functions. The specification for parameters is:

PARAMS := PARAMLIST ?SEPARATOR ?ELLIPSE;
PARAMLIST := *PARAMDECL;
PARAMDECL := (?LAZYDECL NAME ?DFLT_VAL) | (HIDDENDECL ?LAZYDECL NAME DFLT_VAL);
LAZYDECL := '&';
HIDDENDECL := '@';
DFLT_VAL := '=' VALUE;
NAME := 0-9 a-z ƒA-Z etc...
SEPARATOR = ' ' | ',' | EOL;

An eager parameter receives an expression that is evaluated before the final evaluation is performed. An lazy parameter receives the parameter expression as is, without pre-evaluation. An hidden parameter has no positional value in the parameter list, and must be explicitly named to give it a value. It's mandatory to provide a default value for hidden parameters. The ellipse indicates that other positional parameters are accepted (and can be accessed through several means).

For example:

f = {(eagerParam, &lazyParam, p="DefaultValue", @hidden="DfltHidden" ...) 
    // ... do something ...
}

f(
    a+1,       // a+1 is evaluated here, and its result is passed to f
    b+2,       // "b+2" is passed as-is, and could be evaluated within f
               // (where b is a different local variable)
    hidden= 1  // explicitly set hidden...
    "p val"    // ... but p can still be set positionally as third param
    10, 11, 12 // these are varadic parameters passed in coda.
)

The evaluation invocation context constitutes a context separate from its caller, and partially distinct from the actual evaluation context. This means that hidden=1 will create a symbol that will be interpreted as a parameter.

Eta-parameters (e.g. &name) cannot normally be assigned by name, as name=expr will be interpreted as a literal expression given as the eta-parameters, even when the parameter is &name. The prefix & allows to access eta-parameters by name:

f = {(&a, &b) a() + b()*2 }
f( &b= a=1,  &a= b=2 }  // b is the second parameter, so f(2,1) = 4

Passing &x= expr in invocations expands into x = {() expr}, and can be used as a fast mean to pass an expression as-is as to a non-eta named parameter.

Closure declaration

Square brackets after parameter specification in expression and function declaration generate a closure.

Empty square brackets [] indicate that every non-local symbol is to be considered an external symbol to be closed. It is preferable to specify the closed symbols directly, so that non-declared symbols accessed in the declaration will be considered undefined and generate an error.

It is possible to specify an initial value for the closed symbol; this will create a closure instance specific symbol, value, which will not be closed from the outer context.

Examples:

a = 100
c = 101
f1 = {() [] printl(a) }      // Closes 'a', as it's undefined in f1
f2 = {() [] printl(b) }      // Searches 'b' but it will not find it
f2 = {() [a] printl(a) }     // Preferred way to close 'a'
f3 = {() [c=10] printl(c) }  // Will be 10, as 'c' the external c is ignored.
}  

Lazy parameters

Lazy parameters, prefixed with &, receive the expressions passed as parameters as is, that is, not pre-evaluated. This allows to receive expressions directly and then decide if (and how) to evaluate them at a later time.

The following example evaluates the passed expression once every two calls.

// expr is a lazy parameter:
f = {(&expr)[a=0] 
    if a % 2 == 0 
       expr()
    a += 1
}

f(printl("one"))    // "one"
f(printl("two"))    // nothing
f(printl("three"))  // "three"
f(printl("four"))   // nothing

Notice that this would be equivalent to having an eager parameter which receives a indirect expression, but declaring the laziness of the parameter evaluation in the expression parameters allows for a simpler and more readable syntax.

Functional function definition

The functions :defun and :expr are equivalent to the grammar constructs function Name {(...)[...] ... } and {(...)[...] ...}. To define the parameters and closed symbols for the functional definitions, the functions :defp(&...) and :cl(&...) are provided. For example:

x =1
f = :expr( :defp(a, &b) :cl(x, y=10)
    y+=1
    leave x + y + a + b() )

:defp and :cl are specially known by :expr and :defun, and they are evaluated properly by the engine. Invoking them outside a definition is a runtime error.

Frame exit and void evaluations

Normally, every evaluation yields the value of the last sub-expression being evaluated.

The keyword leave (with the options ? and &) terminates the current evaluation frame, possibly assigning a value to it.

It has the form functional form :leave(), which yields a void result, and
:leave(&value, &more=false, &evalOnReturn=false), which correspond to the following statements

  • leave -> :leave()
  • leave x -> :leave(x)
  • leave? x -> :leave(x, true)
  • leave& x -> :leave(x, false, true)
  • leave?& x -> :leave(x, true, true)`

The more parameter indicates that there are more results that the evaluation might yield if invoked again:

generator = {(v)[idx=0]
   id = idx; idx += 1
   :leave(v[id], more=idx < v.len)
}
for i in generator([1 2 3])
    printl(i)

The evalOnReturn indicate to re-evaluate the returned value right after the frame has been left, i.e. in the caller's evaluation frame.

squareA = {() leave& a*a}
a = 10
f()     // This becomes "a*a" in this context
// Return is the same as leave, but it exits the current function frame.

Notice that the value parameter in :leave is lazy: when evalOnReturn is false, it is evaluated in the current (:leave caller's) frame.

It is legal for the value parameter to evaluate as void; in that case, the frame will be void, and both more and evalOnReturn will be ignored.

Void evaluations

Evaluations explicitly using the leave keyword will yield a void value.

void is a non-value which cannot be assigned, tested or iterated upon. Assigning or testing for a void evaluation causes a runtime error, while iterating on that causes the iteration to be stopped (or possibly never performed).

For example, a complete generator can be:

function make_generator {(seq) 
   leave {()[seq, idp=0]
      id = idp; idp += 1
      
      if seq.length() <= id    // empty/ out of range?
          leave                // (1) - leave as void
      elif seq.length() == idp // last loop?
          leave seq[id]        // (2) - leave deterministically
      else leave? seq[id]      // (3) - leave, but more to come
   }
}

Notice (3), generating a doubtful frame exit, which implies more values can be found, and (1) that specifies no result available.

Iterations that need to know whether an element is the last or not will see (2) exit frame as deterministic, and treat that value as the last in the sequence.

This verbose/inefficient three way if statement is shown here for explanatory purposes. A more efficient way of perform the same operation is :leave(seq.at(id), idp < seq.length()); as seq.at(id) will evaluate to void when id is out of range, there isn't any need for the check at (1), and being a lazy parameter, in that case idp < seq.length() will not be evaluated.

Parameter access

Some standard functions allow to access the parameters of the current evaluation by position, by name and by their varadic status.

It is not possible to access parameters of an outer explicit evaluation (function call or expression evaluation).

  • :pcount(): returns the total count of parameters for this evaluation (including varadic parameters).
  • :vpcount(): returns the count of varadic parameters in this evaluation.
  • :param(n): retreives the n-th positional parameter, without distinction for named and varadic parameters (won't access hidden parameters).
  • :vparam(n): retreives the n-th positional varadic parameter, the first one numbered 0.
  • :params(): returns a vector containing all the positional parameters (not including hidden parameters).
  • :vparams(): returns a vector containing the varadic parameters only, respecting their order.
  • :pdict(): returns a dictionary containing all the non-varadic parameters (including hidden parameters); the key of the dictionary is the parameter symbol.

Notice that hidden parameters are non-positional; hence, it's not possible to access them by order or retreive them in an oredered set.

Indirect evaluation

The :eval() function family allows for indirect evaluation.

  • :eval(expr, ...) performs a parametric evaluation of expr, passing its other parameters to it. Named parameters will be passed as such to expr.
  • :veval(expr, pvect, pdict=[=>]) performs a parametric evaluation of expr, expanding the content of the vector pvect as the positional parameters, and assigning the entries in pdict to the parameters by name.

Evaluation forwarding

There is a set of functions forwarding the current evaluation. This means that the current evaluation frame is discarded, and substituted with the forward evaluation frame. This procedure is more efficient than generating another evaluation frame to then capture its result, to pass it back to the current caller. It is particularly useful when handlers and delegates perform some pre-processing, and then need to invoke a different function.

Forward functions don't return; instead the caller receives the result of the forwarded expression.

  • :forward(expr): substitues the current frame with expr; it receives the same parameters as the ones active in the current frame.
  • :forwardp(expr,...): Substitutes the current frame with expr, and replaces the current parameters with its varadic parameters.
  • :forwardv(expr, pvect, pdict=[=>]): Substitutes the current frame with expr, expanding the content of the vector pvect as the positional parameters for the new evaluation, and assigning the entries in pdict to the parameters by name.

Clone this wiki locally