My main beef with this and other proposals is that they don't clear up a fundamental flaw in Go: Multi-value returns with errors as a poor man's sum type.
Most functions in Go have a contract that the return values are disjoint, or mutually exclusive — they either return a valid value or an error:
s, err := getString()
if err != nil {
// It returned an error, but "s" is of no use
}
// s is valid
This pattern so ingrained that there's hardly a single Go doc comments on the planet that says "returns value or error". The same goes for the "v, ok := ..." pattern.
But this is just a pattern and not universally true. A commonly misunderstood contract is that of the `Read` method of `io.Reader`, which says that when `io.EOF` is returned, the returned count must be honoured. This is an outlier, but because the convention of disjointness is so widely adopted, many developers make this assumption (it's trivial to find repos in the wild [1] that make this mistake), and so this is, in my opinion, bad API design.
(As an aside, it's also true that multi-value returns beyond two values almost always become cumbersome and impractical, especially if said values are _also_ mutually exclusive. Structs, having named fields, are almost always better than > 2 return values.)
This kind of careless wart is typical of Go, just like other surprising edge cases like nil channels (or indeed nil anything).
I would much rather see a serious stab made at supporting real sum types, or at least mutually exclusive return values. For example, I could easily see this as being a practical syntax:
func Get() Result | error {
...
}
This union syntax showed up in the Ceylon language, and it's a neat pattern for a conservative language that doesn't want to venture into full-blown GADTs.
Such a syntax would be a much better match for a try() function, since there's no longer any doubt about the flow of data — there's never a result returned with an error, it's always either a result or an error:
result := try(Get())
or simply support existing mechanisms for checking:
if err, ok := Get().(error); ok {
...
}
if result, ok := Get().(Result); ok {
...
}
switch t := Get().(type) {
case Result:
// ...
case error:
// ...
}
I'd love to see a `case` syntax that allows real local variable names:
switch Get().(type) {
case result := Result:
log.Printf("got %d results", len(result.Items))
case err := error:
log.Fatal(err)
}
And of course, you could have more than two values:
switch Get().(type) {
case ParentNode:
// ...
case ChildNode:
// ...
case error:
// ...
}
The Go compiler can be strict here and require that every branch be satisfied or that there's a default fallback, although some might prefer that to be a "go vet" check.
A full-blown sum type syntax would be awesome, though I know it's been discussed before, and been shot down, partly for performance reasons. Personally, I think it's solveable. I'd love to be able to do things like:
type Expression Plus | Minus | Integer
type Plus struct { L, R Expression }
type Minus struct { L, R Expression }
type Integer struct { V int }
I like Rust a lot, and I'm doing a couple of projects in it.
But Rust is an advanced language. In the company I work for, Rust would be a no-go simply because some developers would struggle with it too much.
Go hits a nice sweet spot. You don't need to worry too much about whether something is on the heap or stack, or what the overhead of copying a struct is. It's conducive to incremental "sculpting": You can write a broad, naive implementation where you even wing it a bit with types first, and then slowly fill in detail, refining types (to the extent Go lets you), locking down performance, and so on.
My feeling with Rust is that you start and end with just one level of granularity: You can't really defer the implementation of lifetimes and copying semantics and so on until later; you have to add clone() at the beginning, whereas Go is all copy by default (with the exception of interfaces).
But yes, Rust's enums are much nicer than what Go has.
Most functions in Go have a contract that the return values are disjoint, or mutually exclusive — they either return a valid value or an error:
This pattern so ingrained that there's hardly a single Go doc comments on the planet that says "returns value or error". The same goes for the "v, ok := ..." pattern.But this is just a pattern and not universally true. A commonly misunderstood contract is that of the `Read` method of `io.Reader`, which says that when `io.EOF` is returned, the returned count must be honoured. This is an outlier, but because the convention of disjointness is so widely adopted, many developers make this assumption (it's trivial to find repos in the wild [1] that make this mistake), and so this is, in my opinion, bad API design.
(As an aside, it's also true that multi-value returns beyond two values almost always become cumbersome and impractical, especially if said values are _also_ mutually exclusive. Structs, having named fields, are almost always better than > 2 return values.)
This kind of careless wart is typical of Go, just like other surprising edge cases like nil channels (or indeed nil anything).
I would much rather see a serious stab made at supporting real sum types, or at least mutually exclusive return values. For example, I could easily see this as being a practical syntax:
This union syntax showed up in the Ceylon language, and it's a neat pattern for a conservative language that doesn't want to venture into full-blown GADTs.Such a syntax would be a much better match for a try() function, since there's no longer any doubt about the flow of data — there's never a result returned with an error, it's always either a result or an error:
or simply support existing mechanisms for checking: I'd love to see a `case` syntax that allows real local variable names: And of course, you could have more than two values: The Go compiler can be strict here and require that every branch be satisfied or that there's a default fallback, although some might prefer that to be a "go vet" check.A full-blown sum type syntax would be awesome, though I know it's been discussed before, and been shot down, partly for performance reasons. Personally, I think it's solveable. I'd love to be able to do things like:
[1] https://github.com/search?q=%22read%28%22+%22if+err+io.EOF%2...