Specify the behavior of function calls and the type and behavior of the entity introduced by a function declaration.
Functions are a primitive building block through which we define Carbon's execution semantics, but we don't currently have a specification for what a function is in Carbon nor how function calls work. We have open questions around whether Carbon has first-class function types, how types can overload the function call operation, and how callable entities such as the name of a parameterized class fit into the picture -- are they functions or something else?
This proposal assumes the reader understands how functions and function pointers work in C or C++.
A function declaration in Carbon introduces a new, unique, stateless type, called a function type. The function name is bound to a value of that function type.
Function calls use C-like syntax: an expression naming a callable is followed by a syntax resembling a tuple of arguments. There are several kinds of callable:
my_vector.Begin.Vector or a generic
interface AddWith.In the first four cases, argument deduction is used to determine the values of deduced arguments, and then pattern matching is used to bind parameter patterns to argument values.
In the remaining cases, the built-in Call interface is used to determine the
semantics of the call, and the call F(args) is translated into
F.(Call(ArgTypes).Op)(args). Note that this is itself a bound member function
call, and Call(ArgTypes) is a call to a parameterized entity, so this
transformation is only performed once.
A function declaration such as
fn F[T:! type](x: T) -> T { return x; }
introduces a value named F, whose type is a unique function type. Distinct
functions have distinct function types, even if they have the same signature. A
function type is an empty, trivial type. There is no way to name a function type
other than asking for the type of the function value.
// Compile-time function.
fn TypeOf[T:! type](x: T) -> type { return T; }
// ✅ `F` is a first-class value with a first-class type.
let template FType:! type = TypeOf(F);
var my_f: FType = F;
Function values are regular values that can be stored in variables, passed to functions, and so on.
fn G() -> i32 {
// ✅ `my_f` has function type `FType`. This is a direct call to `F`.
return my_f(1);
}
fn Sort[template F:! type, T:! type](v: Vector(T)*, f: F);
fn Compare(a: i32, b: i32) -> Ordering { return a.(Ordered.Compare)(b); }
fn SortInts(v: Vector(i32)*) { Sort(v, Compare); }
For the purpose of the orphan rule, a function type is considered to be declared by the function declaration that introduces the function value.
For each function type corresponding to a method, there is a corresponding
bound method type. When a member access is performed on an object of class
type to access a method, the result is a bound method value of bound method
type. A bound method type describes the callee in a method call, and a bound
method value describes the self parameter of the call.
class HasMember {
// `HasMember.F` has a stateless function type, with signature
// `[self: Self](n: i32) -> i32`.
fn F[self: Self](n: i32) -> i32;
}
fn F(h1: HasMember, h2: HasMember) -> i32 {
// ✅ `h1.F` is a bound method value whose type is a bound method type,
// with signature `(n: i32) -> i32`.
var hf: auto = h1.F;
// ✅ `h1.F` and `h2.F` are of the same bound method type.
hf = h2.F;
// ✅ Same as `h2.F(4)`.
return hf(4);
}
Calls take the form a(b, c, d) or a(b, c, d,), where:
a is the callee, which can be a name, a literal, a member access, or some
more complex expression enclosed parentheses.b, c, d are any number of argument expressions. Arguments are
separated by commas, and if the argument list is not empty, an optional
trailing comma is permitted but not required after the final argument.Call syntax is syntactically equivalent to a primary expression followed by a
tuple literal, except that a tuple literal requires a trailing comma to form a
single-element tuple (b,), whereas in call syntax both a(b) and a(b,) are
permitted.
A call expression is a direct call when the callee:
In a direct call, a call signature is available which is used to check the given arguments against the callee's declared implicit and explicit parameters. This checking proceeds as follows:
Then, for each binding in the explicit parameter list in turn, all argument values that have been deduced are substituted into the parameter.
template :! binding, the argument expression is
converted to have the same type as the binding and template constant
expression phase.:! binding, the argument expression is
converted to have the same type as the binding and symbolic constant
expression phase.If a parameter is a :! binding, its corresponding converted argument
expression is evaluated, and its value is added to the list of deduced
argument values before any later parameters are processed.
The result of the call expression depends on the callee:
type.self parameter of the called function is bound to
the self value in the bound method value.A generic parameter can be constrained to be a callable type using the Call
interface:
interface Call(Args: type) {
let Result:! type;
fn Op[self: Self](args: Args) -> Result;
}
TODO:
Callshould be variadic. For now, we model it as taking a tuple type, and we modelCall.Opas taking a tuple value.
For example:
fn Sort[T:! type, F:! Call((T, T)) where .Result = Ordering]
(v: Vector(T)*, cmp: F) {
A call expression that is not a direct call is translated into an invocation of
Call(Args).Op, where Args is the type of the call's argument tuple.
// In Sort...
auto ord: auto = cmp((*v)[i], (*v)[j]);
// ... is translated into ...
auto ord: auto = cmp.(Call((T, T)).Op)((*v)[i], (*v)[j]);
Note that the types of the call arguments are modeled as an interface parameter. This permits the call interface to model function overloading. However, the return type is an associated type, not a parameter -- we do not permit overloading on return types, and we don't want type information to propagate from the context in which a call expression appears inwards to the call.
CallA function type or bound method type implements the Call interface for every
set of runtime argument types that a direct call to the function or bound method
would accept. The behavior of Call.Op is to call the function or bound method
with the provided argument list.
fn Select[T:! type](b: bool, x: T, y: T) -> T {
return if b then x else y;
}
// Generated:
impl forall [T:! type, BType:! ImplicitAs(bool)]
Select as Call((BType, T, T)) where .Result = T {
fn Op[self: Self](args: (BType, T, T)) {
let (b: bool, x: T, y: T) = args;
return Select(b, x, y);
}
}
Implicit conversions are permitted for parameters whose types do not involve
deduced parameters. The intent is for the impl to support indirect calls in
the same cases where the function supports direct calls, with the same meaning.
fn TakeI32Fn[F:! Call(i32)](f: F);
fn I64Fn(n: i64);
fn Run() {
// ✅ `I64Fn` can be called with an `i32`, because
// `i32 impls ImplicitAs(i64)`.
TakeI32Fn(I64Fn);
}
Note: A call made using the
Callinterface always takes its arguments as value expressions and the call is always an initializing expression. If the function takes its arguments usingvar, or if a language mechanism is added that permits a direct function call to not be an initializing expression, additional conversions will be required. TheCallinterface is treated as not being implemented in cases where those conversions are not possible, for example because an argument or return value is not copyable. If we add a mechanism to allow animplto specify a different expression category for its parameters or result than the one in the interface, for example by taking avarparameter where the interface took aletparameter, that would automatically also be available for overloaded function calls.We anticipate revisiting these constraints in a future proposal, and expanding the function call interface to provide the same generality that is provided by
fndeclarations. This is described more in the future work section.
The Call interface only models function calls for which arbitrary runtime
values of the given parameter types can be passed to the function. If the
signature of the function has compile-time parameters in its explicit argument
list or has any refutable pattern matching between the call arguments and
function parameters, the function type will not implement Call.
fn Runtime[T:! type](x: T);
fn CompileTime(T:! type, x: T);
// 🤷 Undecided whether this is valid.
fn RefutablePattern(1 as i32);
fn Run() {
// ✅ Calls `Runtime(0)`.
Runtime.(Call(i32).Op)(0);
// ❌ Can't call `CompileTime` this way, it can't implement `Call(type, i32)`
// because the type would be passed at runtime.
CompileTime.(Call(type, i32).Op)(i32, 0);
// ❌ Can't call `RefutablePattern` this way, it can't implement `Call(i32)`
// for arbitrary i32 arguments.
RefutablePattern.(Call(i32).Op)(0);
}
The Call interface can be implemented to overload the meaning of the function
call operator for a type.
class Func(Arg:! type) {
impl as Call((Arg,)) where .Result = () {
fn Op[self: Self](arg: (Arg,)) { Print("hello, world"); }
}
}
fn Run() {
let f: Func(i32) = {};
// ✅ Prints "hello, world".
f(42);
}
There are no constraints on the callee type, beyond the normal constraints for implementing an interface.
class X { var n: i32; }
// ✅ OK, but inadvisable.
impl {.a: X} as Call(()) where .Result = i32 {
fn Op[self: Self](args: ()) -> i32 {
return self.a.n;
}
}
fn Run() -> i32 {
// Returns 1.
return {.a = {.n = 1} as X}();
}
There is no way to directly define a function-like callable class type that
takes a compile-time argument. This is similar to the situation in C++ where
there is no way to define an operator() for a value x of class type so that
x<T>() passes T to operator() at compile time. However, this can be worked
around by putting the compile-time value in the type of an argument instead:
class Wrap(T:! type) {}
class Callable {
impl forall [T:! Printable] as Call((Wrap(T), T)) where .Result = () {
fn Op[self: Self](args: (Wrap(T), T)) {
let (_: auto, v: T) = args;
Print(v);
}
}
}
fn CallIt[F:! Call(Wrap(i32), i32)](f: F) {
f({} as Wrap(i32), 0);
}
fn Run() {
CallIt({} as Callable);
}
Principles:
Goals:
Call, as can member function pointers.We could give each function signature a distinct type, as is done in C and C++. This would provide a story for function pointers that does not rely on generics or type erasure or fancy representation optimizations.
The main down side of this approach is one we see frequently in C++: passing a function to a template generic would be less efficient than passing a function object to the same template. For example, in C++, given
std::vector<int> v;
bool cmp(int a, int b) { return simple_calculation(a, b); }
... calling ranges::sort(v, cmp) may be much less efficient than calling
ranges::sort(v, [](int a, int b) { return cmp(a, b); }), because the latter
performs a direct call and the former passes a function pointer, resulting in an
indirect call.
Making functions result in unique types means that calls to functions, lambdas, and function-like objects have semantics that are more similar and have similar efficiency properties.
It would be desirable to remove the difference between direct and indirect calls. Unfortunately, that doesn't seem to be practical, for a number of reasons:
interface and impl
mechanisms.impl query whether each argument can be passed at
compile time, but any such query will need to be able to fall back to
passing each argument at runtime, which can result in an exponential-time
search for a matching impl.We intend to support function overloading, and although it outside the scope of this proposal, it is important to ensure that we have reasonable paths forward to support overloading within this framework.
An overloaded function has multiple different signatures -- sets of implicit and explicit parameters -- that will be checked against the provided arguments. In this proposal, that means that a set of overloaded functions introduces a single function type that supports being called in different ways.
Depending on how we approach overloading, overload selection might be done by
checking each candidate in turn until we find one that matches, or checking them
all and somehow selecting the best match, and our current intent is to use the
former approach. Translated into this proposal, one important consequence is
that the function type corresponding to the set of overloaded functions has
multiple parameterized implementations of the Call(...) interface, and the
implementation selected for a particular call will need to follow whatever rules
the overloading mechanism picks. For example, if function overloading picks the
first matching function, then given the following, using placeholder syntax:
overloaded fn Abs[T:! Unsigned](x: T) -> T { return x; }
overloaded fn Abs[T:! Floating](x: T) -> T { return x < 0 ? -x : x }
we would synthesize implementations that behave as follows:
match_first {
impl forall [T:! Unsigned] Abs as Call((T,)) where .Result = T { ... }
impl forall [T:! Floating] Abs as Call((T,)) where .Result = T { ... }
}
For a type that is Unsigned & Floating, the former impl will be selected,
matching the behavior of overload selection.
We should consider whether calls can be overloaded on the expression category and expression phase of the callee and arguments. For compile-time arguments, we may also wish to support overloading on the constant value.
Other languages support overloading on the expression category of the callable
object. Rust provides separate Fn, FnMut, and FnOnce to distinguish
between different ways of passing the callable object into an overloaded
function call, and C++ permits operator() to take *this as const,
non-const, or even &&.
In Carbon, we could follow the Rust approach and provide multiple Call
interfaces to handle different kinds of self parameter, modeled in a similar
way to the IndexWith and IndirectIndexWith interfaces used for subscripting.
A more general, ambitious and ergonomic approach that we have started exploring
would be to allow the function call signature to be described as part of the
call interface:
impl MyCallable as call(T:! type, x: T) -> T;
This might be a shorthand syntax for some way of expressing the signature as an interface, or a new form of first-class language primitive.
The options in this space are currently being explored in issue #3154.
Once Carbon supports variadics, the overloaded call mechanism should be revised to make use of them, instead of using a tuple type to approximate a variadic parameter list.
A lambda is expected to behave much like a function under this proposal, with the primary difference being that lambdas can be stateful, whereas functions are stateless.
For interoperability with C++, and for purposes of cheaply storing references to stateless functions, function pointers should be supported. A function pointer could be modeled as:
alias FunctionPtr(Args:! type, Result:! type) =
DynPtr(Stateless & Call(Args) where .Result = Result);
where:
Stateless is an interface describing types that are empty, do not have a
notion of identity, and for which instances can be created and destroyed
trivially.DynPtr is a
type that performs type erasure and runtime dispatch
for a given facet type.DynPtr has an optimization that DynPtr(Stateless & I), where I is an
interface with exactly one associated function, is represented as a pointer
to that function.