5

Ramda for the lazy

Ramda's transduce enables the creation of lazy sequences.

One to many

R.chain can be used in a transducer as a one-to-many operator, like so (REPL):

const tapLog = R.tap( (what) => console.log(what) )

const suits = ['♠', '♥', '♦', '♣']
const ranks = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'J', 'Q', 'K', 'A']

const addRank = (suit) => R.map(concat(suit),ranks)

var transducer = R.compose(
R.chain(addRank),
tapLog,
R.take(2)
);

R.into([], transducer, suits);

// => ♠1 // console.log
// => ♠2 // console.log
// => ["♠1", "♠2"]

The issue

The issue with the snippet above is that R.map(concat(suit),ranks) will not be lazy - all the ranks will be mapped (creating intermediate array), and only then chain will 'pipe' them one by one down the transducer sequence.

This is not an issue, unless you are mapping 680k graph nodes.

Why is this happening?

The implementation of R.chain looks like so:

var chain = _curry2(_dispatchable(['fantasy-land/chain', 'chain'], _xchain, function chain(fn, monad) {
if (typeof monad === 'function') {
    return function(x) { return fn(monad(x))(x); };
}
return _makeFlat(false)(map(fn, monad));
}));

And it's _makeFlat that blocks any lazy evaluation.

The goal

Is there a way to create a lazy one-to-many transducer branch?

Note that R.reduce supports iterables.

Also, see the related github issue, where a solution is provided, but not using ramda - which is what I'm after.

Izhaki
  • 23,372
  • 9
  • 69
  • 107

1 Answers1

3

The problem that you've encountered is that as soon as R.map(concat(suit),ranks) is encountered, it gets evaluated immediately and in full. This is unrelated to the _makeFlat function that you mention, as when transducers are utilised _dispatchable won't actually call the function body inside the definition of R.chain but instead use the transducer definition inside _xchain.

So rather than generating a whole mapped list already, one option we have is to create a new transducer that I will call combineWith, that takes a function like concat in your example and a list to combine each element with going through the transformation. We can do so while checking for @@transducer/reduced along the way.

const combineWith = (fn, xs) => xf => ({
  // proxy both `init` and `result` straight through
  // see internal/_xfBase.js
  '@@transducer/init': xf['@@transducer/init'].bind(xf),
  '@@transducer/result': xf['@@transducer/result'].bind(xf),

  // combine the item at each step with every element from `xs`
  // using `fn`, returning early if `reduced` is ever encountered
  '@@transducer/step': (acc, item) => {
    for (const x of xs) {
      acc = xf['@@transducer/step'](acc, fn(item, x))
      if (acc['@@transducer/reduced']) return acc
    }
    return acc
  }
})

const suits = ['♠', '♥', '♦', '♣']
const ranks = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'J', 'Q', 'K', 'A']

const tapLog = R.tap(console.log.bind(console, 'tapLog'))

const transducer = R.compose(
  combineWith(R.concat, ranks),
  tapLog,
  R.take(2)
)

console.log('result', R.into([], transducer, suits))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Scott Christopher
  • 6,458
  • 23
  • 26
  • So bloody simple! You have not only answered my question, but you have also completely demystified the whole docs on `R.transduce`; and guess what... [the answer was there all along](https://github.com/ramda/ramda/blob/f494250c0aed9ecc096cc5b5b7823661edc14de2/source/internal/_xtap.js), it's all the noise that tricked me! I've amended your answer slightly to elaborate on init/result. – Izhaki Dec 17 '17 at 23:05