Lambdas

Lambdas in Ü are nameless functional objects that are defined in expression context. They may be used as functions (may be called).

A lambda is declared with usage of the lambda keyword, after that follow parameters/return value and other function attributes. Further follows a lambda body. There are no any lambda body limitations - all constructions as in normal functions are allowed.

auto f= lambda( i32 x ) : i32 { return x / 3; };

A lambda expression result is a value of a lambda type. This type is a generated by the compiler class, which is unique for each lambda. This class has overloaded () operator, which is created from the specified lambda body.

Variables capture

One of the important lambda features in Ü is the possibility to capture local variables and function arguments from the surrounding context. [=] after the lambda keyword should be specified in order to do this.

var i32 scale= 11;
auto f= lambda[=]( i32 x ) : i32 { return x * scale; };

If [=] is used, the compiler will find all usages of external variables. When a lambda object is constructed, all these variables are copied into it. Such captured variables become effectively hidden lambda class fields.

Since with usage of [=] variables are copied into the lambda, they may be modified or even destroyed, but this will not affect the lambda object.

var i32 mut scale= 11;
auto f= lambda[=]( i32 x ) : i32 { return x * scale; };
scale= 0; // Change the source variable
halt if( f( 5 ) != 55 ); // Lambda still contains a copy of the old variable value

References capture

It’s also possible to define a lambda that captures references to variables instead of making copies. Mutability of these references is determined by the mutability of source variables. Captured by reference variables become reference fields in the lambda class.

var f32 mut x= 0.0f;
{
    auto f= lambda[&]() { x += 1.0f; }; // "x" variable is captured by mutable reference and will be modified in lambda call.
    f();
    f();
}
halt if( x != 2.0f ); // "x" variable should have new value after lambda calls.

All reference checking rules work for lambdas that capture references, as for any other types with references inside.

Capture list

It’s possible to explicitly specify captured by a lambda variables - via capture list inside []. This list contains comma-separated variable names, which needed to be captured. If a variable in this list has prefix & - it will be captured by reference, else - by value.

fn Foo()
{
    var f32 mut x= 1.0f, mut y= 1.0f;
    {
        // Capture the first variable by mutable reference, the second one by copy.
        auto mut f=
            lambda[&x, y] mut () : f32
            {
                x*= 2.0f; // Affect external variable.
                y*= 3.0f; // Modify captured copy.
                return x * y;
            };
        halt if( f() != 6.0f );
        halt if( f() != 36.0f );
        halt if( f() != 216.0f );
    }
    halt if( x != 8.0f ); // Should be changed.
    halt if( y != 1.0f ); // Should not be changed.
}

It has sense to use capture list where it is necessary to explicitly designate captured variables. It’s also useful in cases where some variables are needed to be captured by reference and other by value. With usage of capture list an error will be produced, if a listed variable is not used inside the lambda, or if an external variable which is not listed is used inside the lambda.

Explicit initializers in capture list

There is also possibility to specify initializer expression for a name in a capture list. If such expression exists, its result will be captured, instead of capturing some named variable.

fn Foo()
{
    // Create captured variable with name "x".
    auto f=
        lambda [ x= 42 ] () : i32
        {
            return x;
        };
}

It’s possible to explicitly capture references:

struct S
{
    i32 x;
    fn Foo( this )
    {
        // Capture "this" reference.
        auto f=
            lambda[ &self= this ]() : i32
            {
                return self.x * 3;
            };
    }
}

Especially this feature is useful for capturing of values of non-copyable types:

class C {} // Classes are non-copyable by default.
fn Foo( C mut c )
{
    // Capture a variable by moving it.
    auto f=
        lambda [ c= move(c) ] ()
        {
            auto& c_ref= c;
        };
}

Lambda object mutability

By default () operator of lambdas has immutable this parameter. But it may be changed by marking a lambda with mut. It’s allowed for a mut lambda to change captured by value variables, even if the source variable isn’t mutable. But such lambdas may be called only if a lambda object is mutable.

fn Foo()
{
    auto x= 0;
    auto mut f=
        lambda [=] mut () : i32
        {
            ++x; // Change captured copy of an external variable.
            return x;
        };
    // Lambda produces different results, because it changes its internal state on each call.
    halt if( f() != 1 );
    halt if( f() != 2 );
    halt if( f() != 3 );
    halt if( x != 0 ); // The source variable should not be changed.
}

