Haskell quickBatch:mconcatでZipListモノイドをテストするとスタックオーバーフローが発生する

Jan 14 2021

ZipListSemigroupとMonoidの孤立したインスタンスを作成しました。ただし、monoidでquickBatchからテストを実行すると、mconcatテストでスタックオーバーフローエラーが発生します。このエラーを解決するにはどうすればよいですか?なぜそのようなエラーがあるのですか?それが原因でありpure mempty、私はほとんどHaskellBook章17節のApplicative 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

回答

1 DDub Jan 18 2021 at 01:54

はい、エラーはによるものですがpure mempty、それpure memptyは間違っているという意味ではありません。最初にそこを見てみましょう。

定義に含まれるタイプを確認するのに大いに役立ちますmempty = pure mempty

mempty :: ZipList a
mempty = (pure :: a -> ZipList a) (mempty :: a)

基本的に、このpure操作を使用して、ZipListoutmempty型を作成しますa。ここから、pureforZipListの定義を確認するのに役立ちます。

pure :: a -> ZipList a
pure x = ZipList (repeat x)

合計すると、memptyforZipList aは、基になる型の値のZipList無限に繰り返されるリストを含むことになりmemptyますa


発生しているこのエラーに戻ります。あなたがテストを実行しようとするとmonoidオーバーZipList (Sum Int)、QuickCheckは、プロパティの順序をテストするために起こっています。

  • 最初の2つは、左側のIDと右側のIDのプロパティを確認します。これらが行うことは、タイプの値を生成しx :: ZipList (Sum Int)、それを検証することx <> mempty = mempty <> x = xです。
  • 3番目は、任意の2つの値についてx, y :: ZipList (Sum Int)、そのx mappendがあることを確認し y = x <> yます。
  • 4番目は、値のリストについてx :: [ZipList (Sum Int)]、これらを折りたたむことはそれらを折りたたむこととmappend同じであるmconcatことを確認します。

続行する前に、「任意の値に対して」と言うとき、QuickCheckがそのArbitraryタイプのインスタンスを使用してそのタイプの値を生成していることを意味することに注意することが非常に重要です。さらに、のArbitraryインスタンスZipList aはのインスタンスと同じArbitraryです[a]が、でラップされZipListます。最後に、のArbitraryインスタンスが[a]無限リストを生成することはありません(無限ループに入ったり、スタックをオーバーフローしたりするなど、等しいかどうかをチェックするときに問題が発生するため)。したがって、これらの「任意の値」タイプZipList (Sum Int)は無限ではありませんどちらか。

具体的にmempty :: ZipList aは、これは無限のリストであるため、QuickCheckが値を任意に生成することは決してないことを意味します。


では、なぜ最初の3つはパスするのに、最後の3つはスタックオーバーフローで失敗するのでしょうか。最初の3つのテストでは、無限リストを無限リストと比較しようとすることは決してありません。理由を見てみましょう。

  • 最初の2回のテストでは、我々は見ているx <> mempty == xmempty <> x == x。どちらの場合も、xは「任意の」値の1つであり、無限になることはないため、この等式が無限ループになることはありません。
  • 3番目のテストでは、我々は2つの有限ZipListsを生成しているxyし、mappendそれらを一緒にINGの。これについては何も無限ではありません。
  • 3番目のケースでは、ZipListのリストを生成し、リストを生成してmconcatいます。しかし、リストが空の場合はどうなりますか?さて、、mconcat [] = memptyそして空のリストを折りたたむと、が生成されmemptyます。つまり、空のリストが任意の入力として生成された場合(これは完全に可能です)、テストは無限リストが別の無限リストと等しいことを確認しようとします。これにより、常にスタックオーバーフローまたはブラックホールが発生します。

どうすればこれを修正できますか?私は2つの方法を思い付くことができます:

  1. あなたは、独自のバージョンを定義することができるEqPropためZipList、それが唯一のリストの一部有限プレフィックスに平等を比較するようにします。これにはnewtype MonZipList a = MonZipList (ZipList a)、(おそらく)newtypeラッパーを作成し、多数のインスタンスを派生させてから、EqProp手動でインスタンスを作成することが含まれる可能性があります。これはおそらく機能しますが、少しエレガントではありません。

  2. monoid4番目のテストの異なるバージョンを使用する独自のバージョンを作成できます。たとえば、テストで空でないリストのみを使用するように制限すると、問題は発生しません。これを行うには、monoidプロパティテストの定義を確認することから始める必要があります。現在、「mconcat」プロパティを次のようproperty mconcatPに定義していることに注意してください。

mconcatP :: [a] -> Property
mconcatP as = mconcat as =-= foldr mappend mempty as

QuickCheck独自のNonEmptyListクラスを使用して、次のように目的に合わせてこれを書き直すことができます。

mconcatP :: NonEmptyList a -> Property
mconcatP (NonEmptyList as) = mconcat as =-= foldr mappend mempty as

明らかに、これは少し弱い状態ですが、少なくともハングしない状態です。