APIAsync

Async

Concurrent tasks, cancellation, and channels.

run starts a root lifetime scope. Every task owns its children; returning from a task or scope cancels unfinished children and drains their cleanup. Await work whose normal completion matters. scope gives helpers a shorter lifetime; both, all, and race own their tasks and handle application Fail. Raw tasks return ordinary values, including Results: an Err does not cancel siblings. Handle application aborts inside spawned callbacks; inherited resumptive handlers share their state cells. Cancellation is cooperative: sleeps, channel transfers, and task waits respond to it, while CPU loops need checkpoint. Blocking IO is interrupted only by cancellation-aware APIs such as Net. cancelled, cancel, and close still work after cancellation; cancellation-sensitive operations in cleanup stop immediately. Draining itself cannot be interrupted. A task that never responds to cancellation can prevent its owner from finishing. Cancellation remains the primary outcome when cleanup also fails; its suppressed failures are discarded. See the language reference under “Handlers in tasks”.

Channel

type Channel valueSource

A channel carrying values between tasks. It lives independently of the tasks that use it.

Outcome

type Outcome value    = Completed value    | CancelledSource

How a task or root body ended.

Constructors

Completed

Completed : value -> Outcome valueSource

It finished with this value.

Cancelled

Cancelled : Outcome valueSource

It was cancelled first.

Instances:

Eq value => Eq (Outcome value)
Show value => Show (Outcome value)

Task

type Task valueSource

A running or finished task that returns a value. A handle may outlive its owning scope, but does not extend the task’s lifetime.

Async

effect AsyncSource

Running inside a structured task scope. run, runOutcome, and scope handle it.

all

all : List (() ->{Fail a, e} b) ->{Fail a, Async, e} List bSource

Starts every action concurrently and returns values in input order. Empty input returns []. The first observed application Fail cancels remaining tasks; after their cleanup it is raised in the caller. A cancelled participant cancels the caller instead. Caller cancellation also drains all owned work. Caller cancellation before drain finishes takes precedence over a selected Fail. Failures are observed after the failing task’s cleanup, not at the raise. Later errors during drain are discarded. Other abort effects need local handling; a returned Err is ordinary data and does not trigger this policy.

import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))

result : Outcome (Result String (List Int))
result = Async.runOutcome { Fail.attempt { Async.all [{ 1 }, { 2 }] } }
result == Completed (Ok [1, 2])

await

await : Task a ->{Async} aSource

Waits for the task’s value. A cancelled target cancels the observing task and its descendants, including when called inside a nested scope; use awaitOutcome to choose a different response. Returned Results remain ordinary values; use Fail.fromResult explicitly to raise an error here. The spawn example shows awaiting successful work; awaitOutcome shows inspecting cancellation without propagating it.

awaitAll

awaitAll : List (Task a) ->{Async} List aSource

Waits for all task values in input order. Empty input returns []. A cancelled target cancels the observing task and drains its descendants. Returned Err values are ordinary data; use all to cancel other work on application Fail.

awaitAny

awaitAny : List (Task a) ->{Async} Maybe (Int, a)Source

Observes one task’s value and its zero-based input index. Empty input returns Nothing; a selected cancellation cancels the observing task and descendants. Selection and repeated observations follow awaitAnyOutcome.

import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))

Async.runOutcome { Async.awaitAny [Async.completed 7] } == Completed (Just (0, 7))

awaitAnyOutcome

awaitAnyOutcome : List (Task a) ->{Async} Maybe (Int, Outcome a)Source

Observes one task’s terminal outcome and its zero-based input index. Empty input returns Nothing. A selected cancellation is data. Simultaneously ready tasks may be selected in any order; repeated calls may select the same task. Waiter cancellation interrupts selection without adopting or cancelling its targets, except through an existing ownership relationship.

import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))

result = Async.runOutcome {
    task = Async.completed 42
    Async.awaitAnyOutcome [task]
    }
result == Completed (Just (0, Completed 42))

awaitOutcome

awaitOutcome : Task a ->{Async} Outcome aSource

