layout: true class: left middle --- # is it worth a proposal to Go?
### my ยข2 share of the error handling saga
v0.1.1
.footnote[ .right[ © 2017 Vlad Didenko | @vldid distributed under the [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) license ] ] --- # consider: ``` srcf, err := os.Open(fn) if err != nil { return err } dstf, err := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, 0600) if err != nil { return err } _, err = io.Copy(dstf, srcf) if err != nil { return err } err = srcf.Close() if err != nil { return err } err = dstf.Close() if err != nil { return err } ``` .note[ example is roughly from the [fst package](https://github.com/didenko/fst/blob/85620ee14081a9a4ef872c74d0d443ab85ee82ab/tree_copy.go#L40-L63) ] --- # existing proposals to mitigate the boilerplate code: [add functionality to remove repetitive if err != nil return](https://github.com/golang/go/issues/16225) [spec: add 'must' operator to return err up](https://github.com/golang/go/issues/18721) ["reflow" keyword for error handling](https://github.com/golang/go/issues/21146) [spec: Bang! notation for error handling boilerplate](https://github.com/golang/go/issues/21155) [Go 2: simplify error handling with || err suffix](https://github.com/golang/go/issues/21161) [Go 2: support "assign if nil" statement to tackle error handling boilerplate](https://github.com/golang/go/issues/21732) .note[ (there are probably more that I missed) ] --- # nature of the proposals and main concerns: * address narrow functionality case - error handling * some rely on detecting a nil - errors' presence or absence * some suggest invasive syntax changes at the point of assignment * some are not-enough backward-compatible * most allow to delegate the failure concern without an option to enrich it with extra context * some seem like a one-off band-aid convoluting the language --- # insight A very foundational consideration of error handling was published in the often-cited Go Blog post [Errors are values](https://blog.golang.org/errors-are-values) in January 2015. Relevant takeaways: * errors are indeed values * errors are not special for an OS or **the Go compiler** * we programmers think of them as special * expressing what is special is not generic but custom logic * we complain that compilers put the burden of that logic on us * if compilers would guess the logic wrong the joke will be on us --- # broader picture It seems it may be more productive to have a solution which: * presents an uninterrupted flow of the main execution path * considers errors as an important use case, of many * does not rely on a special meaning of `nil` * guarantees backward compatibility * follows general C-style visual presentation of code * allows to enrich errors with local context --- # essentially: We are looking to have a * generic mechanism * to trigger a piece of code * each time a variable gets assigned to * regardless if in a single-value or a multi-value context * regardless of where the value comes from (func or something else) --- # how about: ``` srcf, err() := os.Open(fn) dstf, err() := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, 0600) _, err() = io.Copy(dstf, srcf) err() = srcf.Close() err() = dstf.Close() ``` ... and keep the error processing logic somewhere else. --- # reality check: .right[ #
it is not possible! ] #
... today? --- # however, imagine: a `grab` (new keyword) statement attaching a code block to a variable name,
like in its simple form here: ``` grab err() { if err != nil { return err } } ``` ``` srcf, err() := os.Open(fn) dstf, err() := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, 0600) _, err() = io.Copy(dstf, srcf) err() = srcf.Close() err() = dstf.Close() ``` --- # wait, where is error context enrichment? * `grab` statements may have parameters, similar to `func` (variadic ?) * `grab` blocks have no return values * variables being assigned may use parameters to send data to `grab` blocks * empty parameter lists are required both in `grab` definitions and variable assignments ``` grab myErr(instanceId int) { if myErr != nil { return &NewInstErr(myErr, instanceId) } } ... childId, myErr(myId) := cluster.SpinUpChild() ``` --- # is it a better... .right[ # can of worms? ] Let's look at this devil's details --- # grab is: A compile-time directive to inline the code block after ***each*** assignment to the variable .note[ *Note:* compile time inlining is suggested only because it seems simpler to reason about than treating it a-la function call after assignments. Is it really simpler, though? ] --- # grab rules 1/3: Most of the rules seem obvious given the assumption of code inlining. It is sill worth to spell them out in case inlining is questioned. It is also a given that obviousness of the rules is an assumption by itself. The key is not a number of rules, but how natural the actual use will feel. * a variable and everything used in a `grab` block must be declared before a `grab` can associate the variable with the block * each `grab` statement is in effect for: - either the variable lifespan - or until the next `grab` of the same variable --- # grab rules 2/3: * `grab someVar() {}` releases preceeding `grab someVar` statement * `grab` can only be defined at the same level of nesting as the variable declaration (until we know better) * need to decide if overriding `grab` statements are allowed to change signature * assigning to a grabbed variable in an enclosed scope is either prohibited, or ***needs more design thinking*** - see further notes * in absence of `return`, `grab` block only exits at the end of the block --- # grab rules 3/3: * `return` in a `grab` block takes effect at the scope where the `grab` is defined * `break` and `continue` (what else?) are not allowed in a `grab` block * `grab` blocks for multiple variables in multi-value assignments are inlined starting from the rightmost variable * in case of an error in a `grab` block runtime message should include both the troublesome assignment location and that of the `grab` definition --- # elephants: Every complex idea has a few pink elephants in it. I know some for this one. * What happens when a variable is assigned in an inner scope? * What happens if a variable is a pointer - or a `func`, `[]`, etc? * Can `grab` blocks recurse? --- # inner scopes: One version of the problem: ``` grab err() { ... } go func(){ err() = ... }() ``` While it seems to be possible to come up with a reasonable execution path via a combination of rules and restrictions, it would be great to first hear a community take on this. --- # grabbing pointers, funcs, slices: The real issue seems to be around `grab` blocks for variables which have been passed out of scope by pointer or shared outside of the `grab` scope in any other way. However at a closer look it is not indeed an issue. Grab blocks are assosiated with ***identifiers***, not memory. So assignment to the same memory through a different identifier is a subject of that other identifier's context and, maybe, `grab` blocks. --- # recursion: One version of the problem: ``` grab varA() {
} grab varB() { ... varA() = ... ... } grab varA() {
} varB() = ... // is it clear that
will be inlined? ``` It seems that recursion combined with `grab` re-definition adds significant confusion. Which one should be dropped, if no other solution suggested? Both? --- class: center, middle Writing and, more importantly, following through
with a proposal is a large time commitment.
So, from the community perspective, #
tell me
if it is worth a proposal to Go? ??? # revision history: date | version | comment -----------|---------|----------------------------------------------------- 2017-11-20 | 0.1.1 | Fixed doc bug stating optional empty parameter lists 2017-11-17 | 0.1.0 | Added required empty parameter lists to examples 2017-11-17 | 0.0.0 | First version of the grab presentation