StrictCheck 0.1.1 → 0.2.0
raw patch · 9 files changed
+246/−137 lines, 9 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Test.StrictCheck.Observe.Unsafe: entangle :: forall a. a -> (a, Thunk a)
- Test.StrictCheck.Observe.Unsafe: entangleShape :: Shaped a => a -> (a, Demand a)
+ Test.StrictCheck.Demand: instance Data.Foldable.Foldable Test.StrictCheck.Demand.Thunk
+ Test.StrictCheck.Demand: instance Data.Traversable.Traversable Test.StrictCheck.Demand.Thunk
+ Test.StrictCheck.Observe: entangle :: a -> IO (a, IO (Thunk a))
+ Test.StrictCheck.Observe: instrument :: forall a. Shaped a => a -> IO (a, IO (Demand a))
+ Test.StrictCheck.Shaped: foldM :: forall a m f g. (Traversable f, Shaped a, Monad m) => (forall x. Shaped x => f (Shape x g) -> m (g x)) -> f % a -> m (g a)
+ Test.StrictCheck.Shaped: translateA :: forall a c f g. (Shaped a, Applicative c) => (forall x. Shaped x => f x -> c (g x)) -> Shape a f -> c (Shape a g)
+ Test.StrictCheck.Shaped: unfoldM :: forall a m f g. (Traversable g, Shaped a, Monad m) => (forall x. Shaped x => f x -> m (g (Shape x f))) -> f a -> m (g % a)
+ Test.StrictCheck.Shaped: unzipWithM :: (Traversable f, All Functor [g, h], Shaped a, Monad m) => (forall x sx. sx ~ (Shape x ((%) g), Shape x ((%) h)) => f sx -> m (g sx, h sx)) -> (f % a -> m (g % a, h % a))
+ Test.StrictCheck.Shaped.Flattened: traverseFlattened :: forall c d f g h xs. (All c xs, Applicative h) => (forall x. c x => f x -> h (g x)) -> Flattened d f xs -> h (Flattened d g xs)
- Test.StrictCheck.Shaped: type family Shape a :: (* -> *) -> *;
+ Test.StrictCheck.Shaped: type family Shape a = (result :: (* -> *) -> *) | result -> a;
- Test.StrictCheck.Shaped: unzipWith :: (All Functor [f, g, h], Shaped a) => (forall x. f x -> (g x, h x)) -> (f % a -> (g % a, h % a))
+ Test.StrictCheck.Shaped: unzipWith :: (All Functor [f, g, h], Shaped a) => (forall x sx. sx ~ (Shape x ((%) g), Shape x ((%) h)) => f sx -> (g sx, h sx)) -> (f % a -> (g % a, h % a))
Files
- StrictCheck.cabal +5/−5
- src/Test/StrictCheck/Demand.hs +1/−1
- src/Test/StrictCheck/Internal/Inputs.hs +0/−1
- src/Test/StrictCheck/Observe.hs +130/−22
- src/Test/StrictCheck/Observe/Unsafe.hs +0/−76
- src/Test/StrictCheck/Shaped.hs +65/−31
- src/Test/StrictCheck/Shaped/Flattened.hs +7/−1
- tests/Entangle.hs +36/−0
- tests/Tests.hs +2/−0
StrictCheck.cabal view
@@ -1,6 +1,6 @@ name: StrictCheck-version: 0.1.1-synopsis: StrictCheck: Keep Your Laziness In Check+version: 0.2.0+synopsis: Keep Your Laziness In Check description: StrictCheck is a property-based random testing framework for observing, specifying, and testing the strictness behaviors of Haskell functions. Strictness behavior is traditionally considered a non-functional@@ -33,7 +33,6 @@ Test.StrictCheck.Produce, Test.StrictCheck.Demand, Test.StrictCheck.Observe,- Test.StrictCheck.Observe.Unsafe, Test.StrictCheck.Shaped, Test.StrictCheck.Shaped.Flattened, Test.StrictCheck.Internal.Inputs,@@ -51,7 +50,8 @@ DeriveAnyClass, TypeOperators, PolyKinds, GeneralizedNewtypeDeriving, ViewPatterns, LambdaCase, TupleSections, ImplicitParams,- NamedFieldPuns, PatternSynonyms+ NamedFieldPuns, PatternSynonyms,+ DeriveFoldable, DeriveTraversable ghc-options: -Wall -Wno-unticked-promoted-constructors -Wredundant-constraints @@ -59,7 +59,7 @@ type: exitcode-stdio-1.0 hs-source-dirs: tests main-is: Tests.hs- other-modules: Specs, RefTrans+ other-modules: Specs, RefTrans, Entangle default-language: Haskell2010 default-extensions: DataKinds, GADTs, BangPatterns, TypeFamilies, RankNTypes, AllowAmbiguousTypes, UndecidableInstances,
src/Test/StrictCheck/Demand.hs view
@@ -58,7 +58,7 @@ data Thunk a = Eval !a | Thunk- deriving (Eq, Ord, Show, Functor, GHC.Generic)+ deriving (Eq, Ord, Show, Functor, Foldable, Traversable, GHC.Generic) instance Applicative Thunk where pure = Eval
src/Test/StrictCheck/Internal/Inputs.hs view
@@ -19,7 +19,6 @@ ) where import Test.QuickCheck (Gen)-import Data.Semigroup --------------------------------------------------
src/Test/StrictCheck/Observe.hs view
@@ -11,18 +11,23 @@ ( observe1 , observe , observeNP+ , instrument+ , entangle ) where import Data.Bifunctor import Data.Functor.Product+import Data.Functor.Compose+import Data.IORef+import System.IO.Unsafe (unsafePerformIO, unsafeInterleaveIO) -import Generics.SOP hiding (Shape)+import Generics.SOP hiding (Shape, Compose) import Test.StrictCheck.Curry hiding (curry, uncurry) import Test.StrictCheck.Shaped-import Test.StrictCheck.Observe.Unsafe import Test.StrictCheck.Demand + ------------------------------------------------------ -- Observing demand behavior of arbitrary functions -- ------------------------------------------------------@@ -50,18 +55,28 @@ -- This tells us that our context did indeed evaluate the result of @reverse@ -- to force only its first constructor, and that doing so required the entire -- spine of the list to be evaluated, but did not evaluate any of its elements.-{-# NOINLINE observe1 #-}+ observe1 :: (Shaped a, Shaped b) => (b -> ()) -> (a -> b) -> a -> (Demand b, Demand a) observe1 context function input =- let (input', inputD) =- entangleShape input -- (1)- (result', resultD) =- entangleShape (function input') -- (2)- in let !_ = context result' -- (3)- in (resultD, inputD) -- (4)+ -- Using unsafePerformIO here and in observeNP is safe, as the result of the+ -- IO action only depends on it's inputs and has no side-effects.+ unsafePerformIO $ do + -- The numbered lines correspond to the NOTE below+ (input', inputD) <- instrument input -- (1)+ (result', resultD) <- instrument (function input') -- (2)+ let !_ = context result' -- (3)+ (,) <$> resultD <*> inputD -- (4)++ -- NOTE: The observation function:+ -- (1) instruments the input+ -- (2) instruments the result of the function applied to the input+ -- (3) evaluates the instrumented result of the function in the context, and+ -- (4) returns the observed demands on the result and the input.++ -- | Observe the demand behavior -- -- * in a given evaluation context@@ -77,7 +92,7 @@ -- -- This is mostly useful for implementing the internals of StrictCheck; -- 'observe' is more ergonomic for exploration by end-users.-{-# NOINLINE observeNP #-}+ observeNP :: (All Shaped inputs, Shaped result) => (result -> ())@@ -86,18 +101,32 @@ -> ( Demand result , NP Demand inputs ) observeNP context function inputs =- let entangled =- hcliftA- (Proxy @Shaped)- (uncurry Pair . first I . entangleShape . unI)- inputs- (inputs', inputsD) =- (hliftA (\(Pair r _) -> r) entangled,- hliftA (\(Pair _ l) -> l) entangled)- (result', resultD) = entangleShape (function inputs')- in let !_ = context result'- in (resultD, inputsD)+ -- NOTE: See the comment in observe1 about the safety of unsafePerformIO here.+ unsafePerformIO $ do+ -- This function works identically to observe1, except it has more+ -- line-noise to shuffle around newtypes and traverse heterogeneous lists.+ -- To see this, compare the numbered comments below to their corresponding+ -- line labels in observe1. + -- (1) instrument the inputs+ entangled <-+ hctraverse'+ (Proxy @Shaped)+ (fmap (uncurry Pair . bimap I Compose) . instrument . unI)+ inputs+ let inputs' = hliftA (\(Pair r _) -> r ) entangled+ let inputsD = htraverse' (\(Pair _ l) -> getCompose l) entangled++ -- (2) instrument the result of the function on the instrumented inputs+ (result', resultD) <- instrument (function inputs')++ -- (3) evaluate the instrumented result of the function in the context+ let !_ = context result'++ -- (4) return the resultant observed demands+ (,) <$> resultD <*> inputsD++ -- | Observe the demand behavior -- -- * in a given evaluation context@@ -127,6 +156,7 @@ -- -- If you haven't thought very carefully about the strictness behavior of @zip@, -- this may be a surprising result; this is part of the fun!+ observe :: ( All Shaped (Args function) , Shaped (Result function)@@ -139,4 +169,82 @@ observe context function = curryAll (observeNP context (uncurryAll function)) --- NOTE: We don't need a NOINLINE annotation here because this wraps observeNP.++--------------------------------------------------------+-- Instrumenting values to determine their evaluation --+--------------------------------------------------------++-- | Creates a tuple of an instrumented thunk, and an @IO@ action whose return+-- value indicates whether that thunk has yet been evaluated.+--+-- >>> (x, d) <- entangle ()+-- >>> d+-- Thunk+-- >>> x+-- ()+-- >>> d+-- Eval ()++entangle :: a -> IO (a, IO (Thunk a))+entangle a = do+ ref <- newIORef Thunk+ -- Using unsafePerformIO here is safe, i.e. it is referentially transparent,+ -- because the only handle to the mutated IORef is closed over by the IO+ -- action returned as the second element of the resultant tuple, which means+ -- the effect of the unsafePerformIO can only be observed from within IO.+ return (unsafePerformIO $ do+ writeIORef ref (Eval a)+ return a,+ readIORef ref)++-- | Recursively instruments a value, returning a tuple of an instrumented+-- value, and an @IO@ action returning a 'Demand' that corresponds to the+-- portion of the instrumented value which has already been evaluated at the+-- time the action was run.+--+-- >>> (x, d) <- instrument [1..]+-- >>> printDemand =<< d+-- _+-- >>> length . take 3 $ x+-- 3+-- >>> printDemand =<< d+-- _ : _ : _ : _+-- >>> take 2 x+-- [1,2]+-- >>> printDemand =<< d+-- 1 : 2 : _ : _++-- NOTE: There are a two properties we care about:+--+-- 1. Running the Demand action should always result in a consistent state+-- and not depend on when its result is forced.+-- 2. We want to evaluate the input as little as possible. Importantly+-- running the demand action should never force any previously unforced+-- parts of the input. This is especially important for the observe+-- functions and the hardest thing to get right.++instrument :: forall a. Shaped a => a -> IO (a, IO (Demand a))+instrument a = do+ -- We need to use unsafeInterleaveIO here so that we do not force the value+ -- by matching on it when we bind the result above to entangle it.+ --+ -- Using unsafeInterleaveIO here is safe, as the result of the IO action does+ -- not depend on when it is performed and it doesn't matter if it is never+ -- performed, if it's value is not forced.+ (~(~(a', _), da)) <- entangle =<< unsafeInterleaveIO entangledFields+ return (a', Wrap <$> (traverse snd =<< da))+ where+ -- The to-be-entangled value with all its recursive children entangled+ -- We still need to entangle the value itself+ entangledFields :: IO (a, IO (Shape a Demand))+ entangledFields = do+ entangled <- translateA (pairWithDemand . unI) (project I a)+ let a' = embed unI (translate (I . demanded) entangled)+ return (a', translateA getDemand entangled)++ pairWithDemand :: forall x. Shaped x => x -> IO (WithDemand x)+ pairWithDemand = fmap (uncurry WithDemand) . instrument++-- Auxiliary functor for the traversal in 'instrument'+data WithDemand a+ = WithDemand { demanded :: a, getDemand :: IO (Demand a) }
− src/Test/StrictCheck/Observe/Unsafe.hs
@@ -1,76 +0,0 @@-{-| This module defines the underlying __unsafe__ primitives StrictCheck uses- to implement purely functional observation of evaluation.-- The "functions" in this module are __not referentially transparent__!--}-module Test.StrictCheck.Observe.Unsafe where--import System.IO.Unsafe-import Data.IORef--import Data.Bifunctor-import Generics.SOP (I(..), unI)--import Test.StrictCheck.Shaped-import Test.StrictCheck.Demand---- | From some value of any type, produce a pair: a copy of the original value,--- and a 'Thunk' of that same type, with their values determined by the--- /order/ in which their values themselves are evaluated------ If the copy of the value is evaluated to weak-head normal form before the--- returned @Thunk@, then any future inspection of the @Thunk@ will show that it--- is equal to the original value wrapped in an @Eval@. However, if the copy of--- the value is /not/ evaluated by the time the @Thunk@ is evaluated, any future--- inspection of the @Thunk@ will show that it is equal to @Thunk@.------ A picture may be worth 1000 words:------ >>> x = "hello," ++ " world"--- >>> (x', t) = entangle x--- >>> x'--- "hello, world"--- >>> t--- Eval "hello, world"------ >>> x = "hello," ++ " world"--- >>> (x', t) = entangle x--- >>> t--- Thunk--- >>> x'--- "hello, world"--- >>> t--- Thunk-{-# NOINLINE entangle #-}-entangle :: forall a. a -> (a, Thunk a)-entangle a =- unsafePerformIO $ do- ref <- newIORef Thunk- return ( unsafePerformIO $ do- writeIORef ref (Eval a)- return a- , unsafePerformIO $ readIORef ref )---- | Recursively 'entangle' an @a@, producing not merely a @Thunk@, but an--- entire @Demand@ which is piecewise entangled with that value. Whatever--- portion of the entangled value is evaluated before the corresponding portion--- of the returned @Demand@ will be represented in the shape of that @Demand@.--- However, any part of the returned @Demand@ which is evaluated before the--- corresponding portion of the entangled value will be forever equal to--- @Thunk@.------ The behavior of this function is even more tricky to predict than that of--- 'entangle', especially when evaluation of the entangled value and the--- corresponding @Demand@ happen at the same time. In StrictCheck, all--- evaluation of the entangled value occurs before any evaluation of the--- @Demand@; we never interleave their evaluation.-{-# NOINLINE entangleShape #-}-entangleShape :: Shaped a => a -> (a, Demand a)-entangleShape =- first (fuse unI)- . unzipWith entangle'- . interleave I- where- entangle' :: I x -> (I x, Thunk x)- entangle' =- first I . entangle . unI
src/Test/StrictCheck/Shaped.hs view
@@ -1,4 +1,4 @@-{-# language InstanceSigs, DerivingStrategies #-}+{-# language InstanceSigs, DerivingStrategies, TypeFamilyDependencies #-} {-# language PartialTypeSignatures #-} {-# OPTIONS_GHC -fno-warn-partial-type-signatures #-} {-| This module defines the 'Shaped' typeclass, which is used to generically@@ -50,9 +50,13 @@ , (%) , fuse , translate+ , translateA , fold+ , foldM , unfold+ , unfoldM , unzipWith+ , unzipWithM -- , reshape -- * Rendering 'Shaped' things as structured text , QName@@ -86,6 +90,7 @@ import Data.Bifunctor import Data.Bifunctor.Flip import Data.Coerce+import Control.Monad hiding (foldM) import Generics.SOP hiding ( Shape ) @@ -121,7 +126,7 @@ -- | The @Shape@ of an @a@ is a type isomorphic to the outermost level of -- structure in an @a@, parameterized by the functor @f@, which is wrapped -- around any fields (of any type) in the original @a@.- type Shape a :: (* -> *) -> *+ type Shape a = (result :: (* -> *) -> *) | result -> a type Shape a = GShape a -- | Given a function to expand any @Shaped@ @x@ into an @f x@, expand an @a@@@ -231,9 +236,17 @@ translate :: forall a f g. Shaped a => (forall x. Shaped x => f x -> g x) -> Shape a f -> Shape a g-translate t d = match @a d d $ \flat _ ->+translate t d = match d d $ \flat _ -> unflatten $ mapFlattened @Shaped t flat +-- | The 'Applicative' version of 'translate'; maps an effectful translation+-- over a given @Shape@.+translateA :: forall a c f g. (Shaped a, Applicative c)+ => (forall x. Shaped x => f x -> c (g x))+ -> Shape a f -> c (Shape a g)+translateA t d = match d d $ \flat _ ->+ unflatten <$> traverseFlattened @Shaped t flat+ -- | The equivalent of a fold (catamorphism) over recursively 'Shaped' values -- -- Given a function which folds an @f@ containing some @Shape x g@ into a @g x@,@@ -241,8 +254,14 @@ fold :: forall a f g. (Functor f, Shaped a) => (forall x. Shaped x => f (Shape x g) -> g x) -> f % a -> g a-fold alg = alg . fmap (translate @a (fold alg)) . unwrap+fold alg = alg . fmap (translate (fold alg)) . unwrap +-- | The 'Monad' version of 'fold'; folds an interleaved structure effectfully.+foldM :: forall a m f g. (Traversable f, Shaped a, Monad m)+ => (forall x. Shaped x => f (Shape x g) -> m (g x))+ -> f % a -> m (g a)+foldM alg = alg <=< traverse (translateA (foldM alg)) . unwrap+ -- | The equivalent of an unfold (anamorphism) over recursively 'Shaped' values -- -- Given a function which unfolds an @f x@ into a @g@ containing some @Shape x@@ -250,9 +269,14 @@ unfold :: forall a f g. (Functor g, Shaped a) => (forall x. Shaped x => f x -> g (Shape x f)) -> f a -> g % a-unfold coalg = Wrap . fmap (translate @a (unfold coalg)) . coalg+unfold coalg = Wrap . fmap (translate (unfold coalg)) . coalg --- TODO: mapM, foldM, unfoldM, ...+-- | The 'Monad' version of 'unfold'; unfolds an interleaved structure+-- effectfully.+unfoldM :: forall a m f g. (Traversable g, Shaped a, Monad m)+ => (forall x. Shaped x => f x -> m (g (Shape x f)))+ -> f a -> m (g % a)+unfoldM coalg = fmap Wrap . traverse (translateA (unfoldM coalg)) <=< coalg -- | Fuse the interleaved @f@-structure out of a recursively interleaved @f % -- a@, given some way of fusing a single level @f x -> x@.@@ -282,36 +306,46 @@ -- | A higher-kinded @unzipWith@, operating over interleaved structures ----- Given a function splitting some @f x@ into a functor-product @Product g h x@,--- recursively split an interleaved @f % a@ into two interleaved structures:--- one built of @g@-shapes and one of @h@-shapes.------ Note that @Product ((%) g) ((%) h) a@ is isomorphic to @(g % a, h % a)@; to--- get the latter, pattern-match on the 'Pair' constructor of 'Product'.+-- Given a function splitting some @f x@ into a @g x@ and a @h x@, unzip and+-- entire @f % a@ structure using this operation, yielding a @g % a@ and a+-- @h % a@. unzipWith :: (All Functor [f, g, h], Shaped a)- => (forall x. f x -> (g x, h x))+ => (forall x sx. sx ~ (Shape x ((%) g), Shape x ((%) h))+ => f sx -> (g sx, h sx)) -> (f % a -> (g % a, h % a)) unzipWith split =- unPair . fold (crunch . pair . split)- where- crunch- :: forall x g h.- (Shaped x, Functor g, Functor h)- => Product g h (Shape x (Product ((%) g) ((%) h)))- -> Product ((%) g) ((%) h) x- crunch =- pair- . bimap (Wrap . fmap (translate @x (fst . unPair)))- (Wrap . fmap (translate @x (snd . unPair)))- . unPair+ unPair . fold (pair . bimap (Wrap . fmap fst) (Wrap . fmap snd)+ . split+ . fmap crunch) - pair :: (l x, r x) -> Product l r x- pair = uncurry Pair+-- | The monadic equivalent of @unzipWith@; effectfully unzips an interleaved+-- structure+unzipWithM+ :: (Traversable f, All Functor [g, h], Shaped a, Monad m)+ => (forall x sx. sx ~ (Shape x ((%) g), Shape x ((%) h))+ => f sx -> m (g sx, h sx))+ -> (f % a -> m (g % a, h % a))+unzipWithM split =+ fmap unPair . foldM (fmap (pair . bimap (Wrap . fmap fst) (Wrap . fmap snd))+ . split+ . fmap crunch) - unPair :: Product l r x -> (l x, r x)- unPair (Pair lx rx) = (lx, rx)+-- Some helpers for zipping and unzipping... +crunch+ :: forall x g h. Shaped x+ => Shape x (Product ((%) g) ((%) h))+ -> (Shape x ((%) g), Shape x ((%) h))+crunch x =+ (translate (fst . unPair) $ x, translate (snd . unPair) $ x)++pair :: (l x, r x) -> Product l r x+pair = uncurry Pair++unPair :: Product l r x -> (l x, r x)+unPair (Pair lx rx) = (lx, rx)+ -- | TODO: document this strange function {- reshape :: forall b a f g. (Shaped a, Shaped b, Functor f)@@ -323,7 +357,7 @@ Nothing -> hetero d Just HRefl -> Wrap- $ homo . fmap (translate @a (reshape @b homo hetero))+ $ homo . fmap (translate (reshape homo hetero)) $ unwrap d -} @@ -341,7 +375,7 @@ oneLevel :: forall x. Shaped x => f (Shape x (K (Rendered f))) -> K (Rendered f) x- oneLevel = K . RWrap . fmap (render @x)+ oneLevel = K . RWrap . fmap render -- | A @QName@ is a qualified name --
src/Test/StrictCheck/Shaped/Flattened.hs view
@@ -44,8 +44,14 @@ -- | If all the fields in a @Flattened@ satisfy some constraint, map a function -- expecting that constraint across all the fields. This may change the functor--- over which the @Flattened@ value is paramaterized.+-- over which the @Flattened@ value is parameterized. mapFlattened :: forall c d f g xs. All c xs => (forall x. c x => f x -> g x) -> Flattened d f xs -> Flattened d g xs mapFlattened t (Flattened u p) = Flattened u (hcliftA (Proxy @c) t p)++-- | 'traverseFlattened' is to 'traverse' like 'mapFlattened' is to 'fmap'.+traverseFlattened :: forall c d f g h xs. (All c xs, Applicative h)+ => (forall x. c x => f x -> h (g x)) -> Flattened d f xs -> h (Flattened d g xs)+traverseFlattened t (Flattened u p) =+ Flattened u <$> hctraverse' (Proxy @c) t p
+ tests/Entangle.hs view
@@ -0,0 +1,36 @@+module Entangle where++import Test.HUnit+import Test.StrictCheck+import Data.Bifunctor (bimap)++spec :: IO ()+spec = do+ putStrLn "Checking instrument"+ _ <- runTestTT . test $ do+ (x , d ) <- fmap (fmap prettyDemand) <$> instrument ()+ (x', d') <- fmap (fmap prettyDemand) <$> instrument x+ d1 <- d+ d1 @=? "_"+ d'1 <- d'+ d'1 @=? "_"+ d2 <- d+ d2 @=? "_"+ let !_ = x+ d3 <- d+ d3 @=? "()"+ d'2 <- d'+ d'2 @=? "_"+ let !_ = x'+ d'3 <- d'+ d'3 @=? "()"++ putStrLn "Checking observe"+ _ <- runTestTT . test $ do+ let strict = bimap prettyDemand prettyDemand (observe1 id (\() -> ()) ())+ let lazy = bimap prettyDemand prettyDemand (observe1 id (\_ -> ()) ())++ strict @=? ("()", "()")+ lazy @=? ("()", "_")++ pure ()
tests/Tests.hs view
@@ -2,6 +2,7 @@ import Specs import RefTrans+import qualified Entangle main :: IO () main = do@@ -9,3 +10,4 @@ runSpecs -- regression test for issue #2 (CSE breaks referential transparency) checkRefTrans+ Entangle.spec