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
  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

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

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

numbers = [1, 2, 3, 4, 5]
doubled = transform_array(numbers, fn(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.

fn() syntax

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

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

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

print(process(5));  # 11

Pipe syntax |args| expr

A concise alternative, commonly used with collection methods:

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

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

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

Empty pipe || expr

For zero-parameter lambdas:

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

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

Stabby arrow -> expr

Another zero-parameter shorthand:

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

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

Body styles

All lambda syntaxes support three body formats:

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

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

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

mul = |a, b|
  a * b
end

# Multi-line lambdas as method arguments
doubled = [1, 2, 3].map(|x|
  result = x * 2
  result
end)
adults = users.filter(fn(u)
  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|
  doubled = x * 2
  doubled + 1
end
# [3, 5, 7]

# Ruby-style `do |params| ... end` is also accepted
[1, 2, 3].map do |x| x * 2 end                # [2, 4, 6]
{"a": 10, "b": 20}.map do |k, v| [k, v + 5] end  # {"a": 15, "b": 25}
3.times do |i| print(i) end                   # 0 1 2

Practical examples

Lambdas shine when used with collection methods:

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

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

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

# Chaining
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
  count = 0
  def counter -> Int
    count = count + 1
    count
  end
  counter
end

counter1 = make_counter()
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

say_hello = make_greeter("Hello")
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
  user = {"name": name, "role": role}
  if (email != null)
    user["email"] = email
  end
  user
end

user1 = create_user("Alice")
user2 = create_user("Bob", "[email protected]")
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
config1 = configure(host: "example.com", port: 3000, debug: true)
# {"host": "example.com", "port": 3000, "debug": true}

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

# Only some named (defaults fill rest)
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

Ruby-Style Calls Without Parentheses

You can call methods on objects without parentheses, using Ruby-style syntax:

# With parentheses (standard)
user.update(name: "Bob", age: 30);
user.save();
puts("Hello world");

# Without parentheses (Ruby-style)
user.update name: "Bob", age: 30;
user.save;
puts "Hello world";

This works for:

  • Method calls on objects with named arguments: obj.method arg: value
  • Method calls on objects without arguments: obj.method
  • Standalone function calls with named arguments: fn_name arg: value

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

user1 = new User(name: "Alice", age: 30, role: "admin")
user2 = new User("Bob", age: 25)
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
doubled = [1, 2, 3].map(fn(x) x * 2)
print(doubled)  # [2, 4, 6]

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

# Note: `let` as last statement returns null
def nothing
  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

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

Universal Methods on Function Values

Functions are first-class values, and the same universal predicates available on every other type work on them too. Useful in defensive view partials where a local might resolve to a function, a string, or be undefined.

f = fn(x) { x + 1 }

f.nil?       # false — a function value is never null
f.blank?     # false
f.present?   # true
f.class      # "Function"
f.inspect    # "<function>"

Zero-arg caveat: a zero-parameter function auto-invokes on bare access. let g = fn() { 42 }; g.class evaluates g() first, so .class sees the return value, not the function itself. Use a multi-arg function or reference g through a wrapper if you need to inspect the function value itself.