# Difference between revisions of "IO, partible-style"

IO is the [monadic type] you cannot avoid.

...but you kept looking anyway, and here you are!

[...] the input/output story for purely-functional languages was weak and unconvincing, let alone error recovery, concurrency, etc. Over the last few years, a surprising solution has emerged: the monad. I say "surprising" because anything with as exotic a name as "monad" - derived from category theory, one of the most abstract branches of mathematics - is unlikely to be very useful to red-blooded programmers. But one of the joys of functional programming is the way in which apparently-exotic theory can have a direct and practical application, and the monadic story is a good example.

[...] And I still don't believe in monads. :-)

You are not alone.

## Contents

### IO, using OI

Our definition of IO is a type synonym:

type IO a = OI -> a

with OI being an abstract partible type:

data OI a
primitive primPartOI :: OI -> (OI, OI)

instance Partible OI where
part = primPartOI

Like primPartOI, most other primitives for the OI type also accept an OI-value as their last (or only) argument e.g:

primitive primGetChar :: OI -> Char
primitive primPutChar :: Char -> OI -> ()

Borrowing the running example from Philip Wadler's How to Declare an Imperative:

echo   :: OI -> ()
echo u =  let !(u1:u2:u3:_) = parts u
!c            = primGetChar u1 in
if c == '\n' then
()
else
let !_ = primPutChar c u2
in  echo u3

Wadler also provides an SML version:

val echoML    : unit -> unit
fun echoML () = let val c = getcML () in
if c = #"\n" then
()
else
(putcML c; echoML ())
end

in which we replace SML's sequencing operator ;:

val echoML    : unit -> unit
fun echoML () = let val c = getcML () in
if c = #"\n" then
()
else
let val _ = putcML c in
echoML ()
end
end

If we compare it to our Haskell version:

 echo :: OI -> () echo u = let !(u1:u2:u3:_) = parts u !c = primGetChar u1 in if c == '\n' then () else let !_ = primPutChar c u2 in echo u3 -- val echoML  : unit -> unit fun echoML () = let val c = getcML () in if c = #"\n" then () else let val _ = putcML c in echoML () end end

...we can now see just how similar the two versions of echo really are: apart from the obvious changes of syntax, the Haskell version replaces all use of unit-values with OI-values, and adds an extra call to parts to provide them.

So there you have it: for the price of some extra calls and bindings, we can have SML-style I/O in Haskell. Furthermore, as the prevailing definition for SML has been available since 1997, there should be plenty of I/O tutorials to choose from...

At this point, you may be tempted to try something like:

type IO a = () -> a

primitive might_get_Char :: () -> Char
primitive might_put_Char :: Char -> ()
⋮

While this might work in some situations, it's unreliable in general. Why?

But - if after all that - you're still not convinced, then perhaps you'll be happier programming in SML...

...you're still here: nice!  Now for a small example - here's interact, using those OI-based definitions:

interact     :: (String -> String) -> OI -> ()
interact f v =  let !(u1, u2) = part v
gets      = map primGetChar \$ parts u1
puts s    = foldr (\(!_) -> id) () \$ zipWith primPutChar s \$ parts u2
in
puts \$ f \$ gets

### Some annoyances

• Polymorphic references - It's been known for a very long time in the SML community that naive declarations for operations using mutable references breaks type safety:
 primitive newPolyRef :: a -> OI -> PolyRef a primitive readPolyRef :: PolyRef a -> OI -> a primitive writePolyRef :: PolyRef a -> a -> OI -> () kah_BOOM u = let …  !vehicle = newPolyRef undefined u1  !_ = writePolyRef ("0" :: [Char]) u2  !crash = readPolyRef vehicle u3 burn = 1 :: Int in crash + burn
SML's solution is to make all mutable references monomorphic through the use of dedicated syntax:
 let val r = ref (…) ⋮
One alternative for Haskell would be to extend type signatures to support monomorphic type-variables:
primitive newIORef   :: monomo a . a -> OI -> IORef a
primitive readIORef  :: monomo a . IORef a -> OI -> a
primitive writeIORef :: monomo a . IORef a -> a -> OI -> ()

