javascriptfunctional-programminglodashramda.jslifting

How to lift and compose functions together?


I have a list of people in this structure:

const people = [
  {name: 'jenny', friends: ['jeff']},
  {name: 'frank', friends: ['jeff', 'ross']},
  {name: 'sarah', friends: []},
  {name: 'jeff', friends: ['jenny', 'frank']},
  {name: 'russ', friends: []},
  {name: 'calvin', friends: []},
  {name: 'ross', friends: ['frank']},
];

I would like to filter out people two ways: with and without friends; moreover I would like the Predicate of the Array.filter to be lifted, like so:

const peopleWithoutFriends = people.filter(withoutFriends);
console.log(peopleWithoutFriends);

const peopleWithFriends = people.filter(withFriends);
console.log(peopleWithFriends);

I can achieve this behavior by explicitly writing a by function like this:

const by = x => i => {
  return Boolean(get(i, x));
};
const withFriends = by('friends.length');
const peopleWithFriends = people.filter(withFriends);
console.log(peopleWithFriends);

Problem: If I wanted the inverse I would need to explicitly write a whole new function for peopleWithoutFriends

const notBy = x => i => {
  return !Boolean(get(i, x));
};

const withOutFriends = notBy('friends.length');
const peopleWithoutFriends = people.filter(withOutFriends);

I do not want to write my by function twice. I would rather compose smaller functions together.

Question:

How can I write and use small functions like: flow Booleanget curry not and compose withFriends and withOutFriends Predicates for my Array.filter over the list of people.

Repl: https://repl.it/@matthewharwood/ChiefWelloffPaintprogram

const {flow, get, curry} = require('lodash');

const people = [
  {name: 'jenny', friends: ['jeff']},
  {name: 'frank', friends: ['jeff', 'ross']},
  {name: 'sarah', friends: []},
  {name: 'jeff', friends: ['jenny', 'frank']},
  {name: 'russ', friends: []},
  {name: 'calvin', friends: []},
  {name: 'ross', friends: ['frank']},
];
const not = i => !i;

const withFriends = i => flow(
  Boolean,
  get(i, 'friends.length'), // arity of this is 2 so might be harder to lift, is it possible tho with curry?
); // No idea what i'm doing here.


const peopleWithFriends = people.filter(withFriends);
console.log(peopleWithFriends);

const withoutFriends = flow(not, withFriends);
const peopleWithoutFriends = people.filter(withoutFriends);
console.log(peopleWithoutFriends);

Solution

  • Since the result of with/without friends functions is a Boolean, you can negate (or complement) the result of the one to get the other. In addition the arity of the functions is 1 (the object they operate on).

    Lodash/fp:

    const { flow, get, isEmpty, negate } = _;
    
    const people = [
      {name: 'jenny', friends: ['jeff']},
      {name: 'frank', friends: ['jeff', 'ross']},
      {name: 'sarah', friends: []},
      {name: 'jeff', friends: ['jenny', 'frank']},
      {name: 'russ', friends: []},
      {name: 'calvin', friends: []},
      {name: 'ross', friends: ['frank']},
    ];
    
    const withoutFriends = flow(get('friends'), isEmpty); // create a function that gets the friends array, and check if it is empty
    const withFriends = negate(withoutFriends); // negate the result of withoutFriends
    
    const peopleWithFriends = people.filter(withFriends);
    console.log(peopleWithFriends);
    
    const peopleWithoutFriends = people.filter(withoutFriends);
    console.log(peopleWithoutFriends);
    <script src='https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)'></script>

    Ramda:

    const { pipe, prop, isEmpty, complement } = R;
    
    const people = [
      {name: 'jenny', friends: ['jeff']},
      {name: 'frank', friends: ['jeff', 'ross']},
      {name: 'sarah', friends: []},
      {name: 'jeff', friends: ['jenny', 'frank']},
      {name: 'russ', friends: []},
      {name: 'calvin', friends: []},
      {name: 'ross', friends: ['frank']},
    ];
    
    const withoutFriends = pipe(prop('friends'), isEmpty); // create a function that gets the friends array, and check if it is empty
    const withFriends = complement(withoutFriends); // negate the result of withoutFriends
    
    const peopleWithFriends = people.filter(withFriends);
    console.log(peopleWithFriends);
    
    const peopleWithoutFriends = people.filter(withoutFriends);
    console.log(peopleWithoutFriends);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

    Notes:

    1. Both _.flow() and R.pipe execute the sequence from left to right (top to bottom). The functions _.compose() and R.compose order is reversed.
    2. The 1st function in flow/pipe/compose gets everything that is passed to the composed function. The other functions in the sequence always get a single param (the result of the previous function)/.
    3. Both Ramda and Lodash have a reject method, which is the opposite of filter, if the predicate returns true, the item is removed. For example, R.reject(foo, xs) is equivalent to R.filter(R.complement(foo), xs). (noted by @ScottSauyet's in this comment)