mirror of
https://github.com/polysemy-research/polysemy.git
synced 2024-11-26 21:36:58 +03:00
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:
parent
6f9f4541f5
commit
8d49b677fc
@ -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 #-}
|
||||
|
@ -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'.
|
||||
|
@ -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'.
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user