繁体   English   中英

Haskell中的非monadic错误处理?

[英]non-monadic error handling in Haskell?

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

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

更深层次的原因是monad似乎对许多错误处理来说是一种过度杀伤。 我学习使用monad的一个理由是monad允许我们穿过状态。 但是在报告错误的情况下,我认为不需要线程状态(失败状态除外,我真的不知道使用monad是否必不可少)。

编辑:正如我刚才所读到的,在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强制执行某种操作顺序,这在原始表达式中是不必要的。 这就是问题的非一元部分来源(以及monad的“病毒”特征)。

  2. 在monadic计算之后,你没有Int ,相反,你有Either a Int ,你无法直接添加。 当快递变得比添加两个术语更复杂时,样板代码将快速繁殖。 这就是封闭 - 问题的Either一部分来自于问题的一部分。

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

编写可能有错误的函数

好的,正如你指出的那样,你不能只做f (fx) 好吧,让我们来概括这一点更加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

这有点冗长,就像你说的那样,我们希望减少重复。 我们还可以注意到一种模式: Nothing总是Nothing ,并且x'被取出Just x'并被赋予组合。 另外,请注意,我们可以使用任何 Maybe Int值来代替fx ,使事情变得更加通用。 所以让我们把我们的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 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

让我们来看看MonadMaybe实例:

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

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

gComposeF x = f x >>= g

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

gComposeF x = g =<< f x

还有一个内置函数用于组合函数,其类型为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

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

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

当我们可以简化更多

正如您在问题中提到的那样,使用do notation将简单除法函数转换为正确处理错误的函数更难以阅读且更冗长。

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

maybeCount :: String -> Maybe Int ,我们会说我们有一些函数maybeCount :: String -> Maybe Int (可能将我们在String使用单词“compose”的次数除以5 maybeCount :: String -> Maybe Int 。具体是什么它并不重要虽然)我们想要将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

我们可以再次对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)

这个功能也已经存在!

这是Functor fmap

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

-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b

事实上, Maybe实例的定义也完全相同。 这允许我们将任何“正常”函数应用于Maybe值并返回Maybe值,任何失败都会自动传播。

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

countThenMultiply str = multiplyByTen <$> maybeCount str

如果我们有更多Maybes怎么办?

也许我们有多个参数的“正常”函数,我们需要应用于多个Maybe值。 当你在你的问题有,我们可以用做这个Monad ,并do标记,如果我们这样的倾向,但我们并不真正需要的全功率Monad 我们需要FunctorMonad之间的东西。

让我们来看看你给出的分组示例。 我们想要转换g'以使用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部分只是告诉我们,我们的错误以ArithmeticError值的形式提供信息,而不仅仅是Nothing 它可以帮助在精神上取代Either ArithmeticErrorMaybe现在。

嗯,这有点像我们想要的,但是我们需要一种方法将Either 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也已经存在:来自Applicative (<*>) 如果我们使用这个,我们可以到达:

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的中缀同义词。 如果我们使用它,整个事情看起来像:

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 -> Int ,我们可以像这样将f应用于它们:

firstString', secondString' :: Maybe String

result :: Maybe Int
result = f <$> firstString' <*> secondString'

区别在于我们不是添加空格,而是添加<$><*> 这以这种方式推广到更多的参数(给定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 我们只是将它们的方法用作任何其他常规辅助函数。

唯一的区别是这些特定的辅助函数可以在许多不同的类型上工作,但如果我们不想这样做,我们甚至不必考虑它。 如果我们真的想要,我们可以根据它们的特殊类型来考虑fmap<*>>>= etc,如果我们在特定类型上使用它们(在所有这些中我们都是这样)。

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

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

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

您不必提取值。 Maybe作为一个简单的例子,你经常可以编写简单的函数来处理成功案例,然后使用fmap将它们应用于Maybe值, maybe / fromMaybe来处理失败并消除Maybe wrap。 Maybe是一个monad,但这并没有强迫你使用monadic界面或do记法。 一般来说,“monadic”和“pure”之间并没有真正的对立。

我学习使用monad的一个理由是monad允许我们穿过状态。

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

那么,是否存在比monad更弱/更一般的范例,可以用来模拟错误报告? 我现在正在阅读Applicative并试图找出它是否合适。

您当然可以使用Applicative实例链接Maybe计算。 (*>)相当于(>>) ,并且没有等价于(>>=)因为Applicative不如Monad强大。 虽然通常不使用比实际需要更多的功率是一件好事,但我不确定使用Applicative是否比你想要的更简单。

虽然你可以做f (fx)你不能做f' (f' x)

你可以写f' <=< f' $ x但是:

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

您可能会发现关于(>=>)答案 ,以及可能在该问题中的其他讨论,这很有趣。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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