Look at any do block in Haskel, PureScript, Idris etc. It is the imperative code. The individual effects are typed and separated, but it is still the code that depends on implicit state with all its drawbacks.
Then look at Elm code. Elm does not have any imperative hatches. The monad that runs everything is at the very top level (“shell” as the article calls it) and hidden.
As such Elm code is forced to use functional decomposition resulting in very easy to follow, refactor and maintain designs.
Its still quite different than classic imperative code
If you're working with a free monad, or if you don't specify IO (just some of the generic IO like typeclasses like say MonadError), you can still choose your own interpreter for the monad and "program" the semicolon. Which means you get back all the benefits of testability etc.
To get a similar effect in an imperative language, you would use e.g. coroutines and `yield` every side effect to the execution engine. The engine will take the action "specs" (probably a data structure describing the action to perform, e.g. set some value in memory) and decide what to do with them, and you can swap the real engine with a test/mock engine in your tests.
Programming semicolon is not different from mocking interfaces with imperative code. One still has to write it and tests still do not test the real interfaces. Surely the situation is improved compared with imperative code, but it is not as good as with monad-free code.
It is pity that modern conveniences like polimorphic record types with nice syntax for record updates were not invented earlier. With those even with complex code monads can be used only at the top level when the sugar of do blocks is not even necessarily.
Do-notation is Haskell is purely syntactic sugar over function calls. You can remove do-notation from Haskell and still write the exact same programs (with monads and all).
Also, monads are not about state anymore than classes in Java are about Toasters.
Surely a do-block is just sugar for the functional code. But that code can be used to model all imperative effects. As such the code inevitably models all troubles the imperative code can cause.
If one looks at the desugared version one can see where the trouble comes. Functional code using monadic style depends on the state of the monad interpreter that can be arbitrary complex and spread over many closures with many interdependencies. It can be rather hard to uncover what exactly is going on, precisely in the same way as with imperative code it models.