【问题标题】:Functional programming: Return first truthy result of calling a list of different functions with a specific argument函数式编程:返回调用具有特定参数的不同函数列表的第一个真实结果
【发布时间】:2021-04-09 03:45:35
【问题描述】:

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

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

【问题讨论】:

    标签: javascript typescript functional-programming lodash ramda.js


    【解决方案1】:

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

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

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

    【讨论】:

    • 如果你使用 OPs 函数做一个完整的例子,你会发现你的建议并没有真正奏效。
    【解决方案2】:

    每当我想将一组事物简化为一个值时,我都会使用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 以使结果更具可读性。

    【讨论】:

    • 很好的解决方案。 (您可以留下一个 console.log 并将 4 个调用包装在一个数组字面量中)
    • 这和OP贴的基本一样,只是把for循环改写成reduce。想知道是否有任何来自 fp-ts 的现有函数可以处理这个问题。
    【解决方案3】:

    您可以使用 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

    【讨论】:

      【解决方案4】:

      使用 Ramda,我会以 R.cond 为基础,它接受一个对 [谓词,转换器] 的列表,如果 predicate(data) 为真,则返回 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
      &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"&gt;&lt;/script&gt;

      您还可以通过拆分谓词和返回值直接为 R.cond 创建函数数组 (pairs)。由于 cond 需要一个函数作为转换,所以用 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
      &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"&gt;&lt;/script&gt;

      另一种选择是使用Array.find() 来查找返回真实答案(字符串)的函数。如果找到一个函数(使用optional chaining),使用原始数据再次调用它以获得实际结果,如果没有找到则返回null

      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 循环替换为for...of 循环,并在找到结果时提前返回而不是中断:

      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

      【讨论】:

      • 另一方面,它充满了语句而不是表达式,并且它会改变数据。从函数式编程的角度来看,需要不同的实现是有一些理由的。
      • Op 的代码与 R.any(显而易见)和 R.cond 的底层代码(见更新的答案)非常相似。在这种情况下,我会跳过中间人。
      • 是的,Ramda 会做一些讨厌的事情,所以你不必这样做!我喜欢cond 的实现!
      【解决方案5】:

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

      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
      )
      &lt;script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"&gt;&lt;/script&gt;

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

      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
      )

      【讨论】:

      • 不错!我认为这个lazyReduce 不够笼统。通常我们希望将函数应用于累加器和当前值。所以这样的事情可能会更好:const lazyReduce = (fn, acc, list) =&gt; list.length &gt; 0 ? fn(acc, list[0], (x) =&gt; lazyReduce(fn, x, list.slice(1))) : acc。我可能会写得有点不同,如const lazyReduce = (fn, acc, [x, ...xs]) =&gt; x == undefined ? acc : fn (acc, x, (n) =&gt; lazyReduce (fn, n, xs)) 但也有一个论据const lazyReduce = (fn, acc, [x, ...xs]) =&gt; fn (acc, x, (x) =&gt; lazyReduce (fn, x, xs), () =&gt; acc)
      • 最后一个有点不对劲。可能是这样的:const lazyReduce = (fn, acc, [x, ...xs]) =&gt; x == undefined ? acc : fn (acc, x, (x) =&gt; lazyReduce (fn, x, xs), (x) =&gt; x ),也可以这样称呼:lazyReduce ((acc, x, next, done) =&gt; acc &gt; 20 ? done(acc) : next (acc + x), 0, [8, 6, 7, 5, 3, 0, 9]) //=&gt; 21,当然还有const firstTruthy = (fns, value) =&gt; lazyReduce ((acc, f, next, done, res = f(value)) =&gt; res ? done(res) : next(res), null, fns)
      • 我应该澄清lazyReduce是我的例子实际上是lazyReduceRight,其中(有点眯眼)() =&gt; lazyReduce(fn, acc, list.slice(1)) 累积值在示例中通过调用rest() 访问。
      • 很高兴在 JS 中看到正确的右折叠。但是,我会在 JS 中使用 head/tail 对象对惰性进行编码,后者是 getter。这样你就可以节省调用端的括号。这将不再是折叠,而是迈向流的第一步。
      • 我一定很困惑。你可以使用这个lazyReduce,比如说,将一个数组的值相加,直到总和超过20,然后返回部分总和吗?也就是说,lazyReduce(fn, 0, [8, 6, 7, 5, 3, 0, 9] //=&gt; 0 + 8 + 6 + 7 ==&gt; 21 中的fn 是什么?我觉得好像缺少一个允许这样做的论据。但我知道我可能遗漏了一些重要的东西。
      【解决方案6】:

      虽然 Ramda 的 anyPass 在本质上是相似的,但如果任何函数返回 true,它只会返回一个布尔值。 Ramda(免责声明:我是 Ramda 作者)没有这个确切的功能。如果您认为它属于 Ramda,请随时提出 issue 或为它创建 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 或手动像这样对函数进行 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。这是最后一个函数的结果,如果其他函数都不真实的话。我想这是一个小问题,我们总是可以通过在末尾添加 || null 短语来解决它。

      【讨论】:

        【解决方案7】:

        我觉得你的问题和Is there a Variadic Version of either (R.either)?很像

        大部分的困惑来自恕我直言, 我宁愿建议谈论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),
        );
        &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"&gt;&lt;/script&gt;

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-26
          • 2012-11-21
          相关资源
          最近更新 更多