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 putationally 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.
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 putationally 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.
Share Improve this question asked Jan 2, 2021 at 21:21 JimbaliJimbali 2,5381 gold badge24 silver badges28 bronze badges7 Answers
Reset to default 7While 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.
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
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./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
)
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.
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./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./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
I think your question is very similar to Is there a Variadic Version of either (R.either)?
Most of the confusion es 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./ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>
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.