monad-effect: A fast and lightweight effect system.

[ bsd3, control, effect, library, monads ] [ Propose Tags ] [ Report a vulnerability ]

A fast and lightweight effect system. It provides a way to define and handle effects and exceptions in a modular and composable way. Main features: moduled effects, algebraic exceptions, pure states, and good performance.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
bencho0

Compile benchmarks with -O0

Disabled
bencho1

Compile benchmarks with -O1

Disabled
bencho2

Compile benchmarks with -O2

Disabled
noinline

Disable inlining for Countdown benchmark

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.2.0.0, 0.2.1.0, 0.2.2.0, 0.2.3.0
Change log CHANGELOG.md
Dependencies async (<2.4), base (>=4 && <5), data-default (>=0.8.0 && <0.9), deepseq (<1.6), exceptions (<0.11), haskell-src-meta (>=0.8 && <0.9), monad-control (>=1.0.3 && <1.1), mtl (<2.4), parsec (>=3 && <4), resourcet (>=1.3.0 && <1.4), stm (<2.6), template-haskell (<2.24), text (<2.2), transformers-base (>=0.4.6 && <0.5) [details]
License BSD-3-Clause
Author Eiko
Maintainer eikochanowo@outlook.com
Category Control, Monads, Effect
Source repo head: git clone https://github.com/Eiko-Tokura/monad-effect.git
Uploaded by eiko at 2025-12-13T12:13:58Z
Distributions NixOS:0.2.0.0
Reverse Dependencies 1 direct, 0 indirect [details]
Downloads 35 total (14 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for monad-effect-0.2.3.0

[back to package description]

monad-effect - a lightweight, fast, algebraic effect system

This project is still in experimental beta and may evolve quickly. Feedback and contributions are very welcome.

monad-effect gives you:

  • a single, optimisation-friendly monad transformer EffT that combines Reader, State and algebraic errors;
  • modules as the unit of effect (e.g. reader, state, logging, database, HTTP, metrics);
  • explicit, composable error lists instead of using Text / SomeException ; and
  • performant effect stacks, without sacrificing purity.

Most users will work with the Eff / EffT type aliases and the built-in reader/state modules (RModule, SModule) and define their own modules around them.


Project Intuition

At a high level you can think of:

newtype EffT mods es m a =
  EffT { unEffT :: SystemRead mods -> SystemState mods -> m (Result es a, SystemState mods) }

as:

  • one layer of Reader over a heterogeneous environment SystemRead mods,
  • one layer of State over a heterogeneous state SystemState mods, and
  • a typed, algebraic error channel Result es a, where es :: [Type] is a type-level list of error types.

You explicitly say:

  • which modules (mods) your effect depends on (configuration, mutable state, handles, etc.);
  • which errors (es) it can throw (e.g. IOException, ErrorText "http", MyDomainError); and
  • you get back both a result and the final module state.

Typical use-cases:

  • replace one or more layers of ReaderT, StateT, ExceptT or even more equivalent ones, by a single EffT with a small list of modules and error types;
  • easily add or remove error types (effCatch, effCatchIn, errorToEitherAll, ... );
  • run the same effect in pure monads (e.g. Identity) and in IO; and
  • pass around large module stacks in real applications while keeping the types informative and composable.

Key Features

Algebraic exceptions

In classic Haskell (and in other languages like Rust), exceptions are often encoded algebraically as Maybe or Either:

  • Maybe a is composable but not very informative - you lose any structured information about why something failed.
  • Either e a carries an error payload, but composing multiple distinct Either e_i a values across a codebase tends to either:
    • collapse everything to a common super-type like Text/SomeException (and then you lose the ability to catch specific errors in a principled way, and loses the ability to declare that some of them won't happen); or
    • nest Either e0 (Either e1 (Either e2 a)), which is unergonomic.
  • ExceptT e m a has the same compositional issues, and the transformer order matters:
    • StateT s (ExceptT e m) a ~ s -> m (Either e (a, s)) - once an exception is thrown, both a and the intermediate state are lost / rolled-back.
    • ExceptT e (StateT s m) a ~ s -> m (Either e a, s) - the state up to the exception point is preserved, which is often what you actually want.

monad-effect addresses these issues by:

  • using a type-level list of error types es :: [Type] and a non-empty sum EList es to track exactly which error types can occur; and
  • using Result es a as the algebraic error carrier (see the formal definition below), which behaves like Either (EList es) a and collapses to a when es ~ '[].

The underlying representation

SystemRead mods
-> SystemState mods
-> m (Result es a, SystemState mods)

means that module state is preserved when an algebraic exception is thrown (like ExceptT e (StateT s m) a), rather than discarded.

You can throw algebraic errors into the list (effThrowIn / effThrow), catch all errors (effCatchAll), or catch and remove a specific error type (effCatchIn) so that the remaining computation provably no longer produces that error.

Purity

Instead of reaching for IORef / TVar for every bit of mutable state, you can also choose to keep states pure and model them as part of your self-defined modules. It's a design choice you can make : some effect systems force you into IO. While for concurrency programs you need TVars, but we should have the ability to choose pure state where appropriate because we love purity.

In particular the library provides two built-in modules:

  • SModule s - a module holding a pure state of type s;
  • RModule r - a module holding a read-only value of type r.

These integrate with the MonadStateful / MonadReadable classes and provide the familiar getS / putS / modifyS / askR / localR APIs, while still participating in the larger module stack.

Using modules, you can:

  • keep configuration, pure in-memory state, handles, and effect interpreters in a single typed module stack; and
  • run the same code in Identity for pure tests, or in IO for production, by choosing appropriate runners.
  • Use it to run stateful tight computations without IO overhead (which GHC can optimize very well).

Template Haskell helpers in Module.RS.QQ (makeRModule, makeRSModule) make it easy to generate simple reader/state modules with minimal boilerplate. However, right now you are expected to write a lot of modules by hand as they are much more flexible.

Besides tight calculation that benefits from pure states, here is another example function that benefits from pure state: the function can be ran as a pure function with pure logging effect (writer / no-logging), or in IO whose logging prints to console/file.

eventHandler
  :: (Monad pureMonad)
  => InputEvent
  -> EffT
      '[ Logging pureMonad LogData -- ^ We will use logging to generate diagnostics
      , EventState                -- ^ We need to read and update the state
      ]
      '[ ErrorText "not-allowed"
      ]
      pureMonad
      [OutputCommands]      -- ^ Output commands from the event module

