[racket] design and rationale of ports

From: YC (yinso.chen at gmail.com)
Date: Wed Nov 10 16:28:23 EST 2010

On Wed, Nov 10, 2010 at 10:38 AM, Taylor R Campbell <
campbell+racket at mumble.net <campbell%2Bracket at mumble.net>> wrote:

> The documentation describes the contract of MAKE-INPUT-PORT and
> MAKE-OUTPUT-PORT.  That doesn't tell me how the pieces fit together,
> though, or say what one can do with them, or give any guidance on how
> to make them.  For example, given the pieces of an input port, how do
> I implement PEEK-BYTE or READ-BYTE?  Conversely, suppose I have a file
> descriptor and some way to do a non-blocking read on it, or an
> asynchronous (aio) read on it; how do I make the pieces of an input
> port?

make-input-port & make-output-port creates custom port types - this is only
needed if your port needs are not satisfied with what's provided.  Since it
provides the hooks to the underlying port system it is quite complex.  I
have made a few such ports but only know enough to do what I needed to do.
 Again it might be easier to help if you share more details.

There is another way to create custom ports - and that is through the use of
struct property prop:input-port and prop:output-port.  If all you need to do
is to wrap a couple of objects with ports, then this is probably what you
need.  (when I first started I did not realize that and implemented all
custom ports via make-input-port & make-output-port, but I made the
migration and found out that only a few truly need either, most of needs I
found can be addressed through wrapper ports).

Below are what I've learned through my own trial/error.  Maybe this can be a
starting collaboration point for a better guide.

make-input-port requires at least 4 arguments (I've not used beyond the
required arguments so others will have to chime in beyond this).  The first
one is the name of the port, and the second is a procedure for reading from
the underlying source.  The third is for peeking instead of reading, and
fourth is to close the source.

Basically - open, read, peek, and close are the main operation that one
would do with an input-port.  make-input-port itself does the open, and
hence you'll need to customize the read, peek, and the close operations.

And since these hooks all need to have access to the underlying source, you
will need to make the read, peek, and close a closure over the source port
(let's call it IN).

The read-in hook is the key hook to implement - this is how others will read
data from your port.  read-in takes in a mutable bytes (the caller holds the
reference so changes are visible to the caller), so your procedure should
mutate the bytes with the source you read from.  And you should return the #
of bytes read, or eof in the usual circumstances.  So the simplest read proc
will look like the following:

;; *read-bytes!* is the custom read-bytes! that works with your source.  If
your source is a regular scheme port, then it's the same as read-bytes!
(lambda (bytes)
  (*read-bytes!* bytes IN 0 (bytes-length bytes)))

the peek hook allows others to peek data from your port.  It takes in a
mutable byte, the # of bytes to skip, and a "get-progress-evt" to indicate
whether or not we are in a "get-progress".  This will be #f if the fifth arg
to make-input-port is #f, in which case below is the simplest peek:

;; *peek-bytes* is your custom peek-bytes that works with your source, it
will be the same as the default peek-bytes! if you have a regular scheme
(lambda (bytes skip evt)
  (*peek-bytes!* bytes skip IN 0 (bytes-length bytes)))

Make sure do not implement peek with a read to the underlying source since
peek is not supposed to consume bytes.  peek returns either # of bytes read,
or eof.  If you have a get-progress-evt it might also produce a #f, which
indicates that no bytes are peeked.

the close hook allows you to close the underlying source.  The same logic
above applies.

;; *close-input-port* is the custom *close-input-port* that works with your
source - it will be the same as the default close-input-port if you have a
regular scheme port.
(lambda ()
  (*close-input-port* IN))

To put the above together, below is a procedure that will make such a basic
custom input-port:

(define (make-basic-custom-input-port IN
                                      (*object-name object-name)
                                      (*read-bytes!* read-bytes!)
                                      (*peek-bytes!* peek-bytes!)
  (make-input-port (*object-name* name)
                   (lambda (bytes)
                     (*read-bytes!* bytes IN 0 (bytes-length bytes)))
                   (lambda (bytes skip evt)
                     (*peek-bytes!* bytes skip IN 0 (bytes-length bytes)))
                   (lambda ()
                     (*close-input-port* IN))))

The above should provide a basic sketch on how it works, and the rest is to
fill out the details on what's specific to your port.  Obviously IN does not
actually have to be a true scheme port - it can be anything that will
produce the bytes.

I will leave make-output-port as an exercise, at least until others have
further questions.

HTH.  Cheers,
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.racket-lang.org/users/archive/attachments/20101110/8e9b19ae/attachment.html>

Posted on the users mailing list.