Introduction into Channels and Transducers in JavaScript


Introduction

Channels, ES6 generators and CSP in general enable powerful features that we can already use in JavaScript today. I will not go too much into detail about coroutines, promises, generators as this has been covered in my previous post. Read it here in case you need an update.

In the following section we will figure out what transducers are by creating a low-level naive implementation and then exchanging the low-level implementation with the transducers.js library, which offers a number of functions optimized for real world usage.

Once we have an idea of what transducers are and what they should accomplish, the next step will be to explore why channels are useful and create an example or two just to get things off the ground.

Finally we will see how channels and transducers fit together and what powerful pipelines we can create by combing the two. While the initial introductory post Channels and Generators in JavaScript already showed how React and channels can interact, we will take a step back this time and only focus on channels and transducers. Demonstrating how the two fit together and what benefits we can leverage by taking this approach.

All this should be the building block for a another post, where we will try to build more advanced UIs with React, Rebass, Recompose, transducers and channels. This is when we will put our acquired knowledge to use. It makes sense to break the topic up, just to focus on the basics first.

Transducers

Transducers are composable algorithmic transformations. They are independent from the context of their input and output sources and specify only the essence of the transformation in terms of an individual element.
http://clojure.org/reference/transducers
Transform + Reduce = Transduce

A function you pass into a reducer is a reducing function. It takes the current result and a new input and returns the next result. Just think about reducers in redux. A reducer in redux expects the current state and an action and returns a new state based on the given inputs.

(state, action) -> state

A function that transforms a reducing functions can be expressed as:

(xForm, reducingFn) -> reducingFn

A transducer can be described as taking one reducing function and returning another reducing function.

((acc, input) -> acc) -> ((acc, input) -> acc)

Let’s note some important facts:

Transducers have no knowledge of the context they’re being applied in and where the inputs come from nor do they have an idea what the reducing function actually does. Transducers decouple the data structure, enabling us to compose complex transformations. They ask a collection to reduce itself and will work with any reducing function transformer.

Let’s see how we can implement our own transducer.

Any transformation can be expressed with reduce as base, think filter and map f.e. But how, you might be asking. To figure it all out, let’s implement map.

const map = mapFn => (xs, x) => [...xs, mapFn(x)]
const inc = map(add(1))
console.log(reduce(inc, [], [1, 2, 3, 4])) // [2, 3, 4, 5]

map expects a mapping function, and returns a new function that takes two arguments and returns a single result. How can we use reduce to recreate the map function efficiently? Currently we’re assuming that we’re dealing with an array, but what we really need to do, is enable the passing of a reducing function to map.

const map = curry((mapFn, redFn) => (xs, x) => redFn(xs, mapFn(x)))
const inc = reduce(map(add(1), concat), [])
console.log(inc([1, 2, 3, 4])) // [2, 3, 4, 5]

So this actually works. We created our own map function that expects two arguments, a mapping as well as a reducing function.

We can do the same with filter.

const filter = curry((predFn, redFn) => (xs, x) =>
predFn(x) ? redFn(xs, x) : xs)
const greaterThanOne = reduce(filter(x => x > 1, concat), [])
console.log(greaterThanOne([1, 2, 3, 4])) // [2, 3 ,4]

We can even combine filter and map.

const xForm = compose(
filter(x => x > 1),
map(add(1))
)
console.log(reduce(xForm(concat), [], [1, 2, 3, 4]))

This is interesting. Transducers can be easily composed , enabling complex transformations. Also interesting to note is that they compose from left to right.

Compose itself with starts with the right most function and passes the result to the next function on the left side. The difference here is that we’re returning a function. At the end of the composition we have a new function. For an easier understanding of what happens when we compose reducers, take a look at the following.

const xForm = redFn => filter(x => x > 1, map(add(1), redFn))
reduce(xForm(concat), [], [1, 2, 3, 4])

We can still do one more refinement though, by creating a transduce function, which will hide away having to call xForm with the reducing function.

const transduce = curry((xForm, f, init, coll) => 
reduce(xForm(f), init, coll))
console.log(transduce(map(add(1)), concat, [], [1, 2, 3, 4])) 
// [2, 3, 4, 5]

The last example is based on the final example in Transducers.js: A JavaScript Library for Transformation of Data by James Longster, who also implemented a library called transducers.js. But effectively you can choose and mix any library that implements the transducer protocol.

We will see an example later on where we use Ramda’s compose, with map and filter from transducers.js. Transducers can work with arrays, streams, channels and any iterable including immutable-js collections.

I also highly recommend reading CSP and Transducers in JavaScript by Tom Ashworth and Understanding Transducers in JavaScript by Roman Liutikov. Both give a very clear walkthrough on how to create transducers. Also check the links at the end of this post for more great explanations including a thorough explanation by Drew Tipson.

Channels

I have written about channels in a previous post, but we’ll quickly run through channels and generators just to gain a solid basis on what we want to do next, which is combine channels with transducers.

This is one of the introductory examples from the js-csp documentation.

