如何使用 Haskell 的类型系统来强制正确性,同时仍然能够进行模式匹配?
假设我有一个代表某种树结构的 adt:
data Tree = ANode (Maybe Tree) (Maybe Tree) AValType
| BNode (Maybe Tree) (Maybe Tree) BValType
| CNode (Maybe Tree) (Maybe Tree) CValType
据我所知,没有办法针对类型构造函数进行模式匹配(或者匹配函数本身没有类型?),但我仍然想使用编译时类型系统消除了返回或解析树节点的错误“类型”的可能性。例如,CNode 可能只能是 ANode 的父节点。我可能有
parseANode :: Parser (Maybe Tree)
一个 Parsec 解析函数,用作我的 CNode 解析器的一部分:
parseCNode :: Parser (Maybe Tree)
parseCNode = try (
string "<CNode>" >>
parseANode >>= \maybeanodel ->
parseANode >>= \maybeanoder ->
parseCValType >>= \cval ->
string "</CNode>"
return (Just (CNode maybeanodel maybeanoder cval))
) <|> return Nothing
根据类型系统,parseANode 最终可能返回 Maybe CNode、Maybe BNode 或 Maybe ANode,但我真的想确保它只返回一个 Maybe ANode。请注意,这不是我想要做的数据模式值或运行时检查 - 我实际上只是想检查我为特定树模式编写的解析器的有效性。 IOW,我并不是要检查解析数据的架构正确性,我真正想做的是检查我的解析器的架构正确性 - 我只是想确保我不这样做有一天,不要搞砸 parseANode 返回 ANode 值以外的东西。
我希望,如果我与绑定变量中的值构造函数进行匹配,类型推断就会明白我的意思:
parseCNode :: Parser (Maybe Tree)
parseCNode = try (
string "<CNode>" >>
parseANode >>= \(Maybe (ANode left right avall)) ->
parseANode >>= \(Maybe (ANode left right avalr)) ->
parseCValType >>= \cval ->
string "</CNode>"
return (Just (CNode (Maybe (ANode left right avall)) (Maybe (ANode left right avalr)) cval))
) <|> return Nothing
但这有很多问题,其中最重要的是 parseANode 不再可以自由返回没有什么。而且它无论如何都不起作用 - 看起来绑定变量被视为模式匹配,并且当 parseANode 返回 Nothing 或 Maybe BNode 或其他内容时,运行时会抱怨非详尽的模式匹配。
我可以沿着这些思路做一些事情:
data ANode = ANode (Maybe BNode) (Maybe BNode) AValType
data BNode = BNode (Maybe CNode) (Maybe CNode) BValType
data CNode = CNode (Maybe ANode) (Maybe ANode) CValType
但这有点糟糕,因为它假设约束应用于所有节点 - 我可能对此不感兴趣 - 事实上,它可能只是 CNode 只能作为 ANode 的父节点。所以我想我可以这样做:
data AnyNode = AnyANode ANode | AnyBNode BNode | AnyCNode CNode
data ANode = ANode (Maybe AnyNode) (Maybe AnyNode) AValType
data BNode = BNode (Maybe AnyNode) (Maybe AnyNode) BValType
data CNode = CNode (Maybe ANode) (Maybe ANode) CValType
但这使得与 *Node 进行模式匹配变得更加困难 - 事实上这是不可能的,因为它们只是完全不同的类型。我想我可以在任何想要模式匹配的地方创建一个类型类
class Node t where
matchingFunc :: t -> Bool
instance Node ANode where
matchingFunc (ANode left right val) = testA val
instance Node BNode where
matchingFunc (BNode left right val) = val == refBVal
instance Node CNode where
matchingFunc (CNode left right val) = doSomethingWithACValAndReturnABool val
,无论如何,这看起来有点混乱。谁能想到一种更简洁的方法来做到这一点?
Let's say that I have an adt representing some kind of tree structure:
data Tree = ANode (Maybe Tree) (Maybe Tree) AValType
| BNode (Maybe Tree) (Maybe Tree) BValType
| CNode (Maybe Tree) (Maybe Tree) CValType
As far as I know there's no way of pattern matching against type constructors (or the matching functions itself wouldn't have a type?) but I'd still like to use the compile-time type system to eliminate the possibility of returning or parsing the wrong 'type' of Tree node. For example, it might be that CNode's can only be parents to ANodes. I might have
parseANode :: Parser (Maybe Tree)
as a Parsec parsing function that get's used as part of my CNode parser:
parseCNode :: Parser (Maybe Tree)
parseCNode = try (
string "<CNode>" >>
parseANode >>= \maybeanodel ->
parseANode >>= \maybeanoder ->
parseCValType >>= \cval ->
string "</CNode>"
return (Just (CNode maybeanodel maybeanoder cval))
) <|> return Nothing
According to the type system, parseANode could end up returning a Maybe CNode, a Maybe BNode, or a Maybe ANode, but I really want to make sure that it only returns a Maybe ANode. Note that this isn't a schema-value of data or runtime-check that I want to do - I'm actually just trying to check the validity of the parser that I've written for a particular tree schema. IOW, I'm not trying to check parsed data for schema-correctness, what I'm really trying to do is check my parser for schema correctness - I'd just like to make sure that I don't botch-up parseANode someday to return something other than an ANode value.
I was hoping that maybe if I matched against the value constructor in the bind variable, that the type-inferencing would figure out what I meant:
parseCNode :: Parser (Maybe Tree)
parseCNode = try (
string "<CNode>" >>
parseANode >>= \(Maybe (ANode left right avall)) ->
parseANode >>= \(Maybe (ANode left right avalr)) ->
parseCValType >>= \cval ->
string "</CNode>"
return (Just (CNode (Maybe (ANode left right avall)) (Maybe (ANode left right avalr)) cval))
) <|> return Nothing
But this has a lot of problems, not the least of which that parseANode is no longer free to return Nothing. And it doesn't work anyways - it looks like that bind variable is treated as a pattern match and the runtime complains about non-exhaustive pattern matching when parseANode either returns Nothing or Maybe BNode or something.
I could do something along these lines:
data ANode = ANode (Maybe BNode) (Maybe BNode) AValType
data BNode = BNode (Maybe CNode) (Maybe CNode) BValType
data CNode = CNode (Maybe ANode) (Maybe ANode) CValType
but that kind of sucks because it assumes that the constraint is applied to all nodes - I might not be interested in doing that - indeed it might just be CNodes that can only be parenting ANodes. So I guess I could do this:
data AnyNode = AnyANode ANode | AnyBNode BNode | AnyCNode CNode
data ANode = ANode (Maybe AnyNode) (Maybe AnyNode) AValType
data BNode = BNode (Maybe AnyNode) (Maybe AnyNode) BValType
data CNode = CNode (Maybe ANode) (Maybe ANode) CValType
but then this makes it much harder to pattern-match against *Node's - in fact it's impossible because they're just completely distinct types. I could make a typeclass wherever I wanted to pattern-match I guess
class Node t where
matchingFunc :: t -> Bool
instance Node ANode where
matchingFunc (ANode left right val) = testA val
instance Node BNode where
matchingFunc (BNode left right val) = val == refBVal
instance Node CNode where
matchingFunc (CNode left right val) = doSomethingWithACValAndReturnABool val
At any rate, this just seems kind of messy. Can anyone think of a more succinct way of doing this?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我不明白你反对你的最终解决方案。您仍然可以针对
AnyNode
进行模式匹配,如下所示:它有点冗长,但我认为它具有您想要的工程属性。
I don't understand your objection to your final solution. You can still pattern match against
AnyNode
s, like this:It's a little more verbose, but I think it has the engineering properties you want.
这听起来像是 GADT 的用例。
现在,当您不关心节点类型时,可以使用
Tree t
,或者当您关心节点类型时,可以使用Tree ATag
。This sounds like a use case for GADTs.
Now you can use
Tree t
when you don't care about the node type, orTree ATag
when you do.基冈答案的延伸:对红/黑树的正确性属性进行编码是一个典型的例子。该线程的代码显示了 GADT 和嵌套数据类型解决方案: http:// /www.reddit.com/r/programming/comments/w1oz/how_are_gadts_useful_in_practical_programming/cw3i9
An extension of keegan's answer: encoding the correctness properties of red/black trees is sort of a canonical example. This thread has code showing both the GADT and nested data type solution: http://www.reddit.com/r/programming/comments/w1oz/how_are_gadts_useful_in_practical_programming/cw3i9