[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Monadic I/O

Here is the current draft of our proposal for I/O in Haskell 1.3.
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: Quoted-Printable
Comments welcomed!


                This I/O proposal is also a Haskell 1.2 module

> module ProposedIO (

>       PrimIO(..),                 -- This will be abstract
>       IO(..),
>       IoResult(..),

>       returnPrimIO,
>       thenPrimIO,
>       processRequestPrimIO,
>       primIOToDialogue,

>       returnIO,
>       thenIO,
>       handleIO,
>       failIO,
>       raiseIO,
>       (>>=3D),
>       (>>),
>       (?),

>       readFileIO,
>       writeFileIO,
>       appendFileIO,
>       readBinFileIO,
>       writeBinFileIO,
>       appendBinFileIO,
>       deleteFileIO,
>       statusFileIO,
>       readChanIO,
>       appendChanIO,
>       readBinChanIO,
>       appendBinChanIO,
>       statusChanIO,
>       echoIO,
>       getArgsIO,
>       getProgNameIO,
>       getEnvIO,
>       setEnvIO,

>       printIO,
>       printsIO,
>       interactIO,
>       runIO,
>       printStringIO,
>       listIO,
>       mapIO,

>       doIO

>       ) where

        A proposal for Input/Output in Haskell 1.3, version of October 11

There are two purposes behind this proposal:

(1) to define a monadic programming model for the I/O operations supported in
    Haskell 1.2, which is to say a model in which top-level programs of
    type, IO a, denote computations that may engage in I/O operations
    before returning a value of type, a; and

(2) to consider what new I/O operations should be added to Haskell.

We need a new standard for the sake of portability.  A monadic programming
model has now been implemented in several compilers, has found to be useful
and in some respects simpler than the existing stream-based I/O system, so it
would be a pity if monadic programs were not portable.

Two important principles guide this proposal.

(1) Backwards compatibility.  The mission of Haskell 1.3 is to fix up
    non-controversial aspects of the language, while minimising impact on
    existing programs.  In particular, although some people would like to drop
    stream I/O completely from Haskell, this goes against our mission, in that
    stream I/O is uncontroversial in itself, and there are many programs
    dependent directly on stream-based I/O.  So we propose to add monadic I/O
    alongside and compatible with stream-based I/O.

(2) Conservative standardisation.  We've attempted to include in the monadic
    programming model only features that have been tried and tested in

This proposal is the outcome of discussions between Andy Gill, Andy Gordon,
Kevin Hammond and Ian Poole.  The Haskell code is substantially based on a
module circulated by Andy Gill earlier this summer.

                  A Monadic Programming Model for 1.2 I/O

The model is based on two monads, one built on the other.

The primitive monad, PrimIO a, similar to the monad implemented in ghc (where
it is named IO), consists of computations that may perform I/O, and, if they
terminate, return answers of type, a.

The derived monad, IO a, consists of computations that may perform I/O, and,
if they terminate, either return an answer of type a, or an exception of type
IOError (from Haskell 1.2).  This monad supports handling of user or system
generated exceptions in much the same way as `handle' in SML, except of
course that only programs of type, IO a, can raise exceptions.

We need the derived monad to support exception or error handling, as this has
been found to be invaluable in practice at Kent (KAOS project in Miranda) and
Edinburgh (MRC SADLI project in Haskell).  We decided to have two separate
monads to be conservative---nobody, as far as we know, has any experience yet
of directly implementing a monad with exception handling---and to allow
different monads to be built on top of PrimIO.  The intention is that the IO
monad will be sufficient for most programmers' needs, but that if necessary
they can code up different monads on top of PrimIO.  There are two different
ways of combining exceptions with state in a monad, and our proposal allows
either to be implemented on top of PrimIO.

                   T h e   p r i m i t i v e   m o n a d

We propose that a unary type constructor PrimIO and operations be added to the
PreludeBuiltin module, and implemented directly.

> returnPrimIO         :: x -> PrimIO x
> thenPrimIO           :: PrimIO a -> (a -> PrimIO b) -> PrimIO b
> processRequestPrimIO :: Request -> PrimIO Response

A closed expression of type, PrimIO a, for some type, a, is intended to denote
a computation that may engage in I/O operations, and if it terminates, will
return an answer of type, a.

  returnPrimIO x
    is intended to denote the computation that immediately returns the
    answer x

  p `thenPrimIO` q
    is intended to be the computation that performs p, and then, if 
    returns an answer x, performs q x.

  processRequestPrimIO req
    is intended to be the computation that sends req :: Request to the
    operating system, as in Haskell 1.2, gets an res :: Response back,
    and returns res as its answer.

There are several other names circulating instead of "return" and "then",
such as "val" and "let", or "unit" and "bind", etc.

Relative to this intended meaning we can see that the structure
(PrimIO, returnPrimIO, thenPrimIO) is a monad in the sense that the following
equations hold.

  (returnPrimIO x `thenPrimIO` p)
    is the same computation as
      (p x)

  (p `thenPrimIO` returnPrimIO)
    is the same computation as

  (p `thenPrimIO` \x -> (q x `thenPrimIO` r))
    is the same computation as
      ((p `thenPrimIO` q) `thenPrimIO` r)

                     T h e   d e r i v e d   m o n a d

Although the PrimIO monad is sufficient to express all our I/O requirements,
experience suggests a range of useful derived forms.  Furthermore, experience
suggests that an exception handling mechanism is useful.  Hence we propose the
following exception-handling monad built on top of PrimIO.

> data IoResult a =3D IoSucc a
>                 | IoFail IOError
> type IO a =3D PrimIO (IoResult a)

An expression of type IO a denotes a computation that may perform I/O, and if
it terminates, either returns an answer of type a (via IoSucc) or an exception
of type IOError (via IoFail).

We can define the two familiar monadic operations

> returnIO :: a -> IO a
> thenIO   :: IO a -> (a -> IO b) -> IO b

and a complementary pair for raising and catching exceptions.

> failIO   :: IOError -> IO a
> handleIO :: IO a -> (IOError -> IO a) -> IO a

By convention, if a user wants to fail with message str :: String, then
exception OtherError str is raised.

> raiseIO  :: String -> IO a
> raiseIO =3D failIO . OtherError

Basic I/O operations can be derived from processRequestPrimIO.  Errors
are returned as exceptions.

> readFileIO      :: String ->           IO String
> writeFileIO     :: String -> String -> IO ()
> appendFileIO    :: String -> String -> IO ()
> readBinFileIO   :: String ->           IO Bin
> writeBinFileIO  :: String -> Bin    -> IO ()
> appendBinFileIO :: String -> Bin    -> IO ()
> deleteFileIO    :: String ->           IO ()
> statusFileIO    :: String ->           IO String
> readChanIO      :: String ->           IO String
> appendChanIO    :: String -> String -> IO ()
> readBinChanIO   :: String ->           IO Bin
> appendBinChanIO :: String -> Bin    -> IO ()
> statusChanIO    :: String ->           IO String
> echoIO          :: Bool   ->           IO ()
> getArgsIO       ::                     IO [String]
> getProgNameIO   ::                     IO String
> getEnvIO        :: String ->           IO String
> setEnvIO        :: String -> String -> IO ()

We have some derived I/O operations for programmer convenience.

> printIO       :: Text a =3D> a -> IO ()
> printsIO      :: Text a =3D> a -> String -> IO ()
> interactIO    :: (String -> String) -> IO ()
> runIO         :: (String -> String) -> IO ()
> printStringIO :: String -> IO ()
> listIO        :: [IO a] -> IO [a]
> mapIO         :: (a -> IO b) -> [a] -> IO [b]

Finally, an operation to execute IO programs.

> doIO :: IO () -> PrimIO ()

Infix operations for control flow; >>=3D, a synonym for thenIO, and >> and ?,
sequential composition and biased choice respectively.

> (>>=3D)     :: IO a -> (a -> IO b) -> IO b
> (>>)      :: IO a -> IO b -> IO b
> (?)       :: IO a -> IO a -> IO a

> m >>=3D k =3D m `thenIO` k
> m >> k  =3D m >>=3D \ _ -> k
> m ? k   =3D m `handleIO` \ _ -> k

             W h a t   c o n s t i t u t e s   a   p r o g r a m ?

For the sake of compatibility with 1.2 there are two mutually exclusive

If Main.mainIO :: PrimIO () is present then it's the main program, executed
according to the informal semantics we've specified above.

If Main.main :: Dialogue is present, then it's the main program, to be
executed according to the 1.2 semantics published widely in SIGPLAN Notices.

If both are present, then the program is in error.

Support for stream-based I/O only exists in Haskell 1.3 to maintain
compatibility with earlier versions.  We recommend that new programs use the
monadic interface.  If there is a Haskell 2 its I/O mechanism is unlikely
to include stream-based I/O as standard.

We will provide a "compatibility module" to define the meaning of stream-based
Main.main in terms of the monadic semantics, in order to keep the definition
of 1.3 I/O self-contained.

                      A n   e x a m p l e   p r o g r a m

module Main where

mainPrimIO :: PrimIO ()
mainPrimIO =3D doIO mainIO

mainIO :: IO ()
mainIO =3D printStringIO "Hello World\n"

-- the following lines permit simulation in 1.2, by importing this proposal
import ProposedIO
main =3D primIOToDialogue mainPrimIO

          H o w   i s   t h e   p r e l u d e   o r g a n i s e d ?

PrimIO plus associated operations go in PreludeBuiltin, and everything else
goes in a new module PreludeMonadicIO.

           Extending the range of I/O operations beyond Haskell 1.2

At least the following should be considered for Haskell 1.3, but none of these
as yet constitute concrete proposals.

(1) Character-by-character I/O

There should be a convenient way for monadic programs to fetch finite numbers
of characters from "stdin" (or any other readable channel), and to test whether
there are any currently available, equivalent to a non-blocking read.  Such a
mechanism is incompatible with the readChanIO, which delivers an infinite list
containing all the characters that will ever be read.  Hiatons notwithstanding
we've never seen a clean semantics for non-blocking input combined with a
stream of all input characters.  Hence one suggestion is to have two modes for
character input on a channel: all-in-one-go as an infinite stream, or
one-character-at-a-time, blocking or non-blocking.

To achieve the latter we could add two new Requests that would allow the
following two monadic operations to be expressed.

  getCharIO    :: String -> IO Char
  getCharNowIO :: String -> IO Char

The String names the input channel.  The first operation is a blocking
single-character read.  The second is a non-blocking read.  It would fail if
no character is available.  Use of these ops versus use of readChanIO would be
mutually exclusive in any run.  (This is rather a tentative proposal; we
haven't decided on the best way to do it.)

(2) Starting a new OS process

It would be good to have some way for a Haskell program to run an OS process,
but we don't have a concrete proposal.

(3) Asynchronous interrupts

Lennart has a suggestion for dealing with asynchronous interrupts by having
the next processRequestIO operation fail and hand back a special error
message.  We don't yet have a concrete proposal.

                        This ain't a complete proposal!

At least the following things need to be settled.

(1) We ought to define the error messages that can be returned in connection
    with each Request.  Re the proposal for non-blocking input, we need to
    define the error returned by getCharNowIO if there is no input, versus
    the error returned if readChanIO has already been used.

(2) We've left the associativity and priority of the infix operators undefined
    for now, but they need to be fixed.

(3) It would probably be good to include more general purpose monadic ops
    that have been useful in practice.

(4) We need to write the compatibility module that expressed stream-based I/O
    in terms of monadic I/O.  The translation will be the well-known
    space-leaking one (see, e.g., Peyton Jones and Wadler, POPL93).  The point
    is to keep the semantics of 1.3 I/O self-contained.  Existing efficient
    implementations of stream-based I/O should be retained.

(5) We are not completely sure it is wise to base the proposal on two
    separate monads.  It is good to have a primitive PrimIO and a derived IO
    for the sake of semantic simplicity (only PrimIO need be defined outwith
    Haskell) and modularity (IO adds exception handling to PrimIO.  We might
    want a monad with both exceptions and state.  There are two distinct ways
    to do so, and making PrimIO accessible admits both.).

    Are there any good implementation reasons for taking IO as primitive,
    instead of PrimIO?

(6) There is no formal semantics.  This is not damning, as say SML I/O still
    awaits a formal semantics.  It should be possible to sketch a semantics
    for I/O based on a semantics for a fragment of Haskell.

(7) Finally, we're not sure of the status of the "optional" requests in 1.2
    Haskell.  Have they been widely used?  Should they remain in 1.3?

           Definitions of all the operations, and simulation in 1.2

Monadic operations and exception-handling.

> returnIO =3D returnPrimIO . IoSucc
> thenIO m k =3D
>       m               `thenPrimIO` \ r ->
>       case r of
>         IoSucc a -> k a
>         IoFail e -> returnPrimIO (IoFail e)

> failIO =3D returnPrimIO . IoFail
> m `handleIO` k =3D
>       m               `thenPrimIO` \ r ->
>       case r of
>         IoFail e -> k e
>         other    -> returnPrimIO other

> processRequestIOUnit :: Request -> IO ()
> processRequestIOUnit req =3D
>         processRequestPrimIO req                    `thenPrimIO` \ resp ->
>         case resp of
>           Success       -> returnIO ()
>           Failure ioerr -> returnPrimIO (IoFail ioerr)
>           _             -> error "funny Response, expected a Success"
> processRequestIOBin :: Request -> IO Bin
> processRequestIOBin req =3D
>         processRequestPrimIO req                   `thenPrimIO` \ resp ->
>         case resp of
>           Bn bin        -> returnIO bin
>           Failure ioerr -> returnPrimIO (IoFail ioerr)
>           _             -> error "funny Response, expected a Bin"
> processRequestIOString :: Request -> IO String
> processRequestIOString req =3D
>         processRequestPrimIO req                   `thenPrimIO` \ resp ->
>         case resp of
>           Str str       -> returnIO str
>           Failure ioerr -> returnPrimIO (IoFail ioerr)
>           _             -> error "funny Response, expected a String"
> processRequestIOStringList :: Request -> IO [String]
> processRequestIOStringList req =3D
>         processRequestPrimIO req                   `thenPrimIO` \ resp ->
>         case resp of
>           StrList strl  -> returnIO strl
>           Failure ioerr -> returnPrimIO (IoFail ioerr)
>           _             -> error "funny Response, expected a [String]"

Basic I/O operations.

> readFileIO      file     =3D processRequestIOString     ( ReadFile file )
> writeFileIO     file str =3D processRequestIOUnit       ( WriteFile file str )
> appendFileIO    file str =3D processRequestIOUnit       ( AppendFile file str )
> readBinFileIO   file     =3D processRequestIOBin     ( ReadBinFile file )
> writeBinFileIO  file bin =3D processRequestIOUnit    ( WriteBinFile file bin )
> appendBinFileIO file bin =3D processRequestIOUnit    ( AppendBinFile file bin )
> deleteFileIO    file     =3D processRequestIOUnit       ( DeleteFile file )
> statusFileIO    file     =3D processRequestIOString     ( StatusFile file )
> readChanIO      chan     =3D processRequestIOString     ( ReadChan chan )
> appendChanIO    chan str =3D processRequestIOUnit       ( AppendChan chan str )
> readBinChanIO   chan     =3D processRequestIOBin     ( ReadBinChan chan )
> appendBinChanIO chan bin =3D processRequestIOUnit    ( AppendBinChan chan bin )
> statusChanIO    chan     =3D processRequestIOString     ( StatusChan chan )
> echoIO          bool     =3D processRequestIOUnit       ( Echo bool )
> getArgsIO                =3D processRequestIOStringList ( GetArgs )
> getProgNameIO            =3D processRequestIOString     ( GetProgName )
> getEnvIO        var      =3D processRequestIOString     ( GetEnv var )
> setEnvIO        var obj  =3D processRequestIOUnit       ( SetEnv var obj )

Derived I/O operations.

> printIO x    =3D printStringIO (show x)
> printsIO x s =3D printStringIO (shows x s)
> interactIO f =3D
>       readChanIO stdin                >>=3D \ x ->
>       appendChanIO stdout (f x)
> runIO f =3D
>       echoIO False                    >>
>       interactIO f
> printStringIO =3D appendChanIO stdout
> listIO =3D foldr (\ a b ->
>               a                       >>=3D \ x ->
>               b                       >>=3D \ xs -> 
>               returnIO (x:xs))
>               (returnIO [])
> mapIO f =3D listIO . map f


> doIO io =3D
>   (io `handleIO` errorMessage) `thenPrimIO`
>   \ _ -> returnPrimIO ()

> errorMessage :: IOError -> IO ()
> errorMessage err =3D appendChanIO stderr str
>  where
>     str =3D case err of
>       ReadError s   -> "Read Error: "   ++ s
>       WriteError s  -> "Write Error: "  ++ s
>       SearchError s -> "Search Error: " ++ s
>       FormatError s -> "Format Error: " ++ s
>       OtherError s  -> "Error: "        ++ s

Simulation using stream-based I/O.

> type PrimIO a =3D (a -> Dialogue) -> Dialogue
> returnPrimIO x cont =3D cont x
> thenPrimIO m k cont =3D m (\ a -> k a cont)
> processRequestPrimIO req cont ~(resp:resps) =3D req : cont resp resps

> primIOToDialogue :: PrimIO () -> Dialogue
> primIOToDialogue m =3D m (\ _ _ -> [])