Incorporates changes from these proposals: - #2022 - #2875 - #3262 - #3763 - #3848 - #5434 A small amount of updating was done to lambdas.md and variadics.md to harmonize with these changes. Assisted-by: Gemini via Antigravity --------- Co-authored-by: Josh L <josh11b@users.noreply.github.com> Co-authored-by: Geoff Romer <gromer@google.com>
21 KiB
Functions
Table of contents
- Overview
- Function definitions
- Forward declarations
- Function types and values
- Function calls
- Functions in other features
- Alternatives considered
- References
Overview
Functions are the core building block for applications. A function definition or declaration has one of the following syntactic forms (where items in square brackets are optional and independent):
fnname [implicit-parameters] [tuple-pattern]=>expression;fnname [implicit-parameters] [tuple-pattern] [->return-form]{statements}fnname [implicit-parameters] [tuple-pattern] [->return-form];
The first form is a shorthand: => expression ; is equivalent to
-> auto { return expression; }. When a body is present (the first and second
forms), it is a function definition. The body introduces nested scopes which may
contain local variable declarations. A function with only a signature and no
body (the third form) is a forward declaration.
The syntax for parameters and returns is the same for functions and lambdas:
- implicit-parameters: square brackets
[...]enclosing default capture modes, explicit captures, function fields, or deduced parameters, see lambdas. - tuple-pattern: parentheses
(...)enclosing a list of explicit parameter patterns, see pattern matching.
Function definitions
A basic function definition may look like:
fn Add(a: i64, b: i64) -> i64 {
return a + b;
}
Or using the shorthand => return expression syntax:
fn Add(a: i64, b: i64) => a + b;
These declare a function called Add which accepts two i64 parameters, the
first called a and the second called b, and returns an i64 result.
C++ might declare the same thing:
std::int64_t Add(std::int64_t a, std::int64_t b) {
return a + b;
}
// Or with trailing return type syntax:
auto Add(std::int64_t a, std::int64_t b) -> std::int64_t {
return a + b;
}
Function signatures
Captures and function fields
Like lambdas, named function definitions support captures and function fields, with these restrictions:
- They can only be used on functions where the definition is attached to the declaration (so they cannot be forward declared).
- Captures and function fields are only supported on local function definitions immediately defined inside the body of another function. They are not supported on member functions of classes/interfaces.
Positional Parameters
Like lambdas, named function definitions support positional parameters, which are used when the explicit parameter list is omitted. Like captures and function fields, they may only be used with function definitions and not forward declarations. In addition, positional parameters can only be used in a context where there is exactly one enclosing function or lambda that has no explicit parameter list.
Return specification
The return type of a function can be specified using a return clause (->), or
it can be deduced using a signature return expression (=>).
->followed by a return form:- Most commonly, this will be an expression that directly states the
return type, such as
i64.- The expression will be evaluated at compile-time, so must be valid in that context.
- For example,
fn ToString(val: i64) -> strbuf;has a return type ofstrbuf.
- A return form can also use
val,ref, andvarto control the function call's expression category. For example,-> ref i32indicates that the function returns by reference. See "Function calls and returns" for details.
- Most commonly, this will be an expression that directly states the
return type, such as
->followed by theautokeyword indicates that type inference should be used to determine the return type.- For example,
fn Echo(val: i64) -> auto { return val; }will have a return type ofi64through type inference. - Forward declarations must have a known return type, so
autois not valid. - The function must have precisely one
returnstatement. Thatreturnstatement's expression will then be used for type inference. - The
autokeyword may be preceded byval,ref, orvarto specify the return expression category.
- For example,
- Omission of both
->and=>indicates that the return type is the empty tuple,().- For example,
fn Sleep(seconds: i64);is similar tofn Sleep(seconds: i64) -> ();. ()is similar to avoidreturn type in C++.
- For example,
=>followed by an expression defines a shorthand for a function body that returns the expression. The return type is deduced as if-> autowere used.- For example,
fn Add(a: i64, b: i64) => a + b;has a return type ofi64based on the type of the expressiona + b. - Because the return type is deduced and not explicitly known, functions
defined using
=>cannot have a separate forward declaration.
- For example,
Unused parameters
When a parameter introduced in a function definition is not used in the function body, a compiler warning is issued. To suppress this warning, a parameter can be explicitly marked as unused in one of two ways:
- Anonymous parameters: By using
_in place of the parameter name (for example,_: i32). unusedparameters: By preceding the parameter name with theunusedkeyword (for example,unused size: i32), which allows preserving the parameter name for documentation purposes.
Both of these forms are patterns. For more details on the behavior of unused
name bindings and patterns, see the
pattern matching design.
For example:
// Function declaration (for example, in an API file)
fn Sum(x: List(i32), size: i32) -> i32;
// Implementation that does not use the `size` parameter, using an
// anonymous parameter:
fn Sum(x: List(i32), _: i32) -> i32 { ... }
// Or using the `unused` keyword to keep the name for documentation:
fn Sum(x: List(i32), unused size: i32) -> i32 { ... }
unused markers may only appear on definitions, not on non-defining
declarations. The names of parameters must match between redeclarations, but the
presence of the unused marker does not need to match, see
redeclaration matching.
return statements
The return statement is essential to function
control flow. It ends the flow of the function and returns execution to the
caller.
When the return clause is omitted, the return
statement has no expression argument, and function control flow implicitly ends
after the last statement in the function's body as if return; were present.
When the return clause is provided, including when it is -> (), the return
statement must have an expression that is convertible to the return type, and a
return statement must be used to end control flow of the function.
TODO: Update this section to cover the requirements on the form of the expression.
Forward declarations
Functions may be declared separate from the definition by providing only a signature, with no body. This provides an API which may be called. For example:
// Declaration:
fn Add(a: i64, b: i64) -> i64;
// Definition:
fn Add(a: i64, b: i64) -> i64 {
return a + b;
}
The corresponding definition may be provided later in the same file or, when the declaration is in an API file of a library, in an implementation file of the same library.
A function may only be forward declared once in any given file, and any forward declaration must appear before the definition.
To declare a function that is defined in a different library, the extern
modifier is used (for example, extern fn F();). A library that declares a
function as extern cannot define it. The extern modifier is only valid on
namespace-scoped functions, not on member functions of classes. For more details
on cross-library forward declarations and modifier merging, see the
declaring entities design.
Redeclaration matching
Redeclarations of a function must match syntactically. The sequence of tokens
following the fn keyword (and optional scope name) up to the semicolon or open
brace must be identical.
Specifically, the following must match exactly between the forward declaration and the definition:
- Parameter names: You cannot change a parameter name or replace it with
_in the definition. - Parameter types: The types and grouping parentheses must match exactly.
- Return clause: The presence or omission of the return clause must match
exactly (for example, an omitted return type behaves equivalent to
-> (), but they are syntactically different and cannot be mixed).
The only exception is the unused modifier on parameters, which is allowed on a
defining declaration (such as the definition) but disallowed on a non-defining
declaration.
Declaration modifiers (such as access control keywords or virtual) appear
before the fn keyword, so they are not involved in checking whether the two
signatures differ.
Function types and values
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.
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.
fn F(x: i32) -> i32 { return x; }
// Compile-time function.
musteval 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);
}
For the purpose of the orphan rule, a function type is considered to be declared by the function declaration that introduces the function value.
Bound methods
A function with a self parameter is a method. The type of a method is a
stateless type, like other functions. Once the method is
bound to an instance, for
example in the expression object.MethodName, the result is a bound method
value. The type of the result is a bound method type, with the same signature
as the method, but with the self parameter removed. A bound method type
describes the callee in a method call, and a bound method value specifies the
self parameter of the call.
class HasMember {
// `HasMember.F` has a stateless function type, with signature
// `(self, n: i32) -> i32`.
fn F(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);
}
Function calls
Function calls use C-like syntax:
expression
([ expression,expression,... ])
It consists of an expression naming a callee followed by an argument list
enclosed in parentheses, which resembles a tuple of arguments. Calls take the
form a(b, c, d) or a(b, c, d,), where:
ais the callee, which can be a name, a literal, a member access, or some more complex expression enclosed in parentheses.b,c,dare any number of argument expressions, each optionally prefixed withrefif passing to arefparameter. 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
suffix 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 callable value (or callable for short) is a value that can be used as the callee of a call expression. There are several kinds of callable:
- Functions, and more generally values of function types.
- Bound methods, such as
my_vector.Begin. - Lambdas.
- Parameterized entities, such as a generic class
Vectoror a generic interfaceAddWith. - Values of dependent types that are constrained to be callable.
- User-defined class types that overload function call syntax.
Function calls are divided into direct calls and indirect calls.
Direct calls
A call expression is a direct call when the callee:
- is the name of a parameterized entity, like a generic class or interface, or
- has a function type or bound method type.
Note that this includes virtual method calls, even though those can include some indirection. 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:
-
Argument deduction is performed by comparing the declared parameter types against the actual argument types and deducing values for implicit arguments that make the types equal.
-
Then, for each binding in the explicit parameter list in turn, all argument values that have been deduced are substituted into the parameter.
- If the parameter is a
refparameter (other thanself), the corresponding argument expression at the call-site must be prefixed withref. It is a compile-time error if the call-site has a mismatchedrefprefix:- An argument to a non-
refparameter must not be prefixed withref, and - An argument to a
refparameter must be prefixed withref, except in a generic context where the parameter'srefstatus may vary.
- An argument to a non-
- If the parameter is a
template :!binding, the argument expression is converted to have the same type as the binding and template constant expression phase. - If the parameter is a symbolic
:!binding, the argument expression is converted to have the same type as the binding and symbolic constant expression phase. - Otherwise, the parameter is pattern-matched against the argument.
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. - If the parameter is a
The result of the call expression depends on the callee:
- If the callee is a parameterized entity such as a generic class or a generic
interface, the result is the specific instance of that generic, such as a
class or interface, and the call is a value expression of type
type. - If the callee has a function type, the call is an expression whose form is the substituted return form of its signature. When evaluated, the call expression will invoke the function and produce whatever result it returns.
- If the callee has a bound method type, it behaves the same as a function
value, except that the
selfparameter of the called function is bound to theselfvalue in the bound method value.
Indirect calls and the Call interface
A generic parameter can be constrained to be a callable type using the Call
interface:
interface Call(... each Arg: type) {
let Result:! type;
fn Op(self, ... each arg: each Arg) -> Result;
}
A call expression that is not a direct call is an indirect call. It is
translated into an invocation of Call(Arg1, Arg2, ... ArgN).Op, where
Arg1, Arg2, ... ArgN are the types of the call's arguments in order. So
F(arg1, arg2) is translated into F.(Call(Arg1, Arg2).Op)(arg1, arg2).
For example, given:
fn Sort[T:! type, F:! Call(T, T) where .Result = Ordering]
(ref v: Vector(T), cmp: F) {
// ...
auto ord: auto = cmp(v[i], v[j]);
// ...
}
The call cmp(v[i], v[j]) is translated into:
auto ord: auto = cmp.(Call(T, T).Op)(v[i], v[j]);
A 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.
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);
}
Future work: The
Callinterface currently only supports value parameters and initializing returns. It is future work to remove this restriction.
Overloaded call operator
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, 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; }
impl {.a: X} as Call(()) where .Result = i32 {
fn Op(self, args: ()) -> i32 {
return self.a.n;
}
}
fn Run() -> i32 {
// Returns 1.
return {.a = {.n = 1} as X}();
}
Functions in other features
Other designs build upon basic function syntax to add advanced features:
- Generic functions adds support for deduced parameters and compile-time parameters.
- Member functions adds support for methods and non-instance member functions.
Alternatives considered
- Function keyword
- Only allow
autoreturn types if parameters are compile-time - Provide alternate function syntax for concise return type inference
- Allow separate declaration and definition
- Signature-based function types
- Make direct and indirect calls behave uniformly
References
- Proposal #438: Add statement syntax for function declarations
- Proposal #826: Function return type inference
- Proposal #2022: Unused Pattern Bindings (Unused Function Parameters)
- Proposal #2875: Functions, function types, and function calls
- Proposal #3762: Merging forward declarations
- Proposal #3763: Matching redeclarations
- Proposal #3848: Lambdas
- Proposal
#5434:
refparameters, arguments, returns andvalreturns