Lynxer

Documentation

Functions

Lynxer has three function declaration forms plus scoped named lambdas. They differ in where they are written and how they are called. Parameters, return annotations and caller-supplied codeblocks work the same for all four.

FormWrittenWhereCalled from
Global functionglobal name(...)between setup and mainglobal.name(...), or a bare name in the same file
File-level functionfunc name(...)between setup and maina bare name in the same file; global.module.name(...) from an importer
Local functionlocal name(...)inside another function bodya bare name, or local.name(...)
Named lambdalambda name(...)inside a function bodyits name in the declaring scope

global func name(...) is not valid syntax (unexpected character ' '); use func name(...) for a file-level function.

Global functions

global is the default, exported declaration. Declare it top-level, between global setup() and global main():

global setup(){}

global double(int value) -> int {
    return value * 2;
}

global main(){
    println(double(21));        // 42, bare name in the same file
    println(global.double(21)); // 42, fully qualified
}

File-level functions (func)

A func belongs to its source file. Declare it top-level, between global setup() and global main():

global setup(){}

func double(int value) -> int {
    return value * 2;
}

global main(){
    println(double(21));   // 42
}

Local functions

A local function is declared inside another function body and is not visible outside it:

global setup(){}

global main(){
    local square(int n) -> int {
        return n * n;
    }
    println(square(4));        // 16, bare name
    println(local.square(5));  // 25, qualified
}

Named lambdas

A named lambda is declared inside a function and called by its name in that scope. It accepts typed parameters and an optional -> type return annotation:

global setup(){}

global main() {
    str greeting = "Hello";

    lambda greet(str name) -> str {
        return inter"{greeting}, {name}!\n";
    }

    println(greet("Andy")); // Hello, Andy!

    global nested() {
        println(greet("Andy")); // out-of-scope error if nested() is called
    }
}

Lambdas are scoped declarations, not global or exported functions. A lambda is available in the function scope where it is declared; nested function scopes cannot see its name. It can read variables from its enclosing function while that scope is active. Control-flow blocks do not create nested scopes in Lynxer; see language. Lambdas support the same typed and inferred generic parameters as other ordinary functions.

Function values

Named functions, local functions, and lambdas can be used as values. Use the function type for a checked declaration, or any when the value may also be something else:

global setup(){}

global addOne(int value) -> int {
    return value + 1;
}

global apply(function operation, int value) -> int {
    return operation(value);
}

global makeAdder(int base) -> function {
    local add(int value) -> int {
        return base + value;
    }
    return add;
}

global main() {
    function increment = global.addOne;
    println(apply(increment, 4)); // 5

    function addTen = makeAdder(10);
    println(addTen(3)); // 13
}

function values can be passed as arguments, returned, assigned, and called through variables with the same argument and return checks as a direct call. Local functions and lambdas retain their enclosing variable scopes, so a returned function can still read and update captured variables. Their names remain subject to the normal scope rules; in particular, a nested function cannot look up a named lambda declared by its caller. A function value passed explicitly as an argument remains available to the receiving function.

Parameters and defaults

```lynx global setup(){}

global keep(type T of first, T second) -> T { return second; }

global main() { int number = keep(1, 2); str word = keep("one", "two"); println(number); // 2 println(word); // two } ```

T can also be used for local declarations. Each call infers its own type; within one call, arguments using the same T must have the same Lynxer type. The generic parameter itself cannot have a default value; exported C functions and class methods do not support inferred generic parameters.

Return annotations

Only the arrow form is accepted:

global f(int x) -> int { return x + 1; }

Caller-supplied codeblocks

Any of the three forms may declare caller-supplied blocks after its parameter list and before its body. Write each block name in its own {...}:

<kind> name(<params>){<blockName>}{<blockName>}... { <body> }

The body invokes a block with exec(){{<blockName>}}. A caller supplies a block with (){ ... } or, for a named codeblock variable, (){{var}}:

global setup(){}

global runTwice(){first}{second}{
    exec(){{first}}
    exec(){{second}}
}

func mapTwice(int value){apply}{
    exec(value){{apply}}
    exec(value){{apply}}
}

global main(){
    runTwice(){
        println("one");
    }{
        println("two");
    }

    codeblock triple = {
        println(value * 3);
    };
    mapTwice(4){{triple}}   // 12 printed twice
}

See codeblocks for the full reference, including inferred parameters and the codeblock type.

Scoping

There are no nested block scopes; see language. A call to any of these forms gets a fresh scope for its parameters and locals. Those do not leak out, but the function can read and update the top-level variables it closes over.

See also