[英]non-monadic error handling in Haskell?
I was wondering if there is an elegant way to do non-monadic error handling in Haskell that is syntactically simpler than using plain Maybe
or Either
. 我想知道是否有一种优雅的方式在Haskell中进行非monadic错误处理,这在句法上比使用普通的Maybe
或Either
更简单。 What I wanted to deal with is non-IO exceptions such as in parsing, where you generate the exception yourself to let yourself know at a later point, eg, something was wrong in the input string. 我想要处理的是非IO异常,例如在解析中,您自己生成异常以便稍后知道,例如,输入字符串中出现错误。
The reason I ask is that monads seem to be viral to me. 我问的原因是monad似乎对我有病毒感染。 If I wanted to use exception or exception-like mechanism to report non-critical error in pure functions, I can always use either
and do case
analysis on the result. 如果我想使用异常或类似异常的机制来报告纯函数中的非严重错误,我总是可以使用either
并对结果进行case
分析。 Once I use a monad, it's cumbersome/not easy to extract the content of a monadic value and feed it to a function not using monadic values. 一旦我使用monad,它很麻烦/不容易提取monadic值的内容并将其提供给不使用monadic值的函数。
A deeper reason is that monads seem to be an overkill for many error-handling. 更深层次的原因是monad似乎对许多错误处理来说是一种过度杀伤。 One rationale for using monads as I learned is that monads allow us to thread through a state. 我学习使用monad的一个理由是monad允许我们穿过状态。 But in the case of reporting an error, I don't see any need for threading states (except for the failure state, which I honestly don't know whether it's essential to use monads). 但是在报告错误的情况下,我认为不需要线程状态(失败状态除外,我真的不知道使用monad是否必不可少)。
( (
EDIT: as I just read, in a monad, each action can take advantage of results from the previous actions. 编辑:正如我刚才所读到的,在monad中,每个动作都可以利用之前动作的结果。 But in reporting an error, it is often unnecessary to know the results of the previous actions. 但是在报告错误时,通常不必知道先前操作的结果。 So there is a potential over-kill here for using monads. 因此,使用monads可能存在过度杀戮。 All that is needed in many cases is to abort and report failure on-site without knowing any prior state. 在许多情况下,所需要的只是在不知道任何先前状态的情况下中止并报告现场故障。 Applicative
seems to be a less restrictive choice here to me. 对我来说, Applicative
似乎是一个限制较少的选择。
In the specific example of parsing, are the execptions/errors we raise ourselves really effectual in nature? 在解析的具体例子中,我们自己提出的行为/错误是否真的有效? If not, is there something even weaker than Applicative
for to model error handling? 如果没有,是否有一些比Applicative
模型错误处理更弱的东西?
) )
So, is there a weaker/more general paradigm than monads that can be used to model error-reporting? 那么,是否存在比monad更弱/更一般的范例,可以用来模拟错误报告? I am now reading Applicative
and trying to figure out if it's suitable. 我现在正在阅读Applicative
并试图找出它是否合适。 Just wanted to ask beforehand so that I don't miss the obvious. 只是想事先询问,以便我不会错过显而易见的事实。
A related question about this is whether there is a mechanism out there which simply enclose every basic type with,eg, an Either String
. 与此相关的一个问题是,是否存在一种机制,它只是简单地用每个基本类型括起来,例如,一个Either String
。 The reason I ask here is that all monads (or maybe functors) enclose a basic type with a type constructor. 我在这里问的原因是所有monad(或者可能是functor)都包含一个带有类型构造函数的基本类型。 So if you want to change your non-exception-aware function to be exception aware, you go from, eg, 因此,如果您想要将非异常感知功能更改为异常感知,那么您可以从,例如,
f:: a -> a -- non-exception-aware
to 至
f':: a -> m a -- exception-aware
But then, this change breaks functional compositions that would otherwise work in the non-exception case. 但是,这种改变打破了在非例外情况下可以起作用的功能组合。 While you could do 虽然你可以做到
f (f x)
you can't do 你不能这样做
f' (f' x)
because of the enclosure. 因为外壳。 A probably naive way to solve the composibilty issue is change f
to: 解决可能性问题的一种可能天真的方法是将f
改为:
f'' :: m a -> m a
I wonder if there is an elegant way of making error handling/reporting work along this line? 我想知道是否有一种优雅的方式在这条线上进行错误处理/报告工作?
Thanks. 谢谢。
-- Edit --- - 编辑---
Just to clarify the question, take an example from http://mvanier.livejournal.com/5103.html , to make a simple function like 只是为了澄清这个问题,请从http://mvanier.livejournal.com/5103.html举个例子来制作一个像
g' i j k = i / k + j / k
capable of handling division by zero error, the current way is to break down the expression term-wise, and compute each term in a monadic action (somewhat like rewriting in assembly language): 能够处理零除错误,当前的方法是逐项分解表达式,并在monadic动作中计算每个术语(有点像用汇编语言重写):
g' :: Int -> Int -> Int -> Either ArithmeticError Int
g' i j k =
do q1 <- i `safe_divide` k
q2 <- j `safe_divide` k
return (q1 + q2)
Three actions would be necessary if (+)
can also incur an error. 如果(+)
也可能导致错误,则需要三个动作。 I think two reasons for this complexity in current approach are: 我认为当前方法中这种复杂性的两个原因是:
As the author of the tutorial pointed out, monads enforce a certain order of operations, which wasn't required in the original expression. 正如本教程的作者所指出的,monad强制执行某种操作顺序,这在原始表达式中是不必要的。 That's where the non-monadic part of the question comes from (along with the "viral" feature of monads). 这就是问题的非一元部分来源(以及monad的“病毒”特征)。
After the monadic computation, you don't have Int
s, instead, you have Either a Int
, which you cannot add directly. 在monadic计算之后,你没有Int
,相反,你有Either a Int
,你无法直接添加。 The boilerplate code would multiply rapidly when the express get more complex than addition of two terms. 当快递变得比添加两个术语更复杂时,样板代码将快速繁殖。 That's where the enclosing-everything-in-a- Either
part of the question comes from. 这就是封闭 - 问题的Either
一部分来自于问题的一部分。
In your first example, you want to compose a function f :: a -> ma
with itself. 在第一个示例中,您希望自己编写函数f :: a -> ma
。 Let's pick a specific a
and m
for the sake of discussion: Int -> Maybe Int
. 让我们为讨论选择一个特定的a
和m
: Int -> Maybe Int
。
Okay, so as you point out, you cannot just do f (fx)
. 好的,正如你指出的那样,你不能只做f (fx)
。 Well, let's generalize this a little more to g (fx)
(let's say we're given a g :: Int -> Maybe String
to make things more concrete) and look at what you do need to do case-by-case: 好吧,让我们来概括这一点更加g (fx)
比方说,我们给出一个g :: Int -> Maybe String
使事情变得更加具体的),看看你需要做的情况下,逐案的内容:
f :: Int -> Maybe Int
f = ...
g :: Int -> Maybe String
g = ...
gComposeF :: Int -> Maybe String
gComposeF x =
case f x of -- The f call on the inside
Nothing -> Nothing
Just x' -> g x' -- The g call on the outside
This is a bit verbose and, like you said, we would like to reduce the repetition. 这有点冗长,就像你说的那样,我们希望减少重复。 We can also notice a pattern: Nothing
always goes to Nothing
, and the x'
gets taken out of Just x'
and given to the composition. 我们还可以注意到一种模式: Nothing
总是Nothing
,并且x'
被取出Just x'
并被赋予组合。 Also, note that instead of fx
, we could take any Maybe Int
value to make things even more general. 另外,请注意,我们可以使用任何 Maybe Int
值来代替fx
,使事情变得更加通用。 So let's also pull our g
out into an argument, so we can give this function any g
: 所以让我们把我们的g
拉成一个参数,所以我们可以给这个函数任意 g
:
bindMaybe :: Maybe Int -> (Int -> Maybe String) -> Maybe String
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
With this helper function, we can rewrite our original gComposeF
like this: 有了这个辅助函数,我们可以像这样重写我们的原始gComposeF
:
gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g
This is getting pretty close to g . f
这非常接近于g . f
g . f
, which is how you would compose those two functions if there wasn't the Maybe
discrepancy between them. g . f
,如果它们之间没有Maybe
差异,那么你将如何编写这两个函数。
Next, we can see that our bindMaybe
function doesn't specifically need Int
or String
, so we can make this a little more useful: 接下来,我们可以看到我们的bindMaybe
函数并不特别需要Int
或String
,因此我们可以使它更有用:
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
All we had to change, actually, was the type signature. 实际上,我们所有必须改变的是类型签名。
Now, bindMaybe
actually already exists: it is the >>=
method from the Monad
type class! 现在, bindMaybe
实际上已经存在:它是来自Monad
类型类的>>=
方法!
(>>=) :: Monad m => m a -> (a -> m b) -> m b
If we substitute Maybe
for m
(since Maybe
is an instance of Monad
, we can do this) we get the same type as bindMaybe
: 如果我们用Maybe
替换m
(因为Maybe
是Monad
一个实例,我们可以这样做),我们得到与bindMaybe
相同的类型:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Let's take a look at the Maybe
instance of Monad
to be sure: 让我们来看看Monad
的Maybe
实例:
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
Just like bindMaybe
, except we also have an additional method that lets us put something into a "monadic context" (in this case, this just means wrapping it in a Just
). 就像bindMaybe
一样,除了我们还有一个额外的方法让我们把东西放到“monadic context”中(在这种情况下,这只是意味着将它包装在Just
)。 Our original gComposeF
looks like this: 我们原来的gComposeF
看起来像这样:
gComposeF x = f x >>= g
There is also =<<
, which is a flipped version of >>=
, that lets this look a little more like the normal composition version: 还有=<<
,这是>>=
的翻转版本,让它看起来更像普通的合成版本:
gComposeF x = g =<< f x
There is also a builtin function for composing functions with types of the form a -> mb
called <=<
: 还有一个内置函数用于组合函数,其类型为a -> mb
称为<=<
:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
-- Specialized to Maybe, we get:
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> a -> Maybe c
Now this really looks like function composition! 现在这真的看起来像功能组合!
gComposeF = g <=< f -- This is very similar to g . f, which is how we "normally" compose functions
As you mentioned in your question, using do
notation to convert simple division function to one which properly handles errors is a bit harder to read and more verbose. 正如您在问题中提到的那样,使用do
notation将简单除法函数转换为正确处理错误的函数更难以阅读且更冗长。
Let's look at this a little more carefully, but let's start with a simpler problem (this is actually a simpler problem than the one we looked at in the first sections of this answer): We already have a function, say that multiplies by 10, and we want to compose it with a function that gives us a Maybe Int
. 让我们更仔细地看一下这个问题,但让我们从一个更简单的问题开始(这实际上比我们在这个答案的第一部分中看到的问题更简单):我们已经有了一个函数,比如乘以10,我们想用一个给我们一个Maybe Int
的函数来组合它。 We can immediately simplify this a little bit by saying that what we really want to do is take a "regular" function (such as our multiplyByTen :: Int -> Int
) and we want to give it a Maybe Int
(ie, a value that won't exist in the case of an error). 我们可以立即简化这一点,说我们真正想做的是采用“常规”函数(例如我们的multiplyByTen :: Int -> Int
),我们想给它一个Maybe Int
(即一个值)在错误的情况下不存在)。 We want a Maybe Int
to come back too, because we want the error to propagate. 我们想要一个Maybe Int
也可以回来,因为我们希望错误传播。
For concreteness, we will say that we have some function maybeCount :: String -> Maybe Int
(maybe divides 5 by the number times we use the word "compose" in the String
and rounds down. It doesn't really matter what it specifically though) and we want to apply multiplyByTen
to the result of that. maybeCount :: String -> Maybe Int
,我们会说我们有一些函数maybeCount :: String -> Maybe Int
(可能将我们在String
使用单词“compose”的次数除以5 maybeCount :: String -> Maybe Int
。具体是什么它并不重要虽然)我们想要将multiplyByTen
应用于结果。
We'll start with the same kind of case analysis: 我们将从相同类型的案例分析开始:
multiplyByTen :: Int -> Int
multiplyByTen x = x * 10
maybeCount :: String -> Maybe Int
maybeCount = ...
countThenMultiply :: String -> Maybe Int
countThenMultiply str =
case maybeCount str of
Nothing -> Nothing
Just x -> multiplyByTen x
We can, again, do a similar "pulling out" of multiplyByTen
to generalize this further: 我们可以再次对multiplyByTen
进行类似的“拉出”以进一步概括:
overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
case mstr of
Nothing -> Nothing
Just x -> f x
These types also can be more general: 这些类型也可以更通用:
overMaybe :: (a -> b) -> Maybe a -> Maybe b
Note that we just needed to change the type signature, just like last time. 请注意,我们只需更改类型签名,就像上次一样。
Our countThenMultiply
can then be rewritten: 然后我们的countThenMultiply
可以被重写:
countThenMultiply str = overMaybe multiplyByTen (maybeCount str)
This is fmap
from Functor
! 这是Functor
fmap
!
fmap :: Functor f => (a -> b) -> f a -> f b
-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b
and, in fact, the definition of the Maybe
instance is exactly the same as well. 事实上, Maybe
实例的定义也完全相同。 This lets us apply any "normal" function to a Maybe
value and get a Maybe
value back, with any failure automatically propagated. 这允许我们将任何“正常”函数应用于Maybe
值并返回Maybe
值,任何失败都会自动传播。
There is also a handy infix operator synonym for fmap
: (<$>) = fmap
. fmap
: (<$>) = fmap
还有一个方便的中缀运算符同义词。 This will come in handy later. 这将在以后派上用场。 This is what it would look like if we used this synonym: 如果我们使用这个同义词,这就是它的样子:
countThenMultiply str = multiplyByTen <$> maybeCount str
Maybes
? 如果我们有更多Maybes
怎么办? Maybe we have a "normal" function of multiple arguments that we need to apply to multiple Maybe
values. 也许我们有多个参数的“正常”函数,我们需要应用于多个Maybe
值。 As you have in your question, we could do this with Monad
and do
notation if we were so inclined, but we don't actually need the full power of Monad
. 当你在你的问题有,我们可以用做这个Monad
,并do
标记,如果我们这样的倾向,但我们并不真正需要的全功率Monad
。 We need something in between Functor
and Monad
. 我们需要Functor
和Monad
之间的东西。
Let's look the division example you gave. 让我们来看看你给出的分组示例。 We want to convert g'
to use the safeDivide :: Int -> Int -> Either ArithmeticError Int
. 我们想要转换g'
以使用safeDivide :: Int -> Int -> Either ArithmeticError Int
。 The "normal" g'
looks like this: “正常” g'
看起来像这样:
g' i j k = i / k + j / k
What we would really like to do is something like this: 我们真正想做的是这样的事情:
g' i j k = (safeDivide i k) + (safeDivide j k)
Well, we can get close with Functor
: 那么,我们可以与Functor
接近 :
fmap (+) (safeDivide i k) :: Either ArithmeticError (Int -> Int)
The type of this, by the way, is analogous to Maybe (Int -> Int)
. 顺便说一下,这种类型类似于Maybe (Int -> Int)
。 The Either ArithmeticError
part just tells us that our errors give us information in the form of ArithmeticError
values instead of only being Nothing
. Either ArithmeticError
部分只是告诉我们,我们的错误以ArithmeticError
值的形式提供信息,而不仅仅是Nothing
。 It could help to mentally replace Either ArithmeticError
with Maybe
for now. 它可以帮助在精神上取代Either ArithmeticError
与Maybe
现在。
Well, this is sort of like what we want, but we need a way to apply the function "inside" the Either ArithmeticError (Int -> Int)
to Either ArithmeticError Int
. 嗯,这有点像我们想要的,但是我们需要一种方法将Either ArithmeticError (Int -> Int)
的函数应用于Either ArithmeticError Int
。
Our case analysis would look like this: 我们的案例分析如下:
eitherApply :: Either ArithmeticError (Int -> Int) -> Either ArithmeticError Int -> Either ArithmeticError Int
eitherApply ef ex =
case ef of
Left err -> Left err
Right f ->
case ex of
Left err' -> Left err'
Right x -> Right (f x)
(As a side note, the second case
can be simplified with fmap
) (作为旁注,第二种case
可以用fmap
简化)
If we have this function, then we can do this: 如果我们有这个功能,那么我们可以这样做:
g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)
This still doesn't look great, but let's go with it for now. 这仍然看起来不太好,但现在让我们继续吧。
It turns out eitherApply
also already exists: it is (<*>)
from Applicative
. 结果是eitherApply
也已经存在:来自Applicative
(<*>)
。 If we use this, we can arrive at: 如果我们使用这个,我们可以到达:
g' i j k = (<*>) (fmap (+) (safeDivide i k)) (safeDivide j k)
-- This is the same as
g' i j k = fmap (+) (safeDivide i k) <*> safeDivide j k
You may remember from earlier that there is an infix synonym for fmap
called <$>
. 您可能记得早些时候有一个名为<$>
fmap
的中缀同义词。 If we use that, the whole thing looks like: 如果我们使用它,整个事情看起来像:
g' i j k = (+) <$> safeDivide i k <*> safeDivide j k
This looks strange at first, but you get used to it. 这看起来很奇怪,但你已经习惯了。 You can think of <$>
and <*>
as being "context sensitive whitespace." 您可以将<$>
和<*>
视为“上下文敏感的空白”。 What I mean is, if we have some regular function f :: String -> String -> Int
and we apply it to normal String
values we have: 我的意思是,如果我们有一些常规函数f :: String -> String -> Int
,我们将它应用于普通的String
值,我们有:
firstString, secondString :: String
result :: Int
result = f firstString secondString
If we have two (for example) Maybe String
values, we can apply f :: String -> String -> Int
, we can apply f
to both of them like this: 如果我们有两个(例如) Maybe String
值,我们可以应用f :: String -> String -> Int
,我们可以像这样将f
应用于它们:
firstString', secondString' :: Maybe String
result :: Maybe Int
result = f <$> firstString' <*> secondString'
The difference is that instead of whitespace, we add <$>
and <*>
. 区别在于我们不是添加空格,而是添加<$>
和<*>
。 This generalizes to more arguments in this way (given f :: A -> B -> C -> D -> E
): 这以这种方式推广到更多的参数(给定f :: A -> B -> C -> D -> E
):
-- When we apply normal values (x :: A, y :: B, z :: C, w :: D):
result :: E
result = f x y z w
-- When we apply values that have an Applicative instance, for example x' :: Maybe A, y' :: Maybe B, z' :: Maybe C, w' :: Maybe D:
result' :: Maybe E
result' = f <$> x' <*> y' <*> z' <*> w'
Note that none of the above code mentioned Functor
, Applicative
, or Monad
. 请注意,上述代码均未提及Functor
, Applicative
或Monad
。 We just used their methods as though they are any other regular helper functions. 我们只是将它们的方法用作任何其他常规辅助函数。
The only difference is that these particular helper functions can work on many different types, but we don't even have to think about that if we don't want to. 唯一的区别是这些特定的辅助函数可以在许多不同的类型上工作,但如果我们不想这样做,我们甚至不必考虑它。 If we really want to, we can just think of fmap
, <*>
, >>=
etc in terms of their specialized types, if we are using them on a specific type (which we are, in all of this). 如果我们真的想要,我们可以根据它们的特殊类型来考虑fmap
, <*>
, >>=
etc,如果我们在特定类型上使用它们(在所有这些中我们都是这样)。
The reason I ask is that monads seem to be viral to me. 我问的原因是monad似乎对我有病毒感染。
Such viral character is actually well-suited to exception handling, as it forces you to recognize your functions may fail and to deal with the failure cases. 这种病毒特征实际上非常适合异常处理,因为它会强制您识别您的功能可能会失败并处理故障情况。
Once I use a monad, it's cumbersome/not easy to extract the content of a monadic value and feed it to a function not using monadic values. 一旦我使用monad,它很麻烦/不容易提取monadic值的内容并将其提供给不使用monadic值的函数。
You don't have to extract the value. 您不必提取值。 Taking Maybe
as a simple example, very often you can just write plain functions to deal with success cases, and then use fmap
to apply them to your Maybe
values and maybe
/ fromMaybe
to deal with failures and eliminate the Maybe
wrapping. 以Maybe
作为一个简单的例子,你经常可以编写简单的函数来处理成功案例,然后使用fmap
将它们应用于Maybe
值, maybe
/ fromMaybe
来处理失败并消除Maybe
wrap。 Maybe
is a monad, but that doesn't oblige you to use the monadic interface or do
notation all the time. Maybe
是一个monad,但这并没有强迫你使用monadic界面或do
记法。 In general, there is no real opposition between "monadic" and "pure". 一般来说,“monadic”和“pure”之间并没有真正的对立。
One rationale for using monads as I learned is that monads allow us to thread through a state. 我学习使用monad的一个理由是monad允许我们穿过状态。
That is just one of many use cases. 这只是众多用例中的一个。 The Maybe
monad allows you to skip any remaining computations in a bind chain after failure. Maybe
monad允许您在失败后跳过绑定链中的任何剩余计算。 It does not thread any sort of state. 它不会破坏任何类型的状态。
So, is there a weaker/more general paradigm than monads that can be used to model error-reporting? 那么,是否存在比monad更弱/更一般的范例,可以用来模拟错误报告? I am now reading
Applicative
and trying to figure out if it's suitable. 我现在正在阅读Applicative
并试图找出它是否合适。
You can certainly chain Maybe
computations using the Applicative
instance. 您当然可以使用Applicative
实例链接Maybe
计算。 (*>)
is equivalent to (>>)
, and there is no equivalent to (>>=)
since Applicative
is less powerful than Monad
. (*>)
相当于(>>)
,并且没有等价于(>>=)
因为Applicative
不如Monad
强大。 While it is generally a good thing not to use more power than you actually need, I am not sure if using Applicative
is any simpler in the sense you aim at. 虽然通常不使用比实际需要更多的功率是一件好事,但我不确定使用Applicative
是否比你想要的更简单。
While you could do
f (fx)
you can't dof' (f' x)
虽然你可以做f (fx)
你不能做f' (f' x)
You can write f' <=< f' $ x
though: 你可以写f' <=< f' $ x
但是:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
You may find this answer about (>=>)
, and possibly the other discussions in that question, interesting. 您可能会发现关于(>=>)
答案 ,以及可能在该问题中的其他讨论,这很有趣。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.