6
votes

Given a Haskell value (edit per Rein Heinrich's comment) f:

f :: IO Int
f = ... -- ignoring its implementation

Quoting "Type-Driven Development with Idris,"

The key property of a pure function is that the same inputs always produce the same result. This property is known as referential transparency

Is f, and, namely all IO ... functions in Haskell, pure? It seems to me that they are not since, lookInDatabase :: IO DBThing, won't always return the same value since:

  • at t=0, the DB might be down
  • at t=1, the DB might be up and return MyDbThing would result

In short, is f (and IO ... functions in general) pure? If yes, then please correct my incorrect understanding given my attempt to disprove the functional purity of f with my t=... examples.

5
I think understanding IO and understanding Monads are too often put in the same bucket — but they are distinct things; even independent, ultimately.Erik Kaplun
I think early Haskell IO was problematic though. Today, IO could also be implemented in terms of e.g. arrows I think, but then again arrows are also monads... I'd still argue in favor of teaching IO and monads separately though, starting with just IO, not mentioning it's a monad, and then eventually "discovering" it's a monad and that monads are more generally useful, and that IO is just a DSL, etc.Erik Kaplun
What side of IO is pure? The Haskell side or the Haskell RTS side? It's pure from one angle and totally impure from the other.Erik Kaplun
f :: IO Int is not a function, it is a value. lookInDatabase :: IO DBThing doesn't return a value: it is a value. It is always the same value, so it is pure.Rein Henrichs

5 Answers

10
votes

IO is really a separate language, conceptually. It's the language of the Haskell RTS (runtime system). It's implemented in Haskell as a (relatively simple) embedded DSL whose "scripts" have the type IO a.

So Haskell functions that return values of type IO a, are actually not the functions that are being executed at runtime — what gets executed is the IO a value itself. So these functions actually are pure but their return values represent non-pure computations.

From a language design point of view, IO is a really elegant hack to keep the non-pure ugliness completely isolated away while at the same integrating it tightly into its pure surroundings, without resorting to special casing. In other words, the design does not solve the problems caused by impure IO but it does a great job of at least not affecting the pure parts of your code.


The next step would be to look into FRP — with FRP you can make the layer that contains IO even thinner and move even more of non-pure logic into pure logic.

You might also want to read John Backus' writings on the topic of Function Programming, the limitations of the Von Neumann architecture etc. Conal Elliott is also a name to google if you're interested in the relationship between purity and IO.


P.S. also worth noting is that while IO is heavily reliant on monads to work around an aspect of lazy evaluation, and because monads are a very nice way of structuring embedded DSLs (of which IO is just a single example), monads are much more general than IO, so try not to think about IO and monads in the same context too much — they are two separate things and both could exist without the other.

4
votes

First of all, you're right in noticing that I/O actions are not pure. That's impossible. But, purity in all functions is one of Haskell's promising points, so what's happening?

Whether you like it or not, a function that applies into a (may also be incorrectly said "returns a") IO Something with some arguments will always return the same IO Something with the same arguments. The IO monad allows you to "hide" actions inside of the container the monad acts like. When you have a IO String, that function/object does not contain a String/[Char], but rather sort of a promise that you'll get that String somehow in the future. Thus, IO contains information of what to do when the impure I/O action needs to be performed.

After all, the only way for an IO action to be performed is by it having the name main, or be a dependency of main thereof. Because of the flexibility of monads, you can "concatenate" IO actions. A program like this... (note: this code is not a good idea)

main = do
    input <- getLine
    putStrLn input

Is syntatic sugar for...

main =
    getLine >>= (\input -> putStrLn input)

That would state that main is the I/O action resulting from printing to standard output a string read from standard input, followed by a newline character. Did you saw the magic? IO is just a wrapper representing what to do, in an impure context, to produce some given output, but not the result of that operation, because that would need the Haskell language to admit impure code.

Think of it as sort of a receipe. If you have a receipe (read: IO monad) for a cake (read: Something in IO Something), you know how to make the cake, but you can't make the cake (because you could screw that masterpiece). Instead, the master chief (read: the most basic parts of the Haskell runtime system, responsible for applying main) does the dirty work for you (read: doing impure/illegal stuff), and, the best of all, he won't commit any mistakes (read: breaking code purity)... unless the oven breaks of course (read: System.IO.Error), but he knows how to clean that up (read: code will always remain pure).

This is one of the reasons that IO is an opaque type. It's implementation is somewhat controversial (until you read GHC's source code), and is better of to be left as implementation-defined.

Just be happy, because you've been illuminated by purity. A lot of programmers don't even know of Haskell's existence!

I hope this has led some light on you!

2
votes

Haskell is pulling a trick here. IO both is and isn't pure, depending on how you look at it.

