如何处理 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)
  })

供参考,这些是 headertargetColumns

的值
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const targetColumns = [ 'CurrencyCode', 'Name' ]

所以我需要:

正如 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),它有两个子类型,用于错误情况和成功情况(分别为 LeftRight 用于 EitherErrorOk for Result.) 在这些类型中,发现的第一个错误被捕获并向下传递给任何需要它的人,而成功案例继续处理。 (还有 Validation 类型可以捕获错误列表。)

这些类型有很多实现。请参阅 fantasy-land list 以获得一些建议。

Ramda 曾经有自己的一组这些类型,但已经放弃维护它。 Folktale 和 Sanctuary 是我们经常为此推荐的。但即使是 Ramda 的旧实现也应该可以。这个版本使用 Folktale's data.either,因为我更了解它,但后来的 Folktale 版本将其替换为 Result

下面的代码块显示了我如何使用 Eithers 来处理这种失败的概念,特别是我们如何使用 R.sequenceEithers 的数组转换为 Either 拿着一个数组。如果输入包含任何 Left,则输出只是 Left。如果全部是 Right,那么输出是一个 Right,包含它们的值的数组。有了这个,我们可以将我们所有的列名转换成 Eithers 来捕获值或错误,然后将它们组合成一个结果。

需要注意的是这里没有抛出异常。我们的功能将正确组合。失败的概念封装在类型中。

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>

我的要点是 goodIndicesbadIndices 等值本身很有用。如果我们想对它们进行更多处理,我们可以简单地 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)