4

I am using Ramda.js for selector functions, to access data in a Redux store. What I want is to define my selectors as functions not referencing the state that the selectors act on, like:

const getUserName = path(['user', 'name']);

const name = getUserName({
  user: {
    name: 'Some Name'
  }
});

This is easy for simple selectors, but sometimes becomes a problem for composed selectors.

Here is an example, where some items needs to be resolved, referenced by their id on an object:

const getItemById = id => state => path(['items', id], state);

const getConnectedItemIds = obj => path(['items'], obj);

const getItemsFromObj = obj => state => {
    const ids = getConnectedItemIds(obj);
    return ids.map(id => getItemById(id)(state));
};

The first function can easily be expressed without reference to state, and the second function without obj, something I believe is called point-free style. But how to write the third function without state?

I am looking for how to rewrite the third function using Ramda, but also rules and procedures regarding this, such as (without knowing if its true):

All composed functions need to have state as their last argument to be able to pull it out in the final composition.

William
  • 741
  • 4
  • 9

4 Answers4

5

There are many good suggestions already here. Probably most important is the advice to use point-free only when it improves readability, and not as a goal on its own.

My answer does use a point-free version of your main function and of one of your helpers, but skips it for the other, where I think readability would suffer.

const getItemById = id => path(['items', id]);

const getConnectedItemIds = prop ('items');

const getItemsFromObj = pipe (
  getConnectedItemIds, 
  map (getItemById), 
  juxt
)

const obj = {foo: 42, items: ['8', '17']}
const state = {bar: 1, items: {'6': 'a', '8': 'b', '14': 'c', '17': 'd', '25': 'e'}}

console .log (
  getItemsFromObj (obj) (state)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {juxt, map, path, pipe, prop} = R                      </script>

The important function here is juxt, which applies an array of functions to the same values, returning an array of the results.

Here getItemById is simplified from the original, by removing the state point, but making this point-free can be done only at a cost in readability, as far as I can tell. Other had suggestions for this, all of which are fine, but as they pointed out, none were as readable as the above. My version looked like this:

const getItemById = compose (path, flip (append) (['items']));
//  or              pipe (flip (append) (['items']), path);

I don't think it's any better or worse than the suggestions in the other answers, but none of them is as easy to read as

const getItemById = id => path(['items', id]);

Finally, if those helper functions are not used elsewhere, I think it can actually improve readability to inline them:

const getItemsFromObj = pipe (
  prop ('items'), 
  map (id => path(['items', id])), 
  juxt
)

Note, although I didn't use it here, I really do like customcommander's suggestion of propOr([], 'items') for getConnectedItemIds. It cleanly removes one potential point of failure.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I love this, much cleaner than mine which was using `applyTo` – Hitmands Jul 19 '19 at 13:03
  • 1
    @Hitmands: Although I'm happy with this, I don't like how unintuitive the name `juxt` is. We borrowed it from Clojure, and I'm guessing that the name comes from `juxtapose`, but it's not something that one might search for by that name. Then again, I don't have a good alternative... – Scott Sauyet Jul 19 '19 at 13:44
  • well, I personally think that the concept of juxtaposition well applies to the goal of this function... I see how astonishing one might feel at first approach, but that is what it does... – Hitmands Jul 19 '19 at 21:04
  • @Hitmands: I think it would have been better named `juxtapose`, but it's too late for that. – Scott Sauyet Jul 19 '19 at 22:58
  • could add `juxt` as alias of `juxtapose` – Mulan Jul 20 '19 at 16:46
  • @user633183: Ramda remove aliases entirely from the library some years ago because of reasons described in David Chambers' [parable](https://github.com/ramda/ramda/pull/574#issuecomment-64124850). It's not likely to reintroduce them. – Scott Sauyet Jul 21 '19 at 16:25
3

I do like pointfree style. I think it forces you to think twice about the design of your functions, which is always a good thing. However it should never be a goal. Use it only when it makes sense.

This function is already good enough:

const getItemById = id => state => path(['items', id], state);

My only suggestion would be to use curry:

const getItemById = curry((id, state) => path(['items', id], state));

You could convert it into pointfree style though, but I don't think it's actually worth it:

const getItemById = useWith(path, [pair('items'), identity]);

Let's go back to your question.

We have these two functions:

const getItemById = curry((id, state) => path(['items', id], state));
const getConnectedItemIds = propOr([], 'items');

You could design a getItemsFromObj pointfree style like this:

const getItemsFromObj = useWith(flip(map), [getConnectedItemIds, flip(getItemById)]);

Or you could also simply do:

const getItemsFromObj = curry((obj, state) => {
  const ids = getConnectedItemIds(obj);
  const idFn = flip(getItemById)(state);
  return map(idFn, ids);
});

Which version would I recommend? I don't know; here's a couple of ideas to think about:

  1. Does it feel natural to you?
  2. What's your team affinity with FP? Can you train them? Go easy if they're just starting
  3. In six months, which version would you feel more comfortable dealing with?

One thing I'd suggest though is that you get familiar with the Hindley-Milner type system. It's definitely time well invested.

Don't let anybody tell you that you're not doing functional programming correctly if you're not doing it pointfree style.

customcommander
  • 17,580
  • 5
  • 58
  • 84
2

I think you could still write it in a point-free fashion, readability, however, gets a bit compromised.. Hope it helps :)

/**
 * @param {string} id
 * @param {Object<string, *>} state
**/
const getItemById = R.useWith(R.prop, [
  R.identity,
  R.prop('items'),
]);

const getItemsFromObj = R.useWith(R.flip(R.map), [
  R.pipe(R.prop('items'), R.map(getItemById)),
  R.applyTo,
]);


const items = {
  1: { id: 1, title: 'Hello World' },
  2: { id: 2, title: 'Hello Galaxy' },
  3: { id: 3, title: 'Hello Universe' },
};

const state = { items };

// this should only take 'world' and 'universe';
console.log(
  'result', 
  getItemsFromObj({ items: [1, 3] }, state),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>

Notes:

  1. Ramda functions are all curried, so you do not need to declare arguments in tail position: obj => path(['items'], obj); is equal to path(['items']);
  2. Being point-free helps to write small and focussed functions, but it should be balanced with composition readability
Hitmands
  • 13,491
  • 4
  • 34
  • 69
1

It's possible to make it both short and point free, but I'm not sure if it's more readable than the a function using variables.

const getItemsFromObj = R.useWith(R.props, [R.prop('items'), R.prop('items')])

I'm not using your getItemById here, since this case is ideal for R.props instead. If you really want to use both of your original functions, you could write it like this.

const getItemsFromObj = R.useWith(
  R.flip(R.map), [getConnectedItemIds, R.flip(R.uncurryN(2, getItemById))]
)

The flip and uncurry is needed to reverse the getItemById function arguments from id -> state -> value into state -> id -> value

Håken Lid
  • 22,318
  • 9
  • 52
  • 67