Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Functional programming: Return first truthy result of calling a list of different functions with a specific argument

I'm venturing into trying to use functional programming in TypeScript, and am wondering about the most idiomatic way of doing the following using functional libraries such as ramda, remeda or lodash-fp. What I want to achieve is to apply a bunch of different functions to a specific data set and return the first truthy result. Ideally the rest of the functions wouldn't be run once a truthy result has been found, as some of those later in the list are quite computationally expensive. Here's one way of doing this in regular ES6:

const firstTruthy = (functions, data) => {
    let result = null
    for (let i = 0; i < functions.length; i++) {
        res = functions[i](data)
        if (res) {
            result = res
            break
        }
    }
    return result
}
const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]
firstTruthy(functions, 3) // 'multiple of 3'
firstTruthy(functions, 4) // 'times 2 equals 8'
firstTruthy(functions, 8) // 'two less than 10'
firstTruthy(functions, 10) // null

I mean, this function does the job, but is there a ready-made function in any of these libraries that would achieve the same result, or could I chain some of their existing functions together to do this? More than anything I'm just trying to get my head around functional programming and to get some advice on what would be an indiomatic approach to this problem.

like image 930
Jimbali Avatar asked Jan 02 '21 21:01

Jimbali


1 Answers

While Ramda's anyPass is similar in spirit, it simply returns a boolean if any of the functions yield true. Ramda (disclaimer: I'm a Ramda author) does not have this exact function. If you think it belongs in Ramda, please feel free to raise an issue or create a pull request for it. We can't promise that it would be accepted, but we can promise a fair hearing.

Scott Christopher demonstrated what is probably the cleanest Ramda solution.

One suggestion that hasn't been made yet is a simple recursive version, (although Scott Christopher's lazyReduce is some sort of kin.) Here is one technique:

const firstTruthy = ([fn, ...fns], ...args) =>
  fn == undefined 
    ? null
    : fn (...args) || firstTruthy (fns, ...args)

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console .log (firstTruthy (functions, 3)) // 'multiple of 3'
console .log (firstTruthy (functions, 4)) // 'times 2 equals 8'
console .log (firstTruthy (functions, 8)) // 'two less than 10'
console .log (firstTruthy (functions, 10)) // null

I would probably choose to curry the function, either with Ramda's curry or manually like this:

const firstTruthy = ([fn, ...fns]) => (...args) =>
  fn == undefined 
    ? null
    : fn (...args) || firstTruthy (fns) (...args)

// ...

const foo = firstTruthy (functions);

[3, 4, 8, 10] .map (foo) //=> ["multiple of 3", "times 2 equals 8", "two less than 10", null]

Alternatively, I might use this version:

const firstTruthy = (fns, ...args) => fns.reduce((a, f) => a || f(...args), null)

(or again a curried version of it) which is very similar to the answer from Matt Terski, except that the functions here can have multiple arguments. Note that there is a subtle difference. In the original and the answer above, the result of no match is null. Here it is the result of the last function if none of the other were truthy. I imagine this is a minor concern, and we could always fix it up by adding a || null phrase to the end.

like image 58
Scott Sauyet Avatar answered Sep 30 '22 13:09

Scott Sauyet