{-# 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