Syntax-wise, the where expression would look like if. However,
rather than a condition it would take a list of bounds. During
monomorphisation body of the expression would be used if the bounds
are satisfied and the else path would be taken if they aren’t.
Thanks to limited scope and imperative style, this would avoid most of
the complications of
specialisation
while still offering many of the benefits.
Open question is whether the types of each branch would need to agree.
For example, would this be possible:
At least initially requiring the same type would be sufficient and
allow changes in the future if desired.
A bikeshedding question is whether where and if could be chained
together as in where-else-if-else-where-else or whatever. (As an
aside, for a while now I wished if and match could chain as in
if-else-match without the need for braces and another indentation
level around match).
To be honest I don’t see where the problem is. Conceptually, function
with where expressions can be replaced by a generic function with
const bool arguments. For example something like the following:
pub fn extend<
I: IntoIterator<Item = T>,
const A: bool,
const B: bool,
>(&mut self, iter: I) {
if A {
let iter: &[T] = unsafe { transmute(iter) };
self.extend_from_slice(iter)
} else if B {
let iter: Vec<T> = unsafe { transmute(iter) };
self.extend_from_vec(iter)
} else {
self.extend_from_iter(iter.into_iter())
}
}
Same goes for linked maybe traits proposal. A <T: ~Trait> can be
transformed into <T, const T_impls_Trait: bool>.
Well for one the MyVec::extend only compares to concrete types, not trait impls, and does so completely void of lifetimes (I: &[T]).
But I think if there is a "min min specialization" that works and is easier to stabilize than the existing "min specialization," it's one of:
it's just Any's API, i.e. downcast to specific types and any not-syntactically-'static types either aren't allowed or always fail to downcast; or
it's what I've called "specialization without specialization" — exclusively just conditionally applicable default trait method bodies, and the actual impl block for a type chooses a single one for all covered types.
Current "min specialization" isn't contentious primarily due to syntax or power or breaking some substitution principle, it's problematic due to the subtleties of the always applicable rule. If it were a code coherency issue, we'd just stick specialization restrictions in the orphan rules and be fine, but that's not the core challenge.
This is only true when the where bounds do not capture any input lifetimes. If they are allowed to do so, then this at best makes non-generic functions generic. We don't monomorphize for lifetimes; given fn(&T), the same code is used for 'static and 'tiny lifetimes, and where bounds can require that lifetime to be 'static or outlive some other lifetime; information which the function no longer contains after lifetimes have been erased.
TL;DR: if you specialize on a lifetime, it's unsound. Whatever rules you use to restrict specialization must forbid specializing on lifetimes. The issue is how that works, not anything solvable just by expressing specialization in a different manner.
The above doesn’t compile and no one has issues with it. In the same
way I don’t see any issues with lifetimes being erased through calls
to non-generic functions.
And with transformation I’ve proposed bounds included in where
expressions would be propagated up the call chain where all lifetime
information is present.
if you specialize on a lifetime, it's unsound.
Do you have an example of that on hand? Bounds would be found to be
satisfied only if the compiler sees that lifetimes are satisfied.
I don’t see how that could lead to unsoundness. For example, in
where I: &[T] compiler should be able to conclude the bound is met
since it knows that slice outlives the function call.
It might be sound when the types need to agree. At least I couldn't think of an example to cause unsoundness for that variant with only safe code.
But there's still the same surprising behavior that all specialization proposals share: As lifetimes are erased, the compiler cannot safely assume that types with lifetimes compare equal during monomorphization:
fn foo<T,U>(x: T, y: U): bool {
where U: T {
println!("same");
} else {
println!("different");
}
}
fn main() {
foo(1_i32, 2_i32); // would print "same"
foo("a", "b"); // would print "different"
}
It could also get easy to break assumptions in libraries using where blocks when traits are only implemented for some super-/subtype and coercions occur (see e.g. #85863).