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



我正在冒险尝试在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

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

虽然 Ramda 的anyPass在精神上是相似的,但如果任何函数产生 true,它只是返回一个布尔值。 Ramda(免责声明:我是Ramda的作者)没有这个确切的功能。 如果您认为它属于Ramda,请随时提出问题或为其创建拉取请求。 我们不能保证它会被接受,但我们可以保证公平的听证会。

斯科特·克里斯托弗(Scott Christopher)展示了可能是最干净的Ramda解决方案。

一个尚未提出的建议是一个简单的递归版本(尽管斯科特克里斯托弗的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. 在这里,如果其他函数都不真实,则它是最后一个函数的结果。 我想这是一个小问题,我们总是可以通过在末尾添加一个|| null短语来修复它。

您可以使用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.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的"惰性"版本,仅当您应用作为累积值传递的函数时,该版本才会继续,该函数继续递归地遍历列表。这使您可以在归约函数内部控制短路,而无需应用计算列表中其余值的函数。

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
)

每当我想将一系列事物简化为单个值时,我都会使用reduce()方法。这可以在这里工作。

声明一个化简器,该化简器调用数组中的函数,直到找到真实结果。

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以使结果更具可读性。

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

你也可以通过拆分谓词和返回值,直接为 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
<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()查找返回真实答案(字符串)的函数。如果找到函数(使用可选链接),请使用原始数据再次调用它以获得实际结果,如果未找到,则返回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...of循环替换for循环,并在找到结果时提前返回而不是中断:

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

我认为您的问题与是否存在任一(R.both)的可变参数版本非常相似?

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

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>

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

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

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

最新更新