Why doesn't information like Send "leak" about associated types through RPIT functions?

Return position impl Trait happily allows details about the actual concrete implementation to leak to users. For example, even if we return only impl Any we can tell whether the actual type implements Send or not (playground):

fn check_send<T: Send>(t: T) {}

fn foo() -> impl Any {
    0
}

fn bar() -> impl Any {
    fn make_mutex_guard() -> MutexGuard<'static, ()> {
        todo!();
    }

    make_mutex_guard()
}

fn baz() {
    check_send(foo());

    // Doesn't compile:
    // check_send(bar());
}

I've always thought this "leaking" of information was a little weird, but I could believe it's necessary to make other language features work harmoniously. However when I tried to lean into it, I found that it doesn't work for types associated with the anonymous implementation returned (playground):

trait SomeTrait {
    type AssocType;

    fn make_assoc_type(&self) -> Self::AssocType {
        todo!();
    }
}

impl SomeTrait for () {
    type AssocType = i32;
}

fn foo() -> impl SomeTrait {
    ()
}

fn baz() {
    check_send(foo().make_assoc_type());
}
error[E0277]: `<impl SomeTrait as SomeTrait>::AssocType` cannot be sent between threads safely
  --> src/lib.rs:24:16
   |
24 |     check_send(foo().make_assoc_type());
   |     ---------- ^^^^^^^^^^^^^^^^^^^^^^^ `<impl SomeTrait as SomeTrait>::AssocType` cannot be sent between threads safely
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `Send` is not implemented for `<impl SomeTrait as SomeTrait>::AssocType`
note: required by a bound in `check_send`

Why is this treated differently? Why does the leaking of this information only extend for one level? It's not as if the compiler doesn't know the real, concrete associated type here, because I can e.g. allocate space for it on the heap.

1 Like

I always considered it a bug that it's now impossible to fix because of backward compatibility.

Give the fact that RPIT is kinda-sorta weird addon, this sounds plausible: I've seen many strange warts with it (most in error messages).

Because there are no bugs, in that case?

IMHO it appears rather fixable. It would just need a syntax to manually specify the Send bounds extra difficulty arises in that those bounds must be able to express conditionals, e.g. “return type is Send iff type parameter T is Send, and Foo<T> is Sync, or things like that… and it should better not need to talk about private types”. Then an edition boundary could even make the explicit notation required. (If that’s not generally desired, one could still offer a lint that can be used to forbid the inference, and tooling to help fix missing annotations and/or outdated annotations.)

It’s definitely not literally a “bug” of course, in that it’s deliberately specified and discussed in the original RFC – 1522-conservative-impl-trait - The Rust RFC Book (search for “OIBIT” to find all relevant places it’s discussed — OIBIT=“opt-in builtin trait” is an old name for auto-traits).

2 Likes

In this case I want the information to escape, so that I don't have to add a Send bound to the associated type, so that my trait can be agnostic to whether it's Send or not (ultimately because this issue is such a pain in the neck). Is there any way to emulate that behavior?

Make the associated type an impl Trait too.

-fn foo() -> impl SomeTrait {
+fn foo() -> impl SomeTrait<AssocType = impl Sized> {
     ()
 }
4 Likes

This is cursed.

@quinedot: wow, thanks. Are you able to give any detail on that? Like, is it guaranteed in some RFC that this should work, an accident of implementation that is likely to change, etc?

As per the RFC @steffahn linked, opaques leaking auto traits is intentional, and changing that (without some edition gated alternative) would be a breaking change.

This part I don't have a citation for, so can only say that the leakage has to stop somewhere for opaques to keep their "I can change most implementation details" benefits (from the function writer's perspective).

(There are probably technical and practical barriers too, but I'd be mostly speculating if I wrote out my thoughts.)

1 Like

I see, and I guess the impl Sized is just another opaque.

Anyway, do you know how to make this workaround apply more generally, including with generic associated types? I can't figure out a syntax to make it work for this more realistic example of what I'm actually trying to do.

Just add the Send bounds: Rust Playground

The whole point was to simulate a tiny but of duck-typing, lol. And handle both Send and not Send cases with the same code.

I wonder why, though: in all other cases Rust consciously made things as painful as possible for the generic writers to help generic users… I wonder what this particular case warranted an exception.

There's no direct way to write bounds like

// Like an unconditional bound on the GAT (not what you want)
for<'a, A where A: 'a + Future<Output =  ()>> Stream<ConsumerFuture<'a, A>: Send>

// A bound conditional on `A` (like `()`s implementation)
for<
    'a,
    A,
    where A: 'a + Future<Output =  ()> + Send
>
    Stream<ConsumerFuture<'a, A>: Send>

which is what I think of when I imagine a direct fix for your playground.


Assuming the goal is to find a fix by changing foo without changing the rest, one approach would be to use a nominal struct instead of an opaque.

#[non_exhaustive]
pub struct FooStream {}

impl Stream for FooStream {
    type ConsumeFuture<'a, A: Acceptor>
        = impl Future<Output = ()>
    where
        A: 'a;

    fn consume<'a, A: Acceptor>(self, acceptor: &'a mut A) -> Self::ConsumeFuture<'a, A> {
        ().consume(acceptor)
    }
}

fn foo() -> FooStream {
    FooStream {}
}

Now you don't have to write the bounds, and the impl Future is exposed to callers of foo. You maintain some opaqueness via the implementation (and private fields -- probably this is really a struct FooStream(SomeConcreteInnerType)?).

Yeah, exposing a name for the result type seems to be the only way to do what I want. I had hoped to avoid that because it clutters the public API, but then again it seems pretty common in widely-used crates like futures. Is it because of this issue, or is there some other reason that people like that style?

Returning nameable types is nicer for consumers, who might want to have the return type as a field or whatever. I hope std continues this pattern for iterator combinators for example.

That said, I don't know if that's the actual motivation :slightly_smiling_face:.

1 Like