Haskell 中 class 实例实现类型的通用单元测试模式
Pattern for generic unit test of type class instance implementations in Haskell
我想知道是否存在用于编写通用单元测试代码的已知模式,其目的是检查(作为黑盒)类型 class 的各种实例(实现)。例如:
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
添加到 class 的定义中(暂时忽略测试代码和实际代码之间的耦合问题),但我不能简单地使用 tests :: Test
,即使我尝试 tests:: a -> Test
(因此必须人为地传递给定类型的具体元素来调用函数),我也无法弄清楚如何在内部引用 cons
和 foo
代码(类型注释如 (cons 0) :: a
不会做)。
假设我有 class (Eq a) => M a where ...
,类型 A
和 B
派生 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)
]
但这一切对我来说都很丑陋。热烈欢迎任何建议
代理
当前最常见的使 "internal" 类型的函数多态的方法是传递 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
作用域类型变量
函数 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
]
我想知道是否存在用于编写通用单元测试代码的已知模式,其目的是检查(作为黑盒)类型 class 的各种实例(实现)。例如:
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
添加到 class 的定义中(暂时忽略测试代码和实际代码之间的耦合问题),但我不能简单地使用 tests :: Test
,即使我尝试 tests:: a -> Test
(因此必须人为地传递给定类型的具体元素来调用函数),我也无法弄清楚如何在内部引用 cons
和 foo
代码(类型注释如 (cons 0) :: a
不会做)。
假设我有 class (Eq a) => M a where ...
,类型 A
和 B
派生 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)
]
但这一切对我来说都很丑陋。热烈欢迎任何建议
代理
当前最常见的使 "internal" 类型的函数多态的方法是传递 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
作用域类型变量
函数 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
]