Skip to content

Instantly share code, notes, and snippets.

@craigdallimore
Last active December 3, 2022 23:44
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save craigdallimore/8b5b9d9e445bfa1e383c569e458c3e26 to your computer and use it in GitHub Desktop.
Save craigdallimore/8b5b9d9e445bfa1e383c569e458c3e26 to your computer and use it in GitHub Desktop.

Transduce


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.

The docs (v22.1)

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:

Map

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]

Filter

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]

Reduce

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); // '😱'

Chaining

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.

Transduce

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.

Preventing extra iteration

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!

reduce v. transduce

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
    1. As the transducer function will take an array, we need to map over the elements of it.
    1. We use the prop function to extract the value at the named property
    1. We apply the add function to the accumulated value and the extracted value
    1. 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".

Producing a different data structure

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.

Also see

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