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

Tracking issue for error source iterators #58520

Open
sfackler opened this issue Feb 16, 2019 · 19 comments

Comments

@sfackler
Copy link
Member

commented Feb 16, 2019

This covers the <dyn Error>::iter_chain and <dyn Error>::iter_sources methods added in #58289.

@withoutboats

This comment has been minimized.

Copy link
Contributor

commented Feb 16, 2019

My preference before stabilizing would be to make changes to make it more like the original failure design, which I still prefer. There should be one iterator, it should begin from this error type, and its name should be sources. If you want all the sources of error less this layer, you can write .skip(1).

@TimDiekmann

This comment has been minimized.

Copy link
Contributor

commented Feb 16, 2019

I like the idea of a convenient interface to iterate sources. However, the interface feels unergonomic as mentioned in #58289 (comment). Additionally I think this should not be stabilized before #53487

@haraldh

This comment has been minimized.

Copy link
Contributor

commented Feb 18, 2019

My preference before stabilizing would be to make changes to make it more like the original failure design, which I still prefer. There should be one iterator, it should begin from this error type, and its name should be sources. If you want all the sources of error less this layer, you can write .skip(1).

Yeah, my first iteration (pun) had only .iter(), but then @sfackler requested the other iter_*()

@faern

This comment has been minimized.

Copy link
Contributor

commented Mar 29, 2019

These iterators are really good. I have long relied on the display_chain method in error-chain. As I move away from that lib I find it cumbersome to transform my entire chain of errors into a good string representation.

With these iterators I can get the equivalent of display_chain this way:

let display_chain: String = error
    .iter_sources()
    .fold(format!("Error: {}", error), |mut s, e| {
        s.push_str(&format!("\nCaused by: {}", e));
        s
    });

These iterators does not solve the entire problem. But they make it a bit easier to implement.

@haraldh

This comment has been minimized.

Copy link
Contributor

commented May 31, 2019

So, any changes needed here? I really want this to be stabilized

@withoutboats

This comment has been minimized.

Copy link
Contributor

commented Jul 11, 2019

I think a few changes are needed before this can be stabilized:

  1. First, we need to actually reach a consensus about whether we have both of these APIs. I've been consistent that I think the changes made to failure regarding this issue since I stopped maintaining it are a mistake: we should have one iterator which begins with self and calls cause until it returns None. If users want to skip the self value, its just as simple as adding .skip(1) to that iterator. No one else has really expressed an opinion about that question.
  2. Even if we do have both, I think the names right now are not well-chosen. It would be better to change them to something like .errors() and .sources() (or keep one and just call it one of these two names). We tend to avoid the name iter except for iterating through elements of a collection, modified by its ownership type; for iterators which are semantic values pulled out of a type, we prefer just using the name of that kind of value (as in lines, keys, chars, etc).
  3. We need to decide what to do about the trait object vs provided method problem, discussed on the PR. This involves the struggle around the fact that Box<Error> doesn't implement Error. I think the best solution available to us now is to just have the method repeated in both places.
@derekdreery

This comment has been minimized.

Copy link
Contributor

commented Jul 14, 2019

