Haskell quickBatch: Teste ZipList Monoid no mconcat resulta em estouro de pilha
Eu criei instâncias órfãs para ZipList Semigroup e Monoid. No entanto, quando executo os testes do quickBatch no monóide, no teste mconcat, ocorre um erro de estouro de pilha. Como faço para resolver esse erro? Por que existe esse erro? É devido a pure mempty
, o que eu não entendo muito bem, já que obtive isso principalmente de HaskellBook Capítulo 17 Seção aplicável 17.8 ZipList Monoid?
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
Respostas
Sim, o erro é devido a pure mempty
, mas isso não significa que pure mempty
esteja errado. Vamos ver primeiro.
Ajuda muito olhar para os tipos envolvidos na definição mempty = pure mempty
:
mempty :: ZipList a
mempty = (pure :: a -> ZipList a) (mempty :: a)
Basicamente, vamos usar a pure
operação para criar um ZipList
fora mempty
do tipo a
. Ele ajuda daqui a olhar para a definição de pureparaZipList :
pure :: a -> ZipList a
pure x = ZipList (repeat x)
No total, mempty
para ZipList a
se vai ser um ZipList
que contém a lista infinitamente repetição de mempty
valores do tipo subjacente a
.
De volta a este erro que você está obtendo. Quando você tenta executar o teste monoid
novamente ZipList (Sum Int)
, o QuickCheck testa uma sequência de propriedades.
- Os dois primeiros verificam as propriedades de identidade esquerda e direita. O que eles fazem é gerar valores do tipo
x :: ZipList (Sum Int)
e verificar issox <> mempty = mempty <> x = x
. - O terceiro verifica se, para quaisquer dois valores
x, y :: ZipList (Sum Int)
, temos essex
mappendy = x <> y
. - A quarta verifica se, para qualquer lista de valores
x :: [ZipList (Sum Int)]
, dobrá-los commappend
é o mesmo que dobrámconcat
-los.
Antes de continuar, é muito importante notar que quando digo "para qualquer valor", realmente quero dizer que o QuickCheck está usando a Arbitrary
instância do referido tipo para gerar valores desse tipo. Além disso, a Arbitrary
instância de ZipList a
é igual à Arbitrary
instância de, [a]
mas depois incluída ZipList
. Por último, a Arbitrary
instância de [a]
nunca produzirá uma lista infinita (porque isso vai causar problemas quando você está verificando a igualdade, como entrar em um loop infinito ou estourar a pilha), então esses "para quaisquer valores" do tipo ZipList (Sum Int)
nunca serão infinitos qualquer.
Especificamente, isso significa que QuickCheck nunca gerará o valor arbitrariamente, mempty :: ZipList a
porque esta é uma lista infinita.
Então, por que os 3 primeiros passam, mas o último falha com um estouro de pilha? Nos três primeiros testes, nunca acabamos tentando comparar uma lista infinita com uma lista infinita. Vamos ver porque não.
- Nos primeiros dois testes, estamos observando
x <> mempty == x
emempty <> x == x
. Em ambos os casos,x
é um de nossos valores "arbitrários", que nunca será infinito, portanto, essa igualdade nunca entrará em um loop infinito. - No terceiro teste, nós estamos gerando dois ZipLists finitos
x
ey
emappend
ing-los juntos. Nada sobre isso será infinito. - No terceiro caso, estamos gerando uma lista de ZipLists e
mconcat
aumentando a lista. Mas, o que acontece se a lista estiver vazia? Bem,mconcat [] = mempty
e dobrar uma lista vazia produzmempty
. Isso significa que, se a lista vazia for gerada como entrada arbitrária (o que é perfeitamente possível), o teste tentará confirmar que uma lista infinita é igual a outra lista infinita, o que sempre resultará em estouro de pilha ou buraco negro.
Como você pode consertar isso? Posso inventar dois métodos:
Você pode definir sua própria versão de
EqProp
forZipList
para que compare apenas a igualdade em algum prefixo finito da lista. Isso provavelmente envolveria fazer um wrapper newtype (talveznewtype MonZipList a = MonZipList (ZipList a)
), derivar um grupo de instâncias e, em seguida, escrever umaEqProp
à mão. Isso provavelmente funcionará, mas é um pouco deselegante.Você pode escrever sua própria versão do
monoid
que usa uma versão diferente do quarto teste. Por exemplo, se você restringir para que o teste use apenas listas não vazias, você não terá nenhum problema. Para fazer isso, você deve começar examinando a definição dos monoidtestes de propriedade . Observe que atualmente define a propriedade "mconcat" comoproperty mconcatP
onde
mconcatP :: [a] -> Property
mconcatP as = mconcat as =-= foldr mappend mempty as
Usando a própria NonEmptyList
classe do QuickCheck , você pode reescrever isso para seus propósitos como:
mconcatP :: NonEmptyList a -> Property
mconcatP (NonEmptyList as) = mconcat as =-= foldr mappend mempty as
Obviamente, esta é uma condição um pouco mais fraca, mas pelo menos não trava.