Local functions and local value captures

The feature described in this page is experimental and not officially "exposed" or supported and is subject to change in future releases. It requires Analytica 6.0 or later.

Local functions

A local function is a callable entity that is defined within an expression. It does not exist in the global namespace, so like a local value its identifier is only visible within the local scope. In addition, unlike a UDF, a local function is a type of value rather than an object -- i.e., it behaves as a data type and has no attributes of its own.

You can treat a local function as you would a data element like a number -- you can have arrays of local functions, can assign local functions to other local function identifiers, etc. Calling a local function uses the same syntax as any other function call in Analytica. Local functions array abstract and use the same parameter declaration syntax and qualifiers that UDFs use.

In other programming languages, these are called lambda functions, or sometimes just lambdas.

Local functions have an extra special power, which is that they can capture other local values. This is described below.

A local function is created within an expression using the following syntax:

Function «ident»( «parameter declarations» ) : «expression» Do «body»

or

Function «ident»( «parameter declarations» ) : « expression»;
...

It is important to note the use of a colon (and not :=, which has a different meaning) between the parameter declaration and the «expression» that defines the function.

The following is a simple example that computes $ x^2 ( x^2 + 1) $:

Function sqr(x) : x^2 Do sqr(x) * (sqr(x) + 1)
Note: There is a chance we might change the declaration to use LocalFunction...Do in the future for declaring a local function. As stated, this feature is experimental and subject to change.

Local function identifiers

A local function identifier is an identifier with local scope that can hold either a local function, a function object (such as a built-in function or a UDF) or Null. Local function identifiers must be declared along with a parameter declaration. There is a subtle distinction between the terms "local function" and "local function identifier". Basically a local function identifier is a name of a local that can hold a function (whether local or global) and be called using the standard calling syntax, whereas a local function is the actual data value that corresponds to one particular function. You can assign to a local function identifier, therefore changing which function it is holding.

When you declare a local function as shown in the previous section, you also declare a local function identifier. The converse is not true. There are several ways you can declare a local function identifier without declaring a new local function, shown here by example.

  1. Function F1( x,y );
    Initially F1 is Null. Your code will probably assign a function to it later.
  2. Function F2( x,y ) := «expression»;
    • Here «expression» must evaluate to one of: (1) A handle to a built-in function with compatible parameters, (2) A handle to a UDF with compatible parameters, (3) A local function with compatible parameters, (4) Null, or (5) An array of any of these.
    • Notice that := is used to separate the parameters from the expression. This is an assignment and as opposed to a definition which uses a colon.
  3. Function Quadrature( f : Function ( x ) atom ; x1,x2 : atom )
    Here a parameter f of a parent function, Quadrature, is declared to be a local function identifier. The caller might pass a handle to a function object (UDF or built-in function), a local function, or Null.

Since you can assign a local function to a different local function identifier, the name of the original local function and the function you call might differ. For example:

Function F(x) := x^2;
Function G(x) := F;
G(5)

Calling

The syntax for calling a local function identifier is exactly the same as the syntax for calling any other function in Analytica. The identifier is followed by a left parenthesis, the arguments to be passed, and a closing parenthesis. You can use positional or named calling syntax just as with a function.

One slight difference is that a local function identifier might hold an array of functions, or might be Null. When it has an array of functions, it array abstracts, calling each function with the given arguments and collecting the results. When you attempt to call a Null, the result is Null.

You might end up with a local function in the result of a global variable. In that case you cannot call the function using the syntax Va1(x,y) because the global does not have any parameter declaration -- the Analytica parser and compiler doesn't know that it holds a function, nor how many parameters or what parameter types it has. If you want to call a local function that is held in a global variable, you need to use a local function identifier declaration to tell the parser and compiler what the parameter signature is. For example

Function F(x,y : Number) := Va1 Do F(2, 5)

Escaping lexical scope

A local function can escape the lexical scope where it was created. This occurs because the result of an expression can be a local function (remember, it is a value). The simplest example of this is shown here:

Function cosd(x) : Cos(Degrees(x)) Do cosd

Local value capture

A local function's definition can make use of any local identifiers that are in lexical scope at the point where it is defined. In this example, the local function print uses the local count and Y is assumed to be an array of numbers:

Local count:=0;
Function F(x : number atom) : ( count:=count+1; ConsolePrint(f"On call # {count} to F, x is {x}") );
F( Y )

Here the function is able to count how many times it is invoked, and print that to the typescript window.

As mentioned above, a local function can escape its lexical scope, and can even escape the lexical scope of locals that it has captured. This means that it is still using the local variable after the local variable has disappeared from the evaluation stack! We say that the local function has captured the local variable. It can still use its value and even assign to it. In fact, two local functions can capture the same local variable, and hence can share state. This example demonstrates this with a fibonacci generator:

Local a:=0, b:=1;
Function nextFib() : (a,b) := _(b,a+b);
Function resetFib() : (a,b) := _(0,1);
[nextFib,resetFib]

This returns a list of two local functions, nextFib and resetFib, which share a common state. The state (locals a and b) is inaccessible outside of these two functions. Each time you call nextFib(), it returns the next fibonacci number from where it left off last time it was called. When you call resetFib(), it resets the sequence to the beginning. So this

Function fib() := Slice(Va1,1);
For i:=1..10 do fib()

returns [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] the first time it is evaluated. If you evaluated this same thing a second time (without calling resetFib, it would return something different, namely [89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765].

This capability of capture other local values is called closure in programming language lingo. We call it local capture.

Analytica is a functional modeling/programming language, and side-effects are generally frowned up. Overuse of this ability to maintain state can eliminate some of the great properties of Analytica models, such as referential transparency, deterministic calculations, and dependency maintenance. Regarding that last point, you should note that when a call to a local function changes a captured local, there is no invalidation of downstream values. So local capture is a feature that should only be used with great caution. Nevertheless, it does enable an elegant method for implementing stateful calculations when that is required.

Comments


You are not allowed to post comments.