{"article":{"slug":"you-dont-need-an-effect-system","title":"You don't need an effect system","subtitle":null,"summary":"After getting the chance to rewrite an old production Haskell codebase, the author asks what its effect system was actually buying and concludes that plain IO with explicitly passed handles does the job, showing the resulting Servant service structure and listing the trade-offs.","content_type":"blog_post","language":"en","canonical_url":"https://burningwitness.github.io/blog/posts/against-effect-systems/","author":{"name":null,"url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"burningwitness","url":"https://burningwitness.github.io/","listing_slug":null,"listing":null},"topics":[{"name":"Haskell","slug":"haskell","url":"https://listedarticles.com/topics/haskell"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Functional Programming","slug":"functional-programming","url":"https://listedarticles.com/topics/functional-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2350,"reading_minutes":10,"published_at":"2026-10-03T00:00:00.000Z","added_at":"2026-10-05T20:10:37.700Z","updated_at":"2026-10-05T20:10:37.700Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/you-dont-need-an-effect-system","markdown_url":"https://listedarticles.com/articles/you-dont-need-an-effect-system.md","example":false,"citation":"burningwitness. \"You don't need an effect system.\" 3 Oct 2026. https://burningwitness.github.io/blog/posts/against-effect-systems/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://burningwitness.github.io/blog/posts/against-effect-systems/"},"body_markdown":"# You don't need an effect system\n\n## How we got here\n\nA couple of months ago something rather unusual happened:\nI got permission to rewrite an old production codebase.\nNothing wild inside, just a few services bouncing messages\naround and calling other places. I wrote most of it years\nago, and most of it was god awful on account of the fact that\nI had no idea what I was doing at the time.\n\nLike any *real* codebase written in Haskell, it relied on\nan effect system to do… well, things.\n\nIndeed, like most of the community, I couldn't properly\nformulate what an effect system does. Yes, I know there are\nat least\n[ten of them](https://github.com/sayo-hs/heftia/blob/542963d4449d31a0c17a41a1acf56c74ed79ac0d/README.md#comparison),\nall at odds with one another, yet seemingly completely\ninterchangeable. Smarter people have narrowed the goals down to\n[tracking effects, mocking and internal consistency](https://discourse.haskell.org/t/why-use-an-effect-system/10841/115),\nwhich strongly implies that plain IO is incapable of\nthese things, and that's something I could neither confirm nor deny.\n\nSo now, being able to reassemble an entire system from\nthe ground up, the question I got to ask was…\n\n### Am I using the effect system for anything?\n\n* **Do I need it to pass arguments around?**\n\nNo, I can do that manually.\n* **Does it make for a safer codebase?**\n\nNo, tracking effects does not preclude anyone\nfrom adding bad IO to an effect\nimplementation, from importing\n[unsafePerformIO](https://hackage.haskell.org/package/base-4.22.0.0/docs/System-IO-Unsafe.html#v:unsafePerformIO),\nor from finding a sum of an infinite\nlist.[1](#footnote-1)\n* **Am I using it outside of IO?**\n\nNo, for pure functions that do mutation\nthe ST monad works just fine.\n* **Is there any benefit to tracking IO as a\nseparate effect?**\n\nNo, and in fact the opposite: there are small effects\nall around the codebase that I'd rather *not*\ntrack.\n\nFor example, random number generation is commonplace,\nbut the steps necessary to generate a random value\nare different every time. The only shared behavior is\nthe use of generator state, and even then it's unclear\nif passing it around has any benefits over using the\n[global](https://hackage.haskell.org/package/random-1.3.1/docs/System-Random-Stateful.html#v:globalStdGen) generator.\n* **Have I made use of higher-order effects?**\n\nNo, none of the effects I'm using need to overlap or\nnest. I'm not precisely doing rocket science over here;\nI'd prefer that whatever I'm using doesn't come with a\nwhole separate manual.\n\nAfter throwing out pretty much every single feature, I finally\nfound the one thing I was using the effect system for:\nerror handling. Which upon closer inspection turned out\nto be the execution of some set of other effects followed\nby…\n\n## Early return\n\nThis may well be the most embarrassing problem in all of\nHaskell.\n[Over](https://stackoverflow.com/questions/15441956/how-do-i-make-a-do-block-return-early)\nand\n[over](https://www.reddit.com/r/haskell/comments/1qlr4if/how_to_handle_early_returns_or_conditions_in/)\nagain people waltz in with the exact same basic question:\n\"How do I return from a function early?\".\n\nAnd time and time again they're hit with the same three\noptions:\n\n1. Make it so that the error is the last statement in\nthe function, using Either or continuations.\n\n*Code looks awful and constantly drifts to the right.*\n2. Throw an exception.\n\n*Roughly equivalent to burying a landmine\nin your backyard.*\n3. Use an effect system.\n[2](#footnote-2)\n\n*???*\n\nThe catch is that effect systems don't have some special\nthird way of handling errors, they merely wrap the other\ntwo approaches. Notably,\n[ExceptT](https://hackage.haskell.org/package/transformers-0.6.3.0/docs/Control-Monad-Trans-Except.html#t:ExceptT)\nis a faithful implementation of the first approach,\nthreading an Either through every action. That's\nobviously very inefficient and is known to not\ncompose well, so I'd prefer the second option.\n\n### Type-safe exceptions\n\nTo do this we'll need some way to carry the knowledge that\na specific resource (in our case an exception) may only be\nused (thrown) within a specific function. This,\nunsurprisingly, is a problem that has been solved decades ago\nfor file handles and raw pointers through the use of\n[bracket](https://wiki.haskell.org/Bracket_pattern).\nThough in our case there's nothing to allocate, the\nvalue is on the type level:\n\n```haskell\n{-# LANGUAGE RoleAnnotations #-}\n\nmodule Early\n  ( Early\n  , leave\n  , runEarly\n  ) where\n\nimport           Control.Exception\nimport           Data.Typeable\n\n\ntype role Early nominal\ndata Early a = Early\n\n\ndata ReturningEarly a = ReturningEarly a\n\ninstance Typeable e => Show (ReturningEarly e) where\n  show = displayException\n\ninstance Typeable e => Exception (ReturningEarly e) where\n  displayException (ReturningEarly _) = \"Early return exception\"\n\n\nleave :: Typeable e => Early e -> e -> IO a\nleave _Early e = throwIO $ ReturningEarly e\n\n\nrunEarly :: Typeable e => (Early e -> IO a) -> IO (Either e a)\nrunEarly f = catch (Right <$> f Early) (\\(ReturningEarly e) -> pure $ Left e)\n```\n\nAnd the module can then be used like this:\n\n```haskell\nimport           Control.Monad\nimport           Early\n\n\nexample :: Early () -> IO Bool\nexample early = do\n  putStrLn \"Ran this action\"\n  when True $ do\n    leave early ()\n\n  putStrLn \"Didn't run this action\"\n  pure True\n```\n```haskell\nghci> runEarly example\nRan this action\nLeft ()\n```\n\nThere are no caveats to this code beyond those that come\nwith using bracket.\n\n## Intermission\n\nAnd just like that I ran out of reasons to use an\neffect system.\n\nThere's not much of a story to tell from this point on:\nI shaped every other effect much like I had shaped\nEarly and everything fell into place nicely.\nThe rest of the post are my findings, structured to the\nbest of my ability.\n\n## Effects in plain IO\n\nFinding a definition for the word \"effect\" is unfortunately\nas tedious as finding one for \"effect system\".\nIf I am to trust\n[some people on Reddit](https://www.reddit.com/r/haskell/comments/1c9czmn/what_are_effects/),\nan \"effect\" is a convention to only access specific\nside effects through a common interface tracked on\nthe type level.\n\nBoth Early and \"handles\" from the\n[\"handle pattern\"](https://jaspervdj.be/posts/2018-03-08-handle-pattern.html#a-database-handle)\nfit this definition:\n\n1. They have interfaces (leave,\ncreateUser) and implementations\n(runEarly, withHandle). Compare to\nthe similar\neffect/handler\nseparation in\n[eff](https://github.com/hasura/eff/blob/7d7f9ab77f3c7f473d52457e41e9b6e1870d72f6/README.md#eff-in-action).\n2. They're tracked on the type level, contrast\n```haskell\ncreateUser :: Handle -> Text -> IO User\n\nlistDirectory :: FileSystem :> es => FilePath -> Eff es [FilePath]\n\nleave :: Early e -> e -> IO a\n\nthrowError :: Error e :> es => e -> Eff es a\n```\n\nMore generally, an effect in plain IO is a\ndata type that serves as a bridge between interface\nand implementation functions. The data type's\nconstructor is as such an implementation detail and\nshould not be exported.\n\nEffects are tracked as function arguments; this is\nquite different from effect systems, which prefer\nconstraints. One benefit of this is that we don't have\nto deal with the complexities of\n[disambiguating](https://github.com/polysemy-research/polysemy/tree/cf2efec15ae57a46a81694d361da55da8d7b1d73/polysemy-plugin),\n[reordering](https://hackage.haskell.org/package/polysemy-1.9.2.0/docs/Polysemy.html#g:7)\nor\n[reinterpreting](https://hackage.haskell.org/package/polysemy-1.9.2.0/docs/Polysemy.html#g:11)\neffects.\n\n### Module structure\n\nEffect systems tend to put all of their definitions\ninto a single module, but there is no hard requirement\nfor this. For example, Early can be broken into\n\n```haskell\nmodule Effect.Early.Leave (Early, leave) where\n```\n```haskell\nmodule Effect.Early.Runner (Early, runEarly) where\n```\n```haskell\nmodule Effect.Early.Internal (Early, leave, runEarly) where\n```\n\nThis can be used to track function access at module\nlevel; particularly useful if an effect has multiple\ninterfaces (say, multiple programs rely on the\nsame effect data type) and/or multiple implementations\n(say, mocking).\n\n### Error handling\n\nEffects may throw exceptions, but if they do they're also\nresponsible for catching them. Much like the data\ntype constructors, exceptions are an implementation detail.\n\nEffect implementations are generally not invoked alone\nhowever, they're stacked into a\n[terrifyingly large pile](https://discourse.haskell.org/t/effectful-how-to-prevent-big-effect-runner-functions/7173)\nat the very edge of the program. In our case stacking\ndoes work out of the box, but each layer pushes the\nsuccessful case deeper into the chain:\n```haskell\nrunFoo :: (Foo -> IO a) -> IO (Either FooError a)\n\nstack\n  :: (Foo -> Bar -> Qux -> IO a)\n  -> IO (Either FooError (Either BarError (Either QuxError a)))\nstack f =\n  runFoo $ \\foo ->\n    runBar $ \\bar ->\n      runQux $ \\qux ->\n        f foo bar qux\n```\n\nThis can be solved rather nicely by using\nslightly more complicated\ntypes:[3](#footnote-3)\n```haskell\nrunFoo :: (Foo -> IO (Either e a)) -> IO (Either (Either e FooError) a)\n\nlift :: IO a -> IO (Either Void a)\nlift = fmap Right\n\nstack\n  :: (Foo -> Bar -> Qux -> IO a)\n  -> IO (Either (Either (Either (Either Void QuxError) BarError) FooError) a)\nstack f =\n  runFoo $ \\foo ->\n    runBar $ \\bar ->\n      runQux $ \\qux ->\n        lift $\n          f foo bar qux\n```\n\n### Duplicate effects\n\nPassing around two effects with the same name would\nbe wildly confusing; passing something like a\nTagged \"helper\" Database would be a massive\nnuisance. If a user needs two of the same effect,\nthey should wrap the functions on their side into nice\nnewtyped names (e.g. HelperDatabase).\n\nDuplicate exception handling in implementation functions\ncan be addressed in a similar way by providing an extra\nargument and matching on that, essentially letting the\nuser call one effect \"database 1\" and the other \"database 2\".\n\n## Real-world effects\n\nLet's look at all of the categories of effects\nI ended up with (or without) in my codebase.\n\n### Argument passing, fancier\n\nConfiguration data is generally read at application start\nbefore invoking the implementation stack, and is passed\ninto it as either function arguments or \"reader\" effects.\n\nTrying to implement a \"reader\" outside of an effect\nsystem results in a bunch of redundant wrapping:\n```haskell\ndata Conf =\n       Conf\n         { bar :: Int\n         , baz :: Bool\n         , qux :: String\n         }\n\ngetBar :: Conf -> Int\ngetBar = bar\n\nrunConf :: Conf -> Conf\nrunConf = id\n```\n\nI thus prefer to keep configuration data types in\nseparate modules and to pass them around as arguments.\n\n### Mocking\n\nAn effect that can be mocked is simply a product\nof functions:\n```haskell\ndata Database =\n       Database\n         { createUser  :: Text -> IO (Maybe User)\n         , getUserMail :: User -> IO [Mail]\n         }\n\nrunRealDatabase :: RealDatabase -> Database\nrunRealDatabase db =\n  Database\n    { createUser  = Database.createUser db\n    , getUserMail = Database.getUserMail db\n    }\n```\n\nEffects that never fail—like logging and metrics\ncollection—are special cases of mocking:\n```haskell\nimport           Data.ByteString.Builder\nimport           Data.Text (Text)\nimport           Data.Text.Encoding\nimport           System.IO\n\n\nnewtype Logger = Logger (Builder -> IO ())\n\nnote :: Logger -> Text -> IO ()\nnote (Logger f) = f . encodeUtf8Builder\n\nrunLogger :: Handle -> Logger\nrunLogger handle =\n  Logger $ \\msg ->\n    hPutBuilder handle $ msg <> \"\\n\"\n```\n\n### Tracking resources\n\nI'll use the database effect as an example.\nHere's a rough outline of the implementation function:\n```haskell\nimport           Control.Exception\nimport           Database.PostgreSQL.Simple\nimport           GHC.Stack\nimport           System.IO\n\n\nnewtype Database = Database Connection\n\nrunDatabase\n  :: ConnectInfo\n  -> (Database -> IO (Either e a))\n  -> IO (Either (Either e DatabaseError) a)\nrunDatabase connInfo f =\n  mask $ \\unmask -> do\n    conn <- connect connInfo\n\n    let cleanup = close conn\n\n    ei <- unmask (Right <$> f (Database conn))\n                   `catch` \\ex ->\n                     case fromException ex of\n                       Just dbEx -> pure $ Left (dbEx :: DatabaseException)\n                       Nothing   -> _ -- (4) Failure above.\n    case ei of\n      Right (Right a) -> _ -- (1) Success.\n      Right (Left e)  -> _ -- (2) Failure below.\n      Left ex         -> _ -- (3) Failure at this level.\n```\n\nFor the effect to behave the same regardless of its\nposition within the implementation stack cases *(1)*,\n*(2)* and *(4)* should all use the same cleanup\nfunction.\n\nInterface functions may use the resource directly,\nalthough in libraries that use exceptions liberally it's\nmore convenient to funnel all uses through a helper function:\n```haskell\ndata DatabaseException = DatabaseException CallStack DatabaseExceptionKind\n\ndata DatabaseExceptionKind = DatabaseSqlException SqlError\n                           | _\n\nhandlesPostgres :: HasCallStack => Database -> (Connection -> IO a) -> IO a\nhandlesPostgres (Database conn) f =\n  let rethrow :: (e -> DatabaseExceptionKind) -> e -> IO a\n      rethrow kind = throwIO . DatabaseException callStack . kind\n\n  in f conn\n       `catches`\n         [ Handler $ rethrow DatabaseSqlException\n--       , -- however many more of these are necessary --\n         ]\n\n\ncreateUser :: Database -> Text -> IO (Maybe User)\ncreateUser db =\n  handlesPostgres db $ \\conn ->\n    _ conn\n```\n\n## Tying everything together\n\n### Interfaces\n\nTaking argument passing to the extreme, one will\ninevitably end up with a function that looks like\n```haskell\nendpoint\n  :: AuthConf -> ServiceConf\n  -> Logger -> Metrics -> Early ServerError -> Database -> Cache -> Messaging\n  -> AuthHeader -> EndpointRequest -> IO EndpointResponse\nendpoint authConf serviceConf logger metrics early database cache messaging\n                                authHeader EndpointRequest {..} = do\n```\n\nTo combat this the \"handle pattern\" post proposes\nnesting effects; the\n[\"ReaderT design pattern\"](https://academy.fpblock.com/blog/2017/06/readert-design-pattern/)\npost instead suggests carrying all the effects in a\nrecord. Both of these solutions are wrong.\n\nGHC has warnings for\n[unused arguments](https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag-Wunused-matches), and since we\ntreat effects as arguments, we can use it to point out\nunused effects. To leverage this, the code must be\nstructured in such a way that each statement uses at\nmost one effect. A bunch of statements then bundle into\na function, functions bundle into larger functions,\nuntil we reach the aforementioned endpoint.\n\nAs an example, creating a user actually requires at least\nthree effects:\n```haskell\nnewUser :: Database -> Text -> IO (Maybe User)\n\ncreateUser :: Logger -> Early ServerError -> Database -> Text -> IO User\ncreateUser logger early database qux = do\n  mayUser <- newUser database qux\n  case mayUser of\n    Just user -> pure user\n    Nothing   -> do\n      note logger \"Could not create a user entry\"\n      leave early err500\n```\n\nThis approach works in the opposite direction too:\nif we need to find out what caused a\nDatabaseError, we only need to track which\nfunctions are passed the Database argument.\n\n### Implementations\n\nRunning the implementation stack is as straightforward\nas in any effect system:\n```haskell\nmain :: IO ()\nmain = do\n  conf <- readConfiguration\n\n  let logger = runLogger stderr\n\n  metrics <- runMetrics (getMetricsConf conf)\n\n  withDatabasePool (getDatabaseConf conf) $ \\dbPool -> do\n\n    withMessagingEnv (getMessagingConf conf) $ \\msgEnv -> do\n\n      note logger \"Initialization complete\"\n\n      consumeMessagesForever msgEnv $ \\newMsg ->\n        runStack logger metrics dbPool msgEnv $ \\early database messaging ->\n          process logger metrics early database messaging newMsg\n\n\nrunStack\n  :: Logger -> Metrics -> DatabasePool -> MessagingEnv\n  -> (Early () -> Database -> Messaging -> IO ())\n  -> IO ()\nrunStack logger metrics dbPool msgEnv f = do\n  ei <- runEarly $ \\early ->\n          runDatabase logger metrics dbPool $ \\database ->\n            runMessaging logger metrics msgEnv $ \\msg ->\n              lift $\n                f early database msg\n\n  case ei of\n    Right () -> _ -- Success\n    Left err ->\n      case err of\n        Left (Left (Right msgError)) -> _ -- Messaging error \n        Left (Right dbError)         -> _ -- Database error \n        Right ()                     -> _ -- Early return\n\n\nprocess\n  :: Logger -> Metrics -> Early () -> Database -> Messaging\n  -> NewMessage -> IO ()\n```\n\nThe shape remains almost the same when using\n[servant](https://hackage.haskell.org/package/servant).\nThe only quirk is that argument passing renders all\nthe fancy\n[hoisting](https://hackage.haskell.org/package/servant-server-0.20.3.0/docs/Servant-Server.html#g:5) functionality completely\nunusable, so the stack has to be invoked inside each\nendpoint manually.\n```haskell\nrunStack\n  :: Logger -> Metrics -> DatabasePool -> MessagingEnv\n  -> (Early ServerError -> Database -> Messaging -> IO a)\n  -> Handler a\nrunStack logger metrics dbPool msgEnv f =\n  MkHandler $ do\n    ei <- runEarly $ \\early ->\n            runDatabase logger metrics dbPool $ \\database ->\n              runMessaging logger metrics msgEnv $ \\msg ->\n                lift $\n                  f early database msg\n\n    case ei of\n      Right a  -> pure $ Right a\n      Left err ->\n        case err of\n          Left (Left (Right msgError)) -> _ -- Messaging error \n          Left (Right dbError)         -> _ -- Database error \n          Right srvError               -> pure $ Left srvError\n\n\ntype API = CountAPI :<|> _\n\nserver\n  :: Logger -> Metrics -> DatabasePool -> MessagingEnv\n  -> Server API\nserver logger metrics dbPool msgEnv =\n       countServer logger metrics dbPool msgEnv\n  :<|> _\n\n\ntype CountAPI = \"count\"\n             :> ReqBody '[JSON] CountRequest\n             :> Get '[JSON] CountResponse\n\ncountServer\n  :: Logger -> Metrics -> Early ServerError -> DatabasePool -> MessagingEnv\n  -> Server CountAPI\ncountServer logger metrics early dbPool msgEnv request =\n  runStack logger metrics dbPool msgEnv $ \\early database messaging ->\n    countEndpoint logger metrics early database messaging request\n\ncountEndpoint\n  :: Logger -> Metrics -> Early ServerError -> Database -> Messaging\n  -> CountRequest -> IO CountResponse\n```\n\n## Conclusion\n\nThere is only one unique feature effect\nsystems—or, more generally, custom monads designed\nto supercede IO—provide: they can do\nanything in between the lines [of code]. And I don't\nthink that's a good thing.\n\nHere are the advantages of running effects\nin plain IO:\n\n* Works with any Haskell 98 (or later) compiler;\n* Straightforward implementation;\n* No extra dependencies;\n* No weird type errors;\n* Compiles fast;\n* Runs fast.\n\nHere are the disadvantages:\n\n* Some functions will be verbose.\n\nI hope this knowledge is used for things.\n","body_html":"<h1 id=\"you-don-t-need-an-effect-system\">You don&#39;t need an effect system</h1>\n<h2 id=\"how-we-got-here\">How we got here</h2>\n<p>A couple of months ago something rather unusual happened:\nI got permission to rewrite an old production codebase.\nNothing wild inside, just a few services bouncing messages\naround and calling other places. I wrote most of it years\nago, and most of it was god awful on account of the fact that\nI had no idea what I was doing at the time.</p>\n<p>Like any <em>real</em> codebase written in Haskell, it relied on\nan effect system to do… well, things.</p>\n<p>Indeed, like most of the community, I couldn&#39;t properly\nformulate what an effect system does. Yes, I know there are\nat least\n<a href=\"https://github.com/sayo-hs/heftia/blob/542963d4449d31a0c17a41a1acf56c74ed79ac0d/README.md#comparison\" rel=\"nofollow ugc noopener\">ten of them</a>,\nall at odds with one another, yet seemingly completely\ninterchangeable. Smarter people have narrowed the goals down to\n<a href=\"https://discourse.haskell.org/t/why-use-an-effect-system/10841/115\" rel=\"nofollow ugc noopener\">tracking effects, mocking and internal consistency</a>,\nwhich strongly implies that plain IO is incapable of\nthese things, and that&#39;s something I could neither confirm nor deny.</p>\n<p>So now, being able to reassemble an entire system from\nthe ground up, the question I got to ask was…</p>\n<h3 id=\"am-i-using-the-effect-system-for-anything\">Am I using the effect system for anything?</h3>\n<ul><li><strong>Do I need it to pass arguments around?</strong></li></ul>\n<p>No, I can do that manually.</p>\n<ul><li><strong>Does it make for a safer codebase?</strong></li></ul>\n<p>No, tracking effects does not preclude anyone\nfrom adding bad IO to an effect\nimplementation, from importing\n<a href=\"https://hackage.haskell.org/package/base-4.22.0.0/docs/System-IO-Unsafe.html#v:unsafePerformIO\" rel=\"nofollow ugc noopener\">unsafePerformIO</a>,\nor from finding a sum of an infinite\nlist.<a href=\"#footnote-1\">1</a></p>\n<ul><li><strong>Am I using it outside of IO?</strong></li></ul>\n<p>No, for pure functions that do mutation\nthe ST monad works just fine.</p>\n<ul><li><p>**Is there any benefit to tracking IO as a</p><p>separate effect?**</p></li></ul>\n<p>No, and in fact the opposite: there are small effects\nall around the codebase that I&#39;d rather <em>not</em>\ntrack.</p>\n<p>For example, random number generation is commonplace,\nbut the steps necessary to generate a random value\nare different every time. The only shared behavior is\nthe use of generator state, and even then it&#39;s unclear\nif passing it around has any benefits over using the\n<a href=\"https://hackage.haskell.org/package/random-1.3.1/docs/System-Random-Stateful.html#v:globalStdGen\" rel=\"nofollow ugc noopener\">global</a> generator.</p>\n<ul><li><strong>Have I made use of higher-order effects?</strong></li></ul>\n<p>No, none of the effects I&#39;m using need to overlap or\nnest. I&#39;m not precisely doing rocket science over here;\nI&#39;d prefer that whatever I&#39;m using doesn&#39;t come with a\nwhole separate manual.</p>\n<p>After throwing out pretty much every single feature, I finally\nfound the one thing I was using the effect system for:\nerror handling. Which upon closer inspection turned out\nto be the execution of some set of other effects followed\nby…</p>\n<h2 id=\"early-return\">Early return</h2>\n<p>This may well be the most embarrassing problem in all of\nHaskell.\n<a href=\"https://stackoverflow.com/questions/15441956/how-do-i-make-a-do-block-return-early\" rel=\"nofollow ugc noopener\">Over</a>\nand\n<a href=\"https://www.reddit.com/r/haskell/comments/1qlr4if/how_to_handle_early_returns_or_conditions_in/\" rel=\"nofollow ugc noopener\">over</a>\nagain people waltz in with the exact same basic question:\n&quot;How do I return from a function early?&quot;.</p>\n<p>And time and time again they&#39;re hit with the same three\noptions:</p>\n<ol><li><p>Make it so that the error is the last statement in</p><p>the function, using Either or continuations.</p></li></ol>\n<p><em>Code looks awful and constantly drifts to the right.</em></p>\n<ol start=\"2\"><li>Throw an exception.</li></ol>\n<p>*Roughly equivalent to burying a landmine\nin your backyard.*</p>\n<ol start=\"3\"><li><p>Use an effect system.</p><p><a href=\"#footnote-2\">2</a></p></li></ol>\n<p><em>???</em></p>\n<p>The catch is that effect systems don&#39;t have some special\nthird way of handling errors, they merely wrap the other\ntwo approaches. Notably,\n<a href=\"https://hackage.haskell.org/package/transformers-0.6.3.0/docs/Control-Monad-Trans-Except.html#t:ExceptT\" rel=\"nofollow ugc noopener\">ExceptT</a>\nis a faithful implementation of the first approach,\nthreading an Either through every action. That&#39;s\nobviously very inefficient and is known to not\ncompose well, so I&#39;d prefer the second option.</p>\n<h3 id=\"type-safe-exceptions\">Type-safe exceptions</h3>\n<p>To do this we&#39;ll need some way to carry the knowledge that\na specific resource (in our case an exception) may only be\nused (thrown) within a specific function. This,\nunsurprisingly, is a problem that has been solved decades ago\nfor file handles and raw pointers through the use of\n<a href=\"https://wiki.haskell.org/Bracket_pattern\" rel=\"nofollow ugc noopener\">bracket</a>.\nThough in our case there&#39;s nothing to allocate, the\nvalue is on the type level:</p>\n<pre><code class=\"language-haskell\">{-# LANGUAGE RoleAnnotations #-}\n\nmodule Early\n  ( Early\n  , leave\n  , runEarly\n  ) where\n\nimport           Control.Exception\nimport           Data.Typeable\n\n\ntype role Early nominal\ndata Early a = Early\n\n\ndata ReturningEarly a = ReturningEarly a\n\ninstance Typeable e =&gt; Show (ReturningEarly e) where\n  show = displayException\n\ninstance Typeable e =&gt; Exception (ReturningEarly e) where\n  displayException (ReturningEarly _) = &quot;Early return exception&quot;\n\n\nleave :: Typeable e =&gt; Early e -&gt; e -&gt; IO a\nleave _Early e = throwIO $ ReturningEarly e\n\n\nrunEarly :: Typeable e =&gt; (Early e -&gt; IO a) -&gt; IO (Either e a)\nrunEarly f = catch (Right &lt;$&gt; f Early) (\\(ReturningEarly e) -&gt; pure $ Left e)</code></pre>\n<p>And the module can then be used like this:</p>\n<pre><code class=\"language-haskell\">import           Control.Monad\nimport           Early\n\n\nexample :: Early () -&gt; IO Bool\nexample early = do\n  putStrLn &quot;Ran this action&quot;\n  when True $ do\n    leave early ()\n\n  putStrLn &quot;Didn&#39;t run this action&quot;\n  pure True</code></pre>\n<pre><code class=\"language-haskell\">ghci&gt; runEarly example\nRan this action\nLeft ()</code></pre>\n<p>There are no caveats to this code beyond those that come\nwith using bracket.</p>\n<h2 id=\"intermission\">Intermission</h2>\n<p>And just like that I ran out of reasons to use an\neffect system.</p>\n<p>There&#39;s not much of a story to tell from this point on:\nI shaped every other effect much like I had shaped\nEarly and everything fell into place nicely.\nThe rest of the post are my findings, structured to the\nbest of my ability.</p>\n<h2 id=\"effects-in-plain-io\">Effects in plain IO</h2>\n<p>Finding a definition for the word &quot;effect&quot; is unfortunately\nas tedious as finding one for &quot;effect system&quot;.\nIf I am to trust\n<a href=\"https://www.reddit.com/r/haskell/comments/1c9czmn/what_are_effects/\" rel=\"nofollow ugc noopener\">some people on Reddit</a>,\nan &quot;effect&quot; is a convention to only access specific\nside effects through a common interface tracked on\nthe type level.</p>\n<p>Both Early and &quot;handles&quot; from the\n<a href=\"https://jaspervdj.be/posts/2018-03-08-handle-pattern.html#a-database-handle\" rel=\"nofollow ugc noopener\">&quot;handle pattern&quot;</a>\nfit this definition:</p>\n<ol><li><p>They have interfaces (leave,</p><p>createUser) and implementations\n(runEarly, withHandle). Compare to\nthe similar\neffect/handler\nseparation in\n<a href=\"https://github.com/hasura/eff/blob/7d7f9ab77f3c7f473d52457e41e9b6e1870d72f6/README.md#eff-in-action\" rel=\"nofollow ugc noopener\">eff</a>.</p></li><li>They&#39;re tracked on the type level, contrast</li></ol>\n<pre><code class=\"language-haskell\">createUser :: Handle -&gt; Text -&gt; IO User\n\nlistDirectory :: FileSystem :&gt; es =&gt; FilePath -&gt; Eff es [FilePath]\n\nleave :: Early e -&gt; e -&gt; IO a\n\nthrowError :: Error e :&gt; es =&gt; e -&gt; Eff es a</code></pre>\n<p>More generally, an effect in plain IO is a\ndata type that serves as a bridge between interface\nand implementation functions. The data type&#39;s\nconstructor is as such an implementation detail and\nshould not be exported.</p>\n<p>Effects are tracked as function arguments; this is\nquite different from effect systems, which prefer\nconstraints. One benefit of this is that we don&#39;t have\nto deal with the complexities of\n<a href=\"https://github.com/polysemy-research/polysemy/tree/cf2efec15ae57a46a81694d361da55da8d7b1d73/polysemy-plugin\" rel=\"nofollow ugc noopener\">disambiguating</a>,\n<a href=\"https://hackage.haskell.org/package/polysemy-1.9.2.0/docs/Polysemy.html#g:7\" rel=\"nofollow ugc noopener\">reordering</a>\nor\n<a href=\"https://hackage.haskell.org/package/polysemy-1.9.2.0/docs/Polysemy.html#g:11\" rel=\"nofollow ugc noopener\">reinterpreting</a>\neffects.</p>\n<h3 id=\"module-structure\">Module structure</h3>\n<p>Effect systems tend to put all of their definitions\ninto a single module, but there is no hard requirement\nfor this. For example, Early can be broken into</p>\n<pre><code class=\"language-haskell\">module Effect.Early.Leave (Early, leave) where</code></pre>\n<pre><code class=\"language-haskell\">module Effect.Early.Runner (Early, runEarly) where</code></pre>\n<pre><code class=\"language-haskell\">module Effect.Early.Internal (Early, leave, runEarly) where</code></pre>\n<p>This can be used to track function access at module\nlevel; particularly useful if an effect has multiple\ninterfaces (say, multiple programs rely on the\nsame effect data type) and/or multiple implementations\n(say, mocking).</p>\n<h3 id=\"error-handling\">Error handling</h3>\n<p>Effects may throw exceptions, but if they do they&#39;re also\nresponsible for catching them. Much like the data\ntype constructors, exceptions are an implementation detail.</p>\n<p>Effect implementations are generally not invoked alone\nhowever, they&#39;re stacked into a\n<a href=\"https://discourse.haskell.org/t/effectful-how-to-prevent-big-effect-runner-functions/7173\" rel=\"nofollow ugc noopener\">terrifyingly large pile</a>\nat the very edge of the program. In our case stacking\ndoes work out of the box, but each layer pushes the\nsuccessful case deeper into the chain:</p>\n<pre><code class=\"language-haskell\">runFoo :: (Foo -&gt; IO a) -&gt; IO (Either FooError a)\n\nstack\n  :: (Foo -&gt; Bar -&gt; Qux -&gt; IO a)\n  -&gt; IO (Either FooError (Either BarError (Either QuxError a)))\nstack f =\n  runFoo $ \\foo -&gt;\n    runBar $ \\bar -&gt;\n      runQux $ \\qux -&gt;\n        f foo bar qux</code></pre>\n<p>This can be solved rather nicely by using\nslightly more complicated\ntypes:<a href=\"#footnote-3\">3</a></p>\n<pre><code class=\"language-haskell\">runFoo :: (Foo -&gt; IO (Either e a)) -&gt; IO (Either (Either e FooError) a)\n\nlift :: IO a -&gt; IO (Either Void a)\nlift = fmap Right\n\nstack\n  :: (Foo -&gt; Bar -&gt; Qux -&gt; IO a)\n  -&gt; IO (Either (Either (Either (Either Void QuxError) BarError) FooError) a)\nstack f =\n  runFoo $ \\foo -&gt;\n    runBar $ \\bar -&gt;\n      runQux $ \\qux -&gt;\n        lift $\n          f foo bar qux</code></pre>\n<h3 id=\"duplicate-effects\">Duplicate effects</h3>\n<p>Passing around two effects with the same name would\nbe wildly confusing; passing something like a\nTagged &quot;helper&quot; Database would be a massive\nnuisance. If a user needs two of the same effect,\nthey should wrap the functions on their side into nice\nnewtyped names (e.g. HelperDatabase).</p>\n<p>Duplicate exception handling in implementation functions\ncan be addressed in a similar way by providing an extra\nargument and matching on that, essentially letting the\nuser call one effect &quot;database 1&quot; and the other &quot;database 2&quot;.</p>\n<h2 id=\"real-world-effects\">Real-world effects</h2>\n<p>Let&#39;s look at all of the categories of effects\nI ended up with (or without) in my codebase.</p>\n<h3 id=\"argument-passing-fancier\">Argument passing, fancier</h3>\n<p>Configuration data is generally read at application start\nbefore invoking the implementation stack, and is passed\ninto it as either function arguments or &quot;reader&quot; effects.</p>\n<p>Trying to implement a &quot;reader&quot; outside of an effect\nsystem results in a bunch of redundant wrapping:</p>\n<pre><code class=\"language-haskell\">data Conf =\n       Conf\n         { bar :: Int\n         , baz :: Bool\n         , qux :: String\n         }\n\ngetBar :: Conf -&gt; Int\ngetBar = bar\n\nrunConf :: Conf -&gt; Conf\nrunConf = id</code></pre>\n<p>I thus prefer to keep configuration data types in\nseparate modules and to pass them around as arguments.</p>\n<h3 id=\"mocking\">Mocking</h3>\n<p>An effect that can be mocked is simply a product\nof functions:</p>\n<pre><code class=\"language-haskell\">data Database =\n       Database\n         { createUser  :: Text -&gt; IO (Maybe User)\n         , getUserMail :: User -&gt; IO [Mail]\n         }\n\nrunRealDatabase :: RealDatabase -&gt; Database\nrunRealDatabase db =\n  Database\n    { createUser  = Database.createUser db\n    , getUserMail = Database.getUserMail db\n    }</code></pre>\n<p>Effects that never fail—like logging and metrics\ncollection—are special cases of mocking:</p>\n<pre><code class=\"language-haskell\">import           Data.ByteString.Builder\nimport           Data.Text (Text)\nimport           Data.Text.Encoding\nimport           System.IO\n\n\nnewtype Logger = Logger (Builder -&gt; IO ())\n\nnote :: Logger -&gt; Text -&gt; IO ()\nnote (Logger f) = f . encodeUtf8Builder\n\nrunLogger :: Handle -&gt; Logger\nrunLogger handle =\n  Logger $ \\msg -&gt;\n    hPutBuilder handle $ msg &lt;&gt; &quot;\\n&quot;</code></pre>\n<h3 id=\"tracking-resources\">Tracking resources</h3>\n<p>I&#39;ll use the database effect as an example.\nHere&#39;s a rough outline of the implementation function:</p>\n<pre><code class=\"language-haskell\">import           Control.Exception\nimport           Database.PostgreSQL.Simple\nimport           GHC.Stack\nimport           System.IO\n\n\nnewtype Database = Database Connection\n\nrunDatabase\n  :: ConnectInfo\n  -&gt; (Database -&gt; IO (Either e a))\n  -&gt; IO (Either (Either e DatabaseError) a)\nrunDatabase connInfo f =\n  mask $ \\unmask -&gt; do\n    conn &lt;- connect connInfo\n\n    let cleanup = close conn\n\n    ei &lt;- unmask (Right &lt;$&gt; f (Database conn))\n                   `catch` \\ex -&gt;\n                     case fromException ex of\n                       Just dbEx -&gt; pure $ Left (dbEx :: DatabaseException)\n                       Nothing   -&gt; _ -- (4) Failure above.\n    case ei of\n      Right (Right a) -&gt; _ -- (1) Success.\n      Right (Left e)  -&gt; _ -- (2) Failure below.\n      Left ex         -&gt; _ -- (3) Failure at this level.</code></pre>\n<p>For the effect to behave the same regardless of its\nposition within the implementation stack cases <em>(1)</em>,\n<em>(2)</em> and <em>(4)</em> should all use the same cleanup\nfunction.</p>\n<p>Interface functions may use the resource directly,\nalthough in libraries that use exceptions liberally it&#39;s\nmore convenient to funnel all uses through a helper function:</p>\n<pre><code class=\"language-haskell\">data DatabaseException = DatabaseException CallStack DatabaseExceptionKind\n\ndata DatabaseExceptionKind = DatabaseSqlException SqlError\n                           | _\n\nhandlesPostgres :: HasCallStack =&gt; Database -&gt; (Connection -&gt; IO a) -&gt; IO a\nhandlesPostgres (Database conn) f =\n  let rethrow :: (e -&gt; DatabaseExceptionKind) -&gt; e -&gt; IO a\n      rethrow kind = throwIO . DatabaseException callStack . kind\n\n  in f conn\n       `catches`\n         [ Handler $ rethrow DatabaseSqlException\n--       , -- however many more of these are necessary --\n         ]\n\n\ncreateUser :: Database -&gt; Text -&gt; IO (Maybe User)\ncreateUser db =\n  handlesPostgres db $ \\conn -&gt;\n    _ conn</code></pre>\n<h2 id=\"tying-everything-together\">Tying everything together</h2>\n<h3 id=\"interfaces\">Interfaces</h3>\n<p>Taking argument passing to the extreme, one will\ninevitably end up with a function that looks like</p>\n<pre><code class=\"language-haskell\">endpoint\n  :: AuthConf -&gt; ServiceConf\n  -&gt; Logger -&gt; Metrics -&gt; Early ServerError -&gt; Database -&gt; Cache -&gt; Messaging\n  -&gt; AuthHeader -&gt; EndpointRequest -&gt; IO EndpointResponse\nendpoint authConf serviceConf logger metrics early database cache messaging\n                                authHeader EndpointRequest {..} = do</code></pre>\n<p>To combat this the &quot;handle pattern&quot; post proposes\nnesting effects; the\n<a href=\"https://academy.fpblock.com/blog/2017/06/readert-design-pattern/\" rel=\"nofollow ugc noopener\">&quot;ReaderT design pattern&quot;</a>\npost instead suggests carrying all the effects in a\nrecord. Both of these solutions are wrong.</p>\n<p>GHC has warnings for\n<a href=\"https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag-Wunused-matches\" rel=\"nofollow ugc noopener\">unused arguments</a>, and since we\ntreat effects as arguments, we can use it to point out\nunused effects. To leverage this, the code must be\nstructured in such a way that each statement uses at\nmost one effect. A bunch of statements then bundle into\na function, functions bundle into larger functions,\nuntil we reach the aforementioned endpoint.</p>\n<p>As an example, creating a user actually requires at least\nthree effects:</p>\n<pre><code class=\"language-haskell\">newUser :: Database -&gt; Text -&gt; IO (Maybe User)\n\ncreateUser :: Logger -&gt; Early ServerError -&gt; Database -&gt; Text -&gt; IO User\ncreateUser logger early database qux = do\n  mayUser &lt;- newUser database qux\n  case mayUser of\n    Just user -&gt; pure user\n    Nothing   -&gt; do\n      note logger &quot;Could not create a user entry&quot;\n      leave early err500</code></pre>\n<p>This approach works in the opposite direction too:\nif we need to find out what caused a\nDatabaseError, we only need to track which\nfunctions are passed the Database argument.</p>\n<h3 id=\"implementations\">Implementations</h3>\n<p>Running the implementation stack is as straightforward\nas in any effect system:</p>\n<pre><code class=\"language-haskell\">main :: IO ()\nmain = do\n  conf &lt;- readConfiguration\n\n  let logger = runLogger stderr\n\n  metrics &lt;- runMetrics (getMetricsConf conf)\n\n  withDatabasePool (getDatabaseConf conf) $ \\dbPool -&gt; do\n\n    withMessagingEnv (getMessagingConf conf) $ \\msgEnv -&gt; do\n\n      note logger &quot;Initialization complete&quot;\n\n      consumeMessagesForever msgEnv $ \\newMsg -&gt;\n        runStack logger metrics dbPool msgEnv $ \\early database messaging -&gt;\n          process logger metrics early database messaging newMsg\n\n\nrunStack\n  :: Logger -&gt; Metrics -&gt; DatabasePool -&gt; MessagingEnv\n  -&gt; (Early () -&gt; Database -&gt; Messaging -&gt; IO ())\n  -&gt; IO ()\nrunStack logger metrics dbPool msgEnv f = do\n  ei &lt;- runEarly $ \\early -&gt;\n          runDatabase logger metrics dbPool $ \\database -&gt;\n            runMessaging logger metrics msgEnv $ \\msg -&gt;\n              lift $\n                f early database msg\n\n  case ei of\n    Right () -&gt; _ -- Success\n    Left err -&gt;\n      case err of\n        Left (Left (Right msgError)) -&gt; _ -- Messaging error \n        Left (Right dbError)         -&gt; _ -- Database error \n        Right ()                     -&gt; _ -- Early return\n\n\nprocess\n  :: Logger -&gt; Metrics -&gt; Early () -&gt; Database -&gt; Messaging\n  -&gt; NewMessage -&gt; IO ()</code></pre>\n<p>The shape remains almost the same when using\n<a href=\"https://hackage.haskell.org/package/servant\" rel=\"nofollow ugc noopener\">servant</a>.\nThe only quirk is that argument passing renders all\nthe fancy\n<a href=\"https://hackage.haskell.org/package/servant-server-0.20.3.0/docs/Servant-Server.html#g:5\" rel=\"nofollow ugc noopener\">hoisting</a> functionality completely\nunusable, so the stack has to be invoked inside each\nendpoint manually.</p>\n<pre><code class=\"language-haskell\">runStack\n  :: Logger -&gt; Metrics -&gt; DatabasePool -&gt; MessagingEnv\n  -&gt; (Early ServerError -&gt; Database -&gt; Messaging -&gt; IO a)\n  -&gt; Handler a\nrunStack logger metrics dbPool msgEnv f =\n  MkHandler $ do\n    ei &lt;- runEarly $ \\early -&gt;\n            runDatabase logger metrics dbPool $ \\database -&gt;\n              runMessaging logger metrics msgEnv $ \\msg -&gt;\n                lift $\n                  f early database msg\n\n    case ei of\n      Right a  -&gt; pure $ Right a\n      Left err -&gt;\n        case err of\n          Left (Left (Right msgError)) -&gt; _ -- Messaging error \n          Left (Right dbError)         -&gt; _ -- Database error \n          Right srvError               -&gt; pure $ Left srvError\n\n\ntype API = CountAPI :&lt;|&gt; _\n\nserver\n  :: Logger -&gt; Metrics -&gt; DatabasePool -&gt; MessagingEnv\n  -&gt; Server API\nserver logger metrics dbPool msgEnv =\n       countServer logger metrics dbPool msgEnv\n  :&lt;|&gt; _\n\n\ntype CountAPI = &quot;count&quot;\n             :&gt; ReqBody &#39;[JSON] CountRequest\n             :&gt; Get &#39;[JSON] CountResponse\n\ncountServer\n  :: Logger -&gt; Metrics -&gt; Early ServerError -&gt; DatabasePool -&gt; MessagingEnv\n  -&gt; Server CountAPI\ncountServer logger metrics early dbPool msgEnv request =\n  runStack logger metrics dbPool msgEnv $ \\early database messaging -&gt;\n    countEndpoint logger metrics early database messaging request\n\ncountEndpoint\n  :: Logger -&gt; Metrics -&gt; Early ServerError -&gt; Database -&gt; Messaging\n  -&gt; CountRequest -&gt; IO CountResponse</code></pre>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>There is only one unique feature effect\nsystems—or, more generally, custom monads designed\nto supercede IO—provide: they can do\nanything in between the lines [of code]. And I don&#39;t\nthink that&#39;s a good thing.</p>\n<p>Here are the advantages of running effects\nin plain IO:</p>\n<ul><li>Works with any Haskell 98 (or later) compiler;</li><li>Straightforward implementation;</li><li>No extra dependencies;</li><li>No weird type errors;</li><li>Compiles fast;</li><li>Runs fast.</li></ul>\n<p>Here are the disadvantages:</p>\n<ul><li>Some functions will be verbose.</li></ul>\n<p>I hope this knowledge is used for things.</p>","headings":[{"level":1,"text":"You don't need an effect system","id":"you-don-t-need-an-effect-system"},{"level":2,"text":"How we got here","id":"how-we-got-here"},{"level":3,"text":"Am I using the effect system for anything?","id":"am-i-using-the-effect-system-for-anything"},{"level":2,"text":"Early return","id":"early-return"},{"level":3,"text":"Type-safe exceptions","id":"type-safe-exceptions"},{"level":2,"text":"Intermission","id":"intermission"},{"level":2,"text":"Effects in plain IO","id":"effects-in-plain-io"},{"level":3,"text":"Module structure","id":"module-structure"},{"level":3,"text":"Error handling","id":"error-handling"},{"level":3,"text":"Duplicate effects","id":"duplicate-effects"},{"level":2,"text":"Real-world effects","id":"real-world-effects"},{"level":3,"text":"Argument passing, fancier","id":"argument-passing-fancier"},{"level":3,"text":"Mocking","id":"mocking"},{"level":3,"text":"Tracking resources","id":"tracking-resources"},{"level":2,"text":"Tying everything together","id":"tying-everything-together"},{"level":3,"text":"Interfaces","id":"interfaces"},{"level":3,"text":"Implementations","id":"implementations"},{"level":2,"text":"Conclusion","id":"conclusion"}]}}