Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IO vs referential transparency

Sorry newb question here, but how does Haskell know not to apply referential transparency to e.g. readLn or when putStrLn-ing a same string twice? Is it because IO is involved? IOW, will not the compiler apply referential transparency to functions returning IO?

like image 498
Ron Avatar asked Jul 15 '26 20:07

Ron


2 Answers

You need to distinguish between evaluation and execution.

If you evaluate 2 + 7, the result is 9. If you replace one expression that evaluates to 9 with another, different expression that also evaluates to 9, then the meaning of the program has not changed. This is what referential transparency guarantees. We could common up several shared expressions that reduce to 9, or duplicate a shared expression into multiple copies, and the meaning of the program does not change. (Performance might, but not the end result.)

If you evaluate readLn, it evaluates to an I/O command object. You can imagine it as being a data structure that describes what I/O operation(s) you want performed. But the object itself is just data. If you evaluate readLn twice, it returns the same I/O command object twice. You can merge several copies into one; you can split one copy into several. It doesn't change the meaning of the program.

Now if you want to execute the I/O action, that's a different thing. Clearly, I/O operations need to be executed in exactly the way the program specifies, without being randomly duplicated or rearranged. But that's OK, because it's not the Haskell expression evaluation engine that does that. You can pretend that the Haskell runtime runs main, which builds a giant I/O command object representing the entire program. The Haskell runtime then reads this data structure and executes the I/O operations it requests, in the order specified. (Not actually how it works, but a useful mental model.)

Ordinarily you don't need to bother thinking about this strict separation between evaluating readLn to get an I/O command object and then executing the resulting I/O command object to get a result. But strictly that's notionally what it does.

(You may also have heard that I/O "forms a monad". That's merely a fancy way of saying that there's a particular set of operators for changing I/O command objects together into bigger I/O command objects. It's not central to understanding the separation between evaluate and execute.)

like image 94
MathematicalOrchid Avatar answered Jul 17 '26 20:07

MathematicalOrchid


The IO type is defined as:

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

Notice is very similar to the State Monad where the state is the state of the real world, so imho you can think of IO as referentially transparent and pure, what's impure is the Haskell runtime (interpreter) that runs your IO actions (the algebra).

Take a look at the Haskell wiki, it explains IO in greater detail: IO Inside

like image 36
Anler Avatar answered Jul 17 '26 19:07

Anler