Functions

Functions in LSP are first-class citizen. Thus, you can manipulate functions as any other value. They can be passed to other functions as arguments, assigned to variables, … You can even create functions dynamically with lambda expressions. This property has one main consequence: functions use the same namespace that global or local variables use. There are two kinds of functions:

  • Built-in functions that are provided by the modeler (such as io.println, io.openRead or json.parse for example)

  • User-defined functions. An LSP program is a sequence of user-defined functions. No statement or expression can be written outside of functions.

Function definition

A user function is declared using the function keyword followed by its name. The parameters are given in parenthesis separated by a comma, and its code is encapsulated between braces {}. There is no limitation on the number of parameters. Parentheses are mandatory even when no parameter is given.

function
    : 'function' identifier '(' func_identifier_list ')' block_statement
    | 'function' identifier '(' ')' block_statement
    ;

func_identifier_list
    : identifier
    | func_identifier_list ',' identifier
    ;

The function definition does not execute the function statements. They are executed only when the function is called. Furthermore, and contrary to languages like C or C++, you don’t have to declare your function before using it in your code. Thus, the following code is valid:

function a() {
    // You can write b(), even if b is not known by the parser at this step.
    // But b must be a function when executing function a.
    b();
}

function b() {
    ...
}

Function parameters are defined as local variables. As a consequence, any local variable with the same name as a function argument is masked by the function argument.

Arguments are passed by object reference. It means that you can’t modify the variables used to push the argument of the function in the calling code, but if a mutable object is passed (like a map or a file), the caller will see any change the callee makes to it.

For example:

function f(a) {
    a[2] = 8; // applies to the original map
    a = {2,3,4}; // no impact on the map since it just assigns locally a new map to the name "a"
}

function g() {
    a[0] = 0;
    f(a);
    print(a); // will print [  0 => 0  2 => 8 ]
}

Function body defines a new scope for local variables. In fact, user-defined function is the widest possible scope for a local variable. A local variable cannot exist outside a function.

Note that you cannot declare two functions with the same name in the same module. Nor can you attempt to modify a function by using its name to the right side of an assignment operation. For example, the following code is invalid:

function myFunction(a) {

}

function f() {
    // will throw an error since the name myFunction is already reserved by a function
    myFunction = 12;
}

Return statement

Values are returned by using the optional return statement. Any value of any type can be returned (including functions that are first class citizen). This causes the function to end its execution and pass the control back to the line from which it was called. Values are returned by object reference.

If the code of a function completes its execution without encountering a return statement, the nil value is returned by default. Consequently all functions return a (possibly nil) value. The returned type is not necessarily the same in all branches: for example the following code is valid:

function f(a) {
    if (a) return 2;
    else return "zz";
}

Function call

To invoke a function, simply write the name of the function followed by its arguments. Parameters are evaluated in the order of their declaration. There is no need to declare the function before using it:

f(a, b, c); // invokes f with parameters a, b, c

The number of arguments passed to the function must match the number of arguments used to declare the function. Thus, the following code will throw an exception at runtime:

// This function accepts exactly 3 arguments
function f(a, b, c) {
    ...
}

function g() {
    // This statement will throw an error: only 2 arguments passed to f
    f(12, -12);
}

This constraint does not apply to functions declared as variadic. In that case, the function does not have a fixed number of parameters. For instance the function println allows a variable number of parameters and the following code is valid:

println();
println("abc");
println("abc", "de");
println("abc", "de", 180);

For now, only builtin-functions can be variadic. Variadic functions are particularly useful with the operators of the solver (sum, min, max). To see how to use variadic functions with an unknown number of arguments or how to call variadic functions with arguments that are in a list, see the next section Variadic function call.

Variadic function call

The iteration format introduced in section For statements can be used to specify the parameters of functions. This syntax cannot be used with user-defined functions. It is only allowed for built-in functions.

For instance: println[i in 1...5](i, " ") is equivalent to println(1, " ", 2, " ", 3, " ",4," "). Similarly to the for statement, these iterations can be nested and filtered:

// Display even numbers first, then odd numbers
println[k in 0...2][i in 0...10 : i % 2 == k](i, " ");

This functionality is especially useful for modeling expressions or for arithmetic computations, for instance:

x <- sum[i in 0...10](w[i] * y[i]);  // declare x as a sum of 10 terms
v = min[j in 0...5][k in 0...9](m[j][k]); // retrieve the smallest element of a matrix
v = prod[v in a : v != 0](a[j]);  // compute the product of non-zero elements of an map a

Function manipulations

As mentioned at the begining of this document, functions are first-class citizen and can be manipulated like any other variable or value. They can be passed to other functions as arguments or assigned to variables.

In the example below, we pass a function as an argument to another function that filters the content of a map:

