QuickCheck 属性的 DRY 类型注释
DRY type annotation for QuickCheck properties
使用 QuickCheck,可以编写参数化的多态属性,如下所示:
associativityLaw :: (Eq a, Show a, Semigroup a) => a -> a -> a -> Property
associativityLaw x y z = (x <> y) <> z === x <> (y <> z)
这只是一个例子,因为我的实际属性比较复杂,但它已经足够说明问题了。此 属性 验证对于类型 a
,<>
运算符是关联的。
想象一下,我想为不止一种类型练习这个 属性。我可以这样定义我的测试列表:
tests =
[
testGroup "Monoid laws" [
testProperty "Associativity law, [Int]" (associativityLaw :: [Int] -> [Int] -> [Int] -> Property),
testProperty "Associativity law, Sum Int" (associativityLaw :: Sum Int -> Sum Int -> Sum Int -> Property)
]
]
这行得通,但感觉过于冗长。我希望能够简单地说明对于给定的 属性,a
应该是 [Int]
,或者 a
应该是 Sum Int
.
像这样假设的语法:
testProperty "Associativity law, [Int]" (associativityLaw :: a = [Int]),
testProperty "Associativity law, Sum Int" (associativityLaw :: a = Sum Int)
有没有办法做到这一点,或许可以使用 GHC 语言扩展?
我的实际问题涉及更高级的类型,我希望能够说明这一点,例如f a
是 [Int]
,或者 f a
是 Maybe String
。
我知道 this answer,但是这两个选项(Proxy
和 Tagged
)至少如那里所述,似乎太尴尬而无法真正解决这个问题。
您可以使用 TypeApplications
绑定类型变量,如下所示:
{-# LANGUAGE TypeApplications #-}
associativityLaw @[Int]
如果你提到你有更高种类的类型并且你想绑定 f a
到 [Int]
,你必须绑定类型变量 f
和 a
分别为:
fmap @[] @Int
对于具有多个类型变量的函数,您可以按顺序应用参数:
f :: a -> b -> Int
-- bind both type vars
f @Int @String
-- bind just the first type var, and let GHC infer the second one
f @Int
-- bind just the second type var, and let GHC infer the first one
f @_ @String
有时类型变量的"order"可能不是很明显,但是你可以使用:type +v
并向GHCi询问更多信息:
λ> :t +v traverse
traverse
:: Traversable t =>
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> t a -> f (t b)
在标准haskell中,类型变量的"order"没有关系,所以GHC只是给你补了一个。但是在 TypeApplications
存在的情况下,顺序 确实 很重要:
map :: forall b a. (a -> b) -> ([a] -> [b])
-- is not the same as
map :: forall a b. (a -> b) -> ([a] -> [b])
出于这个原因,在使用高度参数化的代码时,或者您预计您的用户会想要在您的函数上使用 TypeApplications
,您可能需要显式设置类型变量的顺序,而不是让 GHC 为你定义一个订单, ExplicitForAll
:
{-# LANGUAGE ExplicitForAll #-}
map :: forall a b. (a -> b) -> ([a] -> [b])
感觉很像 java 或 c#
中的 <T1, T2>
使用 QuickCheck,可以编写参数化的多态属性,如下所示:
associativityLaw :: (Eq a, Show a, Semigroup a) => a -> a -> a -> Property
associativityLaw x y z = (x <> y) <> z === x <> (y <> z)
这只是一个例子,因为我的实际属性比较复杂,但它已经足够说明问题了。此 属性 验证对于类型 a
,<>
运算符是关联的。
想象一下,我想为不止一种类型练习这个 属性。我可以这样定义我的测试列表:
tests =
[
testGroup "Monoid laws" [
testProperty "Associativity law, [Int]" (associativityLaw :: [Int] -> [Int] -> [Int] -> Property),
testProperty "Associativity law, Sum Int" (associativityLaw :: Sum Int -> Sum Int -> Sum Int -> Property)
]
]
这行得通,但感觉过于冗长。我希望能够简单地说明对于给定的 属性,a
应该是 [Int]
,或者 a
应该是 Sum Int
.
像这样假设的语法:
testProperty "Associativity law, [Int]" (associativityLaw :: a = [Int]),
testProperty "Associativity law, Sum Int" (associativityLaw :: a = Sum Int)
有没有办法做到这一点,或许可以使用 GHC 语言扩展?
我的实际问题涉及更高级的类型,我希望能够说明这一点,例如f a
是 [Int]
,或者 f a
是 Maybe String
。
我知道 this answer,但是这两个选项(Proxy
和 Tagged
)至少如那里所述,似乎太尴尬而无法真正解决这个问题。
您可以使用 TypeApplications
绑定类型变量,如下所示:
{-# LANGUAGE TypeApplications #-}
associativityLaw @[Int]
如果你提到你有更高种类的类型并且你想绑定 f a
到 [Int]
,你必须绑定类型变量 f
和 a
分别为:
fmap @[] @Int
对于具有多个类型变量的函数,您可以按顺序应用参数:
f :: a -> b -> Int
-- bind both type vars
f @Int @String
-- bind just the first type var, and let GHC infer the second one
f @Int
-- bind just the second type var, and let GHC infer the first one
f @_ @String
有时类型变量的"order"可能不是很明显,但是你可以使用:type +v
并向GHCi询问更多信息:
λ> :t +v traverse
traverse
:: Traversable t =>
forall (f :: * -> *) a b.
Applicative f =>
(a -> f b) -> t a -> f (t b)
在标准haskell中,类型变量的"order"没有关系,所以GHC只是给你补了一个。但是在 TypeApplications
存在的情况下,顺序 确实 很重要:
map :: forall b a. (a -> b) -> ([a] -> [b])
-- is not the same as
map :: forall a b. (a -> b) -> ([a] -> [b])
出于这个原因,在使用高度参数化的代码时,或者您预计您的用户会想要在您的函数上使用 TypeApplications
,您可能需要显式设置类型变量的顺序,而不是让 GHC 为你定义一个订单, ExplicitForAll
:
{-# LANGUAGE ExplicitForAll #-}
map :: forall a b. (a -> b) -> ([a] -> [b])
感觉很像 java 或 c#
中的<T1, T2>