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.getorfs.read) gives back that call'sOk/Error— match it as you always would. - An arm that faults (a bug — an index out of range,
a nil field) gives an
Errorcarrying 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 the
picture changes: fs/http calls run on an
offload pool, so under wait_all/map_all the
waits genuinely overlap.
scua --io=async --allow-net=api.example.com fetch_all.scua
-- with --io=async, all of these are in flight at once and waited together
let pages = map_all(urls, fn(u) return http.get(u) end, { concurrency = 8 })
--io=async is an optimization flag, not a
requirement: the results are identical either way — ordered,
all-settled — only the wall-clock differs. 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.