ng
ng copied to clipboard
Proposal: introduce separate syntax for error elision
I've been working on a few of the outstanding issues with error elision, but as I started to think about it I realized it quickly gets very hairy.
I define error elision as: "given an expression that yields a tuple ([T1, T2, ...,], error), we support using this in places that expect just (T1, T2, ...). In that case, the runtime dynamically checks if the actual expression returned a non-nil error and panics in that case.
However, by this definition, one could argue these two cases should be supported:
func concat(a, b string) string { return a + b }
concat($$ echo foo $$, $$ echo bar $$)
func closeAll(files ...*os.File) { /* ... */ }
closeAll(os.Open(...), os.Open(...))
Since the error elision definition above supports turning (T, error) into just T, the function calls should be able to be used in single-value contexts. However, this quickly leads to a typechecking nightmare. While it's hard to give an example likely to show up in real code, how would we typecheck a call like this:
f1 := os.Open(...)
f2 := os.Open(...)
func foo(a, b, c error) { }
foo(f1.Close(), f2.Close())
There are two valid interpretations to which Close gets its error elided.
It would be nice to first clarify what error elision semantics would be desired. Am I giving them too much flexibility, and they should just be used in much simpler situations? If the definition to error elision I gave is desirable, I think the best approach to that would be to introduce syntax to let the programmer define the intent.
I think it would end up simplifying the code base, too, given that a fair amount of complexity is needed to determine whether or not error elision is needed or even desired. By asking the programmer to make their intent clear, this difficulty largely goes away.
I don't have a particular syntax suggestion, but I will give a few examples just to illustrate what I mean. I suggest we define the syntax $$ foo $$ to mean shell execution without error elision, and introduce $! foo !$ to mean with error elision. And then something similar for function calls. Alternatively, introduce $$ foo $$! and foo()! or $$ foo $$? and foo()? which are more parallel in syntax.
I agree that the two cases you describe should be supported. I also agree this makes for typechecking headaches.
I don't think your foo example demonstrates a typechecking problem, but possibly that is a typo in your example. You have foo taking three arguments, two calls to close give you at most two error arguments.
If you have some cases where there are ambiguities I'm happy to take a look. I'm reluctant however to use new syntax. The trailing ! or ? syntax is nice and appears to be gaining traction in some other languages, but I think that proposal falls awkwardly between Go and Neugram. It's too dismissive of errors for Go, and still requires too much thinking about errors for Neugram. If we can make typechecking for the current error elision work, I would prefer to do so.
Ah yes, you're right about foo. Consider this instead:
func foo() (error, error) { return nil, nil }
func bar(a, b, c error) {}
bar(foo(), foo())
Thinking about it some more, I suppose we could resolve the above case as the first foo() call's error being elided, since Go semantics don't allow for more arguments to follow after a function call that returns multiple values. That said, I'm wary of the semantics around error elision being hard to understand intuitively. Though perhaps these examples are unlikely to show up in real code.
While considering some gnarly cases a long while ago, I decided I could resolve some of them by saying that error elision only applies to functions whose last return value is of type error, and no other value is.
I'm not sure that's strictly necessary here, because as you say we can elide the first foo(), which is what my instinct says. I agree it's not obvious to users though, and the rule about only one error return argument may help make it obvious.
The other big confusion case I ran into with elision is variadic parameters, particularly of type interface{}. This comes up with: fmt.Println(os.Open("foo")). For this, I'm following the principle of sticking as close to Go as possible. That statement has a valid meaning in Go, so it means the same in neugram, that is, no error elision.
If you come across any others, please let me know. We can use this issue for tracking them.
Sounds good. I sent #78 to implement those rules for interior error elision. It doesn't implement your suggested semantics regarding a single error type, but it should get us pretty far.