# Haskell Effectful

> Design and use Haskell effect stacks with the `effectful` library. Covers defining effects (`Effect` GADTs, `DispatchOf`), writing interpreters (production + in-memory for tests), polymorphic business logic over an effect set, static vs dynamic dispatch, and composition in `main`. Use whenever the user is wiring up application architecture, deciding where IO lives, or modeling a capability as an effect.

- Skill: `ivelten/haskell-effectful` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ivelten/haskell-effectful`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ivelten/haskell-effectful/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ivelten (https://skillmd.com/u/ivelten)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ivelten/haskell-effectful

---


# Effect Management with `effectful`

Default to [`effectful`](https://hackage.haskell.org/package/effectful) for any non-trivial application. Don't use `mtl`, `polysemy`, or `fused-effects` unless the user asks — `effectful` is modern, fast, and avoids the `n²` instances problem of mtl-style stacks.

An effect declares an abstract capability (operations the program needs) without committing to an implementation; an interpreter binds those operations to concrete code. Production and tests use different interpreters of the same effect, which is how the same business logic runs against Postgres in production and against an in-memory map in tests.

## The pattern

```haskell
import Effectful
import Effectful.Reader.Static  (Reader, runReader, ask)
import Effectful.Error.Static   (Error, runError, throwError)

-- Domain effect, defined as a data type (needs GADTs + TypeFamilies)
data UserStore :: Effect where
  GetUser  :: UserId -> UserStore m (Maybe User)
  SaveUser :: User   -> UserStore m ()

type instance DispatchOf UserStore = Dynamic

-- Smart constructors
getUser :: UserStore :> es => UserId -> Eff es (Maybe User)
getUser uid = send (GetUser uid)

saveUser :: UserStore :> es => User -> Eff es ()
saveUser u = send (SaveUser u)

-- Business logic is polymorphic over the effect set
registerUser
  :: (UserStore :> es, Error AppError :> es)
  => Email -> Eff es User
registerUser email = ...

-- Interpreter for production
runUserStorePostgres
  :: (IOE :> es, Reader DbPool :> es)
  => Eff (UserStore : es) a -> Eff es a
runUserStorePostgres = interpret $ \_ -> \case
  GetUser uid -> ...
  SaveUser u  -> ...

-- Interpreter for tests (in-memory)
runUserStoreInMemory :: Eff (UserStore : es) a -> Eff es a
runUserStoreInMemory = ...
```

## Language extensions, in both places

The effect module needs three, and the reason for `DataKinds` is not obvious:

```haskell
{-# LANGUAGE DataKinds #-}     -- 'Dynamic' in the DispatchOf instance is a promoted constructor
{-# LANGUAGE GADTs #-}         -- the effect is declared as a GADT
{-# LANGUAGE TypeFamilies #-}  -- 'type instance DispatchOf'
```

Without `DataKinds` the failure names the wrong thing:

```
Not in scope: type constructor or class 'Dynamic'
  Perhaps you intended to use DataKinds to refer to the data constructor of that name?
```

**Every interpreter module needs `GADTs` too**, even though it only pattern matches:

```haskell
{-# LANGUAGE GADTs #-}
```

Matching the effect's constructors inside `interpret $ \_ -> \case` is a GADT match, and the
type refinement is only sound with `MonoLocalBinds`, which `GADTs` implies. It compiles without
the extension, so the mistake is easy to keep — GHC only warns:

```
[GHC-58008] [-Wgadt-mono-local-binds]
    Pattern matching on GADTs without MonoLocalBinds is fragile.
```

Note that an incremental build will not re-emit that warning for an already-compiled module.
Build into a fresh `--builddir` when checking whether the warning is really gone.

## Key rules

- **One effect per concern.** Don't pile everything into one `App` monad. `UserStore`, `EmailSender`, `PaymentGateway` are separate effects.
- **`IO` only at the edges.** The `IOE` constraint stays in interpreters and `main`, not in business logic. If a function in your domain layer needs `IOE`, that's a smell — model the side-effecting capability as its own effect.
- **Static effects when possible.** `Reader.Static` and `Error.Static` are faster than their dynamic counterparts. Use dynamic only when you genuinely need to swap implementations at runtime (rare).
- **No `MonadIO` / `liftIO` in app code.** That's mtl-land. With `effectful` you use `liftIO` from `Effectful` only at the interpreter boundary, almost never in business logic.
- **Smart constructors per operation.** Don't expose `send (GetUser uid)` to callers — wrap it as `getUser uid`. Keeps call sites clean.

## Composition in `main`

The composition root assembles the interpreter stack — this is where the abstract operations get bound to concrete implementations. Order matters: interpreters peel effects off the stack one at a time.

```haskell
main :: IO ()
main = do
  pool <- createPool dbConfig
  result <- runEff
          . runReader pool
          . runErrorNoCallStack @AppError
          . runUserStorePostgres
          $ registerUser someEmail
  case result of
    Left err -> ...
    Right user -> ...
```

Read top to bottom: `runEff` discharges `IOE`, `runReader` provides the pool, `runError` peels off the error effect (turning it into `Either`), and `runUserStorePostgres` implements `UserStore`. Each `run*` removes one effect from the stack.

## When to use Dynamic vs Static

- **`Dynamic`** (`type instance DispatchOf E = Dynamic`): the interpreter is chosen at runtime. Needed when production and tests use different implementations of the same effect — the typical case for application-level effects like `UserStore`, `EmailSender`.
- **`Static`**: the implementation is fixed at compile time. Faster, less flexible. Use for foundational concerns where the implementation is genuinely universal: `Reader env`, `State s`, `Error e`.

Default new application-domain effects to `Dynamic`. Default infrastructure-shaped effects to `Static`.

## Related

- Domain errors flowing through the `Error` effect: see `haskell-domain-errors` for the rules on error types (especially: don't derive `Exception` for errors that travel via `Error` effect).
- Logging as an effect (`Log :> es`): see `haskell-logging`.
- In-memory interpreters in tests: see `haskell-testing`.
- Language extensions needed (`GADTs`, `TypeFamilies`, `DataKinds`): see `haskell-project-setup`.

