Haskell中的非单调错误处理? [英] non-monadic error handling in Haskell?

查看:115
本文介绍了Haskell中的非单调错误处理?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道在Haskell中是否有一个优雅的方式进行非单一错误处理,这比使用简单的语法更简单可能 。我想要处理的是非IO异常,例如在解析中,您自己生成异常,以便稍后自己知道,例如,输入字符串中的错误。



我问的原因是,monads似乎对我来说是病毒的。如果我想使用异常或异常机制来报告纯函数中的非关键错误,我可以随时使用 并执行 case 分析结果。一旦我使用一个monad,它是繁琐的/不容易提取一个单一的价值的内容,并将其提供给不使用monadic值的功能。



更深的原因是monads似乎是许多错误处理的过分。我所学到的使用单子的一个理由是,monads允许我们穿越一个国家。但是在报告错误的情况下,我看不到线程状态的任何需求(除了失败状态,我真的不知道使用monad是否至关重要)。





编辑:正如我刚刚读到的,在monad中,每个动作都可以利用以前的动作的结果,但是在报告错误通常不需要知道以前操作的结果,所以在这里使用monads有一个潜在的过度杀戮,在许多情况下需要的是在不知道任何先前状态的情况下中止和报告故障现象。 code>应用似乎是对我来说限制较少的选择。



在解析的具体示例中,是exec /错误,我们提出自己真的有效的性质?如果不是,有什么比适用为模型错误处理吗?





那么有没有比可以用于mo的monads更弱/更一般的范例错误报告?我正在阅读适用,并试图弄清楚它是否合适。只是想提前问,所以我不会错过这个明显的。



有关这方面的一个相关问题是,是否有一个机制,只需将每个基本类型包含在一起,例如 C $ C>。我在这里问的原因是所有的monads(或者也许是functor)都包含一个类型构造函数的基本类型。所以如果你想改变你的非异常感知功能是异常感知,你可以从例如

  f: :a  - > a  - 非例外感知

  f':: a  - > ma-exception-aware 

但是,这个变化会破坏否则在非 - 异常情况。虽然你可以做

  f(fx)

你不能做

  f'(f'x)

因为机箱。解决composibilty问题可能是天真的方法是将 f 更改为:

  f'':: ma  - > ma 

我想知道是否有一种优雅的方式来进行此行的错误处理/报告工作? / p>

谢谢。



- 编辑---



只是为了澄清问题,请参考 http://mvanier.livejournal.com/5103.html ,做一个简单的函数,如

  g'ijk = i / k + j / k 

能够通过零错误处理除法,目前的方式是逐句分解表达式,并计算每个术语在一个单一的行动(有点像在汇编语言中重写):

  g':: Int  - > Int  - > Int  - > ArithmeticError Int 
g'ijk =
do q1< - i`safe_divide` k
q2< - j`safe_divide` k
return(q1 + q2)

如果(+)可以也会导致错误。我认为目前这种复杂性的两个原因是:


  1. 正如本教程的作者指出的那样,monads执行一定的在原始表达式中不需要操作顺序。这就是问题的非单一部分来自哪里(以及monads的病毒功能)。


  2. 在一元计算之后,有 Int s,相反,您有一个Int ,您不能直接添加。当表达比两个术语的加法更复杂时,样板代码将快速繁殖。这个问题的一部分来自于



解决方案

在你的第一个例子中,你想组成一个函数 f :: a - > m a 与自己。为了讨论,我们选择一个具体的 a m Int - >可能是



组合可能有错误的功能



你指出,你不能只是做 f(fx)。嗯,让我们再来一点吧,这个更多的一点就是 g(fx)(假设我们给了一个 g :: Int - > Maybe String 使事情更具体),看看你需要做的事情案例:

  f :: Int  - >也许Int 
f = ...

g :: Int - >也许String
g = ...

gComposeF :: Int - >也许String
gComposeF x =
case f x of - 内部的f调用
Nothing - >没有
只是x' - > g x' - 外面的g电话

这有点冗长,就像你说的,我们想减少重复。我们也可以注意到一个模式: Nothing 总是去没有任何,而 x' / code>从 Just x'中取出并赋予组合。另外,请注意,代替 fx ,我们可以采用任何 可能Int 值事情更加普遍。所以我们还将我们的 g 引入参数,所以我们可以给这个函数任何 g

  bindMaybe :: Maybe Int  - > (Int  - > Maybe String) - >可能String 
bindMaybe Nothing g = Nothing
bindMaybe(Just x')g = g x'

有了这个帮助函数,我们可以这样重写我们原来的 gComposeF

  gComposeF :: Int  - >也许String 
gComposeF x = bindMaybe(fx)g

code> g。 f ,如果没有可能之间的差异,那么您将如何撰写这两个功能。



接下来,我们可以看到我们的 bindMaybe 函数不具体需要 Int String ,所以我们可以使这一点更有用:

  bindMaybe: :也许是 - > (a  - >可能b) - >可能b 
bindMaybe Nothing g = Nothing
bindMaybe(Just x')g = g x'

所有我们必须改变的是类型签名。



已经存在!



现在, bindMaybe 实际上已经存在:它是<$ c $的>> = 方法c> Monad type class!

 (>> =):: Monad m => ; m a  - > (a→m b)→> mb 

如果我们替换也许 m (因为也许 Monad 的实例,我们可以做这个)我们得到与 bindMaybe 相同的类型:

 (> > =)::也许a  - > (a  - >可能b) - >可能b 

我们来看看 Maybe Monad的实例确定:

  instance Monad Maybe where 
return x = Just x
Nothing>> = f = Nothing
Just x>> = f = fx

就像 bindMaybe 一样,除了我们还有一个额外的方法可以让我们把东西放入monadic上下文(在这种情况下,这只是意味着将其包装在 Just 中)。我们原来的 gComposeF 如下所示:

  gComposeF x = fx> > = g 

还有 =< ,这是一个翻转版本的>> = ,这让它看起来更像一般的组合版本:

  gComposeF x = g =<< fx 

还有一个内置函数,用于组合函数的类型为 a - > mb 调用< =<

 (< =<):: Monad m => (b→m c)→> (a→m b)→> a  - > m c 

- 专门为Maybe,我们得到:
(< =<)::(b - >可能c) - > (a - >可能b) - > a - >可能c

现在这看起来像功能组合!

  gComposeF = g< =< f  - 这与g非常相似。 f,这是我们如何正常地组成函数



当我们可以简化更多的



正如您在问题中提到的,使用 do 表示法将简单分割函数转换为正确处理错误的函数有点难阅读更详细。



我们再仔细看一下这一点,但让我们从一个更简单的问题开始(这实际上比我们所看到的更简单的问题这个答案的第一部分):我们已经有一个功能,比如说乘以10,我们想用一个给我们一个 Maybe Int 的函数组成。我们可以立即简化这一点,说我们真正希望做的是采用常规功能(例如我们的 multiplyByTen :: Int - > Int ),我们想给它一个 Maybe Int (即,在错误的情况下不会存在的值)。我们想要一个 Maybe Int 也可以回来,因为我们想要传播错误。



为了更具体,我们会说我们有一些函数 maybeCount :: String - >也许在 String 中将5分除以我们使用单词compose的次数,并将其缩小,这并不重要它是什么具体的),我们想应用 multiplyByTen 的结果。



我们将从同样的案例分析:

  multiplyByTen :: Int  - > Int 
multiplyByTen x = x * 10

maybeCount :: String - >也许Int
maybeCount = ...

countThenMultiply :: String - >可能Int
countThenMultiply str =
case maybeCount str of
Nothing - >没有
只是x - > multiplyByTen x

我们可以再次进行类似的拉出 multiplyByTen 可以进一步推广:

  overMaybe ::(Int  - > Int) - >也许Int  - >也许Int 
overMaybe f mstr =
case mstr of
没有 - >没有
只是x - > fx

这些类型也可以更一般:

  overMaybe ::(a  - > b) - >也许是 - >可能b 

请注意,我们只需要像上次一样更改类型签名。



我们的 countThenMultiply 可以重写:

  countThenMultiply str = overMaybe multiplyByTen(maybeCount str)



此功能也已经存在!



这是 fmap Functor

  fmap :: Functor f => (a→b)→> f a  - > f b 

- 专业化f到Maybe:
fmap ::(a - > b) - >也许是 - >可能b

,其实,的定义可能实例也是完全一样的。这使我们可以将任何正常功能应用于可能值,并获得一个可能返回值,任何故障自动传播。



还有一个方便的中缀操作符代码 fmap (< $>)= fmap 。这将在以后派上用场。如果我们使用这个同义词,这将是什么样子:

  countThenMultiply str = multiplyByTen< $>如果我们有更多的 Maybes  



也许我们有一个正常的多个参数的函数,我们需要应用到多个可能值。就像你在问题中一样,我们可以使用 Monad do 我们是如此倾向,但实际上我们并不需要 Monad 的全部功能。我们需要 Functor Monad 之间的东西。



我们来看看你给出的划分例子。我们要转换 g'以使用 safeDivide :: Int - > Int - > ArithmeticError Int 。 正常 g'如下所示:

  g'ijk = i / k + j / k 

我们将真正喜欢做是这样的:

  g'ijk =(safeDivide ik)+(safeDivide jk)

嗯,我们可以用 Functor

  fmap(+)(safeDivide ik):: ArithmeticError(Int  - > Int)

这个类型类似于 Maybe(Int - > Int) ArithmeticError 部分只是告诉我们,我们的错误以 ArithmeticError 值的形式给出了我们的信息,而不是只有没有。现在可以帮助精神上替换 ArithmeticError 也许


$ b $那么这样就像我们想要的那样,但是我们需要一种方法来应用中的函数ArithmeticError(Int - > Int) ArithmeticError Int



我们的案例分析如下所示:

  anyApply :: ArithmeticError(Int  - > Int) - > ArithmeticError Int  - > ArithmeticError Int 
anyApply ef ex =
case ef of
Left err - >左错误
右f - >
case ex of
Left err' - >左错误'
右x - > (fx)

(作为附注,第二个案例可以简化为 fmap



如果我们有这个功能,那么我们可以这样做:

  g'ijk = whateverApply(fmap(+)(safeDivide ik))(safeDivide jk)

这仍然看起来不错,但现在让我们一起去吧。



结果是 anyApply 也已经存在:它是(< *>)应用型。如果我们使用这个,我们可以到达:

  g'ijk =(< *>)(fmap(+) (safeDivide ik))(safeDivide jk)

- 这与
相同g'ijk = fmap(+)(safeDivide ik)*你可能会记得从前面有一个中缀词同义词为$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $
调用< $> 。如果我们使用它,整个事情看起来就像:

  g'i j k =(+)&$ safeDivide i k * safeDivide j k 

这首先看起来很奇怪,但你习惯了。您可以将< $> < *> 作为上下文相关空白。我的意思是,如果我们有一些常规函数 f :: String - >字符串 - > Int ,我们将其应用于正常的 String 值:

  firstString,secondString :: String 

result :: Int
result = f firstString secondString

如果我们有两个(例如) Maybe String 值,我们可以应用 f :: String - >字符串 - > Int ,我们可以这样应用 f

  firstString',secondString':: Maybe String 

result :: Maybe Int
result = f< $> firstString'< *>不同之处在于,我们添加< $($)

> < *> 。这通过这种方式推广到更多的参数(给定 f :: A - > B - > C - > D - > E ):

   - 当我们应用正常值(x :: A,y :: B,z :: C,w :: D)时:
result :: E
result = fxyzw

- 当我们应用具有应用实例的值时,例如x':: Maybe A,y':: Maybe B,z ':: Maybe C,w':: Maybe D:
result':: Maybe E
result'= f< $> x'* y'* z'* w'



一个非常重要的笔记



请注意,上述代码中的 Functor 适用单子。我们只是使用他们的方法,就像他们是任何其他常规的帮助函数一样。



唯一的区别是这些特定的帮助函数可以在许多不同类型的工作,但如果我们不想,我们甚至不必考虑。如果我们真的想要,我们可以想到 fmap < *> >>> = 等等,如果我们使用它们的特定类型(我们是所有这些)。


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. 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, e.g., something was wrong in the input string.

The reason I ask is that monads seem to be viral to me. 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. 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.

A deeper reason is that monads seem to be an overkill for many error-handling. One rationale for using monads as I learned is that monads allow us to thread through a state. 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).

(

EDIT: as I just read, in a monad, each action can take advantage of results from the previous actions. 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. 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.

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?

)

So, is there a weaker/more general paradigm than monads that can be used to model error-reporting? I am now reading Applicative and trying to figure out if it's suitable. 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,e.g., an Either String. The reason I ask here is that all monads (or maybe functors) enclose a basic type with a type constructor. So if you want to change your non-exception-aware function to be exception aware, you go from, e.g.,

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'' :: 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

  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):

  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. That's where the non-monadic part of the question comes from (along with the "viral" feature of monads).

  2. After the monadic computation, you don't have Ints, instead, you have Either a Int, which you cannot add directly. 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.

