My point is that these bad design decisions were entirely predictable, and could have been avoided with a tiny bit of experience and foresight. It’s just a matter of inspecting the history of other languages.
In my experience, most popular languages go through a life-cycle that goes something like:
- No templates, trivial APIs. Everybody is impressed at how lightweight and simple everything is. There are many convenience wrappers around OS concepts such as the POSIX or Win32 file descriptors wrapped in a convenient
Stream class, hiding some of the 1960s legacy behind a thin facade. A half-arsed attempt at i18n is grudgingly included, but there is clearly an anglo-centric feel to the language. That’s okay, all the initial developers are in the western world, so you can get away with this. This is 1990s C++, C# 1.0, Java 1.0, and Go currently.
- That template itch just won’t go away, so it is hacked into the language. A lot of APIs are duplicated, such as
IEnumerable<T> in C#. Legacy APIs like
Stream are left byte-only, because it’s just too hard to fix it now. This is C# 2.0, Java SE5, early 2000s C++, etc…
- There’s a grudging acceptance that the rest of the world will refuse to learn English, so the i18n APIs are aggressively expanded. Now there’s two versions of the String APIs, one with the old defaults and one with the new comparison options. There’s usually several of each of of the date, time, and calendar types now, because the real world is complicated. Sometimes things just get shredded because an intermediate library hasn’t been updated to handle
DateTimeOffset or whatever. Bad luck!
- Turns out templates are hard! They need all sorts of restrictions such as being constrained to value (copy) types, types that implement a particular API, and so forth. The C++ guys are still trying to work this out. Rust has had this since before v1.0 via Traits. <- You are here
- At some point someone realises that with the advances made in step #4, a lot of APIs can be rewritten to be vastly more elegant. “Obviously”, making things like Stream a lightweight synchronous wrapper around a byte-only file descriptor was a mistake, so now the entire language is slowly reinvented piece by piece, leaving an absolute mess of incompatible APIs next to a bunch of legacy garbage. Modern C++, dotnet core 2.x, and Java 10 are here now.
First, please read this article because it spectacularly illustrates my point: Pipelines - a guided tour of the new IO API in .NET
The dotnet core guys came up with this, and it’s great, but now 99% of the code out there is based on the old stuff, so this won’t be used much. Third-party libraries and large enterprise systems will continue to use
Stream, directly or indirectly. For example,
XmlReader will probably never get properly rewritten in terms of the Pipe API, because it would be a breaking change to make proper use of the efficiencies, such as allowing consumers to use
Span<char> instead of heap-allocated
String instances. It’s too late. The language was built up incrementally, and you’d have to make a new – incompatible – version to really make use of the features. (Just look at Python 2 to 3 or Perl 5 to 6 to see how easy that is!)
We know what a stream API should look like in any language that’s gone past stage #4. My point is that Rust essentially started at stage #4, its developers had all of this history to reference, yet
std:io::Read looks very much like the C# 1.0
Stream API that is finally getting replaced. It inherently makes copies, it is inherently byte-based, and it mixes in unrelated string APIs that prevent a backwards-compatible upgrade to a template-based version. Etc, etc…
In fact, as an API the Rust version is objectively worse, to the point that I can 100% guarantee you that it must be eventually thrown out and replaced by something better thought out.
For example, the Rust
Read trait forces UTF-8 on you, so even if you just want to read UCS-16 out of a binary stream, then you… have to go down a completely different API path! Err… wat? Even C# 1.0, back in 2002, got this right! It has a separate
TextReader classes to wrap byte streams in a specifiable encoding. The underlying
Stream class makes no such i18n assumptions. Remember… not everybody speaks English and not everything is UTF-8, no matter how hard we want this to be true!
This is what disappoints me about Rust. It got a running start compared to other languages, it was developed with decades of history to reference, yet it seems to insist on repeating the same mistakes…
PS: I’m not the only one with this point of view: https://www.reddit.com/r/programming/comments/8vjjgu/pipelines_a_guided_tour_of_the_new_io_api_in_net/e1ow1an/
PPS: I take it all back, I just had a play with the C# Pipelines API, and it turns out that it is not template based, its data stream is always made up of bytes. I was tricked by seeing
SomeClass<byte> in code samples, but that’s just code reuse. The API itself does not generalise to other value types such as
char or whatever. Sigh…