如何处理 Ramda 中的错误
How to handle errors in Ramda
我对 Ramda 和函数式编程还很陌生,并尝试用 Ramda 重写脚本,但不确定如何以干净的方式使用 Ramda 处理错误。
这就是我所拥有的,有人对如何使用 Ramda 以功能方式重写它有任何指示吗?
const targetColumnIndexes = targetColumns.map(h => {
if (header.indexOf(h) == -1) {
throw new Error(`Target Column Name not found in CSV header column: ${h}`)
}
return header.indexOf(h)
})
供参考,这些是 header
和 targetColumns
的值
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const targetColumns = [ 'CurrencyCode', 'Name' ]
所以我需要:
- 映射到 targetColumns
- return 来自 header
的 targetColumn 的索引
- 如果索引为
-1
则抛出错误
正如 customcommander 所说,函数式编程不容易使这种抛出异常的方式变得容易是有充分理由的:推理起来要困难得多。
"What does you function return?"
"A number."
"Always?"
"Yes, ... well unless it throws an exception."
"Then what does it return?"
"Well it doesn't."
"So it returns a number or nothing at all?"
"I guess so."
"Hmmm."
函数式编程中最常见的操作之一是组合两个函数。但这只有在一个函数的输出与其后继函数的输入相匹配时才有效。如果第一个可能抛出异常,这就很困难。
为了解决这个问题,FP 世界使用捕捉失败概念的类型。您可能已经看到有关 Maybe
类型的讨论,它处理可能是 null
的值。另一种常见的是 Either
(有时 Result
),它有两个子类型,用于错误情况和成功情况(分别为 Left
和 Right
用于 Either
或 Error
和 Ok
for Result
.) 在这些类型中,发现的第一个错误被捕获并向下传递给任何需要它的人,而成功案例继续处理。 (还有 Validation
类型可以捕获错误列表。)
这些类型有很多实现。请参阅 fantasy-land list 以获得一些建议。
Ramda 曾经有自己的一组这些类型,但已经放弃维护它。 Folktale 和 Sanctuary 是我们经常为此推荐的。但即使是 Ramda 的旧实现也应该可以。这个版本使用 Folktale's data.either
,因为我更了解它,但后来的 Folktale 版本将其替换为 Result
。
下面的代码块显示了我如何使用 Either
s 来处理这种失败的概念,特别是我们如何使用 R.sequence
将 Eithers
的数组转换为 Either
拿着一个数组。如果输入包含任何 Left
,则输出只是 Left
。如果全部是 Right
,那么输出是一个 Right
,包含它们的值的数组。有了这个,我们可以将我们所有的列名转换成 Either
s 来捕获值或错误,然后将它们组合成一个结果。
需要注意的是这里没有抛出异常。我们的功能将正确组合。失败的概念封装在类型中。
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const getIndices = (header) => (targetColumns) =>
map((h, idx = header.indexOf(h)) => idx > -1
? Right(idx)
: Left(`Target Column Name not found in CSV header column: ${h}`)
)(targetColumns)
const getTargetIndices = getIndices(header)
// ----------
const goodIndices = getTargetIndices(['CurrencyCode', 'Name'])
console.log('============================================')
console.log(map(i => i.toString(), goodIndices)) //~> [Right(0), Right(1)]
console.log(map(i => i.isLeft, goodIndices)) //~> [false, false]
console.log(map(i => i.isRight, goodIndices)) //~> [true, true]
console.log(map(i => i.value, goodIndices)) //~> [0, 1]
console.log('--------------------------------------------')
const allGoods = sequence(of, goodIndices)
console.log(allGoods.toString()) //~> Right([0, 1])
console.log(allGoods.isLeft) //~> false
console.log(allGoods.isRight) //~> true
console.log(allGoods.value) //~> [0, 1]
console.log('============================================')
//----------
const badIndices = getTargetIndices(['CurrencyCode', 'Name', 'FooBar'])
console.log('============================================')
console.log(map(i => i.toString(), badIndices)) //~> [Right(0), Right(1), Left('Target Column Name not found in CSV header column: FooBar')
console.log(map(i => i.isLeft, badIndices)) //~> [false, false, true]
console.log(map(i => i.isRight, badIndices)) //~> [true, true, false]
console.log(map(i => i.value, badIndices)) //~> [0, 1, 'Target Column Name not found in CSV header column: FooBar']
console.log('--------------------------------------------')
const allBads = sequence(of, badIndices)
console.log(allBads.toString()) //~> Left('Target Column Name not found in CSV header column: FooBar')
console.log(allBads.isLeft) //~> true
console.log(allBads.isRight) //~> false
console.log(allBads.value) //~> 'Target Column Name not found in CSV header column: FooBar'
console.log('============================================')
.as-console-wrapper {height: 100% !important}
<script src="//bundle.run/ramda@0.26.1"></script>
<!--script src="//bundle.run/ramda-fantasy@0.8.0"></script-->
<script src="//bundle.run/data.either@1.5.2"></script>
<script>
const {map, includes, sequence} = ramda
const Either = data_either;
const {Left, Right, of} = Either
</script>
我的要点是 goodIndices
和 badIndices
等值本身很有用。如果我们想对它们进行更多处理,我们可以简单地 map
覆盖它们。请注意,例如
map(n => n * n, Right(5)) //=> Right(25)
map(n => n * n, Left('oops')) //=> Left('oops'))
所以我们的错误被单独留下,我们的成功被进一步处理。
map(map(n => n + 1), badIndices)
//=> [Right(1), Right(2), Left('Target Column Name not found in CSV header column: FooBar')]
这就是这些类型的意义所在。
我要提出不同意见:Either
是通过静态类型系统的圆孔敲打方钉的好解决方案。 JavaScript(缺乏正确性保证)带来的收益较少,需要大量认知开销。
如果有问题的代码需要快速(如分析和记录的性能预算所证明的那样),您应该以命令式风格编写它。
如果它不需要很快(或者在执行以下操作时足够快),那么您可以检查您正在迭代的内容是否是 CSV 的正确子集(或完全匹配) headers:
// We'll assume sorted data. You can check for array equality in other ways,
// but for arrays of primitives this will work.
if (`${header}` !== `${targetColumns}`) throw new Error('blah blah');
这将关注点与检查数据是否有效和执行所需转换完全分开。
如果您只担心长度,那么只需检查一下,等等。
如果抛出异常违背函数式编程,我想这让我成为无政府主义者。以下是如何以函数式方式编写函数
const { map, switchCase } = require('rubico')
const header = ['CurrencyCode', 'Name', 'CountryCode']
const ifHeaderDoesNotExist = h => header.indexOf(h) === -1
const getHeaderIndex = h => header.indexOf(h)
const throwHeaderNotFound = h => {
throw new Error(`Target Column Name not found in CSV header column: ${h}`)
}
const getTargetColumnIndex = switchCase([
ifHeaderDoesNotExist, throwHeaderNotFound,
getHeaderIndex,
])
const main = () => {
const targetColumns = ['CurrencyCode', 'Name']
const targetColumnIndexes = map(getTargetColumnIndex)(targetColumns)
console.log(targetColumnIndexes) // => [0, 1]
}
main()
地图就像 ramda 的地图
你可以把上面的switchCase想成
const getTargetColumnIndex = x =>
ifHeaderDoesNotExist(x) ? throwHeaderNotFound(x) : getHeaderIndex(x)
我对 Ramda 和函数式编程还很陌生,并尝试用 Ramda 重写脚本,但不确定如何以干净的方式使用 Ramda 处理错误。 这就是我所拥有的,有人对如何使用 Ramda 以功能方式重写它有任何指示吗?
const targetColumnIndexes = targetColumns.map(h => {
if (header.indexOf(h) == -1) {
throw new Error(`Target Column Name not found in CSV header column: ${h}`)
}
return header.indexOf(h)
})
供参考,这些是 header
和 targetColumns
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const targetColumns = [ 'CurrencyCode', 'Name' ]
所以我需要:
- 映射到 targetColumns
- return 来自 header 的 targetColumn 的索引
- 如果索引为
-1
则抛出错误
正如 customcommander 所说,函数式编程不容易使这种抛出异常的方式变得容易是有充分理由的:推理起来要困难得多。
"What does you function return?"
"A number."
"Always?"
"Yes, ... well unless it throws an exception."
"Then what does it return?"
"Well it doesn't."
"So it returns a number or nothing at all?"
"I guess so."
"Hmmm."
函数式编程中最常见的操作之一是组合两个函数。但这只有在一个函数的输出与其后继函数的输入相匹配时才有效。如果第一个可能抛出异常,这就很困难。
为了解决这个问题,FP 世界使用捕捉失败概念的类型。您可能已经看到有关 Maybe
类型的讨论,它处理可能是 null
的值。另一种常见的是 Either
(有时 Result
),它有两个子类型,用于错误情况和成功情况(分别为 Left
和 Right
用于 Either
或 Error
和 Ok
for Result
.) 在这些类型中,发现的第一个错误被捕获并向下传递给任何需要它的人,而成功案例继续处理。 (还有 Validation
类型可以捕获错误列表。)
这些类型有很多实现。请参阅 fantasy-land list 以获得一些建议。
Ramda 曾经有自己的一组这些类型,但已经放弃维护它。 Folktale 和 Sanctuary 是我们经常为此推荐的。但即使是 Ramda 的旧实现也应该可以。这个版本使用 Folktale's data.either
,因为我更了解它,但后来的 Folktale 版本将其替换为 Result
。
下面的代码块显示了我如何使用 Either
s 来处理这种失败的概念,特别是我们如何使用 R.sequence
将 Eithers
的数组转换为 Either
拿着一个数组。如果输入包含任何 Left
,则输出只是 Left
。如果全部是 Right
,那么输出是一个 Right
,包含它们的值的数组。有了这个,我们可以将我们所有的列名转换成 Either
s 来捕获值或错误,然后将它们组合成一个结果。
需要注意的是这里没有抛出异常。我们的功能将正确组合。失败的概念封装在类型中。
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const getIndices = (header) => (targetColumns) =>
map((h, idx = header.indexOf(h)) => idx > -1
? Right(idx)
: Left(`Target Column Name not found in CSV header column: ${h}`)
)(targetColumns)
const getTargetIndices = getIndices(header)
// ----------
const goodIndices = getTargetIndices(['CurrencyCode', 'Name'])
console.log('============================================')
console.log(map(i => i.toString(), goodIndices)) //~> [Right(0), Right(1)]
console.log(map(i => i.isLeft, goodIndices)) //~> [false, false]
console.log(map(i => i.isRight, goodIndices)) //~> [true, true]
console.log(map(i => i.value, goodIndices)) //~> [0, 1]
console.log('--------------------------------------------')
const allGoods = sequence(of, goodIndices)
console.log(allGoods.toString()) //~> Right([0, 1])
console.log(allGoods.isLeft) //~> false
console.log(allGoods.isRight) //~> true
console.log(allGoods.value) //~> [0, 1]
console.log('============================================')
//----------
const badIndices = getTargetIndices(['CurrencyCode', 'Name', 'FooBar'])
console.log('============================================')
console.log(map(i => i.toString(), badIndices)) //~> [Right(0), Right(1), Left('Target Column Name not found in CSV header column: FooBar')
console.log(map(i => i.isLeft, badIndices)) //~> [false, false, true]
console.log(map(i => i.isRight, badIndices)) //~> [true, true, false]
console.log(map(i => i.value, badIndices)) //~> [0, 1, 'Target Column Name not found in CSV header column: FooBar']
console.log('--------------------------------------------')
const allBads = sequence(of, badIndices)
console.log(allBads.toString()) //~> Left('Target Column Name not found in CSV header column: FooBar')
console.log(allBads.isLeft) //~> true
console.log(allBads.isRight) //~> false
console.log(allBads.value) //~> 'Target Column Name not found in CSV header column: FooBar'
console.log('============================================')
.as-console-wrapper {height: 100% !important}
<script src="//bundle.run/ramda@0.26.1"></script>
<!--script src="//bundle.run/ramda-fantasy@0.8.0"></script-->
<script src="//bundle.run/data.either@1.5.2"></script>
<script>
const {map, includes, sequence} = ramda
const Either = data_either;
const {Left, Right, of} = Either
</script>
我的要点是 goodIndices
和 badIndices
等值本身很有用。如果我们想对它们进行更多处理,我们可以简单地 map
覆盖它们。请注意,例如
map(n => n * n, Right(5)) //=> Right(25)
map(n => n * n, Left('oops')) //=> Left('oops'))
所以我们的错误被单独留下,我们的成功被进一步处理。
map(map(n => n + 1), badIndices)
//=> [Right(1), Right(2), Left('Target Column Name not found in CSV header column: FooBar')]
这就是这些类型的意义所在。
我要提出不同意见:Either
是通过静态类型系统的圆孔敲打方钉的好解决方案。 JavaScript(缺乏正确性保证)带来的收益较少,需要大量认知开销。
如果有问题的代码需要快速(如分析和记录的性能预算所证明的那样),您应该以命令式风格编写它。
如果它不需要很快(或者在执行以下操作时足够快),那么您可以检查您正在迭代的内容是否是 CSV 的正确子集(或完全匹配) headers:
// We'll assume sorted data. You can check for array equality in other ways,
// but for arrays of primitives this will work.
if (`${header}` !== `${targetColumns}`) throw new Error('blah blah');
这将关注点与检查数据是否有效和执行所需转换完全分开。
如果您只担心长度,那么只需检查一下,等等。
如果抛出异常违背函数式编程,我想这让我成为无政府主义者。以下是如何以函数式方式编写函数
const { map, switchCase } = require('rubico')
const header = ['CurrencyCode', 'Name', 'CountryCode']
const ifHeaderDoesNotExist = h => header.indexOf(h) === -1
const getHeaderIndex = h => header.indexOf(h)
const throwHeaderNotFound = h => {
throw new Error(`Target Column Name not found in CSV header column: ${h}`)
}
const getTargetColumnIndex = switchCase([
ifHeaderDoesNotExist, throwHeaderNotFound,
getHeaderIndex,
])
const main = () => {
const targetColumns = ['CurrencyCode', 'Name']
const targetColumnIndexes = map(getTargetColumnIndex)(targetColumns)
console.log(targetColumnIndexes) // => [0, 1]
}
main()
地图就像 ramda 的地图
你可以把上面的switchCase想成
const getTargetColumnIndex = x =>
ifHeaderDoesNotExist(x) ? throwHeaderNotFound(x) : getHeaderIndex(x)