为什么收缩树看起来像使用过滤器时的样子

时间:2019-01-29 00:11:19

标签: haskell quickcheck property-based-testing haskell-hedgehog

我试图了解使用hedgehog 集成收缩时,过滤器对发生器的收缩树有什么作用。

考虑以下功能:

{-# LANGUAGE OverloadedStrings #-}

import Hedgehog
import qualified Hedgehog.Gen as Gen

aFilteredchar:: Gen Char
aFilteredchar =
  Gen.filter (`elem` ("x" :: String)) (Gen.element "yx")

打印收缩树时:

>>>  Gen.printTree aFilteredchar

我会得到收缩树,如下所示:

'x'
 └╼'x'
    └╼'x'
       └╼'x'
               ...

                   └╼<discard>

这是一棵很深的树,只包含x,最后是discard

为什么收缩函数不断返回x,而不是返回一个空列表,这表明不可能进一步收缩?

2 个答案:

答案 0 :(得分:2)

Gen本质上是概率单子和树单子的组合,您观察到的行为主要来自树单子和Gen.filter的定义。

Gen.filter p g是一个简单的单子循环try 0,其中:

-- simplified body of filter
try k =
  if k > 100 then
    discard  -- empty tree
  else do
    x <- g
    if p x then
      pure x  -- singleton tree
    else
      try (k + 1)  -- keep looping

因此,要了解您拥有的树,您必须了解此处do表示法下的树monad。

树单子

Gen内部使用的Tree type in hedgehog看起来像这样(如果您正在看刺猬的链接实现,请设置m ~ Maybe):

data Tree a = Empty | Node a [Tree a]  -- node label and children

还有许多其他类似Tree的类型是monad,而monadic绑定(>>=)通常采用树替换的形式。

假设您有一棵树t = Node x [t1, t2, ...] :: Tree a和一个延续/替换k :: a -> Tree b,它用树x :: a替换了每个节点/变量k x :: Tree b。我们可以分两步描述t >>= k,然后依次描述fmapjoin。首先,fmap在每个节点标签上应用替换。因此,我们获得一棵树,其中每个节点都由另一棵树标记。具体来说,请说k x = Node y [u1, u2, ...]

fmap k t
=
Node
  (k x)                        -- node label
  [fmap k t1, fmap k t2, ...]  -- node children
=
Node
  (Node y [u1, u2, ...])       -- node label
  [fmap k t1, fmap k t2, ...]  -- node children

然后,join步骤将嵌套的树结构展平,将标签内部的子级与外部的子级连接起来:

t >>= k
=
join (fmap k t)
=
Node
  y
  ([join (fmap k t1), join (fmap k t2), ...] ++ [u1, u2, ...])

要完成Monad实例,请注意,我们有pure x = Node x []

try循环

现在我们对树monad有一些直觉,我们可以转向您的特定生成器。我们要评估上面的try k,其中p = (== 'x')g = elements "yx"。我在这里挥舞着双手,但是您应该想象g随机地对树Node 'y' [](无收缩地生成'y')求值。 pure 'y'Node 'x' [Node 'y' []](生成'x'并缩小到'y';实际上,“ elements缩小到左侧”),并且每次发生{{ 1}}与其他人无关,因此重试时会得到不同的结果。

让我们分别检查每个案例。如果g会怎样?假设g = pure 'y',那么我们位于顶级k <= 100的{​​{1}}分支中,下面已经对其进行了简化:

else

因此,if评估为-- simplified body of filter try k = do c <- pure 'y' -- g = pure 'y' if c == 'x' then -- p c = (c == 'x') pure c else try (k + 1) -- since (do c <- pure 'y' ; s c) = s 'y' (monad law) and ('y' == 'x') = False try k = try (k + 1) 的所有时间最终都简化为递归项g,剩下的情况是pure 'y'评估为另一棵树try (k + 1)

g

如上一节中所述,单价绑定等同于以下内容,我们以一些方程式推理结束。

Node 'x' [Node 'y' []]

总而言之,从try k = do c <- Node 'x' [Node 'y' []] -- g if c == 'x' then pure c else try (k + 1) 开始,概率为try k = join (Node (s 'x') [Node (s 'y') []]) where s c = if c == 'x' then pure c else try (k + 1) try k = join (Node (pure 'x') [Node (try (k + 1)) []]) try k = join (Node (pure 'x') [pure (try (k + 1))] -- simplifying join try k = Node 'x' [join (pure (try (k + 1)))] -- join . pure = id try k = Node 'x' [try (k + 1)] ,另一半为try 0,最后我们在try k = try (k + 1)停了下来。这解释了您观察到的树。

try k = Node 'x' [try (k + 1)]

(我认为这至少可以为your other question提供部分答案,因为这表明缩小try 100通常意味着从头开始重新运行生成器。)

答案 1 :(得分:1)

尽管Li-yao Xia的详细答案正确地描述了发生方式,但并没有解决为什么的问题; 为什么在每次收缩后会重新运行生成器?答案是它不应该;这是一个错误。在GitHub上查看错误报告Improve Filter