函数式编程:Return 使用特定参数调用不同函数列表的第一个真实结果

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

我正在冒险尝试在 TypeScript 中使用函数式编程,并且想知道使用 ramda、remeda 或 lodash-fp 等函数库执行以下操作的最惯用方法。我想要实现的是将一堆不同的函数应用于特定的数据集和 return 第一个真实的结果。理想情况下,一旦找到真实结果,其余函数就不会 运行 ,因为列表中后面的一些函数在计算上非常昂贵。这是在常规 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

我的意思是,这个函数可以完成这项工作,但是这些库中是否有现成的函数可以达到相同的结果,或者我可以将它们的一些现有函数链接在一起来做到这一点吗?最重要的是,我只是想了解函数式编程,并就什么是解决这个问题的惯用方法获得一些建议。

使用 Array.prototype.find 并重构您的代码:

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

基本上,找到 returns 使用回调函数提供 true 的第一个值。一旦找到值,它就会停止对数组的迭代。

任何时候我想将一组事物缩减为一个值,我都会使用 reduce() 方法。这在这里可以工作。

声明一个 reducer,它调用数组中的函数,直到找到真实的结果。

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)))

我添加了一个 console.log 以使结果更具可读性。

您可以使用 Array#some 并在真值上短路。

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,我会以 R.cond 为基础,它需要一个 [predicate, transformer] 对列表,如果 predicate(data) 是真实的,它 returns transformer(data).在您的情况下,转换器和谓词是相同的,因此您可以使用 R.map 重复它们:

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>

您还可以通过拆分谓词和 return 值直接为 R.cond 创建函数数组 (pairs)。由于 cond 需要一个函数作为转换,因此用 R.alwyas:

包装 return 值

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>

另一种选择是使用 Array.find() 找到一个 return 是真实答案(字符串)的函数。如果找到一个函数(使用 optional chaining),用原始数据再次调用它来得到实际结果,或者 return null 如果 none 找到:

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

但是,您的代码完全符合您的要求,具有可读性,并且在找到结果时也会提前终止。

我唯一要更改的是用 for...of 循环替换 for 循环,并在发现结果时尽早 return 而不是中断:

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

Ramda 有一种使用 R.reduced 函数使 R.reduce(和其他几个)短路的方法,以指示它应该停止遍历列表。这不仅避免了在列表中应用更多函数,而且还避免了在列表本身中进一步迭代的短路,如果您正在使用的列表可能很大,这将很有用。

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>

另一种选择是创建 reduce 的“惰性”版本,只有当您应用作为累加值传递的函数继续递归遍历列表时,它才会继续。这使您可以通过 not 应用评估列表中其余值的函数来控制减少函数的短路。

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
)

虽然 Ramda 的 anyPass 在本质上是相似的,但它只是 returns 一个布尔值,如果任何函数产生 true。 Ramda(免责声明:我是 Ramda 作者)没有这个确切的功能。如果您认为它属于 Ramda,请随时为它提出 issue or create a pull request。我们不能保证它会被接受,但我们可以保证公平的听证会。

Scott Christopher 展示了可能是最干净的 Ramda 解决方案。

一个尚未提出的建议是一个简单的递归版本,(尽管 Scott Christopher 的 lazyReduce 是某种同类。)这是一种技术:

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

我可能会选择使用 Ramda 的 curry 或手动像这样来柯里化函数:

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]

或者,我可能会使用这个版本:

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

(或者它的柯里化版本)这与 Matt Terski 的回答非常相似,除了这里的函数可以有多个参数。请注意,存在细微差别。在原文和上面的答案中,不匹配的结果是null。这是最后一个函数的结果,如果另一个函数的 none 为真。我想这是一个小问题,我们总是可以通过在末尾添加一个 || null 短语来解决它。

我认为您的问题与

非常相似

大部分混淆来自措辞恕我直言, 我宁愿建议谈论 firstMatch 而不是 firstTruthy.

a firstMatch 基本上是一个 either 函数,在你的例子中是一个可变函数。

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>