What is transduce
? What is it for? This document is intended to help people (such as myself) who would be looking through the ramda docs, find transduce
and have no idea if it would be a good fit for my current problem.
transduce :: (c -> c) -> (a,b -> a) -> a -> [b] -> a
Initializes a transducer using supplied iterator function. Returns a single item by iterating through the list, successively calling the transformed iterator function and passing it an accumulator value and the current value from the array, and then passing the result to the next call.
The iterator function receives two values: (acc, value). It will be wrapped as a transformer to initialize the transducer. A transformer can be passed directly in place of an iterator function. In both cases, iteration may be stopped early with the R.reduced function.
A transducer is a function that accepts a transformer and returns a transformer and can be composed directly.
A transformer is an an object that provides a 2-arity reducing iterator function, step, 0-arity initial value function, init, and 1-arity result extraction function, result. The step function is used as the iterator function in reduce. The result function is used to convert the final accumulator into the return type and in most cases is R.identity. The init function can be used to provide an initial accumulator, but is ignored by transduce.
The iteration is performed with R.reduce after initializing the transducer.
There are quite a few words in there that may be unfamiliar. iterator
, accumulator
, transformer
, 0-arity initial value function ... not to worry, let's take a few steps back, look at some similar functions and work towards the particulars of transduce
.
You may already be familiar with the JavaScript Array methods, such as map
, filter
and reduce
. Here's a quick recap:
Array.proptotype.map
gives you a way to turn an array of [a, a, a]
to an array of [b, b, b]
by providing it with a function from a -> b
. (I'm using a
and b
to loosely imply that the values are all the same type.)
// f :: Number -> Number
const f = num => num + 1;
[1,2,3,4].map(f) // [2,3,4,5]
Array.prototype.filter
gives you a way to turn an array of [a, a, a]
into an array of [a, a]
by giving it a function from a -> Boolean
. It will reject any value that would cause that function to return false
.
These functions are normally known as predicates.
// predicate :: Number -> Boolean
const predicate = num => num > 5;
[3,4,5,6].filter(predicate); // [6]
Array.prototype.reduce
takes a function from (acc, value) -> acc
. reduce
will call that function on each element of its array - the acc
value is accumulated, meaning that each time the function is called, the acc
argument is the result of the previous iteration.
In ramda, you might see this sort of function referred to as a 2-arity reducing iterator function. Which is to say, it takes two arguments and returns one that is the same type as one of the arguments it was given - permtting it to be given back to this function along with a new second argument for another iteration.
Note that there must be some starter value for acc
; it is the second argument to reduce
:
const starter = [];
// f :: ([Number], Number) -> [Number]
const f = (acc, item) => acc.concat([ item + 1 ]);
[1,2,3,4].reduce(f, starter); // [2,3,4,5]
reduce
gets interesting when you realise that it doesn't have to merely give back an array (like map
and filter
do):
const add = (x, y) => x + y;
[1,2,3,4].reduce(add, 0); // 10
const times = (x, y) => x * y; // 24
[1,2,3,4].reduce(times, 1);
const obj = { a : { b : { c : { d : 'π±' } } } };
const prop = (obj, key) => obj[key];
['a', 'b', 'c', 'd'].reduce(prop, obj); // 'π±'
Suppose you want to do multiple operations on your array: you may want to increase all your numbers by 10 and also filter out any that are above 5. One way to come at such a problem would be to chain filter
and map
like so:
// `gte` and `add` are ramda functions
const isFiveOrUnder = gte(5);
const add10 = add(10);
[1,2,3,4,5,6,7,8,9,10].filter(isFiveOrUnder)
.map(add10); // [11,12,13,14,15]
Ok. Now suppose you want the first three results:
const take3 = (acc, val) => {
if (acc.length >= 3) { return acc; }
return acc.concat([val]);
};
[1,2,3,4,5,6,7,8,9,10].filter(isFiveOrUnder)
.map(add10)
.reduce(onlyTakeThree); // [11,12,13]
Well, it works ... but deep down you know that it iterated over [11,12,13,14,15] in the reduce
part of the chain. Inefficient!
How can we stop iteration when we have all we need? Lets find out.
We'll take a look at transduce
. It takes four arguments:
- A function called the
transducer
function.
transducer :: c -> c
- A function called the
iterator
function.
iterator :: (a, b) -> a
- The initial accululator value (think of the empty array or the starting numbers in the above examples).
initialValue :: a
- An array of items to iterate over.
arr :: [a]
The very simplest example I can think of is:
const transducer = x => x; // sometimes called an `identity` function.
const iterator = (acc, val) => acc.concat([val])
const initialValue = [];
const arr = [1,2,3,4,5]
transduce(transducer, iterator, initialValue, arr); // [1,2,3,4,5]
Notice that transduce
provides a way to separate out iteration from transformation.
Consider this:
const iterator = (acc, val) => {
if (val > 3) { return reduced(acc)}
return append(val, acc);
};
const arr = [1,2,3,4];
const initialValue = [];
const transducer = map(x => x + 2)
transduce(transducer, iterator, initialValue, arr); // [3]
We put in [1,2,3,4]
, and what came out was [3]
. Notice that the transducer is mapping x => x+2
- if it was given an array [1,2,3,4]
it would give back [3,4,5,6]
. But it doesn't! The iterator has a condition where it will return the (acc)umulated result wrapped in a function called reduced
, given a value greater than 3.
reduced
is a function that you can wrap the accumulated value in when you are ready to stop iteration:
if (val) > 3 return reduced(acc);
Note that this also applies within the 2-arity reducing iterator function
that reduce
uses.
Suppose the first iteration took the first item in our arr
array (1
) and wrapped it in an array [1]
... then started feeding items from that through the transducer: we would get [3]
on the first iteraton; then the next value would be transformed from 2 to 4 - it would be greater than the (val > 3)
condition and the acc will be passing to reduced
and returned.
Thus we have one place where we can control the iteration (the iterator
function) and it isn't mixed with the concerns of how the data is transformed (the transducer
function).
Let's confirm this:
const iterator = (acc, val) => {
if (val > 3) { return reduced(acc)}
return append(val, acc);
};
const arr = [1,2,1,2,1,3,1,2,1];
const initialValue = [];
const transducer = map(x => x + 1)
transduce(transducer, iterator, initialValue, arr) // [2,3,2,3,2]
It only took items until it hit the 3, which - when mapped to 4 - causes the reduced condition to kick in. By putting a cheeky console.log(val)
in the iterator function we see this:
2
3
2
3
2
4
It gets to 4 and stops iterating! Nice!
Suppose we want the sum of all the val
values in this array:
const arr = [
{ val : 5 },
{ val : 7 },
{ val : 3 }
];
const f1 = transduce(map(prop('val'), add, []);
// [ 1 ][ 2 ] [3]
f1(arr) // 15
const f2 = reduce((acc, x) => add(acc, prop('val', x)), 0);
// [ 4 ]
f2(arr) // 15
-
- As the transducer function will take an array, we need to map over the elements of it.
-
- We use the
prop
function to extract the value at the named property
- We use the
-
- We apply the
add
function to the accumulated value and the extracted value
- We apply the
-
- By contrast, to do the same thing using
reduce
we need to expose and name the values being passed around (or do additional manipulations to make it "point-free".
- By contrast, to do the same thing using
const emoji = ['π', 'π΅', 'π²', 'π€', 'π·', 'π€'];
const iterator = (acc, val) => {
acc[`item-${val}`] = emoji[val];
return acc;
};
const arr = [
{ val : -2 },
{ val : -3 },
{ val : -4 }
];
const initialValue = {};
const transducer = compose(
map(prop('val')),
map(add(5))
)
transduce(transducer, iterator, initialValue, arr) // {"item-1": "π΅", "item-2": "π²", "item-3": "π€"}
This is simply to indicate
- You can start with an array but end with a different data structure, depending on how your iterator works.
- You can compose small transformations together to build up a more complex transformer.
- Emoji can be used in JavaScript.
There! That's transduce
, it's no so strange after all!
For more detail about transducing in general, I recommend you read the post linked below which gives more examples, more detail and discusses the composability of transducers.