Raw I/O library proposal, second (more pragmatic) draft

Ashley Yakeley ashley@semantic.org
Sun, 3 Aug 2003 22:51:39 -0700

In article 
 "Simon Marlow" <simonmar@microsoft.com> wrote:

> I wanted to float a generalisation of this scheme, though.  I'm
> wondering whether it might be a good idea to make InputStream and
> OutputStream into type classes, the advantage being that this makes
> streams more extensible - one example is that memory-mapped files fit
> neatly into this framework.  I already have 6 examples of things that
> can have streams layered on top (or *are* streams), and there are almost
> certainly more.
> Here's some signatures for you to peruse:
> class Stream s where
>       closeStream	   :: s -> IO ()
>       streamSetBuffering :: s -> BufferMode -> IO ()
>       streamGetBuffering :: s -> IO BufferMode
>       streamFlush	   :: s -> IO ()
>       isEOS		   :: s -> IO Bool
> class InputStream s where
>       streamGet         :: s -> IO Word8
>       streamReadBuffer  :: s -> Integer -> Buffer -> IO ()
>       streamGetBuffer   :: s -> Integer -> IO ImmutableBuffer
>       streamGetContents :: s -> IO [Word8]
> class OutputStream s where
>       streamPut         :: s -> Word8 -> IO ()
>       streamPuts        :: s -> [Word8] -> IO ()
>       streamWriteBuffer :: s -> Integer -> Buffer -> IO ()

I have some issues/queries with this.

1. The monad (and maybe also the "byte" type) should be parameterised. 
There are two ways of doing this.

General way:

  class InputStream s m | s -> m where
    streamGet :: s -> m Word8

Haskell 98-compatible way:

  class InputStream s where
    streamGet :: s m -> m Word8

Of course you'll need to be able to parameterise your buffer types, too.

2. streamWriteBuffer probably wants an ImmutableBuffer. The idea is that 
a "Buffer" is a special kind of "ImmutableBuffer" that also supports 
being set. Probably you should rename them. They're just flavours of 
"Array" anyway, aren't they?

3. I note all the class members have types of the form "s -> a" each for 
some "a" not dependent on "s". This means streams might be a candidate 
for data structures:

  data InputStream m = {
     isStream :: Stream m, 
     streamGet :: m Word8,
     streamReadBuffer :: Integer -> Buffer -> IO ()

I'm not sure which is preferable however. Data-structure inheritance has 
to be done by hand (except see point 9., it might only be a close 
function), and they don't allow default implementations (yet).

4. Shouldn't "streamFlush" belong to OutputStream?

5. It might be useful to have a "streamAvailable" function in 
InputStream that gets all bytes immediately available without blocking.

6. Shouldn't "isEOS" belong to InputStream? Come to think of it, perhaps 
streamGet should return a (Maybe Word8) instead of a Word8.

7. OutputStream should have a streamSendEOS separate from streamClose. 
This is used in TCP, where one can say "I'm done sending" on the 
outgoing channel while still receiving octets on the incoming channel.

I would argue that for file streams, it would be appropriate for 
streamSendEOS to set the end of file at that point.

8. Buffering is a special thing, isn't it? Should input-buffering and 
output-buffering be separated? Should there be separate classes for 

9. If you accept all this, all you have left in Stream is closeStream, 
which I would argue is all that "sources" and "sinks" really have in 
common. And perhaps some don't even need that. The class might be better 
named "Closable" or somesuch.

> -- Files in the filesystem, with access rights
> data File
> data FileInputStream  -- instance Stream, InputStream
> data FileOutputStream -- instance Stream, OutputStream

Would it be worth also providing a random-access-based interface to 
files in addition to a stream-based? I know either one can be built on 
the other. For instance:

  accessFile :: File -> IO FileAccess
  closeAccess :: FileAccess -> IO ()
  getFileLength :: FileAccess -> IO Integer;
  setFileLength :: FileAccess -> Integer -> IO ();
  readFile :: FileAccess -> Integer -> Integer -> IO ImmutableBuffer
  writeFile :: FileAccess -> Integer -> ImmutableBuffer -> IO ()

Of course, this might be better generalised as a class.

> -- URIs:
> data URIStream
> getURI :: URI -> IO URIStream
> instance InputStream URIStream

Eeesh. URIs hide a lot of semantics, which this rather glosses over. And 
isn't writing an FTP client hard?

Ashley Yakeley, Seattle WA