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.
| Form | Written | Where | Called from |
|---|---|---|---|
| Global function | global name(...) | between setup and main | global.name(...), or a bare name in the same file |
| File-level function | func name(...) | between setup and main | a bare name in the same file; global.module.name(...) from an importer |
| Local function | local name(...) | inside another function body | a bare name, or local.name(...) |
| Named lambda | lambda name(...) | inside a function body | its 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
}
- A global function is callable by its bare name in its own file and by
global.name(...)everywhere, including from an importer asglobal.module.name(...). global setup()andglobal main()are global functions with extra rules:setupmust be the first declaration,mainthe last, and either may take defaulted parameters. See language andoverrideMainin language.- Entry-point functions may not declare codeblock parameters.
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
}
funcnames are file-scoped: two imported files may each define adoublehelper without colliding.- In the defining file, call it by bare name. From an importer, call it through the module:
global.moduleName.double(21)(see modules). funcdeclarations must be top-level and may not followglobal main()(declarations may not follow global main()).
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
}
- A local function is not exported and cannot be called from another function.
- It may close over the top-level variables that are in scope where it runs.
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
- Parameters may be typed (
int n) or untyped (treated asany). - A parameter may have a default (
int b = 1). - A required parameter may not follow one with a default:
required parameters cannot follow a parameter with a default value. type T of valueintroduces a type parameter namedT, inferred from the argumentvalue. UseTto require matching argument types and to type the return value:
```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.
- Class methods are the exception: their parameters are typed but may not have defaults (see classes).
Return annotations
Only the arrow form is accepted:
global f(int x) -> int { return x + 1; }
: intis not valid syntax (unexpected character ':').- Omit the annotation, or use
-> any, to leave the result unchecked. -> noneis accepted: the function must return nothing, and a returned value fails withvalue cannot be assigned to type 'none'.- A mismatched return is a source-located runtime error.
- Named types (a
struct,class, orenum) may be used as the annotation.
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
}
- The codeblock-parameter syntax is identical for
global,funcandlocaldeclarations; the parser treats all three the same way. - Caller-supplied blocks also work on file-level
funcdeclarations; themacrosexample fixture covers both macro-spliced and directfunccalls. - Callers may mix inline blocks (
(){ ... }) and named references ((){{var}}) in the same call. funcandlocalblocks are supplied the same way; afuncreached through an import takes blocks at the call site too:global.module.runTwice(){...}{...}.- Entry-point functions (
setup/main) may not declare codeblock parameters. - A codeblock name must be unique across the source file and may not repeat a regular parameter name of the same function.
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
- language — functions in the context of program structure.
- codeblocks — stored and caller-supplied codeblocks.
- async — local
asyncsub-functions andawait. - classes —
localmethods on a class. - modules — calling exported
globalandfuncnames across files.