6

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.

Jimbali
  • 2,065
  • 1
  • 20
  • 24

7 Answers7

7

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
5

You could use Array#some with a short circuit on a truthy value.

const
    firstTruthy = (functions, data) => {
        let result;
        functions.some(fn => result = fn(data));
        return result || null;
    },
    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
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
4

Ramda has a way of short-circuiting R.reduce (and a couple of others) using the R.reduced function to indicate that it should stop iterating through the list. This not only avoids applying further functions in the list, but also short-circuits iterating further through the list itself which can be useful if the list you are working with is potentially large.

const firstTruthy = (fns, value) =>
  R.reduce((acc, nextFn) => {
    const nextVal = nextFn(value)
    return nextVal ? R.reduced(nextVal) : acc
  }, null, fns)

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'
  firstTruthy(functions, 4), // 'times 2 equals 8'
  firstTruthy(functions, 8), // 'two less than 10'
  firstTruthy(functions, 10) // null
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>

An alternative option is to create a "lazy" version of reduce which only continues if you apply the function passed as the accumulated value which continues iterating recursively through the list. This gives you control inside the reducing function to short-circuit by not applying the function that evaluates the rest of the values in the list.

const lazyReduce = (fn, emptyVal, list) =>
  list.length > 0
    ? fn(list[0], () => lazyReduce(fn, emptyVal, list.slice(1)))
    : emptyVal

const firstTruthy = (fns, value) =>
  lazyReduce((nextFn, rest) => nextFn(value) || rest(), null, fns)

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'
  firstTruthy(functions, 4), // 'times 2 equals 8'
  firstTruthy(functions, 8), // 'two less than 10'
  firstTruthy(functions, 10) // null
)
Scott Christopher
  • 6,458
  • 23
  • 26
  • Nice! I think this `lazyReduce` isn't general enough. Usually we would want to apply the function to an accumulator and the current value. So something like this might be better: `const lazyReduce = (fn, acc, list) => list.length > 0 ? fn(acc, list[0], (x) => lazyReduce(fn, x, list.slice(1))) : acc`. I might write it a bit differently, as `const lazyReduce = (fn, acc, [x, ...xs]) => x == undefined ? acc : fn (acc, x, (n) => lazyReduce (fn, n, xs))` But there is also an argument for `const lazyReduce = (fn, acc, [x, ...xs]) => fn (acc, x, (x) => lazyReduce (fn, x, xs), () => acc)` – Scott Sauyet Jan 03 '21 at 03:19
  • That last one was off a bit. Perhaps something like this: `const lazyReduce = (fn, acc, [x, ...xs]) => x == undefined ? acc : fn (acc, x, (x) => lazyReduce (fn, x, xs), (x) => x )`, which might be called like this: `lazyReduce ((acc, x, next, done) => acc > 20 ? done(acc) : next (acc + x), 0, [8, 6, 7, 5, 3, 0, 9]) //=> 21` or of course `const firstTruthy = (fns, value) => lazyReduce ((acc, f, next, done, res = f(value)) => res ? done(res) : next(res), null, fns)` – Scott Sauyet Jan 03 '21 at 03:46
  • I should've clarified that the `lazyReduce` is my example is actually a `lazyReduceRight`, where (with a bit of squinting) `() => lazyReduce(fn, acc, list.slice(1))` _is_ the accumulated value accessed by calling `rest()` in the example. – Scott Christopher Jan 03 '21 at 05:31
  • 2
    It's nice to see a proper right fold in JS. However, I'd encode laziness with a head/tail object in JS, where the latter is a getter. This way you could spare the brackets on the call side. This wouldn't be a fold anymore, but rather the first step towards streams. –  Jan 03 '21 at 09:53
  • I was just pointing out that `acc` isn't passed into the reducing function, which happens to work for this problem, but not usually. Our am I missing something? – Scott Sauyet Jan 03 '21 at 18:19
  • I've edited the example to use `emptyVal` rather than `acc` inside `lazyReduce`, as it perhaps better indicates the purpose of that argument. I have however kept `rest` named as it is rather than renaming it to `acc` in `firstTruthy`, as I feel it helps convey how the accumulated value is evaluated. – Scott Christopher Jan 03 '21 at 19:52
  • 1
    I must be confused. Could you use this lazyReduce to, say, add the values of an array until the sum exceeds 20, returning that partial sum? That is, what would be the `fn` in `lazyReduce(fn, 0, [8, 6, 7, 5, 3, 0, 9] //=> 0 + 8 + 6 + 7 ==> 21`? I feel as though there is an argument missing to allow that. But I know I'm probably missing something important. – Scott Sauyet Jan 04 '21 at 15:17
  • Not in the traditional sense of lazy right fold, as short-cirtcuiting is only possible when the accumulating value is not needed, i.e. it discards the accumulating value in order to short-circuit. What I believe you're suggesting (which I apologise if I have been speaking past) is combining a strict left fold with a lazy right fold, which I've never really considered before but could prove to be useful. So the reducing function would receive three arguments like `fn(strictLeftAcc, currentVal, lazyRightAcc)` and short-circuiting would take place is `lazyRightAcc()` wasn't called. – Scott Christopher Jan 04 '21 at 20:45
  • Thanks a lot for your answers! Haven't got my head around every little bit of this yet, but it's given me loads to think about as I try and get into the functional way of thinking. I have accepted this answer because it informed me of the existence of `R.reduced`, which is very useful and answers the original question very well, but both of your answers have been very informative. – Jimbali Jan 10 '21 at 15:51
