Should I put where bounds on the `struct` or on the `impl`?

In the standard library and in the futures create, there are a number of adapters, like map. They are interesting in that they place bounds like where F: FnOnce(A::Item) -> U not on the struct, but on the impl.

Why is that?

My intuition says that these bounds belong to the corresponding struct. On the other hand, I've heard somewhere that Haskell guys used to have bounds on data, which were removed in favor on bounds on instance.

So, what exactly is the difference between these two places for bounds? Which is preferable and why?

3 Likes

I tend to prefer on the struct, because if you're able to make a malformed object without some expected bounds, then the errors when you try to use it get confusing. I want errors to be reported as close to the source as possible. In this case though, Future::map requires that bound up front, so I don't think it will be an issue.

I prefer the convention used in std of placing no bounds on the struct, because those bounds become contagious and infect all other structs that try to contain one as a member.

In the case of std::iter::Map and friends, it is impossible to construct an invalid map anyways so long as appropriate bounds are placed on Iterator::map. It is true however that the compiler will not be able to enforce that these bounds are 100% consistent.

Edit: Actually I am somewhat mistaken about std's conventions. For instance std::iter::Peekable has a I:Iterator bound.
Edit 2: Actually in that case, it seems it is simply to dodge a second T parameter by allowing I::Item to be used in the struct declaration.

1 Like

IMO the struct is just data and requires no bounds at all. Only operations on that data require bounds.

Different impls on the same struct might require different bounds. And adding the least restrictive bound on the struct itself would preclude future impls with even less restrictive bounds.

Consider e.g. in std::io:

impl<T: Read> Read for Take<T>

but:

impl<T: BufRead> BufRead for Take<T>

Now let's just pretend that the BufRead trait existed before the Read trait. If the bound T: BufRead were on the struct itself, that would preclude adding the Read trait with only a Read bound.

1 Like

I try to avoid putting them on the struct. My experience has been that when I have a lot of highly generic code, putting bounds on the struct results in having to add a lot of additional bounds that are not actually necessary for this code. Avoiding them makes these other bounds a lot more precise and easier to understand. In practice, every valid type parameter will implement those traits, but its not relevant to this particular function.

On the other hand, if your code is not "highly" generic (and this is a value judgment of course), and so you don't have a lot of intertwined bounds, having the bound on the struct can make it easier to understand your intent for this struct's use case.

3 Likes

I believe, though, there are some cases when you do need bounds on the struct:

trait MyTrait {
    type Thing;
}

struct MyData<T: MyTrait> {
    a: T,
    b: T::Thing
}
2 Likes

True!

This is just like Peekable actually.