通过良好类型的错误处理将相互递归的ADT联系起来

时间:2014-01-03 02:05:58

标签: haskell recursive-datastructures tying-the-knot

(注意:这篇文章是一个literate-haskell文件。你可以将它复制粘贴到文本中 缓冲区,保存为someFile.lhs,然后使用ghc运行它。)

问题描述:我想要创建一个包含两种不同节点类型的图形 互相参考。下面的示例是非常简化。这两种数据类型 AB在这里几乎完全相同,但它们是有原因的 与原计划不同。

我们会把枯燥的东西放在一边。

> {-# LANGUAGE RecursiveDo, UnicodeSyntax #-}
> 
> import qualified Data.HashMap.Lazy as M
> import Data.HashMap.Lazy (HashMap)
> import Control.Applicative ((<*>),(<$>),pure)
> import Data.Maybe (fromJust,catMaybes)

数据类型定义本身很简单:

> data A = A String B
> data B = B String A

为了象征两者之间的差异,我们会给他们一个不同的 Show实例。

> instance Show A where
>   show (A a (B b _)) = a ++ ":" ++ b
> 
> instance Show B where
>   show (B b (A a _)) = b ++ "-" ++ a

然后打结当然是微不足道的。

> knot ∷ (A,B)
> knot = let a = A "foo" b
>            b = B "bar" a
>        in (a,b)

这导致:

ghci> knot
(foo:bar,bar-foo)

这正是我想要的!

现在是棘手的部分。我想在运行时从用户创建此图 输入。这意味着我需要处理错误。让我们模拟一些(有效但是 无意义的用户输入:

> alist ∷ [(String,String)]
> alist = [("head","bot"),("tail","list")]
> 
> blist ∷ [(String,String)]
> blist = [("bot","tail"),("list","head")]

(用户当然不会直接输入这些列表;数据会先输入 按摩成这种形式)

在没有错误处理的情况下执行此操作是非常简单的:

> maps ∷ (HashMap String A, HashMap String B)
> maps = let aMap = M.fromList $ makeMap A bMap alist
>            bMap = M.fromList $ makeMap B aMap blist
>        in (aMap,bMap)
> 
> makeMap ∷ (String → b → a) → HashMap String b
>           → [(String,String)] → [(String,a)]
> makeMap _ _ [] = []
> makeMap c m ((a,b):xs) = (a,c a (fromJust $ M.lookup b m)):makeMap c m xs

String的引用输入列表显然会失败 在各自的地图中找不到的东西。 “罪魁祸首”是fromJust; 我们假设钥匙将在那里。现在,我可以确保 用户输入实际上是有效的,只需使用上面的版本。但这会 需要两次通过,不会很优雅,不是吗?

所以我尝试在递归do绑定中使用Maybe monad:

> makeMap' ∷ (String → b → a) → HashMap String b
>           → [(String,String)] → Maybe (HashMap String a)
> makeMap' c m = maybe Nothing (Just . M.fromList) . go id
>   where go l [] = Just (l [])
>         go l ((a,b):xs) = maybe Nothing (\b' → go (l . ((a, c a b'):)) xs) $
>                                 M.lookup b m
> 
> maps' ∷ Maybe (HashMap String A, HashMap String B)
> maps' = do rec aMap ← makeMap' A bMap alist
>                bMap ← makeMap' B aMap blist
>            return (aMap, bMap)

但这最终会无限循环:aMap需要定义bMapbMap 需要aMap。但是,在我甚至可以开始访问任一地图中的键之前, 它需要进行全面评估,以便我们知道它是Just还是a Nothing。我认为,maybe中对makeMap'的调用是咬我的原因。它 包含一个隐藏的case表达式,因此是一个可反射的模式。

Either也是如此,因此使用某些ErrorT变换器不会 在这里帮助我们。

我不想回到运行时异常,因为这会让我反弹 到IO monad,这将是承认失败。

对上述工作示例的最小修改只是删除 fromJust,只接受实际效果的结果。

> maps'' ∷ (HashMap String A, HashMap String B)
> maps'' = let aMap = M.fromList . catMaybes $ makeMap'' A bMap alist
>              bMap = M.fromList . catMaybes $ makeMap'' B aMap blist
>          in (aMap, bMap)
> 
> makeMap'' ∷ (String → b → a) → HashMap String b → [(String,String)] → [Maybe (String,a)]
> makeMap'' _ _ [] = []
> makeMap'' c m ((a,b):xs) = ((,) <$> pure a <*> (c <$> pure a <*> M.lookup b m))
>                            :makeMap'' c m xs

这也不起作用,并且好奇地导致行为略有不同!

ghci> maps' -- no output
^CInterrupted.
ghci> maps'' -- actually finds out it wants to build a map, then stops.
(fromList ^CInterrupted

使用调试器显示这些甚至不是无限循环(正如我预期的那样)但执行只是停止。使用maps'我得到 nothing ,第二次尝试时,我至少会进行第一次查找,然后停止。

我很难过。为了创建地图,我需要验证输入,但为了验证它,我需要创建地图!两个明显的答案是:间接和预验证。这些都是实用的,如果有点不优雅的话。我想知道是否可以在线进行错误捕获。

我问Haskell的类型系统是否可行? (它 可能是,我只是不知道如何。)显然有可能 在fromJust处将异常渗透到顶层,然后在IO中捕获它们,但这不是我想要的方式。

1 个答案:

答案 0 :(得分:6)

问题在于,当您打结时,您不会“构建”AB的结构,而只是声明它们应该如何构建,然后在需要。这自然意味着如果验证是通过评估“在线”完成的,那么错误检查必须在IO中进行,因为这是唯一可以触发评估的事情(在您的情况下,当您打印{的输出时{1}})。

现在,如果您想要更早地检测错误,则必须声明结构,以便我们可以验证每个节点,而无需遍历整个无限循环结构。这个解决方案在语义上与预验证输入相同,但希望你会发现它在语法上更优雅

show

这首先定义了相互递归的地图import Data.Traversable (sequenceA) maps' :: Maybe (HashMap String A, HashMap String B) maps' = let maMap = M.fromList $ map (makePair A mbMap) alist mbMap = M.fromList $ map (makePair B maMap) blist makePair c l (k,v) = (k, c k . fromJust <$> M.lookup v l) in (,) <$> sequenceA maMap <*> sequenceA mbMap maMap,它们分别具有mbMapHashMap String (Maybe A)类型,这意味着它们将包含所有节点键,但是如果节点无效,则密钥与HashMap String (Maybe B)关联。 “作弊”发生在

Nothing

这实际上只是用c k . fromJust <$> M.lookup v l 查找引用的节点,如果成功,我们只假设返回的节点有效并使用M.lookup。这可以防止在我们尝试一直向下验证fromJust层时发生的无限循环。如果查找失败,则此节点无效,即Maybe

接下来,我们使用Nothing中的HashMap String (Maybe a)Maybe (HashMap String a)地图“内部”转换为sequenceA地图。仅当地图中的每个节点均为Data.TraversableJust _时,结果值才为Just _。这可以保证我们上面使用的Nothing不会失败。