如何处理 Ramda 中的错误

10

我对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' ]

因此我需要:

  • 遍历目标列
  • 返回标题中目标列的索引
  • 如果索引为-1,则抛出错误

4
在函数式编程中,当您将函数组成在一起时,通常不希望通过抛出错误来中断数据流。我认为像单子这样的东西可以帮助你。虽然这绝对是一个值得深入探索的主题,但您也应该问自己,在这种特定情况下抛出错误是否真正有用?例如,不能使用默认标题吗?个人而言,只有在我的程序遇到无法恢复的情况时才会抛出错误。 - customcommander
感谢您的定制,这是一个用于节点脚本的内容,用户提供来自CSV文件的目标列名,然后将这些名称映射到程序所需的新名称。如果在CSV文件中找不到提供的列名,则程序需要向用户发出警报,并且无法运行。 - Le Moi
2
通常你会使用延续传递风格或带有短路功能的单子。然而我不知道Ramda是否提供或者便于这些方法的实现。 - user5536315
2
非常宽泛的问题。请查看folktale的data.either模块以获取指导。 - Mulan
3个回答

13
正如customcommander所说,函数式编程中不易实现抛出异常的原因是存在更难以理解的问题。
“你的函数返回什么?” “一个数字。” “总是这样吗?” “是的……除非它抛出异常。” “那么它返回什么?” “嗯,它不返回任何东西。” “所以它返回一个数字还是什么都没有返回?” “我想是的。” “嗯。”
函数式编程中最常见的操作之一是组合两个函数。但是,如果第一个函数可能会抛出异常,则很难进行推理。
为了解决这个问题,函数式编程使用捕获失败概念的类型。您可能已经看到过Maybe类型的讨论,该类型处理可能为null的值。另一个常见的类型是Either(有时称为Result),它有两个子类型,分别用于错误情况和成功情况(对于Either分别为Left和Right,对于Result分别为Error和Ok)。在这些类型中,第一个找到的错误被捕获并传递给需要它的人,而成功情况则继续处理。(还有Validation类型可以捕获错误列表。)
这些类型有许多实现方式。请参考fantasy-land list以获取一些建议。
Ramda曾经拥有自己的这些类型,但已经停止维护。我们通常推荐使用Folktale和Sanctuary。但是,即使是Ramda的旧实现也应该可以胜任。此版本使用Folktale的data.either,因为我更熟悉它,但后来的Folktale版本将其替换为Result
以下代码块显示了如何使用Either来处理失败的概念,特别是我们如何使用R.sequenceEithers数组转换为包含数组的Either。如果输入包含任何Left,则输出只是一个Left。如果全部都是Right,则输出是一个包含它们值的Right数组。通过这种方式,我们可以将所有列名转换为捕获值或错误的Either,然后将它们合并成单个结果。
需要注意的是,这里没有抛出任何异常。我们的函数将正确组合。失败的概念封装在类型中。

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')]

而这就是这些类型的全部内容。

哎呀,你在 Left('oops') 后面漏了一个 )。非常好的答案,Scott :D - Mulan
@user633183:糟糕!我在使用“前任(predecessor)”时实际上是想用“继任(successor)” 。现在都已更正。谢谢! - Scott Sauyet
谢谢Scott,这太棒了,需要我一点时间来理解它,但这正是我在寻找的东西 - 谢谢 :) - Le Moi
1
好的,今晚两次在两篇不同的帖子中,您阐述了一些有用的概念。我整天都在阅读关于类似事情的博客文章。您应该写一篇 :) - Antoine Claval

0
如果抛出异常违背了函数式编程,那我想我就是个无政府主义者。以下是如何以函数式方式编写函数的方法。
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()

map 就像 ramda 的 map 函数

你可以将上面的 switchCase 想象为

const getTargetColumnIndex = x =>
  ifHeaderDoesNotExist(x) ? throwHeaderNotFound(x) : getHeaderIndex(x)

0
我要提出一个不同的观点:Either是将方形钉子通过静态类型系统的圆孔的好解决方案。在JavaScript中,这会带来很多认知负担,而收益较少(缺乏正确性保证)。
如果所讨论的代码需要快速执行(通过分析和记录的性能预算已经证明),那么应该以命令式风格编写它。
如果不需要快速执行(或者在实现以下内容时足够快),那么您只需检查您正在迭代的内容是否是CSV标题的真子集(或完全匹配)即可:
// 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');

这样可以将数据有效性检查和所需转换的清晰分离。

如果您只关心长度,那么只需检查即可,等等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接