{- |
Module      : Test.Tasty.QuickCheck.Laws.WriterMonad
Description : Prefab tasty trees of quickcheck properties for writer monad laws
Copyright   : 2018, Automattic, Inc.
License     : GPL-3
Maintainer  : Nathan Bloomfield (nbloomf@gmail.com)
Stability   : experimental
Portability : POSIX

This module uses an alternative set of primitives for writer monads which are roughly equivalent in power to the standard @tell@, @listen@, and @pass@, but satisfy tidier properties and have a nice intuitive interpretation. We keep @tell :: w -> m ()@ and replace @listen@ and @pass@ with @draft :: (Monoid w) => m a -> m (a,w)@, which is similar to @listen@ but 'resets' the writer value to @mempty@. Intuitively, @draft@ returns what it /would/ have written, but doesn't actually write it. It is also satisfying that @draft@ uses the monoid constraint on @w@, while @tell@ and @pass@ do not.

Since we are using nonstandard primitives, we also provide an extra tree of tests for checking the equivalence of the two sets.
-}



{-# LANGUAGE Rank2Types #-}
module Test.Tasty.QuickCheck.Laws.WriterMonad (
    testWriterMonadLaws

  -- * Writer Monad Laws
  , testWriterMonadLawDraftTell
  , testWriterMonadLawTellMempty
  , testWriterMonadLawTellMappend
  , testWriterMonadLawDraftReturn
  , testWriterMonadLawDraftBind

  -- * Alternate Primitives
  , testWriterMonadEquivalences
  , testWriterMonadEquivalenceListen
  , testWriterMonadEquivalencePass
  , testWriterMonadEquivalenceDraft
) where



import Data.Proxy
  ( Proxy(..) )
import Data.Typeable
  ( Typeable, typeRep )
import Test.Tasty
  ( TestTree, testGroup )
import Test.Tasty.QuickCheck
  ( testProperty, Property, Arbitrary(..), CoArbitrary(..) )
import Text.Show.Functions
  ()

import Test.Tasty.QuickCheck.Laws.Class


-- | Constructs a @TestTree@ checking that the writer monad laws hold for @m@ with writer type @w@ and value types @a@ and @b@, using a given equality test for values of type @forall u. m u@. The equality context type @t@ is for constructors @m@ from which we can only extract a value within a context, such as reader-like constructors.
-- 
-- We use a slightly different set of primitives for the writer laws; rather than @tell@, @listen@, and @pass@, we use @tell@ and @draft :: (Monoid w) => m a -> m (a,w)@, which is similar to @listen@ but 'resets' the writer value to @mempty@.
testWriterMonadLaws
  :: ( Monoid w, Monad m
     , Eq w, Eq a, Eq b
     , Show t, Show w, Show a
     , Show (m a)
     , Arbitrary t, Arbitrary w, Arbitrary a
     , Arbitrary (m a), Arbitrary (m b)
     , CoArbitrary a
     , Typeable m, Typeable w, Typeable a
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> Proxy a -- ^ Value type
  -> Proxy b -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> TestTree
testWriterMonadLaws pm pt pw pa pb eq tell draft =
  let
    label = "Writer Monad Laws for " ++ (show $ typeRep pm) ++ " with " ++
      "w :: " ++ (show $ typeRep pw) ++ ", " ++
      "a :: " ++ (show $ typeRep pa)
  in
    testGroup label
      [ testWriterMonadLawDraftTell pm pt pw eq tell draft
      , testWriterMonadLawTellMempty pm pt pw eq tell
      , testWriterMonadLawTellMappend pm pt pw eq tell
      , testWriterMonadLawDraftReturn pm pt pw pa eq draft
      , testWriterMonadLawDraftBind pm pt pw pa pb eq draft
      ]



-- | @draft (tell w) === return ((),w)@
testWriterMonadLawDraftTell
  :: ( Monad m
     , Eq w
     , Show t, Show w
     , Arbitrary t, Arbitrary w
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> TestTree
testWriterMonadLawDraftTell pm pt pw eq tell draft =
  testProperty "draft (tell w) === tell w >> return ((),w)" $
    writerMonadLawDraftTell pm pt pw eq tell draft

writerMonadLawDraftTell
  :: (Monad m, Eq w)
  => Proxy m -> Proxy t -> Proxy w
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ tell
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> t -> w -> Bool
writerMonadLawDraftTell _ _ _ eq tell draft t w =
  (eq t) (draft (tell w)) (return ((),w))



-- | @tell mempty === return ()@
testWriterMonadLawTellMempty
  :: ( Monoid w, Monad m
     , Show t
     , Arbitrary t
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> TestTree
testWriterMonadLawTellMempty pm pt pw eq tell =
  testProperty "tell mempty === return ()" $
    writerMonadLawTellMempty pm pt pw eq tell

writerMonadLawTellMempty
  :: (Monoid w, Monad m)
  => Proxy m -> Proxy t -> Proxy w
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ tell
  -> t -> Bool
writerMonadLawTellMempty _ _ _ eq tell t =
  (eq t) (tell mempty) (return ())



-- | @tell w1 >> tell w2 === tell (mappend w1 w2)@
testWriterMonadLawTellMappend
  :: ( Monoid w, Monad m
     , Show t, Show w
     , Arbitrary t, Arbitrary w
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> TestTree
testWriterMonadLawTellMappend pm pt pw eq tell =
  testProperty "tell mempty === return ()" $
    writerMonadLawTellMappend pm pt pw eq tell

writerMonadLawTellMappend
  :: (Monoid w, Monad m)
  => Proxy m -> Proxy t -> Proxy w
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ tell
  -> t -> w -> w -> Bool
writerMonadLawTellMappend _ _ _ eq tell t w1 w2 =
  (eq t) (tell w1 >> tell w2) (tell (mappend w1 w2))



-- | @draft (return a) === return a@
testWriterMonadLawDraftReturn
  :: ( Monoid w, Monad m
     , Eq w, Eq a
     , Show t, Show a
     , Arbitrary t, Arbitrary a
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> Proxy a -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> TestTree
testWriterMonadLawDraftReturn pm pt pw pa eq draft =
  testProperty "draft (return a) === return a" $
    writerMonadLawDraftReturn pm pt pw pa eq draft

writerMonadLawDraftReturn
  :: (Monoid w, Monad m, Eq a, Eq w)
  => Proxy m -> Proxy t -> Proxy w -> Proxy a
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> t -> a -> Bool
writerMonadLawDraftReturn _ _ _ _ eq draft t a =
  (eq t) (draft (return a)) (return (a, mempty))



-- | @draft (x >>= f) === draft x >>= (draft' f)@ where @draft' f (a,w) = mapsnd (mappend w) <$> draft (f a)@
testWriterMonadLawDraftBind
  :: ( Monoid w, Monad m
     , Eq w, Eq b
     , Show t
     , Show (m a)
     , Arbitrary t
     , Arbitrary (m a), Arbitrary (m b)
     , CoArbitrary a
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> Proxy a -- ^ Value type
  -> Proxy b -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> TestTree
testWriterMonadLawDraftBind pm pt pw pa pb eq draft =
  testProperty "draft (x >>= f) === draft x >>= draft' f" $
    writerMonadLawDraftBind pm pt pw pa pb eq draft

writerMonadLawDraftBind
  :: (Monoid w, Monad m, Eq w, Eq b)
  => Proxy m -> Proxy t -> Proxy w -> Proxy a -> Proxy b
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> t -> m a -> (a -> m b) -> Bool
writerMonadLawDraftBind _ _ _ _ _ eq draft t x f =
  let mapsnd g (u,v) = (u, g v) in
  let draft' g (b,v) = mapsnd (mappend v) <$> draft (g b) in
  (eq t) (draft (x >>= f)) (draft x >>= draft' f)



-- | As far as I can tell there isn't an agreed upon set of axioms for the standard writer monad primitives, so the best we can do is show that @tell@+@draft@ is equivalent in power to @tell@+@listen@+@pass@ for the only concrete writer monad we do have: the tuple monad.
--
-- Along with the property tests (for arbitrary writer monads) we'll demonstrate these equivalences (for the tuple monad) with some equational proofs, and for this purpose we need some concrete definitions.
--
-- > data Writer w a = Writer { runWriter :: (a,w) }
--
-- Note that @Writer@ and @runWriter@ are mutual inverses.
--
-- > instance (Monoid w) => Monad (Writer w) where
-- >   return a = Writer (a, mempty)
-- >
-- >   (Writer (a,w)) >>= f =
-- >     let (b,w2) = runWriter (f a)
-- >     in Writer (b, mappend w w2)
-- >
-- > tell :: w -> Writer w ()
-- > tell w = Writer ((),w)
-- >
-- > draft :: (Monoid w) => Writer w a -> Writer w (a,w)
-- > draft (Writer (a,w)) = Writer ((a,w),mempty)
-- >
-- > listen :: Writer w a -> Writer w (a,w)
-- > listen (Writer (a,w)) = Writer ((a,w),w)
-- >
-- > pass :: Writer w (a, w -> w) -> Writer w a
-- > pass u =
-- >   let ((a,f),w) = runWriter u
-- >   in Writer (a, f w)
testWriterMonadEquivalences
  :: ( Monoid w, Monad m
     , Eq w, Eq a
     , Show t
     , Show (m a), Show (m (a, w -> w))
     , Arbitrary t
     , Arbitrary (m a), Arbitrary (m (a, w -> w))
     , Typeable m, Typeable w, Typeable a
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer monad
  -> Proxy a -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> (forall u. m u -> m (u,w)) -- ^ @listen@
  -> (forall u. m (u, w -> w) -> m u) -- ^ @pass@
  -> TestTree
testWriterMonadEquivalences pm pt pw pa eq tell draft listen pass =
  let
    label = "Writer Monad Equivalence for " ++ (show $ typeRep pm) ++ " with " ++
      "w :: " ++ (show $ typeRep pw) ++ ", " ++
      "a :: " ++ (show $ typeRep pa)
  in
    testGroup label
      [ testWriterMonadEquivalenceListen pm pt pw pa eq tell draft listen
      , testWriterMonadEquivalencePass pm pt pw pa eq tell draft pass
      , testWriterMonadEquivalenceDraft pm pt pw pa eq draft listen pass
      ]



-- | @listen x === do (a,w) <- draft x; tell w; return (a,w)@
-- 
-- Suppose @x === Writer (a,w)@. Then we have:
--
-- >     do (a,w) <- draft x; tell w; return (a,w)
-- > ===   (desugar do-notation)
-- >     draft x >>= \(a',w') -> tell w' >> return (a',w')
-- > ===   (substitute x)
-- >     draft (Writer (a,w)) >>= \(a',w') -> tell w' >> return (a',w')
-- > ===   (definition of draft)
-- >     Writer ((a,w), mempty) >>= \(a',w') -> tell w' >> return (a',w')
-- > ===   (definition of >>=)
-- >     let (b,w2) = runWriter (tell w >> return (a,w))
-- >     in Writer (b, mappend mempty w2)
-- > ===   (monoid identity law)
-- >     let (b,w2) = runWriter (tell w >> return (a,w))
-- >     in Writer (b, w2)
-- > ===   (let substitution)
-- >     Writer $ runWriter (tell w >> return (a,w))
-- > ===   (constructor/destructor)
-- >     tell w >> return (a,w)
-- > ===   (definition of tell and >>)
-- >     Writer ((),w) >>= \_ -> return (a,w)
-- > ===   (definition of >>=)
-- >     let (b,w2) = runWriter (return (a,w))
-- >     in Writer (b, mappend w w2)
-- > ===   (definition of return)
-- >     let (b,w2) = runWriter (Writer ((a,w),mempty))
-- >     in Writer (b, mappend w w2)
-- > ===   (constructor/destructor)
-- >     let (b,w2) = ((a,w),mempty)
-- >     in Writer (b, mappend w w2)
-- > ===   (let substitution)
-- >     Writer ((a,w), mappend w mempty)
-- > ===   (monoid identity law)
-- >     Writer ((a,w), w)
-- > ===   (definition of listen)
-- >     listen (Writer (a,w))
-- > ===   (substitute x)
-- >     listen x
testWriterMonadEquivalenceListen
  :: ( Monad m
     , Eq w, Eq a
     , Show t
     , Show (m a)
     , Arbitrary t
     , Arbitrary (m a)
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer monad
  -> Proxy a -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> (forall u. m u -> m (u,w)) -- ^ @listen@
  -> TestTree
testWriterMonadEquivalenceListen pm pt pw pa eq tell draft listen =
  testProperty "listen x === do (a,w) <- draft x; tell w; return (a,w)" $
    writerMonadEquivalenceListen pm pt pw pa eq tell draft listen

writerMonadEquivalenceListen
  :: (Monad m, Eq a, Eq w)
  => Proxy m -> Proxy t -> Proxy w -> Proxy a
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ tell
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> (forall u. m u -> m (u,w)) -- ^ listen
  -> t -> m a -> Bool
writerMonadEquivalenceListen _ _ _ _ eq tell draft listen t x =
  (eq t) (listen x) (do (a,w) <- draft x; tell w; return (a,w))



-- | @pass u === do ((a,f),w) <- draft u; tell (f w); return a@
--
-- Suppose @u = Writer ((a,f),w)@. Then we have:
--
-- >     do ((a,f),w) <- draft u; tell (f w); return a
-- > ===   (desugar do notation)
-- >     draft u >>= \((a',f'),w') -> tell (f' w') >> return a'
-- > ===   (substitute u)
-- >     Writer (((a,f),w),mempty) >>= \((a',f'),w') -> tell (f' w') >> return a'
-- > ===   (definition of >>=)
-- >     let (b,w2) = runWriter (tell (f w) >> return a)
-- >     in Writer (b, mappend mempty w2)
-- > ===   (monoid identity law)
-- >     let (b,w2) = runWriter (tell (f w) >> return a)
-- >     in Writer (b,w2)
-- > ===   (let substitution)
-- >     Writer $ runWriter (tell (f w) >> return a)
-- > ===   (constructor/destructor)
-- >     tell (f w) >> return a
-- > ===   (definition of tell, >>, and return)
-- >     Writer ((), f w) >>= \_ -> Writer (a, mempty)
-- > ===   (definition of >>=)
-- >     let (b,w2) = runWriter (Writer (a,mempty))
-- >     in Writer (b, mappend (f w) w2)
-- > ===   (constructor/destructor)
-- >     let (b,w2) = (a,mempty)
-- >     in Writer (b, mappend (f w) w2)
-- > ===   (let substitution)
-- >     Writer (a, mappend (f w) mempty)
-- > ===   (monoid identity law)
-- >     Writer (a, f w)
-- > ===   (definition of pass)
-- >     pass (Writer (a,f) w)
-- > ===   (substitute u)
-- >     pass u
testWriterMonadEquivalencePass
  :: ( Monad m
     , Eq w, Eq a
     , Show t
     , Show (m (a, w -> w))
     , Arbitrary t
     , Arbitrary (m (a, w -> w))
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> Proxy a -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ @tell@
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> (forall u. m (a, w -> w) -> m a) -- ^ @pass@
  -> TestTree
testWriterMonadEquivalencePass pm pt pw pa eq tell draft pass =
  testProperty "pass u === do ((a,f),w) <- draft u; tell (f w); return a" $
    writerMonadEquivalencePass pm pt pw pa eq tell draft pass

writerMonadEquivalencePass
  :: (Monad m, Eq a, Eq w)
  => Proxy m -> Proxy t -> Proxy w -> Proxy a
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (w -> m ()) -- ^ tell
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> (forall u. m (a, w -> w) -> m a) -- ^ pass
  -> t -> m (a, w -> w) -> Bool
writerMonadEquivalencePass _ _ _ _ eq tell draft pass t u =
  (eq t) (pass u) (do ((a,f),w) <- draft u; tell (f w); return a)



-- | @draft x === pass $ do (a,w) <- listen x; return ((a,w), const mempty)@
--
-- Suppose @x = Writer (a,w)@. Then we have:
--
-- >     pass $ do (a,w) <- listen x; return ((a,w), const mempty)
-- > ===   (desugar do notation)
-- >     pass $ listen x >>= \(a',w') -> return ((a',w'), const mempty)
-- > ===   (substitute x)
-- >     pass $ listen (Writer (a,w)) >>= \(a',w') -> return ((a',w'), const mempty)
-- > ===   (definition of listen)
-- >     pass $ Writer ((a,w),w) >>= \(a',w') -> return ((a',w'), const mempty)
-- > ===   (definition of >>=)
-- >     pass $ let (b,w2) = runWriter (return ((a,w), const mempty))
-- >            in Writer (b, mappend w w2)
-- > ===   (definition of return)
-- >     pass $ let (b,w2) = runWriter (Writer (((a,w), const mempty), mempty))
-- >            in Writer (b, mappend w w2)
-- > ===   (constructor/destructor)
-- >     pass $ let (b,w2) = (((a,w), const mempty), mempty)
-- >            in Writer (b, mappend w w2)
-- > ===   (let substitution)
-- >     pass $ Writer (((a,w), const mempty), mappend w mempty)
-- > ===   (monoid identity law)
-- >     pass $ Writer (((a,w), const mempty), w)
-- > ===   (definition of pass)
-- >     Writer ((a,w), const mempty w)
-- > ===   (definition of const)
-- >     Writer ((a,w), mempty)
-- > ===   (definition of draft)
-- >     draft (Writer (a,w))
-- > ===   (substitute x)
-- >     draft x
testWriterMonadEquivalenceDraft
  :: ( Monoid w, Monad m
     , Eq w, Eq a
     , Show t
     , Show (m a)
     , Arbitrary t
     , Arbitrary (m a)
     )
  => Proxy m -- ^ Type constructor under test
  -> Proxy t -- ^ Equality context for @m@
  -> Proxy w -- ^ Writer type
  -> Proxy a -- ^ Value type
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ @draft@
  -> (forall u. m u -> m (u,w)) -- ^ @listen@
  -> (forall u. m (u, w -> w) -> m u) -- ^ @pass@
  -> TestTree
testWriterMonadEquivalenceDraft pm pt pw pa eq draft listen pass =
  testProperty "draft x === pass $ do (a,w) <- listen x; return ((a,w), const mempty)" $
    writerMonadEquivalenceDraft pm pt pw pa eq draft listen pass

writerMonadEquivalenceDraft
  :: (Monoid w, Monad m, Eq a, Eq w)
  => Proxy m -> Proxy t -> Proxy w -> Proxy a
  -> (forall u. (Eq u) => t -> m u -> m u -> Bool) -- ^ Equality test
  -> (forall u. m u -> m (u,w)) -- ^ draft
  -> (forall u. m u -> m (u,w)) -- ^ listen
  -> (forall u. m (u, w -> w) -> m u) -- ^ pass
  -> t -> m a -> Bool
writerMonadEquivalenceDraft _ _ _ _ eq draft listen pass t x =
  (eq t) (draft x) (pass $ do (a,w) <- listen x; return ((a,w), const mempty))