So I started to wrap my head around Monads (used in Haskell). I\'m curious what other ways IO or state can be handled in a pure functional language (both in theory or realit
It can't be (not if by "state" you mean "I/O or mutable variable behavior like in a procedural language"). In the first place, you have to understand where the use of monads for mutable variables or I/O comes from. Despite popular belief, monadic I/O doesn't come from languages like Haskell, but from languages like ML. Eugenio Moggi developed the original monads while studying the use of category theory for the denotational semantics of impure functional languages like ML. To see why, consider that a monad (in Haskell) can be categorized by three properties:
a) and expressions (in Haskell, of type IO a).x to return x).f =<< a).These properties are obviously true of (at least) the denotational semantics of any impure functional language:
print "Hello, world!\n", can have side-effects, but its value, such as (), cannot. So we need to make a distinction between the two cases in the denotational semantics.3, can be used anywhere an expression is required. So our denotational semantics needs a function to turn a value into an expression.So any denotational semantics for an impure functional (or procedural) language is going to have the structure of a monad under the hood, even if that structure isn't explicitly used in describing how I/O works in the language.
What about purely functional languages?
There are four major ways of doing I/O in purely functional languages, that I know about (in practice) (again, restricting ourselves to procedural-style I/O; FRP is genuinely a different paradigm):
Monadic I/O is obvious. Continuation-based I/O looks like this:
main k = print "What is your name? " $
getLine $ \ myName ->
print ("Hello, " ++ myName ++ "\n") $
k ()
Each I/O action takes a 'continuation', performs its action, and then tail calls (under the hood) the continuation. So in the above program:
print "What is your name? " runs, thengetLine runs, thenprint ("Hello, " ++ myName ++ "\n") runs, thenk runs (which returns control to the OS).The continuation monad is an obvious syntactic improvement to the above. More significantly, semantically, I can only see two ways to make the I/O actually work in the above:
() and do the I/O as a side-effect of calling the individual operations (e.g., print, getLine, etc.). But if evaluation of an expression in your language (which the right-hand side of the main definition above is) is side-effectful, I wouldn't consider that purely functional.What about uniqueness/linear types? These use special 'token' values to represent the state of the world after each action, and enforce sequencing. The code looks like this:
main w0 = let
w1 = print "What is your name? " w0
(w2, myName) = getLine w1
w3 = print $ "Hello, " ++ myName ++ "!\n"
in w3
The difference between linear types and uniqueness types is that in linear types, the result has to be w3 (it has to be of type World), whereas in uniqueness types, the result could be something like w3 `seq` () instead. w3 just has to be evaluated for the I/O to happen.
Again, the state monad is an obvious syntactic improvement to the above. More significantly, semantically, you again have two choices:
print and getLine, strict in the World argument (so the previous operation runs first, and side-effectful (so the I/O happens as a side-effect of evaluating them). Again, if you have side-effects of evaluation, in my opinion that's not really purely functional.World type actually represent the I/O that needs to be performed. This has the same problem as GHC's IO implementation with tail-recursive programs. Suppose we change the result of main to main w3. Now main tail-calls itself. Any function that tail-calls itself, in a purely functional language, has no value (is just an infinite loop); this is a basic fact about how the denotational semantics of recursion works in a pure language. Again, I wouldn't consider any language that broke that rule (especially for a 'special' data type like World) to be purely functional.So, really, uniqueness or linear types a) produce programs that are clearer / cleaner if you wrap them in a state monad and b) aren't actually a way to do I/O in a purely functional language after all.
What about dialogs? This is the only way to do I/O (or, technically, mutable variables, although that's much harder) that truly is both purely functional and independent of monads. That looks something like this:
main resps = [
PrintReq "What is your name? ",
GetLineReq,
PrintReq $ "Hello, " ++ myName ++ "!\n"
] where
LineResp myName = resps !! 1
However, you'll notice a few disadvantages of this approach:
myName before issuing the corresponding getLine request, the compiler would accept your program but it would deadlock at runtime.An easy way to solve all of these problems is to wrap dialogs in continuations, like this:
type Cont = [Response] -> [Request]
print :: String -> Cont -> Cont
print msg k resps = PrintReq msg : case resps of
PrintResp () : resps1 -> k resps1
getLine :: (String -> Cont) -> Cont
getLine k resps = GetLineReq : case resps of
GetLineResp msg : resps1 -> k msg resps1
The code now looks identical to the code for the continuation-passing approac to I/O given earlier. In fact, dialogs are an excellent result type for your continuations in a continuation-based I/O system, or even in a continuation monad-based monadic I/O system. However, by converting back to continuations, the same argument applies, so we see that, even if the run-time system uses dialogs internally, programs should still be written to do I/O in a monadic style.