Go: proposal: Go 2: Elvis Operator

Created on 29 May 2018  路  16Comments  路  Source: golang/go

Yet another solution for error handling TM

The Elvis Operator ?:

Seeing all of the solutions to error handling, here is what matters: _if err isn't nil, what do we do?_

So therefore I propose the following syntax, the "elvis operator": ?:. It evaluates an expression on it's left, and if the left side is not nil, the return statement on the right is executed.

The grammar would be Expression "?:" ReturnStmt. Note that because the right-hand side is ReturnStmt, the Elvis Operator is not a standard binary expression.

Also, the syntax is inspired by Kotlin (although Kotlin's is a true binary operator), which is in turn inspired by C's ternary operator.

The decision to make the right side a ReturnStmt instead of an ExpressionList (which would reduce boilerplate code) is made because _all exits of a function should have a return_. This makes it less confusing for a reader and more clear as to what it does.

Error Handling

err := io.SomeIoOperation()

// possible ways to handle the error:

err ?: return err
// AKA if err != nil { return err }

err ?: return &MyErrorWrapper{"info", err}
// AKA if err != nil { return &MyErrorWrapper{"info", err} }

err ?: return "", 0, err
// AKA if err != nil { return "", 0, err }

This is superior to #21161 because it has much less implicit behavior. The return is shown, and err is previously defined. It is also more flexible, and can be used in other cases...

Argument Verification

func ErrorIfNil(arg interface{}) error {
    arg ?: return ArgumentIsNilErr
    // equivalent: if arg != nil { return ArgumentIsNilErr }
    return nil
}

Advantages

  • This is simple, readable, and doesn't have much implicit behavior.
  • The return is explicit, so it's obvious where the function exits.
  • It handles multi-return and error wrappers extremely well.
  • Intuitive: err ?: return err just look like "if there is an error, return it".
FrozenDueToAge Go2 LanguageChange Proposal error-handling

Most helpful comment

A common idiom:

f, err := os.Open(file)
if os.IsNotExist(err) {
  return nil
}
if err != nil {
  return err
}

becomes:

f, err := os.Open(file)
if os.IsNotExist(err) {
  return nil
}
err ?: return err

I find the asymmetry there pretty strange here. To address that, you could suppose ?: would also execute the statement to its right if its left is not a zero value. Then you could write:

f, err := os.Open(file)
os.IsNotExist(err) ?: return nil
err ?: return err

I prefer the original Go code to either of these examples.

All 16 comments

A possible revision to this is to change the grammar to Expression "?:" Statement, where the statement MUST be either a ReturnStmt or a function/method call. (Why is there no special grammar for function/method calls? They are used for multiple statements)

Either way, this would allow for the err ?: panic(err) pattern, which could be useful as well, although (in my opinion) panic should be avoided, so it may be better to let it require the if err != nil pattern

For Go simplicity sake, I think I'd prefer if gofmt allowed:

func ErrorIfNil(arg interface{}) error {
    if arg != nil { return ArgumentIsNilErr }
    // instead of: arg ?: return ArgumentIsNilErr
    return nil
}

Your proposal saves lines, sure, but at the cost of new syntax. If on the other hand gofmt allowed the above, you save the same number of lines while reducing new syntaxes/etc.

I think zig got it right with the try keyword.

result = try openFile(f)

Essentially equivalent to

result, err := openFile(f); err != nil {
    return err
} 

@isaachier I like that

The zig try expression is great for the simple case of if err != nil { return err }, but what about the other 2 cases that were called out in this proposal?. The if err != nil { return "", 0, err } case could be handled by returning the error in the last position and returning a zero value for any other return values.

It's the if err != nil { return &MyErrorWrapper{"info", err} } case that is of the most concern to me. Any proposal that simplifies passing an error up the call stack as-is, but does not provide a mechanism to wrap that error in appropriate context before passing it up the stack would have the unfortunate side effect of discouraging people from adding context to their errors. AFAICT, the zig try expression doesn't handle this very important case.

Any proposal that simplifies passing an error up the call stack as-is, but does not provide a mechanism to wrap that error in appropriate context before passing it up the stack would have the unfortunate side effect of discouraging people from adding context to their errors.

This is how I feel about these return err proposals too. It's rarely the case when just returning the error without context is the best idea.

Usually just adding a filename, domain, exit code, timeout / duration gives err crucial detail.

OK I simplified the zig case. In that language, the error type is specified as part of the function return type (fn openFile(name: []const u8) File!IOError).

I do not agree to put in new operators, but at least new operator's subject should be return because it have two different AST (expression/statement). And this operator doesn't reduce the AST.

@zevdg luckily zig has built in stack traces (that look much better than Go's sadly cryptic stack traces IMO).

No thank you, no thank you very much.

@as if you don't have new information or technical argumentation to add, please express such opinions using the GitHub voting mechanism. See https://github.com/golang/go/wiki/NoMeToo. Thanks.

A common idiom:

f, err := os.Open(file)
if os.IsNotExist(err) {
  return nil
}
if err != nil {
  return err
}

becomes:

f, err := os.Open(file)
if os.IsNotExist(err) {
  return nil
}
err ?: return err

I find the asymmetry there pretty strange here. To address that, you could suppose ?: would also execute the statement to its right if its left is not a zero value. Then you could write:

f, err := os.Open(file)
os.IsNotExist(err) ?: return nil
err ?: return err

I prefer the original Go code to either of these examples.

you could suppose ?: would also execute the statement to its right if its left is a zero value (not just nil)

Shouldn't this be "if its left is not a zero value"?
I'd prefer just "?" instead of "?:". To me the colon indicates an "else".

f, err := os.Open(file)
os.IsNotExist(err) ? return nil
err ? return err

Also the "Argument Verification" example (test for == nil) does not match the semantics established in the "Error Handling" section (test for != nil).

Why does nobody notice the obvious error in

func ErrorIfNil(arg interface{}) error {
    arg ?: return ArgumentIsNilErr
    // equivalent: if arg != nil { return ArgumentIsNilErr }
    return nil
}

This is plain wrong, it must be

func ErrorIfNotNil(arg interface{}) error {
    arg ?: return ArgumentIsNotNilErr
    // equivalent: if arg != nil { return ArgumentIsNotNilErr }
    return nil
}

Which clearly shows that it's not able to do argument verification (it is very rare that someone wants to verify that an argument should be nil). It also shows that the symbol is confusing, if even the author is not able to use it according to his own definition.

@fzipp:

Shouldn't this be "if its left is not a zero value"?

Right.

My proposal clearly has a lot of design flaws (ie Expression "?:" ReturnStmt is really strange) and I've made quite a few errors making it, haha. I'm going to close this one out and instead promote this one!

I think #25626 is much more simple and readable than #21161, and it's pretty intuitive, so I'd highly recommend checking it out!

Was this page helpful?
0 / 5 - 0 ratings