【问题标题】:non-monadic error handling in Haskell?Haskell 中的非单子错误处理?
【发布时间】:2014-04-20 23:15:21
【问题描述】:

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

我问的原因是单子对我来说似乎很流行。如果我想使用异常或类异常机制来报告纯函数中的非关键错误,我总是可以使用either 并对结果进行case 分析。一旦我使用了 monad,提取 monadic 值的内容并将其提供给不使用 monadic 值的函数会很麻烦/不容易。

更深层次的原因是,对于许多错误处理来说,monad 似乎是一种过度杀伤力。据我所知,使用 monad 的一个基本原理是 monad 允许我们遍历状态。但是在报告错误的情况下,我认为不需要线程状态(除了失败状态,老实说我不知道​​是否必须使用 monad)。

(

编辑:正如我刚刚读到的,在单子中,每个动作都可以利用先前动作的结果。但是在报错时,往往不需要知道前面动作的结果。所以这里有一个潜在的过度杀戮使用单子。在许多情况下,所需要做的只是在不知道任何先前状态的情况下现场中止和报告故障。 Applicative 对我来说似乎是一个限制较少的选择。

在解析的具体例子中,我们自己提出的异常/错误在本质上真的有效吗?如果没有,有没有比Applicative 更弱的东西来模拟错误处理?

)

那么,有没有比 monad 更弱/更通用的范式可用于对错误报告进行建模?我现在正在阅读Applicative 并试图弄清楚它是否合适。只是想事先问一下,以免我错过显而易见的事情。

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

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

能够处理除以零错误,当前的方法是逐项分解表达式,并以一元动作计算每个项(有点像用汇编语言重写):

  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 强制执行特定的操作顺序,这在原始表达式中不是必需的。这就是问题的非单子部分的来源(以及单子的“病毒”特征)。

  2. 在monadic计算之后,你没有Ints,而是你有Either a Int,你不能直接添加。当表达式变得比添加两个术语更复杂时,样板代码会迅速增加。这就是问题的 enclosure-everything-in-a-Either 部分的来源。

【问题讨论】:

  • ... 但是 MaybeEither monads并且很优雅。当它做出如此多有争议的陈述时,很难回答这个问题。也许您正在寻找幺半群?
  • @ThomasM.DuBuisson 我完全同意将MaybeEither 用作monad 与将它们与case 语句一起使用相比更为优雅。我想知道在处理方面是否有比 monad 更优雅的方法,例如,我们自己提出的“逻辑”异常(解析和除以零)。
  • 使用 monads 会强制将表达式分解为类似汇编的动作,如我引用的示例等。我怀疑这是因为单子强加了太多。我知道我问了很多。但是,在某些简单的错误处理中引发 monad 的问题已经困扰了很长一段时间。甚至不确定自己如何提出这个问题。我现在正在调查Applicative,因为似乎有人在 1980 年代以应用风格进行了错误处理。 monoids 的任何具体示例/参考用于错误处理?
  • 没有关于 monad 的“病毒”。我觉得您可能会将 monad 的一般概念与特定的 monad 实例 IO 混淆。你不能“离开”IO 的事实与它是一个 monad 无关。
  • 我想你在这里是关于IO。我想我也在考虑这样一个事实,即与评估函数相比,使用 do 表示法会显着改变编程。有人确实说单子是命令式的重新发明?在 Haskell 世界中编程。一旦你开始涉及 monad,这两种编程风格的混合似乎就会发生。

标签: exception haskell error-handling monads applicative


【解决方案1】:

在你的第一个例子中,你想用它自己组成一个函数f :: a -&gt; m a。让我们选择一个特定的am 进行讨论:Int -&gt; Maybe Int

编写可能出错的函数

