{-# language AllowAmbiguousTypes, BangPatterns #-} -- | Description: Interpreters for 'Scoped' module Polysemy.Scoped ( -- * Effect Scoped, Scoped_, -- * Constructors scoped, scoped_, rescope, -- * Interpreters runScopedNew, interpretScopedH, interpretScopedH', interpretScoped, interpretScopedAs, interpretScopedWithH, interpretScopedWith, interpretScopedWith_, runScoped, runScopedAs, ) where import Data.Function ((&)) import Data.Sequence (Seq(..)) import qualified Data.Sequence as S import Polysemy.Opaque import Polysemy.Internal import Polysemy.Internal.Sing import Polysemy.Internal.Union import Polysemy.Internal.Combinators import Polysemy.Internal.Scoped import Polysemy.Internal.Tactics -- | Construct an interpreter for a higher-order effect wrapped in a 'Scoped', -- given a resource allocation function and a parameterized handler for the -- plain effect. -- -- This combinator is analogous to 'interpretH' in that it allows the handler to -- use the 'Tactical' environment and transforms the effect into other effects -- on the stack. interpretScopedH :: ∀ resource param effect r . -- | A callback function that allows the user to acquire a resource for each -- computation wrapped by 'scoped' using other effects, with an additional -- argument that contains the call site parameter passed to 'scoped'. (∀ q x . param -> (resource -> Sem (Opaque q ': r) x) -> Sem (Opaque q ': r) x) -> -- | A handler like the one expected by 'interpretH' with an additional -- parameter that contains the @resource@ allocated by the first argument. (∀ q r0 x . resource -> effect (Sem r0) x -> Tactical effect (Sem r0) (Opaque q ': r) x) -> InterpreterFor (Scoped param effect) r interpretScopedH withResource scopedHandler = runScopedNew \param sem -> withResource param \r -> interpretH (scopedHandler r) sem {-# inline interpretScopedH #-} -- | Variant of 'interpretScopedH' that allows the resource acquisition function -- to use 'Tactical'. interpretScopedH' :: ∀ resource param effect r . (∀ e r0 x . param -> (resource -> Tactical e (Sem r0) r x) -> Tactical e (Sem r0) r x) -> (∀ r0 x . resource -> effect (Sem r0) x -> Tactical (Scoped param effect) (Sem r0) r x) -> InterpreterFor (Scoped param effect) r interpretScopedH' withResource scopedHandler = go 0 Empty where go :: Word -> Seq resource -> InterpreterFor (Scoped param effect) r go depth resources = interpretH \case Run w act -> scopedHandler (S.index resources (fromIntegral w)) act InScope param main | !depth' <- depth + 1 -> withResource param \ resource -> raise . go depth' (resources :|> resource) =<< runT (main depth) {-# inline interpretScopedH' #-} -- | First-order variant of 'interpretScopedH'. interpretScoped :: ∀ resource param effect r . (∀ q x . param -> (resource -> Sem (Opaque q ': r) x) -> Sem (Opaque q ': r) x) -> (∀ m x . resource -> effect m x -> Sem r x) -> InterpreterFor (Scoped param effect) r interpretScoped withResource scopedHandler = interpretScopedH withResource \ r e -> liftT (raise (scopedHandler r e)) {-# inline interpretScoped #-} -- | Variant of 'interpretScoped' in which the resource allocator is a plain -- action. interpretScopedAs :: ∀ resource param effect r . (param -> Sem r resource) -> (∀ m x . resource -> effect m x -> Sem r x) -> InterpreterFor (Scoped param effect) r interpretScopedAs resource = interpretScoped \ p use -> use =<< raise (resource p) {-# inline interpretScopedAs #-} -- | Higher-order interpreter for 'Scoped' that allows the handler to use -- additional effects that are interpreted by the resource allocator. -- -- /Note/: It is necessary to specify the list of local interpreters with a type -- application; GHC won't be able to figure them out from the type of -- @withResource@. -- -- As an example for a higher order effect, consider a mutexed concurrent state -- effect, where an effectful function may lock write access to the state while -- making it still possible to read it: -- -- > data MState s :: Effect where -- > MState :: (s -> m (s, a)) -> MState s m a -- > MRead :: MState s m s -- > -- > makeSem ''MState -- -- We can now use an 'Polysemy.AtomicState.AtomicState' to store the current -- value and lock write access with an @MVar@. Since the state callback is -- effectful, we need a higher order interpreter: -- -- > withResource :: -- > Member (Embed IO) r => -- > s -> -- > (MVar () -> Sem (AtomicState s : r) a) -> -- > Sem r a -- > withResource initial use = do -- > tv <- embed (newTVarIO initial) -- > lock <- embed (newMVar ()) -- > runAtomicStateTVar tv $ use lock -- > -- > interpretMState :: -- > ∀ s r . -- > Members [Resource, Embed IO] r => -- > InterpreterFor (Scoped s (MState s)) r -- > interpretMState = -- > interpretScopedWithH @'[AtomicState s] withResource \ lock -> \case -- > MState f -> -- > bracket_ (embed (takeMVar lock)) (embed (tryPutMVar lock ())) do -- > s0 <- atomicGet -- > res <- runTSimple (f s0) -- > Inspector ins <- getInspectorT -- > for_ (ins res) \ (s, _) -> atomicPut s -- > pure (snd <$> res) -- > MRead -> -- > liftT atomicGet interpretScopedWithH :: ∀ extra resource param effect r . KnownList extra => (∀ q x . param -> (resource -> Sem (Append extra (Opaque q ': r)) x) -> Sem (Opaque q ': r) x) -> (∀ q r0 x . resource -> effect (Sem r0) x -> Tactical effect (Sem r0) (Append extra (Opaque q ': r)) x) -> InterpreterFor (Scoped param effect) r interpretScopedWithH withResource scopedHandler = runScopedNew \param (sem :: Sem (effect ': Opaque q ': r) x) -> withResource param \resource -> sem & restack (injectMembership (singList @'[effect]) (singList @extra)) & interpretH (scopedHandler @q resource) {-# inline interpretScopedWithH #-} -- | First-order variant of 'interpretScopedWithH'. -- -- /Note/: It is necessary to specify the list of local interpreters with a type -- application; GHC won't be able to figure them out from the type of -- @withResource@: -- -- > data SomeAction :: Effect where -- > SomeAction :: SomeAction m () -- > -- > foo :: InterpreterFor (Scoped () SomeAction) r -- > foo = -- > interpretScopedWith @[Reader Int, State Bool] localEffects \ () -> \case -- > SomeAction -> put . (> 0) =<< ask @Int -- > where -- > localEffects () use = evalState False (runReader 5 (use ())) interpretScopedWith :: ∀ extra param resource effect r. KnownList extra => (∀ q x . param -> (resource -> Sem (Append extra (Opaque q ': r)) x) -> Sem (Opaque q ': r) x) -> (∀ m x . resource -> effect m x -> Sem (Append extra r) x) -> InterpreterFor (Scoped param effect) r interpretScopedWith withResource scopedHandler = runScopedNew \param (sem :: Sem (effect ': Opaque q ': r) x) -> withResource param \resource -> sem & restack (injectMembership (singList @'[effect]) (singList @extra)) & interpretH \e -> liftT $ restack (injectMembership @r (singList @extra) (singList @'[Opaque q])) (scopedHandler resource e) {-# inline interpretScopedWith #-} -- | Variant of 'interpretScopedWith' in which no resource is used and the -- resource allocator is a plain interpreter. -- This is useful for scopes that only need local effects, but no resources in -- the handler. -- -- See the /Note/ on 'interpretScopedWithH'. interpretScopedWith_ :: ∀ extra param effect r . KnownList extra => (∀ q x . param -> Sem (Append extra (Opaque q ': r)) x -> Sem (Opaque q ': r) x) -> (∀ m x . effect m x -> Sem (Append extra r) x) -> InterpreterFor (Scoped param effect) r interpretScopedWith_ withResource scopedHandler = interpretScopedWith @extra (\ p f -> withResource p (f ())) (\ () -> scopedHandler) {-# inline interpretScopedWith_ #-} -- | Variant of 'interpretScoped' that uses another interpreter instead of a -- handler. -- -- This is mostly useful if you want to reuse an interpreter that you cannot -- easily rewrite (like from another library). If you have full control over the -- implementation, 'interpretScoped' should be preferred. -- -- /Note/: In previous versions of Polysemy, the wrapped interpreter was -- executed fully, including the initializing code surrounding its handler, -- for each action in the program. However, new and continuing discoveries -- regarding 'Scoped' has allowed the improvement of having the interpreter be -- used only once per use of 'scoped', and have it cover the same scope of -- actions that the resource allocator does. -- -- This renders the resource allocator practically redundant; for the moment, -- the API surrounding 'Scoped' remains the same, but work is in progress to -- revamp the entire API of 'Scoped'. runScoped :: ∀ resource param effect r . (∀ q x . param -> (resource -> Sem (Opaque q ': r) x) -> Sem (Opaque q ': r) x) -> (∀ q . resource -> InterpreterFor effect (Opaque q ': r)) -> InterpreterFor (Scoped param effect) r runScoped withResource scopedInterpreter = runScopedNew \param sem -> withResource param (\r -> scopedInterpreter r sem) {-# inline runScoped #-} -- | Variant of 'runScoped' in which the resource allocator returns the resource -- rather than calling a continuation. runScopedAs :: ∀ resource param effect r . (param -> Sem r resource) -> (∀ q. resource -> InterpreterFor effect (Opaque q ': r)) -> InterpreterFor (Scoped param effect) r runScopedAs resource = runScoped \ p use -> use =<< raise (resource p) {-# inline runScopedAs #-} -- | Run a 'Scoped' effect by specifying the interpreter to be used at every -- use of 'scoped'. -- -- This interpretation of 'Scoped' is powerful enough to subsume all other -- interpretations of 'Scoped' (except 'interpretScopedH'' which works -- differently from all other interpretations) while also being much simpler. -- -- Consider this a sneak-peek of the future of 'Scoped'. In the API rework -- planned for 'Scoped', the effect and its interpreters will be further -- expanded to make 'Scoped' even more flexible. -- -- @since 1.9.0.0 runScopedNew :: ∀ param effect r . (∀ q. param -> InterpreterFor effect (Opaque q ': r)) -> InterpreterFor (Scoped param effect) r runScopedNew h = interpretWeaving $ \(Weaving effect s wv ex _) -> case effect of Run w _ -> errorWithoutStackTrace $ "top level run with depth " ++ show w InScope param main -> wv (main 0 <$ s) & raiseUnder2 & go 0 & h param & interpretH (\(Opaque (OuterRun w _)) -> errorWithoutStackTrace $ "unhandled OuterRun with depth " ++ show w) & fmap ex where go' :: Word -> InterpreterFor (Opaque (OuterRun effect)) (effect ': Opaque (OuterRun effect) ': r) go' depth = interpretWeaving \ (Weaving sr@(Opaque (OuterRun w act)) s wv ex ins) -> if w == depth then liftSem $ injWeaving $ Weaving act s (go' depth . wv) ex ins else liftSem $ injWeaving $ Weaving sr s (go' depth . wv) ex ins -- TODO investigate whether loopbreaker optimization is effective here go :: Word -> InterpreterFor (Scoped param effect) (effect ': Opaque (OuterRun effect) ': r) go depth = interpretWeaving \ (Weaving effect s wv ex ins) -> case effect of Run w act | w == depth -> liftSem $ injWeaving $ Weaving act s (go depth . wv) ex ins | otherwise -> liftSem $ injWeaving $ Weaving (Opaque (OuterRun w act)) s (go depth . wv) ex ins InScope param main -> do let !depth' = depth + 1 wv (main depth' <$ s) & go depth' & h param & raiseUnder2 & go' depth & fmap ex {-# INLINE runScopedNew #-}