March Docs

Task

Task module: Elixir-style lightweight async tasks for one-off concurrent work.

Tasks provide a simple interface for running functions asynchronously without the ceremony of defining a full actor. They build on the task_spawn / task_await interpreter primitives (Phase 1: single-threaded cooperative scheduler — tasks execute eagerly at spawn time).

Basic usage:

march> let t = Task.async(fn () -> 21 * 2)
march> Task.await(t)  -- Ok(value) or Err(reason)
Ok(42)

Parallel map (each element is Ok(v) or Err(reason), in original order):

march> Task.async_stream([1, 2, 3], fn n -> n * n)
[Ok(1), Ok(4), Ok(9)]

Multiple tasks:

let t1 = Task.async(fn () -> fetch_user(1))
let t2 = Task.async(fn () -> fetch_user(2))
let [r1, r2] = Task.await_many([t1, t2])

Note on overloading: March does not support function overloading. Use await_ms/await_many_ms for explicit timeout variants. Use async_stream_n for a concurrency-capped parallel map.

Functions

fnall_settledall_settled(tasks)#

Wait for all tasks to finish, collecting every result regardless of failure.

Unlike await_many, all_settled does not short-circuit on error. Every element of the returned list is Ok(v) or Err(reason).

march> let tasks = List.map([1, 2, 3], fn x -> Task.async(fn () -> x * 10))
march> Task.all_settled(tasks)
[Ok(10), Ok(20), Ok(30)]
fnanyany(tasks)#

Return the first Ok result; if all tasks fail, return Err(List(reasons)).

Waits for the first success and cancels the remaining tasks. If every task returns Err, collects all reasons and returns Err(reasons_list).

In Phase 1 (interpreter), tasks run eagerly in spawn order; "first Ok" is the first Ok in list order.

let tasks = List.map(endpoints, fn ep -> Task.async(fn () -> fetch(ep)))
Task.any(tasks)  -- first endpoint that responds successfully

With no tasks there is no reason to collect, so the error list is empty:

march> Task.any([])
Err([])
fnasyncasync(f)#

Spawn f as a lightweight async task. f must be a zero-argument function: fn () -> result. Returns a Task handle that can be passed to Task.await.

march> let t = Task.async(fn () -> 1 + 1)
march> Task.await(t)
Ok(2)
fnasync_streamasync_stream(list, f)#

Run f on each element of list concurrently, returning results in original order. Equivalent to a parallel Enum.map. Each result is Ok(v) or Err(reason).

march> Task.async_stream([1, 2, 3], fn n -> n * n)
[Ok(1), Ok(4), Ok(9)]
fnasync_stream_nasync_stream_n(list, f, _max_concurrency)#

Run f on each element concurrently with a cap on parallel tasks. max_concurrency limits how many tasks run at once (advisory in Phase 1). Returns results in original order; each element is Ok(v) or Err(reason).

march> Task.async_stream_n([1, 2, 3], fn n -> n + 1, 2)
[Ok(2), Ok(3), Ok(4)]
fnawaitawait(task)#

Await the result of a task with a default 5-second timeout. Returns Ok(result) or Err(reason).

march> let t = Task.async(fn () -> 42)
march> Task.await(t)
Ok(42)
fnawait_manyawait_many(tasks)#

Await all tasks, returning results in the same order as the input list. Each element is Ok(v) or Err(reason). Uses a default 5-second timeout per task.

march> let t1 = Task.async(fn () -> 1)
march> let t2 = Task.async(fn () -> 2)
march> Task.await_many([t1, t2])
[Ok(1), Ok(2)]
fnawait_many_msawait_many_ms(tasks, _timeout_ms)#

Await all tasks with an explicit timeout in milliseconds. Returns results in the same order as the input list. Each element is Ok(v) or Err(reason). As with await_ms, the timeout is accepted for API compatibility and not yet enforced.

Examples

march> Task.await_many_ms([Task.async(fn () -> 1), Task.async(fn () -> 2)], 1000)
[Ok(1), Ok(2)]
march> Task.await_many_ms([], 1000)
[]
fnawait_msawait_ms(task, _timeout_ms)#

Await the result of a task with an explicit timeout in milliseconds. Returns Ok(result) or Err(reason). timeout_ms is accepted for API compatibility; enforcement requires the async runtime (Phase 2+).

let t = Task.async(fn () -> slow_query())
Task.await_ms(t, 10000)  -- wait up to 10 seconds

Examples

march> Task.await_ms(Task.async(fn () -> 5 * 5), 1000)
Ok(25)
fnawait_unwrapawait_unwrap(task)#

Unwrap the result of a task, raising on error. Returns the result value directly (not wrapped in Ok). Panics if the task failed.

march> let t = Task.async(fn () -> 42)
march> Task.await_unwrap(t)
42
fnracerace(tasks)#

Return the result of the first task that completes; cancel the rest.

In Phase 1 (interpreter), all tasks have already run eagerly at spawn time, so race returns the first task's result and marks the remainder cancelled. In Phase 2+ (multi-core runtime), the first task to actually finish wins and the others are cancelled via their shared cancel token.

Returns Err("race: empty task list") if the list is empty.

let t1 = Task.async(fn () -> slow_query())
let t2 = Task.async(fn () -> cached_query())
Task.race([t1, t2])   -- whichever finishes first

march> Task.race([])
Err("race: empty task list")
fnscopescope(f)#

Run f inside a structured concurrency scope.

Any tasks spawned with Task.async inside f that are still pending when f returns (or raises) are cancelled automatically.

In Phase 1 (interpreter), all tasks complete at spawn time, so this is equivalent to calling f directly. In Phase 2+ (multi-core runtime), a shared cancel token is injected and all in-flight tasks are cancelled on scope exit.

Returns f's result unchanged (it is not wrapped in Ok); a panic in f propagates to the caller.

Task.scope(fn () ->
  let t1 = Task.async(fn () -> fetch_a())
  let t2 = Task.async(fn () -> fetch_b())
  [Task.await_unwrap(t1), Task.await_unwrap(t2)]
)

march> Task.scope(fn () -> Task.await_unwrap(Task.async(fn () -> 6 * 7)))
42