Haskell 在具有良好类型错误处理的相互递归ADT上打结

Haskell 在具有良好类型错误处理的相互递归ADT上打结,haskell,recursive-datastructures,tying-the-knot,Haskell,Recursive Datastructures,Tying The Knot,(注意:这篇文章是一个可读的haskell文件。你可以复制粘贴到文本中。) 缓冲区,将其另存为someFile.lhs,然后使用ghc运行。) 问题描述:我想创建一个包含两种不同节点类型的图 相互参照。下面的例子非常简单。这两种数据类型 A和B在这里实际上是相同的,但这是有原因的 与原程序不同 我们会把那些无聊的东西弄走的 > {-# LANGUAGE RecursiveDo, UnicodeSyntax #-} > > import qualified Data.HashM

(注意:这篇文章是一个可读的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)
为了象征两者之间的差异,我们将给它们一个不同的名称
显示
实例

> 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的输入列表引用,这显然会失败 在相应的地图中找不到的东西。“罪魁祸首”是来自Just的
;
我们只是假设钥匙会在那里。现在,我可以确保
用户输入实际上是有效的,只需使用上面的版本。但这会
需要两次通行证,不会很优雅吧

因此,我尝试在递归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
。然而,在我开始访问任一地图中的键之前, 需要对其进行全面评估,以便我们知道它是
只是
还是
什么都没有
。我想,在
makeMap'
中调用
makeMap'
可能是什么让我感到痛苦。信息技术 包含一个隐藏的
大小写
表达式,因此是一个可反驳的模式

同样的情况也适用于
,因此使用一些
error
转换器将不起作用 帮帮我们

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

对上述工作示例的最小修改就是删除
从just
,只获取实际有效的结果

> 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
使用调试器表明,这些循环甚至不是无限循环(正如我预期的那样),但执行只是停止。使用
maps'
我一无所获,第二次尝试时,我至少可以进行第一次查找,然后暂停

我被难住了。为了创建地图,我需要验证输入,但是为了验证输入,我需要创建地图!两个显而易见的答案是:间接验证和预验证。这两种方法都是实用的,尽管有点不雅观。我想知道是否可以在线捕获错误

我想问的是Haskell的类型系统是否可行?(It) 很可能是的,我只是不知道怎么做。)显然,这是有可能的
将异常从just
渗透到顶层,然后在
IO
中捕获它们,但我不想这样做。

问题是,当你结为一体时,你不会“构建”
A
B
的结构,而只是声明应该如何构建它们,然后在需要时对它们进行评估。这自然意味着,如果验证与求值“一致”完成,那么错误检查必须在
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
这基本上只需使用
M.lookup
查找引用的节点,如果成功,我们只需假设返回的节点有效,并使用
fromJust
。这防止了无限循环,否则,如果我们一直尝试验证
可能
层,就会发生无限循环。如果查找失败,则此节点无效,即


接下来,我们使用
sequenceA
Data.Traversable中的
sequenceA
HashMap字符串(可能a)
映射为
maps“inside-out”(可能a)
。仅当映射中的每个节点都是
Just
时,结果值才是
Just
,否则为
Nothing
。这保证了我们上面使用的
fromJust
不会失败。

我真的很喜欢只做一个快速预处理检查的想法,并且有一种感觉,它最终会比您正在考虑的错误处理方法简单得多。此外,是否确实要使用打结表示法表示图形?如果映射到该结构,则该结构可能会解体,并且在遍历过程中很难检查节点的标识或避免循环。直接使用您所做的邻接列表或使用可变引用可能是可供选择的解决方案。我在<代码> Read Read映射(写入器(Error AsCOCS))中做了类似的事情:< /Cord>单元格。棘手的一点是,当你告诉作者一个关联(或错误)时,你必须无条件地告诉作者(即,告诉一个thunk,稍后将决定它是错误还是关联)。然后,当你结为连理时,运行writer并在此时强制日志,同时构建将反馈给读者的地图。整个东西原来很脆弱,所以当它是一个
ghci> maps' -- no output
^CInterrupted.
ghci> maps'' -- actually finds out it wants to build a map, then stops.
(fromList ^CInterrupted
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
c k . fromJust <$> M.lookup v l