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.