Intermediate

Functions in Odin

Learn how procedures work in Odin - parameters, multiple and named return values, default arguments, recursion, and higher-order procedures with Docker-ready examples

In Odin, the callable unit of code is called a procedure (proc), not a “function.” This naming follows the Pascal and Oberon tradition that influenced the language, and it reflects Odin’s pragmatic, procedural philosophy: a procedure is a named block of work that can take inputs, perform side effects, and return outputs. There is no distinction between “methods” and “functions” as in object-oriented languages – Odin has no classes, so everything is a free-standing procedure.

Procedures in Odin are declared with the same :: constant-declaration syntax you saw in Hello World (main :: proc()). This consistency is deliberate: a procedure is just a compile-time constant whose value happens to be code. Because procedures are first-class values, they can be stored in variables and passed to other procedures, enabling higher-order patterns without any special syntax.

This tutorial covers how to define and call procedures, pass parameters (including default and variadic parameters), return single, multiple, and named values, write recursive procedures, and treat procedures as values. Odin keeps all of this explicit – there are no hidden conversions or implicit control flow, so what you write is what runs.

Defining and Calling Procedures

A procedure is declared with name :: proc(parameters) -> return_type. Parameters list their type after the name (a: int), and consecutive parameters of the same type can share a single annotation (a, b: int). A procedure with no -> clause returns nothing.

Create a file named functions.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "core:fmt"

// Two parameters, one return value
add :: proc(a: int, b: int) -> int {
    return a + b
}

// Parameters of the same type can share a type annotation
multiply :: proc(a, b: int) -> int {
    return a * b
}

// No return type: this procedure only performs an action
greet :: proc(name: string) {
    fmt.printf("Hello, %s!\n", name)
}

main :: proc() {
    sum := add(3, 4)
    product := multiply(5, 6)

    fmt.println("Sum:", sum)
    fmt.println("Product:", product)

    greet("Odin")
}

Notice that the order of declarations does not matter – Odin compiles a whole directory at once, so main can call procedures defined above or below it without forward declarations or header files.

Multiple and Named Return Values

Odin procedures can return more than one value, which is how the language handles patterns that other languages solve with exceptions or output parameters. You can also name the return values; named returns act like pre-declared local variables, and a bare return sends back their current values.

Create a file named returns.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "core:fmt"

// Multiple (unnamed) return values
divmod :: proc(a, b: int) -> (int, int) {
    return a / b, a % b
}

// Named return values: assign to the names, then use a bare return
min_max :: proc(values: []int) -> (min, max: int) {
    min = values[0]
    max = values[0]
    for v in values[1:] {
        if v < min do min = v
        if v > max do max = v
    }
    return
}

main :: proc() {
    q, r := divmod(17, 5)
    fmt.printf("17 / 5 = %d remainder %d\n", q, r)

    lo, hi := min_max([]int{4, 9, 1, 7, 2})
    fmt.printf("min = %d, max = %d\n", lo, hi)
}

Multiple return values are central to Odin’s error-handling style: a procedure commonly returns a result plus an ok boolean or an error value, and the caller checks it explicitly rather than relying on exceptions.

Default and Variadic Parameters

A parameter can be given a default value with = value; callers may then omit it. Odin also supports variadic parameters with the ..type syntax, which collects any number of trailing arguments into a slice.

Create a file named parameters.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "core:fmt"

// `exponent` defaults to 2 when the caller omits it
power :: proc(base: int, exponent: int = 2) -> int {
    result := 1
    for _ in 0..<exponent {
        result *= base
    }
    return result
}

// Variadic parameter: `nums` is a slice of all trailing arguments
sum_all :: proc(nums: ..int) -> int {
    total := 0
    for n in nums {
        total += n
    }
    return total
}

main :: proc() {
    fmt.println("5 squared:", power(5))        // uses default exponent = 2
    fmt.println("2 to the 5th:", power(2, 5))  // overrides the default

    fmt.println("Sum:", sum_all(1, 2, 3, 4, 5))
}

Default parameters keep call sites concise without overloading, and variadic parameters let a single procedure handle a flexible argument count – the fmt.println you have been using is itself variadic.

Recursion

A procedure can call itself. Because Odin has no special restrictions on self-reference, recursion works naturally for problems that are defined in terms of smaller versions of themselves.

Create a file named recursion.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "core:fmt"

// Classic recursive factorial
factorial :: proc(n: int) -> int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n - 1)
}