function input() {
    local data = {0, 2, 45, 67, -78, 123, 4223 };
    process(data, filterOdd); // show 45, 67, 123, 4223
    process(data, filterEven); // show 0, 2, -78
}

function process(content, filterFunc) {
    for[val in content] {
        if(filterFunc(val)) {
            println(val);
        }
    }
}

function filterOdd(num) {
    return num % 2 == 1;
}

function filterEven(num) {
    return num % 2 == 0;
}

Lambda and closures

LSP supports anonymous functions and closures. They are called “lambdas” in LSP.

A lambda is an anonymous function (without name) that can close over the environment in which it was defined. This means that it can capture and access variables not in its parameter list.

There are two ways to declare lambdas:

  • Use the function keyword, followed by the parameters surrounded by parentheses and separated by a comma. The code is encapsulated between braces {}. This syntax is a simple transposition of the Function definition syntax described earlier, without the name of the function.

  • Use the arrow syntax. The parameters are specified on the left side of the arrow operator => surrounded by parentheses and separated by a comma. If exactly one parameter is declared, parentheses can be omited. The body of the function takes place on the right side of the arrow operator. The body can be a block statement (surrounded by braces {}) or an expression. In this last case, the expression is also used as return value.

lambda_expression
    : identifier '=>' block_statement
    | '(' function_identifier_list ')' '=>' block_statement
    | '(' ')' '=>' block_statement
    |  identifier '=>' arithm_expression
    | '(' function_identifier_list ')' '=>' arithm_expression
    | '(' ')' '=>' arithm_expression
    | 'function' '(' function_identifier_list ')' block_statement
    | 'function' '(' ')' block_statement
    ;

The example given in Function manipulations, can be simplified like this:

function input() {
    local data = {0, 2, 45, 67, -78, 123, 4223 };

    // Creates an anonymous function with the arrow operator
    process(data, num => num % 2 == 1); // show 45, 67, 123, 4223

    // Creates an anonymous function with the function keyword
    process(data, function(num) {
            return num % 2 == 0;
        }); // show 0, 2, -78
}

function process(content, filterFunc) {
    for[val in content] {
        if(filterFunc(val)) {
            println(val);
        }
    }
}

Lambdas can refer to outer variables. In that case, values of the captured variables are stored for use in the lambda expression even if the variables would otherwise go out of scope and be garbage collected.

In the following example, value of the variable a is captured. Its content is stored and is made accessible to the lambda, even if the variable does not exist anymore when the lambda is called:

function model() {
    local adder8 = createAdder(8);
    adder8(7.5); // Show "Value of a + b: 15.5"
}

function createAdder(a) {
    local func = function(b) {
        // Capture the variable a. Note that when the following lines
        // are executed, variable `a` goes out of scope.
        local result = a + b;
        println("Value of a + b: ", result);
    };
    return func;
}

Limitations

  1. A lambda cannot alter the control flow of its enclosing function. Thus, a return statement in a lambda expression does not cause the enclosing function to return. The same behavior applies for break and continue statements:

    function model() {
        for[i in 0...10] {
            // The following declaration will throw an error.
            // A lambda cannot alter the control flow of its enclosing function.
            local func = () => { if(i % 2 == 0) break; };
            func();
        }
    }
    
  2. Local variables are captured by value, not by reference. In the following example, the result is 10, not foobar as the variable is captured by value when the lambda is created:

    function model() {
        local i = 10;
        local func = () => { println("Value of i: ", i); }
        i = "foobar";
        func(); // Show "Value of i: 10"
    }
    
  3. Captured variables that are local variables of their enclosing scope cannot be modified inside the lambda. Thus, the following example will throw an error:

        function createAdder(a) {
            local func = function(b) {
                a+= 2; // Will throw an error. `a` is a local variable of `createAdder`
                       // that can't be modified in a lambda.
                local result = a + b;
                println("Value of a + b: ", result);
            };
            return func;
        }
    
    This limitation does not apply to global variables.
    

The this variable

The this variable is treated differently depending on whether the lambda is declared using the function keyword or the arrow syntax:

  • With arrow syntax: this refers to the variable of the enclosing function. The this variable is therefore captured and its value will correspond to the object from which the lambda was created only. Its value will be fixed.

  • With the function keyword: this keeps its usual behavior and its value corresponds to the object on which the lambda will be called.

The difference can easily be seen in the following example:

function model() {
    local lFunc = functionMakeMe("a pudding");
    local lArrow = arrowMakeMe("a pudding");
    local obj = { lambdaFunc: lFunc, lambdaArrow: lArrow };

    // Displays: { ... }, please make me a pudding
    obj.lambdaFunc();
    // Displays: nil, please make me a pudding
    obj.lambdaArrow();

}

function functionMakeMe(order) {
    return function() {
        println(this, ", please make me ", order);
    };
}

function arrowMakeMe(order) {
    return () => {
        println(this, ", please make me ", order);
    };
}