Flexible and modular

Because both modules and errors are tracked at the type level:

  • EffT '[] '[] m a is isomorphic to m a;
  • EffT '[] '[e] m a is isomorphic to m (Either e a);
  • EffT '[] es m a is isomorphic to m (Result es a).

You can:

  • eliminate modules once you have their inputs, using e.g.:

    runEffTOuter
      :: (ConsFDataList c (mod : mods), ConsFData1 c mods, Monad m)
      => ModuleRead mod
      -> ModuleState mod
      -> EffT' c (mod : mods) es m a
      -> EffT' c mods es m (a, ModuleState mod)
    
    runEffTOuter_
      :: ...
      => ModuleRead mod -> ModuleState mod
      -> EffT' c (mod : mods) es m a
      -> EffT' c mods es m a
    

    and similarly runEffTIn/runEffTIn_ to drop an inner module.

  • embed smaller effects inside larger ones, changing only modules (embedMods), only errors (embedError), or both (embedEffT).

  • move fluidly between EffT and more conventional forms via runners and converters like runEffT00, runEffT01, errorToEither, errorToEitherAll, errorToMaybe, and the effEither* / effMaybe* family of helpers.

The result is a small set of primitives that scale well to large applications with many modules such as database access, HTTP clients, metrics, logging, and domain-specific state.

Performant

The core transformer EffT' is one single layer and the FData data family is designed to be optimisation-friendly. The fact that we did not use complicated data structure nor IO for storing data means GHC can optimize, inline the data constructors, utilizing purity.

This gives blazingly fast performance, benchmarks (countdown, local state, deep catch) shows monad-effect is top-1 in most of the benchmarks, top-2 in some other ones. (See the last section for some benchmark pictures).

It is completely feasible to use monad-effect for pure single-threaded stateful tight loops with little or no extra overhead.

Core Types and Abstractions

This section spells out the main types and how they fit together. All snippets in this section are taken directly from the library, except where explicitly marked as simplified.

EffT, Eff and EffT'

The real core transformer is EffT'; Eff and EffT are its specialised aliases using the optimised FData container.

-- | EffTectful computation, using modules as units of effect.
-- The tick indicates the polymorphic type 'c', the data structure
-- used to store the modules (usually 'FData' or 'FList').
newtype EffT' (c    :: (Type -> Type) -> [Type] -> Type)
              (mods :: [Type])
              (es   :: [Type])
              (m    :: Type -> Type) a
  = EffT'
      { unEffT' :: SystemRead  c mods
                -> SystemState c mods
                -> m (Result es a, SystemState c mods)
      }

-- Recommended, specialised aliases (use these in normal code):
type Eff   mods es  = EffT' FData mods es IO
type EffT  mods es  = EffT' FData mods es
type Pure  mods es  = EffT' FData mods es Identity
type In    mods es  = In'   FData mods es

-- Error-enhanced IO and ExceptT-like transformer
type ResultT  es m  = EffT' FData '[] es m
type IO'      es    = EffT' FData '[] es IO

Intuitively:

  • mods :: [Type] - type-level list of modules (effects);
  • es :: [Type] - type-level list of error types that this computation may throw;
  • m - the base monad; and
  • c - the container type (FData by default) used to hold the module environments and states.

