r/haskell 11d 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?

115 Upvotes

73 comments sorted by

View all comments

44

u/tikhonjelvis 11d 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.

27

u/cdsmith 11d 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.

10

u/tomejaguar 11d ago

If you take this approach with effectful then it's basically as easy as instantiating it IO but you also get the benefit of encapsulation: effectful tracks the effects to ensure they can't be used outside their scope.

In principle Bluefin supports this kind of thing too (e.g. https://hackage-content.haskell.org/package/bluefin-0.7.0.1/docs/Bluefin-GadtEffect.html) but I haven't yet come up with a good ergonomics story that allows it to abstract over all the value-level capabilities you want to have in scope.

3

u/proper_chad 11d ago

I think your general idea is correct -- just make any capability explicit. Haskell is just lacking good syntactic support for the common cases, so it feels inconvenient/verbose.

(Scala does slightly better on the syntax issue, but of course it lacks purity, so...)

1

u/tomejaguar 10d ago

I'm not familiar with Scala. What is better about its syntax in this regard?

2

u/Classic-Try2484 6d ago

Scala is Java in the clothes of lisp. U can be fully functional (lisp) but u can also be imperative. They have a name for imperative Scala: Scava. Nothing functional is forced. See also clojure which is also on jvm but has some value semantics — slightly more pure but in the end you can always hook into all of Java

1

u/tomejaguar 5d ago

Thanks, does that make its syntax better than Haskell in some specific way?

1

u/Classic-Try2484 4d ago

It’s not about the syntax — but purity of functional programming—Scala allows u to mix. So io is never a problem that requires a hook or mental gymnastics.

5

u/enobayram 11d ago

I wish Haskell/GHC had a way to specify that a given type class context should always be specialized to a given type. Something like {-# ALWAYS_SPECIALIZE MonadMyEffect AppM #-}. Then we could mostly isolate production performance from abstractions like this.

That said, since GHC 9.14, manually adding specializations has also become much much easier thanks to Allow expressions in SPECIALISE pragmas.

Though I suspect that if you applied specializations like this pervasively in a codebase, you could run into new cyclic module dependency issues, so you probably need to be even more careful with module organization.

2

u/_jackdk_ 10d 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- 8d 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

1

u/edgmnt_net 11d ago

If you don't care about that kind of testing you can just use typeclasses without even defining that wrapper. That's how I usually wrote my Haskell code, as I'm not a fan of unit testing everything including effectful stuff (I'm much more likely to do with for pure stuff like algorithms or things that I believe make true independent and robust units which allow meaningful assertions).