Quantcast
Channel: Should do-notation be avoided in Haskell? - Stack Overflow
Viewing all articles
Browse latest Browse all 8

Answer by Benjamin Hodgson for Should do-notation be avoided in Haskell?

$
0
0

I often find myself first writing a monadic action in do notation, then refactoring it down to a simple monadic (or functorial) expression. This happens mostly when the do block turns out to be shorter than I expected. Sometimes I refactor in the opposite direction; it depends on the code in question.

My general rule is: if the do block is only a couple of lines long it's usually neater as a short expression. A long do-block is probably more readable as it is, unless you can find a way to break it up into smaller, more composable functions.


As a worked example, here's how we might transform your verbose code snippet into your simple one.

main = do    strFile <- readFile "testfile.txt"    let analysisResult = stringAnalyzer strFile    return analysisResult

Firstly, notice that the last two lines have the form let x = y in return x. This can of course be transformed into simply return y.

main = do    strFile <- readFile "testfile.txt"    return (stringAnalyzer strFile)

This is a very short do block: we bind readFile "testfile.txt" to a name, and then do something to that name in the very next line. Let's try 'de-sugaring' it like the compiler will:

main = readFile "testFile.txt">>= \strFile -> return (stringAnalyser strFile)

Look at the lambda-form on the right hand side of >>=. It's begging to be rewritten in point-free style: \x -> f $ g x becomes \x -> (f . g) x which becomes f . g.

main = readFile "testFile.txt">>= (return . stringAnalyser)

This is already a lot neater than the original do block, but we can go further.

Here's the only step that requires a little thought (though once you're familiar with monads and functors it should be obvious). The above function is suggestive of one of the monad laws: (m >>= return) == m. The only difference is that the function on the right hand side of >>= isn't just return - we do something to the object inside the monad before wrapping it back up in a return. But the pattern of 'doing something to a wrapped value without affecting its wrapper' is exactly what Functor is for. All monads are functors, so we can refactor this so that we don't even need the Monad instance:

main = fmap stringAnalyser (readFile "testFile.txt")

Finally, note that <$> is just another way of writing fmap.

main = stringAnalyser <$> readFile "testFile.txt"

I think this version is a lot clearer than the original code. It can be read like a sentence: "main is stringAnalyser applied to the result of reading "testFile.txt"". The original version bogs you down in the procedural details of its operation.


Addendum: my comment that 'all monads are functors' can in fact be justified by the observation that m >>= (return . f) (aka the standard library's liftM) is the same as fmap f m. If you have an instance of Monad, you get an instance of Functor'for free' - just define fmap = liftM! If someone's defined a Monad instance for their type but not instances for Functor and Applicative, I'd call that a bug. Clients expect to be able to use Functor methods on instances of Monad without too much hassle.


Viewing all articles
Browse latest Browse all 8

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>