代数类型数据构造函数的“模式匹配”

13

让我们考虑一个拥有多个构造函数的数据类型:

data T = Alpha Int | Beta Int | Gamma Int Int | Delta Int

我想编写一个函数来检查两个值是否由相同的构造函数产生:
sameK (Alpha _) (Alpha _) = True
sameK (Beta _) (Beta _) = True
sameK (Gamma _ _) (Gamma _ _) = True
sameK _ _ = False

维护sameK并不是很有趣,因为它很难被轻易地检查其正确性。例如,当向T添加新构造函数时,很容易忘记更新sameK。我故意省略了一行代码以作为例子。

-- it’s easy to forget:
-- sameK (Delta _) (Delta _) = True

问题是如何避免在sameK中使用样板代码?或者如何确保它检查所有T构造函数?
我找到的解决方法是为每个构造函数使用单独的数据类型,派生Data.Typeable,并声明一个公共类型类。但我不喜欢这个解决方案,因为它可读性较差,否则只需一个简单的代数类型即可适用于我:
{-# LANGUAGE DeriveDataTypeable #-}

import Data.Typeable

class Tlike t where
  value :: t -> t
  value = id

data Alpha = Alpha Int deriving Typeable
data Beta = Beta Int deriving Typeable
data Gamma = Gamma Int Int deriving Typeable
data Delta = Delta Int deriving Typeable

instance Tlike Alpha
instance Tlike Beta
instance Tlike Gamma
instance Tlike Delta

sameK :: (Tlike t, Typeable t, Tlike t', Typeable t') => t -> t' -> Bool
sameK a b = typeOf a == typeOf b

谢谢大家的回答。你们所有人的答案都很有用。 - sastanin
5个回答

15

另一种可能的方法:

sameK x y = f x == f y
  where f (Alpha _)   = 0
        f (Beta _)    = 1
        f (Gamma _ _) = 2
        -- runtime error when Delta value encountered

运行时错误并不理想,但比默默地给出错误答案要好。


3
我喜欢。使用-Wall的 GHC 友好地报告了在 f' 的定义中存在非穷尽的模式匹配,因此可以防止运行时错误。谢谢您的想法。 - sastanin

10
你需要使用像 Scrap Your Boilerplate 或 uniplate 这样的泛型库来进行通用操作。
如果你不想这么冗长,可以使用 Dave Hinton 的解决方案,结合空记录快捷方式:
...
where f (Alpha {}) = 0
      f (Beta {}) = 1
      f (Gamma {}) = 2

你不需要知道每个构造函数有多少参数,但显然仍有需要改进的地方。


2
花括号的优先级比其他任何东西都要高,因此从技术上讲,您不需要使用括号。 f Alpha {} = 0 可以正常工作,尽管我不确定它的可读性如何,它看起来有点像 f 接受两个参数。我有时会尝试使用 f Alpha{} = 0... - Tom Lokhorst

10
看看Data.Data模块,特别是toConstr()函数。结合DeriveDataTypeable语言扩展,这将为您提供适用于任何Data.Data实例的1行解决方案。您不需要弄清楚所有SYB!
如果由于某种原因(被Hugs粘住了?)这不是一个选项,那么这里有一个非常丑陋而且非常慢的hack。它只适用于可显示(Show)的数据类型(例如,通过使用deriving(Show)-这意味着没有函数类型内部,例如)。
constrT :: T -> String
constrT = head . words . show
sameK x y = constrT x == constrT y

constrT函数获取T值的最外层构造函数的字符串表示,并将其分解为单词并获取第一个单词。我提供了显式类型签名,以免您尝试在其他类型上使用它(并规避单态性限制)。

一些值得注意的缺点:

  • 当您的类型具有中缀构造函数(例如data T2 = Eta Int | T2 :^: T2)时,该函数会出现严重错误。
  • 如果您的一些构造函数具有共享前缀,则速度会变慢,因为需要比较更大的一部分字符串。
  • 无法在具有自定义show的类型上使用,例如许多库类型。

话虽如此,它确实是Haskell 98标准...但这可能是我可以说的唯一好话了!


2

我认为我可以从这些论文开始阅读:http://research.microsoft.com/en-us/um/people/simonpj/papers/hmap/ - sastanin

1

你绝对可以使用泛型来消除样板文件。你的代码是我(以及很多其他人)为什么从不在顶层使用下划线通配符的典型例子。虽然写出所有情况很繁琐,但比处理错误要简单得多。

在这个愉快的例子中,我不仅会使用 Dave Hinton 的解决方案,还会在辅助函数f上打上INLINE注释。


是的,这就是我为什么在问的原因。谢谢你的建议。 - sastanin

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