Лямбды

Лямбды в Ü - это безымянные функциональные объекты, объявляемые в контексте выражения. Они могут быть использованы как функции (вызваны).

Объявляется лямбда с использованием ключевого слова lambda, после которого следует описание параметров/возвращаемого значения и других атрибутов, свойственных функциям. Далее следует тело лямбды. Не существует каких-либо ограничений на содержимое тела лямбды - разрешено всё то, что и в обычных функциях.

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

Результат выражения лямбды - это значение типа лямбды. Тип этот - некий генерируемый компилятором класс, уникальный для каждой лямбды. Класс этот имеет перегруженный оператор вызова (), который создаётся из указанного тела лямбды.

Захват переменных

Одной из важных особенностей лямбд в Ü является возможность использования локальных переменных и аргументов функций из окружающего контекста. Чтобы их использовать, следует написать [=] после ключевого слова lambda.

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

При использовании [=] компилятор обнаружит все использования внешних переменных. При создании объекта лямбды все эти переменные будут скопированы в объект лямбды. По сути захваченные переменные становятся скрытыми полями класса лямбды.

Поскольку при использовании [=] переменные копируются в лямбду, после создания объекта лямбды они могут быть изменены или даже разрушены, что никак не повлияет на объект лямбды.

var i32 mut scale= 11;
auto f= lambda[=]( i32 x ) : i32 { return x * scale; };
scale= 0; // Меняем исходную переменную
halt if( f( 5 ) != 55 ); // Лямбда все ещё содержит копию старого значения переменной

Захват ссылок

Также можно объявить лямбду, которая захватывает ссылки на переменные, вместо их копирования. Изменяемость этих ссылок определяется изменяемостью исходных переменных. Захваченные по ссылке переменные становятся ссылочными полями в классе лямбды.

var f32 mut x= 0.0f;
{
    auto f= lambda[&]() { x += 1.0f; }; // Переменная "x" захвачена по изменяемой ссылке и будет изменена в вызове лямбды.
    f();
    f();
}
halt if( x != 2.0f ); // Переменная "x" должна приобрести новое значение после вызовов лямбды.

Для лямбд, захватывающих ссылки, действуют все те же правила контроля ссылок, что и для других типов со ссылками внутри.

Список захвата

Существует возможность явно указать переменные, которые захватывает лямбда - через список захвата внутри []. В этом списке через запятую перечисляются переменные, которые необходимо захватить. Если перед именем переменной стоит & - она будет захвачена по ссылке, иначе - по значению.

fn Foo()
{
    var f32 mut x= 1.0f, mut y= 1.0f;
    {
        // Захватываем ссылку на первую переменную и делаем копию второй.
        auto mut f=
            lambda[&x, y] mut () : f32
            {
                x*= 2.0f; // Изменяем внешнюю переменную.
                y*= 3.0f; // Изменяем захваченную копию.
                return x * y;
            };
        halt if( f() != 6.0f );
        halt if( f() != 36.0f );
        halt if( f() != 216.0f );
    }
    halt if( x != 8.0f ); // Захваченная по ссылке переменная должна была быть изменена.
    halt if( y != 1.0f ); // Захваченная по копии переменная НЕ должна была быть изменена.
}

Имеет смысл использовать список захвата, когда нужно явно обозначить захваченные переменные. Также он полезен в тех случаях, когда какие-то переменные надо захватить по ссылке, а какие-то по значению. При использовании списка захвата будет порождена ошибка, если указанная переменная не используется в лямбде, или если в лямбде используется не указанная в этом списке переменная.

Явные выражения в списке захвата

Также существует возможность для имени в списке захвата указать инициализатор-выражение. В таком случае будет захвачен именно результат выражения, а не какая-либо переменная по имени.

fn Foo()
{
    // Создаётся захваченная переменная с именем "x".
    auto f=
        lambda [ x= 42 ] () : i32
        {
            return x;
        };
}

Можно таким образом захватывать и ссылки:

struct S
{
    i32 x;
    fn Foo( this )
    {
        // Захватываем в лямбде ссылку на "this".
        auto f=
            lambda[ &self= this ]() : i32
            {
                return self.x * 3;
            };
    }
}

Особенно полезна такая особенность при необходимости захвата некопируемых переменных:

class C {} // Классы некопируемы по умолчанию.
fn Foo( C mut c )
{
    // Захватываем переменную, перемещая её.
    auto f=
        lambda [ c= move(c) ] ()
        {
            auto& c_ref= c;
        };
}

Изменяемость объекта лямбды

По умолчанию оператор () лямбды принимает this лямбды как неизменяемую ссылку. Но это поведение можно изменить, обозначив лямбду как mut. mut лямбда позволяет менять захваченные по значению переменные, при этом даже если исходная переменная была неизменяемой. Но вызвать такую лямбду можно только если объект лямбы изменяем.

