简体   繁体   English

Haskell中的非monadic错误处理?

[英]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错误处理,这在句法上比使用普通的MaybeEither更简单。 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: 我认为当前方法中这种复杂性的两个原因是:

  1. 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的“病毒”特征)。

  2. 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 . 让我们为讨论选择一个特定的amInt -> Maybe Int

Composing functions that can have errors 编写可能有错误的函数

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函数并不特别需要IntString ,因此我们可以使它更有用:

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. 实际上,我们所有必须改变的是类型签名。

This already exists! 这已经存在了!

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 (因为MaybeMonad一个实例,我们可以这样做),我们得到与bindMaybe相同的类型:

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

Let's take a look at the Maybe instance of Monad to be sure: 让我们来看看MonadMaybe实例:

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

When we can simplify even more 当我们可以简化更多

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 function also already exists! 这个功能也已经存在!

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

What if we have more 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 . 我们需要FunctorMonad之间的东西。

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 ArithmeticErrorMaybe现在。

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'

A very important note 一个非常重要的说明

Note that none of the above code mentioned Functor , Applicative , or Monad . 请注意,上述代码均未提及FunctorApplicativeMonad 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 do f' (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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM