让我们考虑一个包含许多构造函数的数据类型:
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
答案 0 :(得分:14)
另一种可能的方式:
sameK x y = f x == f y
where f (Alpha _) = 0
f (Beta _) = 1
f (Gamma _ _) = 2
-- runtime error when Delta value encountered
运行时错误并不理想,但比默默地给出错误答案要好。
答案 1 :(得分:10)
您需要使用像Scrap Your Boilerplate这样的泛型库或uniplate来执行此操作。
如果您不想如此苛刻,可以使用Dave Hinton的解决方案以及空记录快捷方式:
...
where f (Alpha {}) = 0
f (Beta {}) = 1
f (Gamma {}) = 2
所以你不必知道每个构造函数有多少个args。但它显然仍有一些不足之处。
答案 2 :(得分:10)
查看Data.Data模块,特别是toConstr
函数。与{-# LANGUAGE DeriveDataTypeable #-}
一起,它将为您提供一个单行解决方案,适用于Data.Data
实例的任何类型。你不需要弄清楚所有的SYB!
如果由于某种原因(坚持使用Hugs?),这不是一个选项,那么这是一个非常丑陋且非常缓慢的黑客攻击。它仅在您的数据类型为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 ......但这是关于它的唯一好处!
答案 3 :(得分:2)
在某些情况下,“Scrap Your Boilerplate”库会有所帮助。
答案 4 :(得分:1)
您绝对可以使用泛型来消除样板。您的代码是一个教科书示例,为什么我(以及其他许多永远不会在顶层使用_
通配符)。虽然写出所有案例都很繁琐,但与处理错误相比,这样做更乏味。
在这个快乐的例子中,我不仅会使用Dave Hinton的解决方案,而且还会在辅助函数f
上使用INLINE编译指示。