Flows
Flows control how expressions execute. They're one of Hot's most powerful features, enabling parallel execution, conditional branching, and data pipelines.
Flow Types
| Flow | Description |
|---|---|
serial | Execute sequentially (default) |
parallel | Execute concurrently |
cond | First matching branch wins |
cond-all | All matching branches execute |
match | Pattern match on types |
match-all | All matching type patterns execute |
|> | Pipe data through transformations |
Two Ways to Use Flows
Every flow can be used in two ways:
1. As a function modifier — defines the entire function's execution model:
fetch-all-modifier fn parallel (id: Str): Map {
user api-get(`/users/${id}`)
orders api-get(`/orders/${id}`)
}
2. Inline within any expression — for local control flow:
process-inline fn (id: Str): Map {
// Inline parallel block
data parallel {
user api-get(`/users/${id}`)
orders api-get(`/orders/${id}`)
}
// Inline conditional
status cond {
is-empty(data.orders) => { "new-customer" }
=> { "returning-customer" }
}
{data: data, status: status}
}
The examples below show both approaches.
Serial Flow (Default)
Without a flow specifier, functions execute sequentially, returning the last value:
process fn (x: Int): Int {
doubled mul(x, 2) // First
tripled mul(x, 3) // Second
add(doubled, tripled) // Third - returned
}
process(5)
→ 25
You can make it explicit with serial:
process-explicit fn serial (x: Int): Int {
doubled mul(x, 2)
tripled mul(x, 3)
add(doubled, tripled)
}
Parallel Flow
Execute expressions concurrently with parallel:
fetch-all fn parallel (user-id: Str): Map {
user api-get(`/users/${user-id}`)
orders api-get(`/orders/${user-id}`)
preferences api-get(`/prefs/${user-id}`)
}
fetch-all("user-123")
→ {user: {...}, orders: {...}, preferences: {...}}
This is much faster than sequential execution when operations are independent.
When to Use Parallel
Use parallel when:
- Operations involve I/O (HTTP, database, file system)
- You want to speed up multiple slow operations
Hot automatically analyzes dependencies and executes in "levels" - variables at the same level run concurrently, but levels execute in order:
// Parallel with automatic dependency resolution
enrich-user fn parallel (id: Str): Map {
user ::api/get-user(id) // Level 0
orders ::api/get-orders(user.id) // Level 1 (depends on user)
prefs ::api/get-prefs(user.id) // Level 1 (depends on user)
summary build-summary(orders, prefs) // Level 2 (depends on orders, prefs)
}
// user runs first, then orders+prefs run in parallel, then summary
Conditional Flow
Use cond for conditional branching. The first matching condition wins:
classify fn cond (x: Int): Str {
lt(x, 0) => { "negative" }
eq(x, 0) => { "zero" }
=> { "positive" }
}
classify(-5) → "negative"
classify(0) → "zero"
classify(10) → "positive"
The => arrow separates the condition from the result. A branch without a condition is the default case.
Conditions are checked for truthiness: any value that isn't false or null is considered true. This means you can use values directly as conditions:
get-name fn cond (user: Map): Str {
user.nickname => { user.nickname } // Truthy if nickname exists and isn't null
user.name => { user.name }
=> { "Anonymous" }
}
get-name({nickname: "Bob", name: "Robert"}) → "Bob"
get-name({name: "Alice"}) → "Alice"
get-name({}) → "Anonymous"
Multiple Conditions
grade fn cond (score: Int): Str {
gte(score, 90) => { "A" }
gte(score, 80) => { "B" }
gte(score, 70) => { "C" }
gte(score, 60) => { "D" }
=> { "F" }
}
grade(95) → "A"
grade(75) → "C"
grade(55) → "F"
Named Branches
Give branches names for debugging or result identification:
categorize fn cond (x: Int): Str {
lt(x, 0) => negative { "negative" }
eq(x, 0) => zero { "zero" }
=> positive { "positive" }
}
categorize(-5) → "negative"
categorize(0) → "zero"
categorize(5) → "positive"
Complex Conditions
Any expression that returns a boolean works:
validate fn cond (user: Map): Result {
is-null(user.email) => { err("Email required") }
not(valid-email(user.email)) => { err("Invalid email") }
lt(len(user.password), 8) => { err("Password too short") }
=> { ok(user) }
}
Conditional-All Flow
Use cond-all when you want all matching branches to execute:
apply-discounts fn cond-all (order: Map): Map {
order.is-member => member { "10% member discount" }
gt(order.total, 100) => shipping { "Free shipping" }
order.has-coupon => coupon { "Coupon applied" }
=> standard { "Standard pricing" }
}
apply-discounts({is-member: true, total: 150, has-coupon: true})
→ {member: "10% member discount", shipping: "Free shipping", coupon: "Coupon applied"}
Use Cases for cond-all
- Applying multiple rules/transformations
- Collecting all matching categories
- Running side effects for all matches
- Validation that collects all errors
validate-all fn cond-all (user: Map): Map {
is-null(user.name) => name { "Name required" }
is-null(user.email) => email { "Email required" }
lt(length(user.password), 8) => password { "Password too short" }
// Returns ALL validation errors as a map, not just the first
}
validate-all({name: null, email: null, password: "short"})
→ {name: "Name required", email: "Email required", password: "Password too short"}
Match Flow
Use match to pattern match on types. The first matching pattern wins:
Direction enum {
Up,
Down,
Left,
Right
}
describe match fn (dir: Direction): Str {
Direction.Up => "Going up"
Direction.Down => "Going down"
Direction.Left => "Going left"
Direction.Right => "Going right"
}
up Direction.Up
describe(up) // → "Going up"
Inline Match
Use match inline to branch on a value:
result get-result()
message match result {
Result.Ok => `Success: ${result}`
Result.Err => `Error: ${result}`
}
Type-Level Matching
Match any variant of a type:
// Matches any Result variant
is-result match value {
Result => true
=> false
}
Match Functions with Extra Arguments
Match flow functions can have additional arguments beyond the matched value:
Direction enum {
Up,
Down,
Left,
Right
}
describe-direction match fn (dir, prefix: Str): Str {
Direction.Up => concat(prefix, " going up")
Direction.Down => concat(prefix, " going down")
Direction.Left => concat(prefix, " going left")
Direction.Right => concat(prefix, " going right")
}
describe-direction(Direction.Up, "We are")
→ "We are going up"
Match-All Flow
Use match-all when you want all matching patterns to execute:
Trait enum {
Flying,
Swimming,
Walking
}
describe-traits match-all fn (trait): Str {
Trait.Flying => "Can fly"
Trait.Swimming => "Can swim"
Trait.Walking => "Can walk"
}
describe-traits(Trait.Flying)
→ {"Trait.Flying": "Can fly"}
Result Modifiers for Match
Like other flows, match supports result modifiers:
// match defaults to |one (first match)
// match-all defaults to |map (keyed by branch)
// Get results as vector
traits match-all|vec creature {
Trait.Flying => "flies"
Trait.Swimming => "swims"
}
Pipe Flow
The pipe |> chains transformations. The piped value becomes the first argument of the next function:
result 5 |> add(2) |> mul(3)
// 5 |> add(2) → add(5, 2) → 7
// 7 |> mul(3) → mul(7, 3) → 21
Collection Pipelines
Pipes shine with collection operations:
result [1, 2, 3, 4, 5]
|> map((x) { mul(x, 2) }) // [2, 4, 6, 8, 10]
|> filter((x) { gt(x, 5) }) // [6, 8, 10]
|> reduce((a, x) { add(a, x) }, 0) // 24
Pipes with Lambdas
Insert custom transformations:
result 10
|> (x) { mul(x, 2) } // 20
|> (x) { add(x, 5) } // 25
Real-World Pipeline
process-users fn (users: Vec<Map>): Vec<Str> {
users
|> filter((u) { u.active })
|> map((u) { u.email })
|> filter((e) { ends-with(e, "@company.com") })
|> map((e) { lowercase(e) })
}
Combining Flows
Use flows within function bodies:
process-order fn (order: Map): Result {
// Validate first (conditional)
validation cond {
is-null(order.items) => { err("No items") }
eq(length(order.items), 0) => { err("Empty order") }
=> { ok(order) }
}
// Then enrich in parallel (returns a map)
enriched parallel {
customer fetch-customer(order.customer-id)
inventory check-inventory(order.items)
shipping calculate-shipping(order)
}
// Return combined result (access via enriched.*)
ok({
order: order,
customer: enriched.customer,
inventory: enriched.inventory,
shipping: enriched.shipping
})
}
Flow vs Function
Flows are part of functions, not standalone. The fn keyword combined with a flow creates a function:
// Function with conditional flow
classify fn cond (x: Int): Str {
lt(x, 0) => { "negative" }
=> { "positive" }
}
// Standalone flow (inside a function body)
process fn (data: Map): Result {
result cond {
is-null(data) => { err("No data") }
=> { ok(data) }
}
result
}
Result Modifiers
Result modifiers control how a flow collects its results. Append them to any flow with |:
| Modifier | Description |
|---|---|
|one | Return the last/winning value |
|vec | Return all results as a vector |
|map | Return all results as a map (keyed by variable/branch name) |
Default Result Modifiers
Each flow type has a sensible default:
| Flow | Default | Behavior |
|---|---|---|
serial | |one | Returns the last expression's value |
parallel | |map | Returns all results as a map keyed by variable name |
cond | |one | Returns the matching branch's value |
cond-all | |map | Returns all matching results as a map keyed by branch name |
match | |one | Returns the matching arm's value |
match-all | |map | Returns all matching results as a map keyed by arm |
|> (pipe) | |one | Returns the final piped value |
Explicit Result Modifiers
Override the default when you need different results:
// Parallel defaults to |map
data parallel {
user ::api/get-user(id)
orders ::api/get-orders(id)
prefs ::api/get-prefs(id)
}
// => {user: ..., orders: ..., prefs: ...}
// Parallel with |one - get only the last result
last-value parallel|one {
a fetch-a()
b fetch-b()
c fetch-c()
}
// => <c-result>
// Parallel with |vec - get results as a vector
values parallel|vec {
a fetch-a()
b fetch-b()
c fetch-c()
}
// => [<a-result>, <b-result>, <c-result>]
// cond-all defaults to |map
results cond-all {
check-a() => a { "A passed" }
check-b() => b { "B passed" }
check-c() => c { "C passed" }
}
// => {a: "A passed", c: "C passed"} (if A and C pass)
// cond-all with |vec - collect as vector (no branch names)
discounts cond-all|vec {
is-member => { "10% off" }
gt(total, 100) => { "Free shipping" }
has-coupon => { "Coupon applied" }
}
// => ["10% off", "Free shipping"] (if member with $150 order, no coupon)
// Pipe with |vec - collect all intermediate values
steps 5 |> add(2) |> mul(3) |vec
// => [5, 7, 21]
Summary
| Flow | Use When |
|---|---|
serial | Sequential execution (default) |
parallel | Concurrent execution with automatic dependency resolution |
cond | Choose one branch based on conditions |
cond-all | Execute all matching branches |
match | Pattern match on types |
match-all | Execute all matching type patterns |
|> | Chain transformations on data |
Flows make Hot's execution model explicit. You always know whether operations run in sequence, parallel, or conditionally.