Waits for completion and returns the value or the child’s cancellation. Repeated and concurrent observations see one stable outcome. Cancellation of the observer interrupts its wait. Waiting creates no ownership or cancellation relationship; an owned descendant still follows tree cancellation. A target’s Cancelled outcome does not cancel the observer.

import Async exposing (Outcome(..))

result = Async.runOutcome {
    gate : Async.Channel Int
    gate = Async.channel 0
    task = Async.spawn { Async.receive gate }
    Async.cancel task
    Async.awaitOutcome task
    }
result == Completed Cancelled

both

both : (() ->{Fail a, e} b) -> (() ->{Fail a, e} c) ->{Fail a, Async, e} (b, c)Source

Starts two actions concurrently and returns their values as a pair. Each may return a different type, but they report the same Fail error type. Cancellation, first-observed failure, draining, and returned Err data follow all.

import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))

result : Outcome (Result String (Int, String))
result = Async.runOutcome { Fail.attempt { Async.both { 1 } { "two" } } }
result == Completed (Ok (1, "two"))

cancel

cancel : Task a ->{Async} ()Source

Requests the task’s cancellation, and its children’s. It does not cancel the caller or siblings. Returns without waiting for cleanup; awaitOutcome waits for that cleanup and inspects cancellation, as in its example.

cancelled

cancelled : () ->{Async} BoolSource

Whether the current task’s active lifetime scope has been cancelled. It remains usable during cancellation cleanup; it does not stop the task.

import Async exposing (Outcome(..))

Async.runOutcome { Async.cancelled() } == Completed False

channel

channel : Int ->{Async} Channel aSource

A new channel buffering up to capacity values. Zero or less makes it unbuffered: each send waits for a receiver. Channels are independent of task ownership and may outlive their creating scope.

import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))

result = Async.runOutcome {
    messages = Async.channel 1
    ignore (Async.send messages 42)
    Async.close messages
    first = Async.receive messages
    next = Async.receive messages
    rejected = Async.send messages 7
    (first, next, rejected)
    }
result == Completed (Just 42, Nothing, False)

checkpoint

checkpoint : () ->{Async} ()Source

Leaves the current task if it has been cancelled, running cleanup on the way; otherwise does nothing. Long computations call it to respond to cancellation.

close

close : Channel a ->{Async} ()Source

Closes the channel, waking blocked senders and receivers. Closing it again does nothing. It remains usable after task cancellation. Buffered values can still be received before Nothing; see the channel example.

completed

completed : a ->{Async} Task aSource

A task already completed with value. An Err is ordinary data.

import Async exposing (Outcome(..))

Async.runOutcome { Async.await (Async.completed 7) } == Completed 7

contextToken

contextToken : () ->{Async} AnySource

The current lifetime’s opaque cancellation context, for native sidecars that must interrupt a blocking operation when the task or its scope ends. It remains accessible during cancellation cleanup; obtaining it is not a checkpoint.

parMap

parMap : (a -> b) -> List a -> List bSource

Applies a pure function to every element in parallel, keeping their order. It uses at most GOMAXPROCS workers per call and needs no task scope. An empty list never calls the function.

import Async

Async.parMap { n -> n * n } [1, 2, 3] == [1, 4, 9]

race

race : List (() ->{Fail a, e} b) ->{Fail a, Async, e} bSource

Starts every action concurrently and selects the first observed terminal outcome. A winning value is returned, a winning Fail is raised in the caller, and a winning cancellation cancels the caller. Empty input cancels the caller. Remaining tasks are cancelled and drained before return or propagation, so this is a prompt cancellation request, not a bounded return deadline. Simultaneous outcomes may win in any order. Later failures during drain are discarded. A returned Err is an ordinary winning value. Handle other abort effects inside the actions. This does not search for the first success. Caller cancellation before drain finishes takes precedence over the winner.

import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))

result : Outcome (Result String Int)
result = Async.runOutcome {
    Fail.attempt {
        Async.race [
            { 42 }
        , {
              gate : Async.Channel Int
              gate = Async.channel 0
              ignore (Async.receive gate)
              Fail.fail "timeout"
          }
        ]
    }
    }
