content: {-# LANGUAGE AllowAmbiguousTypes #-}
  {-# OPTIONS_HADDOCK not-home #-}
  
  -- | Description: The meta-effect 'Scoped'
  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 . Word -> effect m a -> Scoped param effect m a
    InScope :: ∀ param effect m a . param -> (Word -> m a) -> Scoped param effect m a
  
  -- | An auxiliary effect for 'Scoped'.
  data OuterRun (effect :: Effect) :: Effect where
    OuterRun :: ∀ effect m a . Word -> effect m a -> OuterRun 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 param main =
    send $ InScope @param @effect param $ \w ->
      transform @effect (Run @param w) 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_ = 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 fp =
    transform \case
      Run w e        -> Run @param1 w e
      InScope p main -> InScope (fp p) main
  {-# inline rescope #-}
  
  