Haskell quickBatch: Testowanie ZipList Monoid w mconcat powoduje przepełnienie stosu
utworzyłem osierocone instancje dla ZipList Semigroup i Monoid. Jednak gdy uruchamiam testy z quickBatch na monoidzie, podczas testu mconcat pojawia się błąd przepełnienia stosu. Jak rozwiązać ten błąd? Dlaczego jest taki błąd? Czy jest to spowodowane pure mempty
, czego nie do końca rozumiem, ponieważ dostałem to głównie z HaskellBook Rozdział 17 Zastosowanie sekcja 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
![](https://post.nghiatu.com/assets/images/s/LeU1Q.png)
Odpowiedzi
Tak, błąd jest spowodowany pure mempty
, ale to nie znaczy, że pure mempty
jest zły. Spójrzmy najpierw tam.
Bardzo pomaga przyjrzenie się typom związanym z definicją mempty = pure mempty
:
mempty :: ZipList a
mempty = (pure :: a -> ZipList a) (mempty :: a)
Zasadniczo zamierzamy użyć tej pure
operacji do utworzenia ZipList
out of the mempty
type a
. Pomaga stąd spojrzeć na definicję puredlaZipList :
pure :: a -> ZipList a
pure x = ZipList (repeat x)
W sumie mempty
for ZipList a
będzie ZipList
zawierać nieskończenie powtarzającą się listę mempty
wartości typu bazowego a
.
Wróć do tego błędu, który otrzymujesz. Podczas próby uruchomienia testu monoid
na ZipList (Sum Int)
, QuickCheck zamierza przetestować sekwencję właściwości.
- Pierwsze dwa sprawdzają lewą tożsamość i prawą tożsamość. To, co robią, to generowanie wartości typu
x :: ZipList (Sum Int)
i weryfikacja tegox <> mempty = mempty <> x = x
. - Trzeci sprawdza, czy dla dowolnych dwóch wartości
x, y :: ZipList (Sum Int)
mamy tox
mappendy = x <> y
. - Czwarty sprawdza, czy w przypadku dowolnej listy wartości
x :: [ZipList (Sum Int)]
złożenie ich za pomocąmappend
jest takie samo jakmconcat
ich złożenie .
Zanim przejdę dalej, naprawdę ważne jest, aby zauważyć, że kiedy mówię „dla dowolnej wartości”, naprawdę mam na myśli, że funkcja QuickCheck używa Arbitrary
wystąpienia wspomnianego typu do generowania wartości tego typu. Ponadto Arbitrary
instancja for ZipList a
jest taka sama jak Arbitrary
instancja for, [a]
ale jest następnie opakowana ZipList
. Na koniec Arbitrary
instancja for [a]
nigdy nie utworzy nieskończonej listy (ponieważ będą one powodować problemy podczas sprawdzania równości, np. Wchodzenie w nieskończoną pętlę lub przepełnienie stosu), więc te typy „dla dowolnych wartości” ZipList (Sum Int)
nigdy nie będą nieskończone zarówno.
W szczególności oznacza to, że funkcja QuickCheck nigdy nie wygeneruje arbitralnie wartości, mempty :: ZipList a
ponieważ jest to nieskończona lista.
Dlaczego więc pierwsze 3 pasują, a ostatnie kończy się niepowodzeniem z przepełnieniem stosu? W pierwszych trzech testach nigdy nie próbujemy porównać nieskończonej listy z nieskończoną listą. Zobaczmy, dlaczego nie.
- W pierwszych dwóch testach przyglądamy się
x <> mempty == x
imempty <> x == x
. W obu przypadkachx
jest to jedna z naszych „arbitralnych” wartości, która nigdy nie będzie nieskończona, więc ta równość nigdy nie wejdzie w nieskończoną pętlę. - W trzecim badaniu, jesteśmy generując dwa skończone ZipLists
x
iy
imappend
ing je razem. Nic w tym nie będzie nieskończone. - W trzecim przypadku tworzymy listę ZipLists i
mconcat
uzupełniamy ją. Ale co się stanie, jeśli lista jest pusta? Cóż,mconcat [] = mempty
a zwinięcie pustej listy dajemempty
. Oznacza to, że jeśli pusta lista jest generowana jako dowolne dane wejściowe (co jest całkowicie możliwe), test spróbuje potwierdzić, że nieskończona lista jest równa innej nieskończonej liście, co zawsze spowoduje przepełnienie stosu lub czarną dziurę.
Jak możesz to naprawić? Mogę wymyślić dwie metody:
Możesz zdefiniować własną wersję
EqProp
for,ZipList
tak aby porównywała równość tylko na jakimś skończonym przedrostku listy. Prawdopodobnie wymagałoby to stworzenia opakowania nowego typu (być możenewtype MonZipList a = MonZipList (ZipList a)
), wyprowadzenia kilku instancji, a następnieEqProp
ręcznego napisania jednego. To prawdopodobnie zadziała, ale jest trochę nieeleganckie.Możesz napisać własną wersję,
monoid
która korzysta z innej wersji czwartego testu. Na przykład, jeśli ograniczysz to tak, aby test używał tylko niepustych list, nie będziesz mieć żadnego problemu. Aby to zrobić, należy zacząć od przyjrzenia się definicji monoidtestów właściwości . Zauważ, że obecnie definiuje on właściwość „mconcat” jakoproperty mconcatP
where
mconcatP :: [a] -> Property
mconcatP as = mconcat as =-= foldr mappend mempty as
Korzystając z własnej NonEmptyList
klasy QuickCheck , możesz przepisać to do swoich celów jako:
mconcatP :: NonEmptyList a -> Property
mconcatP (NonEmptyList as) = mconcat as =-= foldr mappend mempty as
Oczywiście jest to nieco słabszy stan, ale przynajmniej taki, który się nie zawiesza.