mut is useful for lambdas with captured values. For lambdas with no captures or lambdas which capture only references it’s pointless to use mut. It’s also possible to mark a lambda with imut, which is the same as the default mutability.

byval lambdas

() method of lambdas may accept the lambda object by value, if the lambda is declared with byval modifier. This works exactly like with any other byval method.

fn Foo()
{
    auto f=
        lambda[ x= 33 ] byval mut () : i32
        {
            x*= 2; // Change the captured value, but this change doesn't affect the source lambda object, because its local copy is modified instead.
            return x;
        };
    // Here the lambda object is copied in each call.
    halt if( f() != 66 );
    halt if( f() != 66 );
}

byval lambdas with captured non-copyable variables may be called only by moving the lambda object.

class C
{
    i32 x;
    fn constructor( i32 in_x ) ( x= in_x ) {}
}
static_assert( !typeinfo</C/>.is_copy_constructible ); // Classes are non-copyable by default.
fn Foo()
{
    var C mut c( 142 );
    // "byval" lambda with a captured non-copyable value.
    auto mut f=
        lambda[ c= move(c) ] byval () : i32
        {
            return c.x;
        };
    // This lambda may be called only once.
    halt if( move(f)() != 142 );
}

It’s possible (if it is necessary) to move captured by value variables in byval mut lambdas.

Lambdas functionality details

A lambda class is generated and has no accessible by the programmer name. But this doesn’t prevent any usage of lambdas. Templates work with lambdas fine - as with any other types. For a lambda variable declaration auto may be used. Also it’s possible to use typeof.

auto f= lambda(){};
var typeof(f) f_copy= f;

Lambdas with captures are just classes with fields that correspond to captured variables. For captured by value variables destructors are called properly. Copy constructor and copy assignment operator may or may not be generated depending on captured variable types. non_sync tag for lambdas is calculated based on captured variable types.

var i32 x= 0, y= 0;
auto f= lambda[=]() : i32 { return x + y; };
static_assert( typeinfo</ typeof(f) />.size_of == typeinfo</i32/>.size_of * 2s );
auto f_copy= f;

A lambda type is constexpr if all lambda fields are of constexpr types. Additionally there is constexpr property for lambda () operator - it is calculated exactly like for template functions. Because of that it’s possible to define a lambda object as constexpr, but it may not be possible to call it in constexpr context, if () operator is not constexpr.

// Lambda object is "constexpr".
auto constexpr f= lambda() { unsafe{} };
// But a call of this lambda can't be "constexpr", because the lambda body contains "unsafe" block inside.
f();

Inner reference tags are created for lambda classes. For each captured by reference variable its own reference tag is created. Also unique reference tags are created for each inner reference tag of a captured by value variable.

auto x= 0;
auto f= lambda[&]() : i32 { return x; };
static_assert( typeinfo</ typeof(f) />.reference_tag_count == 1s );

this itself of the () operator in lambdas is unavailable.

auto f=
    lambda()
    {
        auto& this_ref= this; // Error - "this" is unavailable.
    };

Lambdas can’t capture this in methods with this parameter. Fields of structs and classes can’t be captured directly either. But it’s possible to create local variables/references for this or its parts and capture them.

struct S
{
    i32 x;
    fn Foo( this )
    {
        auto& x_ref= x; // Create a local reference for the struct field.
        auto f=
            lambda[&]() : i32
            {
                return x_ref; // Capture a local reference.
            };
        f();
    }
}

From a lambda that is inside another lambda it’s not possible to capture a variable that is external relative to the outer lambda. But it’s possible to create a reference/copy for such variable in the outer lambda and capture it in the inner lambda.

auto x= 123;
auto f0=
    lambda[=]() : i32
    {
        auto x_copy= x; // Capture an external relative to "f0" variable.
        auto f1=
            lambda[=]() : i32
            {
                return x_copy; // Capture an external relative to "f1" variable.
            };
        return f1();
    };

The important lambdas property that is different from other functions is the auto reference notation calculation. Thus it’s unnecessary to specify reference notation for lambdas manually.

// It will be calculated that this lambda returns a reference to parameter #0.
auto f= lambda( i32& x ) : i32& { return x; };