DEV Community

Cover image for Go channels in JS (4/5): Ranging
Nicolas Lepage for Zenika

Posted on • Updated on

Go channels in JS (4/5): Ranging

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:

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 case value 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.

Top comments (0)