Search code examples
functional-programminglodashramda.js

Splitting/nesting _.flow with lodash (or ramda)


I have two objects, one describes the features of a location, the other describes the prices of those features.

features = {
  improvements: [...] // any array of many id's
  building: {} // only one id, may be undefined
}
prices = {
  id_1: 10,
  ...
}

I want to iterate over features and collate all the prices. Sometimes features.building will be undefined, sometimes features.improvements will be empty.

Additional code/workbench on repl.it

I can do this with lodash in this manner:

result = _(features.improvements)
  .map(feature => prices[feature.id])
  .concat(_.cond([
    [_.isObject, () => prices[features.building.id]]
  ])(features.building)) // would like to clean this up
  .compact()
  .value();

I'm interested in writing this in a more functional manner, and I ended up with:

result = _.flow([
  _.partialRight(_.map, feature => prices[feature.id]),
  _.partialRight(_.concat, _.cond([
    [_.isObject, () => prices[features.building.id]]
  ])(features.building)),
  _.compact,
])(features.improvements)

I still have to almost secretly call features.building mid stream, which feels awkward to me.

What I'd like to get to is (pseudocoded):

flow([
  // maybe need some kind of "split([[val, funcs], [val, funcs]])?
  // the below doesn't work because the first
  // flow's result ends up in the second

  // do the improvement getting
  flow([
    _.map(feature => prices[feature.id])
  ])(_.get('improvements')),

  // do the building getting
  flow([
    _.cond([
      [_.isObject, () => [argument.id]]
    ])
  ])(_.get('building')),

  // concat and compact the results of both gets
  _.concat,
  _.compact,
])(features); // just passing the root object in

Is it possible? How would a more seasoned FP-Programmer approach this?

I'm open to solutions written with lodash-fp or rambda (or anything with good docs I can try to understand) since those probably give cleaner code because they're more functionally-orientated/curried than standard lodash.


Solution

  • Lodash

    Here is a solution that uses _.flow():

    1. Convert the features to an array using _.values(), _.flatten(), and _.compact() (to ignore building when undefined).
    2. Convert to an array of ids with _.map().
    3. Get the values with _.at().

    const { values, flatten, compact, partialRight: pr, map, partial, at } = _;
    
    const fn = prices => _.flow([
      values,
      flatten,
      compact,
      pr(map, 'id'),
      partial(at, prices)
    ])
    
    const prices = {
      i_1: 'cost_i_1',
      i_2: 'cost_i_2',
      i_3: 'cost_i_3',
      i_4: 'cost_i_4',
      b_1: 'cost_b_1',
    };
    
    const features = {
      improvements: [
        {id: 'i_1'},
        {id: 'i_2'},
        {id: 'i_3'},
        {id: 'i_4'},
      ],
      building: {
        id: 'b_1'
      },
    };
    
    const result = fn(prices)(features);
    
    console.log(result);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

    lodash/fp

    const { values, flatten, compact, map, propertyOf } = _;
    
    const fn = prices => _.flow([
      values,
      flatten,
      compact,
      map('id'),
      map(propertyOf(prices))
    ])
    
    const prices = {"i_1":"cost_i_1","i_2":"cost_i_2","i_3":"cost_i_3","i_4":"cost_i_4","b_1":"cost_b_1"};
    const features = {"improvements":[{"id":"i_1"},{"id":"i_2"},{"id":"i_3"},{"id":"i_4"}],"building":{"id":"b_1"}};
    
    const result = fn(prices)(features);
    
    console.log(result);
    <script src='https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)'></script>

    Ramda

    1. Get the values, flatten, and filter undefineds using R.identity.
    2. Get the id props with R.map.
    3. Use a fliped R.props to get the ids values from prices

    const { pipe, values, flatten, filter, identity, map, prop, flip, props } = R;
    
    const propsOf = flip(props);
    
    const fn = prices => pipe(
      values,
      flatten,
      filter(identity),
      map(prop('id')),
      propsOf(prices)
    );
    
    const prices = {"i_1":"cost_i_1","i_2":"cost_i_2","i_3":"cost_i_3","i_4":"cost_i_4","b_1":"cost_b_1"};
    const features = {"improvements":[{"id":"i_1"},{"id":"i_2"},{"id":"i_3"},{"id":"i_4"}],"building":{"id":"b_1"}};
    
    const result = fn(prices)(features);
    
    console.log(result);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>