【问题标题】:How does Haskell do pattern matching without us defining an Eq on our data types?Haskell 如何在没有我们定义数据类型的 Eq 的情况下进行模式匹配?
【发布时间】:2011-01-17 21:26:49
【问题描述】:

我已经定义了一棵二叉树:

data Tree = Null | Node Tree Int Tree

并实现了一个函数,该函数将产生其所有节点的值的总和:

sumOfValues :: Tree -> Int
sumOfValues Null = 0
sumOfValues (Node Null v Null) = v
sumOfValues (Node Null v t2) = v + (sumOfValues t2)
sumOfValues (Node t1 v Null) = v + (sumOfValues t1)
sumOfValues (Node t1 v t2) = v + (sumOfValues t1) + (sumOfValues t2)

它按预期工作。我也想尝试使用守卫来实现它:

sumOfValues2 :: Tree -> Int
sumOfValues2 Null = 0
sumOfValues2 (Node t1 v t2)
    | t1 == Null && t2 == Null  = v
    | t1 == Null            = v + (sumOfValues2 t2)
    |               t2 == Null  = v + (sumOfValues2 t1)
    |   otherwise       = v + (sumOfValues2 t1) + (sumOfValues2 t2)

但是这个不起作用,因为我没有实现Eq,我相信:

No instance for (Eq Tree)
  arising from a use of `==' at zzz3.hs:13:3-12
Possible fix: add an instance declaration for (Eq Tree)
In the first argument of `(&&)', namely `t1 == Null'
In the expression: t1 == Null && t2 == Null
In a stmt of a pattern guard for
             the definition of `sumOfValues2':
        t1 == Null && t2 == Null

那么,必须提出的问题是,Haskell 如何在不知道传递的参数何时匹配的情况下进行模式匹配,而不求助于Eq

编辑

您的论点似乎围绕着这样一个事实,即 Haskell 并没有真正比较函数的参数,而是在“形式”和签名类型上知道要匹配哪个子函数。但是这个怎么样?

f :: Int -> Int -> Int
f 1 _ = 666
f 2 _ = 777
f _ 1 = 888
f _ _ = 999

在运行f 2 9时,是不是一定要用Eq才能知道哪个子函数是正确的?它们都是相等的(与我最初的 Tree 示例相反,当我们有 Tree/Node/Null 时)。或者Int 的实际定义类似于

data Int = -2^32 | -109212 ... | 0 | ... +2^32 

?

【问题讨论】:

  • 当您对数字进行模式匹配时,您使用的是Eq 类。数字文字不是构造函数。如果是,他们就不能拥有Num a => a 类型。请注意,Num 需要 Eq。如果您使用时髦的Eq 实例定义Num 实例,则模式匹配将相应地表现(即,如果(fromInteger 42 :: MyInt) == fromInteger 23 为真,则模式23 也将匹配值42 :: MyInt)。
  • 您知道,并非所有 32 位都可用于表示 Int。在我的机器上,maxBound :: Int 返回 2147483647, 2^31 - 1。
  • 是的,我知道。但是对于问题的上下文,我只是不在乎。
  • 顺便说一句,如果您需要 Eq 进行模式匹配,那么首先自己定义 Eq 实例会相当困难...... ;-)
  • @Wei Hu:实际上,他们有。想一想:maxBound = 2147483647 = 2^31 - 1,minBound = -2147483648 = 2^32,而不是加0,所以你得到:2^31 + 2^31-1 +1 = 2^32,你怎么看想要更多?

标签: haskell pattern-matching


【解决方案1】:

对于模式匹配,Haskell 使用值的结构和使用的构造函数。例如,如果您评估

sumOfValues (Node Null 5 (Node Null 10 Null))

它从上到下检查模式:

  • 第一个 Null 不匹配,因为它的结构不同

  • 第二个 (Node Null v Null) 不匹配,因为最后一个组件 Null 的结构与 (Node Null 10 Null) 不同(模式匹配递归进行)

  • 第三个匹配v绑定到5和t2绑定到(Node Null 10 Null)

Eq 和它定义的 == 运算符是不相关的机制;将Tree 设为Eq 的实例不会改变模式匹配的工作方式。

我觉得你这里的想法有点落后:模式匹配是 Haskell 中使用值的最基本方式;除了Int等一些基本类型,Eq是使用模式匹配实现的,而不是相反。

模式匹配和数字

事实证明,数字文字是一种特殊情况。根据 Haskell 报告 (link):

如果 v == k,则将数字、字符或字符串文字模式 k 与值 v 匹配成功em>,其中 == 是根据模式的类型重载的。

【讨论】:

  • 您的编辑错误。如果 0 等是 Int 类型的构造函数,它们的类型将是 Int,而不是 Num a => a。此外,数字上的模式匹配确实使用了相关数字类型的 Eq 实例。
  • @sepp2k:你说得对,数字文字的模式匹配很特别。我已经编辑了我的答案来纠正这个问题。
【解决方案2】:

Haskell 知道用于构造特定实例的类型构造函数,这就是成功进行模式匹配所需要的一切。

【讨论】:

    【解决方案3】:

    你缺少的是你假设Null 是一些常量值,就像在 C 或 Java 中一样。它不是——它是 Tree 类型的构造函数。

    模式匹配反向进行构造。不是你给构造函数提供两个值,它给你一个适当类型的值,你给构造函数提供一个类型的值,它给你构成类型的组成值。语言知道区分联合的哪个分支用于构造值,因此它可以匹配正确的模式。因此,不需要相等,因为它只是构造函数和变量——没有实际值被比较(甚至不必评估)。

    关于您对 Ints 的编辑:

    是的,Int 基本上是一种具有大量构造函数的类型,类似于-2 | -1 | 0 | 1 | 2 | 3 | 4 …。这有点笨拙,但在实践中有效。

    【讨论】:

    • 您的编辑错误。如果0 等是Int 类型的构造函数,它们将具有Int 类型,而不是Num a => a。此外,数字上的模式匹配确实使用相关数字类型的Eq 实例。
    • 我认为需要注意的一点是 Int 类可以这样实现,但重点是(据我所知)出于速度原因对所有数字都做了一些小技巧.
    • @Robert:那一点是错误的。 Int 没有以这种方式实现的原因与速度无关。原因是如果Int 是这样实现的,你就不能使用例如42 作为 IntegerDouble 类型的值,因此整个 Num 系统将失效。
    【解决方案4】:

    some 点,当您不想使用 Eq 时,您需要模式匹配。例如。你可以定义

    isEmpty :: Tree -> Bool
    isEmpty Null = True
    isEmpty _ = False
    
    sumOfValues2 :: Tree -> Int
    sumOfValues2 Null = 0
    sumOfValues2 (Node t1 v t2)
        | isEmpty t1 && isEmpty t2 = v
        | isEmpty t1 = v + (sumOfValues2 t2)
        | isEmpty t2 = v + (sumOfValues2 t1)
        | otherwise = v + (sumOfValues2 t1) + (sumOfValues2 t2)
    

    顺便说一句,你不需要所有这些情况,只是这样:

    sumOfValues :: Tree -> Int
    sumOfValues Null = 0
    sumOfValues (Node t1 v t2) = v + (sumOfValues t1) + (sumOfValues t2)
    

    【讨论】:

      【解决方案5】:

      模式匹配取决于语法。例如。如果你编写一个涉及构造函数的模式,你会得到构造函数的模式匹配。如果您编写一个涉及 litteral 表达式的模式(以及 litteral 浮点值或整数就是这样),您将使用您的类型可能提供的 (==) 运算符的任何重载定义获得相等测试(来自 Eq 类)。

      因此,如果您重载属于 Num 类并具有构造函数(也称为 Fred)的 Fred 数据类型,您可以通过检查给定的 Integer 是否为 1 来定义与 Integers 的相等性。现在在那种奇怪的情况下,除非我忽略了的东西,以下应该工作:

      getFred :: Fred -- get a Fred
      getFred = Fred -- invoke the Fred constructor
      
      testFred :: Fred -> Bool -- tests if Fred's equality operator considers 1 equal to Fred
      testFred 1 = True -- overloaded (==) tests against 1, so should be true
      testFred _ = False -- shouldn't happen, then...
      
      isFredOne = testFred $ getFred -- returns True
      

      【讨论】:

        【解决方案6】:

        您可以将数据构造函数视为标签。要对 Tree 的值进行模式匹配,编译后的代码会提取标签并知道要分派到哪个分支。它不关心真正的价值,所以你不需要Eq

        【讨论】:

          【解决方案7】:

          我通过说模式匹配是 Haskell 的核心——Eq 类型类不是。

          因此,虽然有些类似于模式匹配的功能需要 Eq,但它们不是模式匹配,可以在模式匹配之上实现。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多