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: Go2: Functions which when called, can cause the current function to return #35093

Closed
iangudger opened this issue Oct 23, 2019 · 17 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@iangudger
Copy link
Contributor

defer is great because it takes one common cause of destructors and splits it out in an easy to use and reason about way. Maybe we could apply this same philosophy to other aspects of the language.

One other major missing feature in Go is a macro system. One feature which macros are commonly used for is returning from a function early. This is especially useful for removing boilerplate code for error handling. A mechanism to do just this without the rest of a macro system could retain much of the language's current positive features while allowing substantial removal of boilerplate code.

In order to maintain clear control flow, it would be nice to make it obvious that one was calling such a function.

Possible ideas for implementation:

  • Keyword other than function for declaration.
  • Use a keyword at the call site like is done for go and defer. If this was required, it would make it obvious that the call could result in a return like an if containing a return.
  • Use a new keyword for the special return within the implementation. Alternatively some modifier like is used for break and continue in nested loops could be used.
  • Allow two lists of return types. One would be for returning from the caller, the other could be return values from the function itself.
  • Only allow as anonymous and within a function. This would ensure that the declaration was nearby to air in identification and that the implementation was handy for easy inspection. The downside is that then these couldn't be shared/reused. Maybe that would simply things by allowing the return types for the caller to be inferred?
@gopherbot gopherbot added this to the Proposal milestone Oct 23, 2019
@beoran
Copy link

beoran commented Oct 23, 2019

An interesting idea. This would make error handling a bit easier in some cases. If we just reuse existing keywords it could look like this:

func break Try(err error) () (error) {
   if err != nil {
       return break err /* will "break out" of the caller and make it return err. */
   }
   return continue /* will continue running the caller with no return values. */
}

@iangudger
Copy link
Contributor Author

@beoran I like it. Using existing reserved words would definitely make backwards compatibility better.

What I was thinking was that most of the proposals related to error handling were focused on error handling to the point that they wouldn't be too useful for anything else. A more general approach could be used for other things as well. For example, this could work with errors which are not of type error or even cases were there wasn't an error, but you need to return early in a way that currently results in a lot of repeated code.

@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Oct 23, 2019
@ZolAnder85
Copy link

ZolAnder85 commented Nov 19, 2019

I like the idea. A little more readable could be:

func ReturnAValueOrBreakTheCaller(a int32) float32 break int32 {
    if a == 10 {
        return break 11
    }
    return 10.0
}

func Foo(a int32) int32 {
    b := ReturnAValueOrBreakTheCaller(a)
    fmt.Println(b)
    return 9
}

@bradfitz bradfitz added the error-handling Language & library change proposals that are about error handling. label Nov 19, 2019
@ianlancetaylor
Copy link
Contributor

I think the main idea here is to permit a non-local return: a function literal can somehow return from the outermost function (or any outer function?).

It might help to see some examples where this would be useful.

@ZolAnder85
Copy link

ZolAnder85 commented Nov 20, 2019

I am not sure it is useful enough I would add it to language. Probably not. But it could have other useful features:

// these below are obviously placeholder names

func Parent1() error {
    func OpenFileForNow(path string) * File {
        file, err := os.Open(path)
        if err != nil {
            return_parent err
        }
        defer_parent file.Close()
        return file
    }
    a := OpenFileForNow("a")
    b := OpenFileForNow("b")
    c := OpenFileForNow("c")
    // do things with them
}

func Parent2() int32 {
    func Calculate1(x int32) int32 {
        if x > 65536 {
            break_parent
        }
        // heavy math
    }
    func Calculate2(x int32) int32 {
        if x < -1 {
            break_parent
        }
        // heavy math
    }
    x := 0
    for {
        x1 := Calculate1(x)
        x2 := Calculate2(x)
        x -= Calculate1(x1 - x2)
        x += Calculate2(x2 - x1)
    }
    return x
}

@ianlancetaylor
Copy link
Contributor

Ping @iangudger Can you show some examples where this functionality would be used?

@networkimprov
Copy link

Here's a case for non-local returns in a closure context...

I'm coding a filesystem tree walker using closures which write to a tar.Writer chained to an http.Client.Post() body. It aborts if there's an error writing to the network, and I would like to return the parent function at that point rather than pass an error back up the recursion stack to the parent.

@ianlancetaylor
Copy link
Contributor

For the error return case within a single package it's reasonable to call panic in the subroutine and call recover in the top level caller. For example, the standard library's encoding/gob package works that way.

@networkimprov
Copy link

networkimprov commented Nov 27, 2019

I would never panic to handle an expected application state; from what I've read, that's not Good Go. I've seen criticism of stdlib packages for violating that principle.

@ianlancetaylor
Copy link
Contributor

There's really nothing wrong with using panic within a single package. It's not something every package should do, but it's acceptable where appropriate.

@urandom2
Copy link
Contributor

