在Haskell中的纯异常处理

6

如何在 Haskell 中使用异常而不必通过 IO?

我有以下代码,用于在二叉搜索树中插入元素,使比较最小且当元素是树的成员时不进行复制。我注意到either被用作catch,而Left则被用作throw

insert x t = either (const t) id (insert' x t Nothing)
    where
    insert' x E m = maybe (Right (T E x E)) (\v -> if x==v then Left E else Right (T E x E)) m
    insert' x t@(T l v r) m = if x<v
                                 then fmap (\l' -> T l' v r) (insert' x l Nothing)
                                 else fmap (\r' -> T l v r') (insert' x r (Just v))

我尝试使用 Control.Monad.Error 重写它,希望能让代码更简单,但结果变得一团糟。有什么建议吗?

3个回答

8

这取决于您需要异常的目的。

如果您正在尝试从函数返回错误值(例如“未找到键”或“键已存在”),那么您应该使用类似以下内容的东西。 "Left"传统上用于错误值,因为"Right"是正确的结果。在这里,错误单子与“Maybe”单子使用方式相同:当发生错误时,其余计算不会执行,而无需您将许多“if then else if then ....”链接在一起。在这种情况下,“异常”并不真正是异常;您的代码必须处理它或以某种方式将其传递到下一级。

另一方面,您可能还想捕获无法预料的异常,例如“head []”,其中您认为某些事情永远不会发生,但您错了。由于这些异常是不可预测的、可能是非确定性的,并且通常不适合类型系统,因此它们必须被视为IO事件进行处理。通常的模式是忽略这些异常,除了程序的最高级别之外,在那里您可以尝试保存用户的工作并显示有用的消息,例如“请报告此错误”。

抛出后一种异常很容易:只需调用“error”。但仅在您确信无法发生某些事情时使用它;它永远不应该是代码的正常部分。


同时,还要看一下《Real World Haskell》书中的错误处理章节。 - Abhinav Sarkar
吹毛求疵的是:如果你不将Left用于错误情况,那么你就无法使Either成为一个函子(functor)、单子(monad)等,这才是更重要的;这不仅仅涉及到Right的两个含义。 - Antal Spector-Zabusky
1
@Antal S-Z:就我个人而言,我认为这只是因为“左侧是邪恶的”(http://en.wiktionary.org/wiki/sinister#Etymology)。 - C. A. McCann

4
在Hackage上,monadLib包含一个异常单子(以及一个异常单子变换器ExceptionT),您可以在没有IO的情况下使用它。运行时,您将得到一个Either类型作为结果。

2

有点棘手!这是一种非常好的机制,可以在最后一刻使用(==)比较值,并且只有在必要的情况下才会进行比较。但是,为什么您至少没有使用类型信息对其进行注释呢?

data Tree a = E | T (Tree a) a (Tree a)

insert :: (Ord a) => a -> Tree a -> Tree a
insert x t = const t `either` id $ insert' x t Nothing
    where
    -- insert' (insert_this) (into_this_empty_tree) (except_if_it_equals_this) (because_then_the_tree_is_Left_unchanged)
    insert' :: (Ord a) => a -> Tree a -> Maybe a -> Either (Tree a) (Tree a)
    insert' x E Nothing = Right (T E x E)
    insert' x E (Just v) | x==v      = Left E
                         | otherwise = Right (T E x E)
    -- insert' (insert_this) (into_this_nonempty_tree) ((anyway)) (recursive:if_it_branches_to_the_left_insert_it_there)
    -- insert' (insert_this) (into_this_nonempty_tree) ((anyway)) (recursive:if_it_equals_or_branches_to_the_right_insert_it_there_except_if_the_right_branch_is_empty)
    insert' x t@(T l v r) _ | x<v       = (\l' -> T l' v r) `fmap` insert' x l Nothing
                            | otherwise = (\r' -> T l v r') `fmap` insert' x r (Just v)

为什么你要使用Either,如果你丢弃了Left的情况然后使用复制?如果不保留该复制以替换相等的树,而是根本不构建相等的树,那么效率会更高。就像这样...

insert' :: (Ord a) => a -> Tree a -> Maybe a -> Maybe (Tree a)

如果你想要真正高效,那么不要为了之后的比较而构建(或许是)一个参数。

--insert'1 :: (Ord a) => a -> Tree a -> Nothin -> Maybe (Tree a)
--insert'2 :: (Ord a) => a -> Tree a -> Just a -> Maybe (Tree a)
insert'1 :: (Ord a) => a -> Tree a -> Maybe (Tree a)
insert'2 :: (Ord a) => a -> Tree a -> a -> Maybe (Tree a)

解决方案如下所示:
insert :: (Ord a) => a -> Tree a -> Tree a
insert x t = fromMaybe t $ insert'1 x t
    where
    insert'1 :: (Ord a) => a -> Tree a -> Maybe (Tree a)
    insert'2 :: (Ord a) => a -> Tree a -> a -> Maybe (Tree a)
    insert'1 x E = Just (T E x E)
    insert'1 x (T l v r) | x<v       = do l' <- insert'1 x l
                                          Just (T l' v r)
                         | otherwise = do r' <- insert'2 x r
                                          Just (T l v r')
    insert'2 x E v = guard (x/=v) >> Just (T E x E)
    insert'2 x t _ = insert'1 x t

在Control.Monad.Error中定义了以下实例:

(EDIT:)

Error e => MonadError e (Either e)

那就意味着,(Either String) 可能是你正在寻找的内容。
insert :: (Ord a,MonadError String m) => a -> Tree a -> m (Tree a)
insert x t = maybe (throwError "Error: element already in tree") return $ insert'1 x t
    where ...

实际上,我从insert'1insert'2完全相反的方向进入了insert'。你绝对是正确的,我可以使用Maybe来表示insert'的结果。但问题是如何将Nothing替换为throw,并将maybe替换为catch - Daniel
1
如果它等于或者分支到右边,请将其插入那里,除非右侧分支为空。这是我见过的最长的标识符。 - fuz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接