Haskell接近error handling
这里没有任何争论,Haskell中有各种机制来处理错误并正确处理它们。 错误monad,可能,exception等
那么为什么它比其他语言中的Haskell编写exception容易的代码更简单呢?
比方说,我想写一个命令行工具,处理在命令行上传递的文件。 我想:
- 提供validation文件名
- validation文件是可用的和可读的
- validation文件有有效的标题
- 创build输出文件夹并validation输出文件是可写的
- 处理文件,错误parsing错误,不变的错误等
- 输出文件,写入错误,磁盘已满等错误
所以一个非常直接的文件处理工具。
在Haskell中,我会用一些单子组合来包装这个代码,使用Maybe和Either,并根据需要翻译和传播错误。 最后,这一切都得到了一个IO monad,我可以输出状态给用户。
用另一种语言,我只是抛出一个exception,并抓住适当的地方。 直截了当。 我不花很多时间在认知上,试图解开我需要的机制。
我只是接近这个错误,或者这是否有这种感觉的一些内容?
编辑:好吧,我收到反馈告诉我,这只是感觉更难,但实际上不是。 所以这里是一个痛点。 在Haskell中,我正在处理monads堆栈,如果我必须处理错误,我要为这个monad堆栈添加另一个图层。 我不知道为了编译代码而添加了多less个提升语句和其他语法垃圾,但却增加了零语义含义。 没有人觉得这增加了复杂性?
在Haskell中,我会用一些单子组合来包装这个代码,使用Maybe和Either,并根据需要翻译和传播错误。 最后,这一切都得到了一个IO monad,我可以输出状态给用户。
用另一种语言,我只是抛出一个exception,并抓住适当的地方。 直截了当。 我不花很多时间在认知上,试图解开我需要的机制。
我不会说你一定会错的。 相反,你的错误是认为这两种情况是不同的。 他们不是。
“简单地抛出和捕捉”相当于在整个程序中强加与Haskellerror handling方法的某种组合完全相同的概念结构。 确切的组合取决于你正在比较的语言的error handling系统,这就是为什么Haskell 看起来更复杂:它可以让你根据需要混合和匹配error handling结构,而不是给你一个隐含的大小适合的解决scheme。
所以,如果你需要特定的error handling风格,你可以使用它; 你只用它的代码,需要它。 不需要它的代码 – 由于既不产生也不处理相关types的错误 – 被标记为这样,这意味着你可以使用该代码而不必担心被创build的那种错误。
在句法笨拙的问题上,这是一个尴尬的话题。 从理论上讲,应该是无痛的,但是:
- Haskell一直是一个研究驱动的语言,在很早的时候,许多事情还在不断变化,有用的习语还没有普及,所以旧的代码可能是一个糟糕的榜样
- 有些图书馆并不像处理错误那样灵活,要么是因为上面的旧代码僵化,要么是缺乏波兰语
- 我并不知道如何最好地构造新的error handling代码的指南,所以新手都留在自己的设备上
我猜测你有可能以某种方式“做错了”,并且可以避免大部分的语法混乱,但是指望你(或任何一般的Haskell程序员)自己find最好的方法可能是不合理的。
就monad变换器堆栈而言,我认为标准方法是为应用程序newtype
整个堆栈,派生或实现相关types类的实例(例如MonadError
),然后使用types类的通常不需要的函数lift
。 为应用程序核心编写的Monadic函数应该都使用newtype
d栈,所以也不需要提升。 关于唯一不能避免的低语义含义的东西,我认为是liftIO
。
处理大量的变压器可能是一个实际的头痛,但只有当有很多不同的变压器嵌套层(堆叠交替层的StateT
和ErrorT
与ContT
中间扔,然后试图告诉我你的代码实际上会这样做)。 不过,这实际上并不是你想要的。
编辑 :作为一个小的附录,我想引起注意,在写一些评论时发生在我身上的更一般的观点。
正如我所说的和@sclv很好地展示,正确的error handling实际上是复杂的。 你所能做的只是把复杂性拖垮,而不是消除它,因为不pipe你正在执行多个操作,可以独立地产生错误,你的程序需要以某种方式处理每一种可能的组合,即使这个“处理”只是简单地结束并死亡。
也就是说,Haskell 在某种程度上与大多数语言确实有着本质的区别:通常,error handling既是明确的 ,也是一stream的 ,意味着一切都是公开的,可以自由操纵。 另一方面是隐式error handling的缺失,这意味着即使你想要打印错误消息并且死掉,你也必须明确地这样做。 所以在Haskell中,实际上做error handling是比较容易的,因为对它进行一级抽象,但是忽略错误更加困难。 然而,这种“所有的手放弃”错误不处理在任何现实世界,生产使用几乎是不正确的 ,这就是为什么它似乎尴尬被搁置。
所以,当你需要明确地处理错误的时候,确实事情比较复杂,重要的是要记住这一切就是这样 。 一旦你学会了如何使用适当的error handling抽象,复杂性几乎达到了一个平台,并且随着程序的扩展并没有真正变得更难; 而你使用这些抽象的越多越自然。
让我们看看你想要做的一些事情:
提供validation文件名
如果他们不是? 刚退出,对吧?
validation文件是可用的和可读的
如果有些不是? 处理剩下的那些,当你碰到坏的时候抛出一个exception,警告那些坏的东西,处理好的东西? 在做任何事之前退出?
validation文件有有效的标题
如果他们不? 同样的问题 – 跳过坏的,提前中止,对不好的提醒等等。
处理文件,错误parsing错误,不变的错误等
再次,做什么,跳过坏行,跳过坏文件,中止,中止和回滚,打印警告,打印可configuration级别的警告?
重点是有可用的select和select。 以一种反映命令的方式来做你想做的事情,你根本不需要任何可能的单子堆栈。 所有你需要的是在IO中抛出和捕获exception。
如果你不想使用exception,并获得一定程度的控制,你仍然可以做到没有monad堆栈。 例如,如果你想处理文件,你可以得到结果,并返回错误的文件,那么Eithers的工作很好 – 只要写一个FilePath -> IO (Either String Result)
的函数FilePath -> IO (Either String Result)
。 然后在你的input文件列表中mapM
。 然后partitionEithers
结果列表,然后在Result -> IO (Maybe String)
映射一个Result -> IO (Maybe String)
的函数,而catMaybe
是错误string。 现在,您可以mapM print <$> (inputErrors ++ outputErrors)
来显示两个阶段中出现的所有错误。
或者,你知道,你也可以做其他的事情。 在任何情况下,在单元栈中使用Maybe
和Either
都有它的位置。 但对于典型的error handling案例来说,它更直接,更直接地处理它们,而且function也非常强大。 它只是需要一些习惯于大量的function,使他们的操作方便。
对Either ea
和模式匹配进行评估与try
和catch
之间有什么区别,除了它以exception传播的事实(如果使用任一monad,则可以模拟这个)
请记住,大多数时候单调使用的东西(在我看来)是丑陋的,除非你有大量的function使用失败。
如果你只有一个可能的失败,那么没有任何问题
func x = case tryEval x of Left e -> Left e Right val -> Right $ val + 1 func x = (+1) <$> trvEval x
它只是代表相同事物的function性方式。