Only run map once, ramda js - ramda.js

const arr = [{
_id: 'z11231',
_typename: 'items'
id: '123',
comment: null,
title: 'hello'
}, {
_id: 'z11231',
_typename: 'items'
id: 'qqq',
comment: 'test',
title: 'abc'
}]
Wanted output:
[['123', null, 'hello'], ['qqq', 'test', 'abc']];
export const convertObjectsWithValues = R.map(R.values);
export const removeMongoIdAndGraphqlTypeName = R.map(R.omit(['_id', '__typename']));
export const getExcelRows = R.pipe(removeMongoIdAndGraphqlTypeName, convertObjectsWithValues);
Problem here is I'm running two separate maps. It's to slow. Can I combine this in a way where only one map is executed. And still keep it clean in three seperate functions?

I'd be curious to see whether you've actually tested that it's too slow. The Knuth quote always seems a propos: "premature optimization is the root of all evil".
But if you've tested, and if multiple iterations are an actual bottleneck in your application, then the composition law of Functors should help. In Ramda terms this law states that
compose ( map (f), map (g) ) ≍ map (compose (f, g) )
and of course similarly that
pipe ( map (g), map (f) ) ≍ map (pipe (g, f) )
That means that you can rewrite your function like this:
const getExcelRows = map (pipe (omit ( ['_id', '_typename'] ), values ))
const arr = [
{_id: 'z11231', _typename: 'items', id: '123', comment: null, title: 'hello'},
{_id: 'z11231', _typename: 'items', id: 'qqq', comment: 'test', title: 'abc'}
]
console .log (
getExcelRows (arr)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script> <script>
const {map, pipe, omit, values} = R </script>

Use R.map with R.props to state which properties you want in the order that you want them. This will always maintain the correct order, unlike. R.values, which is constrained by the way JS orders keys.
const arr = [{"_id":"z11231","_typename":"items","id":"123","comment":null,"title":"hello"},{"_id":"z11231","_typename":"items","id":"qqq","comment":"test","title":"abc"}]
const getExcelRows = keys => R.map(R.props(keys))
const result = getExcelRows(['id', 'comment', 'title'])(arr)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

Related

Ramda - how to pass dynamic argument to function inside pipe

I am trying to add/use a variable inside the pipe to get the name of an object from a different object. Here is what I got so far:
I have an array of IDs allOutgoingNodes which I am using in the pipe.
Then I filter results using tableItemId property and then I am adding additional property externalStartingPoint and after that I would like to add name of tableItem from tableItems object to content -> html using concat.
const startingPointId = 395;
const allNodes = {
"818": {
"id": "818",
"content": {
"html": "<p>1</p>"
},
"outgoingNodes": [
"819"
],
"tableItemId": 395
},
"821": {
"id": "821",
"content": {
"html": "<p>4</p>"
},
"tableItemId": 396
}
}
const tableItems = {
"395": {
"id": "395",
"name": "SP1",
"code": "SP1"
},
"396": {
"id": "396",
"name": "SP2",
"code": "SP2"
}
}
const allOutgoingNodes = R.pipe(
R.values,
R.pluck('outgoingNodes'),
R.flatten
)(tableItemNodes);
const result = R.pipe(
R.pick(allOutgoingNodes),
R.reject(R.propEq('tableItemId', startingPointId)),
R.map(
R.compose(
R.assoc('externalStartingPoint', true),
SomeMagicFunction(node.tableItemId),
R.over(
R.lensPath(['content', 'html']),
R.concat(R.__, '<!-- Table item name should display here -->')
)
)
),
)(allNodes);
Here is a complete working example: ramda editor
Any help and suggestions on how to improve this piece of code will be appreciated.
Thank you.
Update
In the comments, OriDrori noted a problem with my first version. I didn't really understand one of the requirements. This version tries to address that issue.
const {compose, chain, prop, values, lensPath,
pipe, pick, reject, propEq, map, assoc, over} = R
const getOutgoing = compose (chain (prop('outgoingNodes')), values)
const htmlLens = lensPath (['content', 'html'])
const addName = (tableItems) => ({tableItemId}) => (html) =>
html + ` <!-- ${tableItems [tableItemId] ?.name} -->`
const convert = (tableItemNodes, tableItems, startingPointId) => pipe (
pick (getOutgoing (tableItemNodes)),
reject (propEq ('tableItemId', startingPointId)),
map (assoc ('externalStartingPoint', true)),
map (chain (over (htmlLens), addName (tableItems)))
)
const startingPointId = 395;
const tableItemNodes = {818: {id: "818", content: {html: "<p>1</p>"}, outgoingNodes: ["819"], tableItemId: 395}, 819: {id: "819", content: {html: "<p>2</p>"}, outgoingNodes: ["820"], tableItemId: 395}};
const tableItems = {395: {id: "395", name: "SP1", code: "SP1"}, 396: {id: "396", name: "SP2", code: "SP2"}}
const allNodes = {818: {id: "818", content: {html: "<p>1</p>"}, outgoingNodes: ["819"], tableItemId: 395}, 819: {id: "819", content: {html: "<p>2</p>"}, outgoingNodes: ["820"], tableItemId: 395}, 820: {id: "820", content: {html: "<p>3</p>"}, outgoingNodes: ["821"], tableItemId: 396}, 821: {id: "821", content: {html: "<p>4</p>"}, tableItemId: 396}}
console .log (
convert (tableItemNodes, tableItems, startingPointId) (allNodes)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
As well as most of the comments on the version below still applying, we should also note that chain, when applied to functions acts like this:
chain (f, g) (x) //~> f (g (x)) (x)
So chain (over (htmlLens), addName (tableItems))
ends up being something like
(node) => over (htmlLens) (addName (tableItems) (node)) (node)
which in Ramda is equivalent to
(node) => over (htmlLens, addName (tableItems) (node), node)
which we then map over the nodes coming to it. (You can also see this in the Ramda REPL.)
Original Answer
It's not trivial to weave extra arguments through a pipeline because pipelines are designed for the simple purpose of passing a single argument down the line, transforming it at every step. There are of course techniques we could figure out for that, but I would expect them not to be worth the effort. Because the only thing they gain us would be the ability to write our code point-free. And point-free should not be a goal on its own. Use it when it makes your code simpler and more readable; skip it when it doesn't.
Instead, I would break this apart with some helper functions, and then write a main function that took our arguments and passed them as necessary to helper functions inside our main pipeline. Expand this snippet to see one approach:
const {compose, chain, prop, values, lensPath, flip, concat,
pipe, pick, reject, propEq, map, assoc, over} = R
const getOutgoing = compose (chain (prop ('outgoingNodes')), values)
const htmlLens = lensPath (['content', 'html'])
const addName = flip (concat) ('Table item name goes here')
const convert = (tableItemNodes, startingPointId) => pipe (
pick (getOutgoing (tableItemNodes)),
reject (propEq ('tableItemId', startingPointId)),
map (assoc ('externalStartingPoint', true)),
map (over (htmlLens, addName))
)
const startingPointId = 395;
const tableItemNodes = {818: {id: "818", content: {html: "<p>1</p>"}, outgoingNodes: ["819"], tableItemId: 395}, 819: {id: "819", content: {html: "<p>2</p>"}, outgoingNodes: ["820"], tableItemId: 395}};
const allNodes = {818: {id: "818", content: {html: "<p>1</p>"}, outgoingNodes: ["819"], tableItemId: 395}, 819: {id: "819", content: {html: "<p>2</p>"}, outgoingNodes: ["820"], tableItemId: 395}, 820: {id: "820", content: {html: "<p>3</p>"}, outgoingNodes: ["821"], tableItemId: 396}, 821: {id: "821", content: {html: "<p>4</p>"}, tableItemId: 396}}
console .log (
convert (tableItemNodes, startingPointId) (allNodes)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
(You can also see this on the Ramda REPL.)
Things to note
I find compose (chain (prop ('outgoingNodes')), values) to be slightly simpler than pipe (values, pluck('outgoingNodes'), flatten), but they work similarly.
I often separate out the lens definitions even if I'm only going to use them once to make the call site cleaner.
There is probably no good reason to use Ramda in addName. This would work just as well: const addName = (s) => s + 'Table item name goes here' and is cleaner. I just wanted to show flip as an alternative to using the placeholder.
There is an argument to be made for replacing
map (assoc ('externalStartingPoint', true)),
map (over (htmlLens, addName))
with
map (pipe (
assoc ('externalStartingPoint', true),
over (htmlLens, addName)
))
as was done in the original. The Functor composition law states that they have the same result. And that requires one fewer iterations through the data. But it adds some complexity to the code that I wouldn't bother with unless a performance test pointed to this as a problem.
Before I saw your answer I managed to do something like in the example below:
return R.pipe(
R.pick(allOutgoingNodes),
R.reject(R.propEq('tableItemId', startingPointId)),
R.map((node: Node) => {
const startingPointName = allTableItems[node.tableItemId].name;
return R.compose(
R.assoc('externalStartingPoint', true),
R.over(
R.lensPath(['content', 'html']),
R.concat(
R.__,
`<p class='test'>See node in ${startingPointName}</p>`
)
)
)(node);
}),
R.merge(newNodesObject)
)(allNodes);
What do you think?

Ramda.js - how to view many values from a nested array

I have this code:
import {compose, view, lensProp, lensIndex, over, map} from "rambda";
let order = {
lineItems:[
{name:"A", total:33},
{name:"B", total:123},
{name:"C", total:777},
]
};
let lineItems = lensProp("lineItems");
let firstLineItem = lensIndex(0);
let total = lensProp("total");
My goal is to get all the totals of all the lineItems (because I want to sum them). I approached the problem incrementally like this:
console.log(view(lineItems, order)); // -> the entire lineItems array
console.log(view(compose(lineItems, firstLineItem), order)); // -> { name: 'A', total: 33 }
console.log(view(compose(lineItems, firstLineItem, total), order)); // -> 33
But I can't figure out the right expression to get back the array of totals
console.log(view(?????, order)); // -> [33,123,777]
That is my question - what goes where the ????? is?
I coded around my ignorance by doing this:
let collector = [];
function collect(t) {
collector.push(t);
}
over(lineItems, map(over(total, collect)), order);
console.log(collector); // -> [33,123,777]
But I'm sure a ramda-native knows how to do this better.
It is possible to achieve this using lenses (traversals), though will likely not be worth the additional complexity.
The idea is that we can use R.traverse with the applicative instance of a Const type as something that is composable with a lens and combines zero or more targets together.
The Const type allows you to wrap up a value that does not change when mapped over (i.e. it remains constant). How do we combine two constant values together to support the applicative ap? We require that the constant values have a monoid instance, meaning they are values that can be combined together and have some value representing an empty instance (e.g. two lists can be concatenated with the empty list being the empty instance, two numbers can be added with zero being the empty instace, etc.)
const Const = x => ({
value: x,
map: function (_) { return this },
ap: other => Const(x.concat(other.value))
})
Next we can create a function that will let us combine the lens targets in different ways, depending on the provided function that wraps the target values in some monoid instance.
const foldMapOf = (theLens, toMonoid) => thing =>
theLens(compose(Const, toMonoid))(thing).value
This function will be used like R.view and R.over, accepting a lens as its first argument and then a function for wrapping the target in an instance of the monoid that will combine the values together. Finally it accepts the thing that you want to drill into with the lens.
Next we'll create a simple helper function that can be used to create our traversal, capturing the monoid type that will be used to aggregate the final target.
const aggregate = empty => traverse(_ => Const(empty))
This is an unfortunate leak where we need to know how the end result will aggregated when composing the traversal, rather than simply knowing that it is something that needs to be traversed. Other languages can make use of static types to infer this information, but no such luck with JS without changing how lenses are defined in Ramda.
Given you mentioned that you would like to sum the targets together, we can create a monoid instance that does exactly that.
const Sum = x => ({
value: x,
concat: other => Sum(x + other.value)
})
This just says that you can wrap two numbers together and when combined, they will produce a new Sum containing the value of adding them together.
We now have everything we need to combine it all together.
const sumItemTotals = order => foldMapOf(
compose(
lensProp('lineItems'),
aggregate(Sum(0)),
lensProp('total')
),
Sum
)(order).value
sumItemTotals({
lineItems: [
{ name: "A", total: 33 },
{ name: "B", total: 123 },
{ name: "C", total: 777 }
]
}) //=> 933
If you just wanted to extract a list instead of summing them directly, we could use the monoid instance for lists instead (e.g. [].concat).
const itemTotals = foldMapOf(
compose(
lensProp('lineItems'),
aggregate([]),
lensProp('total')
),
x => [x]
)
itemTotals({
lineItems: [
{ name: "A", total: 33 },
{ name: "B", total: 123 },
{ name: "C", total: 777 }
]
}) //=> [33, 123, 777]
Based on your comments on the answer from customcommander, I think you can write this fairly simply. I don't know how you receive your schema, but if you can turn the pathway to your lineItems node into an array of strings, then you can write a fairly simple function:
const lineItemTotal = compose (sum, pluck ('total'), path)
let order = {
path: {
to: {
lineItems: [
{name: "A", total: 33},
{name: "B", total: 123},
{name: "C", total: 777},
]
}
}
}
console .log (
lineItemTotal (['path', 'to', 'lineItems'], order)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {compose, sum, pluck, path} = R </script>
You can wrap curry around this and call the resulting function with lineItemTotal (['path', 'to', 'lineItems']) (order), potentially saving the intermediate function for reuse.
Is there a particular reason why you want to use lenses here? Don't get me wrong; lenses are nice but they don't seem to add much value in your case.
Ultimately this is what you try to accomplish (as far as I can tell):
map(prop('total'), order.lineItems)
you can refactor this a little bit with:
const get_total = compose(map(prop('total')), propOr([], 'lineItems'));
get_total(order);
You can use R.pluck to get an array of values from an array of objects:
const order = {"lineItems":[{"name":"A","total":33},{"name":"B","total":123},{"name":"C","total":777}]};
const result = R.pluck('total', order.lineItems);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>

Ramda zipWith, one argument

A function for telling if a person has any given pets.
const person = {
name: 'joe',
pets: {
dog: true,
cat: false,
fish: true
}
}
const personHasPet = (pet: string) => R.pipe(R.path('pets', pet), R.equals(true));
const personHasPets = (listOfPets: string[]) => R.allPass(R.zipWith(personHasPet, listOfPets, listOfPets))(person);
This works. However, as you can see, I supply zipWith with two arguments. Thats because zipWith requires two argument. Why is this?
I only need one since I only have one argument in persHasPet function. zipWith always requires two? Why is this? Is there any other function when you have one argument? And another when you have three? Makes no sense.
https://ramdajs.com/docs/#zipWith
It's not entirely clear what you're trying to accomplish with zipWith, but the function supplied is definitely designed for working with two lists, and the callback should definitely take two parameters. Thus:
zipWith(fn, [a1, a2, a3, ... an], [b1, b2, b3, ... bn])
//=> [fn(a1, b1), fn(a2, b2), fn(a3, b3), ... fn(an, bn)]
This is what it does, "zipping" together two lists by applying a function to each pair.
I think the code you're looking for might be something like the following, but I'm not really certain:
const personHasPet = compose (path, flip (append) (['pets']) )
const personHasPets = compose (allPass, map (personHasPet) )
const joe = {name: 'joe', pets: {dog: true, cat: false, fish: true}}
console .log (
personHasPet ('dog') (joe), //=> true
personHasPet ('cat') (joe), //=> false
personHasPet ('fish') (joe), //=> true
)
console .log (
personHasPets (['dog', 'cat']) (joe), //=> false
personHasPets (['dog', 'fish']) (joe), //=> true
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {compose, path, flip, append, allPass, map} = R </script>
I'm also not sure if that Ramda implementation gains anything over the vanilla
const personHasPet = (pet) => (person) => person .pets && person .pets [pet]

Functional programming RamdaJs groupBy with transformation

I want to create function that groups the array with specific key as follow:
var items = [
{name: 'n1', prop: 'p1', value: 90},
{name: 'b', prop: 'p2', value: 1},
{name: 'n1', prop: 'p3', value: 3}];
Into this:
{n1: {p1: 90, p3: 3}, {b: {p2: 1}
Basically group by column "name" and sets the prop name as key with the value.
I know there is groupBy function in RamdaJs but it accepts function to generate the group key.
I know I can format the data after that but I will be inefficient.
Is there any way to pass some kind of "transform" function which prepare the data for each item.
Thanks
There is a trade-off involved in using a generic library and writing custom code for every scenario. A library like Ramda with several hundred functions will offer many tools that can help, but they are not likely to cover every scenario. Ramda does have a specific function to combine groupBy with some sort of fold, reduceBy. But if I didn't know that, I would write a custom version.
I would start with what works and remains simple, only worrying about performance if tests showed an issue with this specific code. Here I show a number of steps of changing such a function each time to improve performance. I'll make the main point here: I would actually stick with my first version, which I find easily readable, and not bother with any of the performance enhancements unless I had hard numbers to show that this was a bottleneck in my application.
Plain Ramda version
My first pass might look like this:
const addTo = (obj, {prop, value}) =>
assoc (prop, value, obj)
const transform1 = pipe (
groupBy (prop ('name')),
map (reduce (addTo, {}))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform1 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {assoc, pipe, groupBy, prop, map, reduce} = R </script>
Only Loop Once
This to me is clear and easy to read.
But there is certainly a question of efficiency, given that we have to loop over the list to group and then loop over each group to fold. So perhaps we'd be better off with a custom function. Here's a fairly straightforward modern JS version:
const transform2 = (items) =>
items .reduce(
(a, {name, prop, value}) => ({...a, [name]: {...a[name], [prop]: value}}),
{}
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform2 (items)
)
Don't reduce ({...spread})
This version only loops once, which sounds like a nice improvement... but there is a real question about the performance of what Rich Snap calls the reduce ({...spread}) anti-pattern. So perhaps we want to use a mutating reduce instead. This shouldn't cause problems as it's only internal to our function. We can write an equivalent version that doesn't involve this reduce ({...spread}) pattern:
const transform3 = (items) =>
items .reduce (
(a, {name, prop, value}) => {
const obj = a [name] || {}
obj[prop] = value
a[name] = obj
return a
},
{}
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform3 (items)
)
More Performant Looping
Now we've removed that pattern (I don't in fact agree that it's always an anti-pattern), we have a more performant bit of code, but there is still one thing we can do. It's well known that the Array.prototype functions such as reduce are not as fast as their plain loop counterparts. So we can go one step further and write this with a for-loop:
const transform4 = (items) => {
const res = {}
for (let i = 0; i < items .length; i++) {
const {name, prop, value} = items [i]
const obj = res [name] || {}
obj[prop] = value
}
return res
}
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]; console.log('This version is intentionally broken. See the text for the fix.');
console .log (
transform4 (items)
)
We've reached the limit of what I can think of in terms of performance optimizations.
... And we've made the code much worse! Comparing that last version with the first,
const transform1 = pipe (
groupBy (prop ('name')),
map (reduce (addTo, {}))
)
we see a hand-down winner in terms of code clarity. Without knowing the details of the addTo helper, we can still get a very good sense up-front, of what this function does on first reading. And if we want those details more obvious, we could simply in-line that helper. Version, though, will take a careful reading to understand how it works.
Oh wait; it doesn't work. Did you test it and see that? Do you see what's missing? I pulled this line from the end of the for-loop:
res[name] = obj;
Did you notice that in the code? It's not particularly difficult to spot, but it's not necessarily obvious at a quick glance.
Summary
Performance optimization, when it's needed, has to be done very carefully, as you can't take advantage of many of the tools you get used to using. So, there are times when it's very important, and I do it then, but if my cleaner, easier-to-read code performs well enough, then I'll leave it there.
Point-free (pointless?) Aside
A similar argument applies for pushing too hard for point-free code. It's a useful technique, and many functions become cleaner by using it. But it can be pushed beyond its usefulness. Note that the helper function, addTo, from the initial version above is not point-free. We can make a point-free version of it. There may be simpler ways, but the first thing that comes to my mind is pipe (lift (objOf) (prop ('prop'), prop ('value')), mergeAll). We could write an entirely point-free version of this function in-lining that this way:
const transform5 = pipe (
groupBy (prop ('name')),
map (pipe (
map (lift (objOf) (
prop ('prop'),
prop ('value')
)),
mergeAll
))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform5 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, groupBy, prop, map, lift, objOf, mergeAll} = R </script>
Does this gain us anything? Not that I can see. The code is much more complex and much less expressive. This is as hard to read as the for-loop variant.
So again, focus on keeping the code simple. That's my advice, and I'm sticking to it!
I would use reduceBy instead:
it allows a function to generate a key
and a function to transform the data
const items = [
{name: 'n1', prop: 'p1', value: 90},
{name: 'b', prop: 'p2', value: 1},
{name: 'n1', prop: 'p3', value: 3}];
// {name: 'n1', prop: 'p1', value: 90} => {p1: 90}
const kv = obj => ({[obj.prop]: obj.value});
// {p1: 90}, {name: 'n1', prop: 'p3', value: 3} -> {p1: 90, p3: 3}
const reducer = (acc, obj) => mergeRight(acc, kv(obj));
console.log(
reduceBy(reducer, {}, prop('name'), items)
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {reduceBy, prop, mergeRight} = R;</script>
An imperative for...of loop, with a bit of destruction is readable, albeit verbose, and performant.
const fn = arr => {
const obj = {}
for(const { name, prop, value } of arr) {
if(!obj[name]) obj[name] = {} // initialize the group if it doesn't exist
obj[name][prop] = value // add the prop and it's value to the group
}
return obj
}
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]
const result = fn(items)
console.log(result)
A functional solution using Ramda would be slower but depending on the number of items in the array it might be negligible. I usually start with a functional solution, and only if I have performance issues, I profile, and then fallback to the more performant imperative option.
A readable pointfree solution using Ramda - R.groupBy and R.map would be the basis. In this case I map each group items to their props, and then use R.fromPairs to generate the object.
const { pipe, groupBy, prop, map, props, fromPairs } = R
const fn = pipe(
groupBy(prop('name')),
map(pipe(
map(props(['prop', 'value'])),
fromPairs
))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]
const result = fn(items)
console.log(result)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

Use Ramda.js to pull off items from object

This question is about how to perform a task using RamdaJS.
First, assume I have an object with this structure:
let myObj = {
allItems: [
{
name: 'firstthing',
args: [
{
name: 'arg0'
},
{
name: 'arg1'
}
],
type: {
name: 'type_name_1'
}
},
{
name: 'otherthing',
args: [
{
name: 'arg0'
}
]
}
]
}
I am trying to create an object that looks like:
{
arg0: 'arg0', // myObj.allItems[0].args[0].name
typeName: 'type_name_1' // myObj.allItems[0].type.name
}
(I know the names are stupid, arg0, typeName. It's not important)
So if we weren't using Ramda, this is how I'd do it imperatively:
// The thing I'm searching for in the array (allItems)
let myName = 'firstthing';
// Here's how I'd find it in the array
let myMatch = myObj.allItems.find(item => item.name === myName);
// Here is the desired result, by manually using dot
// notation to access properties on the object (non-functional)
let myResult = {
arg0: myMatch.args[0].name,
typeName: myMatch.type.name
};
// Yields: {"arg0":"arg0","typeName":"type_name_1"}
console.log(myResult)
Finally, just for good measure, this is as far as I've gotten so far. Note that, I'd really like to accomplish this in a single compose/pipe.
(An object goes in, and an object with the desired data comes out)
const ramdaResult = R.compose(
R.path(['type', 'name']),
R.find(
R.propEq('name', myName)
)
)(R.prop('allItems', myObj))
Thanks
A combination of applySpec and path should work:
const transform = applySpec ({
arg0: path (['allItems', 0, 'args', 0, 'name']),
typeName: path (['allItems', 0, 'type', 'name'])
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path} = R </script>
But depending upon your preferences, a helper function might be useful to make a slightly simpler API:
const splitPath = useWith (path, [split('.'), identity] )
// or splitPath = curry ( (str, obj) => path (split ('.') (str), obj))
const transform = applySpec({
arg0: splitPath('allItems.0.args.0.name'),
typeName: splitPath('allItems.0.type.name'),
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path, useWith, split, identity} = R </script>
splitPath is not appropriate for Ramda, but it's a useful function I often include, especially if the paths are coming from a source outside my control.
Update
Yes, I did miss that requirement. Serves me right for looking only at the input and the requested output. There's always multiple incompatible algorithms that give the same result for a specific input. So here's my mea culpa, an attempt to break this into several reusable functions.
Lenses are probably your best bet for this. Ramda has a generic lens function, and specific ones for an object property (lensProp), for an array index(lensIndex), and for a deeper path(lensPath), but it does not include one to find a matching value in an array by id. It's not hard to make our own, though.
A lens is made by passing two functions to lens: a getter which takes the object and returns the corresponding value, and a setter which takes the new value and the object and returns an updated version of the object.
An important fact about lenses is that they compose, although for technical reasons the order in which you supply them feels opposite to what you might expect.
Here we write lensMatch which find or sets the value in the array where the value at a given path matches the supplied value. And we write applyLensSpec, which acts like applySpec but takes lenses in place of vanilla functions.
Using any lens, we have the view, set, and over functions which, respectively, get, set, and update the value. Here we only need view, so we could theoretically make a simpler version of lensMatch, but this could be a useful reusable function, so I keep it complete.
const lensMatch = (path) => (key) =>
lens
( find ( pathEq (path, key) )
, ( val
, arr
, idx = findIndex (pathEq (path, key), arr)
) =>
update (idx > -1 ? idx : length (arr), val, arr)
)
const applyLensSpec = (spec) => (obj) =>
map (lens => view (lens, obj), spec)
const lensName = (name) => lensMatch (['name']) (name)
const transform = (
name,
nameLens = compose(lensProp('allItems'), lensName(name))
) => applyLensSpec({
arg0: compose (nameLens, lensPath (['args', 0, 'name']) ),
typeName: compose (nameLens, lensPath (['type', 'name']) )
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform ('firstthing') (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {lens, find, pathEq, findIndex, update, length, map, view, compose, lensProp, lensPath} = R </script>
While this may feel like more work than some other solutions, the main function, transform is pretty simple, and it's obvious how to extend it with additional behavior. And lensMatch and applyLensSpec are genuinely useful.