Haskell中的非monadic错误处理?

tin*_*lyx 3 error-handling monads haskell exception applicative

我在想,如果有做非一元的错误处理哈斯克尔一种优雅的方式在语法上比使用普通简单的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
Run Code Online (Sandbox Code Playgroud)

f':: a -> m a  -- exception-aware
Run Code Online (Sandbox Code Playgroud)

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

f (f x)
Run Code Online (Sandbox Code Playgroud)

你不能这样做

f' (f' x)
Run Code Online (Sandbox Code Playgroud)

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

f'' :: m a -> m a
Run Code Online (Sandbox Code Playgroud)

我想知道是否有一种优雅的方式在这条线上进行错误处理/报告工作?

谢谢.

- 编辑---

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

  g' i j k = i / k + j / k
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 正如本教程的作者所指出的,monad强制执行某种操作顺序,这在原始表达式中是不必要的.这就是问题的非一元部分来源(以及monad的"病毒"特征).

  2. 在monadic计算之后,你没有Ints,而是你有Either a Int,你无法直接添加.当快递变得比添加两个术语更复杂时,样板代码将快速繁殖.这就是围绕问题的所有内容的Either来源.

Dav*_*vid 7

在第一个示例中,您希望自己编写一个函数f :: a -> m a.让我们选择一个特定的am为讨论的缘故Int -> 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
Run Code Online (Sandbox Code Playgroud)

这有点冗长,就像你说的那样,我们希望减少重复.我们还可以注意到一种模式:Nothing总是去Nothing,并x'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'
Run Code Online (Sandbox Code Playgroud)

有了这个辅助函数,我们可以gComposeF像这样重写我们的原始函数:

gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g
Run Code Online (Sandbox Code Playgroud)

这变得非常接近g . f,如果Maybe它们之间没有差异,那么你将如何编写这两个函数.

接下来,我们可以看到我们的bindMaybe函数并不特别需要Int或者String,所以我们可以使它更有用:

bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing   g = Nothing
bindMaybe (Just x') g = g x'
Run Code Online (Sandbox Code Playgroud)

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

这已经存在了!

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

(>>=) :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

如果我们替换Maybem(因为Maybe是一个实例Monad,我们可以做到这一点),我们得到的类型相同bindMaybe:

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Run Code Online (Sandbox Code Playgroud)

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

instance Monad Maybe where
  return x      = Just x
  Nothing >>= f = Nothing
  Just x  >>= f = f x
Run Code Online (Sandbox Code Playgroud)

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

gComposeF x = f x >>= g
Run Code Online (Sandbox Code Playgroud)

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

gComposeF x = g =<< f x
Run Code Online (Sandbox Code Playgroud)

也有与各类形式的创作功能的内置函数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
Run Code Online (Sandbox Code Playgroud)

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

gComposeF = g <=< f  -- This is very similar to g . f, which is how we "normally" compose functions
Run Code Online (Sandbox Code Playgroud)

当我们可以简化更多

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

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

为了具体,我们会说,我们有一些功能maybeCount :: String -> Maybe Int(也许我们用这个词"撰写"的数量乘以分5 String和轮下来,它并不真正的问题是什么特别虽然)我们希望应用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
Run Code Online (Sandbox Code Playgroud)

我们可以再次做出类似的"拉出" multiplyByTen来进一步概括:

overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
  case mstr of
    Nothing -> Nothing
    Just x  -> f x
Run Code Online (Sandbox Code Playgroud)

这些类型也可以更通用:

overMaybe :: (a -> b) -> Maybe a -> Maybe b
Run Code Online (Sandbox Code Playgroud)

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

countThenMultiply然后我们可以改写:

countThenMultiply str = overMaybe multiplyByTen (maybeCount str)
Run Code Online (Sandbox Code Playgroud)

这个功能也已经存在!

这是fmap来自Functor!

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

-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b
Run Code Online (Sandbox Code Playgroud)

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

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

countThenMultiply str = multiplyByTen <$> maybeCount str
Run Code Online (Sandbox Code Playgroud)

如果我们有更多Maybes

也许我们有多个参数的"正常"函数,我们需要将其应用于多个Maybe值.正如你在问题中所说的那样,如果我们如此倾向,我们可以Monaddo表示法,但我们实际上并不需要充分的力量Monad.我们需要的东西之间FunctorMonad.

让我们来看看你给出的分组示例.我们想转换g'为使用safeDivide :: Int -> Int -> Either ArithmeticError Int."正常" g'看起来像这样:

g' i j k = i / k + j / k
Run Code Online (Sandbox Code Playgroud)

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

g' i j k = (safeDivide i k) + (safeDivide j k)
Run Code Online (Sandbox Code Playgroud)

好了,我们可以得到接近使用Functor:

fmap (+) (safeDivide i k)    :: Either ArithmeticError (Int -> Int)
Run Code Online (Sandbox Code Playgroud)

顺便提一下,这种类型与此类似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)
Run Code Online (Sandbox Code Playgroud)

(作为旁注,第二个case可以简化fmap)

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

g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)
Run Code Online (Sandbox Code Playgroud)

这仍然看起来不太好,但现在让我们继续吧.

原来它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
Run Code Online (Sandbox Code Playgroud)

你可能记得早些时候有一个fmap被叫的中缀同义词<$>.如果我们使用它,整个事情看起来像:

g' i j k = (+) <$> safeDivide i k <*> safeDivide j k
Run Code Online (Sandbox Code Playgroud)

这看起来很奇怪,但你已经习惯了.您可以将<$><*>视为"上下文敏感的空白".我的意思是,如果我们有一些常规函数f :: String -> String -> Int,我们将它应用于正常值,String我们有:

firstString, secondString :: String

result :: Int
result = f firstString secondString
Run Code Online (Sandbox Code Playgroud)

如果我们有两个(例如)Maybe String值,我们可以申请f :: String -> String -> Int,我们可以f像这样申请两个:

firstString', secondString' :: Maybe String

result :: Maybe Int
result = f <$> firstString' <*> secondString'
Run Code Online (Sandbox Code Playgroud)

不同的是,我们添加<$>和而不是空格<*>.这以这种方式推广到更多的参数(给定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'
Run Code Online (Sandbox Code Playgroud)

一个非常重要的说明

请注意,没有上面的代码中提到的Functor,ApplicativeMonad.我们只是将它们的方法用作任何其他常规辅助函数.

唯一的区别是这些特定的辅助函数可以在许多不同的类型上工作,但如果我们不想这样做,我们甚至不必考虑它.如果我们真的想,我们只要想到的fmap,<*>,>>=等在他们的专业类型看,如果我们使用的是他们在特定类型的(我们是在这一切).


dup*_*ode 5

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

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

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

您不必提取值.以Maybe一个简单的例子,很多时候你可以只写简单的函数来处理成功案例,然后用fmap将它们应用到你的Maybe价值观和maybe/ fromMaybe处理故障和消除Maybe包装.Maybe是一个monad,但这并不是要求你一直使用monadic接口或do符号.一般来说,"monadic"和"pure"之间并没有真正的对立.

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

这只是众多用例中的一个.该Maybe单子让你跳过所有剩余的计算以失败后绑定链.它不会破坏任何类型的状态.

那么,是否存在比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
Run Code Online (Sandbox Code Playgroud)

您可能会发现这个问题的答案(>=>)很有趣,也可能是该问题中的其他讨论.