{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_HADDOCK prune #-}

{- |
Effect systems are being actively explored as ways to structure Haskell
programs. This package provides experimental support for the __effectful__
effect system.

This module introcuces a new effect, 'ProgramE', which plumbs the current
program context into the effect system. By calling 'runProgramE' to add the
'ProgramE' effect to the current list of in-scope effects you can then use
'withProgram'' to run a 'Program' action. The more general 'withProgram' gives
you an unlifting function to return back to the effect system so you can
continue processing within your effects stack.

= Usage

As an example, here's an effect which lifts to 'Program' @τ@, does some log
output, then unlifts back to 'Effectful.Internal.Monad.Eff' @es@ to then query
something from the 'Effectful.Environment.Environment' effect (which requires
the 'Effectful.Internal.Monad.IOE' effect to run):

@
retrieveProgramName
    :: forall τ es
     . ('Effectful.Internal.Monad.IOE' 'Effectful.Internal.Effect.:>' es, 'ProgramE' τ 'Effectful.Internal.Effect.:>' es)
    => 'Effectful.Internal.Monad.Eff' es ()
retrieveProgramName = do
    -- we're in (IOE :> es, ProgramE :> es) => Eff es, right?

    'withProgram' @τ $ \\runEffect -> do
        -- now we're in Program τ

        'Core.Program.Logging.info' \"Running in Program\"

        path <- runEffect $ do
            -- now back in (IOE :> es, ProgramE τ :> es) => Eff es, and can call
            -- something that requires the IOE effect be present.

            'Effectful.Environment.runEnvironment' $ do
                -- now in (Environment :> es) => Eff es
                'Effectful.Environment.getExecutablePath'

        'Core.Program.Logging.info' \"Done running effects\"
        'Core.Program.Logging.debugS' "path" path
@

The @@τ@ type application shown here is vital; without it the compiler will
not be able to resolve all the ambiguous types when attempting to determine
which effect to run. It doesn't have to be polymorphic; if you know the actual
top-level application state type you can do @@Settings@ or whatever.

This all assumes you are running with the 'ProgramE' @τ@ effect in-scope. You
can achieve that as follows:

@
main :: 'IO' ()
main = 'Core.Program.Execute.execute' program

program :: 'Program' 'None' ()
program = do
    -- in Program τ, where τ is None here
    context <- 'Core.Program.Execute.getContext'
    'Control.Monad.IO.Class.liftIO' $ do
        -- in IO
        'Effectful.Internal.Monad.runEff' $ do
            -- in (IOE :> es) => Eff es
            'runProgramE' context $ do
                -- in (IOE :> es, ProgramE τ :> es) => Eff es
                ...
@
-}
module Core.Effect.Effectful
    ( -- * Effect
      ProgramE
    , runProgramE

      -- * Lifting and unlifting
    , withProgram
    , withProgram'
    )
where

--
-- We follow the convention used elsewhere in this collection of libraries of
-- using a qualified name for the imports of significant libraries. It's a bit
-- cumbersome, but makes it easier to disambiguate what's going on when
-- comparing to almost identical code in sibling modules covering other
-- webserver frameworks.
--

import Core.Program.Context
import Core.System.Base
import Data.Kind (Type)
import Effectful.Internal.Effect (type (:>))
import Effectful.Internal.Effect qualified as Effect
    ( Effect
    )
import Effectful.Internal.Env qualified as Effect
    ( Dispatch (Static)
    , DispatchOf
    , SideEffects (WithSideEffects)
    )
import Effectful.Internal.Monad qualified as Effect
    ( Eff
    , IOE
    , StaticRep
    , evalStaticRep
    , getStaticRep
    , putStaticRep
    , withEffToIO
    )

-------------------------------------------------------------------------------
-- Effect

{- |
An effect giving you access to return to the 'Program' @τ@ monad.
-}
data ProgramE (τ :: Type) :: Effect.Effect

type instance Effect.DispatchOf (ProgramE τ) = 'Effect.Static 'Effect.WithSideEffects
newtype instance Effect.StaticRep (ProgramE τ) = ProgramE (Context τ)

-------------------------------------------------------------------------------
-- Interpretation

{- |
Given you are in the 'Effectful.Internal.Monad.IOE' effect, raise the
currently in-scope effects to include the 'ProgramE' effect. This will
presumably be invoked fairly soon after entering the effect system, and it
needs to have been done inside a program that was started with
'Core.Program.Execute.execute' or 'Core.Program.Execute.executeWith'. Assuming
that to be the case, get the 'Context' @τ@ object from the outside edge of
your program using 'getContext' and then provide it to this function at the
earliest opportunity.
-}
runProgramE
    :: forall τ es α
     . Effect.IOE :> es
    => Context τ
    -> Effect.Eff (ProgramE τ : es) α
    -> Effect.Eff es α
runProgramE :: forall τ (es :: [Effect]) α.
(IOE :> es) =>
Context τ -> Eff (ProgramE τ : es) α -> Eff es α
runProgramE Context τ
context = forall (e :: Effect) (sideEffects :: SideEffects) (es :: [Effect])
       a.
(DispatchOf e ~ 'Static sideEffects, MaybeIOE sideEffects es) =>
StaticRep e -> Eff (e : es) a -> Eff es a
Effect.evalStaticRep (forall τ. Context τ -> StaticRep (ProgramE τ)
ProgramE Context τ
context)

--------------------------------------------------------------------------------
-- Wrappers

{- |
Simple variant of 'withProgram' which allows you to run a 'Program' @τ@ monad
action from within the effect system, provided that the 'ProgramE' @τ@ effect
is in scope.
-}
withProgram'
    :: forall τ es α
     . (Effect.IOE :> es, ProgramE τ :> es)
    => Program τ α
    -> Effect.Eff es α
withProgram' :: forall τ (es :: [Effect]) α.
(IOE :> es, ProgramE τ :> es) =>
Program τ α -> Eff es α
withProgram' Program τ α
action = do
    ProgramE Context τ
context1 <- forall (e :: Effect) (sideEffects :: SideEffects) (es :: [Effect]).
(DispatchOf e ~ 'Static sideEffects, e :> es) =>
Eff es (StaticRep e)
Effect.getStaticRep

    -- lift to IO
    forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ do
        -- now in IO. Lift to Program τ
        forall τ α. Context τ -> Program τ α -> IO α
subProgram Context τ
context1 forall a b. (a -> b) -> a -> b
$ do
            Program τ α
action

{- |
Run a 'Program' @τ@ monad action within the 'ProgramE' @τ@ effect.

This allows you the ability to lift to the 'Program' @τ@ monad, giving you the
ability to run actions that do logging, telemetry, input/output, and exception
handling, and then unlift back to the 'Eff' @es@ effect to continue work in
the effects system.

The order of the existential types in the @forall@ turned out to matter; it
allows you to use the @TypeApplications@ language extention to resolve the
ambiguous types when invoking this function.

See also "Core.Program.Unlift" for a general discussion of the unlifting
problem and in particular the 'Core.Program.Unlift.withContext' for a function
with a comparable type signature.
-}
withProgram
    :: forall τ es α
     . (Effect.IOE :> es, ProgramE τ :> es)
    => ((forall β. Effect.Eff es β -> Program τ β) -> Program τ α)
    -> Effect.Eff es α
withProgram :: forall τ (es :: [Effect]) α.
(IOE :> es, ProgramE τ :> es) =>
((forall β. Eff es β -> Program τ β) -> Program τ α) -> Eff es α
withProgram (forall β. Eff es β -> Program τ β) -> Program τ α
action = do
    -- extract Context τ
    ProgramE Context τ
context1 <- forall (e :: Effect) (sideEffects :: SideEffects) (es :: [Effect]).
(DispatchOf e ~ 'Static sideEffects, e :> es) =>
Eff es (StaticRep e)
Effect.getStaticRep

    -- lift to IO, using the provided specialized function which gives an unlift
    -- function for later use.
    forall (es :: [Effect]) a.
(HasCallStack, IOE :> es) =>
((forall r. Eff es r -> IO r) -> IO a) -> Eff es a
Effect.withEffToIO forall a b. (a -> b) -> a -> b
$ \forall r. Eff es r -> IO r
runInIO ->
        -- now in IO. Lift to Program τ
        forall τ α. Context τ -> Program τ α -> IO α
subProgram Context τ
context1 forall a b. (a -> b) -> a -> b
$ do
            -- now in Program τ. We form the function that will run an effect
            -- in Program τ, and pass it to the supplied action.
            (forall β. Eff es β -> Program τ β) -> Program τ α
action forall a b. (a -> b) -> a -> b
$ \Eff es β
inner -> do
                Context τ
context2 <- forall τ. Program τ (Context τ)
getContext
                forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ do
                    -- now in IO
                    forall r. Eff es r -> IO r
runInIO forall a b. (a -> b) -> a -> b
$ do
                        -- now in (IOE :> es, ProgramE :> es) => Eff es, but
                        -- we need to update the Context τ in the ProgramE τ
                        -- effect with the one that was present just before we
                        -- ran the unlifting function.
                        forall (e :: Effect) (sideEffects :: SideEffects) (es :: [Effect]).
(DispatchOf e ~ 'Static sideEffects, e :> es) =>
StaticRep e -> Eff es ()
Effect.putStaticRep (forall τ. Context τ -> StaticRep (ProgramE τ)
ProgramE Context τ
context2)

                        -- now we can proceed with running the nested effects.
                        Eff es β
inner