SCUA

How-to

Run many things at once

Sometimes you have a pile of independent work — fetch twenty URLs, read a hundred files, hit a dozen APIs — and doing them one at a time is pure waiting. SCUA has two built-ins for this fan-out: wait_all and map_all. No import, no async/await coloring, no future-wrangling — you hand them a list of things to do and they hand back the results.

#wait_all: run a list, wait for all of it

wait_all takes a list of zero-argument functions and returns their outcomes in order:

let results = wait_all([
    fn() return 1 + 1 end,
    fn() return "two" end,
    fn() return [3, 3, 3] end,
  ])
print(`first arm: {results[0]}`)    -- 2
print(`second arm: {results[1]}`)   -- two

Two properties matter:

  • Ordered. results[0] is the first arm's outcome, results[1] the second — the result list lines up with the arm list, regardless of which finished first.
  • All-settled. One arm failing never cancels the others. You always get a slot for every arm; there's no "first error wins" that throws the rest away.

#The result is the arm's value, as-is

Each slot holds whatever that arm produced, unchanged:

  • A plain arm gives a plain value — read it directly.
  • An arm that does I/O (like http.get or fs.read) gives back that call's Ok/Error — match it as you always would.
  • An arm that faults (a bug — an index out of range, a nil field) gives an Error carrying a tagged fault record.

So you match Ok/Error when your arms return it, and read the value directly when they don't. There's no extra wrapping layer to peel off.

#Telling a bug apart from a failure

Because a faulting arm lands as an Error, you can catch a bug in one arm without it taking down the batch. The fault Error carries a fault tag (and a message) so you can tell a genuine bug apart from an ordinary failure before you decide what to do:

let mixed = wait_all([
    fn() return "ok" end,
    fn() return error("boom") end,
  ])
match mixed[1]
  Ok(v) -> print(`unexpected ok: {v}`)
  Error(e) -> print(`arm 1 failed: fault={e.fault} ({e.message})`)
end

#map_all: the same thing over a list

When every arm does the same thing to a different input, map_all is the natural spelling. It maps a function over a list, running at most concurrency at a time (default 8), waits for all, and aligns the results with the input:

let doubled = map_all([10, 20, 30, 40], fn(x) return x * 2 end, { concurrency = 4 })
print(`doubled: {doubled[0]}, {doubled[1]}, {doubled[2]}, {doubled[3]}`)

Same semantics as wait_all: ordered, all-settled, and each slot is that call's value as-is. Iterate the results like any list:

let lengths = map_all(["a", "bb", "ccc"], fn(s) return s.len() end)
for i in 0:lengths.len() do
  print(`{i}: length {lengths[i]}`)
end

The concurrency cap is the throttle: { concurrency = 8 } keeps at most eight requests in flight, so you can fan out over a thousand URLs without opening a thousand sockets at once.

#Two gotchas

Don't put ? on it. wait_all and map_all never fail "as a whole" — there is no outer Ok/Error to short-circuit. A per-arm failure lives in that arm's slot, and the caller always waits for every arm. wait_all([...])? is a mistake.

Each arm must be a function — don't call it eagerly. wait_all runs the functions you give it; it can't run a value you already computed. Writing wait_all([http.get(u)]) calls http.get first, before wait_all ever sees it, so nothing overlaps — SCUA catches this and raises a loud error naming the arm. Wrap each call in fn() ... end:

wait_all([ fn() return http.get(a) end, fn() return http.get(b) end ])   -- right
wait_all([ http.get(a), http.get(b) ])                                   -- wrong: calls run eagerly

#Making the waits actually overlap

By default SCUA's I/O is blocking, so on the default platform the arms run one after another — you still get the same ordered, all-settled results, but no wall-clock saving. Add --io=async and a call that has to wait parks instead: fs and exec calls run on an offload pool, net reads and writes and plain http:// requests wait for their sockets on a network poller, and https:// requests run on the pool, so the arms' waits overlap. That holds at the top level of a script, inside a task started there with spawn, and inside a partition's handler:

let pages = map_all(urls, fn(u) return http.get(u) end, { concurrency = 8 })
print(len(pages))
scua --io=async --allow-net=api.example.com fetch_all.scua

Three arms that each wait 0.3 s take about 0.3 s under --io=async and about 0.9 s without it. Plain spawned tasks overlap the same way: three tasks that each make one 0.3 s round trip finish in about 0.3 s together.

A few places still wait in place, running their calls one after another exactly as they would without the flag: a wait_all nested inside another wait_all's arm, and a task spawned inside a partition's handler.

An arm may pause for wait(d) or for one of those I/O calls. Inside a partition's handler it may also ask, which is how a handler asks several partitions at once (see Asking several partitions at once). A wait_for or select in an arm doesn't run: that arm's slot holds an Error that says so and gives the arm's line, and the other arms carry on. Do those in the handler itself, before or after the map_all/wait_all.

--io=async is an optimization flag, not a requirement: the values are identical either way — ordered, all-settled — and only the wall-clock differs, along with the order in which lines printed by tasks running at the same time come out. Write the code once; add the flag when overlap pays off.

See the runnable examples/wait_all.scua for all of the above in one file.