Ramda's transducers: Lazy one-to-many branch - ramda.js

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.

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>

Related

Ramda, test two objects with multiple predicates for props equality

I have two objects that have the same props. I created few predicates to test them based on business logic:
const eqId = eqProps('equipmentId');
const eqQuantity = eqProps('quantity');
const eqColor = eqProps('color');
Each predicate accepts (o1, o2) as parameters. I am looking for a nicer way to output single value based on all predicates passing... in other words:
predicates.every(predicate => predicate(o1, o2) === true)
but in a more ramda style. allPass almost works but it accepts only a single object.
I know I can do this with every but this question is to help me learn more how to compose functions.
[...] allPass almost works but it accepts only a single object
Perhaps I'm not understanding 100% but I don't think that is true. The documentation says:
The function returned is a curried function whose arity matches that of the highest-arity predicate.
So given eqProps('foo') returns a binary function then the function returned by allPass will also be a binary function:
const check = allPass([eqProps('lunch'), eqProps('at')]);
check({lunch: '🌯', at: '1pm'}, {lunch: '🌯', at: '2pm'});
//=> false
check({lunch: '🌯', at: '1pm'}, {lunch: '🌯', at: '1pm'});
//=> true
If you have a "blueprint" for what your objects should look like, then I find eqBy(whereEq) easier to the eyes.
eqBy is better illustrated with an example. Hopefully this needs no further explanation:
const streqi = eqBy(toLower); // case insensitive equality
streqi("Foo", "fOO");
//=> true
So going back to eqBy(whereEq):
const check = eqBy(whereEq({lunch: '🌯', at: '1pm'}))
check({lunch: '🌯', at: '1pm', name: 'john'}, {lunch: '🌯', at: '1pm', name: 'tom'});
//=> true
check({lunch: '🌯', at: '1pm', name: 'john'}, {lunch: '🌯', at: '2pm', name: 'tom'});
// ^ ^
//=> false
We need to nest R.allPass and R.all since it is a matrix-like problem
const a = { suit: '♠︎', rank: '4' };
const b = { suit: '♠︎', rank: '9' };
const isSpade = R.propEq('suit', '♠︎');
const isCard = R.has('suit');
const compareWith = R.pipe(
R.allPass,
R.all,
R.unapply,
);
const isASpadeCard = compareWith([isSpade, isCard])
console.log(
isASpadeCard(a, b),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>

Ramda: How to use R.map with two arguments?

It works fine with a pair of array. I don't need to use Ramda in this example.
const addChild1 =  (x , y) => `${x}.addChild(${y}); `
const result1 = addChild(["containerA"], ["a1","a2", "a3"])
console.log(result1) //containerA.addChild(a1,a2,a3)
I couldn't make it work with 2d matrix of strings. I used R.map, but I get 'undefined' in the second argument.
const addChild2 =  R.map ((x , y) => `${x}.addChild(${y}); `)
const result2 = addChild2(["containerA", "containerB", "containerC"], [["a1","a2", "a3"], ["b1","b2", "b3"], ["c1","c2", "c3"]])
console.log(result2) //["containerA.addChild(undefined); ","containerB.addChild(undefined); ","containerC.addChild(undefined); "]
How can I avoid the 'undefined'? Desirable output is the below :
["containerA.addChild("a1","a2", "a3"); ","containerB.addChild("b1","b2", "b3"); ","containerC.addChild("c1","c2", "c3");"]
map takes string as second parameters, so in this case, only ["containerA", "containerB", "containerC"] got in the loop
You should use zipWith in this case instead of map
const addChild2 = R.zipWith((x, y) => `${x}.addChild(${y}); `)
const result2 = addChild2(
["containerA", "containerB", "containerC"],
[
["a1", "a2", "a3"],
["b1", "b2", "b3"],
["c1", "c2", "c3"],
]
)
console.log(result2)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

Typescript Record item count

Is it possible to find the count of items in a Typescript record?
For example something like
const testRecord: Record<string, string> = {
'one': 'value1',
'two': 'value2'
};
var length = testRecord.length;
// looking for length to be 2 but is undefined as there is no length property
For reference: https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt
I've just found this answer here for the length of a javascript object which seems to work just fine:
Length of a JavaScript object
My implementation to answer the example above was:
const testRecord: Record<string, string> = {
'one': 'value1',
'two': 'value2'
};
var length: Object.keys(testRecord).length;
// length equals 2
However please let me know if there is a better, more specific "Record" way to do this?
Maybe it's not what you wanted, but there is a Map type that has size property.
You can use it like this:
let m = new Map<string, any>();
m.set('a', 1)
let one = m.get('a');
console.log('a value is: ' + one + '; size of the map is: ' + m.size);
Map doesn't work exactly as Object does, so take a look at differences in behaviour first: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps

Dynamically combining 'preset' layouts

I'm trying to dynamically place different metabolic pathways (nodes following a preset layout contained in a parent node).
Normally i would define positions for each child node, so as to not overlap the parent nodes. However the graphs( = generated json containing the requested nodes) are generated dynamically, so this is not an option.
Is there a way to achieve this, short of recalculating positions for each node as the json is being generated.
E.g.: requesting glycolysis -> TCA -> Urea
Current situation
Acceptable solution
If you want to do something manual like that you'll probably have to use some code rather than just specify a layout.
let shiftPosition = (pos, delta) => ({ x: pos.x + delta.x, y: pos.y + delta.y });
let shiftNode = (node, delta) => shiftPosition( node.position(), delta );
let findDelta = parent => ({ x: 100, y: 100 }); // determine yourself
cy.nodes(':parent').forEach( parent => {
let delta = findDelta( parent );
parent.children().positions( node => shiftNode( node, delta ) );
} );

Lodash - how to perform error checking in long chained sequence

So lets say I had a chained sequence like the following:
let amount = _
.chain(selectedItemsArray)
.map(item => _.find(availableItems, {id: item.id})
//how can I determine that ALL items were found right here?
.filter('price_money')
...
Note the comment in the code above. It could be possible that the selectedItemsArray is out of date, so some selected items might not be in availableItems. So, my initial thought was to use a .tap or .thru (probably tap) to do something like _.every(selectedItems, _.isObject) or something similar to catch the error state where not all items are found and throw an error if not all items were found. This feels odd though...any better ways of handling this type of error checking mid sequence?
Something like this does work (at least I can throw an error), but seems like I'm using tap for something it's not intended for:
.tap(items => {
if (!_.every(items, _.isObject)) throw new Error('Some selected items are no longer available');
})
You can use another _.filter to check if the element is not an object, and also handle the offending value. You can use || to execute fallback code. See this question.
If you want your code to crash and burn on the first failure, use a function that throws an error instead of using console.error.
var available = [
{ id: 1, amount: 2.00 },
{ id: 2, amount: 4.00 }
];
var selected = [1, 2, 3];
var amount = _(selected)
.map(item => _.find(available, {id:item}) || item)
.filter(item => _.isObject(item) || console.error("Item not available:", item))
.sumBy('amount');
console.log("Amount:", amount.toFixed(2));
<script src="https://cdn.jsdelivr.net/lodash/4.15.0/lodash.min.js"></script>