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

proposal: errors: expose Iser and Aser #39539

Closed
carnott-snap opened this issue Jun 11, 2020 · 19 comments
Closed

proposal: errors: expose Iser and Aser #39539

carnott-snap opened this issue Jun 11, 2020 · 19 comments

Comments

@carnott-snap
Copy link

carnott-snap commented Jun 11, 2020

The interfaces for errors.Is and errors.As seem stable and complete. Unfortunately, it requires reading through the godoc and use of a custom type to ensure that your custom error type implements interface{ As(interface{}) bool } or interface{ Is(error) bool }.

I propose exposing the following symbols:

package errors

// Aser is the interface implemented by types that can be converted into other types.
//
// See As for full usage.
type Aser interface{ As(interface{}) bool }

// Iser is the interface implemented by types that are comparable to other errors.
//
// See Is for full usage.
type Iser interface{ Is(error) bool }

And fixing up errors.Is and errors.As to document and use them:

--- a/src/errors/wrap.go
+++ b/src/errors/wrap.go
@@ -27,7 +27,7 @@
 // repeatedly calling Unwrap.
 //
 // An error is considered to match a target if it is equal to that target or if
-// it implements a method Is(error) bool such that Is(target) returns true.
+// it implements Iser such that Is(target) returns true.
 //
 // An error type might provide an Is method so it can be treated as equivalent
 // to an existing error. For example, if MyError defines
@@ -46,7 +46,7 @@
 		if isComparable && err == target {
 			return true
 		}
-		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
+		if x, ok := err.(Iser); ok && x.Is(target) {
 			return true
 		}
 		// TODO: consider supporting target.Is(err). This would allow
@@ -65,9 +65,9 @@
 // repeatedly calling Unwrap.
 //
 // An error matches target if the error's concrete value is assignable to the value
-// pointed to by target, or if the error has a method As(interface{}) bool such that
-// As(target) returns true. In the latter case, the As method is responsible for
-// setting target.
+// pointed to by target, or if the error implements Aser such that As(target)
+// returns true. In the latter case, the As method is responsible for setting
+// target.
 //
 // An error type might provide an As method so it can be treated as if it were a
 // a different error type.
@@ -92,7 +92,7 @@
 			val.Elem().Set(reflectlite.ValueOf(err))
 			return true
 		}
-		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
+		if x, ok := err.(Aser); ok && x.As(target) {
 			return true
 		}
 		err = Unwrap(err)

This allows error implementers to ensure that the interfaces are implemented correctly, while also cleaning up errors documentation and internal logic:

var _, _, _ = errors.Iser(myError{}), errors.Aser(myError{}), error(myError{})

That being said, the names Iser and Aser sound like fantasy characters and are not strictly required, since they do not exist today and errors.Is and errors.As work fine. Furthermore, I cannot think of a reason to pass, accept, or typecast these interfaces, so their usefulness is limited. That being said, json.Marshaler suffers from similar issues, yet it exists.

@gopherbot gopherbot added this to the Proposal milestone Jun 11, 2020
@ianlancetaylor
Copy link
Member

Any package could define those interfaces itself, of course. I'm not sure how compelling it is to define them in the standard library. I would not expect many packages to use them.

@carnott-snap
Copy link
Author

While I agree that any package can define these symbols, exporting them from errors makes for a formal contract that is verifiable at compile time. For me, this is one of the main benefits of using a compiled language.

@rsc
Copy link
Contributor

rsc commented Jun 24, 2020

Simply implementing the interfaces seems like not enough if you are checking correctness. If you write a test, you'll check the whole end-to-end implementation, including the method signatures.

One thing we could do is update the cmd/vet check to check the signature of an Is or As method on a type implementing error, like we do in the 'stdmethods' check for things like WriteByte.

Both of those would seem better than exporting Iser/Aser.

@carnott-snap
Copy link
Author

carnott-snap commented Jun 24, 2020

I missed this before, but xerrors exports Wrapper; was this omitted for similar reasons? Seems like we should add it, if we are doing the other two.

While I agree such end-to-end tests are better, var _ xxx.Interfacer = Type{} checks are usually sufficient, easier, and more concise. I dislike the concept of having these magical interfaces exist only in the docs, and internal implementation logic, because typos happen. Plus the standard library stance seems unclear, few if any Xxxer interfaces seem required, and we are opposed to this here, but in other packages it is fine, say json.Marshaler.

@rsc
Copy link
Contributor

rsc commented Jul 8, 2020

