
It is not possible to perform I/O outside of the IO monad – your code will not compile – and, furthermore, I/O “colours” all the functions which call it, because if you are calling something which returns IO, then you have to be returning IO as well.
This might look like a lot of bureaucracy, but it actually does two very helpful things: firstly, it immediately tells the programmer “hey, this function performs side-effects in I/O” which means that they don’t have to read the code in order to understand what it does, just the type signature; secondly, it means that you cannot accidentally perform I/O in a function you thought was pure – this, in itself, eliminates whole classes of bugs in which one may think one understands all the dependencies of a function, but actually something is affecting it which you did not realise, because it can perform side-effects.
- https://www.gtf.io/musings/why-haskell