monads are described as the haskell solution to deal with IO. I was wondering if there were other ways to deal with IO in pure functional language.
views:
383answers:
5If by "pure" you mean "referentially transparent", that is, that an applied function is freely interchangeable with its evaluated result (and therefore that calling a function with the same arguments has the same result every time), any concept of stateful IO is pretty much excluded by definition.
There are two rough strategies that I'm aware of:
Let a function do IO, but make sure that it can never be called twice with the exact same arguments; this side-steps the issue by letting the functions be trivially "referentially transparent".
Treat the entire program as a single pure function taking "all input received" as an argument and returning "all output produced", with both represented by some form of lazy stream to allow interactivity.
There are a variety of ways to implement both approaches, as well as some degree of overlap--e.g., in the second case, functions operating on the I/O streams are unlikely to be called twice with the same part of the stream. Which way of looking at it makes more sense depends on what kind of support the language gives you.
In Haskell, IO
is a type of monad that automatically threads sequential state through the code (similar to the functionally pure State
monad), such that, conceptually, each call to an otherwise impure function gets a different value of the implicit "state of the outside world".
The other popular approach I'm aware of uses something like linear types to a similar end; insuring that impure functions never get the same arguments twice by having values that can't be copied or duplicated, so that old values of the "state of the outside world" can't be kept around and reused.
Imperative Functional Programming by Peyton Jones and Wadler is a must read if you are interested in functional IO. The other approaches that they discuss are:
- Dialogues which are lazy streams of responses and requests
type Dialogue = [Response] -> [Request]
main :: Dialogue
Continuations - each IO operation takes a continuation as argument
Linear types - the type system restricts you in way that you cannot copy or destroy the outside state, which means that you can't call a function twice with the same state.
What alternatives are there to monads for I/O in a pure functional language?
I'm aware of two alternatives in the literature:
One is a so-called linear type system. The idea is that a value of linear type must be used exactly one time: you can't ignore it, and you can't use it twice. With this idea in mind, you give the state of the world an abstract type (e.g.,
World
), and you make it linear. If I mark linear types with a star, then here are the types of some I/O operations:getChar :: World* -> (Char, World*) putChar :: Char -> World* -> World*
and so on. The compiler arranges to make sure you never copy the world, and then it can arrange to compile code that updates the world in place, which is safe because there is only one copy.
The uniqueness typing in the language Clean is based on linearity.
This system has a couple of advantages; in particular, it doesn't enforce the total ordering on events that monads do. It also tends to avoid the "
IO
sin bin" you see in Haskell where all effectful computations are tossed into theIO
monad and they all get totally ordered whether you want total order or not.The other system I'm aware of predates monads and Clean and is based on the idea that an interactive program is a function from a (possibly infinite) sequence of requests to a (possibly infinite) sequence of responses. This system, which was called "dialogs", was pure hell to program. Nobody misses it, and it had nothing in particular to recommend it. Its faults are enumerated nicely in the paper that introduced monadic I/O (Imperative Functional Programming) by Wadler and Peyton Jones. This paper also mentions an I/O system based on continuations which was introduced by the Yale Haskell group but which was short-lived.