[英]non-monadic error handling in Haskell?
我想知道是否有一种优雅的方式在Haskell中进行非monadic错误处理,这在句法上比使用普通的Maybe
或Either
更简单。 我想要处理的是非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)
如果(+)
也可能导致错误,则需要三个动作。 我认为当前方法中这种复杂性的两个原因是:
正如本教程的作者所指出的,monad强制执行某种操作顺序,这在原始表达式中是不必要的。 这就是问题的非一元部分来源(以及monad的“病毒”特征)。
在monadic计算之后,你没有Int
,相反,你有Either a Int
,你无法直接添加。 当快递变得比添加两个术语更复杂时,样板代码将快速繁殖。 这就是封闭 - 问题的Either
一部分来自于问题的一部分。
在第一个示例中,您希望自己编写函数f :: a -> ma
。 让我们为讨论选择一个特定的a
和m
: Int -> 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
函数并不特别需要Int
或String
,因此我们可以使它更有用:
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
(因为Maybe
是Monad
一个实例,我们可以这样做),我们得到与bindMaybe
相同的类型:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
让我们来看看Monad
的Maybe
实例:
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
。 我们需要Functor
和Monad
之间的东西。
让我们来看看你给出的分组示例。 我们想要转换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 ArithmeticError
与Maybe
现在。
嗯,这有点像我们想要的,但是我们需要一种方法将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'
请注意,上述代码均未提及Functor
, Applicative
或Monad
。 我们只是将它们的方法用作任何其他常规辅助函数。
唯一的区别是这些特定的辅助函数可以在许多不同的类型上工作,但如果我们不想这样做,我们甚至不必考虑它。 如果我们真的想要,我们可以根据它们的特殊类型来考虑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.