Haskell中的非monadic错误处理?

时间:2014-04-20 23:15:22

标签: exception haskell error-handling monads applicative

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

我问的原因是monad似乎对我有病毒感染。如果我想使用异常或异常机制来报告纯函数中的非严重错误,我可以始终使用either并对结果进行case分析。一旦我使用monad,它很麻烦/不容易提取monadic值的内容并将其提供给不使用monadic值的函数。

更深层次的原因是,对于许多错误处理来说,monad似乎是一种矫枉过正。我学习使用monad的一个理由是monad允许我们穿过状态。但是在报告错误的情况下,我没有看到任何线程状态的需要(失败状态除外,我真的不知道使用monad是否必不可少)。 / p>

编辑:正如我刚才所读到的,在monad中,每个动作都可以利用之前动作的结果。但是在报告错误时,通常不必知道先前操作的结果。因此,使用monads可能存在过度杀戮。在许多情况下,所需要的只是在不知道任何先前状态的情况下中止并报告现场故障。 Applicative对我来说似乎是一个限制较少的选择。

在解析的具体例子中,我们自己提出的行为/错误是否真的有效?如果没有,是否有一些甚至弱于Applicative的模型错误处理?

那么,是否存在比monad更弱/更一般的范例可用于模拟错误报告?我现在正在阅读Applicative并试图弄清楚它是否合适。只是想事先询问,以便我不会错过显而易见的事。

与此相关的一个问题是,是否有一种机制可以简单地用每个基本类型括起来,例如Either String。我在这里问的原因是所有monad(或者可能是functor)都包含一个带有类型构造函数的基本类型。因此,如果您想要将非异常感知功能更改为异常,请从例如

开始
f:: a -> a   -- non-exception-aware

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

但是,这种改变打破了在非例外情​​况下可以起作用的功能组合。虽然你可以做到

f (f x)

你无法做到

f' (f' x)

因为外壳。解决可能性问题的一种可能天真的方法是将f更改为:

f'' :: m a -> m a

我想知道是否有一种优雅的方法可以沿着这条线进行错误处理/报告工作?

感谢。

- 编辑---

只是为了澄清这个问题,从http://mvanier.livejournal.com/5103.html举个例子来制作一个像

这样的简单函数
  g' i j k = i / k + j / k

能够处理零除错误,目前的方法是逐项分解表达式,并在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)

