Monadic Code In Haskell

I suspect that if you’re going hybrid, a better way might be to do it via manual liftA2:

import System.IO (readFile')

main :: IO ()
main =  (==)
    <*> getFile 
    <*> getFile
    >>= putStrLn . bool "Failure!" "Success!"
  where
    getFile = getLine >>= readFile'

The usage of fmap or <$> makes the code much more amenable to verticalization, which helps avoid excessively horizontal code.

Interestingly enough, on IRC I was complaining about how liftA2 and <*> can’t guarantee an order of effect, but it turns out that the 5th monad law (which means that ap = = <*>) guarantees an order of effect if the Monad instance exists for the Applicative type.

@acowley

The point of blended / hybrid / where style is to try to separate specification from implementation, to enable a fast perusal of code without caring for the implementation details unless something seems wonky. You can still name monadic actions, provided that they’re not the last line, by simply giving them a dummy bind (nameOfAction <- myActionChain), which, unfortunately, will make the compiler complain as the nameOfAction is unused.

You can even use this technique to name blocks of code, i.e, nameOfAction <- do... which can be more ergonomic than straight commenting.

Two thoughts here.

First: the benefit of purity in Haskell is not about making it look pure - it is about making it easier to reason about the code, and whether you do or do not get this benefit only superficially depends on the syntax you choose to use for your monadic code. The key thing about pure code is the absence of (side) effects; and depending on how you look at it, monadic code is either effectful, or it’s not, but massaging it into a different surface syntax does not change that. getLine >>= putStrLn has exactly the same effects as do { x <- getLine; putStrLn x }, there is literally no difference wrt purity.
In other words: we don’t write pure functional code so we can be all smug about our superior looking coding style; we do it because it creates opportunities for equational reasoning, because it makes code easier to truly understand, and as a consequence, makes it easier to write correct code.

Then; Haskell offers a wide range of equivalent but different-looking ways of writing the same code, and that is a good thing - it means we can pick the style that most closely matches what we want to say. Code is first and foremost a human-to-human language, and while it needs to “do the right thing”, that’s just the starting point - we also want code to express the programmer’s intentions, make it easy for a reader (possibly the original programmer’s future self) to retrace the thought patterns, knowledge, and assumptions that are encoded in it, see the structure of the problem being solved and how it maps to the machine side of the code, etc. Having many alternative ways of saying the same thing gives us more options to structure code in such a way as to reflect our mental model, beyond the technicalities of making the machine do the right thing.

Sometimes, we want to take the reader by the hand and walk them through a piece of code along a sequence of events. “First, we ask for a name, then we ask for a social security number, then we look up the name in a database, and then we report the result” - I’d write that in quasi-imperative style, hands down:

do
    name <- readPrompt "Name"
    ssn <- readPrompt "Social Security Number"
    row <- query db (getUserByNameAndSSN name ssn)
    print row

Other times, we might prefer to think of a series of monadic actions as a pipeline, and Kleisli arrows might be a better fit; an example might be chaining middlewares in an HTTP API, where each middleware takes a request and produces a response, either short-circuiting, or forwarding things to the next middleware:

myApp =
    sessionMiddleware >=>
    fullPageCacheMiddleware >=>
    staticFilesMiddleware >=>
    autoContentTypeMiddleware >=>
    apiMain

Yet another common situation is where we need to gather a bunch of inputs, and then we construct some kind of data structure from them, like a record type. This is a staple in serialization/deserialization code, for example. Applicative style shines here:

getUser =
    User
        <$> getInt
        <*> getString
        <*> getEmail

And then we have the where vs. let debate - but this one is not specific to monadic code or do notation, the same decision still needs to be made in non-monadic code, and AFAICT, there is no objective winner. Some people prefer a top-down approach, where you state the “big picture” first, and then the reader can, if they so wish, scroll down to read the definitions of the things you used in the big picture. You would generally use where for this. Others prefer a bottom-up approach, where you start by presenting your building blocks, and then proceed to using and combining them, stating the “big picture” last. For this, you would use let. (Of course there are also technical differences between where and let, most importantly scope, but those are relatively minor, and in most cases, the two can be used more or less interchangeably, modulo ordering).

Interestingly, the choice for top-down vs. bottom-up also has a cultural component to it. I learned about these cultural differences while working for a Dutch company that had just bought a German competitor, and I could witness the argument culture clash first hand (and it was occasionally hilarious). In a nutshell: Dutch argument culture starts with the conclusion and then provides the reasoning and evidence as needed; German argument culture starts with the evidence and then proceeds to reasoning and ends with the conclusion. “We should do X, because…” vs. “given the facts …, we should do X”. Both are valid, but if you are unaware of the cultural difference, the two can clash rather violently, with the Dutch thinking “get to the point”, and the Germans thinking “but where is the evidence”.

Personally, I’m a huge fan of having all these different styles at your disposal, and using whichever describes your thought patterns best. And if that’s a tie, I’d go with whichever style looks most straightforward, assuming a reader who is equally fluent in all styles. More often than not, this leads to a “mixed” style, and again, I think that’s a good thing. I’ll even mix let and where within the same function, if that helps me express myself better - e.g., I might introduce intermediate variables used in a multi-step calculation using let, as they become relevant, so that the reader can follow the calculation as it’s built up, but I might factor out sub-calculations into local functions defined in a where block, assuming that their names make it sufficiently clear what they are supposed to do, and putting the definitions upfront would be detrimental to the reading flow.

16 Likes