I'm finally about to sit down and power out the blog post I've been promising for way too long now on my conceptual system for specialization safety which should be both fully sound and allow all possible sound specializations (that are resilient in the face of semver compatible upstream changes) to be implemented. But I need some syntax to use while discussing the semantics, so here I am, asking for ideas.
1. Specialization-safe traits
Traits which can be specialized on MUST be marked as distinct from standard traits, such that they can be treated differently to ensure the safety of specialization. The easy answer is #[specializable] trait, but a keyword like virtual trait (not virtual as in dynamic dispatch, but virtual as in generic code can test for its presence) or contextual keyword like spec trait are much less noisy.
The case for using a keyword gets even stronger when you also consider…
2. Distinguishing between "baseline" and "added" supertraits
When specializing on e.g. trait TrustedIterator, you have a baseline generic T: Iterator that you want to upgrade to trusting. The bound differential between impl TrustedIterator and impl Iterator must be specialization safe.
When specializing on e.g. trait ExactSizeSpec, you still have a baseline generic T: Iterator, but you now want to upgrade to T: ExactSizeIterator. The bound differential between impl ExactSizeSpec and (still) impl Iterator must be specialization safe and must satisfy the requirements for T: ExactSizeIterator.[1]
The default kind of supertrait bound could go either way. We could look to whichever is the conservative choice, which I think is an "added" supertrait, but I'm not confident in if changing supertrait kinds is an API compatible change (i.e. that going from "added" to "baseline" supertrait only relaxes requirements on downstream). As for which is more common, all of the practical examples I can think of have zero to one of either kind. (But limiting to exactly one by syntactically privileging the first supertrait feels wrong, especially since we decided against privileging upcasts to the first supertrait.)
Also note that while I'm focusing on supertrait bounds, since I expect those to be the most common given a pattern of non-specialization standard traits and specialization marker subtraits, but all bounds in a spec trait declaration will need to be marked as "baseline" or "added."
- default "baseline" explicit "added":
spec trait ExactSizeSpec: Iterator + spec ExactSizeIterator - explicit "baseline" default "added":
spec trait ExactSizeSpec: default Iterator + ExactSizeIterator
spec for an "added" supertrait mirrors the spec keyword on the trait item itself and emphasizes that bounds towards satisfying that supertrait must be specialization safe. But I'm not super enthused about this option, since it's not immediately obvious which supertrait kind should be "the specialization supertrait," as both kinds of supertrait have notable special properties for supporting specialization.
default for a "baseline" trait emphasizes the specialization use case of starting with the default bound(s) already satisfied before performing further specializing. Or this keyword could be final, which could be directly tied to…
3. Non-relaxable bounds
One notable wrinkle to specialization is that required bounds in a crate are allowed to be relaxed — it's API compatible to replace impl<T: Eq> PartialEq for Ty<T> with impl<T: PartialEq> PartialEq for Ty<T>.
For trait impls, this isn't all that much of an issue — it means that non-specialization-safe T: Eq doesn't satisfy a "baseline" Ty<T>: PartialEq, but you can always add an explicit Ty<T>: PartialEq bound with no functional change except specialization access.[2]
For generic type names, however, this becomes relevant. The "just works" solution is implied bounds that remain unsatisfied except for naming the type name which implies those bounds. The simple and straightforward solution is to mark the type itself as having minimal bounds on generics that won't ever be further relaxed. But these don't mix well, as there is no way to mark one generic as known minimal but leave another as relaxable.
final struct S<T: Trait> works to mark the struct generics as finalized, thus minimally bounded, but is directly confusable with saying that all fields are known final. Any of T: final Trait, T: default Trait, T: spec Trait, or T: virtual Trait could be justified, depending on how exactly the naming is explained (does it name what or why the modifier exists) when we mark the bound itself.[2:1]
The generic bounds of generic default fn also face the same question about relaxation. But in their case I think it's clear that they have to work like trait methods, where strengthening or relaxing the bounds on the method is semver breaking.
And then I think the final bit of necessary syntax…
4. Impl specialization
The current nightly syntax of default fn to opt in a function to being specialized works well and makes sense to me, even if the likely meaning of default impl is "default" in a different way.
I'd just suggest adding override as a required keyword (already reserved) for functions which specialize a default fn impl, or final if it's not intended to be further specialized.
However, there is a further wrinkle. If the override specializes from a baseline of the impl block with default fn, then that impl block of the default impl is made non-relaxable. If the override specializes from the baseline traits, the override loses access to bounds available to the default impl and almost certainly becomes an improper specialization[3].
My initial choices
Having written this out, but not heard anyone else's opinions yet, I think I'm leaning towards:
spec trait Trait, read as "specializable traitTrait."spec trait ExactSizeSpec: default Iterator + ExactSizeIterator, read as "… refining specialization base traitIteratorand supertraitExactSizeIterator."T: default Bound, read as "Timplementing specialization base traitBound" and makingT: Boundan implied bound whenTy<T>is present.- Privilege crate-local specialization, allowing it to specialize the specific impl bounds, as in a local setting, relaxing the default impl and making the override no longer specialization safe is a local error that can then be fixed without breaking anyone. Also, due to the orphan rules (which still apply to override impls[4]), the cases where this would actually come up cross-crate seems exceptionally rare.[5]
I hope that reasonably covers the syntactic needs of specialization. Do you agree with my choices, or do you have additional insight to offer?
To push myself to hopefully actually finish the blog post, I'm also putting a date on it: by next Sunday for part one (providing specialization bounds) and a week from that for part two (utilizing specialization).
(Aside: I mentioned having this conceptual system offhand at RustConf All Hands, and got a "doubt it's actually complete and sound, but go for it" there. I've been delaying since… stupid executive dysfunction.)
Minor note: the
impl ExactSizeIteratorbounds don't actually have to be specialization safe, just the subset required byExactSizeSpec. I expect this to be a common pattern in practice, e.g.impl<T: Step> ExactSizeIterator for Range<T>and thenimpl<T: TrustedStep> ExactSizeSpec for Range<T>. ↩︎T: spec Sizedwill be an awkward specification. But hopefully unnecessary, since as a specialization safe trait, it's should always allowed to add aSizedbound in a downstream specialization. ↩︎ ↩︎A specialization which is not satisfied only by a proper subset of the specialized impl. ↩︎
There's an argument to be made that inherent
default fnshould act like a trait method for the purpose of override orphan rules, allowingimpl Foreign<Local> { override fn … }. This would need to be decided before specialization to determine if the generic bounds on the fn itself are relaxable ↩︎For the rare case that actually needs it, we can allow
defaultbounds on impl blocks to specify the bounds that can be used as a specialization base. ↩︎