prochan.js
I/O-enabled communicating sequential proc
esses via chan
nels.
Influenced by Go and by Clojure’s core.async.
Compares favorably to the excellent js-csp, a faithful port of core.async, with the addition of process I/O semantics, among other features.
Explores in particular the treatment of processes as first-class I/O primitives, such as this would imply:
- common interface and composability characteristics shared by processes and proper channels
- that a process communicating via I/O is itself an expression of a “logical channel”
- that such processes are generally interchangeable with channels for any channel-based API
Features
- Core CSP entities corresponding to those described by Go and core.async:
- Primitives:
- processes (“goroutines”)
- buffered/unbuffered channels
- Operations:
receive
(take
)send
(put
)select
(alts
)
- Primitives:
- Support for affixing functional transforms to channels via Clojure-style transducers
- Structural simplifications and optimizations for performance, particularly at scale
- Constant-time arbitrary valued buffering queues, with no amortized linear copying
- Space-efficient doubly-linked-list internal queues, with immediate splicing, no deferred GC
- Pooled instances of internal classes
- Clear, approachable annotated source code
Design
- Directly targets any platform supporting ES6 generator functions
- Compatible to as far down as ES3 using manual generator-iterators
- Elegantly sourced in Literate Coffee.
Goals
- Rich process constructs, system snapshots, diagnostics, visualizations, etc.
- Feature parity to the whole of core.async, where appropriate for JS
- Straightforward translation to alternative async models, e.g. Node, Promises, Rx, etc.
Installation
npm install prochan
Examples
All sample code that follows will presume bindings to these functions imported from prochan:
;
These functions are also aliased to names that reflect each operation’s general relation to a corresponding operation defined by core.async:
import {go, chan, take, put, alts} = 'prochan';
### Basic operations
#### Processes: [`proc`](https://github.com/nickfargo/prochan/blob/master/src/index.coffee.md#proc)
Spawning a process is performed by calling the `proc` function, and passing it a generator function. Outwardly this corresponds to calling `go` or similar in other environments:
```js
let p = proc( function* () {
// ... yield ...
});
// >>> Process
The key distinction of proc
is that, whereas a call to go
or similar would return a single-use channel as an indirection to the eventual return value of a “goroutine”, proc
returns an actual Process
object.
However, given equivalent generator functions, the Process
returned by proc
may still be consumed in the same manner as the channel returned by go
:
'foo' === ;'foo' === ;
Discussed further below: Process I/O.
chan
Channels: The chan
function is used in generally familiar fashion to construct an unbuffered, buffered, and/or transduced Channel
:
let ch1 = ;
let ch2 = ;let ch3 = chan;let ch4 = chan;
;let transducer = ;let ch5 = ;let ch6 = ; // No explicit buffering, behaves as unbuffered
Discussed further below: Transduction.
receive
, send
Communication: Basic communications via channels are performed inside a process by yield
ing the effect of a 1-ary receive
or 2-ary send
operation (aliased to take
and put
, respectively):
;;
select
Selection: In prochan the select
operation (aliased to alts
) returns a Selector
generator, intended for immediate use inside a delegated yield (yield*
) expression:
;
Discussed further below, with examples of advanced use cases: Delegated selection.
Distinguishing features
Process I/O
A process in prochan may communicate over its own built-in I/O channels.
;
Internally, a 0-ary receive
call implies communication over the in channel of the current process (p1
), and likewise a 1-ary send
call implies communication over the out channel of the current process.
Externally, because Process
implements the standard channel interface, the process p1
may be passed as an argument to a channel operation, just like a proper channel.
By default processes are constructed without I/O channels; an unbuffered channel is instated automatically on the first call to each end of the process.
Channel values and results
In prochan channels impose no domain restrictions on input values; any value may be conveyed over the channel unless otherwise specified by the user. In particular, channels do not appropriate null
or undefined
, nor introduce any other sentinel identity; no such entity is prohibited from being conveyed as its own instrinsic value through the channel.
A Channel
may also be closed with an optional final result value. This is generally analogous to the return value of a function: by default a channel’s result value is undefined
, but may be set specifically to any value provided in the call to the channel’s idempotent close
method. Once a channel is both closed and empty it becomes done, after which any process that receive
s from the channel will have the result value conveyed immediately to it.
This design leaves channel domain semantics entirely to the discretion of process authors, who may establish between themselves, if necessary, the meanings of any special entities (e.g., whether or not some particular value receive
d from a channel — such as null
or undefined
, perhaps — is indeed meant to be interpreted as a special in-band “done” signal).
However, with respect to such signaling, it is also safe, sufficient, and generally preferable for the current process to determine a channel’s “done” state out-of-band, by calling the chan.isFinal
predicate immediately after yield receive
ing from the channel.
// Process whose sole responsibility is to read from a channel;
TODO: reduce this pattern to a single operation (say,
next
) that returns a destructurable object in a manner idiomatically similar to that of JS’s own [[IteratorResult]], e.g.:
;while let value done = ;
Delegated selection
In prochan a select
(or alias alts
) expression evaluates to a delegated generator, and so is always to be paired with yield*
. This design allows for the cases of a select
expression to be:
- composed by chaining methods, e.g.
send
/receive
,else
, etc. - each distinguished by an optional
label
, or - each associated with a generator function, to which the selector will delegate if an operation defined by that case is selected
Thus, at the user’s discretion, select
may take any of several forms:
Basic form
let value channel = ;
Labeled form
while true // Destructure the yielded selector let label value channel = select ; // Then branch against `label` if label === 'foo' // ... else if label === 'bar' // ... else if label === 'baz' // alternative, selected if none of the other cases’ operations is ready
Delegated form
let ch1 = ch2 = ch3 = ; // Prepare a value to be received from `ch3` ...send; // ... and observe the effect on `select`:'qux' === ;
Transduction
; let ch1 = ;let ch2 = ;
Here both ch1
and ch2
are unbuffered channels. To exhibit unbuffered synchronization behavior and also support transduction, ch2
includes a provisional zero-length buffer, to which items may be added during a single expansion step of the transducer, and which must be emptied completely before the next input is accepted.
👋