go 2 has laid out the problem of error handling (Please read first). A wiki page was suggested for feedback. The feedback overwhelmingly suggested ways to not use scoped handlers, essentially creating alternative proposals
It is amazing to see the error handling problem being properly addressed. The existing proposal is good, but I think we can iterate to make it better. I think we can simplify it and address these issues:
- Defining handlers as a stack is presumptious: it forces handlers to be side-effecting, and ruins interop with
defer
. return
statements are placed in handlers, ruining composition.- using
check
as a prefix function reduces code clarity. - a new anonymous function syntax is introduced into the proposal, but only available to
handle
. - the proposal introduces two keywords, but one may suffice
Lets look at Ruby's block syntax.
irb(main):001:0> def block(&block); block.call() ; end
=> :block
irb(main):008:0> def return1(); block { return 1 } ; return 9 ; end
=> :return1
irb(main):009:0> return1()
=> 1
Its even possible to write the returning function ahead of time
irb(main):006:0> def returns2() ; b = Proc.new { return 2 } ; block(&b) ; 3 end
=> :returns2
irb(main):007:0> returns2()
=> 2
Could we introduce Ruby's block's to go to solve this problem? In the simple case yes, but in the general case there is a composability problem: you cannot really compose two handlers that both want to return early.
irb(main):012:0> def notcomposed()
irb(main):013:1> c = Proc.new { return 10 } ; d = Proc.new { return 20 } ;
irb(main):014:1* c.call() + d.call()
irb(main):015:1> end
=> :notcomposed
irb(main):016:0> notcomposed()
=> 10
The current proposal has a handle
keyword with a new special anonymous function that behaves equivalent to Ruby's Proc.new
.
That is, a return
returns from the outer function.
func process(user string, files chan string) (n int, err error) {
handle err { return 0, fmt.Errorf("process: %v", err) } // handler A
Just as in Ruby, the early return means composition does not work. In go, it should be easy easy to automatically handle the return: return the error given at the check statement, and any other return values should be set to the zero value. In fact, the proposal provides a special "Default Handler" which does just that.
The reason why the existing proposal has non-local returns is because in the proposal we are forced to construct handlers as a stack (see next section).
The proposal forces handlers to compose as a stack scope. This is convenient in many cases.
However, it forces a side-effecting style to handlers.
Additionally, it limits the programmer and adds mental overhead to track the implicit stack.
The stack-style itself is not always desired. That is, once a handler is added, it can only be deleted by adding a return
to a new handler.
Lets look at the example from the proposal:
func SortContents(w io.Writer, files []string) error {
handle err {
return fmt.Errorf("process: %v", err) // handler A
}
lines := []strings{}
for _, file := range files {
handle err {
return fmt.Errorf("read %s: %v ", file, err) // handler B
}
scan := bufio.NewScanner(check os.Open(file)) // check runs B on error
for scan.Scan() {
lines = append(lines, scan.Text())
}
check scan.Err() // check runs B on error
}
sort.Strings(lines)
for _, line := range lines {
check io.WriteString(w, line) // check runs A on error
}
}
Here handler B is able to disregard handler A by using a return
.
However, I think this demonstrates that although stacks may often be convenient, they are not quite what we want.