{-# LANGUAGE AllowAmbiguousTypes #-} {-# OPTIONS_HADDOCK not-home #-} module Polysemy.Internal.Scoped where import Data.Kind (Type) import Polysemy -- | @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 -- <https://hackage.haskell.org/package/polysemy-conc>, in which each program -- using the effect @Polysemy.Conc.Consume@ is interpreted with its own copy of -- the event channel; or a database transaction, in which a transaction handle -- is created for the wrapped program and passed to the interpreter for the -- database effect. -- -- For a longer exposition, see <https://www.tweag.io/blog/2022-01-05-polysemy-scoped/>. -- 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'. -- -- The constructors are not intended to be used directly; the smart constructor -- 'scoped' is used like a local interpreter for @effect@. 'scoped' takes an -- argument of type @param@, which will be passed through to the interpreter, to -- be used by the resource allocation function. -- -- As an example, imagine an effect for writing lines to a file: -- -- > data Write :: Effect where -- > Write :: Text -> Write m () -- > makeSem ''Write -- -- If we now have the following requirements: -- -- 1. The file should be opened and closed right before and after the part of -- the program in which we write lines -- 2. The file name should be specifiable at the point in the program where -- writing begins -- 3. We don't want to commit to IO, lines should be stored in memory when -- running tests -- -- Then we can take advantage of 'Scoped' to write this program: -- -- > prog :: Member (Scoped FilePath Write) r => Sem r () -- > prog = do -- > scoped "file1.txt" do -- > write "line 1" -- > write "line 2" -- > scoped "file2.txt" do -- > write "line 1" -- > write "line 2" -- -- Here 'scoped' creates a prompt for an interpreter to start allocating a -- resource for @"file1.txt"@ and handling @Write@ actions using that resource. -- When the 'scoped' block ends, the resource should be freed. -- -- The interpreter may look like this: -- -- > interpretWriteFile :: Members '[Resource, Embed IO] => InterpreterFor (Scoped FilePath Write) r -- > interpretWriteFile = -- > interpretScoped allocator handler -- > where -- > allocator name use = bracket (openFile name WriteMode) hClose use -- > handler fileHandle (Write line) = embed (Text.hPutStrLn fileHandle line) -- -- Essentially, the @bracket@ is executed at the point where @scoped@ was -- called, wrapping the following block. When the second @scoped@ is executed, -- another call to @bracket@ is performed. -- -- The effect of this is that the operation that uses @Embed IO@ was moved from -- the call site to the interpreter, while the interpreter may be executed at -- the outermost layer of the app. -- -- This makes it possible to use a pure interpreter for testing: -- -- > 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. -- -- 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_ Write) r => Sem r () -- > prog = do -- > scoped_ do -- > write "line 1" -- > write "line 2" -- > scoped_ do -- > write "line 1" -- > write "line 2" -- -- The type 'Scoped_' and the constructor 'scoped_' simply fix @param@ to @()@. 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_ effect = Scoped () effect -- | Constructor for 'Scoped', taking a nested program and transforming all -- instances of @effect@ to @'Scoped' param effect@. -- -- Please consult the documentation of 'Scoped' for details and examples. scoped :: ∀ param effect r . Member (Scoped param effect) r => param -> InterpreterFor effect r scoped :: forall param (effect :: Effect) (r :: EffectRow). Member (Scoped param effect) r => param -> InterpreterFor effect r scoped param param Sem (effect : r) a main = Scoped param effect (Sem r) a -> Sem r a forall (e :: Effect) (r :: EffectRow) a. Member e r => e (Sem r) a -> Sem r a send (Scoped param effect (Sem r) a -> Sem r a) -> Scoped param effect (Sem r) a -> Sem r a forall a b. (a -> b) -> a -> b $ forall param (effect :: Effect) (m :: * -> *) a. param -> m a -> Scoped param effect m a InScope @param @effect param param do forall (e1 :: Effect) (e2 :: Effect) (r :: EffectRow) a. Member e2 r => (forall (rInitial :: EffectRow) x. e1 (Sem rInitial) x -> e2 (Sem rInitial) x) -> Sem (e1 : r) a -> Sem r a transform @effect (forall param (effect :: Effect) (m :: * -> *) a. effect m a -> Scoped param effect m a Run @param) Sem (effect : r) a main {-# inline scoped #-} -- | Constructor for 'Scoped_', taking a nested program and transforming all -- instances of @effect@ to @'Scoped_' effect@. -- -- Please consult the documentation of 'Scoped' for details and examples. scoped_ :: ∀ effect r . Member (Scoped_ effect) r => InterpreterFor effect r scoped_ :: forall (effect :: Effect) (r :: EffectRow). Member (Scoped_ effect) r => InterpreterFor effect r scoped_ = () -> InterpreterFor effect r forall param (effect :: Effect) (r :: EffectRow). Member (Scoped param effect) r => param -> InterpreterFor effect r scoped () {-# inline scoped_ #-} -- | Transform the parameters of a 'Scoped' program. -- -- This allows incremental additions to the data passed to the interpreter, for -- example to create an API that permits different ways of running an effect -- with some fundamental parameters being supplied at scope creation and some -- optional or specific parameters being selected by the user downstream. rescope :: ∀ param0 param1 effect r . Member (Scoped param1 effect) r => (param0 -> param1) -> InterpreterFor (Scoped param0 effect) r rescope :: forall param0 param1 (effect :: Effect) (r :: EffectRow). Member (Scoped param1 effect) r => (param0 -> param1) -> InterpreterFor (Scoped param0 effect) r rescope param0 -> param1 fp = (forall (rInitial :: EffectRow) x. Scoped param0 effect (Sem rInitial) x -> Scoped param1 effect (Sem rInitial) x) -> Sem (Scoped param0 effect : r) a -> Sem r a forall (e1 :: Effect) (e2 :: Effect) (r :: EffectRow) a. Member e2 r => (forall (rInitial :: EffectRow) x. e1 (Sem rInitial) x -> e2 (Sem rInitial) x) -> Sem (e1 : r) a -> Sem r a transform \case Run effect (Sem rInitial) x e -> forall param (effect :: Effect) (m :: * -> *) a. effect m a -> Scoped param effect m a Run @param1 effect (Sem rInitial) x e InScope param0 p Sem rInitial x main -> param1 -> Sem rInitial x -> Scoped param1 effect (Sem rInitial) x forall param (effect :: Effect) (m :: * -> *) a. param -> m a -> Scoped param effect m a InScope (param0 -> param1 fp param0 p) Sem rInitial x main {-# inline rescope #-}