Flexible Explicit API Knobs
A ton of t-lang design decisions hinge on thinking about “how can this code evolve”, “what am I promising my users by writing this code”, etc. And a lot of feature bikeshedding is about choosing sane defaults so crate authors know what they’re committing to while still allowing flexibility.
I propose we could make this a more explicit part of the language by giving crate authors a common language to control their present API surface and opt-in/opt-out of future API changes.
The proposed keyword choices are very much not what I expect to be accepted; I’m just trying to share the idea for now. Please share other compelling examples! (I will edit this blog post to include more examples).
// Equivalent to `#[non_exhaustive]` on enums.
#[future_proof(allow(author, add_variants)]
enum Enum { ... }
enum Enum {
// Kinda opposite of `#[non_exhaustive]`; forbids user from matching or
// constructing this variant.
#[future_proof(allow(author, remove_variant)]
Variant1,
..
}
// Equivalent to `#[non_exhaustive]` on structs.
#[future_proof(allow(author, add_fields)]
struct Struct { ... }
// The author can add fields but only public ones. This means downstream
// crates can use FRU (idea from scottmcm, ty!).
#[future_proof(allow(author, add_fields(pub))]
struct Struct { ... }
// The author can add fields but only with default values. This means downstream
// crates can construct it (idea from scottmcm, ty!).
#[future_proof(allow(author, add_fields(with_defaults))]
struct Struct { ... }
// Allows downstream crates to rely on the layout of this struct. Could be used
// for safe transmutation to reason about API/ABI stability.
#[future_proof(forbid(author, change_layout))]
#[repr(C)]
struct Struct { ... }
// Bound the size of the struct.
#[future_proof(size <= 42)]
struct Struct { ... }
// Commit to keeping these auto traits implemented.
#[future_proof(implements(Send))]
#[future_proof(implements(const Destruct))]
struct Struct { ... }
// Prevents downstream crates from implementing this trait. Basically builtin "sealed traits".
// This isn't a "future-proof" kind of thing so I picked another keyword but I
// don't like it much.
#[api(forbid(downstream, impl)]
trait Trait { ... }
// Ensures a trait is and stays dyn safe.
#[future_proof(is_dyn_safe)]
trait Trait { ... }
// Prevents adding a new method if it's not const.
// IIUC, should be enough to allow `const Trait` bounds. If so, that's an
// alternative to the `const trait Trait` syntax.
#[future_proof(forbid(author, add_method(not_const))]
trait Trait { ... }
trait Trait {
// Prevents downstream crates from overriding this method. Replaces `final`
// methods (https://github.com/rust-lang/rfcs/pull/3678).
#[api(forbid(downstream, override)]
fn method() { ... }
// Prevents downstream crates from calling this method.
#[api(forbid(downstream, call)]
fn method() { ... }
}
// Ensures the lifetime/type param stays covariant.
#[future_proof(covariant('a))]
#[future_proof(covariant(T))]
struct Foo<'a, T> { ... }
// Prevents `foo::<...>` syntax as the generic parameters may change, e.g. going to `impl Trait` instead of an explicit param.
#[future_proof(allow(author, change_generics))]
fn foo<T: Trait>(..) { ... }
// Ensures the generated coroutine implements `Send`.
#[future_proof(implements(Send))]
async fn foo() { ... }
I would imagine, in a future edition, having a lint that warns if you’re relying on un-committed-to
API facts, e.g. you use dyn Trait for a trait that didn’t guarantee it would stay dyn-safe.
I feel like there are quite a number of features that would fit in this, and like we could find a nice common language to talk about those things. This is a call for contributions: do you see other examples?