Soppo - Go, with the features it's missing
58 points by wezm
58 points by wezm
I was skeptical when clicking on this, but color me impressed, this all seems remarkably reasonable.
My only nitpicks being string interpolation being universal instead of opt-in, which I think could be a source of annoyance, and I'm not sure how you can have both seamless interop and nil safety. Also I feel like error handling is still not ergonomic enough. The docs give this example:
// Custom handling
port := parsePort(config) ? {
return fmt.Errorf("parse failed")
}
This feels barely shorter than the usual if err != nil dance, so to me it feels completely superfluous. An actually useful solution imo would be to have a shorthand for return fmt.Errorf("{message}: %w", err).
Say port := parsePort(config) ?? "port parsing failed" would extend to:
port, err := parsePort(config)
if err != nil {
return fmt.Errorf("port parsing failed: %w", err)
}
And port := parsePort(config) ?? ("port parsing on %q failed", config.IPAddress) would turn to:
port, err := parsePort(config)
if err != nil {
return fmt.Errorf("port parsing on %q failed: %w", config.IPAddress, err)
}
(Obviously in cases where (T, error) is the return value, it'd unfold into return ([default value of T], fmt.Errorf(...)))
Other than these, yes, yes, and yes. Go is so close to being a perfect "a hammer is a hammer" language, but it still has some sharp corners, that it feels like it really shouldn't have anymore.
It annoys me so much that const is such a constrained modifier in Go. I'd love if I could mark almost everything const and only do var (or :=) when I know I'll actually modify things. It may be a Rustism, but one I genuinely believe to be good for everyone involved (you're more confident about correctness, the compiler can optimize more freely, the next reader will be certain no monkey business happens dozens of lines down, etc.)
Better matching functionality would clear away the weird "check on variable X, but then variable Y becomes actually that type" syntax. And with nil checks, the compiler can take off some needless mental bookkeeping from the programmer.
I wish more languages would use a backslash to start string interpolation. Like Swift "Time: \(t) seconds" or that one Java proposal STR."Time: \{t} seconds". Using bare {} is annoying if you ever need to output {} themselves, and it adds a new type of escaping, {{ and }}. But we already have backslash reserved for escaping! Just feels more consistent to use backslash for everything.
I did this in Ninja, except where $ is the universal escape character, for just this reason. So $foo for a variable reference, $$ for a literal $, $ followed by newline to escape the newline and so on. (This let us avoid needing to escape backslashes in Windows paths.)
You could just use '/' for windows paths, as (ever since MS-DOS) it has accepted that as a path separator, although individual programs may not.
I think we considered this, but ultimately found that individual programs did not. Ninja has a bunch of goop for tracking within canonicalized paths the original backslash or forwardslash-ness of the path. I don't quite remember but it's something around whether a given program's own parsing of paths supports backslashes or not, which is to say some programs only support path strings containing backslashes, even if the underlying FS doesn't care. Possibly there were even cases where there were mixed forward and back slashes and the distinction mattered.
Great take. This is one of those little things that really isn't obvious when you look at existing designs, and is 100% obvious in retrospect as a clear improvement. <3
I find the original % syntax was already fine.
I am not a fan of this {} syntax either.
I get why they would like it (similar to rust, python, and many other languages), but I don't think it's worth it. A needless change in my opinion.
As for the backslash, it's not as easy to type on non-qwerty keyboards.
{} over % in pure formatting, not interpolation, has the advantage that you're less constrained in the specifier grammar because there's a clear end to it.
Why does it add a new kind of escaping? If you want a literal brace just use "\{". Am I missing something?
About the error handling part, I agree. The point of go is to stay simple and explicit. And I don't see the value in sparing 1 line or 2. To me, the annoyance comes when you get repetitive 3 additional error handling lines after each action that may fail, which in some kinds of programs can become very error prone (because of the noise it can add in goroutines/channel ceremonies, ...).
But then again, this topic has produced so many github issues and propositions. At this point I think there are 2 reasonable ways:
I like your solution. Deal with the 3 basic cases:
I would think a LOT about what keyword or syntax to use, because this is a big deal.
Maybe it is useful to handle any (T, U, ..., error) and not just (T, error)? (always work with the last error I mean)
I do still think the ? case here is better than the alternative when you want to create a new var in the outer scope.
port := parsePort(config) ? {
return fmt.Errorf("parse failed")
}
one might want to do something like this in the pure go case, but then port is in the inner scope
if port, err := parsePort(config); err != nil {
return fmt.Errorf("parse failed")
}
so yes, as long as the ? scopes the new var as expected I do think it's a nice addition compared to
port, err := parsePort(config)
if err != nil {
return fmt.Errorf("parse failed")
}
That said, I think your ?? suggestion would be even better since most of the time you really just want to wrap the existing error with some local context.
The first question I ask when people propose adding tagged unions to Go is how do you handle the fact that every value is default-initialised? It seems that Soppo does not really handle this:
package main
import "fmt"
type Empty enum {}
// Ideally we should not be able to construct this type:
type Foo struct {
empty Empty
}
func main() {
fmt.Println(Foo {}) // Prints "{<nil>}"
}
This is a more esoteric example (using the empty type), but it seems to behave similarly for enums like the Shape type on the homepage. I guess this you could argue Haskell is in a similar boat where all lazy types have the possibility of being undefined… but this is somewhat disappointing for a strict language. I'd also imagine that the prevalence of default values in Go APIs could make this a somewhat significant footgun, that I'd hope was documented at the very least.
If I were “fixing” Go, I'd look towards removing the necessity of a default value for all types, and use compile-time analysis to ensure types are fully initialised before they are used.
this go proposal for sum types proposes the simplest option of just having the first option be the zero value. it's actually a lot easier to reason about than, say, changing nillable pointers.
given that these changes touch on parts of go that are very core to the language (zero values especially have implications everywhere) i'm a little surprised to not hear about how the rest of the language is meant to fit around these changes, honestly.
i’d much rather have some odd default value wart for enum types and the rest of go unchanged, than a very different go that requires i rewrite almost all existing go code in service of removing zero values from the language. even a modicum of safety around nil handling would be a great boon.
Yeah, it would depend on the goals of the language - i.e. if it's meant to be purely an additive change to the language with minimal need for updating existing code, or whether it's meant to be fixing issues with Go in the process. If it's the former then the addition of enums will need some unfortunate compromises - e.g. perhaps one of the following approaches could be chosen:
The nil safety stuff seems to suggest they are aiming for it to be a breaking change, however.
I had a discussion with colleagues, where I was saying Go was slowly becoming a language of choice of mine—the answer was that they would miss algebraic data types and pattern matching, for which i fully agreed. So i'm pretty interesting in Soppo now!
I wish the Go team would just adopt these changes. Go could be the perfect language.
Soppo is great, but unfortunately the chance of widespread adoption is quite low, which eventually kills such projects.
There seems to be quite a lot of hunger out there for this sort of thing. It really takes an effort to get everyone to coalesce around one effort. My best guess would be that some effort that interoperates with dafny and golang (or some other moderately established community with complementary goals) could offer adoption benefits to both communities.
unfortunately the chance of widespread adoption is quite low, which eventually kills such projects.
Since Soppo seems to emit readable Go code, why would widespread adoption be necessary? From what I've seen, it could remain the author's own tool and still be useful to them.
As to the error handling, and the trailing ?, or ? with a capturing error variable, there was a recent GH Go discussion about such a proposal, which eventually got dropped.
Amongst other complaints was that it was very easy to miss the ?, and so make code a lot harder to read when one was trying to find error vs happy paths.
In some respects the earlier try / check proposals would seem a better fit, (easier to read / spot) if formed as a new keyword, whereas they couldn't really be added to Go itself in a backward compatible way.
In summary there is quite a lot of design proposal archaeology which could be considered for improved error handling, and one wonders if they have been considered and weighed as possibilities.