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

Open
iangudger opened this issue Oct 23, 2019 · 11 comments

Comments

@iangudger
Copy link
Contributor

@iangudger iangudger commented Oct 23, 2019

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
@gopherbot gopherbot added the Proposal label Oct 23, 2019
@beoran

This comment has been minimized.

Copy link

@beoran 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

This comment has been minimized.

Copy link
Contributor Author

@iangudger iangudger commented Oct 23, 2019

@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.

@Andari85

This comment has been minimized.

Copy link

@Andari85 Andari85 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
}
@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 19, 2019

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.

@Andari85

This comment has been minimized.

Copy link

@Andari85 Andari85 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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 26, 2019

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

@networkimprov

This comment has been minimized.

Copy link

@networkimprov networkimprov commented Nov 26, 2019

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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 26, 2019

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

This comment has been minimized.

Copy link

@networkimprov 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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 27, 2019

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.

@arnottcr

This comment has been minimized.

Copy link

@arnottcr arnottcr commented Nov 28, 2019

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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants
You can’t perform that action at this time.