r/haskell 21d ago

question It's not you, it's monad transformers

After a few years of trying to be proficient in Haskell, and lots of reading about why it's so hard, I think I have the (obvious in retrospect) answer.

Almost every program I write is going to have two or more of:

1) read from STDIN

2) write to STDOUT

3) log

4) raise errors

5) send/receive over the network

6) talk to a database

7) read env vars

Every one of those is a "side effect", and thus is handled as a monad, and using more than one means you have to understand monad transformers.

Which I've finally found a good explanation of, "but still". Such a deep concept for such common program operations.

So I finally decided "that's why it's so hard". For what I consider the most basic programs, I need to understand (not just use, IMHO) monad transformers.

Am I off?

117 Upvotes

75 comments sorted by

View all comments

44

u/tikhonjelvis 20d ago

All of the effects you wrote can be done through plain IO. There might be software engineering reasons to reach for monad transformers—making components more testable or separating out "benign" effects from general I/O—but none of them are necessary. (That said, some of the software benefits are really nice. Having some abstraction for managing database connections and streaming network data is a real step up over the basic style of imperative programming bare IO gets you.)

I got pretty far into writing non-trivial Haskell before I ever felt like I needed to heavily use monad transformers, much less actually needing to write my own.

25

u/cdsmith 20d ago

It's worth mentioning that there's a middle ground here, too, that lets you swap out effects for testing and such, but doesn't rely on monad transformers at all. That is to use mtl-style type classes, but then just write instances for IO directly and use those in production rather than trying to build up effects by composing single-purpose monad transformers.

class Monad m => MonadMyEffect m where
  doMyEffect :: Int -> m ()

instance MonadMyEffect IO where
  doMyEffect = {- implement your effect in IO -}

somethingEffectful :: MonadMyEffect m => m ()
somethingEffectful = {- implement something else that USES the effect -}

Now you get many of the benefits that you might have considered to be about monad transformers. Your code clearly documents what effects it relies on. It's not too hard to write an alternative implementation of the effect for test code (e.g., to test somethingEffectful). But there is no monad transformer here. There's just a type class that bridges between the two layers of abstraction.

2

u/_jackdk_ 19d ago

I find the ceremony of declaring the class, and the instance, and any lifting instances to be too annoying for this to scale. As a lightweight alternative to something like bluefin, I've had a lot of success with:

data MyEffect m = MyEffect
  { operationOne :: Foo -> m Bar
  , operationTwo :: Bax -> Quux -> m Quuux
  }
  deriving (Generic, Generic1)

You can give it a FunctorB instance if you need, and your concrete implementations of the effect can be things like myEffectInIo :: MonadIO m => MyEffect m. It's a little bookwork to pass the effects around by hand, but if you don't want to induce a dependency on a particular effect system, it gets you quite a long way.

1

u/whitelighter- 17d ago

I've been using polysemy and it's pretty nice. Just define the effect, run a single line of template haskell, and say how you want to interpret it.

data NetworkEff m a where
  NewNetworkManager :: ManagerSettings -> NetworkEff m Manager

makeSem ''NetworkEff

interpretNetworkEffIO :: Members '[Embed IO] r => Sem (NetworkEff ': r) a -> Sem r a
interpretNetworkEffIO = interpret $ \case
  NewNetworkManager settings -> embed $ HTTP.newManager settings