如何解决Haskell将不确定性与错误处理相结合
假设我正在创建一个简单的解释器,该解释器可能会引发错误,例如
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]
,其中列表中的每个项目代表一个可能的评估。但是,我在如何在不修改Choice
和eval
情况下将Con
情况添加到Div
函数中的问题中。
我尝试过的事情:
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]
。
解决方法
问题的根源是ExceptT
的MonadPlus
实例。
(Monad m,Monoid e) => MonadPlus (ExceptT e m)
它不会搭载在基本monad MonadPlus
的{{1}}实例上。取而代之的是,它需要错误m
中的Monoid
实例。
并且e
不会返回所有失败和成功的集合。取而代之的是,它返回 first 成功或所有失败的简单组合:
mplus
我们可以尝试定义自己的单子,并拥有所需的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
到ExceptT
的所有其他实例,以避免重复样板。)
deriving
现在,它似乎可以按预期工作:
{-# 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]
的{{1}}实例的一个可能令人不安的方面是,它似乎不符合documentation中提到的MonadPlus
律。例如:
OopsT
也许我们可以使用等效的v >> mzero = mzero
实例,但这似乎并不需要该法律?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。