A common thread that I see in language design over time is that the “standard library” designers are often forced to revisit hard-coded concepts and then convert them into abstract interfaces in later rewrites or “2.0” versions. A particularly smart approach – used by the Microsoft .NET language team, among others – is to define core interfaces in the standard library early on, even if the implementations of such interfaces are trivial, or only expected to be provided in expansion libraries.
These interfaces then serve as a glue between third-party packages, which is particularly important for Rust, a language geared towards the “crates.io” model of package-based code distribution. Secondly, getting these interfaces correct to begin with can save a lot of rework, avoiding a messy standard library littered with deprecated interfaces that can never be removed.
The most familiar example of one of these “low level” interfaces that Rust does provide in the standard library is probably Iter
, which thankfully has been implemented by the Rust team in a forward-thinking way.
However, there are a variety of similar core interfaces that are still missing. These are interfaces that the standard library will have to have sooner or later. These are simply unavoidable, and are required for orthogonal behaviour, or are just commonly needed for practical software of every sort. I see these re-invented a lot, but left out of most “1.0” languages (not just Rust), despite mountains of evidence from other, older languages that this is a mistake.
A list of some proposals (apologies if some of these already exist or have already been proposed):
Observer and Observable: the matching “push” equivalents of the Iter
trait. Incredibly important for event-based and asynchronous code. The entire Reactive Extensions library/concept/manifesto is based around this. Note that I’m not proposing that the std
library contain Rx, I’m just suggesting that it contain the two traits. (Correct me if I’m wrong, but as far as I know, Rust doesn’t even have the concept of an event at all in the standard library!)
Clock: I don’t mean a timer, or the system clock API, but the abstraction of now() -> Time
, even if “Time” is a template parameter, not a concrete type. Why is this important you ask? There is a classic “lessons learnt” talk by John Carmak (of Doom/Quake fame), who made an entire class of bugs become vastly easier to reproduce and debug by coming to the realisation that “get the time” is an interface, and should be treated as such. By replacing all calls to the C++ SDK’s “get system time” with a pluggable interface, he could reproduce replays exactly by replaying the time along with other captured events. He figured this out in something like 1999! He’s not the only one. Microsoft’s Reactive Extensions heavily depends on pluggable time sources for testing, reproducibility, and the like.
Compare with the “time” crate: https://doc.rust-lang.org/time/time/fn.now.html Bzzt! Wrong! This can’t be plugged into a future “Rx” implementation. It’ll have to be wrapped in a Trait. Where does that trait belong? In Rx? No. Time? Not really. It belongs in std
, the common glue between non-standard crates.
Future or Task: I know that there are proposals for adding C#-style asynchronous programming to Rust, but it’s worth noting that Microsoft added the Task
type to the .NET standard library in version 4.0, but the async
keyword was added later in version 4.5. The concept of a “future result” is incredibly generic, and frankly all IO should be rewritten in terms of it, with or without language support for “async”. Sooner or later, this will be the standard. Java added “NIO2”, .NET added async methods to all IO libraries, and then dropped the synchronous versions for Universal App development. There is a reason!
Speaking of asynchronous programming in general, C# is a goldmine for abstract concepts that belong in the standard Rust library. The CancellationSource, ProgressNotification, Scheduler and Dispatcher concepts are core to Task-based and GUI programming on most platforms.
Transaction, Enlistable, and TransactionScope: these are core abstractions over a wide range of APIs, that not only all behave almost identically, but also are often expected to interact. Transactions are the only multi-threading primitive that compose safely, making them an essential abstraction for safe multi-threaded code, not just databases. What is a “transaction”, really? It’s a Trait inheriting from “Drop” that has a single “commit()” function. If it’s dropped before the commit call, it’s rolled back. That’s it. One method!
Lock and/or Monitor: currently, the synchronization primitives are distinct struct types. Many abstractions can be built on top of an abstract concept of a lockable or waitable object, without specifying that it is a Mutex, SpinLock, Semaphore, or even a “Null” lock that does nothing. A brilliant example of this type of library design is the Oswego multi-threading library for Java, which Sun eventually adopted into the standard library. Note the ‘eventually’! Why not start with the elegant design from the beginning?
To add further to the Lock
trait example above: currently RwLock has separate methods for acquiring a read or a write lock. The correct abstraction would be to recognise that it is simply a tiered lock with two levels. A RwLock should instead “contain” (or return) two objects that implement “Lock”. This allows better encapsulation in library design – an API could expose just one of the two lock types, and can also abstract away the internal implementation details.
This kind of abstraction allows things like DebugLock
, which wraps any Lock
, performing tracing and deadlock analysis. The next set of abstractions below also rely on abstract Lock interfaces:
Sink, Source, and Queue: again, examples from the Oswego library, which contained the abstract multi-threading concept of a Queue split into two interfaces. Queues were “built” out of a lock and a container interface, allowing a single class to implement everything from a “synchronous slot” to a priority queue based on circular buffers.