(注意:这篇文章是一个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
的引用输入列表显然会失败
在各自的地图中找不到的东西。 “罪魁祸首”是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
需要定义bMap
,bMap
需要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
中捕获它们,但这不是我想要的方式。
答案 0 :(得分:6)
问题在于,当您打结时,您不会“构建”A
和B
的结构,而只是声明它们应该如何构建,然后在需要。这自然意味着如果验证是通过评估“在线”完成的,那么错误检查必须在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
,它们分别具有mbMap
和HashMap 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.Traversable
且Just _
时,结果值才为Just _
。这可以保证我们上面使用的Nothing
不会失败。