go: proposal: Alternate to try(): 1. Call func/closure from assignment and 2. break/continue/return more than one level.

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.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 23
  • Comments: 16 (7 by maintainers)

Most upvoted comments

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.

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?
}

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

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

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.