I\'ve had the IO monad described to me as a State monad where the state is \"the real world\". The proponents of this approach to IO argue that this makes IO operations pure
Well, this is what we have been taught at college -
Function is referentially transparent when it always returns the same value for specified input (or the same expression always evaluates to same value in the same context). Therefore, for example getChar
would not be referentially transparent if it had type signature just () -> Char
or Char
, because you can get different results if you call this function multiple times with the same argument.
But, if you introduce IO monad, then getChar
can have type IO Char
and this type has only one single value - IO Char
. So getChar
allways reutrns the same value, no matter on which key user really pressed.
But you are still able to "get" the underlying value from this IO Char
thing. Well, not really get, but pass to another function using bind operator (>>=
), so you can work with the Char that user entered further in your program.
In what sense is the monadic
IO
type pure?
In the sense that values of the IO
type are portions of Standard ML abstract imperative code which ideally can only be processed by the RTS of a Haskell implementation - in How to Declare an Imperative, Philip Wadler provides a hint as to how this is possible:
(* page 26 *)
type 'a io = unit -> 'a
infix >>=
val >>= : 'a io * ('a -> 'b io) -> 'b io
fun m >>= k = fn () => let
val x = m ()
val y = k x ()
in
y
end
val return : 'a -> 'a io
fun return x = fn () => x
(* page 27 *)
val execute : unit io -> unit
fun execute m = m ()
However, not everyone finds this situation acceptable:
[...] a state-less model of computation on top of a machinery whose most eminent characteristic is state [means] the gap between model and machinery is wide, and therefore costly to bridge. [...]
This has in due time also been recognized by the protagonists of functional languages. They have introduced state (and variables) in various tricky ways. The purely functional character has thereby been compromised and sacrificed. [...]Niklaus Wirth.
...anyone for Miranda(R)?
I've had the IO monad described to me as a State monad where the state is "the real world".
That would be the classic pass-the-planet model of I/O, which Clean uses directly:
import StdFile
import StdMisc
import StdString
Start :: *World -> *World
Start w = putString "Hello, world!\n" w
putString :: String *World -> *World
putString str world
# (out, world1) = stdio world
# out1 = fwrites str out
# (_, world2) = fclose out1 world1
= world2
The proponents of this approach to I/O argue that this makes I/O operations pure, as in referentially transparent. Why is that?
Because it's usually correct.
From the standard Haskell 2010 library module Data.List:
mapAccumL _ s [] = (s, [])
mapAccumL f s (x:xs) = (s'',y:ys)
where (s', y ) = f s x
(s'',ys) = mapAccumL f s' xs
If this idiom is so common that it has specific definitions to support it, then its use as a model of I/O (with a suitable state-type) is really no great surprise.
From the most recent Clean Language Report (I/O on the Unique World on page 24 of 148)
The world which is given to the initial expression is an abstract data structure, an abstract world of type
*World
which models the concrete physical world as seen from the program. The abstract world can in principle contain anything what a functional program needs to interact during execution with the concrete world. The world can be seen as a state and modifications of the world can be realized via state transition functions defined on the world or a part of the world. By requiring that these state transition functions work on a unique world the modifications of the abstract world can directly be realized in the real physical world, without loss of efficiency and without losing referential transparency.
But consider this small Clean program:
Start :: *World -> *World
Start w
# w1 = putString "Ha "
= loop w1
loop :: *World -> *World
loop w = loop w
If putString
is pure, then w1
denotes the string "Ha "
being sent to Clean's standard I/O mechanism, so if w1
is never returned then "Ha "
should not be seen at all...
Also, isn't it possible to describe pretty much any non-pure function like a function of the real world?
Yes; that's more-or-less how the FFI works in Haskell 2010.
From my perspective it appears that code inside the monadic
IO
type have plenty of observable side effects.
If you're using GHC, it isn't an appearance - from A History of Haskell (page 26 of 55) by Paul Hudak, John Hughes, Simon Peyton Jones, and Philip Wadler:
Of course, GHC does not actually pass the world around; instead, it passes a dummy “token,” to ensure proper sequencing of actions in the presence of lazy evaluation, and performs input and output as actual side effects!
But that's merely an implementation detail:
An
IO
computation is a function that (logically) takes the state of the world, and returns a modified world as well as the return value.
Logic doesn't apply to the real world.
Marvin Lee Minsky.
Even though its title is a bit weird (in that it doesn't precisely match the content) the following haskell-cafe thread contains a nice discussion about different IO models for Haskell.
http://www.mail-archive.com/haskell-cafe@haskell.org/msg79613.html