-
Notifications
You must be signed in to change notification settings - Fork 8
Functional Programming
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.
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.
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( ...).
expr = {(a) a+1}
expr(1) // evaluates to 2
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
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 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
nameparameter to call itself, as it is known to the system through the symbols or concept properties to which it is assigned.
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=1will 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= exprin invocations expands intox = {() expr}, and can be used as a fast mean to pass an expression as-is as to a non-eta named parameter.
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, 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.
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() )
:defpand:clare specially known by:exprand:defun, and they are evaluated properly by the engine. Invoking them outside a definition is a runtime error.
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
:leaveis lazy: whenevalOnReturnis false, it is evaluated in the current (:leavecaller'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.
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
ifstatement is shown here for explanatory purposes. A more efficient way of perform the same operation is:leave(seq.at(id), idp < seq.length()); asseq.at(id)will evaluate tovoidwhen id is out of range, there isn't any need for the check at(1), and being a lazy parameter, in that caseidp < seq.length()will not be evaluated.
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.
The :eval() function family allows for indirect evaluation.
-
:eval(expr, ...)performs a parametric evaluation ofexpr, passing its other parameters to it. Named parameters will be passed as such toexpr. -
:veval(expr, pvect, pdict=[=>])performs a parametric evaluation ofexpr, expanding the content of the vectorpvectas the positional parameters, and assigning the entries inpdictto the parameters by name.
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 withexpr; it receives the same parameters as the ones active in the current frame. -
:forwardp(expr,...): Substitutes the current frame withexpr, and replaces the current parameters with its varadic parameters. -
:forwardv(expr, pvect, pdict=[=>]): Substitutes the current frame withexpr, expanding the content of the vectorpvectas the positional parameters for the new evaluation, and assigning the entries inpdictto the parameters by name.