# Lambdas
# Table of contents
- Syntax Overview
- Positional parameters
- Captures
- Function fields in lambdas
- Copy semantics
- Self and recursion
- Alternatives considered
# Syntax Overview
One goal of Carbon's lambda syntax is to have continuity between lambdas and function declarations. Below are some example declarations:
Implicit return types:
// In a variable:
let lambda: auto = fn => T.Make();
// Equivalent in C++23:
// const auto lambda = [] { return T::Make(); };
// As an argument to a function call:
Foo(10, 20, fn => T.Make());
// Equivalent in C++23:
// Foo(10, 20, [] { return T::Make(); });
Explicit return types:
// In a variable:
let lambda: auto = fn -> T { return T.Make(); };
// Equivalent in C++23:
// const auto lambda = [] -> T { return T::Make(); };
// As an argument to a function call:
PushBack(my_list, fn -> T { return T.Make() });
// Equivalent in C++23:
// PushBack(my_list, [] { return T::Make(); });
# Return type
There are three options for how a lambda expresses its return type, parallel to how function declarations express returns: using a return expression, using an explicit return type, or having no return.
# Return expression
A return expression is introduced with a double arrow (=>) followed by an
expression describing the function's return value. In this case, the return type
is determined by the type of the expression, as if the return type was auto.
// In a variable:
let lambda: auto = fn => T.Make();
// Equivalent in C++23:
// const auto lambda = [] { return T::Make(); };
// As an argument to a function call:
Foo(fn => T.Make());
// Equivalent in C++23:
// Foo([] { return T::Make(); });
# Explicit return type
An explicit return type is introduced with a single arrow (->), followed by
the return type, and finally the body of the lambda with a sequence of
statements enclosed in curly braces ({...}).
// In a variable:
let lambda: auto = fn -> T { return T.Make(); };
// Equivalent in C++23:
// const auto lambda = [] -> T { return T::Make(); };
// As an argument to a function call:
Foo(fn -> T { return T.Make(); });
// Equivalent in C++23:
// Foo([] -> T { return T::Make(); });
# No return
Lambdas that don't return anything end with a body of statements in curly braces
({...}).
// In a variable:
let lambda: auto = fn { Print(T.Make()); };
// Equivalent in C++23:
// const auto lambda = [] -> void { Print(T::Make()); };
// As an argument to a function call:
Foo(fn { Print(T.Make()); });
// Equivalent in C++23:
// Foo([] -> void { Print(T::Make()); });
# Implicit parameters in square brackets
Lambdas support captures, fields and deduced parameters in the square brackets.
fn Foo(x: i32) {
// In a variable:
let lambda: auto = fn [var x, var y: i32 = 0] { Print(++x, ++y); };
// Equivalent in C++23:
// const auto lambda = [x, y = int32_t{0}] mutable -> void { Print(++x, ++y); };
// As an argument to a function call:
Foo(fn [var x, var y: i32 = 0] { Print(++x, ++y); });
// Equivalent in C++23:
// Foo([x, y = int32_t{0}] mutable -> void { Print(++x, ++y); });
}
# Parameters
Lambdas also support so-called "positional parameters"
that are defined at their point of use using a dollar sign and a non-negative
integer. They are implicitly of type auto.
fn Foo() {
let lambda: auto = fn { Print($0); };
// Equivalent in C++23:
// auto lambda = [](auto _0, auto...) -> void { Print(_0); };
// Equivalent in Swift:
// let lambda = { Print($0) };
}
Of course, lambdas can also have named parameters, but a single lambda can't have both named and positional parameters.
fn Foo() {
// In a variable:
let lambda: auto = fn (v: auto) { Print(v); };
// Equivalent in C++23:
// const auto lambda = [](v: auto) -> void { Print(v); };
// As an argument to a function call:
Foo(fn (v: auto) { Print(v); });
// Equivalent in C++23:
// Foo([](v: auto) { Print(v); });
}
And in additional the option between positional and named parameters, deduced parameters are always permitted.
fn Foo() {
let lambda: auto = fn [T:! Printable](t: T) { Print(t); };
}
# Syntax defined
Lambda expressions have one of the following syntactic forms (where items in square brackets are optional and independent):
fn[implicit-parameters] [tuple-pattern] => expression
fn [implicit-parameters] [tuple-pattern] [-> return-type] {
statements }
The first form is a shorthand for the second: "=> expression" is equivalent
to "-> auto { return expression ; }".
implicit-parameters consists of square brackets enclosing a optional default
capture mode and any number of explicit captures, function fields, and deduced
parameters, all separated by commas. The default capture mode (if any) must come
first; the other items can appear in any order. If implicit-parameters is
omitted, it is equivalent to [].
Function definitions are distinguished from lambdas by the presence of a name
after the fn keyword.
The presence of tuple-pattern determines whether the function body uses named or positional parameters.
The presence of "-> return-type" determines whether the function body can
(and must) return a value.
To understand how the syntax between lambdas and function declarations is reasonably "continuous", refer to this table of syntactic positions and the following code examples.
// Lambdas (all the following are in an expression context and are
// themselves expressions)
fn => A1
fn [B, C] => A1
fn (D) => A2
fn [B, C](D) => A2
fn { E1; }
fn -> F { E2; }
fn [B, C] { E1; }
fn [B, C] -> F { E2; }
fn (D) { E3; }
fn (D) -> F { E4; }
fn [B, C](D) { E3; }
fn [B, C](D) -> F { E4; }
# Positional parameters
Positional parameters, denoted by a dollar sign followed by a non-negative integer (for example, $3), are auto-typed parameters defined within the lambda's body.
let lambda: auto = fn => $0
They can be used in any lambda declaration that lacks an explicit parameter list
(parentheses). They are variadic by design, meaning an unbounded number of
arguments can be passed to any function that lacks an explicit parameter list.
Only the parameters that are named in the body will be read from, meaning the
highest named parameter denotes the minimum number of arguments required by the
function. The lambda body is free to omit lower-numbered parameters (ex:
fn { Print($10); }).
This syntax was inpsired by Swift's Shorthand Argument Names.
// A lambda that takes two positional parameters being used as a comparator
Sort(my_list, fn => $0.val < $1.val);
// In Swift: { $0.val < $1.val }
# Positional parameter restrictions
Lambdas with positional parameters have the restriction that they can only be used in a context where there is exactly one enclosing function or lambda that has no explicit parameter list. For example:
fn Foo1 {
// ❌ Invalid: Foo1 is already using positional parameters
let lambda: auto = fn => $0 < $1
}
fn Foo2 {
my_list.Sort(
// ❌ Invalid: Foo2 is already using positional parameters
fn => $0 < $1
);
}
fn Foo3() {
my_list.Sort(
// ✅ Valid: Foo3 has explicit parameters
fn => $0 < $1
);
}
fn Foo4() {
let lambda: auto = fn -> bool {
// ❌ Invalid: Outer lambda is already using positional parameters
return (fn => $0 < $1)($0, $1);
};
}
fn Foo5() {
let lambda: auto = fn (x: i32, y: i32) -> bool {
// ✅ Valid: Outer lambda has explicit parameters
return (fn => $0 < $1)(x, y);
};
}
# Captures
Captures in Carbon mirror the non-init captures of C++. A capture declaration
consists of a capture mode (for var captures) followed by the name of a
binding from the enclosing scope, and makes that identifier available in the
inner function body. The lifetime of a capture is the lifetime of the function
in which it exists. For example...
fn Foo() {
let handle: Handle = Handle.Get();
var thread: Thread = Thread.Make(fn [var handle] { handle.Process(); });
thread.Join();
}
fn Foo() {
let handle: Handle = Handle.Get();
fn MyThread[handle]() { handle.Process(); }
var thread: Thread = Thread.Make(MyThread);
thread.Join();
}
# Capture modes
Lambdas can capture variables from their surrounding scope using let or var,
just like regular bindings.
Capture modes can be used as default capture mode specifiers or for explicit captures as shown in the example code below.
fn Example() {
var a: i32 = 0;
var b: i32 = 0;
let lambda: auto = fn [a, var b] {
// ❌ Invalid: by-value captures are immutable (default `let`)
a += 1;
// ✅ Valid: `b` is a mutable copy (captured with `var`)
b += 1;
};
lambda();
}
fn Example {
fn Invalid() -> auto {
var s: String = "Hello world";
return fn [s]() => s;
}
// ❌ Invalid: returned lambda references `s` which is no longer alive
// when the lambda is invoked.
Print(Invalid()());
}
Note: If a function object F has mutable state, either because it has a by-object capture or because it has a by-object function field, then a call to F should require the callee to be a reference expression rather than a value expression. We need a mutable handle to the function in order to be able to mutate its mutable state.
# Default capture mode
By default, there is no capturing in lambdas. The lack of any square brackets is
the same as an empty pair of square brackets. Users can opt into capturing
behavior. This is done either by way of individual explicit captures, or more
succinctly by way of a default capture mode. The default capture mode roughly
mirrors the syntax [=] and [&] capture modes from C++ by being the first
thing to appear in the square brackets.
fn Foo1() {
let handle: Handle = Handle.Get();
fn MyThread[var]() {
// `handle` is captured by-object due to the default capture
// mode specifier of `var`
handle.Process();
}
var thread: Thread = Thread.Make(MyThread);
thread.Join();
}
fn Foo2() {
let handle: Handle = Handle.Get();
fn MyThread[let]() {
// `handle` is captured by-value due to the default capture
// mode specifier of `let`
handle.Process();
}
var thread: Thread = Thread.Make(MyThread);
thread.Join();
}
# Function fields in lambdas
Function fields in lambdas mirror the behavior of init captures in C++. A
function field definition consists of an irrefutable pattern, =, and an
initializer. It matches the pattern with the initializer when the lambda
definition is evaluated. The bindings in the pattern have the same lifetime as
the function, and their scope extends to the end of the function body.
fn Foo() {
var h1: Handle = Handle.Get();
var h2: Handle = Handle.Get();
var thread: Thread = Thread.Make(fn [a: auto = h1, var b: auto = h2] {
a.Process();
b.Process();
});
thread.Join();
}
# Copy semantics
To mirror the behavior of C++, lambdas will be as copyable as their contained function fields and function captures. This means that, if a function holds a by-object function field, if the type of the field is copyable, so too is the function that contains it. This also applies to captures.
The other case is by-value function fields. Since C++ const references, when made into fields of a class, prevent the class from being copied assigned, so too should by-value function fields prevent the function in which it is contained from being copied assigned.
# Self and recursion
To mirror C++'s use of capturing this, self should always come from the
outer scope as a capture. self: Self is never permitted on lambdas.
// ❌ Not allowed
let lambda: auto = fn [self: Self] { self.F(); };
// ✅ Captures `self` from outer scope
let lambda: auto = fn [self] { self.F(); };
Note: Following
#3720, an expression
of the form x.(F), where F is a function with a self or ref self
parameter, produces a callable that holds the value of x, and does not hold
the value of F. As a consequence, we can't support combining captures and
function fields with a self parameter.