I think I'd like to hear more argumentation about why unidirectionality is a desirable property rather than merely a sensible one.
Unidirectionality is strictly less powerful than bidirectionality, so one can only argue for it from the standpoint of simplicity and ergonomics.
Unidirectionality makes selection straightforward. Any operation declared inside a
select will behave exactly as if it was a standalone operation (outside
select). No surprises there.
With bidirectional closing, however, we have to introduce a special
closed case in
select, which will not be automatically enforced, unfortunately. Alternatively, we must reach for more complicated variants of
send cases, which adds a considerable penalty on ergonomics.
In one sentence, unidirectionality makes the interface smaller, simpler, and more consistent (in terms of
select vs standalone operations, at least).
Maybe somebody can elaborate on what this means and why? It seems to me that the receiver doing a close() followed by the sender detecting the failure of its send and reacting appropriately could serve as those two steps, at least under one way of interpreting them.
My understanding is that in Go this is simply considered an antipattern.
If any send operation can fail, that means most users will just ignore the error and prefer to panic instead. Basically every use of channels in Rust is littered with
.unwrap()s, which sometimes looks silly. This is reminding of tedious
.unwrap()s one has to write after locking a
Mutex, which prompted some people to create
Mutexes as in
parking_lot decided to do away with
Results, too. The
chan crate sends into a channel without a
Also, signalling to the sender that the receiver is shutting down is not hard to do using a 0-capacity channel anyway, and may lead to cleaner design. At least the Go team believes so.
Finally, it's interesting to ponder how one would actually implement channel closing. In Go, each channel is wrapped into a single mutex so there's a
closed: bool flag in there, but what if we had a lock-free queue and no mutexes behind the scenes? While it's tempting to use a
closed: AtomicBool flag, that would be wrong and cause linearizability issues. Instead, a close operation in
crossbeam-channel marks a special bit in the sender index.
Note that "close the channel" is literally just a synonym for "freeze the sending side". Since channel closing is only concerned with the sending side, perhaps only the sending side should be allowed to close? The answer to this question probably depends on whether your mental model of a channel is "a simple concurrent queue" or "like a unix pipe where both sides can signal closing".
(Is the "cannot serve as both" claim only intended to be true within the context of Go, e.g. because it happens to have no way to detect the failure of a send, or is it something more fundamental?)
While Go doesn't have a way of detecting failure of a send operation, they could've easily implemented this feature (e.g.
ok := channel <- msg). To omit the feature was a deliberate decision.
Which is what OCaml is thinking about doing. Of course, as I meant to imply with the name there, this only makes sense for 0-buffered channels. But, at least, it suggests to me that bidirectionality can also be a sensible property.
This is an interesting example. Indeed, the symmetry is beautiful. However... :)
I've done a lot of research on queues and channels in other languages and libraries. They will often boast about performance, features, and so on. The rosy story always falls apart when it comes to select. Go is the only language where channel selection is simple, easy, and just works. Many channel implementations don't have any kind of selection at all! Let's take a look at the
mpsc_select feature in Rust as an example. The macro has fragile syntax (poor compilation errors), only accepts receive operations (no sends!), and has seen no progress in years. The macro is basically deprecated (see here) and at this point it is only kept alive because Servo still depends on it.
chan crate is an alternative that sacrifices performance, but provides a nice Go-like interface with proper
chan_select! macro. Apart from a few quirks and bad performance, it works very well. In 2015, a request for adding a
send operation returning a
Result was submitted. was originally hesitating:
The second design decision I made was, "sending a value that can never be received is either a bug or an intentional leak." This may be a bad decision, but it is one that has a large body of evidence that suggests it may not be a horrible idea. (e.g., Go.) The idea here is too encourage users of chan to write code that doesn't enter that state. I was motivated to do things this way because so many uses of the std::sync::mpsc channel are tx.send(...).unwrap(). In other words, it makes the common case verbose.
In the end, he was sold on the feature request and decided to implemented the non-panicking send method. However, it turned out that fitting the concept of non-panicking send into
chan_select! was a very difficult design problem, and that's where the story ends.
crossbeam-channel is a successor to
std::sync::mpsc. I've put a lot more time and effort into its development and managed to overcome some key difficulties other channels have run into. However, when it comes to selection, now I'm too facing the same old infamous selection problem. There seem to be only two ways forward:
std::sync::mpsc-like design and implement a slightly complicated
select! with explicit
Take a more opinionated stance and implement
chan/Go-like channels with simple