Clearly, >>=
is not a way to represent function composition. Function composition is simply done with .
. However, I don't think any of the articles you've read meant this, either.
What they meant was “upgrading” function composition to work directly with “monadic functions”, i.e. functions of the form a -> m b
. The technical term for such functions is Kleisli arrows, and indeed they can be composed with <=<
or >=>
. (Alternatively, you can use the Category
instance, then you can also compose them with .
or >>>
.)
However, talking about arrows / categories tends to be confusing especially to beginners, just like point-free definitions of ordinary functions are often confusing. Luckily, Haskell allows us to express functions also in a more familiar style that focuses on the results of functions, rather the functions themselves as abstract morphisms†. It's done with lambda abstraction: instead of
q = h . g . f
you may write
q = (x -> (y -> (z -> h z) (g y)) (f x))
...of course the preferred style would be (this being only syntactic sugar for lambda abstraction!)‡
q x = let y = f x
z = g y
in h z
Note how, in the lambda expression, basically composition was replaced by application:
q = x -> (y -> (z -> h z) $ g y) $ f x
Adapted to Kleisli arrows, this means instead of
q = h <=< g <=< f
you write
q = x -> (y -> (z -> h z) =<< g y) =<< f x
which again looks of course much nicer with flipped operators or syntactic sugar:
q x = do y <- f x
z <- g y
h z
So, indeed, =<<
is to <=<
like $
is to .
. The reason it still makes sense to call it a composition operator is that, apart from “applying to values”, the >>=
operator also does the nontrivial bit about Kleisli arrow composition, which function composition doesn't need: joining the monadic layers.
†The reason this works is that Hask is a cartesian closed category, in particular a well-pointed category. In such a category, arrows can, broadly speaking, be defined by the collection of all their results when applied to simple argument values.
‡@adamse remarks that let
is not really syntactic sugar for lambda abstraction. This is particularly relevant in case of recursive definitions, which you can't directly write with a lambda. But in simple cases like this here, let
does behave like syntactic sugar for lambdas, just like do
notation is syntactic sugar for lambdas and >>=
. (BTW, there's an extension which allows recursion even in do
notation... it circumvents the lambda-restriction by using fixed-point combinators.)