Intermediate

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) -> i32 declares a function taking two 32-bit integers and returning one. The types are mandatory—Carbon will not infer parameter types.
  • return hands a value back to the caller. A function whose signature has no -> Type (like Announce) simply runs to the end and returns nothing.
  • Core.Print(value) prints a 32-bit integer followed by a newline, while Core.PrintStr(label) prints a string with no trailing newline—so label and value land 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 parameter T constrained to types that implement Ordered. The i32 type implements Ordered, so calling Min(8, 3) deduces T as i32.
  • if x <= y then x else y is Carbon’s expression form of if: it evaluates to one of two values, unlike the statement form used in the recursion example.
  • The single definition of Min works for any Ordered type, 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.

1
2
3
4
5
# Pull the Ubuntu image
docker pull ubuntu:22.04

# Compile, link, and run functions.carbon (downloads the nightly toolchain inside the container)
docker run --rm -v $(pwd):/app -w /app ubuntu:22.04 bash -c "apt-get update -qq && apt-get install -y -qq wget libgcc-11-dev > /dev/null 2>&1 && VERSION=0.0.0-0.nightly.2026.02.07 && wget -q https://github.com/carbon-language/carbon-lang/releases/download/v\${VERSION}/carbon_toolchain-\${VERSION}.tar.gz && tar -xzf carbon_toolchain-\${VERSION}.tar.gz && ./carbon_toolchain-\${VERSION}/bin/carbon compile --output=functions.o functions.carbon && ./carbon_toolchain-\${VERSION}/bin/carbon link --output=functions functions.o && ./functions"

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

  • fn declares every function, with parameters written as name: Type and 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.
  • return exits 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—Factorial and Fibonacci are textbook examples.
  • Names are local by default. Parameters and var bindings 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 :! Interface lets 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 and if … then … else … when you need a value.

Running Today

All examples can be run using Docker:

docker pull ubuntu:22.04
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining