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