Search code examples
javascriptfunctional-programmingramda.jspointfree

Ramda: Remember and use value


I'm trying to "Ramdafy" a function and make it point-free. This is how far I came:

const hasFetchedForFilterQueryString = state => {
  const filterQueryString = getFilterQueryString(state);
  return either(
    pipe(getPages, has(filterQueryString)),
    pipe(getNewlyLoadedUsers, has(filterQueryString)),
  )(state);
};

// or

const hasFetchedForFilterQueryString = state =>
  either(
    pipe(getPages, has(getFilterQueryString(state))),
    pipe(getNewlyLoadedUsers, has(getFilterQueryString(state))),
  )(state);

I wonder if there is a way to write even more point-free, so without holding filterQueryString in a variable and having to manually apply state?


Solution

  • I'm usually with Bergi's comment. Point-free should not be a goal on its own. I was enamored of it when I started writing Ramda, and there are a number of functions in it designed to help make things point-free. I rarely recommend them anymore. I don't think that either useWith or converge often make functions easier to read or maintain.

    But there is a function, lift, known across the FP world which is helpful here. I think of lift as lifting a function from the realm of values to the realm of containers of those values. Thus lift (add) (Maybe(3), Maybe(5)) //=> Maybe(8). When used with functions that yield our values, it might work like this:

    lift ((a, b) => a + b) (
      n => n * 2, 
      n => n - 1
    ) (5) 
    //=> (5 * 2) + (5 -1) //=> 14`.
    

    And that is precisely what you want to do with your function. So here is a lift-based point-free version:

    const hfffqs2 = either (
      lift (has) (getFilterQueryString, getPages),
      lift (has) (getFilterQueryString, getNewlyLoadedUsers)
    )
    

    I find this more readable than either of your versions above.

    I think that version rivals the simplest non-Ramda version I can come up with for readability:

    const hfffqs3 = state => {
      const query = getFilterQueryString(state);
      return query in getPages(state)
          || query in getNewlyLoadedUsers(state)
    }
    

    It's not necessarily more readable, though. And it's less efficient as -- beyond the additional function invocations -- it will have to call getFilterQueryString twice. But it is a readable and maintainable function.

    If lift is still not quite clear, an intermediate format might look like this:

    const hfffqs1 = either (
      state => has(getFilterQueryString(state), getPages(state)),
      state => has(getFilterQueryString(state), getNewlyLoadedUsers(state))
    )
    

    By applying getFilterQueryString and getState to lift (has) we get the equivalent of state => has(getFilterQueryString(state), getPages(state)), and similarly with the next line. We've lifted the function has from the realm of strings and objects to that of functions which return strings and objects. We can pass state to these two functions to get a string and an object.


    You can see these in action in the following snippet:

    // Dummy implementations just for demo.  `state` is a small integer.
    const getFilterQueryString = (state) => ['foo', 'bar', 'baz', 'qux', 'corge', 'grault'][state]
    const getPages = (state) => [{foo: 1, bar: 2}, {foo: 1, corge: 2, grault: 3}, {baz: 1}, {foo: 1, grault: 2}, {corge: 1, baz: 2}, {bar: 1, qux: 2}][state]
    const getNewlyLoadedUsers = (state) => [{baz: 1, qux: 2}, {corge: 1, grault: 2}, {foo: 1, corge: 2}, {baz: 1, qux: 2}, {foo: 1, qux: 2}, {bar: 1, corge: 2}][state]
    
    const hasFetchedForFilterQueryString = state => {
      const filterQueryString = getFilterQueryString(state);
      return either(
        pipe(getPages, has(filterQueryString)),
        pipe(getNewlyLoadedUsers, has(filterQueryString)),
      )(state);
    };
    
    console .log ([0, 1, 2, 3, 4, 5].map(hasFetchedForFilterQueryString))
    
    const hfffqs1 = either (
      state => has(getFilterQueryString(state), getPages(state)),
      state => has(getFilterQueryString(state), getNewlyLoadedUsers(state))
    )
    
    console .log ([0, 1, 2, 3, 4, 5].map(hfffqs1))
    
    const hfffqs2 = either (
      lift (has) (getFilterQueryString, getPages),
      lift (has) (getFilterQueryString, getNewlyLoadedUsers)
    )
    
    console .log ([0, 1, 2, 3, 4, 5].map(hfffqs2))
    
    const hfffqs3 = state => {
      const query = getFilterQueryString(state);
      return query in getPages(state)
          || query in getNewlyLoadedUsers(state)
    }
    
    console .log ([0, 1, 2, 3, 4, 5].map(hfffqs3))
    .as-console-wrapper {min-height: 100% !important; top: 0}
    <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
    <script> const {either, pipe, has, lift} = R                         </script>