I agree that IO can and probably should be taught before Monad. It happens to be one of the most useful monads, but return and (>>=) for IO can be taught before one gets deep into type classes and monad laws.
That said, one thing that can’t be avoided is Haskell’s execution model, which probably requires more cognitive work than Monad itself. The type signature String -> IO String literally means, “this is a function that takes a String and returns an IO String action”, and not, “this is a function that does IO and returns a String”, since the latter makes no sense in a world where functions are pure. Then you get into the distinction between computing an action and executing it, which means that the student needs to understand Haskell’s nonstrictness and the concept of a “thunk” as well.
String -> IO String
As far as I can see, the hardest part of Haskell for new people to get isn’t type classes or monadic IO, but the difference between lazy and strict evaluation and how that interacts with the type system, i.e. an Int is no longer an existing bit pattern but merely a computation proven to deliver an Int (well, modulo ⊥).
For people coming from Racket: it helped me more to imagine IO being all about constructing a 3D AST for an I/O interpreter.
Read cis194, it’s awesome.
constructing a 3D AST for an I/O interpreter.
What makes it hard to grasp when you start digging in later is it’s much lighter weight than that.