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

Re: Why are byte ports "ports" as such?

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

   Date: Fri, 14 Apr 2006 09:37:23 -0400
   From: Marc Feeley <feeley@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


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))

   (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))
                 (proc file)

   (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?