(注意:这篇文章是一个 literate-haskell 文件。您可以将其复制粘贴到文本缓冲区中,将其另存为someFile.lhs
,然后使用 ghc 运行它。)
问题描述:我想创建一个具有相互引用的两种不同节点类型的图。下面的例子非常简化。两种数据类型
A
和B
在这里实际上是相同的,但是在原始程序中它们不同是有原因的。
我们会把无聊的东西扔掉。
> {-# 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
s 的输入列表引用了在相应地图中找不到的内容,这显然会失败。“罪魁祸首”是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的调用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'
,第二次尝试时,我至少可以进行第一次查找,然后停止。
我难住了。为了创建地图,我需要验证输入,但为了验证它,我需要创建地图!两个明显的答案是:间接和预验证。这两个都是实用的,如果有点不雅。我想知道是否可以在线进行错误捕获。
Haskell 的类型系统可以满足我的要求吗?fromJust
(可能是这样,我只是不知道如何。)通过将异常渗透到顶层,然后将它们捕获,显然是可能的IO
,但这不是我想要的方式。