Haskell simplifies this all by just currying everything so that a function of two variables looks like a single variable function. Then you just have to define “applicative” and you get fmap that can apply to functions of any arity.
In python, by contrast, any equivalent definition of Applicative would be super non-idiomatic. On the other hand, you could write a very idiomatic ApplicativeOfTwoVariables or ApplicativeOfThreeVariables and so on.
The difference between programming with curried and non-curried functions runs really really deep, and I don’t think we’ve found the right abstractions yet that makes functor/applicative/monad analogs actually simplify developers’ lives in uncurried languages.
(Yet another in the long list of Very Popular But Also Just Wrong ideas surrounding this not-that-complicated concept.)
What the monadic interface to the IO type (very carefully phrased) in Haskell allows is building up a value to be interpreted at run time with a convenient API that looks very like it is an imperative programming language. But it actually constructs a large, complicated value that (per the linked article) is really just a big data structure, which at runtime Haskell lazily walks through (laziness being key here since the vast majority of programs have effectively infinite loops which mean the data structure representing the program would be infinite in size if strictly manifested in memory) to run the program.
In practice, you don't need to think about this distinction and you program with the IO type in the top-level IO context as if it is "really" an imperative language that is really doing what you tell it to do.
But "monad" has nothing to do enabling side effects. Monad is just an interface. Interfaces lack the power to fundamentally rewrite the nature of a language. If you just hacked out the place in the Haskell base library where IO declares that it is an instance of Monad, you could still do everything Haskell does today. Nothing about IO itself would change. What would change is that all the generic functions that have a Monad constraint would no longer work on IO, and you'd have to clone all of https://hackage.haskell.org/package/base-4.16.3.0/docs/Contr... into a new specialized set of functions that work on IO. But that's all that would happen, and a whole lot of code would have to be mechanically translated to use these new non-generic functions. Haskell would not break. Haskell would not suddenly become unable to speak on the network or read files. It would merely become less convenient to use for no benefit.
Edit: You may be thinking of pseq, but I don't think that's in the Haskell 98 report.
WARNING: the below contains math. Ignore if math makes you mad.
If "side effects" means the availability of certain operations such as mutating state, throwing errors etc, then monads certainly do have something to do with side effects in that these operations can often be described by an algebraic theory and algebraic theories have a direct correspondence to certain kinds of monads (the monad of free algebras of the theory).
You'll be stuck with a wrong understanding of them as long as you think that's what they are. That is one of the things they can be used for, but that's all. It isn't what they are. There are plenty of "monads" that are not being used for "side effects" and are perfectly pure no matter how you look at them, such as the list instance of monad. Rather than arguing with me about how if you bend the list a bit and sort of redefine "side effect" and if you squint a bit, the list instance really is related to "side effects", just don't look at it that way.
Haskell then decided to use this DSL for arbitrary types, again just to build data structures. Then, people noticed that monads were really cool.
I think it's worth expanding this point out a bit. As well as your other point about knowing that a function from `a -> a` is sound. It's the declarative nature of monads that are truly valuable, especially when communicating with other devs through code. The explicit declaration of side-effects really comes into its own when applications are large. Often it means we don't have to go and 'look inside' a function to see what its interaction with the world will be.
Obviously there's the 'other stuff' too:
* Compositionality
* Package up complex boilerplate so it's never written again
* Ability to optimise the core implementations later without rewrites (I'm thinking for more complex domain managing monads, rather than the 'built-ins')
> For (say) a method in Ruby, which can already do anything, monads are not very useful IMO.
That's probably true for dynamic languages where there's no declarative value to using monads; although the encapsulation of common behaviour is still useful.
I develop language-ext [1] a functional framework for C# that tries to bring the benefits of monads (and other functional goodness) into C#. It is true that a programmer can still launch nuclear missiles in between lines of code if they want to. But with a bit of self-discipline it's entirely possible to reap similar benefits to what you'd see in Haskell. LINQ in particular is very powerful for this. The compiler doesn't get a look in, like it does for Haskell, and that is definitely a downside - because it can't ever be optimised as well as Haskell can, but that's just a trade-off.
Of course it isn't Haskell, but it's still possible to get 95% of the benefits.
I just don't think the generalization of those concepts is very interesting. Monoids seem a lot more interesting to me, especially when it comes to splitting up computations (mapreduce) or partial computation caching, you can really come up with quite a few useful ones
Monads in comparision seem boring.
In Haskell, it's almost exactly the opposite: by default a function can do nothing but access its argument and do computations, but by adding more and more monadic contexts it can do more. This is useful for things like lazy evaluation, because the compiler can infer many more optimizations for pure functions than for functions that might do "anything". It also has benefits for security: a function with type `a -> a` is (almost) certainly not going to access the database or the filesystem. You can have the compiler confirm many assertions about the program when all side effects have to be declared through the type system.
For (say) a method in Ruby, which can already do anything, monads are not very useful IMO.