Haskell 将不确定性与错误处理相结合
Haskell Combining non-determinism with error handling
假设我正在创建一个可以抛出错误的简单解释器,例如
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]
,其中列表中的每个项目都代表一个可能的评估。但是,我正在努力如何在不修改 Con
和 Div
案例的情况下将 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
然而,这只是returns第一个可能的结果,不会回溯。
> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
> runEval t
[Left "Division by zero"]
我的预期:[Left "Division by zero", Right 1]
。
结合不确定性和错误处理的正确方法是什么?
问题的根源在于 ExceptT
的 MonadPlus
实例。
(Monad m, Monoid e) => MonadPlus (ExceptT e m)
它不搭载基础 monad m
的 MonadPlus
实例。相反,它需要一个来自错误 e
.
的 Monoid
实例
而mplus
并不是return所有失败和成功的集合。相反,它 return 是 第一次 成功或所有失败的幺半群组合:
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']
我们可以尝试定义我们自己的具有我们想要的 MonadPlus
实例的 monad(同时重用从 ExceptT
到 deriving
的所有其他实例以避免样板)。 =31=]
{-# 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]
OopsT
的 MonadPlus
实例的一个潜在麻烦方面是它似乎不满足 documentation 中提到的 v >> mzero = mzero
法则。例如:
ghci> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT []}
ghci> throwError 'c' >> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT [Left 'c']}
也许我们可以使用等效的 Alternative
实例来代替,这似乎不需要该法则?
假设我正在创建一个可以抛出错误的简单解释器,例如
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]
,其中列表中的每个项目都代表一个可能的评估。但是,我正在努力如何在不修改 Con
和 Div
案例的情况下将 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
然而,这只是returns第一个可能的结果,不会回溯。
> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
> runEval t
[Left "Division by zero"]
我的预期:[Left "Division by zero", Right 1]
。
结合不确定性和错误处理的正确方法是什么?
问题的根源在于 ExceptT
的 MonadPlus
实例。
(Monad m, Monoid e) => MonadPlus (ExceptT e m)
它不搭载基础 monad m
的 MonadPlus
实例。相反,它需要一个来自错误 e
.
Monoid
实例
而mplus
并不是return所有失败和成功的集合。相反,它 return 是 第一次 成功或所有失败的幺半群组合:
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']
我们可以尝试定义我们自己的具有我们想要的 MonadPlus
实例的 monad(同时重用从 ExceptT
到 deriving
的所有其他实例以避免样板)。 =31=]
{-# 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]
OopsT
的 MonadPlus
实例的一个潜在麻烦方面是它似乎不满足 documentation 中提到的 v >> mzero = mzero
法则。例如:
ghci> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT []}
ghci> throwError 'c' >> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT [Left 'c']}
也许我们可以使用等效的 Alternative
实例来代替,这似乎不需要该法则?