This post is the fourth of a series about how I wrote in JavaScript the equivalent of Go(lang) channels.
If you haven't already, I highly recommend reading at least the first post before reading this one:
Go channels in JS (1/5): Sending and Receiving
Nicolas Lepage for Zenika ・ Dec 2 '19
So far we have built an equivalent of Go channels in JS, which allows us to create channels, buffered or unbuffered, send values to these, receive values from these, and finally close these.
This time we will use the closing feature we added last time as a base to build a new feature: Ranging.
First, let's have a look at how to range over a channel in Go.
Ranging over channel
If you remember last time, we used the ability of the receive operator to return two values in order to know if a channel has been closed:
for { // This is like while (true)
i, ok := <-ch
if !ok {
break
}
fmt.Println(i)
}
The second value returned by the receive operator is a boolean which tells us if something has actually been received.
However Go offers a better syntax to do the exact same thing, which is the range operator:
for i := range ch {
fmt.Println(i)
}
This range loop will iterate over the values received from ch
until this one is closed.
Let's take back our send123()
example:
func main() {
ch := make(chan int) // Create an integer channel
go send123(ch) // Start send123() in a new goroutine
// Receive an integer from ch and print it until ch is closed
for i := range ch {
fmt.Println(i)
}
}
func send123(ch chan int) {
// Send 3 integers to ch
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // Close ch
}
This is much easier to read than last time!
Now how could we transpose this in JS?
Of course using a for ... of
would be nice.
But for ... of
uses the iterator protocol which is synchronous, whereas the receive operation is asynchronous.
Good news, JS has asynchronous iteration since ES2018, which comes with a new for await ... of
syntax.
So we could create a new range operation, which would return an asynchronous iterable.
Let's try this with our send123()
example:
async function* main() {
const ch = yield chan() // Create a channel
yield fork(send123, ch) // Start send123()
// Receive a value from ch and log it to console until ch is closed
for await (const i of yield range(ch)) {
console.log(i)
}
}
function* send123(ch) {
// Send 3 integers to ch
for (let i = 1; i <= 3; i++) {
yield send(ch, i)
}
yield close(ch) // Close ch
}
Nice! Like in Go the code is much easier to understand, even if having a yield
inside a for await ... of
is not simple.
Now let's implement our new range operation!
Implementing ranging over channel
As usual we need an operation factory:
const RANGE = Symbol('RANGE')
export const range = chanKey => {
return {
[RANGE]: true,
chanKey,
}
}
We have only one chanKey
argument which is the key of the channel we want to iterate over.
Then we have to handle range operations in the channel middleware:
export const channelMiddleware = () => (next, ctx) => async operation => {
// ...
if (operation[RANGE]) {
// Handle range operation
}
// ...
}
for await ... of
needs an asynchronous iterable, which is an object able to return an iterator.
A common pattern is to use the same object as iterable and iterator:
if (operation[RANGE]) {
const it = {
[Symbol.asyncIterator]: () => it,
}
return it
}
As you can see it
returns itself when asked for an iterator, and will therefore satisfy the async iterable protocol.
Now it
needs to implement the async iterator protocol.
All the async iterator protocol needs is an async next()
function, which must return an object with two properties:
-
value
which is the current value -
done
, a boolean which tells us if the iterator has ended, in which casevalue
may be omitted
This looks a lot like the detailed receive operation we created last time, which returns a scalar with a value and a boolean which tells us if a value was actually received.
The only actual difference is that the boolean is inverted.
So we should be able to use the doRecv()
function we created last time to implement next()
:
const it = {
[Symbol.asyncIterator]: () => it,
next: async () => {
const [value, ok] = await doRecv(ctx, operation.chanKey)
return {
value,
done: !ok,
}
}
}
And this is it! Well that was easy.
Let's try this out on repl.it with our send123()
example (it uses esm to benefit from modules):
What next
With ranging done, we are not far from having the full feature set of channels, the only thing missing is select.
The last post "Go channels in JS (5/5): Selecting" will be a big one, I'll need some time to write it...
In the meantime I might publish some bonus posts (did you know that Go allows receiving from a nil channel?), so stay tuned.
I hope you enjoyed this fourth post, give a ❤️, 💬 leave a comment, or share it with others, and follow me to get notified of my next posts.