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
    if (n <= 1)
        return n;
    end
    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);

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) { -x } else { x }
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