Co
Module co
import co
| Module | co |
| Source | co/co.zirr, co/helpers.zirr, co/module-docs.zirr, co/timers.zirr |
Concurrency without parallelism, and without new syntax.
A blocking call blocks the whole program, which is fine until something has to wait on two things at once — a key and a timer, a slow job and a user who presses q. co adds routines for that, and nothing else: no go keyword, no send or receive operator, no async/await. Everything here is a function.
Two rules govern it. One routine runs at a time, and it only gives way at a switch point — send, receive, select, wait, sleep, iterating a channel, the end of a scope body, and every stdlib call that waits on the outside world: io.read, io.write, fmt.fprint* and the functions of fs, whatever reader, writer or filesystem they are given. Between two switch points, code runs as if it were alone, so a shared var or array needs no lock. And every routine belongs to a scope, and a scope does not end before its routines — scope is the only way to get a Scope, so nothing runs in the background that the code does not show.
A Channel is @Iterable, which is why a producer and a consumer need nothing new: for line <- produce(s, ...) receives until the producer closes. select takes from the first ready channel in list order, so priority is visible at the call site and the same in every run.
Timer is a capability, passed in like fs.FileSystem is. immediateTimer fires before a spawned body ever runs and neverTimer never fires, so a timeout is tested in both directions without waiting for either.
cancel stops a routine at the switch point it is parked on, and returns at once. What it cannot take back is a call already handed to the operating system: a write in flight reaches the file or the terminal, possibly after scope has returned. A read is different, because bytes cannot be asked for twice — a host stream owns its reads, so a routine cancelled while waiting for input leaves the bytes behind for whoever reads next rather than discarding them.
Dependencies
Contents
- Unions —
Selected - Data —
Cancelled,Closed,Received,TimedOut,Timer - Attributes —
HasTimer - Types —
Channel,Routine,Scope - Functions —
after,all,cancel,channel,close,first,immediateTimer,map,merge,neverTimer,produce,receive,scope,select,send,sleep,spawn,timeout,wait
Unions
Selected
`co/co.zirr:48`
union Selected {
Received
Closed
}
What select found.
Cases
| Case | Interpretation |
|---|---|
Received |
A value was taken from channel. |
Closed |
channel is closed and drained, so it will never carry anything again. Drop it from the list, or stop. |
Data
Cancelled
`co/co.zirr:66`
data Cancelled
The routine was stopped by cancel.
Closed
`co/co.zirr:58`
data Closed {
channel: Channel
}
channel is closed and drained, so it will never carry anything again. Drop it from the list, or stop.
Fields
| Field | Description |
|---|---|
channel |
The channel that ended. |
Received
`co/co.zirr:50`
data Received {
channel: Channel
value
}
A value was taken from channel.
Fields
| Field | Description |
|---|---|
channel |
The channel the value came from. |
value |
The value. |
TimedOut
`co/timers.zirr:46`
data TimedOut {
limit: Duration
}
timeout gave up before the body finished.
Fields
| Field | Description |
|---|---|
limit |
The limit that was exceeded. |
Timer
`co/timers.zirr:13`
data Timer {
after: fn(Duration) -> Channel
}
The capability to wait for time to pass.
It is a plain value holding a function, so a test hands over one that fires at once or one that never fires, and waits for nothing.
Fields
| Field | Description |
|---|---|
after |
Returns a channel that receives one value once the duration has passed, and then closes. |
Attributes
HasTimer
`co/timers.zirr:6`
attr HasTimer {
timer(self: @HasTimer) -> Timer
}
Provides the capability to wait for time to pass, the way clock.HasMonotonicClock provides a clock.
Fields
| Field | Description |
|---|---|
timer |
Returns the timer this value provides. |
Types
Channel
`co/co.zirr:11`
extern type Channel
A queue between routines. It is @Iterable, so a for loop receives from it until it is closed and drained.
Routine
`co/co.zirr:7`
extern type Routine
A running function, returned by spawn.
Scope
`co/co.zirr:4`
extern type Scope
Owns routines. Only scope creates one, so every spawn traces back to a visible scope call.
Functions
after
`co/timers.zirr:19`
fn after(timer: Timer, d: Duration) -> Channel
A channel that receives one value after d has passed, and then closes.
all
`co/helpers.zirr:4`
fn all(bodies: [fn() -> Result]) -> [Result]
Runs every body at once and returns every result, in the order the bodies were given.
cancel
`co/co.zirr:27`
extern fn cancel(s: Scope) -> Void
Stops every routine spawned into s, each at the switch point it is parked on.
Neither the scope’s body nor the calling routine is stopped, and buffered values are lost: cancelling closes nothing.
channel
`co/co.zirr:30`
extern fn channel(capacity: Int) -> Channel
A channel holding up to capacity values. A capacity of 0 makes every send wait for a receiver.
close
`co/co.zirr:41`
extern fn close(ch: Channel) -> Void
Says no more values will be sent. The values already in the channel are still received.
Closing a closed channel is a bug in the caller, so it stops the program.
first
`co/helpers.zirr:29`
fn first(bodies: [fn() -> Result]) -> Result
The result of whichever body finishes first. The rest are stopped.
immediateTimer
`co/timers.zirr:30`
fn immediateTimer() -> Timer
A timer whose every after has already fired. This is how code under a timeout is tested without waiting for one.
map
`co/helpers.zirr:12`
fn map(items: @Iterable, limit: Int, transform: fn(Any) -> Result) -> [Result]
Applies transform to every item, at most limit at a time, and returns the results in order.
merge
`co/helpers.zirr:77`
fn merge(s: Scope, channels: [Channel]) -> Channel
All values from all channels in one, which closes once the last of them has.
neverTimer
`co/timers.zirr:40`
fn neverTimer() -> Timer
A timer that never fires, so the same code is tested as though its deadline were never reached.
produce
`co/helpers.zirr:66`
fn produce(s: Scope, body: fn(Channel) -> Any) -> Channel
A generator: body sends into the channel, which closes when body returns.
The channel is unbuffered, so the producer moves in lockstep with its consumer. A producer that should run ahead is written with channel and spawn directly.
receive
`co/co.zirr:37`
extern fn receive(ch: Channel) -> Option
Blocks until a value arrives, and returns it as Some. Once the channel is closed and drained, None, forever.
scope
`co/co.zirr:15`
extern fn scope(body: fn(Scope) -> Any) -> Any
Runs body with a fresh scope, waits for every routine spawned into it, and returns body’s value.
Nothing outlives the block: a forgotten wait never loses work, and no routine is left running behind it.
select
`co/co.zirr:45`
extern fn select(channels: [Channel]) -> Selected
Blocks until one of the channels can be received from, and takes from the first ready one in list order.
Order is priority: visible at the call site and the same in every run. The cost is that a channel that is always ready starves the ones after it.
send
`co/co.zirr:34`
extern fn send(ch: Channel, value: Any) -> Void
Blocks until there is room, then puts value in.
Sending on a closed channel is a bug in the caller, so it stops the program.
sleep
`co/timers.zirr:24`
fn sleep(timer: Timer, d: Duration)
Blocks the calling routine for d. The other routines keep running.
spawn
`co/co.zirr:19`
extern fn spawn(s: Scope, body: fn() -> Result) -> Routine
Starts body in a new routine owned by s and returns at once.
The new routine runs when the spawning one reaches a switch point, not before.
timeout
`co/helpers.zirr:48`
fn timeout(timer: Timer, limit: Duration, body: fn() -> Result) -> Result
body’s result, or Err(TimedOut(limit)) if it is still running when limit passes.
wait
`co/co.zirr:23`
extern fn wait(routine: Routine) -> Result
Blocks until the routine ends, and returns what it ended with: its own Result, or Err(Cancelled()).
May be called more than once and from any routine, and answers the same each time.