Is it possible to use channels as input/output ports?

I've been playing around with the idea of having channels that can be used like ports -- for example, (print ch) would become (channel-put ch) and (read ch) would become (channel-get ch).

I thought I might be able to get some traction via structs and custom ports:

(define in (make-channel))
(define out (make-channel))
(define p1 (make-input-port 'in-port (lambda (b) (channel-get in))))
(define p2 (make-output-port 'in-port (lambda (b) (channel-put out))))
(struct in/out-ch (in out) #:prop:input-port p1 #:prop:output-port p2)

...sadly, that got me nowhere because make-*-port are onto my tricksy ways and won't allow it. Is there a way to do this? Not critical, but it's been nagging at me.

Can you clarify what you mean by this? How does it fail?
Your example uses 2 parameters, when I look at the documentation those functions take 4 required parameters and more optional ones. (Indicated by the brackets)
Is this more a pseudo code question or did you expect your example to work?

Personally I haven't used custom ports yet and the functions are quite a bit to read and understand in the documentation, which is why I decided to not tackle this challenge today (can't get too distracted from my own stuff...). However if I did I would probably experiment a bit with one of the provided examples to get a better understanding how those functions work and then try to adapt those towards a solution.

1 Like

Pseudo-code, yes. The issue is that the read procedure cannot return an arbitrary value. It must return:

  • an exact positive integer
  • eof
  • a pipe input port
  • a synchronizable event, except the result of the sync must ultimately be one of the other valid options
  • a procedure of 4 arguments which can return anything except a character or eof and will raise an exn:fail:contract if called by some read functions

This is a neat idea. A speed bump, if it were me, would be that ports seem to be a significantly richer and more complicated abstraction --- with respect to ideas like blocking, buffering, and peeking --- than either channels or async channels?

(Maybe that's partly why defining custom ports feels intimidating?)

As a result, trying to build a port abstraction on top of a channel seems like it might be challenging? I'm not asserting impossible, I'm saying this is what would gnaw at me during the long, dark, tea-time of the soul. :slight_smile:

2 Likes

Hm...channels (as opposed to async-channels) block and async channels buffer... async-channel-try-get isn't the same as peek and I'm not sure how to simulate that. I suppose it could offer a particular implementation of port functionality and it's fine as long as the customer knows what to expect.

If peek is #f, then peeking for the port is implemented automatically in terms of reads, but with several limitations. First, the automatic implementation is not thread-safe. Second, the automatic implementation cannot handle special results (non-byte and non-eof), so read-in cannot return a procedure for a special when peek is #f. Finally, the automatic peek implementation is incompatible with progress events, so if peek is #f, then get-progress-evt and commit must be #f. See also make-input-port/read-to-peek, which implements peeking in terms of read-in without these constraints.

Maybe setting peek to #f would be a good first try? Or alternatively the last sentence may be a good alternative. But as said, I haven't tried yet.

make-input-port/read-to-peek states:

Similar to make-input-port, but if the given read-in returns an event, the event’s value must be 0. The resulting port’s peek operation is implemented automatically (in terms of read-in) in a way that can handle special non-byte values. The progress-event and commit operations are also implemented automatically. The resulting port is thread-safe, but not kill-safe (i.e., if a thread is terminated or suspended while using the port, the port may become damaged).

I am guessing that it starts buffering whenever somebody tries to peek ahead or maybe it buffers it always and makes use of that (speculation, haven't looked at its implementation).

I'm struggling to think of why I would want to use such an abstraction instead of just channels or a pipe. Is there a more complex use case that you see using this abstraction as desirable?

I often find myself wanting to have an in/out channel pair for communicating between a manager thread and a series of workers. A struct with two channels in it, where each worker gets a unique receive thread and a shared transmit thread would make a lot of sense, and being able to read/write to the struct would be more elegant than having to constantly dig fields out or write explicit read/write procedures.

Are your threads communicating using byte/character IO, or values? If the former, would pipes work for you? (In the sense of 13.1.7 Pipes .)

1 Like

General values. Obviously, any message can be encoded into a byte string, but it would be nice to be able to use a straightforward encoding.

If you're dealing with general values, I'm not sure why you want to get ports involved, rather than something like:

#lang racket

(struct mailbox (in out))

(define (mailbox-send mb v)
  (channel-put (mailbox-out mb)))

(define (mailbox-recv mb)
  (channel-get (mailbox-in mb)))

1 Like

Yes, clearly that works. As I said above, being able to read/write to the struct would be more elegant than having to constantly dig fields out or write explicit read/write procedures.

This isn't a "hey, here's a problem that cannot be solved in any other way", it's a "hey, here's a problem that would be elegantly solve in this particular way, is it possible to do that?"

I would find it extremely confusing, but here's something that runs:

#lang racket

(struct in/out-ch (in out)
  #:guard (λ (in-ch out-ch name)
            (define-values [in out]
              (make-pipe))
            (port-read-handler in (case-lambda
                                    [(in)
                                     (channel-get in-ch)]
                                    [(in name)
                                     (error 'in/out-ch "no read-syntax mode")]))
            (port-print-handler out (λ (v out)
                                      (channel-put out-ch v)))
            (values in out))
  #:property prop:input-port (struct-field-index in)
  #:property prop:output-port (struct-field-index out))

(define ch (make-channel))
(define in/out (in/out-ch ch ch))

(thread (λ ()
          (print (void) in/out)))

(void? (read in/out))