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

Multiple Return Values

Functions can return multiple values using tuples, enabling elegant error handling and complex result patterns.

Two rules apply throughout this chapter:

  • Return several values with return a, b, which builds a tuple.
  • Destructure without parentheses: a, b = f(). A line that begins with ( is parsed as a call on the previous expression, so (a, b) = f() does not destructure.

Basic Multiple Returns

import std:println

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

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

Destructuring Returns

Basic Destructuring

import std:println

get_coordinates = || (10, 20, 30)

x, y, z = get_coordinates()
println("x:${x}, y:${y}, z:${z}")  # x:10, y:20, z:30

Ignoring Values

Use underscore to ignore unwanted values:

import std:println

# total, average, min, max
get_stats = || (100, 50, 25, 10)

total, avg, _, _ = get_stats()
println("Total: ${total}, Average: ${avg}")  # Total: 100, Average: 50

Result/Error Pattern

Common pattern for error handling:

import std:println

safe_divide = |a, b| {
    match { b == 0 => (nil, "Division by zero"), _ => (a / b, nil), }
}

result, error = safe_divide(10, 2)
match error {
    nil => println("Result: ${result}"),   # Result: 5
    _ => println("Error: ${error}"),
}

failed, why = safe_divide(10, 0)
println(why)  # Division by zero

Patterns do not bind, so the second arm is _ and reads error directly. This is the only way to react to a failure: a real division by zero terminates the program, so the check has to happen before the operation.

Validation Results

import std:println

validate_user = |user| {
    # Check multiple conditions
    user:name == nil && return false, "Name is required"
    user:name::length() < 3 && return false, "Name too short"
    user:age < 18 && return false, "Must be 18+"

    return true, nil
}

valid, error = validate_user({name: "Alice", age: 30})
match { valid => println("User is valid"), _ => println("Validation error: ${error}"), }
# User is valid

ok, problem = validate_user({name: "Al", age: 30})
println(problem)  # Name too short

Parsing Results

import std:println

parse_int = |text| {
    # Validate before converting: to_number() on junk is a runtime error
    match { text ~ /^\d+$/ => (text::to_number(), nil), _ => (nil, "Invalid integer format"), }
}

value, error = parse_int("123")
match error {
    nil => println("Parsed: ${value}"),   # Parsed: 123
    _ => println("Error: ${error}"),
}

bad, why = parse_int("12x")
println(why)  # Invalid integer format

Multiple Validation Results

import std:println

validate_form = |form| {
    errors = []
    warnings = []

    # Collect errors — get() avoids the "Key not found" error on absent keys
    email = form::get("email")
    phone = form::get("phone")

    match { email == nil => errors::push("Email required"), _ => nil, }
    match { email != nil && !(email ~ /@/) => errors::push("Invalid email"), _ => nil, }

    # Collect warnings
    match { phone == nil => warnings::push("Phone recommended"), _ => nil, }

    return errors, warnings
}

errors, warnings = validate_form({email: "test@example.com"})
println("Errors: ${errors::length()}")      # Errors: 0
println("Warnings: ${warnings::length()}")  # Warnings: 1

Complex State Returns

import std:println

process_transaction = |account, amount| {
    old_balance = account:balance
    new_balance = old_balance + amount

    status = match {
        new_balance < 0 => "overdrawn",
        new_balance < 100 => "low",
        _ => "ok",
    }

    return new_balance, old_balance, status
}

new_bal, old_bal, status = process_transaction({balance: 150}, -75)

println("Old: ${old_bal}, New: ${new_bal}, Status: ${status}")
# Old: 150, New: 75, Status: low

Option Pattern

Represent optional values:

import std:println

find_user = |id| {
    users = {
        1: {name: "Alice", age: 30},
        2: {name: "Bob", age: 25},
    }

    user = users::get(id)
    match user {
        nil => (nil, false),
        _ => (user, true),
    }
}

user, found = find_user(1)
match { found => println("Found: ${user:name}"), _ => println("User not found"), }
# Found: Alice

missing, was_found = find_user(99)
println(was_found)  # false

Suji has no Option type; a (value, found) pair is the idiom, and map::get(key) returns nil instead of raising when the key is absent.

Tuples of Structures

Tuple elements can be any value, including maps:

import std:println

get_user_with_address = || {
    person = {name: "Alice", age: 30}
    address = {city: "Boston", zip: "02101"}
    return person, address
}

user, address = get_user_with_address()
println("${user:name} lives in ${address:city}")  # Alice lives in Boston

Common Patterns

Success/Failure

import std:println

try_operation = |input| {
    success = input != nil

    match {
        success => (input::upper(), nil),
        _ => (nil, "no input given"),
    }
}

data, error = try_operation("payload")
println(data)  # PAYLOAD

nothing, problem = try_operation(nil)
println(problem)  # no input given

Before/After State

import std:println

update_counter = |current| {
    before = current
    after = current + 1
    return before, after
}

old, new = update_counter(5)
println("Changed from ${old} to ${new}")  # Changed from 5 to 6

Min/Max Pair

import std:println

get_range = |list| {
    match list::length() {
        0 => (nil, nil),
        _ => (list::min(), list::max()),
    }
}

lowest, highest = get_range([3, 1, 4, 1, 5, 9])
println("Min: ${lowest}, Max: ${highest}")  # Min: 1, Max: 9

Split Result

import std:println

partition_by_predicate = |list, pred| {
    matching = []
    non_matching = []

    loop through list with item {
        match pred(item) {
            true => { matching::push(item) },
            false => { non_matching::push(item) },
        }
    }

    return matching, non_matching
}

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens, odds = partition_by_predicate(numbers, |x| x % 2 == 0)

println("Evens: ${evens}")  # [2, 4, 6, 8, 10]
println("Odds: ${odds}")    # [1, 3, 5, 7, 9]

Best Practices

DO:

  • Use tuples for multiple related values
  • Use (value, error) pattern for operations that can fail
  • Destructure immediately when possible
  • Use underscore for unwanted values
  • Keep tuple size reasonable (2-4 elements)

DON’T:

  • Return large tuples (use maps instead)
  • Mix unrelated values in tuples
  • Forget to check error values
  • Overuse tuples when single value suffices
  • Create deeply nested tuples

Tuples Are Not Indexable

Destructuring is the only cheap way into a tuple. t[0] is a type error, and there is no t::get(0) or t::first(); the fallback is to_list(), which copies:

import std:println

function_returning_tuple = || (1, 2)

# Preferred
a, b = function_returning_tuple()
println("${a} ${b}")  # 1 2

# Works, but allocates a list
values = function_returning_tuple()::to_list()
println(values[0])  # 1

# values = function_returning_tuple()
# values[0]
# => Type error: Cannot index tuple

Tuples also support length() and to_string().

Examples

HTTP Response Pattern

import std:println

fetch_data = |url| {
    # Simulated HTTP response
    status = 200
    data = {users: ["Alice", "Bob"]}

    return status, data, nil
}

status, data, error = fetch_data("https://api.example.com")

match status {
    200 => println("Success: ${data}"),
    _ => println("Error: ${error}"),
}
# Success: {users: [Alice, Bob]}

Database Query Result

import std:println

query_users = |conditions| {
    # Simulated query
    rows = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]

    return rows, rows::length(), false
}

users, total, has_more = query_users({active: true})
println("Found ${total} users")  # Found 2 users

Parser Result

import std:println

parse_command = |input| {
    parts = input::split()

    match parts::length() {
        0 => (nil, nil, "Empty command"),
        1 => (parts[0], [], nil),
        _ => (parts[0], parts[1;], nil),
    }
}

cmd, args, error = parse_command("git commit -m message")
println(cmd)   # git
println(args)  # [commit, -m, message]

See Also