Bear with me for a bit, because I still don't get it (although I'm not the OP, I share the same doubts). In OO terms, if you have N types and M functions, with M different behaviours (function code), you aggregate the N types into an inheritance tree that makes you write 1xM functions (one function against the ancestor of the N types). If you have 2M behaviours, you aggregate the types in two different inheritance hierarchies and then write 2M functions. What expressiveness advantages do monads provide against this OO scenario?
(There are arguments that type classes are a more expressive way to solve these kind of N*M problems than inheritance - see https://en.wikipedia.org/wiki/Expression_problem - but that's orthogonal really)
That part has nothing to do with monads. The advantage of monads is the actual functionality they provide, of first-class effects and easy EDSL creation.
In general, its O(M+N), because you write the M methods in the ancestor that you talk about, and then O(N) class-specific implementations of the underlying functionality on which the M methods rely.
You only have to write N class specific implementations if each class shows a different behaviour and thus requires a different implementation. If one function code can handle N different classes that share 1 behaviour, you encode this behavioural information in the class hierarchies: by inheritance/delegation, you inform the type system that the N classes share a common behaviour. You write the code in one class, and have the N classes either inherit from that class or delegate onto that class (inheritance vs delegation is orthogonal to this discussion).
For example, consider a pattern for reading length-prefixed lists: read a number N, then read N numbers, then sum them.
main :: IO ()
main = do
n <- readLn
xs <- replicateM n readLn
print (sum xs)
“replicateM” (replicate + M for monadic) does an action N times and accumulates the results. Above we used it in IO, but we can use it in any monad, for example a parser: import Text.Parsec
import Text.Parsec.String
import Control.Monad
main :: IO ()
main = do
line <- getLine
parseTest sumParser line
sumParser :: Parser Int
sumParser = do
n <- number
xs <- replicateM n number
return (sum xs)
number :: Parser Int
number = spaces *> (read <$> many1 digit)
And if we wanted to abstract over this pattern, we could do so trivially: lengthPrefixedSum :: (Monad m) => m Int -> m Int
lengthPrefixedSum get = do
n <- get
xs <- replicateM n get
return (sum xs)
Now our first “main” becomes: main = print =<< lengthPrefixedSum readLn
And “sumParser” becomes: sumParser = lengthPrefixedSum number
There are many such functions in the standard library, such as “forM” which implements “for each” loops, and “when” which implements conditionals. If your data type is a monad, all of these control structures are available to you for free, so you can easily implement very expressive DSLs.
Same goes for monads. If we have N data types and M functions, instead of writing N×M implementations, we can write just N monad instances + M generic implementations.
So it’s kind of tautological, but monads are basically useful because lots of useful things happen to form monads—exceptions, loggers, parsers, dependency injection, persistent state operations, continuations, futures, STM transactions, I/O actions, and so on.
If you can write a data type that represents an API, and implement a couple of interfaces, you get a complete, expressive EDSL for free.
With Facebook’s Haxl, for example, you can write ordinary serial-looking I/O code, and with just a few typeclass instances, instantly get concurrent/async data fetching without changing a single line of business logic. You can’t readily do that without the kind of first-class effects that monads provide.