Light Dark

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

  • fn modifies a flow to become a function (default is serial, omittable)
  • Use fn cond for conditional functions, fn parallel for concurrent execution
  • Call with no space before (: func(args)
  • Overload by arity or type
  • Use lambdas (x) { body } for inline functions
  • Mark args lazy for deferred evaluation
  • Tail-recursive functions are automatically optimized (TCO)