【问题标题】:How to test Semigroup law for this data type?如何测试这种数据类型的半群定律?
【发布时间】:2019-07-12 04:39:05
【问题描述】:

我正在尝试解决与"Haskell Programming from First Principles" 第 15 章中的this other question 相同的练习。我已经创建了一个 Semigroup 实例,但在编写练习的 QuickCheck 部分时遇到了问题。

一个 Semigroup 实例应该满足:

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

其中&lt;&gt; 是半群映射。

我想出了以下几点:

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 实例吗?

【问题讨论】:

  • 最新版本的 QC 支持 functions,但您必须更改类型 - newtype Combine' fun a b = Combine (fun a b); type Combine = Combine' (-&gt;); type Combine_Test = Combine' Fun(或创建一个复制结构的不同类型,但将 -&gt; 替换为 Fun
  • 虽然在这个例子中他们希望你在整体上使用quickCheck,但使用 lambda 演算减少函数通常可以证明相等。

标签: haskell types typeclass quickcheck


【解决方案1】:

您无法决定两个函数是否相等。但您可以测试

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

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

请注意,“可判定相等”(==) :: X -&gt; X -&gt; Bool 类型中的 Bool 结果被替换为 Property,我们可以称之为“可测试相等”funEquality :: X -&gt; X -&gt; Property。实际上没有必要使用property并将函数a -&gt; Property(或者a -&gt; Bool,如果你使用(==))转换为Property,但是这样看起来类型更整洁。

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

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

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

编辑:此时我们实际上仍然缺少CombineShow 实例。 QuickCheck 提供了一个包装器Fun 来生成和显示函数作为反例。

main = quickCheck $ \(Fn f) (Fn g) (Fn h) ->
  (combineAssoc :: CombineAssoc Int Bool) (Combine f) (Combine g) (Combine h)

【讨论】:

  • 这是一个非常好的答案!但请看我的编辑。似乎作者打算为这种类型使用/制作一个 CoArbitrary 实例并用它来测试。但是怎么做呢?
  • 提示不是建议创建CoArbitrary (Combine a b),而是使用CoArbitrary a 实例来实现Arbitrary (Combine a b)。当使用 QuickCheck 库提供的 Fun 生成函数时,我隐式使用 CoArbitrary a(在参数类型上),提示是关于如何实现这样的函数生成器。
  • 您可以使用Test.QuickCheck.Checkers 中的=-= 运算符代替funEquality 函数,以相同的方式比较任何函数。
  • 认为 combineAssoc 缺少Semigroup b
【解决方案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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-01
    • 1970-01-01
    相关资源
    最近更新 更多