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
proposal: errors: add All and AllAs iterators #66455
Comments
CC @neild |
Iterating the error tree became substantially more complicated when we introduced multiple-wrapping. Prior to then, you could just call We've got an iterator API now, so it seems reasonable to add an error-tree iteration function. Perhaps As a higher-level comment, however, I believe that any error semantics where you need to iterate over the tree for everything matching a code are too confusing. I's reasonable for the |
See #65428 |
Adding |
Yup, I was thinking exactly that last night. I've updated my PoC CL accordingly.
I'm not so sure. Consider a function F that can return an error with a code as I described. I don't think that's too unreasonable. As another concrete example, and part of the motivation for creating this proposal, the Open Container Initiative (OCI) protocol specifies that errors are returned like this:
That is, it's always possible for an API call to return multiple errors. To me it seems natural to represent this as a single error containing an error for each entry in So although I'd like to agree with your sentiment, I think that there are legitimate cases where it's OK, or even necessary, to expect clients to use the tree-walking function directly. |
@earthboundkid Apologies for duplicating your proposal! Happy to close this as a dupe if you like, although it's a little different so perhaps worth keeping for a bit until a decision is made?
When you say "other helpers", I think there's only one, right? Although it's true that that is technically unnecessary (as I pointed out, |
Change https://go.dev/cl/573357 mentions this issue: |
In the other issue there was some skepticism about how common the need for users to walk the tree themselves is. I think making |
If there wasn't a good use case for users to walk the tree themselves, we wouldn't be proposing adding |
One other thing that's perhaps worth pointing out: I tried a generic version of
It turned out quite a bit more efficient than the regular
I also tried implementing
Unfortunately the performance was considerably worse:
Hopefully the compiler will be able to make that better in time. |
@earthboundkid Apologies for responding to this proposal and not #65428; I must have missed yours when it was created.
I don't think G should return an error that wraps the results of all the individual sub-operations. If the user is likely to care about individual errors, it should return an error that contains the per-operation errors, but does not wrap them. As a concrete example, let's say we have a function that downloads a set of URLs to disk:
If I'd instead either return an
Or define a structured error that contains the individual errors (and does not wrap them):
This makes the structure of the error explicit and is easy to work with.
I would probably represent this as something like:
Usage:
|
@neild I agree that both of your suggestions are viable (although the error message strings implied by your That's a reasonable stance, but given that we have |
When implementing traversing inside my gitlab.com/tozd/go/errors errors package, I realized that there are multiple ways to traverse and that it is really hard to make one function how you want to traverse:
So I think traversing errors should have API closer to walking directory structure with a function callback and not an iterator. In the case of errors, the directory is an joined error. At that point you have to make a decision how you want to continue iterating and if at all. Instead of files, you can have a chain of parent errors until the joined error, or something. |
Here is an observation, that is IMO interesting, but not terribly actionable: When the The problem @rogpeppe is now running into is thus the diamond problem of multiple inheritance: There are multiple paths along which his Ultimately, these design problems around the That's not actionable, of course. The API is, what it is and we must deal with it, pragmatically. But it describes and predicts the scope of the problems. |
Depth-first. (Why depth-first? We needed to pick one, and when we added error wrapping to
An iterator is a function that takes a callback; see #61897. |
The proposal defines
I remember there being a reason |
I think it's probably because not all interfaces that we might be interested in inspecting the error for necessarily implement I suspect that consideration was amplified by the fact before Go 1.14, issue #6977 meant that it generally wasn't a good idea to embed interfaces because that ran the risk of conflicts with other types doing the same thing. In general, I think I agree with you and support using There's one thing that gives me pause (and was the reason I chose |
For your particular use case (a package deeply involved with the core error implementation), I'd suggest that you'd be best off just writing your own tree-traversal function. Most users will not be in that situation. Both of your other points are easily implemented using the proposed API. FWIW I have also considered whether it might be worth adding an API like this:
but on balance I thought this was too trivial to be worthwhile.
You might be amused to see that I've actually proposed an API for walking a directory structure with an iterator: #64341 |
In there a benefit in making the constraint |
Making the constraint |
The current proposal as I understand it:
Is that right? |
I asked ChatGPT about that and it made a good argument: By accepting interface{} as the target, errors.As allows you to check if an error implements any interface, providing greater flexibility and utility in type assertion scenarios. So you maybe have your errors implement an interface which does not embed |
That's a great example of ChatGPT saying something that looks useful, but really isn't. Yes, you can check for a
But you also write that with:
So this isn't about flexibility or utility, but maybe it's about convenience. I still don't recall whether there was one motivating example that informed the decision on On one hand, we could say that the same argument applies to On the other hand, we could say that type parameters allow us a greater degree of compile-time checking than was available when we added I lean somewhat towards a constraint of |
I agree. The only slight friction to my mind is the kind of scenario demonstrated here: In this case there's no way to get |
This proposal has been added to the active column of the proposals project |
Proposal Details
The original error inspection proposal contained the following paragraph:
Traversing the error chain multiple times to call
Is
is wasteful,and it's not entirely trivial to do it oneself. In fact, the way to
iterate through errors has changed recently with the advent of
multiple error wrapping.
If many people had been traversing the error chain directly, their
code would now be insufficient.
If we're checking for some particular code in some error type, it's
tempting (but wrong) to write something like this:
The above code is wrong because there might be several errors in the
error tree of type
*errorWithCode
, but we will only ever see thefirst one. It would be possible to abuse the
Is
method to consideronly the
Code
field when comparingerrorWithCode
types, but thatseems like an abuse:
Is
is really intended for identical errors,not errors that one might sometimes wish to consider equivalent.
With the advent of iterators, we now have a natural way to design an
API that efficiently provides access to all the nested errors without
requiring creation of an intermediate slice.
I propose the following two additions to the
errors
package:Technically only
IterAs
is necessary, becauseIterAs[error]
is entirely equivalent toIter
, butIter
is more efficient andIterAs
is easily implemented in terms of it.Both
Is
andAs
are easily and efficiently implemented in terms of the above API.I consider
IterAs
to be worthwhile because it's convenient and type-safe to use, and it hides the not-entirely-trivial interface check behind the API.The flawed code above could now be written as follows, correctly this time, and slightly shorter than the original:
I've pushed a straw-man implementation at https://go-review.googlesource.com/c/go/+/573357
The text was updated successfully, but these errors were encountered: