Regardless of your opinion on the matter, that's how it's actually implemented. IO is roughly equivalent to "State RealWorld#" with some extra strictness/unboxing stuff.
What you're talking about is basically the free monad over the (Request, Response -> a) functor. That would probably work, but be less performant and harder to extend.
IO is nice because it's really easy to safely integrate with FFI calls. I don't think you could easily do the same with your proposal.
The interpreter is simpler and easy to implement in any language too.
If you look at the generated core code, it's important to notice that it's not the same "token" being passed around everywhere. Each IO action returns a new token, which could be a different value from the old token. (Again, this is entirely theoretical, as the token/world state doesn't exist in the binary.)
Saying
> when you call "print", it returns a "new world" that is the state of the world after printing
makes no sense, since the word "state of the world" will be interpreted completely differently by the receiver of the explanation. State of the world = a large state object with lots of fields describing every single thing in some "World" (what world: program world? computer world? network world? all the world?). Thats what people hear.
The name of the type is horrible, and the world-passing intuition is also horrible. "Token" would've been much better, but its still not clear how to implement or why its even necessary (in a non-lazy language)
On the other hand, a tuple representing the current action and the function that will return the next action (or nil) is fairly clear, easy to implement an interpreter for, the pure/nonpure distinction becomes obvious (the runtime interpreter is the impure part), and laziness doesn't even need to get into the mix.
oh, and also regarding FFI: {type: 'FFICall', fn: string, arguments: [...]}
> {type: 'FFICall', fn: string, arguments: [...]}
I agree that this is simpler to understand, but it's no good in practice. It's obviously not type safe, and you still have to bake support into the interpreter/runtime for whatever your string value is. It's also effectively impossible to inline/optimize away. Which, again, is probably why it's done using "State RealWorld#" in GHC.
I would like to reiterate that however RealWorld# happens to be defined, the way its use is enforced in GHC is the exact same as if it did actually contain a description of the entire universe. You just aren't allowed to look at it. But, you are correct, this is more confusing than a request/response free monad.
So in JS notation: