Functions
In Hot, everything is a Var/Value pair—function names are Vars, and function definitions are Values.
Defining Functions
Use fn followed by parameters and a body:
greet fn (name: Str): Str {
`Hello, ${name}!`
}
add-numbers fn (a: Int, b: Int): Int {
add(a, b)
}
greet("World") → "Hello, World!"
add-numbers(5, 3) → 8
The last expression in the body is the return value—no return keyword needed.
Functions are Flows
The fn keyword modifies a flow to turn it into a function definition. The default flow is serial, which can be omitted:
// These are equivalent:
greet-v1 fn (name: Str): Str { `Hello, ${name}!` }
greet-v2 fn serial (name: Str): Str { `Hello, ${name}!` }
You can use other flow types to change how the function body executes:
// Conditional function using cond flow
is-admin fn cond (user): Bool {
eq(user.role, "admin") => { true }
=> { false }
}
See Flows for more on serial, parallel, cond, and cond-all.
Calling Functions
Call functions with parentheses:
result-greet greet("World") // "Hello, World!"
sum add-numbers(5, 3) // 8
Qualified Calls
Call functions from other namespaces using the full path:
upper ::hot::str/uppercase("hello") // "HELLO"
data ::hot::http/get("https://api.example.com")
Or create a function alias:
uppercase ::hot::str/uppercase
result uppercase("hello") // "HELLO"
Or create a namespace alias:
::str ::hot::str
result ::str/uppercase("hello") // "HELLO"
Parameter Types
Types are optional but recommended:
// Fully typed
greet-typed fn (name: Str): Str {
`Hello, ${name}!`
}
// Untyped (accepts anything)
echo fn (x) {
x
}
// Mixed
process fn (data: Map, options): Map {
data
}
Nullable Parameters
Use ? for types that accept null (shorthand for Type | Null):
greet-titled fn (name: Str, title: Str?): Str {
if(title,
`Hello, ${title} ${name}!`,
`Hello, ${name}!`)
}
greet-titled("Alice", null) → "Hello, Alice!"
greet-titled("Smith", "Dr") → "Hello, Dr Smith!"
Function Overloading
Define multiple versions of a function with different parameter counts (arity):
slice fn
(coll: Vec, start: Int): Vec {
// slice from start to end (stub)
coll
},
(coll: Vec, start: Int, end: Int): Vec {
// slice from start to end (stub)
coll
}
Or different parameter types:
process-data fn
(x: Int): Str { `Integer: ${x}` },
(x: Str): Str { `String: ${x}` },
(x: Vec): Str { `Vector with ${length(x)} items` }
Hot dispatches to the correct version based on arguments.
Variadic Functions
Accept any number of arguments with ...:
concat-all fn (first-vec: Vec, ...rest: Vec): Vec {
reduce(rest, (acc, v) { concat(acc, v) }, first-vec)
}
Lambdas (Anonymous Functions)
Create inline functions with (params) { body }:
doubled map([1, 2, 3], (x) { mul(x, 2) })
sum-lambda reduce([1, 2, 3], (acc, x) { add(acc, x) }, 0)
map([1, 2, 3], (x) { mul(x, 2) }) → [2, 4, 6]
reduce([1, 2, 3], (acc, x) { add(acc, x) }, 0) → 6
Lambdas are just values—assign them to vars:
double-fn (x) { mul(x, 2) }
result-double double-fn(5) // 10
Lazy Arguments
Arguments marked lazy aren't evaluated until needed. This enables short-circuit evaluation:
if fn
cond (pred: Any, lazy then: Any): Any {
pred => { do then }
},
cond (pred: Any, lazy then: Any, lazy else: Any): Any {
pred => { do then }
=> { do else }
}
Use do to evaluate a lazy argument:
safe-access fn (data, lazy fallback) {
if(data, data, do fallback)
}
This is how if, and, and or avoid evaluating unused branches.
Metadata on Functions
Add documentation, test markers, or event handlers:
// Documentation
greet meta {doc: "Greets a user by name"}
fn (name: Str): Str {
`Hello, ${name}!`
}
// Test function
test-greet meta ["test"]
fn () {
assert-eq(greet("World"), "Hello, World!")
}
// Event handler
on-user-created meta {on-event: "user:created"}
fn (event) {
send-welcome-email(event.data.email)
}
// Scheduled function
daily-cleanup meta {schedule: "@daily"}
fn (event) {
cleanup-old-records()
}
Core Functions
These functions are available everywhere without imports. See hot-std for full documentation.
Math: add, sub, mul, div, mod, pow, round, floor, ceil, rand
Comparison: eq, ne, lt, gt, lte, gte
Logic: if, and, or, not, is-truthy
Collections (eager): map, filter, reduce, first, rest, last, length, concat, flatten, merge, keys, vals, some, all, range, sort, reverse, distinct, slice
Iterators (lazy): Iter, next, collect, for-each, take, range
Strings: uppercase, lowercase, trim, split, join, starts-with, ends-with, contains, replace
Results: ok, err, is-ok, is-err, Result
Types: Str, Int, Dec, Bool, Vec, Map, Any, Null, is-null, is-some
Tail Call Optimization (TCO)
Hot automatically optimizes tail-recursive functions, enabling stack-safe recursion for any depth.
A call is in tail position when its result is returned directly without further processing:
factorial fn cond (n: Int, acc: Int): Int {
lte(n, 1) => { acc }
=> { factorial(sub(n, 1), mul(n, acc)) }
}
factorial(5, 1) → 120
factorial(1, 1) → 1
Use the accumulator pattern to make functions tail-recursive:
// NOT tail-recursive (result passed to add, not returned)
sum fn (xs: Vec): Int {
if(is-empty(xs), 0, add(first(xs), sum(rest(xs))))
}
// Tail-recursive with accumulator (stack-safe)
sum fn (xs: Vec): Int { sum-acc(xs, 0) }
sum-acc fn cond (xs: Vec, acc: Int): Int {
is-empty(xs) => { acc }
=> { sum-acc(rest(xs), add(acc, first(xs))) }
}
Summary
fnmodifies a flow to become a function (default isserial, omittable)- Use
fn condfor conditional functions,fn parallelfor concurrent execution - Call with no space before
(:func(args) - Overload by arity or type
- Use lambdas
(x) { body }for inline functions - Mark args
lazyfor deferred evaluation - Tail-recursive functions are automatically optimized (TCO)