I'm having some trouble understanding why get_direct() works here, but get() doesn't here - it seems like the value of A::To should be known at compile time and resolve to u64.. but it seems like that information is lost if working with an impl Map, rather than with A - but, it must be possible to know the value of <impl Map>::To in order for any <impl Map>::map to ever be called, right?
pub trait Map<From> {
type To;
fn map(&self, f: From) -> Self::To;
fn and<O>(self, o: O) -> And<Self, O> where Self: Sized {
And {a: self, b: o}
}
}
pub struct And<A, B> {
a: A,
b: B
}
impl<A, B, From> Map<From> for And<A, B>
where
A: Map<From>,
B: Map<A::To>,
{
type To = B::To;
fn map(&self, f: From) -> Self::To {
self.b.map(self.a.map(f))
}
}
struct A;
struct B;
impl Map<usize> for A {
type To = u64;
fn map(&self, f: usize) -> Self::To {
(f + 1) as u64
}
}
impl Map<u64> for B {
type To = usize;
fn map(&self, f: u64) -> Self::To {
(f + 2) as usize
}
}
pub fn get_a() -> impl Map<usize> {
A
}
pub fn get_b() -> impl Map<u64> {
B
}
// This compiles just fine
pub fn get_direct() -> impl Map<usize> {
let a = A;
let b = B;
a.and(b)
}
// This causes a compiler error
pub fn get() -> impl Map<usize> {
get_a().and(get_b())
}
Hmmm yeah, I understand that I can do that, but I don't fully understand why I need to - if, for example, I wrote
pub fn get_a() -> impl Map<usize, To = bool> {
A
}
I'd get an error for not implementing the relevant trait, so the type of A::To is known, and the compiler treats two impl Trait as distinct (this is why you can't have a Vec<impl Trait>, from my understanding) - why isn't the associated type propagated?
I guess my question is what about rusts type system doesn't permit get, but permits get_direct
The purpose of impl Trait is to hide implementation details. Anything not explicitly specified in the function signature is treated as unknown when you call it.
This allows you to change the underlying type without it being a breaking change.
Also worth noting, the compiler doesn't take the current implementers of a trait into account. (with the helpful side effect that adding more implementers, or exposing the trait as pub doesn't break existing code)
struct HypotheticalNewC;
impl Map<u64> for HypotheticalNewC {
type To = ();
// ...
}
// now any `impl Map<u64>` does not have a known `To` type
Rust never infers anything in a function signature from the function body, by design. Unless you explicitly constrain the associated type, it’s treated just like, say, an unconstrained type parameter (or indeed an unconstrained associated type of a constrained type parameter). There are very few things you can do with an unconstrained type variable.
I'd add that impl Trait is same as a generic type.
fn my_func() -> impl SomeTrait { .... }
is the same as
fn my_func<T>() -> T
where T: SomeTrait { ... }
or
fn my_func<T: SomeTrait>() -> T { ... }
And as I learned the hard way and with help of the guys here in the forum, generics are defined by the caller, not by the function.
The first example complies because types are told explicitly. In the second one you say my_function<_>() and ask the compiler to guess what you pass to .and(get_b<_>()) which means .and(get_b<I can't tell what type I need>()).
How? Why? The opposite would be a footgun. Not having to specify the associated type would mean that it could silently change without a change in the signature, just based on the body.
Generics and argument-position impl Trait are chosen by the caller, and return-position impl Trait is chosen by the body. The type-theoretic technical term is that generics and APIT are universally-quantified, while RPIT is existential. (In this regard, RPIT is more similar to a trait object, except that opaqueness isn't attained via type erasure but via the compiler intentionally hiding the otherwise statically-known concrete type.)
Generic return types are chosen by the caller. Impl trait return types are chosen by the function body.
For an example of generic return types, see the collect method. You can use it to take an iterator and generate a Vec or HashSet or many other choices. The return type is chosen by the caller.
The Send and Sync traits are an exception here. They leak in a way that nothing else does when using impl Trait (or async fn).
This can actually be a problem for crates that want to be backwards compatible. They could accidentally change whether an async function is Send or not without noticing. It's why Tokio has this test:
If these are an exception then what is the actual rule here? Do these auto-traits always propagate, or only for impl Future’s generated by async? Is there a place these rules are written down?
The actual, full rule is that what you write in the function signature determines all information that's visible to callers. This applies to normal functions, impl Trait return types, async functions, and any other kind of function. The function body never has an effect on the signature in any way.
Except for one exception: For impl Trait in return position and async fn, whether the return type implements an auto trait is inferred from the function body. Currently, the complete list of auto traits is: Send, Sync, Unpin, UnwindSafe, and RefUnwindSafe.
Hmmm, I don’t ask to be argumentative, to be clear, I am more wondering if it’s worth a PR on the rust reference to document the special casing of async fn.
Maybe also in the original case the compiler error could be clearer (something like “the value of A::To is erased due to the signature of get_a. You can avoid this by…” in cases where the erasure has erased a type that would cause the failing bound to be satisfied?
Edit: as an aside, this kind of special casing feels like a bit of a wart on the language, but there’s no real way to fix it with the current syntax I guess.