const ch = csp.chan(1);
yield csp.put(ch, 42);
yield csp.take(ch); // 42
ch.close()
yield csp.take(ch); // csp.CLOSED

The aforementioned library offers a number of useful functions for creating and interacting with channels.

js-csp enables to create a new channel via the chan function with a buffer size of 1, which is optional, as long as you don’t pass in a transducer as second argument.

We can run put operations on the newly created channel by calling put, prefixed by yield, and passing in the channel and the value 42. We yield the value with take and then close the channel .

Typical channels offer a set of functions, but knowing about put and take as seen in the previous example will take you a long way.

Processes communicate via channels. We can push to the queue via put, and retrieve via take. All we need to think about to begin with, is that we have a channel and a consumer.

Here’s one more example from the js-csp documentation.

var ch = go(function*(x) {
yield timeout(1000);
return x;
}, [42]);
console.log((yield take(ch)));

We spawn a goroutine by calling the go function with a generator function. The go function will immediately return a channel, enabling us to retrieve any values from the channel via take.

Extending this idea onto the UI enables us to create a function like the following listen.

const listen = (el, type) => {
const ch = chan()
el.addEventListener(type, e => putAsync(ch, e))
return ch
}

listen enables us to convert any event on an element into a channel.

For a more detailed write-up on channels and generators, read the previous post Channels and Generators in JavaScript.

Channels and Transducers

If you can recall, I mentioned that transducers also work with channels. So now that we have seen the two powerful concepts in isolation, it’s time to figure what combing the two concepts brings us.

import { chan, go, putAsync, takeAsync } from 'js-csp'
import
{ filter, map } from 'transducers.js'
import
{ add, compose } from 'ramda'

var ch
= chan(1, compose(
filter(x => x > 1),
map(add(1))
))

const logValue = v => console.log("Received", v)

putAsync(ch, 1)
putAsync(ch, 2)
putAsync(ch, 3)
putAsync(ch, 4)
putAsync(ch, 5)

takeAsync(ch, logValue) // Received 3
takeAsync(ch, logValue) // Received 4
takeAsync(ch, logValue) // Received 5
takeAsync(ch, logValue) // Received 6
takeAsync(ch, logValue) // Received null
takeAsync(ch, logValue) // Received null

ch.close()

We can create a channel via chan and pass in buffer and transducer. It’s important to note that you have to define a buffer to work with transducers. Now calling putAsync multiple times will result in the inputs being transformed. You might have noticed that we’re using Ramda’s compose because it plays nice with map and filter from transducers.js, which also comes with its own compose.

Here’s a second example, which is inspired by an example in CSP and Transducers in JavaScript, which explains stateful transducers. We detect if a keydown took place and check if the keys pressed are either ‘z’ or ‘x’. Pressing ‘z’ starts logging the x and y positions of any mousemove event and pressing ‘x’ stops the logging again.

import { chan, go, putAsync, take } from 'js-csp'
import
{ compose, filter, map } from 'transducers.js'
import
{ equals, head, indexOf, lte, prop, } from 'ramda'

const
listen = (el, type, c) =>
el.addEventListener(type, e => putAsync(c, e))
const filterMouseEvents = (k, v) => e =>  e[k] && e[k] === v

const filterKeyEvents = (...keys) => {
let open = false
return
e => {
return (
open = lte(0, indexOf(prop('key', e), keys)) ?
equals(prop('key', e), head(keys)) :
open
)
}
}

const xForm = compose(
filter(filterKeyEvents('z', 'x')),
filter(filterMouseEvents('type', 'mousemove')),
map(function (e) {
return [e.pageX, e.pageY]
})
)
const c = chan(1, xForm)

listen(document, 'keydown', c)
listen(document, 'mousemove', c)

go(function* () {
while(true) {
console.log(yield take(c))
}
})

Although we didn’t cover advanced topics like merging channels and more advanced transformations, these two examples demonstrate what is possible when we use channels and transducers quite well.

I think this should suffice for now. The best advice from here on is to really try to re-implement the transducer function and the channel/transducer example as a starting point.

Outro

We should have a general understanding of channels and transducers now. This combined with the initial post Channels and Generators in JavaScript should be enough to figure out how we can use these concepts with React, Rebass, Recompose to build advanced UIs. This is what the next post in this series will be all about.

If there is some interest for a more thorough walkthrough of transducers including more advanced implementations, let me know. I might create a gist showing a more fine grained approach and cover more details a long the way. Let me know.

In case you have any questions or feedback leave a comment here or leave feedback via twitter.

Links

Channels and Generators in JavaScript

transducers.js

Transducers are coming

CSP and Transducers in JavaScript

Transducers.js: A JavaScript Library for Transformation of Data

Anatomy of a Reducer

Understanding Transducers in JavaScript

Everything Reduced: Transducers in Javascript

Javascript Transducers 2: Stateful & Gateful

Streaming Logs with Transducers and Ramda

Clojure Reference on Transducers

Transducer Protocol