Skip to content

Functions

Functions are the core building block in Almide. Every function has explicit parameter types and a return type. The body is a single expression after =.

fn add(a: Int, b: Int) -> Int = a + b
fn greet(name: String) -> String = "Hello, ${name}!"

Key rules:

  • Parameter types are required
  • Return type is required
  • The body follows = (not {})
  • The last expression is the return value (no return keyword)

Use a block { } for multiple statements. The last expression in the block is the return value:

fn classify(n: Int) -> String = {
let abs_n = int.abs(n)
let label = if abs_n > 100 then "large" else "small"
"${label}: ${int.to_string(n)}"
}

Functions with side effects (I/O, network, randomness) must be marked effect fn:

effect fn save(path: String, content: String) -> Result[Unit, String] = {
fs.write(path, content)!
ok(())
}

The effect modifier enforces a strict boundary:

  • Calling an effect fn from a non-effect function is a compile error
  • effect fn typically returns Result[T, E]
  • Use the ! operator to unwrap Result values and propagate errors explicitly
fn pure() -> String =
fs.read_text("file.txt") // Compile error: cannot call effect fn
effect fn safe() -> Result[String, String] =
fs.read_text("file.txt") // OK: effect fn calling effect fn

See Error Handling for the !, ??, and ? operators.

All parameters require type annotations:

fn distance(x1: Float, y1: Float, x2: Float, y2: Float) -> Float =
float.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)

Parameters can have default values. All parameters after the first default must also have defaults:

fn connect(host: String, port: Int = 8080, secure: Bool = false) -> String =
"${host}:${int.to_string(port)}"

Arguments can be named at the call site, useful with default parameters:

connect("localhost")
connect("localhost", port: 443, secure: true)
connect(host: "localhost", secure: true) // skip port, use default

Named arguments after positional ones are allowed. Positional arguments after named ones are not:

connect("localhost", secure: true) // OK
connect(secure: true, "localhost") // Compile error

Functions ending in ? must return Bool:

fn empty?(xs: List[Int]) -> Bool = list.len(xs) == 0
fn positive?(n: Int) -> Bool = n > 0

The ? suffix is a naming convention enforced by the compiler. It cannot be used with non-Bool return types.

Lambdas (anonymous functions) use the (params) => expr syntax:

let double = (x: Int) => x * 2

Type annotations on lambda parameters are optional when inferrable:

let xs = [1, 2, 3]
let doubled = list.map(xs, (x) => x * 2)
let evens = list.filter(xs, (x) => x % 2 == 0)

Multi-parameter lambdas:

let add = (a: Int, b: Int) => a + b
list.fold([1, 2, 3], 0, (acc, x) => acc + x) // 6

Use _ for unused lambda parameters:

list.map(xs, (_) => 0) // replace every element with 0

There is only one lambda syntax. No shorthand forms like {x -> x} or x => x.

Three visibility levels control access to functions:

ModifierScopeRust equivalent
(none)Public (default)pub
modSame project onlypub(crate)
localSame file only(private)
fn public_api() -> String = "anyone can call this"
mod fn internal_helper() -> String = "project-internal"
local fn file_private() -> String = "only this file"

Visibility applies to fn, type, and let declarations:

local type Internal = { data: String }
mod let THRESHOLD = 100

When combining modifiers, the order is: [local|mod]? effect? fn

mod effect fn internal_io() -> Result[Unit, String] = {
println("internal")
ok(())
}
local effect fn private_io() -> Result[Unit, String] = {
println("private")
ok(())
}

Use _ (hole) or todo(msg) as placeholder implementations:

fn parse(text: String) -> Ast = _ // type-checked stub
fn optimize(ast: Ast) -> Ast = todo("implement later") // todo with message

The compiler accepts any expected type for holes and reports the expected type, available variables, and suggestions.

f(x, y) and x.f(y) are equivalent. The compiler resolves automatically:

string.trim(text) // canonical module.function form
text.trim() // UFCS dot-call form (equivalent)
string.split(text, ",")
text.split(",") // equivalent

The pipe operator |> passes a value as the first argument to the next function:

text |> string.trim |> string.split(",")
// equivalent to: string.split(string.trim(text), ",")

Use _ as a placeholder when the piped value is not the first argument:

xs |> list.filter(_, (x) => x > 0)

Pipe into match:

list.get(args, 1) |> match {
some(cmd) => cmd,
none => "default",
}