0

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.

4 Answers 4

3

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]
Sign up to request clarification or add additional context in comments.

Comments

2

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.

Comments

1

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);

2 Comments

The reason I want to use lenses is because the data structure is a lot more complex than this example, and I don't know the structure beforehand. I am building the lens path based on a schema that is provided to me at runtime.
But I now see that I can compose a batch at runtime using the non-lens prop and propOr functions - thanks.
1

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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.