Skip to content

Instantly share code, notes, and snippets.

@kybernetikos
Last active March 25, 2017 20:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kybernetikos/faa9605474b9304caa05be6f87aea94a to your computer and use it in GitHub Desktop.
Save kybernetikos/faa9605474b9304caa05be6f87aea94a to your computer and use it in GitHub Desktop.
Receivers and Receiver Transformers

Transducers Primer

function reducerFn(output, value) {
	// reducer functions know how to build a structure when repeatedly given
	// that structure and values (think of it as 'builder')
	return output
}
function reduceFn(collection, reducerFn, output) {
	// reduce functions understand how to extract values from the structure
	// they operate on (think of it as 'eater')
	for (let item of collection) {
		output = reducerFn(output, item)
	}
	return output
}
function transducer(reducerFn) {
	// transducers are things that transform a reducerFn (builder)
	// many useful operations can be written as transducers, e.g. map, filter, etc.
	// transducers can be chained together using function composition
	return reducerFn
}

an example

// The below are transducer versions of map and filter.
// They can be thought of as map(mapFn, reducerFn) and filter(predicate, reducerFn)
// curried to fit the transducer shape so that they can be composed as simple functions.
function map(mapFn) {
	return (reducerFn) => (acc, item) => reducerFn(acc, mapFn(item))
}
function filter(predicate) {
	return (reducerFn) => (acc, item) => predicate(item) ? acc : reducerFn(acc, item)
}
// an implementation of immutable array append as a reduceFn (builder)
function append(array, value) {
	return array.concat([value])
}

console.log(reduceFn([1, 2, 3, 4, 5], filter((x) => x%2)(map((x) => x+3)(append)), []))

function compose(f, g) {
	return (x) => f(g(x))
}

const myTransformation = compose(filter((x) => x%2), map((x) => x + 5))

console.log(reduceFn([1, 2, 3, 4, 5], myTransformation(append), []))

If you think in terms of source and sink,

  • a source is any structure plus the reduce function that knows how to extract values from it (e.g. an array, plus array.reduce).
  • a sink is any structure plus the reducerFn that knows how to put items in it.

Things transducers do well:

  • the structures they operate on can be immutable or mutable
  • they can operate asynchronously or synchronously
  • they can compose like ordinary functions

Transducers are push based, in that the source determines when a value goes into the chain, however there is no requirement that the sink receives one value for every value that goes in. It may receive no values or many values.

Other push systems in javascript are observables, promises and streams.

This is in contrast with iterators and iterator transformers which are pull based: iterators must provide a single value when a sink asks for it.

Things transducers don't do naturally:

  • explictly handle source exhaustion - you just stop getting values
  • handle early termination -e.g. you can't implement an efficient 'some value matches this predicate' or 'take the first 5 values' as a naive transducer
  • handle errors (e.g. websocket disconnected)
  • although the transducers are very decoupled, the structures and their reducer (builder) and reduce (eater) functions are closely coupled. If the language ecosystem allows you to use functions to build or eat almost all structures using standard functions this isn't a problem, but js doesn't.

Cognitect's transducers-js solves these problems, but in the process the appealing simplicity of transducers is turned into complex objects that fulfill a 'transformer protocol'.

Receivers and Receiver Transformers

function receiver(value, completion, stopper) {
	// completion may be true (source exhaustion) or false/undefined or an error
	
	// value is null if completion is not false/undefined
	
	// stopper is a function that you can call to signal that you don't want
	// any more values after this one
	
	return outputStructure
}

Map as a receiver transformer:

function map(mapFn) {
	return (receiver) => (value, done, close) => receiver(done ? null : mapFn(value), done, close)
}

Receiver transformers are composable as simple functions, just like transducers.

Having the done value be a boolean or an error object is a bit ugly, an alternative to (value, completion, stopper) might be ({value, error, complete}, unsubscribe) (which is closer to observable api naming), but this would lose the ability for a simple onValue(value) function to work as a receiver.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment