IO t is a black box too. If it's a data structure, it's a crappy one: you can't pick it apart like a list, inspect it, or do anything except sequence it.
I didn't follow what you meant about "runtime vs evaluation." To me they seem hopelessly intertwined: if you evaluate head [], you get a runtime error.
You can build a completely pure version of the IO type assuming Haskell's evaluation semantics, so long as you have one base "escape" function ffi : String -> String (or whatever serialization you want) that serves as a foreign function interface. Mind yo, the ffi function is evaluated the same way (with all the non-strictness involved) as other functions in Haskell.
There's no such thing as inpure Haskell, even in your IO monad (this is more theory than fact with GHC, for performance reasons). Basically your IO type is some instructions for the runtime. The runtime does stuff and then injects values back into Haskell, but for the language itself, values are not changing.
I have a hard time explaining it. But I think GP's point was that you evaluate main to get something of type IO (), and the runtime takes something of type IO () to do stuff with it (much like the difference between javac and java). There's some juggling going along with it...
It's not a black box in the sense you can move it around, evaluate later, not evaluate at all, etc. There is fundamentally nothing you can inspect about a general IO operation: it's a handle to a computation dealing with the external world.
I'm pretty sure you can move black boxes around in the real world too :)
There are definitely sensible things to do with "general IO operations." For example, consider `withArgs`, which replaces argv for the duration of an action. A natural approach is to pick through the received action, and replace all calls to `getArgs` with `return tempArgs`. But `withArgs` doesn't work this way because Haskell can't pick apart IO actions. Instead it uses a nasty FFI to modify a global. (It doesn't even look thread-safe: I'll bet withArgs "leaks" into other threads.)
It's also overbroad: IO encapsulates "real world" computations like deleting files, but also basic operations like getting argv! I'd argue it's a failure of Haskell that the type system does not distinguish between a function that erases your hard drive and a function that gets your command line arguments.
You either enforce 100% purity, or your language is not pure. Haskell chose to walk the former path. In that case reading global state needs to happen in the IO monad even if it is reading a couple of command line arguments. Though I completely agree it is nasty to change them in runtime, it is also assumed you know what you're doing when you tinker with them (cross-thread implications included.)
To a Haskell program, command line arguments are exactly the same outer world as your super important files.
well, maybe gray then; it's not completely opaque. You know it's type, you can apply functions to the value inside. You can pass it around like a variable and if desired, completely throw it away. the point was to show that this is different than some bytecode.
I'm not sure about the head [] example, it might actually be represented by a pure value such as _|_ (bottom) which bubbles up and stops your program when running.
I didn't follow what you meant about "runtime vs evaluation." To me they seem hopelessly intertwined: if you evaluate head [], you get a runtime error.