I'm pretty sure we didn't copy Wrapper over because all code should use errors.Unwrap. There's little need for it once you have Unwrap. We could make the same vet check check an Unwrap method on an error implementation too.

It sounds like people are generally in favor of the vet check instead of exposing interfaces? Do I have that right?

@carnott-snap
Copy link
Author

carnott-snap commented Jul 8, 2020

While I dislike that these interfaces only exist in documentation, the vet check would serve the same purpose as var _ ... with lower user impact. To confirm, types that implement error that contain methods with the given name must meet the following interface:

  • Is: interface{ Is(error) bool }
  • As: interface{ As(interface{}) bool }
  • Unwrap: interface{ Unwrap() error }

Are we concerned with collisions, since these words are not use case specific, like json.Marshaler, interface{ MarshalJSON() ([]byte, error) }? Maybe it would have been good to have named these interfaces XxxError.

@rsc
Copy link
Contributor

rsc commented Jul 15, 2020

Are we concerned with collisions,

Not really, because the check would only happen if you also implement Error() string. At that point we know it's an error. The method being named IsError wouldn't clarify much.

Based on the discussion above, it sounds like this is a likely decline.

@seankhliao
Copy link
Member

Can this be exported for documentation purposes. Like how json.Unmarshaler is exported even though almost no one will use it directly. Hunting for the method signature in the errors.Is doc comments isn't very nice and having it exported will make it easier to point people to

@as
Copy link
Contributor

as commented Jul 16, 2020

I think exporting a named interface weakens the abstraction, as the name of the interface matters in some cases unlike its anonymous counterpart (there was an issue about this, but I can't find it).

Also, errors.Iser and errors.Aser are not great names, but I can't think of anything better, since errors.As and errors.Is are already used in that package.

@rsc
Copy link
Contributor

rsc commented Jul 22, 2020

No change in consensus, so declined.

@carnott-snap
Copy link
Author

Would you like me to submit a different proposal for the vet check, or does it make more sense to transmute this one?

@ianlancetaylor
Copy link
Member

@carnott-snap I think a different proposal would be better. Thanks.

@carnott-snap
Copy link
Author

@rsc: closing this one as well, did the process change, or were these two just missed?

@natefinch
Copy link
Contributor

natefinch commented Apr 14, 2021

For me, the most compelling argument for an interface defined in this package is one of documentation. It's much easier to find on an exported interface than buried in the docs of Is().

But also, the argument for "you can just write a test for it" falls flat for me. I could just write a test for lots of things the compiler could do for me. But why?

I guess I don't see why you wouldn't want it here? It would be more clear and discoverable. It would give one canonical definition of the interface. It gives you a place to anchor the docs. I understand that keeping an API small is desirable, but this interface already exists in the API of this package... It's just invisible. That's bad.

Edit: One more thing that I think is important - by defining the interface in the package, we give it a name. Without a name, it's really hard to have a conversation about this interface, or reference it. Right now, you can't tell someone "oh, you need to implement errors.Iser to do that...." you have to say "oh you have to add a method to your type called Is(error)bool to do that ...."

The json.Marshaler example is perfect. How do you do custom json marshalling? Implement json.Marshaler. I can google for json.Marshaler. I can't google (easily) for interface{Is(error)bool}.

@smasher164
Copy link
Member

Reopening to merge discussion on #45568.

@smasher164 smasher164 reopened this Apr 14, 2021
@perrito666
Copy link

As I stated in #45568 given that errors expect these interfaces (or perhaps the word is not expects but allows, invites? sorry can't find the word in english) to be implemented in errors, it makes sense that it provides the code contract for them, as @natefinch says, this IS a compiled language, asking people to implement these interfaces in their packages is akin, for me, to ask them to implement error. Additionally the interface is defined dynamically in a loop on the actual implementation of Is and As so that is at least one use of it.

This is my proposed implementation and naming for it #45568 (comment)

@ianlancetaylor
Copy link
Member

@smasher164 Thanks, but I don't think it's ideal to reopen a closed proposal. The conversation gets complicated and it's harder to see what the decision was and why.

I think it's preferable to open a new proposal, cite this one, and say "we should revisit this because of this new information that was not considered in the original discussion." Thanks.

@smasher164
Copy link
Member

Gotcha, sorry about that. Should I close this issue and reopen the other one, or keep it as is?

@ianlancetaylor
Copy link
Member

I think it would be fine to close this issue and reopen the other one. Thanks.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants