-
Notifications
You must be signed in to change notification settings - Fork 50
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
API design question: Should Node.AsFoo methods return error? #43
Comments
(forgot to note this in the original post) It's worth also comparing our interfaces to stdlib's https://golang.org/pkg/reflect/#Value and methods on it like |
I continue to slow-cook on this and continue to fail to come up with good reasons these errors need to be adding complexity to the return signatures of commonly used functions. I also polled @hannahhoward who's written a lot of consuming code for thoughts on this, though (especially whether it might make any sense to bundle with the already-big set of changes in #49 ) , and feedback there was meh, "not super needing to lose those errors right now personally." So, while this might still deserve review and doing later: still not slating it for today. |
I'm getting used to the braindead error checking in Go code too, it just seems to be part of what you expect, with APIs that do non-trivial work not returning errors making me question whether there really are no error conditions in their work. My preference is for idiomatic APIs as long as it doesn't come at severe performance cost (and even then, surely there's space for an opt-out like you're suggesting for fasttraversal). The error handling in Go is sufficiently annoying and pervasive that it's likely to be something that's dealt with in a future version of Go with sugar. Like So, idiomatic and cross fingers that the Go gods follow sensible language design and iron out needless boilerplate at some future date. |
I'm strolling through some production + not-written-by-me code and gathering some observations that make me update my beliefs on this subject. Observation: people can and readily will use the This contrasts with a lot of code I've been writing lately, in which it's almost always felt natural to do a This usage pattern would also be a lot different if the user had schemas + codegen, in which case all of the assertions of the expectations would be done in a consistent up-front way by the schema system, and you'd then get no-error accessor methods thanks to the codegen, nicely solving everything. But, when users don't have that (or choose not to use those tools), this destructuring assignment is the next most natural thing -- and as such, ought to work reasonably well. Updated belief: no-error-return in the signature of the Observation: It's extremely common for users doing this kind of destructuring work to call As already discussed in the original post, the It gets more syntactically irritating as it goes deeper, but even at two nestings, it's already irritating... and it's rarely less than at least two. Updated belief: something like the It would just be really nice to come up with a way to do Even if we don't find an ideal-performance way to do it, though, we really ought to have a batteries-included good ergonomic option for fluent-style code available in the core library. |
I personally agree with the earlier point that a It is true that they are perhaps easier to misuse due to the lack of an error return, but it's pretty common for APIs to have invariants which cause panics when broken. I'm also not a fan of the |
Yeah, as I've left this on the slow-cooker in the back of my brain, I've come back around again to preferring fewer errors in return signatures in this scenario. (In other words, I agree with your comment @mvdan, and retract about half the "updated belief" in my own last comment.) The "expectation-heavy destructuring code" user story is an interesting and important one that I'd like to provide good ergonomics for. But I guess more research into how to provide that best is still in order -- making the
Agree, and well said. I still think I like the But it is also somewhat too tempting to use it in non-test circumstances at present, and that is indeed feeling like a strong hint that most of these methods should just as well panic themselves. I think at this point I'm convinced that we should remove the error returns from these methods. It'll be a pretty big change, though, so we'll want to plan and sequence it carefully. A couple of other things to think about in the planning:
|
If you want chaining and error reporting, the only nice way that I know of (besides panics directly) is to "chain" together a query, working out the result at the end. For example:
I'm not quite visualizing that problem. Perhaps you can show me some examples. |
Boiling down to a similar API that you and I have been bounding around in our discussions about how the ideas in ipld-prime might translate into JS @warpfork, but that's forced more by the async boundary (Promises) than error handling but ends in a similar place.. chaining a thing that isn't the final thing until you execute or "resolve" it. I'd be fine with an API like this because we could end up building something that roughly mirrors it in JavaScript and that's a little bit neat. The question is - how comfortable is such an API for a typical Go user? Do you see these kinds of APIs in the wild today? It'd be nice to avoid novel API styles as much as possible. |
I wouldn't say this is common - most people would go with the less abstract "each step returns an error" API, but that doesn't mean it's the best or most idiomatic :) Here's a somewhat similar example from the standard library: https://golang.org/pkg/bufio/#Scanner The more obvious way to design the API would be like:
Instead, it uses
This is slightly different from our case here, but it's a similar idea. "Buffer" the errors until the user cares about them, also meaning that they can handle them in one place instead of many. Instead of chaining with a final "do" method, we could also use a receiver, more similar to what
I think the chaining makes sense if you generally want to catch an error for every chained lookup, and the "query receiver" makes sense if it's normal to do many separate batched lookups and checking for any error only at the very end. I imagine it's the first of those two. |
I opened a new issue for this, and loaded it up with some examples: #73 (It might not be a solvable issue. And we might not "need" to "solve" it; it's just lament about boilerplate. I just wanted to mention it as something that's within ~about one jump of relevance when we discuss these error returns.) |
A lot of this discussion about "how to make multi-step actions chain syntactically nicely": I did ask for it :) We might want to break that out into other issues, though. It seems like that discussion is all heading in the direction of "how to make a better One last comment I'll leave on that subject, though: One of the big issues I had with the |
I should note that the compiler has gotten better with interfaces and inlining in the last couple of years, so perhaps the fluent style can actually be made to avoid allocations for simple cases today. I'd certainly want to give that a try. |
Error values and fluent APIs aside, I still strongly believe that these "as T" methods should simply mirror the |
This one's been on simmer for a while (and still will be for a while more, because it's a big API change, and we're going to be careful about wrangling it), but just to have a clear post on the end of the thread here: Yes, we're going to change this, and almost all of these methods are going to drop their In review:
(And also, I'm increasingly hoping to get to this "soon". Still no promises on a date, though.) |
Okay, no, we're not going to do this after all. It doesn't fly. We got right up to the edge of trying to do this, and while writing the draft of what the change notes would be, realized there's still problems here. Only in some scenarios! But they're critical scenarios.
Scenario 2 (ADLs can return errors that aren't statically avoidable) is what cinches this. We really can't let the desire for ease-of-use be so over-weighted that it would result in inconsistency of contracts between ADLs and regular nodes. We're just going to have to keep working on more nice facades and assistive code that makes it more pleasant to use this core API, errors and all. |
The
ipld.Node
interface currently has a bunch of methods for unboxing its content to a primitive golang type:These methods return errors.
The question is: should they?
the utility of errors
Returning errors is generally considered better than panicing, as a rule of thumb and a default stance in the golang community at large.
Returning errors is also often faster, when you're doing something where speed matters. Returning an error is just moving some memory around, and later facing a conditional jump instruction: this is about as cheap as it gets (especially if you return an error value, rather than allocating a new value with any specific information). By contrast, panicing and recovering is a pretty complex thing.
Panicking also (note: in the current versions of the go compiler, and to the best of my knowledge) prevents functions from being inlined. This can be a pretty significant obstruction to optimization. And these unboxing functions we have here are certainly small, nearly-trivial functions that we'd like to have inlined!
... but a bunch of that is dubiously applicable
We're returning new error values that include helpful contextual information in almost all cases. Which means we're allocating in the error path. Which means it's already not fast: you'd much rather not hit the error, if you're concerned about the speed. In practice, for these methods, that means: if you care about speed at all, you're absolutely going to want to check the
Kind()
method and switch on that, rather than blindly calling theseAsFoo()
methods and checking errors.Worrying about inlining is vacuous. We're talking about an interface. There's already no inlining.
So that shoots down most of the concrete reasons to prefer error returns, at least as far as I can think of. That leaves us with just the vague "rule of thumb" reason intact.
downsides of returning errors
Returning errors from these methods makes them significantly more irritating to use in many common situations.
You can't chain calls easily when a function has multiple returns.
You can't use the result as an argument to other functions, either. (Mostly. There's some special cases; see next heading.)
Even if you're not worried about ergonomics of function composition, even checking these error values often just plain feels silly in practice in a lot of the code I've written. Very frequently, I statically know the error is impossible, either because I know for sure what shape of data I'm handling, or because I've done a
n.Kind()
switch just a moment ago anyway. So, if I'm writinganswer, _ := node.AsFoo()
in the majority of cases... is having this error return really worth it?other mitigation techniques
'must'
The
must
package seems to do well here.E.g.:
... and this can then be used as:
The
must
pattern can get icky if you have to use it recursively (this is why it's Not Great when it comes to handling a series of "Lookup" operations), but since all the "AsFoo" methods are leafs of the call tree of handling some IPLD data, it tends to work out fine here.We don't have
must.String
andmust.Int
methods at present, but it'd take all of about five minutes to whip them off.'fluent'
The
fluent
package also already does away with error returns.It's clearly okay for this package to do it, even if it's unclear for the core
ipld.Node
interface, because it's opt-in.It's got downsides though (namely, it incurs an allocation for boxing into the fluent interface; see the Background section in issue #42 for some other discussion about that). A mitigation to an ergonomic issue that's got unavoidable performance costs is not a very good mitigation.
related APIs
Several other methods already don't return errors for wrong kind, such as
Length()
. But this is a little different from theAsFoo()
methods, becauseLength
can make a clear out-of-bound return (a negative number).The
MapIterator()
andListIterator()
methods don't return error, but they did used to return a dummy iterator which would return an error on first use. I recently concluded this was also silly, and elected to change these methods to simply returningnil
if they're used on the wrongkind
. But this is a little different from theAsFoo
methods, because they return interface types, so we can usenil
as a clear out-of-bound sentinel value.The
LookupString()
andLookupIndex()
methods (and etc methods that traverse a piece of recursive data and return anotherNode
) do return error. But this is a little different from theAsFoo
methods, because there's more than one error they can return (wrong-kind vs key-not-found vs not-a-struct-field, etc), and those errors can't always be avoided by probing another method (by contrast, theAsFoo
method errors can always be avoided by probingKind
first). These methods also have more (and different, Because Recursive) mitigations in the form of thefluent
,traversal
, proposed `cursor, and other packages.what's the balance?
Are returning errors from these methods worth it?
Can we make sufficiently user-friendly higher level interfaces that we don't care if the core API is a little rough?
What's the balance?
Feedback welcome.
The text was updated successfully, but these errors were encountered: