Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconsider C-STRUCT-BOUNDS #217

Open
Kixunil opened this issue Apr 3, 2020 · 1 comment
Open

Reconsider C-STRUCT-BOUNDS #217

Kixunil opened this issue Apr 3, 2020 · 1 comment
Labels
amendment Amendments to existing guidelines

Comments

@Kixunil
Copy link

Kixunil commented Apr 3, 2020

First let me say that C-STRUCT-BOUNDS talks about derived bounds mainly, which is completely fine, just a bit confusing (as the text down below speaks about bounds in general). I'd like to see a more visible distinction between the two, but that's not my main point here.

I suggest adding a new execption: "when a struct is supposed to wrap a type implementing a specific trait" - that means specialized wrappers for Iterator, Future, Stream, Read, Write etc. Reason: if the user attempts to instantiate the type without generic implementing the trait, he hits the error somewhere else, which is confusing and causes long error messages.

Requiring the trait will cause the error sooner, pointing at the correct location.

As an alternative, one might omit the bounds from struct itself and put them on every constructing function instead. This works until the crate author forgets about one case and exactly that case will be experienced by the user. (Murphy law: Everything that can break will break.)

#6 reasoned that it's annoying, but since generics are just type-level functions, it's equally annoying as having to write types in function signatures, which was very reasonably considered as a good thing.

@Kixunil
Copy link
Author

Kixunil commented Nov 2, 2020

Let me clarify this to make the rules clearer and maybe more obvious.

Terminology:

A type is considered Trait wrapper if:

  • it's generic
  • it implements Trait
  • nobody would write it if Trait didn't exist

A type is multi trait wrapper if

  • it's generic
  • implements TraitA, TraitB ...
  • nobody would write it if none of TraitA, TraitB ... existed

By "nobody would write it" I mean that there's no good reason to write it.
In other words, if S<T> is a generic type and rewriting it as type S<T> = T; would not remove any functionality besides the traits it wraps (and constructors), then nobody would write it without those traits existing.

I propose that:

  • All Trait wrappers have bounds on the type definition.
  • All multi trait wrappers have at least one constructor for each trait with that trait in the bound.

Examples:

#[Derive(Clone, Debug)]
struct IterFilter<I, F> where I: Iterator, F: FnMut(&I::Item) -> bool {
// ...
}

impl<T, F> Iterator for IterFilter<T, F> // ...

This is a combinator analogous to iterator.filter(). If the Iterator trait didn't exist, then this type would not provide any useful functionality. Deleting it and rewriting all occurrences to T would not change the behavior of the program. (As the only thing that it can do is clone and print itself. Debug impl would change but that's not really interesting/relevant.)

As we can see this type also impls Clone and Debug (via derive) but it'd still be useful without them existing.

Multi trait wrapper example:

// Can be used for Read, Write or both
struct Buffer<T> {
    io: T,
    read_buf: Vec<u8>,
    writ_buf: Vec<u8>,
}

impl<T: Read> Buffer<T> {
    fn new_reader(reader: T) -> Self {
        // ...
    }
}

impl<T: Write> Buffer<T> {
    fn new_writer(writer: T) -> Self {
        // ...
    }
}

impl<T> Read for Buffer<T> where T: Read // ...
impl<T> Write for Buffer<T> where T: Write // ...

This type provides useful functionality if Read exists but Write does not; if Write exists but Read does not; if both Read and Write exists.
If neither Read not Write exist, the type is completely useless.
Making sure that Buffer<T> can't be constructed if T doesn't impl Read nor Write makes sure user is informed about the fact at the point where it's constructed, not later where it can be confusing.

However, consider this alternative:

// Can be used for Read, Write or both
struct ReadBuffer<T> where T: Read {
    io: T,
    read_buf: Vec<u8>,
}

impl<T: Read> Buffer<T> {
    fn new(reader: T) -> Self {
        // ...
    }
}

impl<T> Read for Buffer<T> where T: Read // ...
impl<T> Write for Buffer<T> where T: Write // ...

This type implements buffering for readers only, not for writers. It additionally delegates writes if the inner type is a writer but does not buffer them.

Such type is still useless without Read existing because it can be replaced with T without loss of functionality.

I hope this explanation and the examples make the idea and reasons behind it more clear. I'll be happy to clarify if something is still not obvious.

@KodrAus KodrAus added the amendment Amendments to existing guidelines label Dec 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
amendment Amendments to existing guidelines
Projects
None yet
Development

No branches or pull requests

2 participants