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 aMaybe String

我知道 this answer,但是这两个选项(ProxyTagged)至少如那里所述,似乎太尴尬而无法真正解决这个问题。

您可以使用 TypeApplications 绑定类型变量,如下所示:

{-# LANGUAGE TypeApplications #-}

associativityLaw @[Int]

如果你提到你有更高种类的类型并且你想绑定 f a[Int],你必须绑定类型变量 fa分别为:

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>