fn Foo()
{
    auto x= 0;
    auto mut f=
        lambda [=] mut () : i32
        {
            ++x; // Меняем захваченную копию внешней переменной.
            return x;
        };
    // Лямбда производит разные результаты, ибо меняет своё состояние в каждом вызове.
    halt if( f() != 1 );
    halt if( f() != 2 );
    halt if( f() != 3 );
    halt if( x != 0 ); // Исходная переменная не должна измениться.
}

mut имеет смысл применять только для лямбд, захватывающих значения. Для лямбд без захвата или лямбд, захватывающих только ссылки, обозначать лямбду как mut бессмысленно. Можно также обозначить лямбу явно imut, что совпадает с умолчательной изменяемостью.

byval лямбды

Метод () лямбд может принимать объект лямбды по значению, если лямбда объявлена с модификатором byval. Это работает аналогично любым другим byval методам.

fn Foo()
{
    auto f=
        lambda[ x= 33 ] byval mut () : i32
        {
            x*= 2; // Изменяем захваченное значение, но это изменение не изменит исходный объект лямбды, т. к. изменится его текущая локальная копия.
            return x;
        };
    // Здесь при вызове объект лямбы копируется.
    halt if( f() != 66 );
    halt if( f() != 66 );
}

byval лямбды с захваченными некопируемыми переменными можно вызывать, только переместив объект лямбды.

class C
{
    i32 x;
    fn constructor( i32 in_x ) ( x= in_x ) {}
}
static_assert( !typeinfo</C/>.is_copy_constructible ); // Классы по умолчанию не копируемы.
fn Foo()
{
    var C mut c( 142 );
    // byval лямбда с захваченным некопируемым значением.
    auto mut f=
        lambda[ c= move(c) ] byval () : i32
        {
            return c.x;
        };
    // Вызвать эту лямбду можно только один раз.
    halt if( move(f)() != 142 );
}

В byval mut лямбдах также разрешено перемещать захваченные по значению переменные, если это необходимо.

Детали функционирования лямбд

Класс лямбды является сгенерированным и не имеет доступного программисту имени. При этом это не сильно мешает работе с лямбдами. Шаблонный код с ними работает, как с любыми другими типами. Для объявления локальной переменной лямбды можно использовать auto. Также возможно использование typeof.

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

Лямбды с захватом это просто классы с полями, соответствующими захваченным переменным. Для захваченных по значению переменных должным образом вызываются деструкторы. В зависимости от состава полей генерируются или нет конструктор копирования и оператор копирующего присваивания. non_sync тэг для лямбд выводится на основании их полей.

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;

Тип лямбды является constexpr, если все поля лямбды являются constexpr типами. При этом отдельно существует constexpr свойство для оператора () - оно выводится по тем же правилам, что и для шаблонных функций. Из этого следует, что можно объявить объект-лямбду как constexpr, но вызвать как constexpr его будет нельзя, если оператор () не является constexpr.

// Объект лямбды является "constexpr".
auto constexpr f= lambda() { unsafe{} };
// А вот вызов этой лямбды уже не может быть "constexpr", т. к. лямбда содержит "unsafe" блок внутри.
f();

Для классов лямбд создаются внутренние ссылочные теги. Под каждую захваченную по ссылке переменную создаётся свой тег. Также создаются уникальные теги под каждый внутренний тег захваченных по значению переменных.

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

this оператора () в лямбдах не доступен.

auto f=
    lambda()
    {
        auto& this_ref= this; // Ошибка - "this" не доступен.
    };

Лямбды не могут захватывать this внутри методов с this параметром. Не могут быть захвачены и отдельные поля структур/классов. Но можно создать локальные копии/ссылки для this или его частей, и уже их захватить в лямбду.

struct S
{
    i32 x;
    fn Foo( this )
    {
        auto& x_ref= x; // Создаём локальную ссылку на поле структуры.
        auto f=
            lambda[&]() : i32
            {
                return x_ref; // Захватываем локальную ссылку.
            };
        f();
    }
}

Из лямбды, вложенной в другую лямбду, не работает захват переменных внешних по отношению ко внешней лямбде. Но можно внутри внешней лямбды создать ссылку/копию для такой переменной и захватить уже её из внутренней лямбды.

auto x= 123;
auto f0=
    lambda[=]() : i32
    {
        auto x_copy= x; // Захватываем внешнюю по отношению к "f0" переменную.
        auto f1=
            lambda[=]() : i32
            {
                return x_copy; // Захватываем внешнюю по отношению к "f1" переменную.
            };
        return f1();
    };

Важное свойство лямбд, отличающее их от других функций - автовывод ссылочной нотации. Поэтому вручную её указывать не надо.

// Автоматически будет вычислено, что лямбда возвращает ссылку на параметр #0.
auto f= lambda( i32& x ) : i32& { return x; };