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: Alternate to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level. #32473

Closed
mikeschinkel opened this issue Jun 7, 2019 · 16 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@mikeschinkel
Copy link

mikeschinkel commented Jun 7, 2019

I am going to make this brief with the potential of fleshing it out later because I have seen few proposals accepted here so I only like to invest a lot of time in fleshing it out if there is some chance it will actually be considered.

Overview

Add two non-error specific language features to enable much streamlined support for error handling that addresses the use-cases that try() would address, but that are more flexible and can handle a far greater number of use-cases.

This can be achieved without builtins that require the understanding of "magic" behavior.

This proposal explicitly does not address the expression aspect of try(); it assumes func calls that can error will each be called on their own lines, which I think is more in the nature of Go than using nestable expressions.

For some additional details please see my comment with concerns about the try() proposal.

Additions Required

  1. Ability to call a func/closure in an assignment statement.
  2. Ability to break, continue or return more than one level.

Example

The following example illustrates both; here e1 and e2 are closures — but could have as easily be declared funcs — and the syntax items, e1(err) := GetItems() means to call e1() with the 2nd return value after GetItems() returns:

type Item struct{}
func ProcessItems() error {
    e1 := func(err error) {
        if err != nil {
            return^2 err
        }
    }
    e2 := func(err error) {
        if err == nil {
            break^2 
        }
    }
    items, e1(err) := GetItems()
    for _,item := range items {
        e2(err) = ProcessItem(item)
    }
    return err    
}

Above we named the returned error err but we could have instead used _ as in items, e1(_) := GetItems(), assuming that is not considered wrong given _ usually throws away values.

As for #2 a we are assume the return can jump up the call stack by the number of levels indicated by "^n." return^1 would be synonymous to return, and in this example return^2 would bypass ProcessItems() and return to its caller.

Note also in the example we assume break^2 will exits the current closure and then break out of the for{...} loop inside of ProcessItems(). Similarly, continue^2 should be allowed to work similarly.

Please consider the ^n syntax is just one hypothetical we could choose so hopefully anyone responding won't bikeshed the syntax but instead discuss the general purpose concept of giving a developer a way to specifying how far up the call stack to break, continue or return.

Special-case builtins on assignment calls

Using the simple example from try() that takes this:

f, err := os.Open(filename)
if err != nil {
        return …, err  // zero values for other results, if any
}

And simplies into this:

f := try(os.Open(filename))

Let us instead allow any of the following three (3) special case builtins to do the same, except that the later two (2) use break and continue, respectively, instead of only supporting return as try() does:

f,return(err) := os.Open(filename)
f,break(err) := os.Open(filename)
f,continue(err) := os.Open(filename)

Example #2

Now let's revision the try() proposal's CopyFile() example but using this proposal's system instead. Unlike try(), all coupling is explicit and easier to reason about when reading the code:

func CopyFile(src, dst string) (err error) {
    e := func(err error) {
        if err != nil {
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
            return^2 err
        }
    }
    r, e(err) := os.Open(src)
    defer r.Close()
    var w *os.File
    w, e(err) = os.Create(dst)
    e2 := func(err error) {
        w.Close()
        if err != nil {
            os.Remove(dst) 
            return^2 err
        }    
    }
    e2(err) = io.Copy(w, r)
    e2(err) = w.Close()
    return err
}

Summary

Rather than create a special case try() I propose that Go instead add two (2) new general case language features vs. a feature useful for only a constrained set of use-cases. Further, I think these additions would be more in-line with the existing nature of Go but would address the same use-cases that try() was targeting. And finally, these features could potentially combined to address other needs that might otherwise force the Go team to add more "magic" builtins in the future.

@mikeschinkel mikeschinkel changed the title Alternate Proposal to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level. Alternate Proposal to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level. Jun 7, 2019
@mikeschinkel mikeschinkel changed the title Alternate Proposal to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level. proposal: Alternate to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level. Jun 7, 2019
@gopherbot gopherbot added this to the Proposal milestone Jun 7, 2019
@mikeschinkel
Copy link
Author

mikeschinkel commented Jun 7, 2019

For those who are voting thumbs down or reacting with confused, can you at least give me the courtesy of providing some explanation for your reactions? Right now it's not clear what about the proposal you disliked or did not find clear.

@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Jun 7, 2019
@ianlancetaylor
Copy link
Contributor

