如何使用 Haskell 的类型系统来强制正确性,同时仍然能够进行模式匹配?

发布于 2024-09-26 17:34:11 字数 2902 浏览 10 评论 0原文

假设我有一个代表某种树结构的 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

第几種人 2024-10-03 17:34:11

我不明白你反对你的最终解决方案。您仍然可以针对 AnyNode 进行模式匹配,如下所示:

f (AnyANode (ANode x y z)) = ...

它有点冗长,但我认为它具有您想要的工程属性。

I don't understand your objection to your final solution. You can still pattern match against AnyNodes, like this:

f (AnyANode (ANode x y z)) = ...

It's a little more verbose, but I think it has the engineering properties you want.

世俗缘 2024-10-03 17:34:11

我仍然想使用编译时类型系统来消除返回或解析错误的树节点“类型”的可能性

这听起来像是 GADT 的用例。

{-# LANGUAGE GADTs, EmptyDataDecls #-}
data ATag
data BTag
data CTag

data Tree t where
  ANode :: Maybe (Tree t) -> Maybe (Tree t) -> AValType -> Tree ATag
  BNode :: Maybe (Tree t) -> Maybe (Tree t) -> BValType -> Tree BTag
  CNode :: Maybe (Tree t) -> Maybe (Tree t) -> CValType -> Tree CTag

现在,当您不关心节点类型时,可以使用 Tree t ,或者当您关心节点类型时,可以使用 Tree ATag

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

This sounds like a use case for GADTs.

{-# LANGUAGE GADTs, EmptyDataDecls #-}
data ATag
data BTag
data CTag

data Tree t where
  ANode :: Maybe (Tree t) -> Maybe (Tree t) -> AValType -> Tree ATag
  BNode :: Maybe (Tree t) -> Maybe (Tree t) -> BValType -> Tree BTag
  CNode :: Maybe (Tree t) -> Maybe (Tree t) -> CValType -> Tree CTag

Now you can use Tree t when you don't care about the node type, or Tree ATag when you do.

海风掠过北极光 2024-10-03 17:34:11

基冈答案的延伸:对红/黑树的正确性属性进行编码是一个典型的例子。该线程的代码显示了 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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文