constrained-generators-0.2.0.0: examples/Constrained/Examples/CheatSheet.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ViewPatterns #-}
module Constrained.Examples.CheatSheet where
import Constrained.API
import Data.Set (Set)
import Data.Set qualified as Set
import GHC.Generics
import Test.QuickCheck (Property, label)
-- The `constrained-generators` library allows us to write
-- constraints that give us random generators, shrinkers, and checkers
-- for data using a small embedded DSL, which defines a limited first order logic.
--
-- Every first order logic has 4 parts, as does our DSL.
-- 1) Terms : e.g. x, 5, (member_ x set) (x ==. y)
-- Implemented as (Term a). We have variables like 'x', and constants like '5'.
-- 'member_' and '==.' are function symbols, and build Terms from other terms.
-- By convention, a name followed by '_' or an infix operator followed by '.' are function symbols.
-- 2) Predicates (over terms). Predicates commonly used are
-- TruePred,
-- FalsePred (pure "explain"),
-- assert $ termWithTypeBool,
-- Some more unusual predicates are described below.
-- Implemented as type (Pred fn)
-- 3) Combinators (combining predicates). In general, And, Or, Not, Implies, True, False
-- But in the DSL, we are limited to
-- 'And' using Block :: [Pred] -> Pred
-- 'Not' using the function symbol not_ :: Term Bool -> Term Bool
-- for example: assert $ not_ (x ==. y)
-- limited form of 'Or' using
-- chooseSpec :: (Int, Specification a)- > (Int, Specification a) -> Specification a
-- 4) Quantifiers (applying constraints to many things) :
-- forAll: Term t -> (Term a -> p) -> Pred fn
-- exists: ((forall b. Term b -> b) -> GE a) -> (Term a -> p) -> Pred fn
-- These are explained in detail below
-- In case you are interested, here is a list of supported function symbols (note the use of the '_' and '.' convention)
-- disjoint_, dom_, elem_, length_, member_, not_, rng_, singleton_, sizeOf_, subset_, sum_, (/=.),
-- (<.), (<=.), (==.), (>.), (>=.), fromList_, null_, union_
-- You may also use the methods of Num (+) (-) (*), since there is a (Num (Term fn)) instance.
-- The first order logic DSL is used to build Specifications
-- A specifcation with type (Specification x) has two uses
-- 1) To generate a random values of type 'x', subject to the constraints in the specifications definition.
-- This is implemented by genFromSpec :: Specification x -> Gen x (Gen is the QuickCheck Gen)
-- 2) To test if a value of type 'x' meets all of the constraints given in the specifications definition.
-- This is implemented by conformsToSpec :: HasSpec a => a -> Specification a -> Bool
-- Lets get started. We can talk about numbers:
specInt :: Specification Int
specInt = constrained $ \i ->
[ assert $ i <. 10
, assert $ 0 <. i
]
-- What's going on here? In short:
-- `constrained :: (HasSpec a, IsPred p fn) => (Term a -> p) -> Specification a`
-- Introduces the variable `i` over which we can write constraints of type `p` over something
-- of type `a` to produce a `Specifcation a` using a list of
-- `assert :: Term Bool -> Pred with `Term -level versions (function symbols) of familiar functions like
-- `(<.) :: OrdLike a => Term a -> Term a -> Term Bool`, `null_ :: Term [a] -> Term Bool`,
-- `rng_ :: (HasSpec k, HasSpec v, Ord k) => Term (Map k v) -> Term (Set k)` etc.
-- We get a generator from `genFromSpec :: Specification BaseFn a -> Gen a`:
-- λ> sample $ genFromSpec specInt
-- 1
-- 5
-- 6
-- 6
-- 8
-- 5
-- 3
-- 1
-- 1
-- 4
-- 8
-- Likewise, `shrinkWithSpec :: Specification BaseFn a -> a -> [a]` gives us
-- a shrinker:
-- λ> shrinkWithSpec specInt 10
-- [5,8,9]
-- λ> shrinkWithSpec specInt 5
-- [3,4]
-- λ> shrinkWithSpec specInt 3
-- [2]
-- λ> shrinkWithSpec specInt 1
-- []
-- And, `conformsToSpec :: a -> Specification BaseFn a -> Bool` gives us a checker:
-- λ> 10 `conformsToSpec` specInt
-- False
-- λ> 5 `conformsToSpec` specInt
-- True
-- Note that the type of `constrained` says the binding function of type `Term a -> p` doesn't
-- have to produce a `Pred (which is the return type of `assert`), but can produce something of type `p`
-- that satisfies `IsPred p`. This basically just means something that can be readily turned into a
-- `Pred`, like e.g. `Pred`, `Bool`, `Term Bool`, `[p]` for `IsPred p`. Consequently, we could
-- have written `specInt` as:
specInt' :: Specification Int
specInt' = constrained $ \i ->
[ i <. 10
, 0 <. i
]
-- However, beware that when we start mixing `Term Bool` and `Pred` in these lists we can end
-- up getting some inscrutable error messages. So, if a call to `constrained` or another function that
-- has `IsPred` as a constraint, starts giving you strange error messages, double check that you have
-- used `assert` instead of raw `Term Bool` everywhere relevant.
-- We also have support for product types with functions like `fst_`, `snd_`, and `pair_`:
specProd :: Specification (Int, Int)
specProd = constrained $ \p ->
[ fst_ p <. 10
, snd_ p <. 100
]
-- However, product types can also be a bit finicky:
specProd0 :: Specification (Int, Int)
specProd0 = constrained $ \p -> assert $ fst_ p <. snd_ p
-- λ> sample $ genFromSpec specProd0
-- *** Exception: Simplifying:
-- constrained $ \ v0 -> assert $ Less (Fst (ToGeneric v0)) (Snd (ToGeneric v0))
-- optimisePred => assert $ Less (Fst (ToGeneric v0)) (Snd (ToGeneric v0))
-- assert $ Less (Fst (ToGeneric v0)) (Snd (ToGeneric v0))
-- Can't build a single-hole context for variable v0 in term Less (Fst (ToGeneric v0)) (Snd (ToGeneric v0))
-- This gives us the _fundamental restriction_:
-- A variable can not appear twice in the same constraint
-- The fundamental restriction is very important to make the system compositional
-- and modular. We will get back to talking about it in detail when we discuss how to
-- extend the system. However, for now suffice to say that it's a lot easier to solve
-- constraints that look like `2 * x <. 10` than it is to solve constraints
-- like `x <. 10 - x` (i.e. ones that mention the same variable more than once).
-- To overcome the fundamental restriction we can use `match`:
-- match ::
-- forall p a.
-- ( HasSpec a
-- , IsProductType a
-- , IsPred p fn
-- ) =>
-- Term a ->
-- FunTy (MapList (Term fn) (ProductAsList a)) p ->
-- Pred fn
specProd1 :: Specification (Int, Int)
specProd1 = constrained $ \p ->
match p $ \x y ->
x <. y
-- λ> sample $ genFromSpec specProd1
-- (-1,0)
-- (-4,-2)
-- (1,2)
-- (-2,1)
-- (7,8)
-- (-9,-4)
-- (-3,3)
-- (-1,12)
-- (-7,-6)
-- (-11,17)
-- (-53,-14)
-- Bringing variables into scope.
-- 'constrained' and 'match' are the ways we bring variable into scope, And they are often nested.
-- Consider writing a specification for pair of nested pairs: Specification ((Int,Int),(Int,Int))
-- How do we name the four different Int's ?
nested :: Specification ((Int, Int), (Int, Int))
nested =
constrained $ \pp ->
match pp $ \p1 p2 ->
match p1 $ \x1 y1 ->
match p2 $ \x2 y2 ->
[x1 <=. y1, y1 <=. x2, x2 <=. y2]
-- ghci> sample $ genFromSpec nested
-- ((0,0),(0,0))
-- ((-9,-5),(-1,0))
-- ((-12,-10),(-5,-2))
-- ((-8,-4),(-3,-2))
-- ((-33,-18),(-15,-6))
-- ((-21,-12),(-1,3))
-- ((-36,-12),(1,9))
-- ((-64,-37),(-30,-4))
-- ((-53,-37),(-33,-10))
-- ((-49,-15),(-6,8))
-- ((-72,-34),(-26,-19))
-- A good rule of thumb when starting a new specification is to think about how you would
-- use 'constrained' and 'match' to bring variables, naming each of the parts that you want
-- to constrain, into scope.
-- Let's look under the hood of `match`, it introduces two auxilliary variables `v0` and `v1`
-- that circumvents the fundamental restriction by allowing us to generate values for `v1` and
-- `v0` before we generate a value for `v3`.
-- λ> simplifySpec specProd1
-- constrained $ \ v3 ->
-- let v1 = Fst (ToGeneric v3) in
-- let v0 = Snd (ToGeneric v3) in
-- assert $ Less v1 v0
-- This pattern of `constrained $ \ p -> match p $ \ x y -> ...` is very common
-- and has a shorthand in the form of `constrained'`:
specProd2 :: Specification (Int, Int)
specProd2 = constrained' $ \x y -> x <. y
-- How does generation actually work when we have multiple variables? For example,
-- it is not obvious (to the computer) what the best way of generating values satisfying
-- this constraint is:
solverOrder :: Specification (Int, Int)
solverOrder = constrained' $ \x y ->
[ x <. y
, y <. 10
]
-- For example, if you tried generating a value for `x` first chances are you'd generate
-- something larger than 10, which would make it impossible to generate a valid `y`. However,
-- when we run it we get reasonable values out:
-- sample $ genFromSpec solverOrder
-- (-1,0)
-- (0,2)
-- (-4,4)
-- (-7,-3)
-- (-7,3)
-- (-11,-3)
-- (4,8)
-- (-15,-14)
-- (-25,-10)
-- (-23,-6)
-- (-51,-20)
-- But how does the system know to generate `y` first? Unfortunately, there is nothing smart about
-- it. The system simply solves things "right to left" - variables that appear to the right in assertions
-- are solved before variables to the left. If one wants to understand the consequences of this and how it
-- affects the generator the `printPlan` function comes in handy:
-- λ> printPlan solverOrder
-- Simplified spec:
-- constrained $ \ v_3 ->
-- let v_1 = Fst (ToGeneric v_3) in
-- let v_0 = Snd (ToGeneric v_3) in
-- {assert $ Less v_0 10
-- assert $ Less v_1 v_0}
-- SolverPlan
-- Dependencies:
-- v_0 <- []
-- v_1 <- [v_0]
-- v_3 <- [v_0, v_1]
-- Linearization:
-- v_0 <- TypeSpec [..9] []
-- v_1 <- assert $ Less v_1 v_0
-- v_3 <-
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
-- assert $ Equal (Snd (ToGeneric v_3)) v_0
-- There are three parts to the output:
-- - The "Simplified spec" is the input specification after it has gone through a number of optimization
-- and simplification passes to make it amenable to solving.
-- - The "Dependencies" tells us what variables depend on what other variables to be solved. In this case `v0` (y)
-- has no dependencies, `v1` (x) is solved after `v0` and `v3` (the actual pair we are generating) is solved
-- last.
-- - Finaly, the "Linearization" tells us _what constraints define what varible_. This is an important aspect of the
-- system: variables are only constrained by assertions that talk about the variable itself and variables that
-- are solved before it. In this case `v0` (y) is defined by `y <. 10`, `v1` (x) by `x <. y` and `v3` by the equalities
-- in the `Let` constructs.
--
-- As the generator executes this plan it will pick the variables in the order in which they appear in the linearization
-- and generate the corresponding values. For example, an execution trace could go like the following pseudo-trace (the details of how
-- this works are slightly more involved but the basic order of operations is accurate):
-- v0 <- pick from (-∞, 10)
-- v0 = 4
-- v1 <- pick from [4/v0](-∞, v0)
-- -> pick from (-∞, 4)
-- v1 = 2
-- v3 <- pick from [4/v0, 2/v1]{fst == v1, snd == v0}
-- -> pick from {fst == 2, snd == 4}
-- v3 = (2, 4)
-- As an aside, the frustrating thing about making sense of the output of `printPlan` is the `v0`, `v1`, etc. naming.
-- To introduce proper names we can use the `var` quasi-quoter:
solverOrder' :: Specification (Int, Int)
solverOrder' = constrained' $ \ [var|x|] [var|y|] ->
[ x <. y
, y <. 10
]
-- Now we get more reasonable looking oputput from `printPlan`:
-- λ> printPlan solverOrder'
-- Simplified spec:
-- constrained $ \ v_3 ->
-- let x_1 = Fst (ToGeneric v_3) in
-- let y_0 = Snd (ToGeneric v_3) in
-- {assert $ Less y_0 10
-- assert $ Less x_1 y_0}
-- SolverPlan
-- Dependencies:
-- y_0 <- []
-- x_1 <- [y_0]
-- v_3 <- [y_0, x_1]
-- Linearization:
-- y_0 <- TypeSpec [..9] []
-- x_1 <- assert $ Less x_1 y_0
-- v_3 <-
-- assert $ Equal (Fst (ToGeneric v_3)) x_1
-- assert $ Equal (Snd (ToGeneric v_3)) y_0
-- A consequence of the default dependency order approach is that it's possible
-- to write constraints that put you in a tricky situation:
tightFit0 :: Specification (Int, Int)
tightFit0 = constrained' $ \x y ->
[ 0 <. x
, x <. y
]
-- λ> sample $ genFromSpec tightFit0
-- *** Exception: genFromPreds:
-- let v_1 = Fst (ToGeneric v_3) in
-- let v_0 = Snd (ToGeneric v_3) in
-- {assert $ Less v_1 v_0
-- assert $ Less 0 v_1}
-- SolverPlan
-- Dependencies:
-- v_0 <- []
-- v_1 <- [v_0]
-- v_3 <- [v_0, v_1]
-- Linearization:
-- v_0 <-
-- v_1 <-
-- TypeSpec [1..] []
-- ---
-- assert $ Less v_1 v_0
-- v_3 <-
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
-- assert $ Equal (Snd (ToGeneric v_3)) v_0
-- Stepping the plan:
-- SolverPlan
-- Dependencies:
-- v_1 <- []
-- v_3 <- [v_1]
-- Linearization:
-- v_1 <- ErrorSpec [1..-1]
-- v_3 <-
-- TypeSpec (Cartesian TrueSpec (MemberSpec [0])) []
-- ---
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
-- Env (fromList [(v_0,EnvValue 0)])
-- genFromSpecT ErrorSpec{} with explanation:
-- [1..-1]
-- The generator fails with output similar to what we saw above and a message telling us we tried to generate
-- a value from the (empty) interval [1..-1]. Inspecting the output above carefully we see that the graph and the
-- linearization tell us that `v0` (y) is completely unconstrained. The consequence of this is that when we get to the
-- point of trying to generate `v1` (x) we've already picked a value (-1) for `v0` that makes it impossible to satisfy
-- the constraints on `v1` and its constraints have specialized away to an error spec.
-- The solution to this issue is to introduce `dependsOn`, which lets us override the dependency order in constraints:
tightFit1 :: Specification (Int, Int)
tightFit1 = constrained' $ \x y ->
[ assert $ 0 <. x
, assert $ x <. y
, y `dependsOn` x
]
-- λ> printPlan tightFit1
-- Simplified spec:
-- constrained $ \ v_3 ->
-- let v_1 = Fst (ToGeneric v_3) in
-- let v_0 = Snd (ToGeneric v_3) in
-- {v_0 <- v_1
-- assert $ Less v_1 v_0
-- assert $ Less 0 v_1}
-- SolverPlan
-- Dependencies:
-- v_0 <- [v_1]
-- v_1 <- []
-- v_3 <- [v_0, v_1]
-- Linearization:
-- v_1 <- TypeSpec [1..] []
-- v_0 <- assert $ Less v_1 v_0
-- v_3 <-
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
-- assert $ Equal (Snd (ToGeneric v_3)) v_0
-- This gives us more balanced constraints that solve `v1` before they solve `v0`!
-- Consequently, this constraint generates reasonable values:
-- λ> sample $ genFromSpec tightFit1
-- (1,2)
-- (2,3)
-- (9,15)
-- (4,10)
-- (12,27)
-- (15,21)
-- (10,30)
-- (23,51)
-- (7,34)
-- (21,46)
-- (28,49)
-- We also support booleans with `ifElse :: Term Bool -> Pred -> Pred -> Pred`
-- where the branches of the `ifElse` depend on the scrutinee.
booleanExample :: Specification (Int, Int)
booleanExample = constrained' $ \x y ->
ifElse
(0 <. x)
(y ==. 10)
(y ==. 20)
-- sample $ genFromSpec booleanExample
-- (0,20)
-- (2,10)
-- (4,10)
-- (1,10)
-- (-2,20)
-- (3,10)
-- (7,10)
-- (-8,20)
-- (-5,20)
-- (-2,20)
-- (-19,20)
-- We can combine `ifElse` and `dependsOn` to write a nice example saying
-- that a PVP version pair `q` can follow a pair `p`.
-- Because we will need to re-use this multiple times we start by defining a valid
-- PVP constraint as any constraint that has non-negative major and minor version number.
validPVPVersion :: Specification (Int, Int)
validPVPVersion = constrained' $ \ma mi -> [0 <=. ma, 0 <=. mi]
-- Now we are ready to define the constraints for valid PVP succession. Note here that
-- we use the `satisfies :: Term a -> Specification BaseFn a -> Pred` combinator
-- to re-use the `validPVPVersion` constraint.
canFollowExample :: Specification ((Int, Int), (Int, Int))
canFollowExample = constrained' $ \p q ->
[ match p $ \ma mi ->
match q $ \ma' mi' ->
[ ifElse
(ma' ==. ma)
(mi' ==. mi + 1)
(mi' ==. 0)
, -- Note how these two constraints imply a cycle:
-- ma' <- ma <- ma'
assert $ ma' <=. ma + 1
, assert $ ma <=. ma'
, -- We break that cycle by specifying a concrete order
-- Another option would be to define `>=.` but that doesn't
-- exist right now and we will get to extending the language
-- later on!
ma' `dependsOn` ma
]
, p `satisfies` validPVPVersion
, q `satisfies` validPVPVersion
]
-- λ> sample $ genFromSpec canFollowExample
-- ((0,0),(0,1))
-- ((1,0),(1,1))
-- ((4,2),(4,3))
-- ((12,1),(12,2))
-- ((11,16),(11,17))
-- ((20,7),(21,0))
-- ((18,12),(18,13))
-- ((6,18),(7,0))
-- ((29,24),(30,0))
-- ((23,21),(23,22))
-- ((26,14),(26,15))
-- We have native support for sum types using `caseOn` and `branch`:
sumExample :: Specification (Either Int Bool)
sumExample = constrained $ \e ->
(caseOn e)
(branch $ \i -> i <. 0)
(branch $ \b -> not_ b)
-- Furthermore, cases are solved _inside-out_ by default:
sumExampleTwo :: Specification (Int, Either Int Bool)
sumExampleTwo = constrained' $ \i e ->
[ caseOn
e
(branch $ \j -> i <. j)
(branch $ \b -> not_ b)
, assert $ 20 <. i
]
-- We can work with sets with operations like `subset_`, `union_` (or `<>`), `disjoint_`, and `singleton_`:
setExample :: Specification (Set Int, Set Int, Set Int)
setExample = constrained' $ \xs ys zs ->
[ xs `subset_` (ys <> zs)
, sizeOf_ ys <=. 10
]
-- We can also quantify over things like sets with `forAll`:
forAllFollow0 :: Specification ((Int, Int), Set (Int, Int))
forAllFollow0 = constrained' $ \p qs ->
[ forAll qs $ \q -> pair_ p q `satisfies` canFollowExample
]
-- λ> sample $ genFromSpec forAllFollow0
-- ((0,0),fromList [])
-- ((1,-1),fromList [])
-- ((2,3),fromList [(2,4),(3,0)])
-- ((4,2),fromList [(4,3),(5,0)])
-- ((-2,6),fromList [])
-- ((10,-9),fromList [])
-- ((-1,-8),fromList [])
-- ((-8,-1),fromList [])
-- ((1,4),fromList [(1,5),(2,0)])
-- ((-17,-5),fromList [])
-- ((-2,12),fromList [])
-- How come the sets are so small? Note that we sometimes still generate
-- negative values for the components of `p`. But we said in the `canFollowExample`
-- that `p` needs to be a valid PVP version. However, the constraints only say that
-- it needs to be a valid PVP version _if `qs` is non-empty!_. This is easily fixed
-- by specifying that `p` is _always_ a valid PVP version!
forAllFollow :: Specification ((Int, Int), Set (Int, Int))
forAllFollow = constrained' $ \p qs ->
[ forAll qs $ \q -> pair_ p q `satisfies` canFollowExample
, p `satisfies` validPVPVersion
]
-- λ> sample $ genFromSpec forAllFollow
-- ((0,0),fromList [])
-- ((0,1),fromList [])
-- ((1,5),fromList [(1,6),(2,0)])
-- ((8,10),fromList [(8,11)])
-- ((12,15),fromList [(12,16)])
-- ((6,16),fromList [])
-- ((4,11),fromList [(4,12)])
-- ((10,21),fromList [(10,22),(11,0)])
-- ((28,2),fromList [(28,3),(29,0)])
-- ((20,3),fromList [(20,4),(21,0)])
-- ((16,29),fromList [(16,30),(17,0)])
-- We also have existential quantification in the language. The first argument to
-- `exists` tells you how to reconstruct the value from known values.
existentials :: Specification (Set Int, Set Int)
existentials = constrained' $ \xs ys ->
exists (\eval -> pure $ Set.intersection (eval xs) (eval ys)) $ \zs ->
[ assert $ not_ $ null_ zs
, assert $ zs `subset_` xs
, assert $ zs `subset_` ys
, xs `dependsOn` zs
, ys `dependsOn` zs
]
-- You can work with your own types relatively easily. If they are `Generic`
-- you even get all the machinery of sum and product types for free!
data FooBarBaz = Foo Int Int | Bar Bool | Baz deriving (Eq, Show, Generic)
-- All you need to do is introduce instances for `HasSimpleRep` and `HasSpec`:
instance HasSimpleRep FooBarBaz
instance HasSpec FooBarBaz
fooBarBaz :: Specification FooBarBaz
fooBarBaz = constrained $ \fbb ->
caseOn
fbb
(branch $ \i j -> i <. j)
(branch $ \b -> not_ b)
(branch $ \_ -> False)
-- λ> sample $ genFromSpec fooBarBaz
-- Foo (-1) 0
-- Bar False
-- Foo (-9) (-3)
-- Bar False
-- Foo 1 3
-- Foo (-20) (-8)
-- Foo (-35) (-11)
-- Bar False
-- Foo (-8) 5
-- Bar False
-- Foo (-4) 7
-- Some functions don't exist on the term level. In this case we can use
-- `reifies :: (HasSpec a, HasSpec b) => Term b -> Term a -> (a -> b) -> Pred`
-- to introduce a one-way evaluation of a Haskell function:
reifyExample :: Specification (Int, Int)
reifyExample = constrained' $ \ [var|a|] [var|b|] ->
reifies b a $ \x -> mod x 10
-- Here we introduce two variables `a` and `b` without any immediate dependency and we say that
-- `b` reifies `a` via the haskell function `\x -> mod x 10`. The best way to understand what this
-- cryptic code means is to imagine there was a `mod_` function, in that case this code would be equivalent
-- to:
reifyExample' :: Specification (Int, Int)
reifyExample' = constrained' $ \a b ->
[ assert $ b ==. mod_ a 10
, b `dependsOn` a
]
where
mod_ :: Term Int -> Term Int -> Term Int
mod_ = error "This doesn't exist"
-- When we look at the plan we get from `reifyExample` we get what we'd expect:
-- λ> printPlan reifyExample
-- Simplified spec:
-- constrained $ \ v_3 ->
-- let v_1 = Fst (ToGeneric v_3) in
-- let v_0 = Snd (ToGeneric v_3) in reifies v_0 v_1
-- SolverPlan
-- Dependencies:
-- v_0 <- [v_1]
-- v_1 <- []
-- v_3 <- [v_0, v_1]
-- Linearization:
-- v_1 <-
-- v_0 <- reifies v_0 v_1
-- v_3 <-
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
-- assert $ Equal (Snd (ToGeneric v_3)) v_0
-- Sometimes it is convenient to introduce an auxilliary variable to represent the result of applying the
-- haskell-level function to the term, for this purpose we have
-- `reify :: (HasSpec a, HasSpec b, IsPred p fn) => Term a -> (a -> b) -> (Term b -> p) -> Pred`.
-- We have tools to control the distribution of test cases and monitor those distributions. Using `branchW` we can
-- attach weights to branches in a `caseOn` and using `monitor :: ((forall. Term a -> a) -> Property -> Property) -> Pred`
-- we can use the normal QuickCheck functions for monitoring distributions of generators to see the effects of this.
monitorExample :: Specification (Either Int Int)
monitorExample = constrained $ \e ->
caseOn
e
(branchW 1 $ \_ -> monitor $ \_ -> label "Left")
(branchW 2 $ \_ -> monitor $ \_ -> label "Right")
-- The `forAllSpec :: (Testable p, HasSpec a) => Specification a -> (a -> p) -> Property` we
-- automatically get the monitoring from the spec in our property:
prop_monitoring :: Property
prop_monitoring = forAllSpec monitorExample $ \_ -> True
-- λ> quickCheck $ prop_monitoring
-- +++ OK, passed 100 tests:
-- 64% Right
-- 36% Left
-- Other tools for controlling distributions of specifications are available too, for example
-- `chooseSpec :: HasSpec a => (Int, Specification a) -> (Int, Specification a) -> Specification a`,
-- the definition of which constitutes a useful object of study to better understand how to use the compositional
-- nature of the system to build powerful features.
chooseSpecExample :: Specification Int
chooseSpecExample =
chooseSpec
(1, constrained $ \i -> i <. 0)
(2, constrained $ \i -> 0 <. i)
prop_chooseSpec :: Property
prop_chooseSpec = forAllSpec chooseSpecExample $ \i ->
label (show $ signum i) True
-- λ> quickCheck prop_chooseSpec
-- +++ OK, passed 100 tests:
-- 67% 1
-- 33% -1