2

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

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返回 a的函数,该函数Test以某种方式指定tests代码适用的特定实例。我正在考虑tests使用默认实现添加类的定义(暂时忽略测试代码和实际代码之间的耦合问题),但我不能简单地拥有tests :: Test,即使我尝试tests:: a -> Test(因此必须人为地通过一个具体的给定类型的元素来调用函数),我不知道如何引用consfoo在代码内部(类型注释(cons 0) :: a不会这样做)。

假设我有class (Eq a) => M a where ...,使用 typesABderiving Eq,我可以用类似的东西欺骗编译器(添加到 的定义中M):

tests :: a -> Test
tests x = let 
            y = (cons 0)
            z = (x == y)       -- compiler now knows y :: a
          in
            TestCase (assertEqual "foo" (foo y)  "foo")

main = do
  runTestTT $ TestList
   [ tests (A 0)
   , tests (B 0)
   ]

但这对我来说非常难看。任何建议都热烈欢迎

4

1 回答 1

4

代理人

当前在“内部”类型中使函数多态的最常见方法是传递一个Proxy. Proxy有一个类似于 的空元构造函数(),但它的类型带有一个幻像类型。这避免了必须传递undefined或虚拟值。Data.Proxy.asProxyTypeOf然后可以用作注释。

tests :: M a => Proxy a -> Test
tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")

代理人

我们也可以概括该类型,因为Proxy实际上并不需要作为值。这只是一种使类型变量不含歧义的方法。你需要重新定义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

作用域类型变量

functionasProxyTypeOf或您的(==)技巧实际上是无法从签名中引用类型变量的产物。这实际上是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
  ]
于 2017-05-05T13:31:41.767 回答