On the "IO is pure" side, you're fallen into the very common error of thinking of a function returning an IO DBThing as of it were returning a DBThing. When someone claims that a function with type Stuff -> IO DBThing is pure they are not saying that you can feed it the same Stuff and always get the same DBThing; as you correctly note that is impossible, and also not very useful! What they're saving is that given particular Stuff you'll always get back the same IO DBThing.

You actually can't get a DBThing out of an IO DBThing at all, so Haskell don't ever have to worry about the database containing different values (or being unavailable) at different times. All you can do with an IO DBThing is combine it with something else that needs a DBThing and produces some other kind of IO thing; the result of such a combination is an IO thing.

What Haskell is doing here is building up a correspondence between manipulation of pure Haskell values and changes that would happen out in the world outside the program. There are things you can do with some ordinary pure values that don't make any sense with impure operations like altering the state of a database. So using the correspondence between IO values and the outside world, Haskell simply doesn't provide you with any operations on IO values that would correspond to things that don't make sense in the real world.

There are several ways to explain how you're "purely" manipulating the real world. One is to say that IO is just like a state monad, only the state being threaded through is the entire world outside your program;= (so your Stuff -> IO DBThing function really has an extra hidden argument that receives the world, and actually returns a DBThing along with another world; it's always called with different worlds, and that's why it can return different DBThing values even when called with the same Stuff). Another explanation is that an IO DBThing value is itself an imperative program; your Haskell program is a totally pure function doing no IO, which returns an impure program that does IO, and the Haskell runtime system (impurely) executes the program it returns.

But really these are both simply metaphors. The point is that the IO value simply has a very limited interface which doesn't allow you to do anything that doesn't make sense as a real world action.

Note that the concept of monad hasn't actually come into this. Haskell's IO system really doesn't depend on monads; Monad is just a convenient interface which is sufficiently limited that if you're only using the generic monad interface you also can't break the IO limitations (even if you don't know your monad is actually IO). Since the Monad interface is also interesting enough to write a lot of useful programs, the fact that IO forms a monad allows a lot of code that's useful on other types to be generically reused on IO.

Does this mean you actually get to write pure IO code? Not really. This is the "of course IO isn't pure" side of the coin. When you're using the fancy "combining IO functions together" you still have to think about your program executing steps one after the other (or in parallel), affecting and being affected by outside conditions and systems; in short exactly the same kind of reasoning you have to use to write IO code in an imperative language (only with a nicer type system than most of them). Making IO pure doesn't really help you banish impurity from the way you have to think about your code.

So what's the point? Well for one, it gets us a compiler-enforced demarcation of code that can do IO and code that can't. If there's no IO tag on the type then impure IO isn't involved. That would be useful in any language just on its own. And the compiler knows this too; Haskell compilers can apply optimizations to non-IO code that would be invalid in most other languages because it's often impossible to know that a given section of code doesn't have side effects (unless you can see the full implementation of everything the code calls, transitively).

Also, because IO is pure, code analysis tools (including your brain) don't have to treat IO-code specially. If you can pick out a code transformation that would be valid on pure code with the same structure as the IO code, you can do it on the IO code. Compilers make use of this. Many transformations are ruled out by the structure that IO code must use (in order to stay within the bounds of things that have a sensible correspondence to things in the outside world) but they would also be ruled out by any pure code that used the same structure; the careful construction of the IO interface makes "execution order dependency" look like ordinary "data dependency", so you can just use the rules of data dependency to determine the rules of using IO.

2
votes

Short answer: Yes, that f is referential transparent.

Whenever you look at it, it equals the same value.
But that doesn't mean it will always bind the same value.

0
votes

In short, is f (and IO ... functions in general) pure?

So what you're really asking is:

Are IO definitions in Haskell pure?

You're really not going to like it.

Deep Thought.

It depends on what you mean by "pure".

From section 6.1.7 (page 75) of the Haskell 2010 report:

The IO type serves as a tag for operations (actions) that interact with the outside world. The IO type is abstract: no constructors are visible to the user. IO is an instance of the Monad and Functor classes.

the crucial point being:

The IO type is abstract

There is no "standard definition" for IO, so there's no common way to determine such a property for that type, let alone any of its expressions. We can't even provide a simple proof that IO is monadic (i.e. it satisfies the monad laws) as return and (>>=) cannot be defined in standard Haskell 2010.

To get some idea on how this affects the determining of various IO-related properties, see:

So when you next hear or read about Haskell being "referentially transparent" or "purely functional", you now know that (at least for I/O) they're just conjectures - no actual standard definition means there's no way to prove or disprove them.

(If you're now wondering how Haskell got into this state, I provide some more details here.)