The entire point of Monads, is restricting the ability to do these operations into functions that are tagged with having this ability, precisely so you _cannot_ invoke IO in a random pure function. It's the entire point of the language in fact.
If you want to just write IO, you can just define a function with an IO () value and use it in any other function that resolves to IO (), or call other functions that live in IO *, or any pure functions, etc etc.
I am no Haskell buff, but I believe the common complaint is that "just need to mark your function as IO ()" has non-local effects, you end up needing to propagate a bunch of extra stuff everywhere.
AFAIU the Haskell community recognized the issue as common enough to grant the existence of Debug.Trace.
> I am no Haskell buff, but I believe the common complaint is that "just need to mark your function as IO ()" has non-local effects, you end up needing to propagate a bunch of extra stuff everywhere.
That's a feature. It forces you to separate pure functions from I/O. If you spend time thinking about how you (re)factor your work you can end up with most of the interesting logic being in pure functions that are then very easy to write tests for (because you don't have to mock network services, clocks, etc.), and all the interesting I/O logic gets segregated and made [hopefully] small.
> I believe the common complaint is that "just need to mark your function as IO ()" has non-local effects, you end up needing to propagate a bunch of extra stuff everywhere
Yes, indeed you do. But it's a bit strange that that is a complaint. It's the whole point of fine grained effect tracking. If you change what effects a function does then you have to acknowledge that by changing the code that use that function (directly or indirectly)!
> you end up needing to propagate a bunch of extra stuff everywhere.
Declaring a function as non-IO is a contract to your callers that you don't do IO. You don't need to do it. You can write everything in IO if you choose. You can also call into the wonderful ecosystem of libraries, because IO functions can call other IO functions, as well as non-IO functions.
There is only ever friction if you declare your function to be IO-free. If you declare your function to be IO-free, but call IO from inside it, it's a compile error because of course it is.
So why bother declaring anything IO-free? If you do arbitrary IO in a Parser, you can't back-track. If you do arbitrary IO in Parallel code, you invite race conditions. Transactions is my favourite example, though:
.NET [1]
> Disillusionment Part I: the I/O Problem
> It wasn’t long before we realized another sizeable, and more fundamental, challenge with unbounded transactions [...] What do we do with atomic blocks that do not simply consist of pure memory reads and writes? (In other words, the majority of blocks of code written today.) This was not just a pesky question of how to compile a piece of code, but rather struck right at the heart of the TM model.
Scala: [2]
> ScalaSTM does not have the goal of running arbitrary existing code, which is where most of their problems arose.
Java/Akka: [3]
> STM is considered as a failed experiment
Clojure: [4]
> Very simply the side-effects will happen again. In the above case this probably doesn’t matter, the log will be inconsistent but the real source of data (the ref) will be correct.
Sorry for the rant, but I really needed to highlight the fact that nonIO-calling-IO is not some language design flaw created by out-of-touch academics. It's a fundamental problem.
printGreeting = print "Hello, World!"
main = printGreeting
works every bit as much as
main = print "Hello, World"
The only difference for main is that it's run because it's the entry point, but that's true of most languages and almost certainly not what you're complaining about I think?