← All guided builds

Guided build · 05

Build reusable domain logic

Extract repeated calculations into inferred named functions, then model a closed order lifecycle with payload-bearing choices and exhaustive matching.

You will finish with: A reusable billing toolkit and a contract-checked order lifecycle with explicit states, payloads, guards, and summaries.

25 minIntermediateSource checked for Grid 0.61.0Reviewed 2026-08-25
Related canonical example27-named-functions.grid
Get Grid
Starter modelbilling-toolkit-starter.gridThe job ledger and expected output bindings, ready for named function definitions.

Watch it in Grid

See the workflow before you build it.

Follow the finished interaction, then use the written steps below to build and inspect it yourself.

On this page

What you will build

First, a billing toolkit whose functions work across scalar values, open records, and collections. Then, a closed order lifecycle whose possible states and payloads are checked by the compiler.

1. Read the function boundary

Open the starter above or the canonical Named Function Toolkit:

identity(value) = value
net_of_fee(amount, fee_rate) = amount * (1 - fee_rate)
rush_premium(job) = job.amount * job.rush_rate
floor_at(value, minimum) = MAX(value, minimum)
same_shape(values) = MAP(values, value => value)
billed_total(jobs) = REDUCE(0, jobs, (acc, job) => acc + job.amount)

Grid infers one generalized signature for each name. identity can accept unrelated value kinds at different call sites; rush_premium requires the fields it reads while allowing extra record fields.

Checkpoint: B1 = 42, B2 = "invoice-run", C1 = 4,576, C2 = 740, and C3 = 0.

2. Reuse functions over rows and collections

D1 = MAP(Jobs, job => net_of_fee(job.amount, A1))
D2 = MAP(Jobs, job => rush_premium(job))
D3 IS currency = billed_total(Jobs)
D4 IS currency = REDUCE(0, D2, (acc, value) => acc + value)
D5 = MAP(D1, value => floor_at(value - A2, 0))

Checkpoint: D1 = [4576, 2728, 6512], D2 = [1300, 0, 740], D3 = 15,700, D4 = 2,040, and D5 = [4176, 2328, 6112].

same_shape also preserves both a simple array and the Jobs collection:

H1 = same_shape([10, 20, 30])
H2 = same_shape(Jobs)

Checkpoint: H1 = [10, 20, 30]; H2 retains the three original job records and their extra fields.

3. Verify the composed result

L1 IS currency = D3 * (1 - A1) + D4
L2 = L1 > 12000 THEN "healthy-month" ELSE "lean-month"
L3 = TEXT(L1, "0.00")
L4 = `net={L3} status={L2}`

Checkpoint: L1 = 15,856, L2 = "healthy-month", L3 = "15856.00", and L4 = "net=15856.00 status=healthy-month".

4. Declare a closed lifecycle

Open the canonical Algebraic Choice Lifecycle:

CHOICE Order =
  Draft |
  Placed(total: number) |
  Shipped(total: number, transit_days: number) |
  Cancelled(reason: string)

Each value is exactly one constructor with a checked payload:

A1 = Order.Placed(1840)
A2 = Order.Shipped(2620, 3)
A3 = Order.Cancelled("payment declined")
A4 = Order.Draft()

5. Match every state

B1 = MATCH(
  A2,
  Order.Draft() -> "draft",
  Order.Placed(total) -> "placed " & TEXT(total, "0"),
  Order.Shipped(total, transit_days) -> "shipped " & TEXT(transit_days, "0") & "d",
  Order.Cancelled(reason) -> "cancelled: " & reason
)

Checkpoint: B1 = "shipped 3d", B2 = "priority-pick", and B3 = 2,620.

A guarded Placed arm does not make the match exhaustive; its unguarded companion handles every other placed order. Temporarily remove Order.Placed(_) -> "standard-pick" from B2.

Checkpoint: compilation reports a non-exhaustive MATCH and identifies the missing unguarded Placed case. Restore the arm.

6. Keep finite recursive state as data

The Approval choice stores a finite escalation chain. The nested matches fold it into a final approver and hop count.

Checkpoint: D1 = "finance-chief", D2 = 2, and E1 = "final_approver=finance-chief escalation_hops=2 pick=priority-pick".

Try it yourself

In Named Function Toolkit, add:

priority_jobs(jobs, threshold) = FILTER(jobs, job => job.amount > threshold)
M1 IS currency = billed_total(priority_jobs(Jobs, 5000))

Checkpoint: M1 = 12,600 because Meridian and Harbor exceed the threshold.

In Algebraic Choice Lifecycle, change A1 to Order.Placed(1200).

Checkpoint: B2 = "standard-pick" and E1 ends with pick=standard-pick.

You are done when

  • One named function is reused without duplicating its formula.
  • A function accepts records with required and additional fields.
  • Every Order constructor has an unguarded match path.
  • Changing a constructor payload updates the dependent classification and summary.
Build statusReached the expected checkpoint?