Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Function Basics

Learn the fundamentals of defining, calling, and using functions in Suji.

Defining Functions

Functions are created with the pipe syntax |parameters| { body } and bound to a name with =. There is no separate declaration keyword:

import std:println
import std:time

# Basic function
greet = |name| {
    "Hello, ${name}!"
}

# Multiple parameters
add = |a, b| {
    a + b
}

# No parameters
get_timestamp = || {
    time:now():epoch_ms
}

# Single expression (no braces needed)
double = |x| x * 2
square = |x| x * x

Calling Functions

import std:println

greet = |name| "Hello, ${name}!"
add = |a, b| a + b
double = |x| x * 2

# Direct call
println(greet("Alice"))  # Hello, Alice!

# Store result
message = greet("Bob")
println(message)  # Hello, Bob!

# Chain calls
result = double(add(3, 4))
println(result)  # 14

Parameters

Required Parameters

Every parameter without a default must be provided. Arity is checked at call time, so a missing argument is a runtime error, not nil:

import std:println

calculate = |x, y, operation| {
    match operation {
        "add" => x + y,
        "subtract" => x - y,
        "multiply" => x * y,
        "divide" => x / y,
        _ => 0,
    }
}

println(calculate(10, 5, "add"))  # 15

# calculate(10, 5)
# => Arity mismatch: Function expects 3 arguments, got 2

Default Values

Give a parameter a default with = in the parameter list. The default is used when the argument is omitted:

import std:println

greet = |name, title = "Mr./Ms."| "${title} ${name}"

println(greet("Alice", "Dr."))  # Dr. Alice
println(greet("Bob"))           # Mr./Ms. Bob

Passing nil explicitly passes nil — it does not fall back to the default. Handle that case with match if callers may supply nil:

import std:println

greet = |name, title| {
    label = match title {
        nil => "Mr./Ms.",
        _ => title,
    }
    "${label} ${name}"
}

println(greet("Alice", "Dr."))  # Dr. Alice
println(greet("Bob", nil))      # Mr./Ms. Bob

A bare identifier in a match pattern is a string literal, not a binding, so the fallback arm has to be _ and read the parameter directly.

Variable Arguments Pattern

There are no variadic parameters. Accept a list instead:

import std:println

sum_all = |numbers| {
    numbers::fold(0, |acc, x| acc + x)
}

println(sum_all([1, 2, 3]))       # 6
println(sum_all([1, 2, 3, 4, 5])) # 15

Return Values

Implicit Return

Last expression is automatically returned:

import std:println

add = |a, b| {
    a + b  # Returned automatically
}

println(add(3, 4))  # 7

Explicit Return

Use return for early exit:

import std:println

find_first = |list, predicate| {
    loop through list with item {
        predicate(item) && return item  # Early return
    }
    nil  # Not found
}

numbers = [1, 2, 3, 4, 5]
println(find_first(numbers, |x| x > 3))  # 4

Multiple Return Values

Return several values with return a, b, and destructure them without parentheses:

import std:println

divide_with_remainder = |a, b| {
    return (a / b)::floor(), a % b
}

quotient, remainder = divide_with_remainder(17, 5)
println("${quotient} remainder ${remainder}")  # 3 remainder 2

No Return Value

Functions can perform side effects without returning:

import std:println

log_message = |message| {
    println("[LOG] ${message}")
    # No explicit return (returns nil implicitly)
}

log_message("Application started")

Function Scope

Local Variables

Variables defined in functions are local:

import std:println

calculate = |x| {
    temp = x * 2
    result = temp + 10
    result
}

println(calculate(5))  # 20
# temp and result are not accessible here

Capturing Outer Variables

Functions can access variables from outer scope:

import std:println

multiplier = 10

scale = |x| {
    x * multiplier  # Accesses outer variable
}

println(scale(5))  # 50

Function as Values

Functions are first-class values:

import std:println

# Store in variable
double = |x| x * 2

# Store in list
operations = [
    |x| x + 1,
    |x| x * 2,
    |x| x ^ 2,
]

times_2 = operations[1]
println(times_2(5))  # 10 (second function)

# Store in map
math_ops = {
    add: |a, b| a + b,
    sub: |a, b| a - b,
    mul: |a, b| a * b,
}

println(math_ops:add(3, 4))  # 7

Common Patterns

Predicate Functions

Return boolean:

import std:println

is_even = |x| x % 2 == 0
is_positive = |x| x > 0
is_adult = |age| age >= 18

println(is_even(4))      # true
println(is_positive(-5)) # false
println(is_adult(25))    # true

Transformer Functions

Transform input to output:

import std:println

to_upper = |text| text::upper()
trim_and_lower = |text| text::trim()::lower()
add_prefix = |text| "PREFIX_${text}"

println(to_upper("hello"))  # HELLO

Validator Functions

Validate and return result:

import std:println

validate_email = |email| {
    missing = email == nil || email::length() == 0
    missing && return false, "Email is required"
    !(email ~ /^[^@]+@[^@]+$/) && return false, "Invalid email format"

    return true, nil
}

valid, error = validate_email("test@example.com")
println(valid)  # true

ok, why = validate_email("not-an-email")
println(why)  # Invalid email format

Note the explicit return true, nil at the end: a line starting with ( would be parsed as a call on the previous line’s value.

Best Practices

DO:

  • Use descriptive verb names (calculate, validate, transform)
  • Keep functions small and focused
  • Document complex behavior
  • Handle edge cases
  • Return consistent types

DON’T:

  • Create functions with too many parameters
  • Mix different concerns
  • Use cryptic abbreviations
  • Ignore error cases
  • Create side effects unexpectedly

See Also