// Recursive Fibonacci
fibonacci :: proc(n: int) -> int {
    if n < 2 {
        return n
    }
    return fibonacci(n - 1) + fibonacci(n - 2)
}

main :: proc() {
    fmt.println("5! =", factorial(5))
    fmt.println("10! =", factorial(10))

    fmt.print("First 10 Fibonacci numbers:")
    for i in 0..<10 {
        fmt.printf(" %d", fibonacci(i))
    }
    fmt.println()
}

Each recursive call gets its own stack frame with its own local variables. For deeply recursive work, an iterative loop is often more efficient in a systems language like Odin, but recursion remains the clearest expression of many algorithms.

Procedures as Values (Higher-Order Procedures)

Because a procedure is a first-class value, it can be stored in a variable or passed to another procedure. The type of a procedure value is written just like its signature: proc(int) -> int.

Create a file named higher_order.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "core:fmt"

// `op` is a parameter whose type is "procedure taking an int, returning an int"
apply :: proc(x: int, op: proc(int) -> int) -> int {
    return op(x)
}

double :: proc(x: int) -> int {
    return x * 2
}

square :: proc(x: int) -> int {
    return x * x
}

main :: proc() {
    fmt.println("double(5):", apply(5, double))
    fmt.println("square(5):", apply(5, square))

    // A procedure can also be stored in a variable and called later
    op := double
    fmt.println("via variable:", op(21))
}

Passing procedures as arguments lets you write generic, reusable logic – a sort routine that accepts a comparison procedure, or a callback invoked when an event fires – without inheritance or interfaces.

A Note on Scope

Variables declared inside a procedure are local to it and disappear when the procedure returns. Identifiers declared at the top level of a file (outside any procedure) have package scope and are visible to every procedure in that package. Each { ... } block also introduces a new scope, so a variable declared inside an if or for block is not visible outside it. Odin does not allow implicitly capturing local variables into a nested named procedure, which keeps data flow explicit – pass what you need as parameters.

Running with Docker

Each example is a complete program with its own main procedure. Since odin run . compiles every .odin file in a directory, copy one file at a time into a clean working directory before running it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official image
docker pull primeimages/odin:latest

# Run the basic procedures example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp functions.odin /tmp/functions.odin && cd /tmp && odin run ."

# Run the multiple/named return values example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp returns.odin /tmp/returns.odin && cd /tmp && odin run ."

# Run the default/variadic parameters example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp parameters.odin /tmp/parameters.odin && cd /tmp && odin run ."

# Run the recursion example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp recursion.odin /tmp/recursion.odin && cd /tmp && odin run ."

# Run the higher-order procedures example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp higher_order.odin /tmp/higher_order.odin && cd /tmp && odin run ."

The file is copied to /tmp because odin run . compiles all .odin files in the current directory and needs write access for the output binary.

Expected Output

Running functions.odin:

Sum: 7
Product: 30
Hello, Odin!

Running returns.odin:

17 / 5 = 3 remainder 2
min = 1, max = 9

Running parameters.odin:

5 squared: 25
2 to the 5th: 32
Sum: 15

Running recursion.odin:

5! = 120
10! = 3628800
First 10 Fibonacci numbers: 0 1 1 2 3 5 8 13 21 34

Running higher_order.odin:

double(5): 10
square(5): 25
via variable: 42

Key Concepts

  • Procedures, not functions - Odin’s callable unit is the proc, declared as a constant with name :: proc(...) -> .... There are no classes or methods; every procedure is free-standing.
  • Shared type annotations - Consecutive parameters of the same type can be written as a, b: int instead of repeating the type.
  • Multiple return values - Procedures can return several values at once (-> (int, int)), the foundation of Odin’s exception-free error handling.
  • Named returns - Naming return values lets you assign to them as locals and finish with a bare return.
  • Default and variadic parameters - exponent: int = 2 supplies a default; nums: ..int collects any number of trailing arguments into a slice.
  • Recursion is natural - A procedure may call itself; each call gets its own stack frame, though loops are often more efficient for deep recursion.
  • Procedures are first-class values - A procedure can be stored in a variable or passed as an argument with the type proc(...) -> ..., enabling higher-order patterns without inheritance.
  • Explicit scope - Locals live inside their procedure and block; top-level declarations have package scope. Odin keeps data flow explicit rather than implicitly capturing locals.

Running Today

All examples can be run using Docker:

docker pull primeimages/odin:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining