【问题标题】:Pattern for generic unit test of type class instance implementations in HaskellHaskell 中类型类实例实现的通用单元测试模式
【发布时间】:2017-05-06 19:33:15
【问题描述】:

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

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,即使我尝试@987654327 @ (因此必须人为地传递给定类型的具体元素来调用函数),我无法弄清楚如何在代码中引用 consfoo(类型注释像 (cons 0) :: a 不会这样做) .

假设我有class (Eq a) => M a where ...,而类型AB 派生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)
   ]

但这一切对我来说都很丑陋。任何建议都热烈欢迎

【问题讨论】:

    标签: haskell hunit


    【解决方案1】:

    代理

    当前使函数多态为“内部”类型的最常用方法是传递ProxyProxy 有一个像() 这样的空元构造函数,但它的类型带有一个幻像类型。这避免了必须传递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
    

    作用域类型变量

    函数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
      ]
    

    【讨论】:

    • 这太好了,谢谢!!将仔细尝试所有这些建议,因为我希望从练习中学到很多东西。
    猜你喜欢
    • 2015-05-05
    • 2013-11-25
    • 2016-08-12
    • 1970-01-01
    • 2011-10-17
    • 2015-04-07
    • 2015-12-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多