3

Anytime I want to reduce an array of things into a single value, I reach for the reduce() method. That could work here.

Declare a reducer that invokes functions in the array until a truthy result is found.

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),
];

const firstTruthy = (functions, x) =>
    functions.reduce(
        (accumulator, currentFunction) => accumulator || currentFunction(x),
        false
    );

[3, 4, 8, 10].map(x => console.log(firstTruthy(functions, x)))

I added a console.log to make the result more readable.

Matt Terski
  • 891
  • 8
  • 11
  • 1
    Good solution. (You could leave one `console.log` and wrap the 4 calls in an array literal) – trincot Jan 02 '21 at 21:57
  • 1
    this is basically the same thing as OP posted, just rewrite for loop into reduce. wondering if there is any existing functions from fp-ts that can handle this. – ABOS Jan 02 '21 at 22:03
1

Using Ramda, I would base this around R.cond, which takes a a list of pairs [predicate, transformer], and if predicate(data) is truthy it returns transformer(data). In your case the transformer, and predicate are the same, so you can use R.map to repeat them:

const { curry, cond, map, repeat, __ } = R

const firstTruthy = curry((fns, val) => cond(map(repeat(__, 2), fns))(val) ?? null)

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
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

You can also create your array of functions (pairs) directly for R.cond by splitting the predicate, and the return value. Since cond expects a function as the transform, wrap the return value with R.alwyas:

const { curry, cond, always } = R

const firstTruthy = curry((pairs, val) => cond(pairs)(val) ?? null)

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

console.log(firstTruthy(pairs, 3)) // 'multiple of 3'
console.log(firstTruthy(pairs, 4)) // 'times 2 equals 8'
console.log(firstTruthy(pairs, 8)) // 'two less than 10'
console.log(firstTruthy(pairs, 10)) // null
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

Another option is to use Array.find() to find a function that returns a truthy answer (the string). If a function is found (using optional chaining), call it again with the original data to get the actual result, or return null if none found:

const firstTruthy = (fns, val) => fns.find(fn => fn(val))?.(val) ?? null

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

However, your code is doing exactly what you want, is readable, and also terminates early when a result is found.

The only things I would change are to replace the for loop, with a for...of loop, and return early instead of breaking, when a result is found:

const firstTruthy = (functions, data) => {
  for (const fn of functions) {
    const result = fn(data)
    
    if (result) return result
  }
  
  return null
}

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
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • On the other hand it's full of statements instead of expressions and it mutates data. From a functional programming perspective, there is some rationale for wanting a different implementation. – Scott Sauyet Jan 03 '21 at 03:22
  • The Op's code is pretty similar to the underlying code of R.any (the obvious), and the R.cond (see updated answer). In this case, I would just skip the middleman. – Ori Drori Jan 03 '21 at 07:16
  • 1
    Yes, Ramda does the nasty stuff so you don't have to! I liked the `cond` implementation! – Scott Sauyet Jan 03 '21 at 18:10
1

I think your question is very similar to Is there a Variadic Version of either (R.either)?

Most of the confusion comes from the wording imho, I'd rather suggest to talk of firstMatch instead of firstTruthy.

a firstMatch is basically a either function, and in your case a variadic either function.

const either = (...fns) => (...values) => {
  const [left = R.identity, right = R.identity, ...rest] = fns;
  
  return R.either(left, right)(...values) || (
    rest.length ? either(...rest)(...values) : null
  );
};

const firstMatch = either(
  (i) => i % 3 === 0 && 'multiple of 3',
  (i) => i * 2 === 8 && 'times 2 equals 8',
  (i) => i + 2 === 10 && 'two less than 10',
)

console.log(
  firstMatch(8),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
-1

Use Array.prototype.find and refactor your code:

const input = [3, 4, 8, 10];
const firstTruthy = input.find(value => functions.find(func => func(value)))

Basically, find returns the first value that provides true using the callback function. It stops the iteration on the array once the value is found.

misha1109
  • 358
  • 2
  • 5
  • 2
    If you make a complete example using OPs function you'd see that your suggestion does not really work. – zerkms Jan 02 '21 at 21:56