go: proposal: Go 2: chaining method calls with return type (T, error)
There are many instances where chain calling methods yields code that is more readable.
For instance, a series of math operations on a custom type.
if myNewVar, err := myVar.Add(5).Multiply(2).Pow(4); err != nil {
//handle error
}
//also allow
//myNewVar := myVar.Add(5).Multiply(2).Pow(4)
Is far more readable than calling and error checking each method one at a time. The boilerplate error checking inhibits readability and debugging. Compare the above with the following:
myNewVar := new(myType)
var err error
if myNewVar, err = myVar.Add(5); err != nil {
//handle error
}
if myNewVar, err = myNewVar.Multiply(2); err != nil {
//handle error
}
if myNewVar, err = myNewVar.Pow(4); err != nil {
//handle error
}
If there more than a few methods called with extensive error handling, it is easy to lose sight on what exactly is being accomplished.
I personally experienced this when using a library. I was performing serial transformations on a type. I had erroneously called two methods out of order. I could not find the bug until I rewrote the library to allow chain calls (by not returning errors).
This allowed me to greatly improve the readability of my code. What had taken my many hours to write incorrectly and attempt to debug, was now written correctly, the first time using only a half hour. Not only that, but I found and fixed a bug in the imported code as I rewrote the library with chain calling to better see what it was doing internally.
However, I lost my error handling along the way.
Here is a way of handling the issue using golang as is:
https://stackoverflow.com/a/50601526/11012871
It makes the error part of the struct and checks for existing errors at the beginning of each method.
I would like to see something like that baked into golang, so I can use external libraries this way without writing wrappers or otherwise modifying them.
My suggestion would be to allow a method call on a function which returns (t *myType, err error) by calling the function on “t” if the error is nil. If the error returned is not nil, stop the chain and return as is.
I would also require the that the methods in the chain have only two values returned (t *myType, err error).
I would also require that the chained methods must return the same type as the first value. That is, all methods must return the same type (t *myType, err error). (I think it would get sloppy fast if we allowed a method which returns (i *int, err error) to chain call of off (s *string, err error). But I am open to hearing good ways to handle this.)
Then you could have something like this:
type myType struct{
\\...
}
func (t *myType) Method1(input int) (r *myType, err error){\\must return two values with second being error to allow chain calling
\\sets r and err
return
}
func (t *myType) Method2(input int) (r *myType, err error){\\must return two values with second being error to allow chain calling
\\sets r and err
return
}
func main() {
\\initialize t of type *myType
if result, err := t.Method1(5).Method2(23); err != nil {
\\handle error
}
\\...
}
Method2 operates on the *myType returned by Method1. If Method1 returns a non-nil error, the whole chain is stopped and its return value is returned. Method2 is only called if Method1 has a nil error.
This would allow for more readable and easier to write code.
Obviously, I can write my own types as suggested in the stack overflow post. I can also write wrappers.
This, however, would allow me to use imported types this way without needless boilerplate.
Of course, I am open to other suggestions on how to allow chain calling of methods with errors.
To summarize:
If all the methods have the same two return values (t *myType, err error) then allow
myNewVar, err := myVar.Method1().Method2()
If Method1 returns a non-nil error, Method2 is not called and myNewVar and err are set to Method1’s return values.
If Method1 returns a nil error, Method2 is called on the *myType returned by Method1 and myNewVar and err are set to Method2’s return values.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 13
- Comments: 19 (5 by maintainers)
You can already do it with a slightly different chaining API: https://play.golang.org/p/MiEhXUIdxbS
And with generics, it will be even easier, I think.
The original idea in this proposal seems well suited for certain specific packages that use methods that fit the required pattern. But it does not seem like it works well for most Go code. Specific packages can already use other mechanisms, such as the
panic/recoverused by packages like encoding/json.Adding this as to the language will require adding a new feature that everybody will have to learn, although it seems unlikely to be generally useful.
Therefore, this is a likely decline. Leaving open for four weeks for final comments.
@beoran I appreciate that you can create a new package with existing go syntax that accomplishes my goal of chaining methods and handling errors. But what I want is an easy, baked in way to chain calls from existing packages without altering them or making a bunch of boilerplate wrappers.
I think the idea is potentially valid, though I’m not sure about the specifics of this proposal.
Thinking out loud here: The issue that a lot of people had with the old
checkandhandleproposal was that it hid things and made fine-grained error handling feel more awkward, even if you could just resort to the current way of doing it for cases where you needed it. What if an error handling proposal didn’t deal at all with handling errors, and only with checking them? For example, as this proposal says, you can’t chain method calls if any of the methods in the chain return more than one value, but sometimes this results in a fairly large number ofif err != nilblocks and extra variables that aren’t really necessary for the specific case that you’re dealing with. So how about a way to chain methods that can return errors, not as a way to replace the existing error checking system, but just as a way to make dealing with those specific annoyances easier? Less of a complete overhaul, in other words.What I’m thinking is something like this:
The idea here is that a
?.could be used to chain a method call off of a method that returned a value and an error, thus making such chaining explicit. The entire chain of method calls would have the return types of the final method, and if any of the method calls that had a?.attached to them had a non-nil error, everything except for theerrorreturn would just be zero values. Each method would also have to be defined for the first return of the attached method, thus resulting in theintreturn fromM2()in the example just simply being completely ignored.This approach also doesn’t break the fine-grained support of
if err != nil, as it provides no mechanisms for actually handling errors, i.e. no automatic panicking or return. It just makes it a bit easier to deal with large numbers of calls in cases where differentiating for every call is overkill.Hmmm… As I finish writing this, I’m not entirely sure what my opinion is of it. I’ll just leave it here so that I can see what other people think, assuming that anyone responds, of course.
related: https://github.com/golang/go/issues/33361
I’d like to add that there is actually a case in language where (a kind of) some similar ideas were implemented. It’s pipelines in Go templates. So if for example we have these functions:
which process some input, we can than construct this in templates:
The result in case of
nilerror in pipeline will be the output of F3.So the output at each step is passed to next function and in case of error, whole pipe will be stopped:
edit: grammar
Speaking purely personally, I think it can be useful in limited contexts, but I think that it encourages people to restrict themselves to that specific style, which then leads to contorted code for methods that really ought to return more than a single result. I’m not at all convinced that we should have a language construct that specifically and only supports this style of programming. That’s just me, though, I’m willing to listen to other opinions.