Mojo Functions

A function is a named, reusable block of code that performs a specific task. Instead of copying the same logic in multiple places, you write it once as a function and call it whenever you need it. Mojo gives you two ways to define functions — fn for performance-critical code and def for Python-compatible flexibility.

Functions as Machines

  Input (arguments)
        │
        ▼
  ┌─────────────┐
  │  Function   │  ← Named block of code
  │  (machine)  │
  └─────────────┘
        │
        ▼
  Output (return value)

Example:
  Input: 5, 3
     ↓
  [add function]
     ↓
  Output: 8

Defining a Function with fn

The fn keyword defines a Mojo-native function. Mojo requires you to declare the type of each parameter and the return type.

fn add(a: Int, b: Int) -> Int:
    return a + b

fn main():
    var result = add(10, 3)
    print(result)   # 13

Anatomy of an fn Function

  fn  add  (a: Int, b: Int)  -> Int:
  │   │     │                  │
  │   │     │                  └── Return type
  │   │     └─ Parameters with types
  │   └── Function name
  └── Keyword

Functions with No Return Value

When a function performs an action but produces no output value, omit the return type arrow entirely. Mojo treats this as a function that returns None.

fn greet(name: String):
    print("Hello,", name)

fn main():
    greet("Priya")   # Hello, Priya
    greet("Carlos")  # Hello, Carlos

Defining a Function with def

The def keyword defines a Python-compatible function. Parameter types and return types are optional. Use def when you want flexibility or are reusing Python-style code.

def multiply(x, y):
    return x * y

fn main():
    print(multiply(4, 7))   # 28

fn vs def — Quick Comparison

Feature              | fn              | def
---------------------|-----------------|------------------
Type annotations     | Required        | Optional
Speed                | Faster          | Slightly slower
Python compatibility | Less compatible | More compatible
Error checking       | At compile time | At runtime
Ownership control    | Full            | Limited

Use fn for new Mojo code where performance matters. Use def when prototyping or integrating Python libraries.

Default Parameter Values

Assign a default value to a parameter so callers can omit that argument when the default is appropriate.

fn power(base: Int, exponent: Int = 2) -> Int:
    var result = 1
    for _ in range(exponent):
        result *= base
    return result

fn main():
    print(power(3))     # 9  (uses default exponent=2)
    print(power(3, 4))  # 81 (overrides default)

Multiple Return Values

Mojo functions return a single value. To return multiple pieces of data, pack them into a tuple.

fn min_max(a: Int, b: Int, c: Int) -> (Int, Int):
    var lo = a
    var hi = a
    if b < lo: lo = b
    if b > hi: hi = b
    if c < lo: lo = c
    if c > hi: hi = c
    return (lo, hi)

fn main():
    var low, high = min_max(4, 1, 9)
    print("Min:", low, "Max:", high)   # Min: 1 Max: 9

Argument Passing: Borrowed vs Mutable

Mojo gives you control over whether a function can modify its arguments. This is one of Mojo's key performance and safety features.

borrowed (Default)

The function reads the value but cannot change it. Passing a borrowed argument costs nothing in memory — no copy is made. The caller's variable stays unchanged.

fn describe(borrowed text: String):
    print("Length:", len(text))

fn main():
    var sentence = "Mojo is fast"
    describe(sentence)
    # sentence is still "Mojo is fast" — unchanged

inout

The function can read and modify the argument. Changes to the parameter inside the function affect the caller's original variable.

fn double(inout value: Int):
    value *= 2

fn main():
    var x = 5
    double(x)
    print(x)   # 10 — the original variable changed
Diagram:

  borrowed:
    Caller  ──read-only──→  Function
    (variable safe)

  inout:
    Caller  ←──read/write──→  Function
    (variable may change)

Recursive Functions

A function that calls itself is recursive. Recursion solves problems that break down into smaller versions of the same problem.

fn factorial(n: Int) -> Int:
    if n == 0:
        return 1
    return n * factorial(n - 1)

fn main():
    print(factorial(5))   # 120
Call Stack for factorial(4):
  factorial(4)
  → 4 × factorial(3)
       → 3 × factorial(2)
            → 2 × factorial(1)
                 → 1 × factorial(0)
                       → 1
            ← 2 × 1 = 2
       ← 3 × 2 = 6
  ← 4 × 6 = 24

Every recursive function needs a base case — a condition that stops the recursion. Without it, the function calls itself forever until the program runs out of stack space.

Function Overloading

Mojo allows multiple functions with the same name as long as their parameter types differ. The compiler picks the correct version based on the argument types you pass.

fn area(side: Int) -> Int:
    return side * side

fn area(length: Int, width: Int) -> Int:
    return length * width

fn area(radius: Float64) -> Float64:
    return 3.14159 * radius * radius

fn main():
    print(area(5))          # 25      — square
    print(area(4, 6))       # 24      — rectangle
    print(area(3.0))        # 28.27...— circle

Key Takeaways

Functions encapsulate reusable logic under a name. Use fn for typed, high-performance Mojo functions and def for Python-compatible flexibility. Declare parameter types and return types with fn. Use borrowed for read-only access and inout when the function must modify the caller's variable. Recursive functions solve self-similar problems but always need a base case. Function overloading lets the same name serve different input types.

Leave a Comment

Your email address will not be published. Required fields are marked *