This is a suggested "interpretation" of the
IO monad. If you want to take this "interpretation" seriously, then you need to take "RealWorld" seriously. It's irrelevant whether
action world gets speculatively evaluated or not,
action doesn't have any side-effects, its effects, if any, are handled by returning a new state of the universe where those effects have occurred, e.g. a network packet has been sent. However, the result of the function is
((),world) and therefore the new state of the universe is
world. We don't use the new universe that we may have speculatively evaluated on the side. The state of the universe is
You probably have a hard time taking that seriously. There are many ways this is at best superficially paradoxical and nonsensical. Concurrency is especially either non-obvious or crazy with this perspective.
"Wait, wait," you say. "
RealWorld is just a 'token'. It's not actually the state of the entire universe." Okay, then this "interpretation" explains nothing. Nevertheless, as an implementation detail, this is how GHC models
IO.1 However, this means that we do have magical "functions" that actually do have side-effects and this model provides no guidance to their meaning. And, since these functions actually have side-effects, the concern you raise is completely on point. GHC does have to go out of its way to make sure
RealWorld and these special functions are not optimized in ways that change the intended behavior of the program.
Personally (as is probably evident by now), I think this "world-passing" model of
IO is just useless and confusing as a pedagogical tool. (Whether it's useful for implementation, I don't know. For GHC, I think it is more of a historical artifact.)
One alternative approach is to view
IO as a describing requests with response handlers. There are several ways of doing this. Probably the most accessible is to use a free monad construction, specifically we can use:
data IO a = Return a | Request OSRequest (OSResponse -> IO a)
There are many ways to make this more sophisticated and have somewhat better properties, but this is already an improvement. It doesn't require deep philosophical assumptions about the nature of reality to understand. All it states is that
IO is either a trivial program
Return that does nothing but return a value, or it's a request to the operating system with a handler for the response.
OSRequest can be something like:
data OSRequest = OpenFile FilePath | PutStr String | ...
OSResponse might be something like:
data OSResponse = Errno Int | OpenSucceeded Handle | ...
(One of the improvements that can be made is to make things more type safe so that you know you won't get
OpenSucceeded from a
PutStr request.) This models
IO as describing requests that get interpreted by some system (for the "real"
IO monad this is the Haskell runtime itself), and then, perhaps, that system will call the handler we've provided with a response. This, of course, also doesn't give any indication of how a request like
PutStr "hello world" should be handled, but it also doesn't pretend to. It makes explicit that this is being delegated to some other system. This model is also pretty accurate. All user programs in modern OSes need to make requests to the OS to do anything.
This model provides the right intuitions. For example, many beginners view things like the
<- operator as "unwrapping"
IO, or have (unfortunately reinforced) views that an
IO String, say, is a "container" that "contains"
Strings (and then
<- gets them out). This request-response view makes this perspective clearly wrong. There is no file handle inside of
OpenFile "foo" (\r -> ...). A common analogy to emphasize this is that there is no cake inside of a recipe for cake (or maybe "invoice" would be better in this case).
This model also works readily with concurrency. We can easily have a constructor for
Fork :: (OSResponse -> IO ()) -> OSRequest and then the runtime can interleave the requests produced by this extra handler with the normal handler however it likes. With some cleverness you can use this (or related techniques) to actually model things like concurrency more directly rather than just saying "we make a request to the OS and things happen." This is how the
IOSpec library works.
1 Hugs used a continuation-based implementation of
IO which is roughly similar to what I describe albeit with opaque functions instead of an explicit data type. HBC also used a continuation-based implementation layered over the old request-response stream-based IO. NHC (and thus YHC) used thunks, i.e. roughly
IO a = () -> a though the
() was called
World, but it isn't doing state-passing. JHC and UHC used basically the same approach as GHC.