When someone writes return^2, what are the actual values that are returned? In general a function can be called by any other function, and the caller's result parameters could be anything, so it's not obvious how return^2 can describe the results to return. If this syntax can only return an error value, then it doesn't seem very orthogonal.

For break^2 it also seems necessary to explain what to do with the results.

func B(i int) (int, int) {
    if i > 3 {
        break^2
    }
    return i, i
}

func F() {
    var a, b int
    for i := 0; i < 10; i++ {
        a, b = B(i)
    }
    // What are the values of a and b here?
}

@ianlancetaylor
Copy link
Contributor

Let me expand slightly: I'm sure we can decide on answers for the questions I am asking. But for a really good language proposal the answers to these questions should be in some sense obvious. If they are not obvious--and I don't think they are--then this proposal makes the language more confusing and harder to learn. That is a significant cost, and it requires a significant benefit.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jun 7, 2019

Hi @ianlancetaylor — thanks for commenting. Honestly, I am caught between really wanting to make a difference and fearing that any efforts I put into this will be for naught. I had already invested over 8 hours into the comments I left on try() and I didn't have the stamina to do the same here, but at the same time felt I needed to get it done now before the ship sails on error handling.

That said, you make some great points and I hope you won't mind me massaging the proposal to address those questions. I'll do that and then follow up with another reply.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jun 7, 2019

@ianlancetaylor - So in my haste to complete this proposal I forgot to include the return parameters; I have updated to include them now.

To explain in more depth, think of the placeholder syntax of "^n" I proposed as being completely orthogonal to the parameters being returned. That syntax was just my standin to represent the concept of returning more than one call level.

To illustrate, each the first two returns will behave identically by returning one call stack level and the 3rd will returns two call stack levels, but they all return the same parameters:

return foo, bar, baz
return^1 foo, bar, baz
return^2 foo, bar, baz

Note that I am explaining a potential feature that is orthogonal to error handling but can instead be used for error handling.

Of course we could use a different syntax to specify the levels to return (not that I am proposing this syntax, I just wanted to show something different):

return foo, bar, baz
return (foo, bar, baz) => 1
return (foo, bar, baz) => 2

Now there is one issue I did not address and that is what happens when your return of more than one level does not match the return value signature of the func that it attempts to exit from? It should of course panic() if they do not match.

Which bring it to this point. This capability could easily be limited to returning from closures and that would simplify this latter issue.

But if you did that then we could not create a named func that could be used as an error handler across multiple functions, which is one of the most powerful things that I think my proposal offers; the ability to create reusable error handlers across methods, across structs and even across packages.

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Jun 7, 2019

Panicking if the result parameters do not match is an interesting approach, but I have to say that at first glance I do not understand how to implement that.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jun 7, 2019

"I do not understand how to implement that."

In that case you've got me. I wish I had the skill to code at that level but at this point that level is Greek to me.

I guess if it cannot be done — sadly — that makes at least the more general part of my proposal moot albeit you could still support closures since you can see both scopes when compiling.

@ianlancetaylor
Copy link
Contributor

Features in Go should be orthogonal. It would be less than ideal to add break^2 but only permit it to be used within a function closure. Even restricting it to a function closure isn't necessarily helpful, since that function closure can be passed to another function, and invoked there. Prohibiting that would make the feature even less orthogonal.

But if we permit any function call to invoke break^2, then we have to prepared to handle that exceptional return in any function call in a loop. That is likely to carry a noticeable run time cost, which is an issue for a somewhat obscure feature like this.

This is an interesting idea but I'm having a hard time seeing how it could really work.

@mikeschinkel
Copy link
Author

@ianlancetaylor Thanks for the follow up.

But It has been over a month since I proposed this, and a lot of water has flown under the bridge. I have since seen several other approaches proposed by others that I would as easily get behind as this — if we could even identify a way to achieve this.

The one solution I definitely find problematic though is the current iteration of try() being most considered by the core team right now, ironically because of its lack of orthogonality and more general applicability.

But no need to rehash why as I previously wrote in depth my issues on the other ticket. I for one really hope that we identify a better, more general solution and don't move forward with what it on the table. #fwiw.

@ianlancetaylor
Copy link
Contributor

We do our best to evaluate proposals as they are presented. As far as I can see it's the only fair and reasonable approach. If you do not think this specific proposal should be adopted, it's fine to withdraw it. Thanks.

@mikeschinkel
Copy link
Author

@ianlancetaylor Sorry, I think my words caused you to misinterpret my meaning. For clarity:

  1. I was not in any way criticizing you for the time passed. You have a tremendous amount to deal with and I appreciate that you do in fact comment on proposals, no matter when that is.

  2. I was instead pointing out that over the time since I wrote this I have been exposed to many other proposed alternatives to the try() proposal that I would not have envisioned on my own.

  3. Of those approaches I have seen numerous that I felt would be a better approach than try(), and that includes the one I proposed here.

  4. I was not implying a desire to withdraw proposal, I was only reacting to the fact you stated you don't know how it would be implemented, and since I am not a language implementor it indeed may not be possible to implement. If so then this proposal would be apparently be moot. But whatever the case I still think it makes sense conceptually.

  5. And lastly I was making a plea that the Go team not pursue the try() proposal and instead wait for an alternative that would address more uses cases and that the majority of the community could get behind.

@mvndaai
Copy link

mvndaai commented Jul 16, 2019

The main problem with a multi-level return is that it makes functions less explicit. If I call foo() and all of a sudden I have returned 4 levels because someone else in part of a codebase uses it 4 levels deep and I only used it 2 levels deep this causes issues.

I someone like the idea allowing functions on the left side of :=, but that goes back to the problems of either not adding context to errors or hidden return statements.

@ianlancetaylor
Copy link
Contributor

Supporting return, break, and continue at more than one level is an interesting idea, but as noted above it's not clear how to implement it in the general case. It also adds a considerable potential for confusion about control flow, especially as we start to see return^3 and return^10.

Without that feature, the rest of this proposal does not seem to do what the proposal is intended to solve.

There also doesn't seem to be great deal of enthusiasm for this approach.

-- writing for @golang/proposal-review

@mks-colibri
Copy link

mks-colibri commented Jul 17, 2019

@ianlancetaylor Thanks for taking the time to thoroughly consider it. As I said above, It was just one of the many approaches I have since found that might be a good solution.

However, I would like to ask that the team seriously consider break as a return strategy on par with return. The following is pulled from one of our projects, modified slightly for presentation:

package only
const Once = "1"
package config
func LoadConfig() (myerr my.Error) {
	fp := GetFilepath()
	for range only.Once {
		if !filesys.FileExists(fp) {
			myerr = my.Errorf("file not found: '%s'", fp)
			break
		}
		b, err := ioutil.ReadFile(fp)
		if err != nil {
			myerr = my.Wrap(err,"unable to read from file")
			break
		}
		cfg := Config{}
		err = json.Unmarshal(b, cfg)
		if err != nil {
			myerr = my.Wrap(err,"unable to unmarshal into config.Config")
			break
		}
	}
	if my.IsError(myerr) {
		myerr = my.Wrapf(myerr,"while loading config file")
	}
	return myerr
}

The benefits here is that the "clean up" code is at the end where code logically flows, and is using a simple if <expr> {...} mechanism rather than needing any changes to Go.

This all works today, but if return becomes the "standard" way to do error handling, clients won't like this approach and I'll reasonably need to move to the newly idioms. But I am hoping that newer idioms can embrace a simply flow like the above, maybe with some small changes to streamline it.

One simple change I would love is for Go to support one of these two syntax as an alternate to an empty block so that i don't have to redeclare the package only and the const only.Once in every app:

for once {
   // Code here that always but only executes once.
}

Or:

once {
   // Code here that always but only executes once.
}

There are obviously other potentials for improvement — as many have debated for weeks — but this one is a super simple next ask. This is nothing more than do-nothing sugar but it could come to be know to signal intent to use a block for error handling with break.


P.S. This comment should be attributed to @mikeschinkel. I was commenting using a GitHub account a client is requiring me to use vs. my regular account.

@ianlancetaylor
Copy link
Contributor

For what it's worth, you can write for once today as

    for once := true; once; once = false {

@mks-colibri
Copy link

@ianlancetaylor

True... but that is rather verbose, no? :-)

@bradfitz bradfitz added the error-handling Language & library change proposals that are about error handling. label Oct 29, 2019
@golang golang locked and limited conversation to collaborators Oct 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

6 participants