6

I was playing around with some API concepts and noticed something peculiar in Rust's Iterator trait.

I have the following trait definition:

trait Observable {
    type Item;

    fn subscribe<F>(self, f: F) -> bool
    where
        Self: Sized,
        F: FnMut(Self::Item) + 'static;
}

I then proceeded to write the following test:

#[test]
#[should_panic]
fn trait_obj() {
    let mut v: Vec<Box<dyn Iterator<Item = ()>>> = vec![];
    let mut v2: Vec<Box<dyn Observable<Item = Ref<u8>>>> = vec![];

    v.remove(0).for_each(|_| {});
    v2.remove(0).subscribe(|_| {});
}

The above test does not compile, as one would expect; subscribe() takes self by value, and has a Sized constraint on Self, therefore is not object safe. However, if I comment out the ...subscribe line, it does compile!

The odd thing to me is, Iterator::for_each() has the same constraints. Why is this allowed for Iterator and not for Observable? Is it an experimental feature that enables this?

Here is the function signature of Iterator::for_each for reference:

// Iterator::for_each
fn for_each<F>(self, f: F)
where
    Self: Sized,
    F: FnMut(Self::Item);

The function signatures for Iterator::for_each and Observable::subscribe are pretty much identical.

What gives?

Boann
  • 48,794
  • 16
  • 117
  • 146
Jose Quesada
  • 417
  • 3
  • 6

2 Answers2

6

Your mistake is that you think you call <dyn Iterator<Item = ()>>::for_each(), and then you wonder rightfully how you can if for_each() requires Self: Sized but dyn Iterator<Item = ()> is obviously not Sized. But you are wrong. And you can see that if you'll use UFCS (Universal Function Call Syntax):

#[test]
#[should_panic]
fn trait_obj() {
    let mut v: Vec<Box<dyn Iterator<Item = ()>>> = vec![];

    // v.remove(0).for_each(|_| {});
    <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
}

Playground.

Emits:

error[E0277]: the size for values of type `dyn Iterator<Item = ()>` cannot be known at compilation time
   --> src/lib.rs:7:5
    |
7   |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    |
    = help: the trait `Sized` is not implemented for `dyn Iterator<Item = ()>`
note: required by a bound in `for_each`

error[E0308]: mismatched types
 --> src/lib.rs:7:41
  |
7 |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
  |                                         ^^^^^^^^^^^ expected trait object `dyn Iterator`, found struct `Box`
  |
  = note: expected trait object `dyn Iterator<Item = ()>`
                   found struct `Box<dyn Iterator<Item = ()>>`
help: consider unboxing the value
  |
7 |     <dyn Iterator<Item = ()>>::for_each(*v.remove(0), |_| {});
  |                                         +

error[E0277]: the size for values of type `dyn Iterator<Item = ()>` cannot be known at compilation time
 --> src/lib.rs:7:41
  |
7 |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
  |                                         ^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `dyn Iterator<Item = ()>`
  = note: all function arguments must have a statically known size

And this error also hints you why the previous version worked: you didn't call <dyn Iterator<Item = ()>>::for_each(), you called Box::<dyn Iterator<Item = ()>>::for_each()! Box<Iterator> implements Iterator itself, and so it worked. You can see that explicitly in the MIR:

v.remove(0).for_each(|_| {});
// Snip
_2 = <Box<dyn Iterator<Item = ()>> as Iterator>::for_each::<[closure@src/lib.rs:4:26: 4:32]>(move _3, move _5) -> [return: bb3, unwind: bb5];
// Snip

Playground (choose "Show MIR" from the menu).

If you has had implemented Observable for Box<O> where O: Observable, it would work for you too...

...Except you cannot. Because you cannot forward the call to for_each(). The reason it works with Iterator is that it does not forward this call, and rather uses the default implementation that calls next() again and again. And because next() takes &mut self, it doesn't require Self: Sized.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Hmm, is there a reason that there isn't a blanket `impl> T for D`? That would avoid needing to write out the obvious implementation of various traits on the std lib smart pointers. – BallpointBen Apr 11 '22 at 17:04
  • @BallpointBen Rust has no way to express genericness over a trait, and this impl is also a rude violation of the orphan rules. – Chayim Friedman Apr 11 '22 at 21:45
  • I disagree on the second point -- it seems silly that implementing Deref gets you all the individual methods of Target, but not the traits those methods were implemented to satisfy. The whole point of Deref is to be more or less a transparent wrapper around Target. – BallpointBen Apr 11 '22 at 21:55
  • @BallpointBen I was talking about what is possible, not whether this is desirable. But I also disagree, and I think this is exactly _not_ the point of `Deref` (and this is the reason why, for example, implementing `Deref` for newtypes is not a good idea). [`Deref` denotes a (smart) pointer](https://doc.rust-lang.org/stable/std/ops/trait.Deref.html). That's it. And pointer supports the dereference operator, and again, that's it. The fact that Rust sometimes dereferences automagically doesn't mean `Deref` is a wrapper of any kind. – Chayim Friedman Apr 11 '22 at 22:10
  • @BallpointBen Adding this impl now, even if it will became possible, is also a breaking change. – Chayim Friedman Apr 11 '22 at 22:11
  • @BallpointBen Also, not all trait are implementable using just `Deref`: some require `self`, some have associated types/consts/functions (static methods), some include parameters of type `Self`... – Chayim Friedman Apr 11 '22 at 22:13
0

A similar question was asked over on the Rust forum. To summarize, the full signature for Iterator::for_each is

fn for_each<F>(self, f: F)
    where
        Self: Sized,
        F: FnMut(Self::Item);

It's the Self: Sized part that's key. That means "this function can only be called if Self has a known size", so for_each is explicitly disallowed on trait objects of type dyn Iterator. This is true of several of the methods on Iterator and is Rust's way of getting the best of both worlds: A trait that can still be used as an object but that has additional functionality available when it's being used statically.

I'm not sure why the Self: Sized is hidden from the Rustdoc page; you actually have to click "View Source" to see it, and that seems like an oversight to me.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 1
    If you see the test I wrote, I can call `for_each` on a trait object, which you mentioned should not be allowed, which I agree with. But this doesn't answer the question as to why it _is_ allowed. – Jose Quesada Apr 11 '22 at 01:00