There's really nothing wrong with using panic within a single package. It's not something every package should do, but it's acceptable where appropriate.

This surprised me, since my understanding was that panic was much heavier operation than a return, and panic was really only for exceptional or absurd behaviour.

That being said, if we take this advice and apply it to a concrete example, it does produce a much sought after interface:

import "git.sr.ht/~urandom/errors"

func Set(v interface{}) (err error) {
        defer errors.Handlef(&err, "setting: %v", v)
        _, err := set(v)
        errors.Check(err)
        return nil
}

@griesemer
Copy link
Contributor

There is a strong connection between the functionality of panic and a non-local return (i.e., a mechanism to return from an outer function): In both cases the stack has to be unwound safely, and in both cases one might want to protect a stack frame from being "silently" unwound. That protection in Go is via defer and (and possibly recover), which allows an activation frame to install clean-up actions to be executed before a stack frame is unwound.

Thus, the heavy lifting that panic and recover are doing will also have to be done by another non-local return mechanism (it is very likely that one would want to use defer with any such mechanism).

In other words, panic, defer and recover are equivalent to any other non-local return mechanism and form of stack unwinding protection (that latter of which was not mentioned in this proposal, but would be a likely follow-up demand).

Since we already have panic, defer and recover there's really no need for yet another mechanism. Or, vice versa, if we had another mechanism, we wouldn't need panic.

(For comparison: Smalltalk is a language that actually does support non-local returns. It also supports a method called "unwindProtect:" (or similiar; unfortunately Smalltalk dialects are not standardized). "unwindProtect:" is exactly equivalent to Go's defer. A Go panic can be emulated in Smalltalk by executing a non-local return of a top-level closure (which essentially causes the entire stack to unwind).

In summary, I don't think we need an alternative feature here. And to confirm what @iant has been stating earlier, it is perfectly ok to use a panic (and defer plus recover) to "quickly exit" a top-level function from a deep call-stack. For another example see e.g., text/tabwriter and its use of panics for that purpose.

@iangudger
Copy link
Contributor Author

@griesemer, thank you for the thoughtful response.

Most of the complaints around error handling in Go have been about the syntax. Explicit error checking with a conditional works fine. panic can work as well. The amount of code required for explicit error handling with conditionals seems to bother a non-trivial portion of programmers. I personally find the usage of recover a bit awkward myself (I always assumed it was intentional to discourage use).

There have been a number of seriously considered proposals for changing the language in the name of error handling. All of these proposals that I looked at were adding redundant functionality and the same behavior could be achieved today with more verbose syntax. What bothered me about each was that they seemed to me to be over fit on the most common patterns and lacked flexibility. I tried to address the flexibility issue in this proposal.

I personally like the status quo to be honest. I made this proposal under the assumption that the language is going to be changed to add a less verbose syntax for handling errors.

@griesemer
Copy link
Contributor

@iangudger Thanks for providing additional background.

Unfortunately, even a nice non-local return mechanism doesn't quite address the error handling problem because - while some of the machinery could be factored out - the factored out code (a local function/closure) would only work for that one enclosing function. As you mention yourself, those couldn't be easily shared/re-used. Furthermore, even in one function a single closure may not suffice due to signature type requirements that may not be uniform.

Real macros have even more problems. Macros were a clear non-goal from day one.

At some point in the distant past I would preferred a non-local return over a panic approach. The non-local return can be more light-weight (no need to recover to stop the unwinding as there is a definite stack frame which is the one from which to return) and it allows the simulation of panic (albeit that requires passing around some top-level closure from which to non-locally return). They also seem simply a (if non-trivial) generalization of the existing concept of the "return". But non-local returns are somewhat alien to C-like languages. We already have an explicit return, and there would have to be another form of return.

In Smalltalk it works fine because there's only one kind of return - if it happens to be in a closure it's always non-local (and causing the surrounding method to return). Without an explicit return, Smalltalk methods and closures simply return the result of the last expression executed. Such an approach would probably be a poor fit for Go.

The various attempts are improving error handling all failed one way or another. For now, the Go Team is not actively looking for another change in this direction - what we have may not be perfect but is has worked for 10 years. If a promising new idea comes up we can always pick it up. Better to err on the side of caution.

@beoran
Copy link

beoran commented Dec 20, 2019

Perhaps, then, since panic, in conjunction with recover, already gives us a way to do non-local returns, we should make that way easier to use and more performant?

I'm not sure if it wasn't proposed before, but , for instance, we could allow recover to take a list of interfaces that the panic should implement for it to actually do the recover, if none matches, then it will not recover? That will make some cases of using panic and recover easier for use within a single package.

@ianlancetaylor
Copy link
Contributor

Based on the comments above, this proposal doesn't provide functionality significantly different than what we already have in panic and recover. Therefore, this is a likely decline. Leaving open for four weeks for further comments.

@ianlancetaylor
Copy link
Contributor

There were no further comments.

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 Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

9 participants