I come from a background of extremely abstract (yet still efficient) languages such as Haskell, where everything is reduced down to the most abstract trait possible that is required to implement each function (or type class).
Your comment about the errors returned by Read/Write APIs is a valid point, of course. However, I do feel that in terms of abstract interfaces, even Rust gets stuck in the “pit of overspecialisation”. I’m not the only one who thinks this.
For example, see this [post] (C++ has vector(n, value). c has calloc(). rust has, uh,) by a user complaining that the “string search” functions work only on string types. Which makes sense. Unless you want to search for a byte sequence in a byte array, which crops up often in real-world code. The substring search code could easily accept any iterable-of-equatables, not just “string”, which happens to be a specific one. It could just as easily be
u8, or even a
struct. Why does it have to be "just
char", and nothing else? Because that’s the most common use, or the first problem that someone had to solve, so we should stop there and call it a day?
Just because Read and Write were originally intended for stream I/O, doesn’t mean that the abstract traits should be overspecialised for that purpose alone. Buffered transmission of data is a very common design pattern, which will force developers to come up with their own Read/Write traits. Except they might call them Source/Sink, BufInput/BufOutput, or whatever. The implementation details will look an awful lot like Read/Write, but they won’t be able to re-use the functions in the standard library like “copy”, “take”, “chain”, etc… They’ll have to re-invent those wheels as well. Of course, then the next logical step will be the adaptors: ReadSource/WriteSink, ReadBufInput/WriteBufOutput, and so forth. Blech…
Think about what you just said when you made the comment that the return value of the Read and Write functions are designed to map to the
write syscalls: an abstract API has just baked the specifics of POSIX into it forever and ever. Eww! First of all, not all the world is POSIX, Windows still has a pretty big market share, last time I checked. Second, the error codes of read/write don’t even come close to covering even a subset of reasonable use-cases. Think compression, cryptography, multiplexing, inter-process communication, etc… Wildly different errors. In fact, I have this exact problem, right now. I’m trying to implement the
Read trait for a decompression library, and the errors returned a very restrictive.
In my opinion, there should be a clear chain of “more and more specialised traits” with increasing functionality and specialisation along the lines of:
Iterable<T> -> Read<T:Copy+Sized> -> Read<u8> -> StreamRead
Ideally, the “Error” type should be a template parameter as well. In the case of Iterable, it could be a zero-sized type (equivalent of
Option::None), which would preserve its current behavior, but the
StreamRead could require
std::io::Error or whatever. This way, a “CryptoRead” could return errors specific to the Cryptographic algorithm. Decompression could return detailed error types. All of the APIs could then share the various adaptors and utility functions.
This isn’t unique to Rust, of course, I’m just saying that the “templated from the beginning” API design could allow a much more elegant language than almost any other. Rust is already way ahead of most similar languages in this respect, I just feel like it’s got one foot stuck in the past.