Functions

Functions in Stone are defined with the fn keyword. They can be single-expression functions for simple transformations or block functions for more complex logic.

Important binding rules:

  • Function parameters are bindings and cannot be rebound within the function body
  • No reassignment exists within function bodies—if you need a different value, bind it to a new name
  • The last expression in a block function is its return value
  • return(expr) is only for early exit—it exits the innermost function. Note: return without parentheses is not valid.

Single-Expression Functions

For simple functions, use the = syntax:

fn add(a, b) = a + b
fn square(x) = x * x
fn double(x) = x * 2

The expression after = is the return value. No return keyword is needed.

import sqrt from math
fn distance(x, y) = sqrt(x * x + y * y)
fn average(a, b) = (a + b) / 2
fn greet(name) = "Hello, " + name

Block Functions

For functions needing intermediate bindings, use braces. The last expression in the block is the return value:

import sqrt from math
fn hypotenuse(a, b) {
    a_sq = a * a
    b_sq = b * b
    sqrt(a_sq + b_sq)
}

For conditional logic, prefer expression functions with if/elif/else:

fn abs(x) = if (x >= 0) { x } else { -x }

fn clamp(x, lo, hi) = if (x < lo) { lo } elif (x > hi) { hi } else { x }

Multiple Statements

Block functions can contain any number of bindings. The last expression is the return value:

fn process(x) {
    doubled = x * 2
    adjusted = doubled + 1
    adjusted
}

Intermediate values are local to the function. Remember: parameters cannot be rebound, so you must create new bindings:

// ERROR: Cannot rebind parameter
fn bad_example(x) {
    x = x * 2  // ERROR: x is a parameter binding
    x
}

// CORRECT: Bind to a new name
fn good_example(x) {
    doubled = x * 2
    doubled
}

Calling Functions

Call functions by name with arguments in parentheses:

result = add(3, 4)        // 7
dist = distance(3, 4)     // 5
msg = greet("Alice")      // "Hello, Alice"

Functions can call other functions:

fn square(x) = x * x

fn sum_of_squares(a, b) = square(a) + square(b)

result = sum_of_squares(3, 4)  // 25

Keyword Arguments

When calling a function, you can pass arguments by name instead of position. Named arguments are matched to parameter names and reordered at compile time:

fn greet(name, greeting = "Hello") = greeting + ", " + name

greet("Alice")                          // positional — "Hello, Alice"
greet(name = "Alice")                   // named — "Hello, Alice"
greet(name = "Alice", greeting = "Hi")  // both named — "Hi, Alice"
greet("Alice", greeting = "Hi")         // positional then named — "Hi, Alice"

This is especially useful when a function has many optional parameters. You can skip defaults and name only what you need:

fn bdf(f, y0, t, max_order = 5, tol = 1e-6, max_newton = 10)

bdf(f, y0, t, tol = 1e-9)              // skip max_order, override tol
bdf(f, y0, t, max_newton = 50)         // skip max_order and tol

Keyword arguments work with the pipe operator. The pipe fills the first positional slot, and you can name the rest:

fn smooth(data, window = 5, method = "gaussian") { ... }

signal |> smooth(window = 10)           // signal goes to data

Rules:

  • Positional arguments must come before named arguments. foo(name = 1, x) is a compile error.
  • Function signatures are always positional — there is no keyword-only parameter syntax.
  • Backends see purely positional calls; named arguments are resolved at compile time.

Functions with Objects

Functions can accept and return objects. Stone uses width subtyping — a record with extra fields satisfies a type that requires fewer fields:

fn make_point(x, y) = { x = x, y = y}

fn translate(point, dx, dy) = {
    x = point.x + dx,
    y = point.y + dy
}

p = make_point(10, 20)
moved = translate(p, 5, -5)
// moved is {x = 15, y = 15}

When a function has type-annotated record parameters, extra fields are allowed but missing required fields are an error:

fn get_x(obj: {x: num}) = obj.x

get_x({x = 1, y = 2})    // OK — extra field y is fine
get_x({y = 2})            // ERROR: Missing field: x

Extraction works with function results:

fn min_max(xs) {
    min, max := evolve { min = xs[0], max = xs[0] } for (x in xs) {
        if (x < min) {
            min' = x
        }
        if (x > max) {
            max' = x
        }
    }
    { min = min, max = max }
}

min, max := min_max([3, 1, 4, 1, 5, 9])

Functions with Loops

Functions can contain loops:

fn sum(xs) {
    total = evolve (total = 0) for (x in xs) {
        total' = total + x
    }
    total
}

fn count_positive(xs) {
    count = evolve (count = 0) for (x in xs) {
        if (x > 0) {
            count' = count + 1
        }
    }
    count
}

Functions in Mapping

Functions are commonly used with indexed mapping:

fn square(x) = x * x

xs = [1, 2, 3, 4, 5]
squares[i] = square(xs[i])
// squares is [1, 4, 9, 16, 25]
fn normalize(x, min, max) = (x - min) / (max - min)

values = [10, 20, 30, 40, 50]
normalized[i] = normalize(values[i], 10, 50)
// normalized is [0, 0.25, 0.5, 0.75, 1]

No Arguments

Functions with no arguments still need parentheses:

fn pi() = 3.14159
fn newline() = "\n"

area = pi() * r * r

Return Behavior

In most cases, the last expression in a block function is its return value—no return keyword needed.

For early exit, use return(expr) with parentheses. return without parentheses is not valid:

fn divide(a, b) {
    if (b == 0) { return(0) }    // early exit
    a / b                         // last expression is the return value
}

Inside a value-producing conditional, return() follows the conditional semantics described in Conditionals.

fn classify(x) {
    category = if (x > 0) {
        "positive"
    } else {
        "negative"
    }
    // Execution continues here
    print("Classified as: " + category)
    category
}

Early Return

Use return(expr) to exit a function early:

fn find_index(xs, target) {
    idx, found := evolve { idx = -1, found = false, i = 0 } for (x in xs) {
        if (x == target) {
            idx' = i
            found' = true
            break
        }
        i' = i + 1
    }
    if (found) { return(idx) }
    -1
}

Examples

Computing factorial:

fn factorial(n) {
    result := evolve { result = 1, i = 1 } while (i <= n) {
        result' = result * i
        i' = i + 1
    }
    result
}

Checking if a value is in a list:

fn contains(xs, target) {
    found = evolve (found = false) for (x in xs) {
        if (x == target) {
            found' = true
            break
        }
    }
    found
}

Computing the mean:

fn mean(xs) {
    sum, count := evolve { sum = 0, count = 0 } for (x in xs) {
        sum' = sum + x
        count' = count + 1
    }
    sum / count
}

Vector operations:

import sqrt from math
fn dot_product(a, b) {
    sum := evolve { sum = 0, i = 0 } for (x in a) {
        sum' = sum + x * b[i]
        i' = i + 1
    }
    sum
}

fn magnitude(v) {
    sum_squares = evolve (sum_squares = 0) for (x in v) {
        sum_squares' = sum_squares + x * x
    }
    sqrt(sum_squares)
}

String utilities:

fn repeat(s, n) {
    result := evolve { result = "", i = 0 } while (i < n) {
        result' = result + s
        i' = i + 1
    }
    result
}

stars = repeat("*", 5)  // "*****"

Filtering with a predicate:

fn count_where(xs, predicate) {
    count = evolve (count = 0) for (x in xs) {
        if (predicate(x)) {
            count' = count + 1
        }
    }
    count
}

fn is_positive(x) = x > 0

positives = count_where([-1, 2, -3, 4, 5], is_positive)
// positives is 3