nick8325 / nick8325/quickcheck

RFC: New method, generate values in (==) equivalence class (for quotient types)

Open
#214 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Haskell
Stars
790
Forks
130
Avg merge
16h 41m
Merged PRs (30d)
2

Description

Sometimes we want to generate structurally different but equal elements, for set we may want a generator for Set [1,2,3], Set [3,2,1], ..

This is an idea to add equivArbitrary (please propose a better name, approach etc) to the Arbitrary class with a default definition for arbitrary = join equivArbitrary:

class Arbitrary a where
  arbitrary :: Gen a
  arbitrary = join equivArbitrary

  equivArbitrary :: Gen (Gen a)
  equivArbitrary = do
    a <- arbitrary
    pure (pure a)

  {-# Minimal arbitrary | equivArbitrary #-}
  ..

Set: Outer-most Gen initializes the model (eg Set [1,2,3]) that the inner-most Gen gets to shuffle (Set [1,3,2], Set [3,2,1] ..):

newtype Set a = Set [a] deriving newtype Show

instance Ord a => Eq (Set a) where
  (==) :: Set a -> Set a -> Bool
  Set as == Set bs = sort as == sort bs

-- >> equivSet <- toIO (equivArbitrary @(Set Int))
-- >> equivSet
-- [-16,7,1,13]
-- >> equivSet
-- [13,-16,7,1]
-- >> equivSet
-- [13,1,-16,7]
instance Arbitrary a => Arbitrary (Set a) where
  equivArbitrary :: Gen (Gen (Set a))
  equivArbitrary = do
    as <- arbitrary
    pure $ do
      shuffle'd <- shuffle as
      pure (Set shuffle'd)

toIO :: Gen (Gen a) -> IO (IO a)
toIO = generate . fmap generate

This allows us to construct the example in 4.4 Testing for Invariance (Random Testing of Purely Functional Abstract Datatypes) in a backwards compatible way for every Arbitrary type without defining a new class:

-- (Equiv a) values contain two (a)-values (x :=: y) such that (x == y)
data Equiv a = a :=: a

-- >> equivSet <- toIO (equivArbitrary @(Equiv (Set Int)))
-- >> equivSet
-- [19,11,-25,-20,-8] :=: [-20,19,11,-25,-8]
-- >> equivSet
-- [19,11,-8,-20,-25] :=: [-20,-8,11,19,-25]
-- >> equivSet
-- [19,-25,-8,-20,11] :=: [11,-25,19,-8,-20]
instance Arbitrary a => Arbitrary (Equiv a) where
  equivArbitrary :: Gen (Gen (Equiv a))
  equivArbitrary = do
    gen <- equivArbitrary
    pure (liftA2 (:=:) gen gen)
property $ \x (q :=: q') -> enqueue x q == enqueue x q'
property $ \  (q :=: q') -> isEmpty   q == isEmpty   q' ..

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

This RFC names no files or tests; start by reviewing the Arbitrary and Gen APIs, along with the proposed Set and Equiv examples. Progress requires agreeing on the API and approach before implementation, with the invariance properties in the issue serving as the intended outcome.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.