Is production code always written monadically?

This is probably quite a naive question.

I’m writing an application, and up until this point, most of my functions have been written to take parameters that are not monads, and to return a monad, usually Either. I am usually calling them on monads, using the monadic bind, or sequence or similar. So something like

addOneToHead :: [Int] -> Maybe Int
addOneToHead [] = Nothing
addOneToHead (x:xs) = Just $ x + 1

main =
    addOneToHead =<< (Just [1, 2])

The bulk of my functions are pure functions, with just a few involving the IO monad. This is all great and feels like a comfortable pattern to work with.

However, I’m now at the stage where I need some reasonable logging output, and, realistically, this means peppering my code with logInfo function calls. I had faintly realized that this would involve expanding the use of the IO monad, but hadn’t realized quite how much. There seem to be a few recommended patterns for logging - using a MonadLogger package like Blammo, using ReaderT to pass around a log function, using RIO - but a point of commonality between all of them is that most or all of your code is run within these monads. This means that almost every function starts with a do, and is then written in do-syntax rather than conventional Haskell syntax.

I’ve seen some proposed methods for mitigating the potential impurities resulting from this (eg, https://www.fpcomplete.com/blog/readert-design-pattern/), and I’ve been looking at the RIO library, which seems neat but leans very heavily into this approach. Passing around a global state object via the ReaderT pattern seems like a generally good idea. But before I go down this path - and rewrite almost all of my existing code - I thought I’d check I’m understanding this situation correctly. Can anyone tell me which of these statements is most correct:

  • Most production Haskell doesn’t use logging, or just uses Debug.Trace opportunistically and so remains pure
  • Most production Haskell does use this pattern, and most production haskell functions are written in do-syntax
  • What I’ve written above is a misunderstanding or is in some other way incorrect.

Thanks!

5 Likes

The most correct statement is inevitably “Most production Haskell uses whatever ideas worked initially because rewriting bad code while preserving compatibility is both hard and unfun”.

If logging is not your only stateful effect and performance is not critical, using polysemy or effectful to abstract logging is the most “production-grade” thing you can do. These packages only came to exist in the past few years, so most codebases don’t use them.

If performance is critical, yes, you’ll have to pass around a logger. You obviously need to be in IO to flush logs, however the log collection itself can be as pure as you want, sky’s the limit here.

ReaderT pattern

I have no idea why people like it so much, I personally prefer passing arguments around explicitly. ReaderT Thing IO a is the same as Thing -> IO a, so why obfuscate.

3 Likes

…I suggest reading:

Functional Pearl: On generating unique names (1994)

If you’re only going to be occasionally using effects, just passing an extra parameter to each site of use is another option. See the value-supply library for a simple working implementation.

But more generally, most Haskell code being monadic is a matter of legacy - to quote Richard Bird:

…so in an alternate reality where John Hughes introduced arrows to Haskell first, the majority of the various “effect-management” libraries would be using that abstract interface instead (and the Haskell logo would probably be somewhat different :-) .

I just wrote some production code with Applicatives alone today :grin:

3 Likes

Did you use the Applicative methods directly, or use do-notation? If the latter, then:

Is it any wonder as to why this thread was so “popular” :

Thanks for this response. polysemy and effectful look interesting, but I’d rather not detour to learn them right now, so the main message I’m actually taking away from this thread is “stick to simple vanilla haskell”. I think I’m going to start by just passing around an AppState record between functions, and have them return either the IO or Writer (or something similar) monads. As you say - and I hadn’t thought about this - IO only needs to be present when the log is being flushed, not when it’s being added to.

2 Likes

Normally you have a nice situation where a lot of your code can be pure, with only some bits of plumbing being monadic. Logging is indeed IME the thing that forces you to make a lot of stuff monadic. Still, you can often make e.g. most of the functions in a module pure, and then only the exported ones which log what happens monadic. But it does kind of suck.

I have recently been using the co-log/contra-tracer style of logging, which is… fine.

3 Likes

But it does kind of suck.

…no, it just does:

1 Like

@harris-chris Hi, let me answer quickly.

  • Debug.Trace is not for logging. It’s for debug only, and using it for logging is a bad idea. (As bad as using Read and Show for serialization)
  • Yes, most Haskell codebases are heavily monadic with various monadic approaches used. You can find FT/mtl, ReaderT, effect systems, Free monads and custom monads everywhere. That’s okay in Haskell. We actually don’t want to write purely functional applications, so we write imperative applications in Haskell instead.

When it comes to questions about real Haskell code, there are resources you might be interested in. After all, your questions are about the architecture and design of application. I’d recommend reading two Haskell books on this subject, my and @parsonsmatt :

It’s an advanced stuff, but it’s worth learning anyway.

1 Like

I actually think effects or monad stacks or whatever aren’t the advanced stuff of production Haskell. If only because they aren’t the advanced stuff of backend software engineering. Very little bad happens when you mess that part up (I guess you have to work with bad code?) Also, monad stack architecture stuff tends to be the most mechanical part of a program to refactor.

So my advice to people is always is to just use … whatever. Usually that’s determined by a library doing more important things (like which HTTP server library you’re using). It’s fine to just spew lines to stderr (which is what traceM does [1]) starting out and learn as you go. Thinking about doing the Right Thing from the jump is more hindering than making mistakes along the way.

[1] I actually do use traceM for logging in my games due to Windows concerns. It does the right thing more than a “proper” stderr logging library. I built my own context logger on top though. Because that sort of library is so trivial to build yourself that it’s worth it for the customization.

3 Likes

I disagree that most haskell codebases are heavily monadic, and would say that it depends heavily on what you are actually writing. (i.e. if that is some application, or a library to solve some particular problem).

I would guess that about 90% of the haskell code I write is pure, simply because the type of problems I solve are algorithmic in nature. Furthermore, I would think that for most of the packages on hackage, a large part of their codebase is pure. If, in the end you want to build an application it is hard to avoid some monadic shell, but I don’t think that suddenly means that “most codebases are heavily monadic”

5 Likes

Much of Haskell’s code is private, a lot of it is on GitHub only. Take a look at Cardano, for example. An enormous code base that is heavily monadic. I believe various backends in summary can easily compete with Hackage

I mean just because there are a bunch of do blocks and binds doesn’t mean the logic is monadic.

For instance…are esqueleto queries “monadic”? They have a monadic interface. But is the Monad part interesting? Not really!

What about my code that runs megaparsec on a blob in a background job? The b/g job uses a Reader to pass config. And my megaparsec code has some do’s and binds I guess. But the monadic part isn’t interesting. And using applicative, semigroup, alternative, foldable, traversable tends to do the heavy lifting in my parsers.

I’d actually argue that production Haskellers should aim to solve problems with Applicative, Semigroup, Semialign, Traversable if they want their code to be good. Obviously I don’t believe in hard rules, but it’s definitely a nice rule of thumb.

It really depends on the type of applications. I have two apps in production.
One is a website (backend in Haskell, frontend in Elm).
The code of the backend is basically, load things from the DB, encode to JSON, send it to the front end.
This is really monadic, but there is nothing exiting about it. The code doesn’t do anything. The most “complex” things is to compute the next working day.
My second app (the big one), does lots of “exciting” things mainly managing the stock and linking in the accouting system. This involves tracking where boxes are in the warehouse, finding where to put boxes in the warehouse for next delivery, replaying history of stock movement to correct bugs in the accounting system, reading bank statement and doing automatic reconciliation, displaying charts with with the forecast of the “quantity on hand” for a given product etc …
All of that is basically,

  • load things, 5-20 LOC monadic
  • do exciting stuff 10-2000 LOC) pure
  • save/display stuff 5 LOC monadic
    During the “exciting”/pure bit, there is nothing to log (unless for debugging purpose), nothing is happening from a user point of view.

If your code is more like my backend one, there is lots of things to log, but its is because you are already doing IO (and you are therefore already in an IO monad).

3 Likes

I’d add that a healthy haskell codebase should try to converge to isolating effects as much as possible.
You generally don’t need to log pure functions because they are deterministic by definition.

6 Likes

That’s what i was trying to explain badely.