result == Completed (Ok 42)

receive

receive : Channel a ->{Async} Maybe aSource

The next value, waiting while none is buffered, or Nothing once the channel is closed and drained. Cancellation stops the receiving task instead of returning Nothing. See channel for a buffered send/close/drain example.

run

run : (() ->{Async, e} ()) ->{IO, e} ()Source

Runs a Unit-returning body in a new root scope. On every exit it cancels unfinished children, including ignored handles, and drains their cleanup. Returns Unit after completion or cancellation, so use Async.run fits a Unit-returning block such as main(). Application aborts leaving the body cancel and drain its children before propagating. The caller must permit the body’s residual effects; run introduces IO. Use runOutcome to inspect completion or cancellation.

import Async

Async.run { ignore (Async.await (Async.spawn { 7 })) } == ()

runOutcome

runOutcome : (() ->{Async, e} a) ->{IO, e} Outcome aSource

Runs body in a new root scope, returning its value or cancellation after cancelling and draining unfinished children, including ignored handles. Application aborts leaving the body drain its children before their outer handler. The caller must permit the body’s residual effects; runOutcome introduces IO.

import Async exposing (Outcome(..))

Async.runOutcome { Async.await (Async.spawn { 42 }) } == Completed 42

scope

scope : (() ->{Async, e} a) ->{Async, e} aSource

Runs body on the calling task with a shorter lifetime for its children. Every exit cancels unfinished children and drains their cleanup. Normal exit returns the body’s value without cancelling the caller or outside tasks. Cancellation propagates through the calling task; it is not a scope result. Keep this scope inside any resource scope its tasks use. Unfinished tasks cannot escape by returning their handles; the handles keep their final outcomes.

import Async exposing (Outcome(..))

result = Async.runOutcome {
    Async.scope {
        gate : Async.Channel Int
        gate = Async.channel 0
        ignore (Async.spawn { Async.receive gate })
        42
    }
    }
result == Completed 42

send

send : Channel a -> a ->{Async} BoolSource

Sends a value, waiting while the buffer is full. Answers False when the channel is closed, including while waiting. Cancellation stops the sending task instead of returning False. See channel for a buffered send/close/drain example.

sleep

sleep : Int ->{Async} ()Source

Pauses the task for milliseconds, waking early when it is cancelled. A duration of zero or less is a checkpoint.

import Async exposing (Outcome(..))

Async.runOutcome { Async.checkpoint(); Async.sleep 0; 42 } == Completed 42

spawn

spawn : (() ->{e} a) ->{Async, e} Task aSource

Starts body as a child of the current lifetime scope and returns its handle at once. The child owns its own spawns: when its body exits, unfinished descendants are cancelled and drained before its outcome is published. Await children that must finish normally. There is no detached spawn. The caller must permit all of the body’s effects, including IO, even for ignored handles. Application aborts must be handled inside the callback; known escapes are rejected statically and hidden dependencies before user code runs. An ordinary returned Err is a value and does not cancel siblings.

import Async exposing (Outcome(..))

result = Async.runOutcome {
    first = Async.spawn { 10 }
    second = Async.spawn { 20 }
    Async.awaitAll [first, second]
    }
result == Completed [10, 20]

spawnAttempt

spawnAttempt : (() ->{Fail a, e} b) ->{Async, e} Task (Result a b)Source

Starts action with its named Fail application handled inside the child, returning a task whose value is a Result. Other abort dependencies still need local handling. An Err does not cancel siblings or fail the scope.

import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))

Async.runOutcome { Async.await (Async.spawnAttempt { Fail.fail "missing"; 1 }) } == Completed (Err "missing")

wait

wait : Task a ->{IO} Outcome aSource

Waits, outside any task scope, for the task to finish and answers how it ended. This IO wait does not provide an Async cancellation point. Repeated observations keep the same outcome, including after its owner has exited.

import Async exposing (Outcome(..))

saved = Async.runOutcome { Async.completed 7 }
result = case saved of
    Completed task -> Async.wait task
    Cancelled -> Cancelled
result == Completed 7