An article I wrote a while back after I wrapped up the chapter on IO in the “Haskell programming from first principles” book (great book btw, highly recommend).
It’s interesting because, despite having been doing Haskell work professionally and generally working with Haskell for over two years at that point, I don’t think I’d have come to that realization on my own. I’d love to hear other people’s thoughts on this!
I like this explanation of IO, although I think the describing it as
representation/description of an action/statement that when executed at runtime (given the current world state) yields a value of type a
is unnecessarily complicated. Why not simply
action/statement that when executed yields a value of type a
That is, why describe it as a “representation/description”? We don’t call Integer a “representation/description” of an integer, we just call it an integer.
And why say “when executed at runtime”? When else could it be executed?
That is, why describe it as a “representation/description”?
The advantage of this choice of words is that it allows IO A to be a first-class value — if you pass values of type IO A around, then you’re not executing anything, but only pass “descriptions” around.
I find the distinction useful: Integer represents the mathematical integers, but there are different representations and two types can be distinct while representing the same mathematical / semantic object.
And why say “when executed at runtime”? When else could it be executed?
It could be executed at no point in time, for example — a Haskell program of type main :: IO () could be evaluated to a description of an action, but that description is never executed at runtime. We do need the additional convention that a Haskell compiler emits code that computes the value main and also executes the action main.
That is, why describe it as a “representation/description”?
Like @HeinrichApfelmus says, when you have a value of type IO a, you don’t actually have an action or statement; what you have is a value that describes an action or statement. I called it a representation because IO is, IMO at least, Haskell’s interpretation of the notion of actions and statements.
With programming, we have this concept of “something that can be executed to change the state of the world” (AKA an action or a statement), and Haskell’s reification of that concept is the IO type. It is its representation of that idea.
It’s kinda like how there is a difference between numbers and numerals. The latter being a representation of the former. Most of the time, the difference doesn’t really matter until you start dealing with numerals beyond just base 10, then you’re left wondering how 80 in base 16 is equal to 128 in base 10
IME, it pays to make things like this clear to avoid confusion when the differences start to matter. It ends up being more words, but I think conflating stuff, or having things be “implied by omission” tends to do more harm than good
I can definitely see why it seems strange to say we’re “passing a statement around”. But why does “passing an action around” not allow IO A to be a first-class value?
Sure, but that objection applies equally well to each viewpoint.
What’s the difference between having an action and having a value that describes an action?
Right, so do you say that Integer is an “infinite-precision base-2 encoded numeral describing an integer”?
With all that said, it might be accurate to consider Haskell’s IO a not as a value like String , Either e a , or Int , but rather as a representation/description of an action/statement that when executed at runtime (given the current world state) yields a value of type a potentially performing side effects along the way.
I think that’s a good way to look at it. As Phil and I said in Imperative functional programming (1993), Section 2: “Notice the distinction between an action and its performance. Think of an action as a \script”, which is performed by executing it. Actions themselves are first-class citizens. How, then, are actions performed? In our system, the value of the entire program is a single (perhaps large) action, called mainIO, and the program is executed by performing this action."
Do you think it’s good to look at IO a “not as a value”. If so, what purpose does that view achieve?
Notice the distinction between an action and its performance. Think of an action as a “script”, which is performed by executing it. Actions themselves are first-class citizens.
Can this view view of IO a also be applied to functions? That is, is this an equally valid statement:
Notice the distinction between a function and its performance. Think of a function as a “script”, which is performed by executing it. Functions themselves are first-class citizens.
The reason I care about this issue is that there is longstanding confusion about the supposed contradiction between Haskell being a “pure” language, and Haskell programs obviously being able to perform impure operations (using IO). One attempted resolution to the supposed contradiction is viewpoints like this:
In a way it seems like Haskell cheats when it comes to I/O: one describes a pure program that evaluated to an impure program that can be run by the runtime
I find that point of view unnecessarily contorted. In fact, I think the knots that one has to twist oneself into to take that point of view substantially harms adoption of Haskell.
I prefer to be completely agnostic about whether Haskell is “pure” but instead to say it is “referentially transparent”, and to describe a value of type IO A simply as a possibly-effecting procedure returning an A.