In Argentum lambdas are special functions:
- anonymous
- having access to the outer lexical scopes
- extremely lightweight (no allocations)
Syntax
Lambda type declarations:
If functions, methods or delegates take lambdas as parameters, these lambda types need to be declared:
fn forRange(from int, to int, handler(int)) ....
// The third parameter of the `forRange` function named `handler` is of type
// lambda taking one int parameter and returning nothing
fn binarySearch(from int, to int, comparer(int, int)int) int...
// The third parameter of the `binarySearch` function named `comparer` is of type
// lambda taking two int parameters and returning int.
When creating lambdas, their parameters and return types are inferred from the first call or the first assignment to the already typed lambda, so they never declared explicitly.
Lambda instance definition:
Lambda syntax is extremely variative in Argentum. They can reside in places of general expressions:
// Lambda with explicit parameters and a single expression body:
a = `x `y x + y;
// Lambda with no parameters and a single expression body:
a = \1 + function().method(2)["key"];
// Lambda with an implicit `_` parameter:
a = \expression(_);
// Lambda having explicit parameters and complex body:
a = `x `y {
multiple;
expressions
};
// Lambda having no parameters or one implicit parameter and a complex body:
a= \{
multiple;
expressions;
possibly using _
};
In the last example the `\`
is mandatory, it distinguishes lambdas from { blocks }
that executes immediately.
So rule of thumb is: when lambda is a standalone expression, it should start either with backtick `
or backslash \
.
Lambdas as last (only) function parameters:
Lambdas often passed to the functions/methods/lambdas as parameters, in most cases they are passed as last parameter to make the callee to look and feel as a control structure. For these occasions Argentum has somewhat simplified syntax. After any call expression there might go a lambda outside the call expression parenthesis, and these lambda declaration allow the curly braces form {}
without prepending \
:
forRange(0, 100) `i log("{i}");
x = array.findFirst{_<0};
Mandatory lambdas:
There are operators that require their right-hand-side operands to always be lambdas:
- operators: if
?
, and&&
, pipe->
, init.{}
- expect single-parameter lambda as RHS operand - operators: else
:
, or||
- expect lambda with no parameters.
In these places there might be used all lambda syntax mentioned above plus simple expression possibly using `_` name:
a = focused ? _.text; // _.text is a lambda.
a = focused ?`f f.text; // the same but using the explicit `f` parameter
log(a : "none") // "none" is a lambda with no parameters
evaluateHeight(widget) -> `height {
log(height);
use(height);
}
Lambda as a factory:
Sometimes functions want to take from caller some newly created objects (@Pointers) to put them somewhere in fields of other objects, but in Argentum @Pointers cannot be passed to function as parameters (otherwise it would allow to create loops in hierarchies of ownership). Instead the caller passes to callee a lambda, that creates a new instance on demand and returns a @Pointer. The high-level idea behind that is: caller does not create object in advance but instead gives callee a promise to create an object if needed (and as much objects as needed).
This factory lambda declared as such:
// Here `producer` is a lambda with no parameters returning newly created Item
fn placeItemInGrid(int x, int y, producer()@Item) {
...
someObj.field := producer(); // here we call lambda to get a new Item instance
...
}
When calling this function we can instead of lambda pass a generic expression returning @Item, and it will be converted to lambda automatically:
placeItemInGrid(42, 11, InteractiveItem.init("Hi").setColor(red));
// The third parameter is an expression, it will be automatically converted to lambda.
Implicit parameter binding
In case of multiple nested lambdas, the inner lambda has access to all names of outer scopes:
focused = &PageElement; // weak poiner to a TextElement
...
a = focused &&`f
f~TextBlock ?`t {
// here we can use:
// `t` of type `TextBlock`
// `f` of type `PageElement`
// `focused` of type `&PageElement`
t.caption;
};
// now a is of type ?String, if TextBlock.caption returns String
The above example can be rewritten using the implicit parameter names:
a = focused && _~TextBlock ? _.caption;
// in this example
// - the first `_` name is `f` from the previous example,
// - and the second `_` corresponds to `t`.
// They have different type, and the second `_` name hides the first one.
This rule has two exceptions:
- If an inner nested lambda uses a
`_`
-name, but at the caller site it turns out that this lambda is a no-parameter one, this inner`_`
-name is considered the name of the outer lambda:
fn useString(s String, onError())... // onError has no parameters
maybeAcquiureString() ?
useString(_) {
log("Error with {_}") // <- this `_` is a string from the ?-operator
}
- if the lambda is an operand of
`?`
or`&&`
operator, and its parameter isvoid
, then the`_`
-name also binds to the outer scope, b/c it makes no sense to have variable of typevoid
.
readLine() -> // readLine returns String,
_ != "" // operator != returns bool aka ?void,
? // so operator ? passes void to its right operand
use(_); // this _ binds not to the inner `void` but to the outer `String`
These two rules make implicit `_` parameter useful in many more situations.
Lambdas internals
Internally lambda is just a pair of pointers:
- one points to the CPU stack frame in which lambda is declared,
- other is a function entry point.
Thus lambdas are extremely lightweight constructions cheap-n-easy to create, pass and call.
In many cases lambdas get transformed into simple blocks of code i.e. they have zero overhead.
Lambdas limitations
Since lambda point to the stack frame, it cannot be returned out of the function/method/lambda and stored in object fields. If you need this functionality, you can use delegates and capable objects.