【问题标题】:Haskell Combining non-determinism with error handlingHaskell 将非确定性与错误处理相结合
【发布时间】:2021-02-14 08:14:14
【问题描述】:

假设我正在创建一个可以抛出错误的简单解释器,例如

type Error = String

data Term = Con Int | Div Term Term

eval :: (MonadError Error m) => Term -> m Int
eval (Con a) = return a
eval (Div u v) = do
  a <- eval u
  b <- eval v
  if b == 0 then
    throwError "Division by zero"
  else
    return $ a `div` b

具体错误处理程序的典型选择是Either Error

runEval :: Term -> Either Error Int
runEval = eval

现在假设我想扩展这个解释器来处理非确定性。例如,我可以添加一个术语 Choice Term Term,它可以评估其第一个或第二个参数。

data Term = Con Int | Div Term Term | Choice Term Term

然后我可以将具体评估表示为[Either Error Int],其中列表中的每个项目都代表一个可能的评估。但是,我正在努力如何在不修改 ConDiv 案例的情况下将 Choice 案例添加到我的 eval 函数中。

我尝试了什么:

eval :: (MonadError Error m, MonadPlus m) => Term -> m Int
-- The Con and Div cases remain unchanged.
eval (Choice u v) = do
  w <- return u `mplus` return v  -- pick either u or v
  eval w

runEval :: Term -> [Either Error Int]
runEval = runExceptT . eval

但是,这只会返回第一个可能的结果,并且不会回溯。

> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
> runEval t
[Left "Division by zero"]

我的预期:[Left "Division by zero", Right 1]

结合非确定性和错误处理的正确方法是什么?

【问题讨论】:

  • 你可以看看monad transformers。这或多或少用于创建 monad 上下文的“堆栈”。
  • @WillemVanOnsem 谢谢你的评论。实际上,目标是使用转换器将错误单子和列表单子结合起来。这实际上就是我在runEval 中所做的:在那里我使用runExceptT,我认为这意味着Haskell 将有效地使用monad ExceptT Error [] Int 进行我的计算。但是,正如我的问题所解释的那样,它并没有像我预期的那样工作。
  • 目前,你的evalExceptT Error [] Int,但我想你想要的是ListT (ExceptT Error) Int
  • @ArtemPelenitsyn 对于ListT (ExceptT Error),一个错误就会停止计算。 runEval 会有一个类似Term -&gt; Either Error [Int] 的签名。

标签: haskell monads monad-transformers non-deterministic


【解决方案1】:

问题的根源是ExceptTMonadPlus 实例。

(Monad m, Monoid e) => MonadPlus (ExceptT e m)

它不会搭载基本单子mMonadPlus 实例。相反,它需要来自错误eMonoid 实例。

并且mplus 不会返回所有失败和成功的集合。取而代之的是,它返回 first 成功或所有失败的 monoidal 组合:

ghci> throwError ['a'] `mplus` throwError ['b'] :: Except String ()
ExceptT (Identity (Left "ab"))
ghci> throwError ['a'] `mplus` throwError ['b'] `mplus` return () :: Except String ()
ExceptT (Identity (Right ()))
ghci> return 'a' `mplus` return 'b' :: ExceptT () [] Char
ExceptT [Right 'a']

我们可以尝试定义我们自己的 monad,使其具有我们想要的 MonadPlus 实例(同时重用从 ExceptTderiving 的所有其他实例,以避免样板)。

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Except

newtype OopsT e m a = OopsT { runOopsT :: ExceptT e m a }
    deriving stock (Show)
    deriving newtype (Show,Functor,Applicative,Monad,MonadError e,MonadTrans)

-- We delegate on the Alternative/Monadplus instance of the base monad m
instance MonadPlus m => Alternative (OopsT e m) where
    empty = OopsT (ExceptT empty)       
    OopsT (ExceptT xs) <|> OopsT (ExceptT ys) = OopsT (ExceptT (xs <|> ys)

instance MonadPlus m => MonadPlus (OopsT e m) where
    mzero = empty       
    mplus = (<|>)

runEval :: Term -> [Either Error Int]
runEval = runExceptT . runOopsT . eval

现在它似乎按预期工作:

ghci> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
ghci> runEval t
[Left "Division by zero",Right 1]

OopsTMonadPlus 实例的一个潜在问题是它似乎不满足documentation 中提到的v &gt;&gt; mzero = mzero 法则。例如:

ghci> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT []}
ghci> throwError 'c' >> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT [Left 'c']}

也许我们可以改用等效的 Alternative 实例,这似乎不需要该法律?

【讨论】:

  • 很好的解释。请问:Oops 的完整形式是什么?
  • @Safron 我不确定您所说的“完整形式”是什么意思。 Oops 是一个常见模式的例子:我们选择性地派生或定义实例的辅助新类型。我们从底层类型派生了很多有用的实例(从FunctorMonadTrans),这节省了我们的工作,并且必须手动定义两个实例(AlternativeMonadPlus)以满足我们的需求。
  • 对,我以为它是某种缩写,但现在我发现它可能只是作为“可能失败的东西”又名“哎呀”的同义词。我的坏:-)
  • 一个问题:runEval $ Div (Div (Con 1) (Con 0)) (Choice (Con 1) (Con 2))的结果是什么?结果中是否存在一两个错误? (一个合法的实例应该有 2 个)
  • 一个相关的 SO 问题 stackoverflow.com/questions/10167879/… 和 Haskell Wikibook 中讨论 AlternativeMonadPlus en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus 的条目
猜你喜欢
  • 2014-05-18
  • 1970-01-01
  • 2013-11-08
  • 1970-01-01
  • 2015-02-15
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
相关资源
最近更新 更多