Haskell中类型类实例实现的通用单元测试模式

Haskell中类型类实例实现的通用单元测试模式,haskell,hunit,Haskell,Hunit,我想知道是否有一种编写通用单元测试代码的已知模式,其目的是检查(作为黑盒)类型类的各种实例(实现)。例如: import Test.HUnit class M a where foo :: a -> String cons :: Int -> a -- some constructor data A = A Int data B = B Int instance M A where foo _ = "foo" cons = A instance M B wh

我想知道是否有一种编写通用单元测试代码的已知模式,其目的是检查(作为黑盒)类型类的各种实例(实现)。例如:

import Test.HUnit

class M a where
foo :: a -> String
cons :: Int -> a     -- some constructor

data A = A Int
data B = B Int

instance M A where
  foo _ = "foo"
  cons  = A

instance M B where
  foo _ = "bar"     -- implementation error
  cons  = B 
我想编写一个函数
tests
返回一个
Test
,用某种方式指定
tests
代码所应用的特定实例。我在考虑使用默认实现将
tests
添加到类的定义中(暂时忽略测试代码和实际代码之间的耦合问题),但我不能简单地使用
tests::Test
,即使我尝试
tests::a->Test
(因此必须人工传递给定类型的具体元素来调用函数),我不知道如何在代码中引用
cons
foo
(类型注释如
(cons 0)::a
不行)

假设我有
class(Eq a)=>ma,其中…
相反,用类型
a
B
派生
Eq
,我可以用类似的东西欺骗编译器(添加到
M
的定义中):

但这对我来说太难看了。任何建议都欢迎 当前使函数在“内部”类型中多态的最常见方法是传递一个
代理
代理
代理
具有一个空构造函数,如
()
,但其类型带有幻影类型。这避免了必须传递未定义的
或伪值。
Data.Proxy.asProxyTypeOf
然后可以用作注释

tests :: M a => Proxy a -> Test
tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")
代理 我们还可以概括该类型,因为实际上不需要将代理作为值。这只是使类型变量不含糊的一种方法。不过,您需要重新定义
asProxyTypeOf
。与前一种类型相比,这主要是样式问题。能够使用更多值作为潜在代理可以生成一些代码更简洁,有时以可读性为代价

-- proxy is a type variable of kind * -> *
tests :: M a => proxy a -> Test
tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")
  where
    asProxyTypeOf :: a -> proxy a -> a
    asProxyTypeOf = const
作用域类型变量 函数
asProxyTypeOf
,或者你的
(==)
技巧实际上是无法从签名中引用类型变量的结果。这实际上是
ScopedTypeVariables
+
RankNTypes
扩展所允许的

显式量化将变量
a
带入函数体的范围

tests :: forall a proxy. M a => proxy a -> Test
tests _ = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.
如果没有
ScopedTypeVariables
扩展名,
cons 0::a
将被解释为
cons 0::forall a.a

以下是如何使用这些功能:

main = runTestTT $ TestList
  [ tests (Proxy :: Proxy A)
  , tests (Proxy :: Proxy B)
  ]
类型应用程序 自GHC 8以来,
AllowAmbiguousTypes
+
TypeApplications
扩展使得
Proxy
参数变得不必要

tests :: forall a. M a => Test
tests = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.

main = runTestTT $ TestList
  [ tests @A
  , tests @B
  ]

这太棒了,谢谢!!我会仔细尝试所有这些建议,因为我希望从练习中学到很多。
tests :: forall a. M a => Test
tests = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.

main = runTestTT $ TestList
  [ tests @A
  , tests @B
  ]