10

(注意:这篇文章是一个 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

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

所以我尝试Maybe在递归 do 绑定中使用 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需要bMap定义,bMap 需要aMap. 但是,在我开始访问任一映射中的键之前,需要对其进行全面评估,以便我们知道它是 aJust还是 a Nothing。我认为,对 in的调用maybemakeMap'这里让我很头疼。它包含一个隐藏的case表达式,因此是一个可反驳的模式。

这同样适用,Either因此在这里使用一些ErrorT变压器对我们没有帮助。

我不想退回到运行时异常,因为那会让我回到IOmonad,那就是承认失败。

对上述工作示例的最小修改是仅删除 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',第二次尝试时,我至少可以进行第一次查找,然后停止。

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

Haskell 的类型系统可以满足我的要求吗?fromJust(可能是这样,我只是不知道如何。)通过将异常渗透到顶层,然后将它们捕获,显然是可能的IO,但这不是我想要的方式。

4

1 回答 1

6

问题是,当你打结时,你并没有“构建”结构,AB只是声明它们应该如何构建,然后在需要时对它们进行评估。这自然意味着,如果验证是与评估“在线”完成的,那么必须进行错误检查,IO因为这是唯一可以触发评估的事情(在您的情况下,它是在您打印 的输出时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

这首先定义了相互递归的映射maMapmbMap它们分别具有 和 类型HashMap String (Maybe A)HashMap String (Maybe B)这意味着它们将包含所有节点键,但Nothing如果节点无效,则键与它们相关联。“作弊”发生在

c k . fromJust <$> M.lookup v l

这基本上只是查找引用的节点,M.lookup如果成功,我们只是假设返回的节点是有效的并使用fromJust. 如果我们尝试Maybe一直向下验证层,这可以防止无限循环。如果查找失败,则该节点无效,即Nothing

接下来,我们使用from将HashMap String (Maybe a)地图“由内而外”转换为地图。结果值仅当地图内的每个节点都存在时,否则。这保证了我们上面使用的不会失败。Maybe (HashMap String a)sequenceAData.TraversableJust _Just _NothingfromJust

于 2014-01-03T12:06:11.000 回答