The library provides runners such as:

runEffT
  :: Monad m
  => SystemRead  c mods
  -> SystemState c mods
  -> EffT' c mods es m a
  -> m (Result es a, SystemState c mods)

-- No modules
runEffT0
  :: (Monad m, ConsFNil c)
  => EffT' c '[] es m a
  -> m (Result es a)

-- No modules, no errors
type NoError = '[]

runEffT00
  :: (Monad m, ConsFNil c)
  => EffT' c '[] NoError m a
  -> m a

-- No modules, single error, exposed as Either
runEffT01
  :: (Monad m, ConsFNil c)
  => EffT' c '[] '[e] m a
  -> m (Either e a)

There are much more runners and combinators, see actual haddock documentation.

EffT and Eff can therefore be specialised to behave like a more flexible ExceptT:

-- A flexible replacement for 'ExceptT es m'
type ResultT es m = EffT '[] es m

Result and EList - algebraic error lists

Errors are represented by the Result and EList types:

-- | Sum of types, non-empty by construction.
data EList (ts :: [Type]) where
  EHead :: !t          -> EList (t : ts)
  ETail :: !(EList ts) -> EList (t : ts)

-- | Error-aware result.
data Result (es :: [Type]) (a :: Type) where
  RSuccess :: a         -> Result es a
  RFailure :: !(EList es) -> Result es a

resultNoError :: Result '[] a -> a
resultNoError (RSuccess a) = a

Important facts:

  • Result es a behaves like Either (EList es) a.
  • When es ~ '[], Result '[] a is effectively just a (resultNoError witnesses this).
  • EList es is a non-empty sum type: you cannot construct an EList '[], so a RFailure always carries one of the listed error types.

Named error types - ErrorText, ErrorValue, MonadExcept

To avoid defining a new ADT for every small error case, the library provides ad-hoc, named error wrappers:

-- | A named textual error.
newtype ErrorText (s :: k) = ErrorText Text
  deriving newtype (IsString)

-- | Use type application 'errorText @"http" "text"'
errorText :: forall s. Text -> ErrorText s
errorText = ErrorText

-- | A named error that wraps an arbitrary value.
newtype ErrorValue (a :: k) (v :: Type) = ErrorValue v

-- | Type application helper
errorValue :: forall s v. v -> ErrorValue s v
errorValue = ErrorValue

-- | MonadExcept without a functional dependency,
-- so a monad can throw multiple error types.
class Monad m => MonadExcept e m where
  throwExcept :: e -> m a

Some useful instances:

  • instance Exception e => MonadExcept e IO
  • instance MonadExcept e (Either e)
  • instance Monad m => MonadExcept e (ExceptT e m)
  • instance KnownSymbol s => MonadExcept (ErrorText s) (Either (Text, Text))

This makes it very convenient to use ErrorText "http", ErrorText "decode" etc. in larger codebases.

Modules and the system view

Modules are the unit of effect. A module describes:

  • what read-only data it exposes (ModuleRead); and
  • what mutable state it keeps (ModuleState).
class Module mod where
  data ModuleRead  mod :: Type
  data ModuleState mod :: Type

-- Modules that may be part of a "system"
class Module mod => SystemModule mod where
  data ModuleEvent    mod :: Type
  data ModuleInitData mod :: Type

-- System-wide containers (usually backed by 'FData')
type SystemRead     c mods = c ModuleRead  mods
type SystemState    c mods = c ModuleState mods
type SystemEvent      mods = UList ModuleEvent    mods
type SystemInitData c mods = c ModuleInitData     mods

Within EffT' you can access the data families, module reads and states using the provided helpers:

