如何测试此数据类型的Semigroup定律?

时间:2016-12-27 18:18:29

标签: haskell types typeclass quickcheck

我试图在this other question的第15章中解决与"Haskell Programming from First Principles"相同的练习。我已经制作了一个Semigroup实例,而且我在编写QuickCheck部分练习时遇到了麻烦。

Semigroup实例应满足:

a <> (b <> c) == (a <> b) <> c

其中<>是Semigroup mappend。

我想出了以下内容:

import Data.Semigroup
import Test.QuickCheck

semigroupAssoc :: (Eq m, Semigroup m) => m -> m -> m -> Bool
semigroupAssoc a b c = (a <> (b <> c)) == ((a <> b) <> c)

newtype Combine a b = Combine { unCombine :: (a -> b) }

instance Semigroup b => Semigroup (Combine a b) where
  (Combine f) <> (Combine g) = Combine (\x -> (f x) <> (g x))

instance CoArbitrary (Combine a b) where
  coarbitrary (Combine f) = variant 0

instance (CoArbitrary a, Arbitrary b) => Arbitrary (Combine a b) where
  arbitrary = do
    f <- arbitrary
    return $ Combine f

type CombineAssoc a b = Combine a b -> Combine a b -> Combine a b -> Bool

main :: IO ()
main = do
  quickCheck (semigroupAssoc :: CombineAssoc Int Bool)

所有内容都会编译,但quickCheck行除外,它会抱怨No instance for (Eq (Combine Int Bool)) arising from a use of ‘semigroupAssoc’

我认为没有办法测试两个任意函数是否相等(由Combine包裹的函数),但是练习文本表明这样的事情是可能的。

关于如何使这项工作的任何想法?

编辑:

作者给出了这个练习的暗示:

  

提示:此函数最终将应用于单个值   类型a。但是你将拥有多个可以产生的功能   b型的值。我们如何组合多个值,以便我们拥有   一个b?这个可能会很棘手!记得那个   Combine中的值的类型是函数的值。如果你   无法弄清楚CoArbitrary,不用担心QuickChecking   这个。

@ Li-yao Xia的答案似乎是最好的答案。但我不应该使用这个CoArbitrary实例吗?

2 个答案:

答案 0 :(得分:8)

你不能决定两个函数是否相等。但你可以测试它!

当且仅当对于任何输入它们给出相同的输出时,两个函数是相等的。这是一个可测试的属性:生成一些输入,比较输出。如果他们不同,你就会得到一个反例。

-- Test.QuickCheck.(===) requires (Eq b, Show b)
-- but you can use (==) if you prefer.
funEquality :: (Arbitrary a, Show a, Eq b, Show b) => Combine a b -> Combine a b -> Property
funEquality (Combine f) (Combine g) =
  property $ \a -> f a === g a

请注意,Bool会导致&#34;可判定的等式&#34; (==) :: X -> X -> BoolProperty替换为我们可能称之为&#34;可测试的平等&#34; funEquality :: X -> X -> Property。实际上没有必要使用property并将函数a -> Property(或a -> Bool如果使用(==))转换为Property,但类型看起来更整洁。

我们需要重写与associativity属性相对应的函数,因为我们不再依赖Eq

type CombineAssoc a b = Combine a b -> Combine a b -> Combine a b -> Property

combineAssoc :: (Arbitrary a, Show a, Eq b, Show b) => CombineAssoc a b
combineAssoc f g h = ((f <> g) <> h) `funEquality` (f <> (g <> h))

修改:此时我们实际上仍然缺少Show的{​​{1}}个实例。 QuickCheck提供了一个包装器Combine来生成和显示函数作为反例。

Fun

答案 1 :(得分:2)

确实不可能或至少不可行,但是你真的不需要像Int这样大的参数类型的测试用例!

对于较小的类型,例如Int16,您可以详尽地尝试所有可能的参数来确定平等。 universe package有一个方便的类:

import Data.Universe

instance (Universe a, Eq b) => Eq (Combine a b) where
  Combine f == Combine g = all (\x -> f x == g x) universe

然后你的原始支票会起作用,虽然速度慢得令人无法接受;我建议将其更改为quickCheck (semigroupAssoc :: CombineAssoc Int16 Bool)