函数式编程: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>
我正在冒险尝试在 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:
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>