Most Haskell tutorials teach the use of do-notation for IO.
I also started with the do-notation, but that makes my code look more like an imperative language more th
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.