ESC
Type to search...
S
Soli Docs

Functions

Function declarations, lambdas, closures, higher-order functions, default parameters, and named parameters in Soli.

Function Declaration

fn / def

Declares a function with optional parameters and return type. def is an alias for fn (Ruby-style).

# No parameters, no return
def say_hello end

# With parameters
def greet(name: String)
    print("Hello, " + name + "!")
end

# With return value (implicit return)
def add(a: Int, b: Int) -> Int
    a + b
end

# Void function
def log_message(msg: String)
    print("[LOG] " + msg)
end

# Early return
def absolute(x: Int) -> Int
    if (x < 0)
        return -x
    end
    x
end

Recursive Functions

# Calculate factorial
def factorial(n: Int) -> Int
    if (n <= 1)
        return 1
    end
    n * factorial(n - 1)
end

print(factorial(5));  # 120

# Calculate Fibonacci
def fibonacci(n: Int) -> Int
    return n if n <= 1
    fibonacci(n - 1) + fibonacci(n - 2)
end

print(fibonacci(10));  # 55

# Check if a number is prime
def is_prime(n: Int) -> Bool
    if (n < 2)
        return false
    end
    if (n == 2)
        return true
    end
    if (n % 2 == 0)
        return false
    end
    let i = 3
    while (i * i <= n)
        if (n % i == 0)
            return false
        end
        i = i + 2
    end
    true
end

Higher-Order Functions

Functions can accept other functions as parameters:

# Function as parameter
def apply(x: Int, f: (Int) -> Int) -> Int
    f(x)
end

def double(x: Int) -> Int
    x * 2
end

def square(x: Int) -> Int
    x * x
end

let result = apply(5, double)   # 10
let squared = apply(5, square)  # 25

# Passing anonymous functions
def transform_array(arr: Int[], transformer: (Int) -> Int) -> Int[]
    let result = []
    for item in arr
        push(result, transformer(item))
    end
    result
end

let numbers = [1, 2, 3, 4, 5]
let doubled = transform_array(numbers, def(x) x * 2)  # [2, 4, 6, 8, 10]

Lambdas & Anonymous Functions

Soli provides four interchangeable syntax styles for creating anonymous functions. They all produce the same result — the choice is purely stylistic.

def() syntax

The most explicit form. Supports inline expressions and multi-line bodies:

# Inline expression body
let doubled = [1, 2, 3].map(def(x)
x * 2)
print(doubled);  # [2, 4, 6]

# Multi-line body with end
let process = def(x)
    let result = x * 2
    result + 1
end

print(process(5));  # 11

Pipe syntax |args| expr

A concise alternative, commonly used with collection methods:

# Single parameter
let doubled = [1, 2, 3].map(|x| x * 2)
print(doubled);  # [2, 4, 6]

# Multiple parameters
let sum = [1, 2, 3].reduce(0, |acc, x| acc + x)
print(sum);  # 6

# With collection methods
let names = ["alice", "bob", "charlie"]
let upper = names.map(|name| name.upcase())
print(upper);  # ["ALICE", "BOB", "CHARLIE"]

Empty pipe || expr

For zero-parameter lambdas:

# Zero-parameter lambda
let greet = || "Hello, world!"
print(greet())  # "Hello, world!"

# Useful for deferred execution
let lazy_value = || expensive_computation
# ... later ...
let result = lazy_value()

Stabby arrow -> expr

Another zero-parameter shorthand:

# Arrow lambda (no parameters)
let greet = -> "Hello, world!"
print(greet());  # "Hello, world!"

# Equivalent to || syntax
let a = || 42
let b = -> 42
print(a());  # 42
print(b());  # 42

Body styles

All lambda syntaxes support three body formats:

# 1. Inline expression (single expression)
let add = def(a, b)
a + b
let mul = |a, b| a * b

# 2. Brace-delimited block
let add = def(a, b) { a + b }
let mul = |a, b| { a * b }

# 3. Multi-line with end
let add = def(a, b)
    a + b
end

let mul = |a, b|
    a * b
end

# Multi-line lambdas as method arguments
let doubled = [1, 2, 3].map(|x|
    let result = x * 2
    result
end)
let adults = users.filter(def(u)
    let age = u["age"]
    age >= 18
end);

Trailing block syntax

When a method's last argument is a lambda, you can pass it as a trailing block — outside the parentheses, terminated by end:

# Trailing block — no parentheses needed
[1, 2, 3].map |x| x * 2 end          # [2, 4, 6]
[1, 2, 3].filter |x| x > 1 end       # [2, 3]
3.times |i| print(i) end              # 0 1 2

# Equivalent parenthesized form
[1, 2, 3].map(|x| x * 2)             # [2, 4, 6]
[1, 2, 3].filter(|x| x > 1)          # [2, 3]
3.times(|i| print(i))                 # 0 1 2