{- would be rejected by the extended type system:
kah_BOOM u = let !(u1:u2:u3:_) = parts u
!vehicle      = newIORef undefined u1          -- vehicle :: monomo a . IORef a
!_            = writeIORef ("0" :: [Char]) u2  -- vehicle :: IORef [Char]
!crash        = readIORef vehicle u3           -- vehicle :: IORef [Char] ≠ IORef Int
burn          = 1 :: Int
in
crash + burn
-}
In standard Haskell, one of the few places this already occurs (albeit implicitly) is the parameters of a function:
{- will be rejected by the standard Haskell type system

ker_plunk f = (f True, f 'b')

-}

### One solution

• Extra parameters and arguments - What is needed is a succinct interface to "hide the plumbing" used to pass around OI-values:
return x = \u -> case part u of !_ -> x
m >>= k  = \u -> case part u of
(u1, u2) -> case m u1 of
!x -> k x u2
• Polymorphic references - we now make IO into an abstract data type:
module Abstract.IO
(
getChar, putChar,

)
where

return x = \u -> case part u of !_ -> x
m >>= k  = \u -> case part u of
(u1, u2) -> case m u1 of
!x -> k x u2

getChar    :: IO Char
getChar    =  primGetChar

putChar    :: Char -> IO ()
putChar    =  primPutChar

newIORef   :: a -> IO (IORef a)
newIORef   =  primNewIORef

readIORef  :: IORef a -> IO a

writeIORef :: IORef a -> a -> IO ()
writeIORef =  primWriteIORef

-- these are now local, private entities --
type IO a = OI -> a

data OI a
primitive primPartOI  :: OI -> (OI, OI)

primitive primGetChar :: OI -> Char
primitive primPutChar :: Char -> OI -> ()

data IORef
primitive primNewIORef    :: a -> OI -> IORef a
primitive primReadIORef   :: IORef a -> OI -> a
primitive primWriteIORef  :: IORef a -> a -> OI -> ()

With the IO type now made abstract, the only way to use IO-values is by using:
• the visible IO operations: getChar, putChar, etc.
The key here is the type of (>>=), for IO-values:
(>>=) :: IO a -> (a -> IO b) -> IO b
in particular, the type of the second argument:
 (a -> IO b)
...it's a function, so the value it receives will be rendered monomorphic in the function's result (of type IO b).
As (>>=) is now the only IO operation which can retrieve a result from an IO-value, mutable references (IORef …) simply cannot be used polymorphically.

### GHC's solution

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

...you may have noticed that we've already made liberal use of one Haskell extension - bang-patterns - and it would be useful to stay as close as possible to standard Haskell, so we'll simplify matters:

newtype IO a = IO (IOState -> (IOState, a))  -- unboxed-tuple replaced by standard one

type IOState = State# RealWorld

Now to make the changes:

• to the type - IOState uses an OI-value:
newtype IOState = IOS OI
• to the I/O-specific operations - each one will use the OI-value in the initial state to provide two new OI-values: one to make up the final state; the other being used by the OI-primitive:
getChar   :: IO Char
getChar   =  IO \$ \(IOS u) -> let !(u1, u2) = parts u
!c        = primGetChar u1
in  (IOS u2, c)

putChar   :: Char -> IO ()
putChar c =  IO \$ \(IOS u) -> let !(u1, u2) = parts u
!t        = primPutChar c u1
in  (IOS u2, t)

-- etc.
• to the overloaded operations - you've probably seen it all before:
return x   = IO \$ \!s -> (s, x)
IO m >>= k = IO \$ \!s -> let !(s', x) = m s
!IO w    = k x
in  w s'
(...if you haven't: it's ye ol' pass-the-planet state-passing technique.)

One aspect which doesn't change is IO and its operations being abstract. In fact, the need is even more pressing: in addition to preventing the misuse of certain OI-operations, being an abstract data type prevents IOState-values from being erroneously reused.

### Conclusions

• Why is Haskell I/O monadic - to avoid having to use extra arguments and parameters everywhere.
• Why is Haskell I/O abstract - to ensure I/O works as intended, by preventing the misuse of internal data.
• Why is Haskell I/O unusual - because of Haskell's nonstrict evaluation and thus its focus on referential transparency, contrary to most other programming languages.