最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Functional programming: Return first truthy result of calling a list of different functions with a specific argumen

programmeradmin6浏览0评论

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 badges
Add a ment  | 

7 Answers 7

Reset to default 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.

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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论