如果(+)也可能导致错误,则需要采取三项措施。我认为当前方法中这种复杂性的两个原因是:

  1. 正如本教程的作者所指出的那样,monad强制执行某种操作顺序,这在原始表达式中并不是必需的。这个问题的非一元部分来自(以及&#34;病毒&#34; monad的特征)。

  2. 在monadic计算之后,你没有Int s,而是Either a Int,你无法直接添加Either。当快递变得比添加两个术语更复杂时,样板代码将快速繁殖。这个问题的所有部分来自{{1}}部分。

2 个答案:

答案 0 :(得分:7)

在第一个示例中,您希望自己编写一个函数f :: a -> m a。为了便于讨论,我们选择一个特定的amInt -> Maybe Int

编写可能有错误的函数

好的,正如你指出的那样,你不能只做f (f x)。好吧,让我们稍微概括一点g (f x)(让我们说我们给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

这有点冗长,就像你说的那样,我们希望减少重复。我们还可以注意到一种模式:Nothing始终转到Nothingx'Just x'中取出并提供给合成。另请注意,我们可以使用任何 f x值来代替Maybe Int,以使事情变得更加通用。所以,让我们将g拉出一个参数,这样我们就可以给这个函数任何 g

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

使用这个辅助函数,我们可以像这样重写原始的gComposeF

gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g

这非常接近g . f,如果不存在Maybe之间的差异,就可以将这两个函数组合起来。

接下来,我们可以看到我们的bindMaybe函数并不特别需要IntString,因此我们可以使其更有用:

bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing   g = Nothing
bindMaybe (Just x') g = g x'

实际上,我们必须改变的是类型签名。

这已经存在!

现在,bindMaybe实际上已经存在:它是>>=类型类中的Monad方法!

(>>=) :: Monad m => m a -> (a -> m b) -> m b

如果我们将Maybe替换为m(因为MaybeMonad的实例,我们可以这样做)我们获得与bindMaybe相同的类型:

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

让我们来看看Maybe Monad的{​​{1}}实例,以确定:

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

就像bindMaybe一样,除了我们还有一个额外的方法可以让我们把东西放到&#34; monadic context&#34; (在这种情况下,这只是意味着将其包装在Just中)。我们原来的gComposeF看起来像这样:

gComposeF x = f x >>= g

还有=<<,它是>>=的翻转版本,让它看起来更像普通的合成版本:

gComposeF x = g =<< f x

还有一个内置函数,用于使用a -> m b <=<形式的函数组合函数:

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

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

gComposeF = g <=< f  -- This is very similar to g . f, which is how we "normally" compose functions

当我们可以简化更多

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

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

为了具体,我们会说我们有一些函数maybeCount :: String -> Maybe Int(可能将我们使用单词&#34的次数除以5;在String中编写&#34;并向下舍入。尽管如此具体并不重要,我们希望将multiplyByTen应用于结果。

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

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

我们可以再次做一个类似的&#34;拔出&#34; multiplyByTen进一步概括:

overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
  case mstr of
    Nothing -> Nothing
    Just x  -> f x

这些类型也可以更通用:

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

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

然后可以重写我们的countThenMultiply

countThenMultiply str = overMaybe multiplyByTen (maybeCount str)

此功能也已存在!

这是来自fmap的{​​{1}}!

Functor
事实上,fmap :: Functor f => (a -> b) -> f a -> f b -- Specializing f to Maybe: fmap :: (a -> b) -> Maybe a -> Maybe b 实例的定义也完全相同。这让我们可以应用任何&#34;正常&#34;函数为Maybe并返回Maybe值,任何失败都会自动传播。

Maybe还有一个方便的中缀运算符同义词:fmap。这将在以后派上用场。如果我们使用这个同义词,这就是它的样子:

(<$>) = fmap

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

,该怎么办?

也许我们有一个正常的&#34;我们需要应用于多个Maybes值的多个参数的函数。正如您在问题中所说的那样,如果我们如此倾向,我们可以MaybeMonad符号执行此操作,但我们实际上并不需要充分的力量do。我们需要介于MonadFunctor之间的内容。

让我们看看你给出的分组示例。我们希望转换Monad以使用g'。 &#34;正常&#34; safeDivide :: Int -> Int -> Either ArithmeticError Int看起来像这样:

g'

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

g' i j k = i / k + j / k

好吧,我们可以使用g' i j k = (safeDivide i k) + (safeDivide j k)

关闭
Functor

顺便提一下,这种类型与fmap (+) (safeDivide i k) :: Either ArithmeticError (Int -> Int) 类似。 Maybe (Int -> Int)部分只是告诉我们,我们的错误会以Either ArithmeticError值的形式向我们提供信息,而不仅仅是ArithmeticErrorNothing替换为Either ArithmeticError

嗯,这有点像我们想要的,但我们需要一种方法来应用函数&#34; inside&#34; MaybeEither ArithmeticError (Int -> Int)

我们的案例分析如下:

Either ArithmeticError Int

(作为旁注,第二个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) 可以使用case简化

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

fmap

这仍然看起来不太好,但现在让我们继续使用它。

结果g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k) 也已存在:来自eitherApply的{​​{1}}。如果我们使用它,我们可以到达:

(<*>)

您可能还记得,之前有一个名为Applicative的{​​{1}}的中缀同义词。如果我们使用它,整个事情看起来像:

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

一开始看起来很奇怪,但你已经习惯了。您可以将fmap<$>视为&#34;上下文敏感的空白。&#34;我的意思是,如果我们有一些常规函数g' i j k = (+) <$> safeDivide i k <*> safeDivide j k ,我们将它应用于我们的正常<$>值:

<*>

如果我们有两个(例如)f :: String -> String -> Int值,我们可以应用String,我们可以将firstString, secondString :: String result :: Int result = f firstString secondString 应用于这两个值:

Maybe String

不同之处在于,我们不是添加空格,而是添加f :: String -> String -> Intf。这以这种方式推广了更多的参数(给定firstString', secondString' :: Maybe String result :: Maybe Int result = f <$> firstString' <*> secondString' ):

<$>

非常重要的注意事项

请注意,上述代码的 none 提到了<*>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' 。我们只是将它们的方法用作任何其他常规辅助函数。

唯一的区别是这些特定的帮助函数可以在许多不同的类型上工作,但如果我们不想,我们甚至不必考虑这个问题。如果我们真的想要,我们可以根据其特殊类型考虑FunctorApplicativeMonad等,如果我们在特定类型上使用它们(我们是所有这一切)。

答案 1 :(得分:5)

  

我问的原因是monad似乎对我有病毒。

这种病毒特征实际上非常适合异常处理,因为它会强制您识别您的功能可能会失败并处理失败案例。

  

一旦我使用monad,它很麻烦/不容易提取内容   一个monadic值并将其提供给不使用monadic值的函数。

您不必提取价值。以Maybe为例,您经常可以编写普通函数来处理成功案例,然后使用fmap将它们应用于Maybe值和maybe / fromMaybe处理失败并消除Maybe包装。 Maybe是一个monad,但这并不是要求您始终使用monadic接口或do表示法。一般来说,monadic&#34;之间没有真正的对立。和&#34;纯粹&#34;。

  

我学习使用monad的一个理由是monad允许我们这样做   穿越一个州。

这只是众多用例中的一个。 Maybe monad允许您在失败后跳过绑定链中的任何剩余计算。它不会阻止任何形式的状态。

  

那么,是否存在比monad更弱/更一般的范例   用于模拟错误报告?我正在阅读Applicative并尝试   弄清楚它是否合适。

您当然可以使用Maybe实例链接Applicative次计算。 (*>)相当于(>>),并且没有与(>>=)相当,因为Applicative的功能不如Monad。虽然不使用比实际需要更多的功率通常是一件好事,但我不确定在您瞄准的意义上使用Applicative是否更简单。

  

虽然您可以执行f (f x),但您无法f' (f' x)

您可以写f' <=< f' $ x

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

您可能会发现this answer about (>=>),以及该问题中的其他讨论,这很有趣。