What you say is correct, but let me try and explain a little further. The Haskell function `getChar` is used to get a character from stdin. It has the type `IO Char` and is a totally, 100% pure. In fact, it isn't even a function, it's just a value.
You can write `getChar` as many times as you want, and you will get the same result every time, and it will have nothing to do with whatever character is being sent to stdin. Instead, what you will get is an instruction, saying "please get a character from stdin".
What the IO monad allows you to do is to compose instructions together. For example, you could write `getChar >> getChar". This makes a new instruction that says "please get a character from stdin, discard it, then get a character from stdin". The `>>` operator means "follow the instruction on the left, discard the result, then do the instruction on the right".
Your entire program is made by composing instructions like this, into one new huge instruction that specifies your entire program's behaviour. You assign that to the special name `main`. At runtime, the instruction is executed.
The only bit of compiler magic that's necessary is the code for interpreting the instructions and actually executing them. In theory, although haskell doesn't let you do this, you could write `getChar` yourself, and just return the same thing that `getChar` does, and it would behave identically.
This I feel is the simplest explanation for why monads are necessary in Haskell. They allow you to conveniently specify what you want your program to do, using only pure functions and values. Why do you want all values to be pure? Many don't because it's kind of a pain, but Haskell kind of exists to see if a lazy, pure functional language can be fun to use and many find that it is. Hope this helps someone!