-- | Synonyms
queryModule  :: (Monad m, In' c mod mods, Module mod)
             => EffT' c mods es m (ModuleRead  mod)
askModule    :: (Monad m, In' c mod mods, Module mod)
             => EffT' c mods es m (ModuleRead  mod)

queriesModule, asksModule
  :: (Monad m, In' c mod mods, Module mod)
  => (ModuleRead mod -> a)
  -> EffT' c mods es m a

getModule    :: (Monad m, In' c mod mods, Module mod)
             => EffT' c mods es m (ModuleState mod)
getsModule   :: (Monad m, In' c mod mods, Module mod)
             => (ModuleState mod -> a) -> EffT' c mods es m a

putModule    :: (Monad m, In' c mod mods, Module mod)
             => ModuleState mod -> EffT' c mods es m ()
modifyModule :: (Monad m, In' c mod mods, Module mod)
             => (ModuleState mod -> ModuleState mod)
             -> EffT' c mods es m ()

The SystemModule/ModuleEvent/ModuleInitData pieces are primarily used by higher-level orchestration helpers (e.g. scoped initialisation via withModule).

Built-in Reader and State modules - RModule and SModule

The Module.RS module gives you ready-made reader/state modules and helpers to integrate existing ReaderT/StateT code.

-- Reader module
data RModule (r :: Type)

instance Module (RModule r) where
  newtype ModuleRead  (RModule r) = RRead  { rRead  :: r }
  data    ModuleState (RModule r) = RState deriving (Generic, NFData)

-- State module
data SModule (s :: Type)

instance Module (SModule s) where
  data    ModuleRead  (SModule s) = SRead
  newtype ModuleState (SModule s) = SState { sState :: s }
    deriving newtype (Generic, NFData)

Convenience helpers:

-- Reader-like interface
askR   :: (Monad m, In' c (RModule r) mods) => EffT' c mods errs m r
asksR  :: (Monad m, In' c (RModule r) mods) => (r -> a) -> EffT' c mods errs m a
localR :: (Monad m, In' c (RModule r) mods)
       => (r -> r) -> EffT' c mods errs m a -> EffT' c mods errs m a

-- State-like interface
getS   :: (Monad m, In' c (SModule s) mods) => EffT' c mods errs m s
getsS  :: (Monad m, In' c (SModule s) mods) => (s -> a) -> EffT' c mods errs m a
putS   :: (Monad m, In' c (SModule s) mods) => s -> EffT' c mods errs m ()
modifyS:: (Monad m, In' c (SModule s) mods) => (s -> s) -> EffT' c mods errs m ()

You also get helpers to run and embed modules:

runRModule   :: (ConsFDataList c (RModule r : mods), Monad m)
             => r -> EffT' c (RModule r : mods) errs m a
             -> EffT' c mods errs m a

runSModule   :: (ConsFDataList c (SModule s : mods), Monad m)
             => s -> EffT' c (SModule s : mods) errs m a
             -> EffT' c mods errs m (a, s)

runSModule_  :: (ConsFDataList c (SModule s : mods), Monad m)
             => s -> EffT' c (SModule s : mods) errs m a
             -> EffT' c mods errs m a

Integrating with ReaderT/StateT (more in Module.RS):

liftReaderT :: forall r mods errs m c a. (Monad m, In' c (RModule r) mods)
            => ReaderT r m a
            -> EffT' c mods errs m a

embedReaderT :: forall r mods errs m c a. (Monad m, In' c (RModule r) mods)
             => ReaderT r (EffT' c mods errs m) a
             -> EffT' c mods errs m a

RS.Class - MonadReadOnly, MonadReadable, MonadStateful

Control.Monad.RS.Class defines type-class interfaces similar to MonadReader/MonadState, but without functional dependencies, so a single monad can have many readable and stateful values:

class Monad m => MonadReadOnly r m where
  query   :: m r
  queries :: (r -> r') -> m r'

class MonadReadOnly r m => MonadReadable r m where
  local :: (r -> r) -> m a -> m a

class Monad m => MonadStateful s m where
  get    :: m s
  put    :: s -> m ()
  gets   :: (s -> a) -> m a
  modify :: (s -> s) -> m ()

Instances are provided for EffT' and for monad transformers.


Getting Started - Examples

This section focuses on how to use the core abstractions.

Quick start - algebraic state and errors

A small (made-up) example that:

  • stores a Map as a state module,
  • throws a named ErrorText "Map.keyNotFound" when a key is missing,
  • and computes an average from a file.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}

module Examples where

import Control.Exception (IOException)
import Control.Monad
import Control.Monad.Effect        -- EffT types and combinators
import Module.RS                   -- RModule / SModule helpers
import System.IO
import qualified Data.Map  as M
import qualified Data.Text as T

-- | Wrap a lookup into an algebraic error instead of 'Maybe'.
myLookup
  :: (Show k, Ord k, Monad m)
  => k
  -> EffT '[SModule (M.Map k v)] '[ErrorText "Map.keyNotFound"] m v
myLookup k =
  effMaybeInWith -- this converts a (Maybe v) return value into an algebraic error
    (errorText @"Map.keyNotFound" $
       " where key = " <> T.pack (show k)) -- using this converter function
    (getsS (M.lookup k))   -- :: EffT '[SModule (M.Map k v)] es m (Maybe v)

-- | This effect can run in pure monads like 'Identity' as well as 'IO'.
lookups
  :: forall v m. Monad m
  => EffT '[SModule (M.Map T.Text v)] '[ErrorText "Map.keyNotFound"] m (v, v, v)
lookups = do
  foo <- myLookup "foo"
  bar <- myLookup "bar"
  baz <- myLookup "baz"
  pure (foo, bar, baz)

parse :: String -> Maybe [Double]
parse = undefined  -- parsing logic

computeAverageFromFile
  :: FilePath
  -> Eff
       '[SModule (M.Map T.Text Int)]
       [ IOException
       , ErrorText "empty-file"
       , ErrorText "zero-numbers"
       , ErrorText "Map.keyNotFound"
       ]
       Double
computeAverageFromFile fp = do
  -- Capture 'IOException' from 'readFile' as an algebraic error.
  content <- embedError . liftIOException $ readFile' fp

  when (null content) $
    effThrowIn (errorText @"empty-file" "file is empty")

  -- Turn a 'Maybe' into an ad-hoc algebraic error and immediately handle it.
  parsed <- pureMaybeInWith (errorText @"parse-error" "parse error") (parse content)
              `effCatch` \(_ :: ErrorText "parse-error") ->
                pure [0]

  -- Use another effect that requires the same module.
  _ <- embedEffT (lookups @Int)

  when (null parsed) $
    effThrowIn (errorText @"zero-numbers" "zero numbers")

  pure $ sum parsed / fromIntegral (length parsed)

You can run such effects in various ways. For example, using the state module runner from Module.RS and runEffT01:

import Control.Monad.Effect
import Module.RS
import qualified Data.Map as M

runCounter
  :: Int
  -> Eff '[SModule Int] '[ErrorText "zero"] IO ()
  -> IO (Either (ErrorText "zero") ((), Int))
runCounter initial action =
  runEffT01 (runSModule initial action)

Here:

  • runSModule eliminates the SModule from the module list while threading the state, and
  • runEffT01 eliminates the remaining error list as an Either.

Embedding and reshaping effects

Sometimes you want to run a smaller effect inside a bigger one; or change modules while keeping the error list, or vice versa. The library provides:

-- Embed a smaller effect into a larger one (modules and/or errors).
embedEffT
  :: (SubList c mods mods', SubListEmbed es es', Monad m)
  => EffT' c mods es  m a
  -> EffT' c mods' es' m a

-- Only change the module list.
embedMods
  :: (Monad m, ConsFDataList c mods', SubListEmbed es es, SubList c mods mods')
  => EffT' c mods es m a
  -> EffT' c mods' es m a

-- Only change the error list.
embedError
  :: (Monad m, SubList c mods mods, SubListEmbed es es')
  => EffT' c mods es m a
  -> EffT' c mods es' m a

This is very useful when you have a reusable component that only needs a subset of modules.

Scoped module initialisation

For scoped initialisation and teardown, many projects define higher-level wrappers around the Loadable/withModule pattern. A (simplified) example from a real project:

runApp
  :: EffT
       '[ RModule ProxyState
        , RModule ProxySettings
        , PrometheusMan
        , LoggingModuleB
        ]
       NoError
       IO
       ()
  -> IO ()
runApp app = do
  -- Parse options, set up logging and metrics, etc...
  opts :: ProxyOptions Unwrapped <- unwrapRecord "Haskell Proxy Server"
  case optionsToSettings opts of
    Nothing       -> putStrLn "Invalid options provided."
    Just settings -> do
      -- Initialise loggers (implementation-specific).
      baseStdLogger  <- ...
      baseFileLogger <- ...
      let logger = baseStdLogger <> baseFileLogger

      -- Compose initialisation modules inside Eff, then eliminate back to IO.
      runEffT00 $
        withLoggerCleanup logger $
        (if settings.setPrometheus
           then withPrometheus ...
           else withNoPrometheusMan) $
        do
          state <- initializeState settings
          runRModule settings $
          runRModule state    $
          app

The exact set of modules (PrometheusMan, LoggingModuleB, ...) is either project-specific or reusable components you can build between projects, but the pattern is always:

build an EffT stack of modules, run your application logic there, then eliminate modules and errors with the provided runners.

Large application - a bot with many modules

In a larger system you might have many modules and a domain-specific error list. (Taken from a real-world bot application.)

-- The modules loaded into the bot
type Mods =
  [ LogDatabase
  , AsyncModule
  , ProxyWS
  , CronTabTickModule
  , StatusMonitorModule
  , CommandModule
  , RecvSentCQ
  , MeowActionQueue
  , SModule BotConfig
  , SModule WholeChat
  , SModule OtherData
  , MeowConnection
  , BotGlobal
  , ConnectionManagerModule
  , MeowDataDb
  , MeowCoreDb
  , PrometheusMan
  , LoggingModule
  ]

-- Exceptions that require restarting the bot
type MeowErrs =
  '[ ErrorText "recv_connection"
   , ErrorText "send_connection"
   , ErrorText "meowCoreDb"
   ]

type MeowT mods m = EffT mods MeowErrs m
type Meow         = MeowT Mods IO

The bot initialisation then becomes a composition of withX helpers and run*Module runners:

runBot
  :: BotInstance -- ^ Initial bot configuration
  -> Meow a -- ^ the bot loop function
  -> EffT
       '[ BotGlobal
        , ConnectionManagerModule
        , MeowDataDb
        , MeowCoreDb
        , PrometheusMan
        , LoggingModule
        ]
       '[ErrorText "meowCoreDb"]
       IO
       ()
runBot bot meow = do
  -- embed some code into a larger context
  botModule <- embedEffT $ botInstanceToModule bot

  -- Build additional configuration/state modules...
  -- initialise counters, status, etc.
  -- not relevant for this example.

  embedNoError -- requires the scope inside to have 'NoError' and embeds it to a larger error context
    $ effAddLogCat' (LogCat botModule.botId) -- adds a logging category to logs within the scope
    $ ( case botRunFlag bot of
          RunClient addr port -> void . withClientConnection addr port
          RunServer addr port ->        withServerConnection addr port
      ) -- running connection
    $ (\app -> do
        AllData wc bc od <- embedEffT $ initAllData botconfig
        runSModule_ od $ runSModule_ wc $ runSModule_ bc $ app
      ) -- domain state
    $ withMeowActionQueue
    $ withRecvSentCQ
    $ withModule CommandModuleInitData
    $ withModule (StatusMonitorModuleInitData meowStat)
    $ maybe id (\init -> withWatchDog init . const) mWatchDogInit
    $ withCronTabTick
    $ withProxyWS (ProxyWSInitData [(add, ip) | ProxyFlag add ip <- bot.botProxyFlags])
    $ withAsyncModule
    $ withLogDatabase
    $ meow

The details of withMeowActionQueue, withRecvSentCQ, withProxyWS, etc. are application-specific, but the pattern is always:

  • each withX introduces one or more modules into the EffT stack and arranges their initial ModuleRead/ModuleState; and
  • the final Meow computation runs in a rich module environment, with its error list (MeowErrs) tracking only the domain errors that matter at that layer.

Example - database access

You can also use the type system to enforce that certain low-level errors are handled at the call-site. A typical pattern is to wrap a database action so it:

  • requires a specific module to be present; and
  • requires a specific error to be in the error list:
runMeowDataDB
  :: ( In'   c MeowDataDb mods
     , InList (ErrorText "meowDataDb") es
     )
  => ReaderT SqlBackend IO b
  -> EffT' c mods es IO b

Because ErrorText "meowDataDb" is in the error list, callers must either:

  • keep that error in their own es (and propagate it upward), or
  • explicitly catch and handle it. If they forget, GHC will report a type error.

For example:

-- Forgetting to handle 'ErrorText "meowDataDb"' here leads to a type error.
fetchBlockMessages :: BotId -> ChatId -> ChatBlockSpan -> Meow [ChatMessage]
fetchBlockMessages bid cid span = do
  entities <-
    runMeowDataDB (selectList [...conditions based on 'bid', 'cid', 'span'...])
      `effCatch` \(_ :: ErrorText "meowDataDb") ->
        pure []
  pure (map entityVal entities)

Here effCatch both catches the database error and removes ErrorText "meowDataDb" from the error list, so the rest of fetchBlockMessages no longer has to account for it.


Selected API Reference

This section is not exhaustive. It highlights key exports; for full details, please consult the Haddock documentation.

Core monad and runners

  • Eff mods es a, EffT mods es m a, Pure mods es a, ResultT es m a, IO' es m a
  • runEffT, runEffT_, runEffT0, runEffT00, runEffT01, runResultT
  • runEffTOuter, runEffTOuter', runEffTOuter_ - eliminate the outermost module while supplying its ModuleRead/ModuleState
  • runEffTIn, runEffTIn', runEffTIn_ - eliminate an inner module identified by its type
  • replaceEffTIn - replace a module with another, using custom conversion functions
  • NoError, checkNoError, declareNoError, embedNoError
  • applyErrors, applyMods - helpers that expose es / mods to type applications without changing the value

Error machinery

  • Types: Result es a, EList es, SystemError
  • Named wrappers: ErrorText s, ErrorValue s v, errorText, errorValue
  • Throwing:
    • effThrowIn, effThrow - throw an error
    • effThrowEList, effThrowEListIn - throw multiple errors via EList
    • MonadExcept e m integration (e.g. via tryAndThrow, tryAndThrowText)
  • Catching:
    • effCatch - catch the first error in the list
    • effCatchIn - catch a specific error type and remove it from the list
    • effCatchAll - catch all algebraic errors as an EList es
  • Converting errors:
    • errorToEither, errorToEitherAll, eitherAllToEffect
    • errorInToEither, errorToMaybe, errorInToMaybe, errorToResult
    • mapError - map one error list into another
  • Turning Either / Maybe into errors:
    • effEitherWith, effEither
    • effEitherInWith, effEitherIn, effEitherSystemException
    • effMaybeWith, effMaybeInWith
    • pureMaybeInWith, pureEitherInWith
    • baseEitherIn, baseEitherInWith, baseMaybeInWith

IO lifting and exception bridging

These functions help you bridge IO exceptions into algebraic errors:

  • liftIOException :: MonadIO m => IO a -> EffT' c mods '[IOException] m a
  • liftIOAt :: (Exception e, MonadIO m) => IO a -> EffT' c mods '[e] m a
  • liftIOSafeWith :: (Exception e', MonadIO m) => (e' -> e) -> IO a -> EffT' c mods '[e] m a
  • liftIOText :: MonadIO m => (Text -> Text) -> IO a -> EffT' c mods '[ErrorText s] m a
  • liftIOPrepend :: Text -> IO a -> EffT' c mods '[ErrorText s] IO a

Try/catch style:

  • effTry, effTryWith - catch exceptions thrown in the base monad and turn them into algebraic errors
  • effTryIO, effTryIOWith, effTryIOIn, effTryIOInWith
  • effTryUncaught - catch uncaught exceptions into error lists
  • tryAndThrow, tryAndThrowWith, tryAndThrowText - lift IO and rethrow via MonadExcept

Modules and module helpers

  • Core type classes:
    • Module - defines ModuleRead and ModuleState associated data families
    • SystemModule - extends Module with ModuleEvent and ModuleInitData
    • Loadable c mod mods es - provides withModule for scoped module initialisation
  • System-wide aliases:
    • SystemRead c mods, SystemState c mods
    • SystemEvent mods, SystemInitData c mods
    • SystemError
  • Accessors from Control.Monad.Effect:
    • queryModule, queriesModule, askModule, asksModule
    • getModule, getsModule, putModule, modifyModule

withModule (from Control.System) is particularly useful for implementing custom withX helpers that allocate resources, push a module on the stack, run an EffT computation, and then clean up.

Bracket patterns and concurrency

Resource-safe patterns:

  • maskEffT - mask in the base monad while staying in EffT'
  • generalBracketEffT, generalBracketEffT'
  • bracketEffT, bracketEffT'
  • bracketOnErrorEffT, bracketOnErrorEffT'

Concurrency:

  • forkEffT - fork an EffT computation onto a new thread
  • forkEffTFinally - variants with finalisers
  • asyncEffT, withAsyncEffT, withAsyncEffT' - integrate async with EffT
  • restoreAsync, restoreAsync_ - restore an EffT computation from an Async result

RS modules and interfaces

From Module.RS:

  • Types:
    • RModule r, RNamed name r
    • SModule s, SNamed name s
  • Running modules:
    • runRModule, runRModuleIn
    • runSModule, runSModule_, runSModuleIn
  • Embedding existing ReaderT/StateT:
    • liftReaderT, embedReaderT, addReaderT, asReaderT
    • liftStateT, embedStateT, addStateT, asStateT
  • Convenience:
    • askR, asksR, localR
    • getS, getsS, putS, modifyS
    • readOnly - treat a state module as a read-only module inside a scope

From Control.Monad.RS.Class:

  • MonadReadOnly r m, MonadReadable r m, MonadStateful s m - reader/state-like APIs without functional dependencies; EffT' has instances for these.

From Control.Monad.Class.Except:

  • MonadExcept e m - multiple error types per monad
  • ErrorText, ErrorValue, errorText, errorValue

Template Haskell utilities

From Module.RS.QQ:

  • makeRModule, makeRModule_, makeRModule__
  • makeRSModule, makeRSModule_

makeRModule example (simplified):

[makeRModule|MyModule
  myRecord1 :: !MyType1
  myRecord2 ::  MyType2
|]

Generates (conceptually):

  • data MyModule
  • instance Module MyModule with:
    • data ModuleRead MyModule = MyModuleRead { myRecord1 :: !MyType1, myRecord2 :: MyType2 }
    • data ModuleState MyModule = MyModuleState (plus derivations)
  • instance SystemModule MyModule with:
    • data ModuleEvent MyModule
    • data ModuleInitData MyModule
  • runners runMyModule, runMyModuleIn, etc., and convenient type synonyms for ModuleRead / ModuleState.

makeRSModule similarly builds a combined reader/state module from a compact specification, including optional lens generation for fields tagged with Lens.


Performance, style and benchmarks

The core EffT design is very optimisation-friendly:

  • modules are stored in a specialised data family FData, not a linked list;
  • FData is generated by Template Haskell up to a fixed length (e.g. FData3, FData4, ...) with strict fields; and
  • GHC can often optimise EffT-based code down to tight loops, competitive with or better than hand-written StateT/ExceptT stacks.

The benchmarks in benchmark/ compare:

  • EffT with FList (heterogeneous list),
  • EffT with FData, and
  • StateT from mtl.

On typical countdown/state benchmarks:

  • EffT + FData is around 25 times faster than StateT without optimisation, and
  • about as fast as a properly optimised StateT (-O2 -flate-dmd-anal).

See the SVG charts under benchmark/bench-result-* in the repository for details.

Some Benchmarks

See the benchmark folder for more benchmarks. The benchmarks are copied from heftia, another effect system library, with some modifications.

Countdown -O2

Countdown-O2

Countdown -O0

Countdown-O0

Deep Catch -O2

Catch-O2

Deep Catch -O0

Catch-O0

Local State -O2

Local-O2

Local State -O0

Local-O0

Flags

GHC's type-checker sometimes needs more fuel for large module/error lists. It is recommended to build with:

-fconstraint-solver-iterations=16

or slightly higher when using very deep stacks.

Style and module design

monad-effect does not impose a particular way to structure your modules. You can:

  • package a concrete implementation (e.g. Prometheus counter, HTTP manager, database connection pool) directly into a module; or
  • use a more “algebraic effects” style, where modules carry handlers for an algebraic effect GADT.

An example of the latter is a Prometheus counter module that carries a handler as read-only state

{-# LANGUAGE DataKinds, TypeFamilies, RequiredTypeArguments #-}
module Module.Prometheus.Counter where

import Control.Monad.Effect
import System.Metrics.Prometheus.Metric.Counter as C

-- | A prometheus counter module that has a name
data PrometheusCounter (name :: k)

-- | Counter effects written in algebraic effect style
data PrometheusCounterEffect a where
  AddAndSampleCounter :: Int -> PrometheusCounterEffect CounterSample
  AddCounter          :: Int -> PrometheusCounterEffect ()
  IncCounter          ::        PrometheusCounterEffect ()
  SetCounter          :: Int -> PrometheusCounterEffect ()
  SampleCounter       ::        PrometheusCounterEffect CounterSample

-- | The effect handler type for a prometheus counter with given counter name
type PrometheusCounterHandler (name :: k) = forall c mods es m a. (In' c (PrometheusCounter name) mods, MonadIO m) => PrometheusCounterEffect a -> EffT' c mods es m a

-- | The module is declared as a reader module that carries a counter handler
instance Module (PrometheusCounter name) where
  newtype ModuleRead  (PrometheusCounter name) = PrometheusCounterRead { prometheusCounterHandler :: PrometheusCounterHandler name }
  data    ModuleState (PrometheusCounter name) = PrometheusCounterState

-- | Specify / interpret a counter effect with given counter name
runPrometheusCounter
  :: forall name
  -> ( ConsFDataList c (PrometheusCounter name : mods)
     , Monad m
     )
  => PrometheusCounterHandler name -> EffT' c (PrometheusCounter name ': mods) es m a -> EffT' c mods es m a
runPrometheusCounter name handler = runEffTOuter_ (PrometheusCounterRead @_ @name handler) PrometheusCounterState
{-# INLINE runPrometheusCounter #-}

-- | Carry out a counter effect with given counter name
prometheusCounterEffect :: forall name -> (In' c (PrometheusCounter name) mods, MonadIO m) => PrometheusCounterEffect a -> EffT' c mods es m a
prometheusCounterEffect name eff = do
  PrometheusCounterRead handler <- askModule @(PrometheusCounter name)
  handler eff
{-# INLINE prometheusCounterEffect #-}

-- | Use a specific counter to carry out a counter effect
useCounter :: Counter -> PrometheusCounterHandler name
useCounter counter IncCounter              = liftIO $ C.inc counter
useCounter counter (AddCounter n)          = liftIO $ C.add n counter
useCounter counter (SetCounter n)          = liftIO $ C.set n counter
useCounter counter (AddAndSampleCounter n) = liftIO $ C.addAndSample n counter
useCounter counter SampleCounter           = liftIO $ C.sample counter
{-# INLINE useCounter #-}

-- | A counter handler that does nothing
noCounter :: Monad m => PrometheusCounterEffect a -> EffT mods es m a
noCounter IncCounter              = pure ()
noCounter (AddCounter _)          = pure ()
noCounter (SetCounter _)          = pure ()
noCounter (AddAndSampleCounter _) = pure (CounterSample 0)
noCounter SampleCounter           = pure (CounterSample 0)
{-# INLINE noCounter #-}

Documentation changes from previous versions

Compared to earlier versions of the README:

  • The formal definition of Result has been updated to match the code in Data.Result (single RSuccess constructor, with Result '[] a behaving like a).

  • All examples now use the errorText / errorValue smart constructors instead of directly constructing using constructors ErrorText / ErrorValue. This avoids the filling kind type parameter ErrorText @_ @"..." noise.

  • The myLookup / computeAverageFromFile example has been synchronised with the code in test/Examples.hs and corrected to use errorText @"Map.keyNotFound" with a proper Text value.

  • Experimental system orchestration types (WithSystem, EventLoopSystem, event loops) and the Resource module are intentionally not described in detail here, as they may change or be removed in future versions. The focus is on EffT, modules, algebraic errors, and the RS/Except helper classes.

  • Minor wording updates were made throughout to reflect that EffT/Eff (with FData) are the recommended entry points for most users.

For full API details, please refer to the Haddock documentation generated from the source.