Functions in Carbon
Learn how to define functions in Carbon - parameters, return values, recursion, scope, and the language-defining checked generics, with Docker-ready examples
Functions are the primary unit of reuse in Carbon. As a multi-paradigm systems language designed as a successor to C++, Carbon keeps function syntax familiar to C++ developers while modernizing it: every function is introduced with the fn keyword, parameters and return types are explicitly annotated, and the return type follows an arrow (->). You have already met one function—the Run() entry point from the Hello World page. This tutorial shows how to write your own.
Because Carbon is statically and nominatively typed, the compiler checks every call against the function’s declared signature at compile time. That means a function’s parameter types and return type form a contract the compiler enforces before the program ever links. Carbon’s most distinctive contribution to this area is checked generics: unlike C++ templates, which are only type-checked when instantiated, a Carbon generic function is verified at definition time against the constraints (interfaces) you declare for its type parameters.
In this tutorial you will learn how to define and call functions, pass parameters and return values, write recursive functions, reason about variable scope, and write a checked generic function. Every example compiles and links with the nightly Carbon toolchain and prints its results with Core.PrintStr and Core.Print.
Defining and Calling Functions
A Carbon function declares its parameters as name: Type pairs and its return type after ->. A function with no -> clause returns nothing. Functions can call other functions, and the call order in the source does not matter.
Create a file named functions.carbon:
import Core library "io";
// A function with two parameters that returns their sum.
fn Add(x: i32, y: i32) -> i32 {
return x + y;
}
// Functions can call other functions.
fn AddThree(a: i32, b: i32, c: i32) -> i32 {
return Add(Add(a, b), c);
}
// A function with no return type prints instead of returning.
fn Announce(label: str, value: i32) {
Core.PrintStr(label);
Core.Print(value);
}
fn Run() {
Announce("Add(3, 4) = ", Add(3, 4));
Announce("AddThree(1, 2, 3) = ", AddThree(1, 2, 3));
}
Key points in this example:
fn Add(x: i32, y: i32) -> i32declares a function taking two 32-bit integers and returning one. The types are mandatory—Carbon will not infer parameter types.returnhands a value back to the caller. A function whose signature has no-> Type(likeAnnounce) simply runs to the end and returns nothing.Core.Print(value)prints a 32-bit integer followed by a newline, whileCore.PrintStr(label)prints a string with no trailing newline—solabelandvalueland on the same line.
Recursion
Carbon functions can call themselves, so recursion works exactly as you would expect from C++. Because a recursive call is just an ordinary call checked against the same signature, factorial and Fibonacci translate directly.
Create a file named recursion.carbon:
import Core library "io";
// Classic recursive factorial.
fn Factorial(n: i32) -> i32 {
if (n <= 1) {
return 1;
}
return n * Factorial(n - 1);
}
// Recursive Fibonacci.
fn Fibonacci(n: i32) -> i32 {
if (n < 2) {
return n;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
fn Run() {
Core.PrintStr("5! = ");
Core.Print(Factorial(5));
Core.PrintStr("Fibonacci(10) = ");
Core.Print(Fibonacci(10));
}
Each function uses an early return for its base case and a recursive return for the general case. Factorial(5) evaluates to 120, and Fibonacci(10) evaluates to 55.
Variable Scope
Names declared inside a function are local to that function. A parameter or a var declared in one function is invisible to another—the only way data crosses a function boundary is through arguments and return values. Carbon favors this explicit passing over shared global state.
Create a file named scope.carbon:
import Core library "io";
fn Describe(n: i32) {
// 'doubled' is local to Describe; it does not exist outside this body.
var doubled: i32 = n * 2;
Core.PrintStr("Doubled: ");
Core.Print(doubled);
}
fn Run() {
var value: i32 = 21;
Describe(value);
// 'doubled' from Describe is not visible here, but 'value' still is.
Core.PrintStr("Original: ");
Core.Print(value);
}
Here value lives only inside Run, and doubled lives only inside Describe. Describe receives a copy of value through its parameter n; modifying n or doubled would have no effect on the caller’s value.
Checked Generics
Carbon’s signature feature is the checked generic function. You introduce type parameters in square brackets and constrain them with an interface using :!. The constraint tells the compiler what operations the type must support, so the generic body is fully type-checked at definition time—before any particular type is plugged in. This is the key difference from C++ templates, which are only checked once instantiated.
Create a file named generics.carbon:
import Core library "io";
// 'T:! Ordered' means T must implement the Ordered interface,
// which provides the comparison operators used below.
fn Min[T:! Ordered](x: T, y: T) -> T {
return if x <= y then x else y;
}
fn Max[T:! Ordered](x: T, y: T) -> T {
return if x >= y then x else y;
}
fn Run() {
Core.PrintStr("Min(8, 3) = ");
Core.Print(Min(8, 3));
Core.PrintStr("Max(8, 3) = ");
Core.Print(Max(8, 3));
}
Notes on this example:
[T:! Ordered]declares a generic type parameterTconstrained to types that implementOrdered. Thei32type implementsOrdered, so callingMin(8, 3)deducesTasi32.if x <= y then x else yis Carbon’s expression form ofif: it evaluates to one of two values, unlike the statement form used in the recursion example.- The single definition of
Minworks for anyOrderedtype, yet the compiler verified the body once, at definition time.
Running with Docker
Carbon’s nightly toolchain runs on Linux. The command below downloads the toolchain inside an Ubuntu container, then compiles, links, and runs functions.carbon. To run a different example, replace each occurrence of functions with recursion, scope, or generics.
| |
Note: The first run downloads roughly 200 MB for the toolchain, so it takes a few minutes. Subsequent runs can reuse the cached Ubuntu image.
Expected Output
Running functions.carbon:
Add(3, 4) = 7
AddThree(1, 2, 3) = 6
Running recursion.carbon:
5! = 120
Fibonacci(10) = 55
Running scope.carbon:
Doubled: 42
Original: 21
Running generics.carbon:
Min(8, 3) = 3
Max(8, 3) = 8
Key Concepts
fndeclares every function, with parameters written asname: Typeand the return type following->. A function with no->clause returns nothing.- Parameter and return types are mandatory and statically checked. Carbon does not infer parameter types, and every call is validated against the declared signature at compile time.
returnexits a function and supplies its value; early returns are idiomatic for base cases in recursion.- Recursion works directly, since a recursive call is just another type-checked call—
FactorialandFibonacciare textbook examples. - Names are local by default. Parameters and
varbindings live only inside their function; data crosses boundaries through arguments and return values, not shared globals. - Checked generics are Carbon’s defining feature. A type parameter constrained with
:! Interfacelets the compiler verify a generic body at definition time, in contrast to C++ templates checked only at instantiation. - Carbon has both statement and expression forms of
if. Use the statement form for control flow andif … then … else …when you need a value.
Comments
Loading comments...
Leave a Comment