Re: Why are byte ports "ports" as such? Marc Feeley 14 Apr 2006 16:19 UTC

On 14-Apr-06, at 10:53 AM, Taylor R. Campbell wrote:

>    Date: Fri, 14 Apr 2006 09:37:23 -0400
>    From: Marc Feeley <xxxxxx@iro.umontreal.ca>
>
>    On 14-Apr-06, at 8:49 AM, John Cowan wrote:
>
>> Marc Feeley scripsit:
>>
>>> It is a pain to carry those two ports around in the code when the
>>> program needs to communicate bidirectionally with some other entity
>>> (another process, a user at a terminal, a socket, etc).  Moreover
>>> the
>>> separation of a conceptually bidirectional channel into distinct
>>> ports (input and output) destroys the conceptual link that they
>>> have.  This hinders program understanding.  For example, with
>>> bidirectional ports (close-port port) will close both sides of the
>>> bidirectional port (i.e. the link between the input and output port
>>> is preserved).  With two unidirectional ports you have to duplicate
>>> some operations (closing ports, changing port settings, ...).
>>
>> I find this rationale convincing (and think it should be added to the
>> SRFI).
>
>    OK.
>
> I find this rational unconvincing: I see no reason why introducing a
> third object (say socket, tty, &c.) would hinder program understanding
> any more than the bidirectional ports do -- personally, I think
> bidirectional ports do more to hinder it.

I view these other types of ports as subtypes of byte ports, or
character ports, or object ports.  See below.

>
> If we introduce this third object, then we can simplify the model of
> input and output ports very much, so that an input port is purely a
> source and an output port purely a sink, and we can specify any
> options common to the resource that they are both associated with on
> the third object; for instance, we could have a (SET-SOCKET-OPTION
> socket level option value).  This furthermore simplifies issues about
> closing; see below.
>
> Continuing on the socket theme, port I/O doesn't even make sense for
> some sockets, such as stream listener sockets and unconnected datagram
> sockets.  In these cases it would be absurd to represent the socket as
> a port, while it makes very much sense to represent it as a distinct
> `socket' object, from which in certain cases (specifically, connected
> sockets) one can obtain ports.
>

This may surprise you but that is exactly how stream listener sockets
are handled in Gambit, where tcp-server ports (stream listener
sockets) are object ports (the generalization of character ports
which is mentioned in the SRFI).  When the program reads from a tcp-
server port (with the read procedure) the program blocks until a
connection is established from a client.  The read procedure returns
a bidirectional byte port (tcp-client port) which represents the
connection.  In other words:

    (define (start-http-server)
      (let ((http-listener (open-tcp-server 80)))
        (let loop ()
          (handle-connection (read))
          (loop))))

    (define (handle-connection port)
      (let ((request (read-line port)))
        ...
        (close-port port)))

As you can see this gives a nice high-level interface to stream
listener sockets.  Gambit also provides an object port interface to
directories:

    (define (for-each-file-in-directory path proc)
      (let ((dir (open-directory path)))
        (let loop ()
          (let ((file (read dir)))
            (if (not (eof-object? file))
                (begin
                  (proc file)
                  (loop)))))))

    (for-each-file-in-directory "/" pretty-print)

Doing it this way has the advantage of avoiding using lots of memory
to hold the list of file names when the directory is very long.  Of
course, for convenience Gambit also provides a (directory-files path)
procedure that returns a list of file names.

Object ports are also useful for inter-thread communication, as
FIFOs.  This is convenient for a message passing paradigm where
objects are exchanged between threads (and serializing them with
characters or bytes would be a waste of time or would alter the
semantics of the objects (eq?-ness, cycles, etc)).

To be clear: I want to keep the high-level nature of the R5RS I/O
subsystem.  I think a lot of useful things can be done within that
high-level framework as shown by this SRFI.  I *strongly* dislike
some I/O systems where you need to create layer upon layer to achieve
what is fundamentally a simple I/O task.  As an example taken from a
Java tutorial on the web:

    File outFile;
    PrintWriter pw;
    outFile = new File("output.text");
    if (! outFile.exists() || (outFile.isFile() && outFile.canWrite()))
      {
        pw = new PrintWriter(new BufferedWriter(new FileWriter
(outFile)));
        ...
      }

This is plain ugly.  This is one of the main reasons I dislike SRFI
68 (Comprehensive I/O).

> I think this is actually a bad idea.  It's not entirely clear when the
> resource itself gets closed, versus when individual directions are
> closed.  CLOSE-PORT, it seems, cannot be defined as
>
>   (define (close-port port)
>     (if (input-port? port) (close-input-port port))
>     (if (output-port? port) (close-output-port port))),

An implementation of the SRFI can be smart and optimize the
implementation of the close-port procedure so that a single low-level
close is performed on the low-level device.  But in principle the
implementation you give above is correct (at least for the types of
ports we are considering in this SRFI).  The implementation can
easily keep a one bit reference count to tell when both sides have
been closed.  Do you truly view this as an implementation problem?

Marc