Add lazy interpretations for Output and Writer (#311)

* Add lazy interpretations for Output and Writer

* INLINE the helper function of interpretViaLazyWriter

* Added strictness tests for runLazyWriter
This commit is contained in:
KingoftheHomeless 2020-02-13 21:20:24 +01:00 committed by GitHub
parent 6f9f4541f5
commit 8d49b677fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 1 deletions

View File

@ -5,12 +5,17 @@ module Polysemy.Internal.Writer where
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import qualified Control.Monad.Trans.Writer.Lazy as Lazy
import Data.Bifunctor (first)
import Data.Semigroup
import Polysemy
import Polysemy.Final
import Polysemy.Internal
import Polysemy.Internal.Union
------------------------------------------------------------------------------
-- | An effect capable of emitting and intercepting messages.
@ -156,3 +161,32 @@ runWriterSTMAction write = interpretH $ \case
writeTVar switch True
return o'
{-# INLINE runWriterSTMAction #-}
-- TODO (KingoftheHomeless):
-- Benchmark to see if switching to a more flexible variant
-- would incur a performance loss
interpretViaLazyWriter
:: forall o e r a
. Monoid o
=> (forall m x. Monad m => Weaving e (Lazy.WriterT o m) x -> Lazy.WriterT o m x)
-> Sem (e ': r) a
-> Sem r (o, a)
interpretViaLazyWriter f sem = Sem $ \(k :: forall x. Union r (Sem r) x -> m x) ->
let
go :: forall x. Sem (e ': r) x -> Lazy.WriterT o m x
go = usingSem $ \u -> case decomp u of
Right (Weaving e s wv ex ins) -> f $ Weaving e s (go . wv) ex ins
Left g -> Lazy.WriterT $ do
~(o, a) <- k $
weave
(mempty, ())
(\ ~(o, m) -> (fmap . first) (o <>) (interpretViaLazyWriter f m))
(Just . snd)
g
return (a, o)
{-# INLINE go #-}
in do
~(a,s) <- Lazy.runWriterT (go sem)
return (s, a)
{-# INLINE interpretViaLazyWriter #-}

View File

@ -9,8 +9,11 @@ module Polysemy.Output
-- * Interpretations
, runOutputList
, runLazyOutputList
, runOutputMonoid
, runLazyOutputMonoid
, runOutputMonoidAssocR
, runLazyOutputMonoidAssocR
, runOutputMonoidIORef
, runOutputMonoidTVar
, outputToIOMonoid
@ -22,6 +25,7 @@ module Polysemy.Output
import Data.IORef
import Control.Concurrent.STM
import qualified Control.Monad.Trans.Writer.Lazy as Lazy
import Data.Semigroup (Endo(..))
import Data.Bifunctor (first)
@ -29,6 +33,9 @@ import Polysemy
import Polysemy.State
import Control.Monad (when)
import Polysemy.Internal.Union
import Polysemy.Internal.Writer
------------------------------------------------------------------------------
-- | An effect capable of sending messages. Useful for streaming output and for
@ -53,6 +60,22 @@ runOutputList = fmap (first reverse) . runState [] . reinterpret
)
{-# INLINE runOutputList #-}
------------------------------------------------------------------------------
-- | Run an 'Output' effect by transforming it into a list of its values,
-- lazily.
--
-- __Warning: This inherits the nasty space leak issue of__
-- __'Lazy.WriterT'! Don't use this if you don't have to.__
--
-- @since 1.3.0.0
runLazyOutputList
:: forall o r a
. Sem (Output o ': r) a
-> Sem r ([o], a)
runLazyOutputList = runLazyOutputMonoidAssocR pure
{-# INLINE runLazyOutputList #-}
------------------------------------------------------------------------------
-- | Run an 'Output' effect by transforming it into a monoid.
--
@ -69,6 +92,25 @@ runOutputMonoid f = runState mempty . reinterpret
)
{-# INLINE runOutputMonoid #-}
------------------------------------------------------------------------------
-- | Run an 'Output' effect by transforming it into a monoid, and accumulate
-- it lazily.
--
-- __Warning: This inherits the nasty space leak issue of__
-- __'Lazy.WriterT'! Don't use this if you don't have to.__
--
-- @since 1.3.0.0
runLazyOutputMonoid
:: forall o m r a
. Monoid m
=> (o -> m)
-> Sem (Output o ': r) a
-> Sem r (m, a)
runLazyOutputMonoid f = interpretViaLazyWriter $ \(Weaving e s _ ex _) ->
case e of
Output o -> ex s <$ Lazy.tell (f o)
------------------------------------------------------------------------------
-- | Like 'runOutputMonoid', but right-associates uses of '<>'.
--
@ -90,6 +132,31 @@ runOutputMonoidAssocR f =
. runOutputMonoid (\o -> let !o' = f o in Endo (o' <>))
{-# INLINE runOutputMonoidAssocR #-}
------------------------------------------------------------------------------
-- | Like 'runLazyOutputMonoid', but right-associates uses of '<>'.
--
-- This asymptotically improves performance if the time complexity of '<>' for
-- the 'Monoid' depends only on the size of the first argument.
--
-- You should always use this instead of 'runLazyOutputMonoid' if the monoid
-- is a list, such as 'String'.
--
-- __Warning: This inherits the nasty space leak issue of__
-- __'Lazy.WriterT'! Don't use this if you don't have to.__
--
-- @since 1.3.0.0
runLazyOutputMonoidAssocR
:: forall o m r a
. Monoid m
=> (o -> m)
-> Sem (Output o ': r) a
-> Sem r (m, a)
runLazyOutputMonoidAssocR f =
fmap (first (`appEndo` mempty))
. runLazyOutputMonoid (\o -> let o' = f o in Endo (o' <>))
-- ^ N.B. No bang pattern
{-# INLINE runLazyOutputMonoidAssocR #-}
------------------------------------------------------------------------------
-- | Run an 'Output' effect by transforming it into atomic operations
-- over an 'IORef'.

View File

@ -12,7 +12,9 @@ module Polysemy.Writer
-- * Interpretations
, runWriter
, runLazyWriter
, runWriterAssocR
, runLazyWriterAssocR
, runWriterTVar
, writerToIOFinal
, writerToIOAssocRFinal
@ -23,6 +25,7 @@ module Polysemy.Writer
) where
import Control.Concurrent.STM
import qualified Control.Monad.Trans.Writer.Lazy as Lazy
import Data.Bifunctor (first)
import Data.Semigroup
@ -31,6 +34,7 @@ import Polysemy
import Polysemy.Output
import Polysemy.State
import Polysemy.Internal.Union
import Polysemy.Internal.Writer
@ -55,7 +59,8 @@ outputToWriter = interpret $ \case
------------------------------------------------------------------------------
-- | Run a 'Writer' effect in the style of 'Control.Monad.Trans.Writer.WriterT'
-- | Run a 'Writer' effect in the style of
-- 'Control.Monad.Trans.Writer.Strict.WriterT'
-- (but without the nasty space leak!)
runWriter
:: Monoid o
@ -81,6 +86,35 @@ runWriter = runState mempty . reinterpretH
)
{-# INLINE runWriter #-}
------------------------------------------------------------------------------
-- | Run a 'Writer' effect in the style of 'Control.Monad.Trans.Writer.WriterT'
-- lazily.
--
-- __Warning: This inherits the nasty space leak issue of__
-- __'Lazy.WriterT'! Don't use this if you don't have to.__
--
-- @since 1.3.0.0
runLazyWriter
:: forall o r a
. Monoid o
=> Sem (Writer o ': r) a
-> Sem r (o, a)
runLazyWriter = interpretViaLazyWriter $ \(Weaving e s wv ex ins) ->
case e of
Tell o -> ex s <$ Lazy.tell o
Listen m -> do
let m' = wv (m <$ s)
~(fa, o) <- Lazy.listen m'
return $ ex $ (,) o <$> fa
Pass m -> do
let m' = wv (m <$ s)
Lazy.pass $ do
ft <- m'
let f = maybe id fst (ins ft)
return (ex (fmap snd ft), f)
{-# INLINE runLazyWriter #-}
-----------------------------------------------------------------------------
-- | Like 'runWriter', but right-associates uses of '<>'.
--
@ -102,6 +136,31 @@ runWriterAssocR =
. raiseUnder
{-# INLINE runWriterAssocR #-}
-----------------------------------------------------------------------------
-- | Like 'runLazyWriter', but right-associates uses of '<>'.
--
-- This asymptotically improves performance if the time complexity of '<>'
-- for the 'Monoid' depends only on the size of the first argument.
--
-- You should always use this instead of 'runLazyWriter' if the monoid
-- is a list, such as 'String'.
--
-- __Warning: This inherits the nasty space leak issue of__
-- __'Lazy.WriterT'! Don't use this if you don't have to.__
--
-- @since 1.3.0.0
runLazyWriterAssocR
:: Monoid o
=> Sem (Writer o ': r) a
-> Sem r (o, a)
runLazyWriterAssocR =
(fmap . first) (`appEndo` mempty)
. runLazyWriter
. writerToEndoWriter
. raiseUnder
{-# INLINE runLazyWriterAssocR #-}
--------------------------------------------------------------------
-- | Transform a 'Writer' effect into atomic operations
-- over a 'TVar' through final 'IO'.

View File

@ -11,6 +11,7 @@ import Control.Exception (evaluate)
import Polysemy
import Polysemy.Async
import Polysemy.Error
import Polysemy.Input
import Polysemy.Writer
censor' :: forall e s a r
@ -156,3 +157,44 @@ spec = do
Right end2 <- runFinal . runError $ test6
end1 `shouldBe` "message has been received"
end2 `shouldBe` "message has been received"
describe "runLazyWriter" $ do
let
runLazily = run . runInputConst () . runLazyWriter @[Int]
runSemiLazily = runLazily . runError @()
runStrictly = run . runError @() . runLazyWriter @[Int]
runStrictlyM = runM . runLazyWriter @[Int]
act :: Member (Writer [Int]) r => Sem r ()
act = do
tell @[Int] [1]
tell @[Int] [2]
error "strict"
it "should build the final output lazily, if the interpreters after \
\runLazyWriter and the final monad are lazy" $ do
(take 2 . fst . runLazily) act `shouldBe` [1,2]
(take 2 . fst . runSemiLazily) act `shouldBe` [1,2]
evaluate (runStrictly act) `shouldThrow` errorCall "strict"
runStrictlyM act `shouldThrow` errorCall "strict"
it "should listen lazily if all interpreters and final monad are lazy" $ do
let
listenAct :: Member (Writer [Int]) r => Sem r [Int]
listenAct = do
(end,_) <- listen @[Int] act
return (take 2 end)
(snd . runLazily) listenAct `shouldBe` [1,2]
evaluate ((snd . runSemiLazily) listenAct) `shouldThrow` errorCall "strict"
evaluate (runStrictly listenAct) `shouldThrow` errorCall "strict"
runStrictlyM listenAct `shouldThrow` errorCall "strict"
it "should censor lazily if all interpreters and final monad are lazy" $ do
let
censorAct :: Member (Writer [Int]) r => Sem r ()
censorAct = censor @[Int] (\(_:y:_) -> [0,y]) act
(fst . runLazily) censorAct `shouldBe` [0,2]
evaluate ((fst . runSemiLazily) censorAct) `shouldThrow` errorCall "strict"
evaluate (runStrictly censorAct) `shouldThrow` errorCall "strict"
runStrictlyM censorAct `shouldThrow` errorCall "strict"