Light Dark

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

FlowDescription
serialExecute sequentially (default)
parallelExecute concurrently
condFirst matching branch wins
cond-allAll matching branches execute
matchPattern match on types
match-allAll 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 |:

ModifierDescription
|oneReturn the last/winning value
|vecReturn all results as a vector
|mapReturn all results as a map (keyed by variable/branch name)

Default Result Modifiers

Each flow type has a sensible default:

FlowDefaultBehavior
serial|oneReturns the last expression's value
parallel|mapReturns all results as a map keyed by variable name
cond|oneReturns the matching branch's value
cond-all|mapReturns all matching results as a map keyed by branch name
match|oneReturns the matching arm's value
match-all|mapReturns all matching results as a map keyed by arm
|> (pipe)|oneReturns 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

FlowUse When
serialSequential execution (default)
parallelConcurrent execution with automatic dependency resolution
condChoose one branch based on conditions
cond-allExecute all matching branches
matchPattern match on types
match-allExecute 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.