diff --git a/package.yaml b/package.yaml index ff7f37f8..329942ab 100644 --- a/package.yaml +++ b/package.yaml @@ -91,13 +91,12 @@ tests: - -threaded - -rtsopts - -with-rtsopts=-N - build-tools: - - hspec-discover >= 2.0 dependencies: - polysemy - inspection-testing >= 0.4.2 && < 0.5 - hspec >= 2.6.0 && < 3 - doctest >= 0.16.0.1 && < 0.19 + - hspec-discover >= 2.0 generated-other-modules: - Build_doctests diff --git a/polysemy-plugin/package.yaml b/polysemy-plugin/package.yaml index 5283e6a3..b9e89e17 100644 --- a/polysemy-plugin/package.yaml +++ b/polysemy-plugin/package.yaml @@ -56,9 +56,8 @@ tests: ghc-options: - -dcore-lint - -dsuppress-all - build-tools: - - hspec-discover dependencies: + - hspec-discover - polysemy >= 1.3.0.0 - polysemy-plugin - hspec >= 2.6.0 && < 3 diff --git a/polysemy-plugin/polysemy-plugin.cabal b/polysemy-plugin/polysemy-plugin.cabal index bb9c8948..ac262560 100644 --- a/polysemy-plugin/polysemy-plugin.cabal +++ b/polysemy-plugin/polysemy-plugin.cabal @@ -1,6 +1,6 @@ cabal-version: 2.0 --- This file has been generated from package.yaml by hpack version 0.35.0. +-- This file has been generated from package.yaml by hpack version 0.34.7. -- -- see: https://github.com/sol/hpack @@ -108,8 +108,6 @@ test-suite polysemy-plugin-test TypeFamilies UnicodeSyntax ghc-options: -threaded -rtsopts -with-rtsopts=-N -fplugin=Polysemy.Plugin - build-tool-depends: - hspec-discover:hspec-discover build-depends: base >=4.9 && <5 , containers >=0.5 && <0.7 @@ -117,12 +115,13 @@ test-suite polysemy-plugin-test , ghc >=8.6.5 && <10 , ghc-tcplugins-extra >=0.3 && <0.5 , hspec >=2.6.0 && <3 + , hspec-discover , inspection-testing >=0.4.2 && <0.5 , polysemy >=1.3.0.0 , polysemy-plugin , should-not-typecheck >=2.1.0 && <3 , syb ==0.7.* , transformers >=0.5.2.0 && <0.6 - default-language: Haskell2010 if flag(corelint) ghc-options: -dcore-lint -dsuppress-all + default-language: Haskell2010 diff --git a/polysemy.cabal b/polysemy.cabal index 1142e6e9..5d76dd07 100644 --- a/polysemy.cabal +++ b/polysemy.cabal @@ -1,6 +1,6 @@ cabal-version: 2.0 --- This file has been generated from package.yaml by hpack version 0.35.0. +-- This file has been generated from package.yaml by hpack version 0.34.7. -- -- see: https://github.com/sol/hpack @@ -114,7 +114,6 @@ library , transformers >=0.5.2.0 && <0.6 , type-errors >=0.2.0.0 , unagi-chan >=0.4.0.0 && <0.5 - default-language: Haskell2010 if impl(ghc < 8.6) default-extensions: MonadFailDesugaring @@ -126,6 +125,7 @@ library if impl(ghc < 8.2.2) build-depends: unsupported-ghc-version >1 && <1 + default-language: Haskell2010 test-suite polysemy-test type: exitcode-stdio-1.0 @@ -143,6 +143,7 @@ test-suite polysemy-test InterceptSpec KnownRowSpec OutputSpec + ScopedSpec TacticsSpec ThEffectSpec TypeErrors @@ -170,8 +171,6 @@ test-suite polysemy-test TypeFamilies UnicodeSyntax ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-tool-depends: - hspec-discover:hspec-discover >=2.0 build-depends: async >=2.2 && <3 , base >=4.9 && <5 @@ -179,6 +178,7 @@ test-suite polysemy-test , doctest >=0.16.0.1 && <0.19 , first-class-families >=0.5.0.0 && <0.9 , hspec >=2.6.0 && <3 + , hspec-discover >=2.0 , inspection-testing >=0.4.2 && <0.5 , mtl >=2.2.2 && <3 , polysemy @@ -189,8 +189,8 @@ test-suite polysemy-test , transformers >=0.5.2.0 && <0.6 , type-errors >=0.2.0.0 , unagi-chan >=0.4.0.0 && <0.5 - default-language: Haskell2010 if impl(ghc < 8.6) default-extensions: MonadFailDesugaring TypeInType + default-language: Haskell2010 diff --git a/src/Polysemy/Internal/Scoped.hs b/src/Polysemy/Internal/Scoped.hs index e999cac9..3f585ec9 100644 --- a/src/Polysemy/Internal/Scoped.hs +++ b/src/Polysemy/Internal/Scoped.hs @@ -7,10 +7,9 @@ import Data.Kind (Type) import Polysemy --- | @Scoped@ transforms a program so that @effect@ is associated with a --- @resource@ within that program. This requires the interpreter for @effect@ to --- be parameterized by @resource@ and constructed for every program using --- @Scoped@ separately. +-- | @Scoped@ transforms a program so that an interpreter for @effect@ may +-- perform arbitrary actions, like resource management, before and after the +-- computation wrapped by a call to 'scoped' is executed. -- -- An application for this is @Polysemy.Conc.Events@ from -- , in which each program @@ -20,6 +19,8 @@ import Polysemy -- database effect. -- -- For a longer exposition, see . +-- Note that the interface has changed since the blog post was published: The +-- @resource@ parameter no longer exists. -- -- Resource allocation is performed by a function passed to -- 'Polysemy.Scoped.interpretScoped'. @@ -46,7 +47,7 @@ import Polysemy -- -- Then we can take advantage of 'Scoped' to write this program: -- --- > prog :: Member (Scoped FilePath resource Write) r => Sem r () +-- > prog :: Member (Scoped FilePath Write) r => Sem r () -- > prog = do -- > scoped "file1.txt" do -- > write "line 1" @@ -61,7 +62,7 @@ import Polysemy -- -- The interpreter may look like this: -- --- > interpretWriteFile :: Members '[Resource, Embed IO] => InterpreterFor (Scoped FilePath Handle Write) r +-- > interpretWriteFile :: Members '[Resource, Embed IO] => InterpreterFor (Scoped FilePath Write) r -- > interpretWriteFile = -- > interpretScoped allocator handler -- > where @@ -78,24 +79,20 @@ import Polysemy -- -- This makes it possible to use a pure interpreter for testing: -- --- > interpretWriteOutput :: Member (Output (FilePath, Text)) r => InterpreterFor (Scoped FilePath FilePath Write) r +-- > interpretWriteOutput :: Member (Output (FilePath, Text)) r => InterpreterFor (Scoped FilePath Write) r -- > interpretWriteOutput = -- > interpretScoped (\ name use -> use name) \ name -> \case -- > Write line -> output (name, line) -- -- Here we simply pass the name to the interpreter in the resource allocation --- function. Note how the type of the effect changed, with the @resource@ --- parameter being instantiated as @FilePath@ instead of @Handle@. --- This change does not need to be anticipated in the business logic that uses --- the scoped effect – as is visible in the signature of @prog@, the @resource@ --- parameter is always chosen concretely by an interpreter. +-- function. -- -- Now imagine that we drop requirement 2 from the initial list – we still want -- the file to be opened and closed as late/early as possible, but the file name -- is globally fixed. For this case, the @param@ type is unused, and the API -- provides some convenience aliases to make your code more concise: -- --- > prog :: Member (Scoped_ resource Write) r => Sem r () +-- > prog :: Member (Scoped_ Write) r => Sem r () -- > prog = do -- > scoped_ do -- > write "line 1" @@ -105,39 +102,37 @@ import Polysemy -- > write "line 2" -- -- The type 'Scoped_' and the constructor 'scoped_' simply fix @param@ to @()@. -data Scoped (param :: Type) (resource :: Type) (effect :: Effect) :: Effect where - Run :: ∀ param resource effect m a . resource -> effect m a -> - Scoped param resource effect m a - InScope :: ∀ param resource effect m a . param -> (resource -> m a) -> - Scoped param resource effect m a +data Scoped (param :: Type) (effect :: Effect) :: Effect where + Run :: ∀ param effect m a . effect m a -> Scoped param effect m a + InScope :: ∀ param effect m a . param -> m a -> Scoped param effect m a -- |A convenience alias for a scope without parameters. -type Scoped_ resource effect = - Scoped () resource effect +type Scoped_ effect = + Scoped () effect -- | Constructor for 'Scoped', taking a nested program and transforming all --- instances of @effect@ to @'Scoped' param resource effect@. +-- instances of @effect@ to @'Scoped' param effect@. -- -- Please consult the documentation of 'Scoped' for details and examples. scoped :: - ∀ resource param effect r . - Member (Scoped param resource effect) r => + ∀ param effect r . + Member (Scoped param effect) r => param -> InterpreterFor effect r scoped param main = - send $ InScope @param @resource @effect param \ resource -> - transform @effect (Run @param resource) main + send $ InScope @param @effect param do + transform @effect (Run @param) main {-# inline scoped #-} -- | Constructor for 'Scoped_', taking a nested program and transforming all --- instances of @effect@ to @'Scoped_' resource effect@. +-- instances of @effect@ to @'Scoped_' effect@. -- -- Please consult the documentation of 'Scoped' for details and examples. scoped_ :: - ∀ resource effect r . - Member (Scoped_ resource effect) r => + ∀ effect r . + Member (Scoped_ effect) r => InterpreterFor effect r -scoped_ = scoped @resource () +scoped_ = scoped () {-# inline scoped_ #-} -- | Transform the parameters of a 'Scoped' program. @@ -147,13 +142,13 @@ scoped_ = scoped @resource () -- with some fundamental parameters being supplied at scope creation and some -- optional or specific parameters being selected by the user downstream. rescope :: - ∀ param0 param1 resource effect r . - Member (Scoped param1 resource effect) r => + ∀ param0 param1 effect r . + Member (Scoped param1 effect) r => (param0 -> param1) -> - InterpreterFor (Scoped param0 resource effect) r + InterpreterFor (Scoped param0 effect) r rescope fp = transform \case - Run res e -> Run @param1 res e + Run e -> Run @param1 e InScope p main -> InScope (fp p) main {-# inline rescope #-} diff --git a/src/Polysemy/Scoped.hs b/src/Polysemy/Scoped.hs index a5eda371..fa105489 100644 --- a/src/Polysemy/Scoped.hs +++ b/src/Polysemy/Scoped.hs @@ -36,7 +36,7 @@ import Polysemy.Internal.Tactics -- use the 'Tactical' environment and transforms the effect into other effects -- on the stack. interpretScopedH :: - ∀ param resource effect r . + ∀ 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'. @@ -44,18 +44,19 @@ interpretScopedH :: -- | A handler like the one expected by 'interpretH' with an additional -- parameter that contains the @resource@ allocated by the first argument. (∀ r0 x . resource -> effect (Sem r0) x -> Tactical effect (Sem r0) r x) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r interpretScopedH withResource scopedHandler = - go + -- TODO investigate whether loopbreaker optimization is effective here + go (errorWithoutStackTrace "top level run") where - go :: InterpreterFor (Scoped param resource effect) r - go = + go :: resource -> InterpreterFor (Scoped param effect) r + go resource = interpretWeaving \ (Weaving effect s wv ex ins) -> case effect of - Run resource act -> - ex <$> runTactics s (raise . go . wv) ins (go . wv) + Run act -> + ex <$> runTactics s (raise . go resource . wv) ins (go resource . wv) (scopedHandler resource act) InScope param main -> - withResource param \ resource -> ex <$> go (wv (main resource <$ s)) + withResource param \ resource' -> ex <$> go resource' (wv (main <$ s)) {-# inline interpretScopedH #-} -- | Variant of 'interpretScopedH' that allows the resource acquisition function @@ -66,15 +67,19 @@ interpretScopedH' :: Tactical e (Sem r0) r x) -> (∀ r0 x . resource -> effect (Sem r0) x -> - Tactical (Scoped param resource effect) (Sem r0) r x) -> - InterpreterFor (Scoped param resource effect) r + Tactical (Scoped param effect) (Sem r0) r x) -> + InterpreterFor (Scoped param effect) r interpretScopedH' withResource scopedHandler = - interpretH \case - Run resource act -> - scopedHandler resource act - InScope param main -> - withResource param \ resource -> - runTSimple (main resource) + go (errorWithoutStackTrace "top level run") + where + go :: resource -> InterpreterFor (Scoped param effect) r + go resource = + interpretH \case + Run act -> + scopedHandler resource act + InScope param main -> + withResource param \ resource' -> + raise . go resource' =<< runT main {-# inline interpretScopedH' #-} -- | First-order variant of 'interpretScopedH'. @@ -82,7 +87,7 @@ interpretScoped :: ∀ resource param effect r . (∀ x . param -> (resource -> Sem r x) -> Sem r x) -> (∀ m x . resource -> effect m x -> Sem r x) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r interpretScoped withResource scopedHandler = interpretScopedH withResource \ r e -> liftT (scopedHandler r e) {-# inline interpretScoped #-} @@ -93,7 +98,7 @@ interpretScopedAs :: ∀ resource param effect r . (param -> Sem r resource) -> (∀ m x . resource -> effect m x -> Sem r x) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r interpretScopedAs resource = interpretScoped \ p use -> use =<< resource p {-# inline interpretScopedAs #-} @@ -132,7 +137,7 @@ interpretScopedAs resource = -- > interpretMState :: -- > ∀ s r . -- > Members [Resource, Embed IO] r => --- > InterpreterFor (Scoped s (MVar ()) (MState s)) r +-- > InterpreterFor (Scoped s (MState s)) r -- > interpretMState = -- > interpretScopedWithH @'[AtomicState s] withResource \ lock -> \case -- > MState f -> @@ -149,27 +154,27 @@ interpretScopedWithH :: (KnownList extra, r1 ~ Append extra r) => (∀ x . param -> (resource -> Sem r1 x) -> Sem r x) -> (∀ r0 x . resource -> effect (Sem r0) x -> Tactical effect (Sem r0) r1 x) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r interpretScopedWithH withResource scopedHandler = interpretWeaving \case Weaving (InScope param main) s wv ex _ -> - ex <$> withResource param \ resource -> inScope $ + ex <$> withResource param \ resource -> inScope resource $ restack (injectMembership - (singList @'[Scoped param resource effect]) - (singList @extra)) $ wv (main resource <$ s) + (singList @'[Scoped param effect]) + (singList @extra)) $ wv (main <$ s) _ -> errorWithoutStackTrace "top level Run" where - inScope :: InterpreterFor (Scoped param resource effect) r1 - inScope = + inScope :: resource -> InterpreterFor (Scoped param effect) r1 + inScope resource = interpretWeaving \case Weaving (InScope param main) s wv ex _ -> restack (extendMembershipLeft (singList @extra)) - (ex <$> withResource param \resource -> - inScope (wv (main resource <$ s))) - Weaving (Run resource act) s wv ex ins -> - ex <$> runTactics s (raise . inScope . wv) ins (inScope . wv) + (ex <$> withResource param \resource' -> + inScope resource' (wv (main <$ s))) + Weaving (Run act) s wv ex ins -> + ex <$> runTactics s (raise . inScope resource . wv) ins (inScope resource . wv) (scopedHandler resource act) {-# inline interpretScopedWithH #-} @@ -182,7 +187,7 @@ interpretScopedWithH withResource scopedHandler = -- > data SomeAction :: Effect where -- > SomeAction :: SomeAction m () -- > --- > foo :: InterpreterFor (Scoped () () SomeAction) r +-- > foo :: InterpreterFor (Scoped () SomeAction) r -- > foo = -- > interpretScopedWith @[Reader Int, State Bool] localEffects \ () -> \case -- > SomeAction -> put . (> 0) =<< ask @Int @@ -193,7 +198,7 @@ interpretScopedWith :: (r1 ~ Append extra r, KnownList extra) => (∀ x . param -> (resource -> Sem r1 x) -> Sem r x) -> (∀ m x . resource -> effect m x -> Sem r1 x) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r interpretScopedWith withResource scopedHandler = interpretScopedWithH @extra withResource \ r e -> liftT (scopedHandler r e) {-# inline interpretScopedWith #-} @@ -209,7 +214,7 @@ interpretScopedWith_ :: (r1 ~ Append extra r, KnownList extra) => (∀ x . param -> Sem r1 x -> Sem r x) -> (∀ m x . effect m x -> Sem r1 x) -> - InterpreterFor (Scoped param () effect) r + InterpreterFor (Scoped param effect) r interpretScopedWith_ withResource scopedHandler = interpretScopedWithH @extra (\ p f -> withResource p (f ())) \ () e -> liftT (scopedHandler e) {-# inline interpretScopedWith_ #-} @@ -246,29 +251,29 @@ interpretScopedWith_ withResource scopedHandler = -- -- > runScoped (\ initial use -> use =<< embed (newTVarIO initial)) runAtomicStateTVar runScoped :: - ∀ param resource effect r . + ∀ resource param effect r . (∀ x . param -> (resource -> Sem r x) -> Sem r x) -> (resource -> InterpreterFor effect r) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r runScoped withResource scopedInterpreter = - go + go (errorWithoutStackTrace "top level run") where - go :: InterpreterFor (Scoped param resource effect) r - go = + go :: resource -> InterpreterFor (Scoped param effect) r + go resource = interpretWeaving \ (Weaving effect s wv ex ins) -> case effect of - Run resource act -> + Run act -> scopedInterpreter resource - $ liftSem $ injWeaving $ Weaving act s (raise . go . wv) ex ins + $ liftSem $ injWeaving $ Weaving act s (raise . go resource . wv) ex ins InScope param main -> - withResource param \ resource -> ex <$> go (wv (main resource <$ s)) + withResource param \ resource' -> ex <$> go resource' (wv (main <$ s)) {-# inline runScoped #-} -- | Variant of 'runScoped' in which the resource allocator returns the resource -- rather tnen calling a continuation. runScopedAs :: - ∀ param resource effect r . + ∀ resource param effect r . (param -> Sem r resource) -> (resource -> InterpreterFor effect r) -> - InterpreterFor (Scoped param resource effect) r + InterpreterFor (Scoped param effect) r runScopedAs resource = runScoped \ p use -> use =<< resource p {-# inline runScopedAs #-} diff --git a/test/ScopedSpec.hs b/test/ScopedSpec.hs new file mode 100644 index 00000000..87fe27f4 --- /dev/null +++ b/test/ScopedSpec.hs @@ -0,0 +1,67 @@ +{-# language TemplateHaskell, DerivingStrategies, GeneralizedNewtypeDeriving #-} + +module ScopedSpec where + +import Control.Concurrent.STM +import Polysemy +import Polysemy.Scoped +import Test.Hspec + +newtype Par = + Par { unPar :: Int } + deriving stock (Eq, Show) + deriving newtype (Num, Real, Enum, Integral, Ord) + +data E :: Effect where + E1 :: E m Int + E2 :: E m Int + +makeSem ''E + +data F :: Effect where + F :: F m Int + +makeSem ''F + +handleE :: + Member (Embed IO) r => + TVar Int -> + E m a -> + Tactical effect m (F : r) a +handleE tv = \case + E1 -> do + i1 <- embed (readTVarIO tv) + i2 <- f + pureT (i1 + i2 + 10) + E2 -> + pureT (-1) + +interpretF :: + Member (Embed IO) r => + TVar Int -> + InterpreterFor F r +interpretF tv = + interpret \ F -> do + embed (atomically (writeTVar tv 7)) + pure 5 + +scope :: + Member (Embed IO) r => + Par -> + (TVar Int -> Sem (F : r) a) -> + Sem r a +scope (Par n) use = do + tv <- embed (newTVarIO n) + interpretF tv (use tv) + +spec :: Spec +spec = parallel do + describe "Scoped" do + it "local effects" do + (i1, i2) <- runM $ interpretScopedWithH @'[F] @(TVar Int) @Par @E scope handleE do + scoped @Par @E 20 do + i1 <- e1 + i2 <- scoped @Par @E 23 e1 + pure (i1, i2) + 35 `shouldBe` i1 + 38 `shouldBe` i2