my 2¢: I think the simplest (for users) implementation is a single iter method, with a note in the documentation that

  1. The iterator length is always >= 1, that is, you can call iter.next().unwrap() once without causing a panic.
  2. If you just want the sources, use e.iter().skip(1).
  3. An example like
    fn print_error(e: impl Error) { // or Box<Error> or whatever other actual type this is implemented for
      let mut chain = e.iter();
      eprintln!("error: {}", chain.next().unwrap());
      for source in chain {
        eprintln!("caused by {}", source);
      }

This leaves the option open of an iter_sources method later if it's decided that it's really needed (that can just be implemented as iter().skip(1)).

@BurntSushi

This comment has been minimized.

Copy link
Member

commented Jul 14, 2019

I agree with @withoutboats and @derekdreery. I had to carefully read the docs for each method before I figured out how they differed. Moreover, I suspect it will be quite difficult to pick names for both methods that are easy to remember and distinguish. Having one method and requiring the caller to use skip(1) if they don't want the current error seems much nicer IMO.

@withoutboats

This comment has been minimized.

Copy link
Contributor

commented Jul 14, 2019

It does seem that many users found the terminology causes confusing for an iterator that returns self (myself, it seemed intuitive, if the documentation says so, that this error can be considered a member of the chain of causality). So a better name than sources might be preferred here. However, both of the other options I can think of are also imperfect:

  • I think iter violates our naming convention, in that I think iter must return the same iterator returned by <&T as IntoIterator>::into_iter, and I don't expect that impl to exist for error types.
  • Error::errors does not seem great either, just because it can be confusing to say that this error contains many errors.

As I said previously, I don't think names like iter_chain are a good choice either, they're pretty far outside of our general style for iterator names.

@faern

This comment has been minimized.

Copy link
Contributor

commented Jul 14, 2019

I agree causes/sources would be confusing if the iterator contains the error it's called on. In err.causes() I want the errors that caused err. err did not cause itself, nor is it part of the sources for this error. It would not be consistent with the existing err.source() which returns the first error under err not err itself.

I do fully agree that err is part of the chain of causality however, which is why I think something like chain would work.

@derekdreery

This comment has been minimized.

Copy link
Contributor

commented Jul 15, 2019

@withoutboats

I think iter violates our naming convention, in that I think iter must return the same iterator returned by <&T as IntoIterator>::into_iter, and I don't expect that impl to exist for error types.

Is this set in stone. I think iter is the best name, because it feels familiar, in that the iter() method should return an iterator that makes most sense for the object. I wouldn't expect FromIterator to be implemented for &Error, but if it were to be implemented then it should look like this (i.e. include the base error).

Could the naming convention be changed to "iter must return the same iterator returned by <&T as IntoIterator>::into_iter, or if this is not implemented, it must return the canonical iterator for the object (implying that such a canonical iterator must exist)".

@derekdreery

This comment has been minimized.

Copy link
Contributor

commented Jul 15, 2019

To give some more context on why I would like it to be called iter:

When I'm reading the documentation for some object with methods, when I see an iter method I think to myself "what iterator makes most sense for this object". So for a vector I would assume it returns refs to the elements, or for a linked list (which is what our error chain is essentially), I would expect it to walk the nodes and return a node per iteration. I don't think "how is FromIterator implemented for the reference type for this object". Maybe I should be thinking that :P

@haraldh

This comment has been minimized.

Copy link
Contributor

commented Jul 30, 2019

I think iter violates our naming convention, in that I think iter must return the same iterator returned by <&T as IntoIterator>::into_iter, and I don't expect that impl to exist for error types.

Hmm, this works:

impl<'a> IntoIterator for &'a (dyn Error + 'static) {
    type Item = &'a (dyn Error + 'static);
    type IntoIter = ErrorIter<'a>;

    fn into_iter(self) -> Self::IntoIter {
        ErrorIter {
            current: Some(self),
        }
    }
}
let mut iter = <&(dyn Error + 'static) as IntoIterator>::into_iter(&err);

or

for e in err.borrow() as &(dyn Error) {
    eprintln!("{}", e);
}

and iter_chain() (or iter() how I would call it) is implemented for dyn Error

@withoutboats

This comment has been minimized.

Copy link
Contributor

commented Aug 1, 2019

@haraldh We can add that impl, but I don't think we should (and it seems completely circular to add it just to justify naming the method iter)

@haraldh

This comment has been minimized.

Copy link
Contributor

commented Aug 1, 2019

@haraldh We can add that impl, but I don't think we should ...

Agreed

@teiesti

This comment has been minimized.

Copy link
Contributor

commented Sep 4, 2019

I'd like to point to the issues discussing the addition (#48420) and stabilization (#50894) of std::path::Path::ancestors. At first glance this API might seem unrelated but the situation back then was quite similar:

  1. The objective was to add an iterator that recursively calls an existing, well-known method.
  2. There was a discussion if self should be included.
  3. There was a lengthy discussion about the name.

In the end, the rationale was:

  1. Such iterators are helpful. They should better be stabilized sooner than later.
  2. self should be included. It is easy to .skip(1) it. Not including self is harmful because it is harder to add self to the iterator than to remove it. (I think, it is not a good idea to add two APIs just to cover both semantics because that will clutter both, the namespace and the human mind.)
  3. The chosen name should be telling and reflect the fact that self is included. Adding an "s" to the original method name is a good to start unless that leads to unwanted associations which, I think, is often the case. (I think, .iter() is not a good choice, because it is natural to iterate over vector items but it is not natural to iterate over error sources. In theory, there may be other properties of an Error that are a more or equally natural subject to iteration, e.g., descriptions provided in different languages. In particular, a type implementing Error might also want to implement a method called .iter() for whatever reason.)

I'd like to throw two naming ideas into the ring that might be considered for further debate:

  • .ancestors(), because the iterator iterates over the ancestors in a ancestry of errors which are cause to their respective children.
  • .chained() in honor of error-chain and because the iterator iterates over the chain of errors that is somehow included in self.

I'd also like to propose to rename the ErrorIter struct. I think, it is good practice throughout the standard library to name the struct after the method creating it (example).

@ehuss

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2019

Sorry if this was brought up earlier, I tried to read the RFC/issues, and didn't see it brought up.

What is the intent of how to handle older code that overrides cause but not source? For example, IntoStringError only implements cause. If you iterate over sources (iter_sources), then the cause is lost. In this case it is the Utf8Error which provides useful detail like invalid utf-8 sequence of 1 bytes from index 0 .

Should these errors be changed to implement source instead of cause? Or maybe implement both?

@faern

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2019

IMO all new errors should only implement source. All old code that want to stay relevant and usable should be updated to implement source as well. I do not think we should focus the development of new features in libstd to cover deprecated items.

@derekdreery

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2019

IMO all new errors should only implement source. All old code that want to stay relevant and usable should be updated to implement source as well. I do not think we should focus the development of new features in libstd to cover deprecated items.

If we were to do this, could we make an effort to submit PRs to popular crates using cause.

More generally (and slightly OT), is there a mechanism for the community to help crate maintainers with stuff like this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants
You can’t perform that action at this time.