If these are important, you can get a pretty good approximation with structs, e.g.
type Point struct{ X, Y float64 }
func EuclidianDistance(p Point) float64 { ... }
d := EuclidianDistance(Point{X: 3.0, Y: 9.6})
—
Absence of sum/discriminated-union types
Because reposurgeon events can have any one of a set of types (Blob, Tag, Commit, Callout, Passthrough, Reset) I found myself writing a lot of stupid boilerplate code like this:
Yeah, type assertions usually aren’t the way to go, as they’re not runtime-safe. Generics will make this better but the best approach right now is a non-discriminated union type e.g.
type Event struct {
*Blob
*Tag
*Commit
...
}
and checking for nils.
—
Catchable exceptions require silly contortions
Go does not have exceptions. Using panic/recover as ersatz exceptions is essentially always a design error. If you ever find yourself trying to recreate an exception control flow, you are almost certainly on a bad path.
I kept looking at places where existing nonlocal control transfers in Python could be replaced by explicit Go-style passing upwards of an error status. But I noticed that there were a significant percentage of cases in which doing this made the code more difficult to follow rather than easier . . . An early reviewer pointed out that if the Go code were an entire function it could be expressed something like this . . . That’s still a lot of eyeball friction compared to functional-style with exceptions. And it gets worse faster as the number of stages rises.
Effective use of Go requires the programmer to choose to see that example function as wonderfully explicit, rather than full of “eyeball friction”. I’ve said this many times: in Go, the happy path is equally important to the sad path. You have to just accept that and work with it, or you’ll hate the language.
—
Figuring out how to do type-safe polymorphism in the event list
Go does not currently support type polymorphism. Generics will change that, but at the moment, trying to approximate it is a sure-fire way to frustrate yourself and produce bad code.
—
Keyword arguments should be added to the language.
Emphatically no.
True iterators are felt by their absence and would be easy to add.
Not without sacrificing the performance goals of the language.
Any enlargement in the range of what can be declared const would be good for safety and expressiveness.
+1.
Yes, throw()/catch() needs to be writeable in the language. Two minimal relaxations of compilation rules would make writing it possible.
Emphatically no. One of Go’s fundamental opinions is that exceptions are bad. You have to buy in to that. If you don’t want to, no problem! Go isn’t the language for you.
A technical writer with an outside-in view of the language should be hired on to do an edit pass and reorganization of the documents.
+1.
Lookbehinds should be added to the regexp library.
There are good reasons this doesn’t exist currently. It’s unlikely to change.
There are good reasons this doesn’t exist currently.
This comment is Go community in the nutshell. How many people were happy about lack of generics, and now they are happy about their future presence. Same think can happen here.
Every week, I get another bullshit security notification from Depndabot that my JavaScript depends in some tangential way on a package with a malicious regex vulnerability. If people want lookbehinds, whatever, but it shouldn’t be in the standard library because it’s a total safety hazard.
Package regexp implements RE2 which does not support backtracking or lookbehind (are they the same thing? I’m not totally sure) by design. Adding support for these capabilities would mean switching to a different flavor of regexp, which would break the compatibility guarantee.
There are several third-party PCRE modules available, though I’m not sure what compromises they may make.
Not without sacrificing the performance goals of the language.
Is that true? I don’t necessarily agree that Go should get iterators, but I can’t wrap my head around why they would make Go lose performance. Couldn’t it be as simple as having range accept an interface like: (yes yes, no generics in the language, but range is special)
Broadly I agree, but I’m a little confused on this point:
Not without sacrificing the performance goals of the language.
Iterators should be zero cost, no? Is your concern that the next() method won’t be properly inlined? I also haven’t been following the generics saga closely, but presumably we’ll have typesafe generic iterators either via the stdlib or third-party once generics land.
Go definitely has an inliner, but it probably can’t see across package boundaries to your point. Personally I would like to know more about the compilation cost of link-time optimizations which would optimize across compilation units.
Really? I thought packages were compilation units and Go didn’t do LTO (I assume LTO is required for optimizing across compilation units, but I’m pretty out of my depth)?
It is not true that Go prefers to sacrifice runtime speed for compilation speed. Rather, compilation speed is a first order priority of the language, and compiler optimizations are made only when people decide to make them. The lack of a register based calling convention, for example, didn’t become the lowest hanging performance fruit until relatively recently, so effort was spent elsewhere. That’s fine.
Some of these I agree are real pain points, but in the case of others the author is suggesting making changes to the core language with little argument beyond accommodating what is a frankly really bizarre situation – wanting to transliterate a large python codebase into Go without having to make non-trivial changes to the way the program is written. In particular, you can get into arguments about the value of exceptions, but making porting code from another language easier strikes me as a dramatically weak argument.
Unrelatedly, there’s a slightly safer version of the type switch you can use:
switch v := child.(type) {
case *Commit:
successorBranches.Add(v.branch)
case *Callout:
complain("internal error: callouts do not have branches: %s", child.idMe())
default:
panic("in tags method, unexpected type in child list")
}
…which at least doesn’t require the type assertion. You can also kindof limit what variants are possible, by (ab)using an interface
The method being lower case will mean that you can’t extend the type by defining that method in other packages, though you can embed the interface, so it’s not foolproof:
type Foo struct {
pkg.MyUnion
}
…and it still doesn’t do exhaustiveness checking for you, though it wouldn’t be that hard to build a tool that recognized this pattern and did so.
This pattern sees use in the go/ast package.
It is definitely a hack though, and I miss sum types when working in Go.
I feel like a lot of the language limitations described in this post are based on expectations from other languages. Some of them are valid points that have known workarounds (e.g. representing sum types as a types satisfying an interface with private method), and I absolutely agree that current Go documentation doesn’t make a good introduction to the “Go mindset”. That said, Go is not Python and some things work and/or should be implemented differently.
For example (exception control flow): if the transformations are expected to fail, they should return an error (or at least a bool indicating a failure). To run these in pipeline, you can define transform function to return an error and accept the result of the previous transformation (suboptimal, but closest to the Python implementation).
Or we could define a method on T that accepts a list of func(T) (T, error) transformations and either applies all transformations in order or returns early on error.
It should be possible to write a generic implementation with Go 1.18, though I haven’t been following the generics development and I’m not sure what the current syntax is 😅
If these are important, you can get a pretty good approximation with structs, e.g.
—
Yeah, type assertions usually aren’t the way to go, as they’re not runtime-safe. Generics will make this better but the best approach right now is a non-discriminated union type e.g.
and checking for nils.
—
Go does not have exceptions. Using panic/recover as ersatz exceptions is essentially always a design error. If you ever find yourself trying to recreate an exception control flow, you are almost certainly on a bad path.
Effective use of Go requires the programmer to choose to see that example function as wonderfully explicit, rather than full of “eyeball friction”. I’ve said this many times: in Go, the happy path is equally important to the sad path. You have to just accept that and work with it, or you’ll hate the language.
—
Go does not currently support type polymorphism. Generics will change that, but at the moment, trying to approximate it is a sure-fire way to frustrate yourself and produce bad code.
—
Emphatically no.
Not without sacrificing the performance goals of the language.
+1.
Emphatically no. One of Go’s fundamental opinions is that exceptions are bad. You have to buy in to that. If you don’t want to, no problem! Go isn’t the language for you.
+1.
There are good reasons this doesn’t exist currently. It’s unlikely to change.
This comment is Go community in the nutshell. How many people were happy about lack of generics, and now they are happy about their future presence. Same think can happen here.
Every week, I get another bullshit security notification from Depndabot that my JavaScript depends in some tangential way on a package with a malicious regex vulnerability. If people want lookbehinds, whatever, but it shouldn’t be in the standard library because it’s a total safety hazard.
Package regexp implements RE2 which does not support backtracking or lookbehind (are they the same thing? I’m not totally sure) by design. Adding support for these capabilities would mean switching to a different flavor of regexp, which would break the compatibility guarantee.
There are several third-party PCRE modules available, though I’m not sure what compromises they may make.
https://github.com/google/re2/wiki/WhyRE2
Context for the Regexp limitations: https://swtch.com/~rsc/regexp/regexp1.html
Insightful comment. Thank you.
Is that true? I don’t necessarily agree that Go should get iterators, but I can’t wrap my head around why they would make Go lose performance. Couldn’t it be as simple as having
range
accept an interface like: (yes yes, no generics in the language, butrange
is special)I’m unclear why no one has brought up channels and go routines as an alternative to iterators. Channel size of 1, blocks on send making it “lazy”.
The linked blog post in the original post does go into that: https://ewencp.org/blog/golang-iterators/index.html
Blargh! you are correct. I overlooked that when I read the post. :)
They’re too slow. See a recent issue discussion: https://github.com/golang/go/issues/48567
Broadly I agree, but I’m a little confused on this point:
Iterators should be zero cost, no? Is your concern that the
next()
method won’t be properly inlined? I also haven’t been following the generics saga closely, but presumably we’ll have typesafe generic iterators either via the stdlib or third-party once generics land.My guess is that the go compiler isn’t optimizing hard enough. Until very recently, it didn’t even put arguments in registers when calling functions.
Rust made iterators “zero-cost”, but the real cost is in compilation time, and Go would rather sacrifice runtime speed for compilation speed.
Go definitely has an inliner, but it probably can’t see across package boundaries to your point. Personally I would like to know more about the compilation cost of link-time optimizations which would optimize across compilation units.
It does indeed inline across package boundaries.
Really? I thought packages were compilation units and Go didn’t do LTO (I assume LTO is required for optimizing across compilation units, but I’m pretty out of my depth)?
I’m no expert, but here’s a basic example https://godbolt.org/z/98xKj1dfc
Hmm, I’m suspicious about stdlib packages because I know they are treated specially by the compiler, at least in some cases.
I believe you’re referring to compiler intrinsics. But from what I’ve seen, the compiler doesn’t special case inlining rules for the stdlib.
It is not true that Go prefers to sacrifice runtime speed for compilation speed. Rather, compilation speed is a first order priority of the language, and compiler optimizations are made only when people decide to make them. The lack of a register based calling convention, for example, didn’t become the lowest hanging performance fruit until relatively recently, so effort was spent elsewhere. That’s fine.
Some of these I agree are real pain points, but in the case of others the author is suggesting making changes to the core language with little argument beyond accommodating what is a frankly really bizarre situation – wanting to transliterate a large python codebase into Go without having to make non-trivial changes to the way the program is written. In particular, you can get into arguments about the value of exceptions, but making porting code from another language easier strikes me as a dramatically weak argument.
Unrelatedly, there’s a slightly safer version of the type switch you can use:
…which at least doesn’t require the type assertion. You can also kindof limit what variants are possible, by (ab)using an interface
The method being lower case will mean that you can’t extend the type by defining that method in other packages, though you can embed the interface, so it’s not foolproof:
…and it still doesn’t do exhaustiveness checking for you, though it wouldn’t be that hard to build a tool that recognized this pattern and did so.
This pattern sees use in the
go/ast
package.It is definitely a hack though, and I miss sum types when working in Go.
The generics implementation adds a sum type, but for now at least you’re only allowed to use it as a generic type constraint.
You can’t yet declare a variable of type myunion, but it will probably come eventually.
I feel like a lot of the language limitations described in this post are based on expectations from other languages. Some of them are valid points that have known workarounds (e.g. representing sum types as a types satisfying an interface with private method), and I absolutely agree that current Go documentation doesn’t make a good introduction to the “Go mindset”. That said, Go is not Python and some things work and/or should be implemented differently.
For example (exception control flow): if the transformations are expected to fail, they should return an error (or at least a bool indicating a failure). To run these in pipeline, you can define transform function to return an error and accept the result of the previous transformation (suboptimal, but closest to the Python implementation).
Or we could define a method on T that accepts a list of
func(T) (T, error)
transformations and either applies all transformations in order or returns early on error.IMO that’s way more concise compared to
f(g(h(v)))
right-to-left transformations.Is there a way to avoid reimplementing it for every type
T
? It looks a lot likeEndoM
wherem ~ Either e
: https://hackage.haskell.org/package/foldl-1.4.12/docs/Control-Foldl.html#t:EndoMIt should be possible to write a generic implementation with Go 1.18, though I haven’t been following the generics development and I’m not sure what the current syntax is 😅