好的,正如你所指出的,你不能只做f (f x)。好吧,让我们将其进一步概括为g (f x)(假设我们得到了g :: Int -&gt; 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' 中取出并赋予组合。另外,请注意,我们可以使用 any Maybe Int 值而不是 f x,以使事情更加普遍。所以让我们也将g 拉到一个参数中,这样我们就可以给这个函数any 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 类型类的 &gt;&gt;= 方法!

(>>=) :: 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 一样,除了我们还有一个额外的方法可以让我们将某些东西放入“一元上下文”中(在这种情况下,这只是意味着将其包装在Just 中)。我们原来的gComposeF 是这样的:

gComposeF x = f x >>= g

还有=&lt;&lt;,它是&gt;&gt;= 的翻转版本,这让它看起来更像普通的合成版本:

gComposeF x = g =<< f x

还有一个内置函数用于组合类型为 a -&gt; m b 的函数,称为 &lt;=&lt;

(<=<) :: 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。我们可以通过说我们真正想要做的是采用“常规”函数(例如我们的multiplyByTen :: Int -&gt; Int)并且我们想给它一个Maybe Int(即,在发生错误的情况下将不存在的值)。我们希望Maybe Int 也能返回,因为我们希望错误传播。

具体来说,我们会说我们有一些函数maybeCount :: String -&gt; 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

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

这个函数也已经存在了!

这是来自Functorfmap

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

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

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

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

countThenMultiply str = multiplyByTen <$> maybeCount str

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

也许我们有一个包含多个参数的“正常”函数,我们需要将其应用于多个Maybe 值。正如您在您的问题中所提到的,如果我们愿意,我们可以使用Monaddo 表示法,但我们实际上并不需要Monad 的全部功能。我们需要介于FunctorMonad 之间的东西。

让我们看看你给出的除法示例。我们想将g' 转换为使用safeDivide :: Int -&gt; Int -&gt; Either ArithmeticError Int。 “正常”g' 看起来像这样:

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

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

g' i j k = (safeDivide i k) + (safeDivide j k)

好吧,我们可以通过Functorclose

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

顺便说一下,这个类型类似于Maybe (Int -&gt; Int)Either ArithmeticError 部分只是告诉我们,我们的错误以ArithmeticError 值的形式向我们提供信息,而不仅仅是Nothing。现在用Maybe 替换Either ArithmeticError 可能会有所帮助。

嗯,这有点像我们想要的,但我们需要一种方法来将函数“内部”应用到 Either ArithmeticError (Int -&gt; 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(&lt;*&gt;)。如果我们使用它,我们可以得出:

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

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

一开始这看起来很奇怪,但你会习惯的。您可以将&lt;$&gt;&lt;*&gt; 视为“上下文相关空白”。我的意思是,如果我们有一些常规函数 f :: String -&gt; String -&gt; Int 并将其应用于我们拥有的普通 String 值:

firstString, secondString :: String

result :: Int
result = f firstString secondString

如果我们有两个(例如)Maybe String 值,我们可以应用f :: String -&gt; String -&gt; Int,我们可以像这样将f 应用到它们:

firstString', secondString' :: Maybe String

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

不同之处在于,我们添加了&lt;$&gt;&lt;*&gt;,而不是空格。这可以通过这种方式推广到更多参数(给定f :: A -&gt; B -&gt; C -&gt; D -&gt; 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&lt;*&gt;&gt;&gt;= 等的特殊类型,如果我们在特定类型上使用它们(我们在所有这些中都是这样)。

【讨论】:

  • 非常感谢您的详细回答。这真的很有帮助。一个额外的问题。假设我想做 'safeDivde 1 (safeDivide 1 (... (safeDivide 1 x)..))' 任意次数(例如在牛顿的方法中),我将如何使用 Control.Applicative 来表达它?似乎使用 和 会导致包含在无限数量的 'Either ArithmeticError' 中的东西。
  • @TingL:我可能会使用 Data.Foldable 中的 foldrMfoldrM 类似于 foldr,但它适用于一元上下文):flipFlop nestingLevel n = foldrM safeDivide n (replicate nestingLevel 1)foldrM 的类型签名看起来有点奇怪,但请记住我们使用的 Traversable 实例是 [],因此您可以将 t a 替换为 [a]
  • 这与非单子等效项非常相似:flipFlop nestingLevel n = foldr (/) n (replicate nestingLevel 1)。此外,这可能是类型类的额外抽象级别何时有用的示例:即使您更改safeDivide 使用的Monad,此代码仍然有效(也许您决定要Maybe Int 而不是@987654459 @ 在某个时候)。
  • 哦,应该说“Foldable 实例”而不是“Traversable 实例”
  • 这是一个等价的定义,它使单子绑定更​​加明显:flipFlop 0 n = return n; flipFlop nestingLevel n = safeDivide 1 =&lt;&lt; flipFlop (nestingLevel - 1) n(它不使用任何列表,因此在结构上有点不同。但它执行相同的功能)。
【解决方案2】:

我问的原因是单子对我来说似乎很流行。

这种病毒特性实际上非常适合异常处理,因为它迫使您认识到您的功能可能会失败并处理失败的情况。

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

您不必提取价值。以Maybe 为例,很多时候你可以编写简单的函数来处理成功案例,然后使用fmap 将它们应用到你的Maybe 值和maybe/fromMaybe 来处理失败并消除 Maybe 包装。 Maybe 是一个单子,但这并不要求您一直使用单子接口或 do 表示法。一般来说,“一元”和“纯粹”之间并没有真正的对立。

据我所知,使用 monad 的一个基本原理是 monad 允许我们 穿过一个状态。

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

那么,有没有比 monad 更弱/更通用的范式? 用于模型错误报告?我现在正在阅读Applicative 并尝试 看看是否合适。

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

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

你可以写f' &lt;=&lt; f' $ x

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

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

【讨论】:

  • 感谢您的意见。正如您所指出的,我将阅读 (>=>)。另一方面,虽然我认识到 monad 的病毒性特征在处理 IO 执行时的必要性,但我仍然相信在错误处理其他场合(例如在 OP 中我们可以通过使用纯MaybeEither。我的第一点是是否有办法以优雅的方式进行非病毒错误处理。
  • @TingL 在某种意义上,普通的Maybe 也是病毒式的,因为您需要fmap(&gt;&gt;=) 等等来处理包装的值,直到您消除Maybe .也许我错过了你的观点。非病毒错误处理会是什么样子?您是否有任何不涉及MaybeEither 的特定情况?
  • 我不想消除Either,如果可能的话,我想找到一种比monads更优雅的使用Either的方式。
  • 几年前,我正在阅读一篇关于在 Haskell 中用 48 小时编写 Lisp 解释器的教程。一切都是至关重要的清晰表达,直到您到达错误处理时,作者开始使用 IOException 或另一个 monad,这使得有必要从一开始就彻底检查整个类型系统。从那时起我就在想:这真的有必要吗?如果您正在解析某些内容并且经常会出错,为什么要将所有相关内容都包含在 Either 中?
  • 我确实意识到我可以使用EitherMaybe 并使用case 分析来避免将单子引入类型系统。但很明显,样板代码太多,不可读。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-14
  • 1970-01-01
  • 1970-01-01
  • 2021-12-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多