Haskell quickBatch:在mconcat测试ZipList幺半群会导致堆栈溢出

Haskell quickBatch:在mconcat测试ZipList幺半群会导致堆栈溢出,haskell,Haskell,我已经为ZipList半群和幺半群创建了孤立实例。但是,当我在monoid上运行quickBatch的测试时,在mconcat测试中,出现了堆栈溢出错误。如何解决此错误?为什么会有这样的错误?这是因为我不太理解纯mempty,因为我主要是从HaskellBook第17章应用程序第17.8节ZipList幺半群中得到的 zl :: ZipList (Sum Int) zl = ZipList [1,1 :: Sum Int] instance Semigroup a => Semigr

我已经为ZipList半群和幺半群创建了孤立实例。但是,当我在monoid上运行quickBatch的测试时,在mconcat测试中,出现了堆栈溢出错误。如何解决此错误?为什么会有这样的错误?这是因为我不太理解纯mempty,因为我主要是从HaskellBook第17章应用程序第17.8节ZipList幺半群中得到的

zl :: ZipList (Sum Int)
zl = ZipList [1,1 :: Sum Int]
instance Semigroup a 
  => Semigroup (ZipList a) where
    (<>) = liftA2 (<>)
instance (Eq a, Monoid a)
  => Monoid (ZipList a) where
    mempty = pure mempty 
    mappend = (<>)
    mconcat as = 
      foldr mappend mempty as
main :: IO ()
main = do 
  quickBatch $ monoid zl
zl::ZipList(Sum Int)
zl=ZipList[1,1::Sum Int]
实例半群a
=>半群(ZipList a),其中
()=liftA2()
实例(等式a,幺半群a)
=>幺半群(ZipList a),其中
mempty=纯mempty
mappend=()
mconcat as=
foldr mappend mempty as
main::IO()
main=do
quickBatch$monoid zl

是的,错误是由于
纯mempty
引起的,但这并不意味着
纯mempty
是错误的。让我们先看看那里

查看定义中涉及的类型有很大帮助
mempty=pure mempty

mempty::ZipList a
mempty=(pure::a->ZipList a)(mempty::a)
基本上,我们将使用
pure
操作从
a
类型的
mempty
创建一个
ZipList
。从这里可以看到:

pure::a->ZipList a
纯x=ZipList(重复x)
总之,
ZipList a
mempty
将成为一个
ZipList
,包含基础类型
a
mempty
值的无限重复列表


回到你遇到的这个错误。当您尝试在
ZipList(Sum Int)
上运行测试
monoid
时,QuickCheck将测试一系列属性

  • 前两个检查左标识和右标识属性。它们所做的是生成类型为
    x::ZipList(Sum Int)
    的值,并验证
    x mempty=mempty x=x
  • 第三个检查对于任何两个值
    x,y::ZipList(Sum Int)
    ,我们得到了
    x
    mappend
    y=xy
  • 第四个检查对于任何值列表
    x::[ZipList(Sum Int)]
    ,使用
    mappend
    折叠这些值与使用
    mconcat
    折叠它们相同
在我继续之前,请务必注意,当我说“for any value”时,我的意思是QuickCheck使用所述类型的
任意
实例来生成该类型的值。此外,
ZipList a
arbitral
实例与
[a]
arbitral
实例相同,但随后被包装在
ZipList
中。最后,
[a]
任意
实例永远不会产生无限列表(因为在检查是否相等时,这些列表会导致问题,例如进入无限循环或堆栈溢出),因此
ZipList(Sum Int)
类型的“任何值”也永远不会是无限的


具体来说,这意味着QuickCheck永远不会任意生成值
mempty::ZipList a
,因为这是一个无限列表


那么为什么前3个会通过,但最后一个会因堆栈溢出而失败呢?在前三个测试中,我们从未尝试过将无限列表与无限列表进行比较。让我们看看为什么不

  • 在前两个测试中,我们关注的是
    xmempty==x
    memptyx==x
    。在这两种情况下,
    x
    是我们的“任意”值之一,它永远不会是无限的,因此这个等式永远不会进入无限循环
  • 在第三个测试中,我们将生成两个有限的ziplist
    x
    y
    以及
    mappend
    将它们组合在一起。这一切都不是无限的
  • 在第三种情况下,我们生成一个ZipLists列表,并启用该列表。但是,如果列表为空,会发生什么?嗯,
    mconcat[]=mempty
    ,折叠空列表会产生
    mempty
    。这意味着,如果空列表作为任意输入生成(这是完全可能的),那么测试将尝试确认一个无限列表等于另一个无限列表,这将始终导致堆栈溢出或黑洞

你怎么能解决这个问题?我可以想出两种方法:

  • 您可以为
    ZipList
    定义自己版本的
    EqProp
    ,以便它仅在列表的某个有限前缀上比较相等。这可能涉及制作一个新类型包装器(可能是
    newtype MonZipList a=MonZipList(ZipList a)
    ),派生一组实例,然后手工编写一个
    EqProp
    。这可能会奏效,但有点不雅观

  • 您可以编写自己版本的
    monoid
    ,使用第四个测试的不同版本。例如,如果您限制它,使测试只使用非空列表,那么您就不会有任何问题。要做到这一点,您应该从以下内容开始。请注意,它当前将“mconcat”属性定义为
    property mconcatP
    where

  • mconcapt::[a]->属性
    mconcatP as=mconcat as=-=foldr mappend mempty as
    
    使用QuickCheck自己的
    NonEmptyList
    类,您可以将其重写为:

    mconcapt::NonEmptyList a->Property
    mconcatP(非空列表as)=mconcat as=-=foldr mappend mempty as
    

    显然,这是一个稍微弱一点的条件,但至少它不会挂起。

    mempty::ZipList a
    是一个无限列表:我想知道它是否在尝试做类似
    mempty mempty
    的事情(尽管它会尝试
    mconcat
    而不是
    mappend
    本身)@chepner谢谢你的提示,但是我仍然对
    pu的用法感到困惑