6

Over the years I've run into (and created) APIs around device communication (Serial, USB, TCP, Bluetooth, etc). At the bottom of these APIs are usually byte streams that can send data to the device and receive data from the device. Where they sometimes differ however is whether they give you one bidirectional stream, or two separate unidirectional streams.

I recently ran into such an API gain, specifically Android's BluetoothSocket, which has a separate getInputStream() and getOutputStream(). This made me wonder, what are the benefits and downsides of having two separate unidirectional stream objects, versus a single bidirectional stream object?

One downside I've run into myself for having separate streams is that the meaning of "input" and "output" depends on the perspective, which can cause confusion. The "input" from the server side is the "output" from the client side, and vice versa. In languages where the interface for a stream is inherently bidirectional (see C#'s Stream for example), this can lead to bugs when the wrong stream is accidentally used.

2
  • 1
    Isn't that called a pipe finally, and what's input/output is defined by the participants? Commented Jun 27 at 10:10
  • 2
    Do note that the underlying connection usually has 2 unidirectional streams, the unified interface is just an abstraction.
    – Ccm
    Commented Jun 28 at 13:03

4 Answers 4

7

In languages where the interface for a stream is inherently bidirectional (see C#'s Stream for example), this can lead to bugs when the wrong stream is accidentally used.

I agree. That's why there should be separate interfaces for "incoming" and "outgoing" streams. Btw, you don't have to use C#'s Stream interface/abstract class. This interface has sooooooo many methods and properties that my head hurts. You can (and in my opinion should) write your own abstractions.

So, two separate interfaces. And if you need something bidirectional, you can simply require it to implement both.

Honestly, I don't see any advantage of having "bidirectional" interface from the begining. Not all streams are bidirectional. And even if they are you might be interested in a single side only at given time (which is common). Which implies that you need to expose check for direction. In C# you have CanRead, CanWrite properties. Why? I would rather have a compiler (or some other static analysis tool for non-compiled languages) to verify that.

In my opinion it is a classical example of having too much responsibility in a single place. And I think that this particular Android's API is well designed.

And as a final touch: look at Rust. It has separate Write and Read traits as well. But built in. Because Rust designers already know. If I ever need two-directions, I simply require a type to implement both. Voilà. It is an unfortunate situation that C# and many other languages (Python) mix both.

4
  • Well, not saying the C# Stream couldn't be improved, but I guess the idea behind it is to be able to allow switching of readability and writeability at run time, not just at compile time.
    – Doc Brown
    Commented Jun 27 at 11:18
  • @DocBrown like, dynamically, depending on some runtime context you read instead of write? What would be an example of a system with such requirement? Because honestly I don't see it. Regardless, nothing that cannot be achieved with two separate interfaces. Especially in C#, where generics can implement multiple interfaces.
    – freakish
    Commented Jun 27 at 11:20
  • Well, a simple use case for this is a file where one first writes data into it, then closes it (which blocks it for further writing), and reader which reads until the end is reached (which then blocks it for further reading). Does not look very far-fetched, if you ask me. Don't get me wrong, I am not saying separate reading and writing interfaces would not also result in a sound design. Still the .Net lib designers chose a different approach for that in the form of StreamReader, StreamWriter, BinaryReader, BinaryWriter, ... classes, and that approach does not look so much worse to me.
    – Doc Brown
    Commented Jun 27 at 12:50
  • @DocBrown well, this example can of course be emulated by two functions, one accepting writer, and one accepting reader. And just pass the same file to both. Of course at some point these have to originate from the same bi-directional stream. Because that's how operating systems expose files. And a self respecting language/runtime won't duplicate those file descriptors for no reason (which is not a matter of simple copy, I'm talking about dup syscall or equivalent).
    – freakish
    Commented Jun 27 at 14:24
7

One use case for a bidirectional stream is when editing files. This would often require both reading and writing, and editing files is a quite common use case. I also believe most OSes exposes files as bidirectional streams, so exposing this in the language may help reduce complexity. You can always provide separate read/write abstractions on top of the basic stream.

For communication, using separate streams for reading and writing seem like the more natural model.

One may also question if it is a good idea to use the same abstraction for files and communication channels. But API design is difficult, and there is plenty of chances for leaky abstractions, or ideas that seemed good at the time.

1
  • This made be thing of another fun design problem with bidirectional streams; should a seek operation update both the read and the write position, or should you be able to set them separately? C++ says yes (with a related defect report even), Win32 says no Commented Jun 29 at 20:55
0

Superfluously, unidirectional streams come with a smaller surface but bidirectional streams come with fewer objects to handle. In the end, this is mostly a matter of personal preference of broad versus deep usage surface.
Many of the design issues are otherwise shared. For example, whether you have to mentally juggle input versus output methods or input versus output objects does not change that you must know what input and output refer to. In modern languages, a pair of unidirectional streams or a bidirectional stream are semantically equivalent:

  • If a language easily supports composition, defaulting to separate streams is no usability restriction - you can just stitch them together to look like a bidirectional stream.

  • If a language easily supports limiting capabilities1, defaulting to a bidirectional stream is no usability restriction - you can just separate them to look like unidirectional streams.

However, in practice some connections are single unidirectional streams. No amount of composition is going to turn them into bidirectional streams. So treating unidirectional streams as the basic interface building block is more general.


1Say, via Traits as in Rust.

0

It's easier to specify a pair of uni-directional streams rather than a bi-directional stream. In theory, one needs to define the interactions between the function calls in both directions (both reads and writes) to fully define a bi-directional stream. In practice, designers implicitly decide to think of them as two uni-directional streams and write the API accordingly.

A few answers have mentioned files as an example where bi-directional streams matter. In the case of files, if one writes bytes to a file, one expects to read them, so the interactions do indeed need to be specified. However, for other APIs, like network sockets, we think of them asynchronously and don't make any assumptions about the interleaving of reads and writes unless the application layer add such an assumption (such as waiting to read a request before sending a reply).

One advantage to having the functions on two different objects is that one is not obliged to keep the function names lexically separated. If the reader API and the writer API want to have a function call with the same name but slightly different semantics, that can be done. Names like close are particularly valuable. If a language already supports stream operations (rather than making a custom API for, say, BluetoothSockets), the language may have already picked meanings for something like close which need to be honored.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.