在Haskell函数中,是否可能对模式匹配进行调试?

8

我定义了一个类型

data Expr = 
    Const Double
    | Add Expr Expr
    | Sub Expr Expr

并将其声明为 Eq 类型类的一个实例:

instance Eq Expr where
    (Add (Const a1) (Const a2)) == Const b = a1+a2 == b
    (Add (Const a1) (Const a2)) == (Add (Const b1) (Const b2)) = a1+a2 == b1 + b2

当然,表达式Sub (Const 1) (Const 1) == Const 0的评估将失败。我如何在运行时调试模式匹配过程以发现它失败了?我想看看Haskell如何获取==的参数并遍历这些模式。这是否可能?

4
如果你使用-Wall进行编译,GHC可以在编译时检测到问题,它会警告你存在不完整的模式并显示你错过了哪些情况。请注意,我已尽力让翻译通俗易懂,但并未改变原意。 - hammar
6
精确的选项是-fwarn-incomplete-patterns。请查看https://dev59.com/-Wsz5IYBdhLWcg3weHgA以了解其工作原理。顺便说一句,在您的示例中,我建议编写`eval :: Expr -> Double,然后编写x == y = eval x == eval y`,但这仍然相当不标准。 - sdcvvc
3
如果没有其他信息,我会建议您到 StackOverflow 上发表这个问题 :) - rampion
1
Haskell旨在在编译时捕获错误,这就是为什么它不提供运行时调试工具的原因。虽然我并不是说你的问题是“错误”的,但学习使用编译时可用的工具是值得的,因为它们显著更强大。此外,您应该认真考虑打开-fwarn-incomplete-patterns并将其视为错误。强制自己编写一个完整的函数将帮助您了解缺少哪些模式匹配。 - Gabriella Gonzalez
你可以做的一件事就是为你想要进行模式匹配的数据类型创建一个 Show 实例,以查看它实际上是由哪些构造函数构建的。有时,如果某个值没有匹配到你认为应该匹配的模式,那么可能是因为它实际上不是你认为的那个值。 - Gabriella Gonzalez
显示剩余3条评论
3个回答

3

编辑:给出真实回答...

我发现查看匹配模式的最简单方法是添加trace语句,例如:

import Debug.Trace

instance Eq Expr where
    (Add (Const a1) (Const a2)) == Const b = trace "Expr Eq pat 1" $ a1+a2 == b
    (Add (Const a1) (Const a2)) == (Add (Const b1) (Const b2)) = trace "Expr Eq pat 2" $ a1+a2 == b1 + b2
    -- catch any unmatched patterns
    l == r = error $ "Expr Eq failed pattern match. \n l: " ++ show l ++ "\n r: " ++ show r

如果您不包含最终语句来捕获任何未匹配的模式,您将得到一个运行时异常,但我发现更有用的是看到您正在获取的数据。然后通常很容易看出为什么它不匹配以前的模式。
当然,您不希望在生产代码中留下这个。 我只在必要时插入跟踪,完成后再将其删除。 您还可以使用CPP将它们排除在生产构建之外。
我还想说,我认为模式匹配是错误的方法。 模式数量会呈指数级增长,很快就变得难以管理。 如果您想为Expr创建Float实例,例如,您将需要几个更多的原始构造函数。
相反,您可能有一个解释器函数interpret :: Expr -> Double,或者至少可以编写一个。 然后您可以定义
instance Eq Expr where
  l == r = interpret l == interpret r

通过模式匹配,您实质上是在 Eq 实例中重新编写解释函数。如果您想创建一个 Ord 实例,那么您将不得不再次重新编写解释函数。


跟踪是正确的方法。将其评估为双精度浮点数不起作用,因为 Expr 的某些实例可以是函数。 - quant_dev

2
如果您想了解如何进行匹配失败的示例,可以看看QuickCheck。该手册中有一个示例(测试数据的大小)关于生成和测试递归数据类型,似乎非常适合您的需求。
虽然-Wall标志会给出未匹配的模式列表,但运行QuickCheck会给出导致给定命题失败的输入数据示例。 例如,如果我为您的Expr编写一个生成器,并向quickCheck输入一个命题prop_expr_eq :: Expr -> Bool,检查Expr是否等于自身,那么我很快就会得到Const 0.0作为第一个不匹配的输入示例。
import Test.QuickCheck
import Control.Monad

data Expr = 
    Const Double
    | Add Expr Expr
    | Sub Expr Expr
    deriving (Show)

instance Eq Expr where
    (Add (Const a1) (Const a2)) == Const b = a1+a2 == b
    (Add (Const a1) (Const a2)) == (Add (Const b1) (Const b2)) = a1+a2 == b1 + b2

instance Arbitrary Expr where
    arbitrary = sized expr'
      where
        expr' 0 = liftM Const arbitrary
        expr' n | n > 0 = 
            let subexpr = expr' (n `div` 2)
            in oneof [liftM Const arbitrary,
                      liftM2 Add subexpr subexpr,
                      liftM2 Sub subexpr subexpr]

prop_expr_eq :: Expr -> Bool
prop_expr_eq e = e == e

正如您所见,运行测试会给您提供一个反例来证明您的相等性测试是错误的。我知道这可能有点过度杀伤力,但是如果您编写得好的话,优势在于您还可以获得针对代码的单元测试,这些测试查看任意属性,而不仅仅是模式匹配的穷举。

*Main> quickCheck prop_expr_eq
*** Failed! Exception: 'test.hs:(11,5)-(12,81): Non-exhaustive patterns in function ==' (after 1 test):  
Const 0.0

PS: 《Real World Haskell》是一本免费书籍,其中有关于使用QuickCheck进行单元测试的优秀阅读材料。


1
你可以把复杂的模式分解成简单的模式,并使用trace来查看发生了什么。就像这样:
instance Eq Expr where
    x1 == x2 | trace ("Top level: " ++ show (x, y1)) True,
               Add x11 x12 <- x1,
               trace ("First argument Add: " ++ show (x11, x12)) True,
               Const a1 <- x11,
               trace ("Matched first Const: " ++ show a1) True,
               Const a2 <- x12,
               trace ("Matched second Const: " ++ show a2) True,
               Const b <- x2
               trace ("Second argument Const: " ++ show b) True
             = a1+a2 == b

有点绝望,但绝望的时候需要采取绝望的措施。 :) 随着你逐渐熟悉 Haskell,你几乎永远不需要做这件事。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接