# Methods with extra arguments — trailing block after parens
1.upto(5) |i| print(i) end           # 1 2 3 4 5
numbers.reduce(0) |acc, x| acc + x end

# Multi-line trailing block
[1, 2, 3].map |x|
    let doubled = x * 2
    doubled + 1
end
# [3, 5, 7]

Practical examples

Lambdas shine when used with collection methods:

let users = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 17},
    {"name": "Charlie", "age": 25}
]

# Filter adults
let adults = users.filter(|u| u["age"] >= 18)
# Extract names
let names = adults.map(|u| u["name"])
print(names);  # ["Alice", "Charlie"]

# Find first match
let bob = users.find(|u| u["name"] == "Bob")
print(bob);  # {"name": "Bob", "age": 17}

# Chaining
let result = [1, 2, 3, 4, 5, 6]
    .filter(|x| x % 2 == 0)
    .map(|x| x * 10)
print(result);  # [20, 40, 60]

Closures

Functions that capture variables from their enclosing scope:

# Counter using closure
def make_counter -> () -> Int
    let count = 0
    def counter -> Int
        count = count + 1
        count
    end
    counter
end

let counter1 = make_counter()
let counter2 = make_counter
print(counter1());  # 1
print(counter1());  # 2
print(counter1());  # 3

print(counter2());  # 1
print(counter2());  # 2

# Closure capturing variables
def make_greeter(greeting: String) -> (String) -> String
    def greet(name: String) -> String
        greeting + ", " + name + "!"
    end
    greet
end

let say_hello = make_greeter("Hello")
let say_hola = make_greeter("Hola")
print(say_hello("Alice"));  # "Hello, Alice!"
print(say_hola("Bob"));     # "Hola, Bob!"

Default Parameters

def greet(name: String, greeting: String = "Hello") -> String
    greeting + ", " + name + "!"
end

print(greet("Alice"));              # "Hello, Alice!"
print(greet("Bob", "Hi"));          # "Hi, Bob!"
print(greet("Charlie", "Welcome")); # "Welcome, Charlie!"

# Optional parameters
def create_user(name: String, email: String = null, role: String = "user") -> Hash
    let user = {"name": name, "role": role}
    if (email != null)
        user["email"] = email
    end
    user
end

let user1 = create_user("Alice")
let user2 = create_user("Bob", "[email protected]")
let user3 = create_user("Charlie", "[email protected]", "admin");

Named Parameters

Call functions with named parameters using colon syntax for improved readability:

def configure(host: String = "localhost", port: Int = 8080, debug: Bool = false) -> Hash
    {"host": host, "port": port, "debug": debug}
end

# All named parameters
let config1 = configure(host: "example.com", port: 3000, debug: true)
# {"host": "example.com", "port": 3000, "debug": true}

# Mixed positional and named
let config2 = configure("api.example.com", debug: true)
# {"host": "api.example.com", "port": 8080, "debug": true}

# Only some named (defaults fill rest)
let config3 = configure(port: 443)
# {"host": "localhost", "port": 443, "debug": false}

Rules

  • Named arguments use colon syntax: parameter_name: value
  • Named arguments must come after all positional arguments
  • Duplicate named arguments cause a runtime error
  • Unknown parameter names cause a runtime error

Constructor Named Parameters

Named parameters also work with class constructors:

class User
    name: String
    age: Int
    role: String

    new(name: String = "Guest", age: Int = 0, role: String = "user")
        this.name = name
        this.age = age
        this.role = role
    end
end

let user1 = new User(name: "Alice", age: 30, role: "admin")
let user2 = new User("Bob", age: 25)
let user3 = new User(name: "Charlie", role: "moderator");

Implicit Returns

The last expression in a function body is automatically returned, without needing the return keyword. This applies to all functions, methods, and lambdas.

# Simple implicit return
def add(a: Int, b: Int) -> Int
    a + b
end
print(add(2, 3))  # 5

# Works with if/else expressions
def abs(x: Int) -> Int
    if x < 0 then -x else x end
end
print(abs(-5))  # 5

# Works with lambdas
let doubled = [1, 2, 3].map(def(x) x * 2)
print(doubled)  # [2, 4, 6]

# Works with closures
def make_adder(n: Int) -> (Int) -> Int
    def(x) x + n
end
let add5 = make_adder(5)
print(add5(3))  # 8

# Note: `let` as last statement returns null
def nothing
    let x = 1
end
print(nothing())  # null

When to use explicit return

Use return for early exits from a function — when you want to exit before reaching the last expression. For the final expression in a function body, omit return for cleaner code.

Early Return

return

Exits a function early and returns a value. Use return when you need to exit before the last expression.

def find_first_even(numbers: Int[]) -> Int
    for (n in numbers)
        if (n % 2 == 0)
            return n;  # Early return
        end
    end
    -1  # Implicit return: not found
end

let result = find_first_even([1, 3, 5, 4, 7]);  # 4