解决方案

In your first example, you want to compose a function f :: a -> m a with itself. Let's pick a specific a and m for the sake of discussion: Int -> Maybe Int.

Composing functions that can have errors

Okay, so as you point out, you cannot just do f (f x). Well, let's generalize this a little more to g (f x) (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:

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. Also, note that instead of f x, we could take any Maybe Int value to make things even more general. So let's also pull our g out into an argument, so we can give this function any 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 :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g

This is getting pretty close to g . f, which is how you would compose those two functions if there wasn't the Maybe discrepancy between them.

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 :: 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!

(>>=) :: 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 a -> (a -> Maybe b) -> Maybe b

Let's take a look at the Maybe instance of Monad to be sure:

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). Our original gComposeF looks like this:

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 -> m b called <=<:

(<=<) :: 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.

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. 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 (i.e., a value that won't exist in the case of an error). We want a Maybe Int to come back too, because we want the error to propagate.

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.

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:

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 str = overMaybe multiplyByTen (maybeCount str)

This function also already exists!

This is fmap from Functor!

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. This lets us apply any "normal" function to a Maybe value and get a Maybe value back, with any failure automatically propagated.

There is also a handy infix operator synonym for 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?

Maybe we have a "normal" function of multiple arguments that we need to apply to multiple Maybe values. 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. We need something in between Functor and Monad.

Let's look the division example you gave. We want to convert g' to use the safeDivide :: Int -> Int -> Either ArithmeticError Int. The "normal" g' looks like this:

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:

fmap (+) (safeDivide i k)    :: Either ArithmeticError (Int -> Int)

The type of this, by the way, is analogous to 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. It could help to mentally replace Either ArithmeticError with Maybe for now.

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.

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)

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. 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 <$>. 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:

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:

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):

-- 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. 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).

这篇关于Haskell中的非单调错误处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