It's somewhat amusing to see Go rediscover old ideas in programming language theory, given the stance against PLT that the Go developers took in the early years of the language.
It's somewhat amusing to see Go rediscover old ideas in programming language theory, given the stance against PLT that the Go developers took in the early years of the language.
Nil being another big one. Even more impressively they doubled down on this mistake with typed nils. Even if you explicitly do a comparison with nil you can still shoot yourself in the foot because it was a different nil than the one you compared against.
func foo() *bar {
// ...
if something_wrong {
return nil;
}
}
var x interface{}
x = bar()
if x != nil {
// Dereference x
}
This will crash if `foo()` returns nil, because it's checking if `x == interface{}(nil)`, which is false. What you wanted to check was whether `x == *bar{nil}` or one of the other nil types that implements the interface; which must be done with `reflect.ValueOf(x).IsNil()`.https://dave.cheney.net/2017/08/09/typed-nils-in-go-2
If Dave Cheney says it hits every go programmer at least once, it caused hours of consternation for his coworkers, and it even has its own entry in the language FAQ, I don’t know what else to tell you.
Furthermore, even for experienced developers, there's a limit to how much context / rules / whatever your brain can keep. This footgun takes up space and intellectual energy that could be used for something else.
All things being equal, a language that doesn't have this kind of footgun is better than one that does: less experienced reviewers will let fewer bugs slip through, and more experienced reviewers will either spend less effort reviewing (meaning the mental energy can be used somewhere else) or will have more review capacity (meaning they'll find more bugs / improve the code more).
https://github.com/gwd/session-scheduler/blob/master/handle_...
Basically, I have several pages I'm rendering, which have common prerequisites regarding checks, and common handling processes (passing some sanitized data to a template). The *GetDisplay() functions take a structure from the "database" layer and sanitize it / process it for handing to the templates. The two *GetDisplay() functions return pointers to two different types, appropriate for the template to which they will be passed; and return nil if there's an issue.
So I have a map, `data` of type `map[string]interface{}` that I pass into the templates; and two different paths set `data["Display"]`; then at the end I want to check if either of the `*GetDisplay()` functions returned `nil`. So naturally, the first version of the code checked `data["Display"] == nil`, which was always false, since it was implicitly checking `data["Display"] == interface{}(nil)`, but the value in case of an error would be either `*DiscussionDisplay(nil)` or `*UserDisplay(nil)`.
I mean, sure, there are other ways to structure this; I could return an error or a boolean in addition to returning nil. But 1) the only reason to do that is to work around this language limitation 2) it's a "foot gun" that it's easy to fall into.
And sure, a golang developer who'd shot themselves in the foot a few times with this would catch it during review; but I don't think a bunch of newer developers would catch it, even if they had extensive experience in other languages.
data := map[string]interface{}{}
So this is the problem, basically. Go isn't a dynamically typed language, and doesn't really let you create an arbitrary map of keys to objects like e.g. Javascript or Python does. Any time you see `map[something]interface{}` that's a huge red flag that something is fucky. In your case you want to define `data` as a struct type with a Display field (and whatever else). if ... reflect.ValueOf(display).IsNil() {
Any use of `package reflect` in application code is a similarly huge red flag. 99 times out of 100 it's a design error that ought to be fixed.So first of all, the reason things are defined that way is to interact with the golang templating libraries. Secondly, your suggestion wouldn't really solve the issue in this case: content of "Display" is different for each web page, and so the only way to assign both types to the same value is to make it an interface.
> Any use of `package reflect` in application code is a similarly huge red flag. 99 times out of 100 it's a design error that ought to be fixed.
I'm not using reflect for fun; there is literally no other way to check for nil with interfaces (other than manually checking nil for all possible types).
At any rate, I wrote code in a way that's intuitive, at least to a C programmer (using 'nil' value as an indicator that there was an error); the code had a bug. Sure I could have rearchitected the whole function, and if this were a commercial product I may have. But it's a simple webapp to help scheduling discussions at my project's conferences; a quick fix that robustly works around Golang's deficiencies is perfectly reasonable.
EDIT: And honestly, there are exactly three ways of addressing this:
1. Separating the two page paths, duplicating all the logic which is common to the two. This makes it less DRY, which risks checks becoming inconsistent, increasing the chance that there will be a security issue.
2. Make the *GetDisplay() functions return a second value to indicate failure. This is honestly kind of a dumb thing to do to work around a language deficiency.
3. Continue to use 'nil' to indicate failure, and fix the check to be able to properly check for nil. This can be done by listing out the various possible values of 'nil', which is ugly, annoying, and fragile (since it would silently break if we added a third type); or it can be done using reflection.
#3 is obviously the most reasonable thing to do here.
if display := data["Display"]; display == nil || reflect.ValueOf(display).IsNil() {
<code>
I mean, even ignoring all of the interface{} design errors, the simple fix here is just if _, ok := data["Display"]; !ok {
<code>
To reiterate, you should almost never need to interact with `interface{}` values in application code. If you find yourself trying to use, inspect, check, or otherwise program against `interface{}` values in application code, it almost always means that you're fighting the language, and that you need to change your approach to your problem. var typeA Interface = (*TypeA)(nil)
println(typeA == nil) // false
println(typeA == (*TypeA)(nil)) // true
Yes really
https://go.dev/play/p/sz44kJW8OuTAnd generics is an example of a language feature they took their time for, to avoid making the same mistakes as e.g. Java did, where generics ended up taking up half the language spec and compiler / runtime implementation.
There's nothing casual about Go's approach to language design, and I haven't seen any evidence that they're unaware of what other languages do.
I also haven't seen much criticism of languages other than C++ or Java.
Non PL theory but related: incorrect implementation of monotonic clocks, and then refusing to fix it because “just use google smear time bro”.
The error design can return the value, or an error, or both, for some inexplicable reason and you can check it - or not - and if your function returned some indication of severe error, and you happen to not check it, you can totes just continue your program, in who knows what invalid state. Oh and also, because the devs are apparently deathly allergic to abstractions of apparently kind, you’ve got to do this janky little if err != nil check at. every. single. point. Which occludes your fundamentally important logic in pointless line noise, with zero opportunity for streamlining or improving the logic or guarantees.
https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
However, that still doesn’t mean that it’s wrong.
(though I disagree that ".ext" is not an extension of an empty base name)
In any case - I seem to remember that they discussed the rationale for the original decision in the release notes for go 1.21, along with additional context.
For whatever it's worth, I don't see any evidence that Go is specifically antagonistic to programming language theory at all - the existence of first-class constructs like channels and closures suggests otherwise. There are always costs and tradeoffs involved in adopting certain theoretical paradigms, and PLT is subject to fashion as much as any other endeavour.
Go focusses on simplicity, and when talking about simplicity I really like this quote from Dijkstra:
"Simplicity requires hard work to be obtained and education for its appreciation, and complexity sells much better.” [0]
I think Go works hard to be simple, and sometimes that comes across as being simplistic. Indeed, I was sceptical of Go when I set out to learn it, but having spent enough time with it to consider myself a professional Go developer, I also find that enjoy coding more than I have for many years, `err != nil` notwithstanding.[0] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD10xx/E...
> a = []; for (i = 0; i < 3; i++) a.push(() => i); a.map(f => f())
[ 3, 3, 3 ]
> a = []; for (let i = 0; i < 3; i++) a.push(() => i); a.map(f => f())
[ 0, 1, 2 ]
> a = []; for (i of "abc") a.push(() => i); a.map(f => f())
[ 'c', 'c', 'c' ]
> a = []; for (const i of "abc") a.push(() => i); a.map(f => f())
[ 'a', 'b', 'c' ] var _loop = function (i) {
a.push(() => i);
};
for (var i = 0; i < 3; i++) {
_loop(i);
}
The key is that the scoping happens for each iteration, not around the entire loop. That detail is nonobvious, given how many other languages have gotten it wrong, but I wouldn’t say it’s wild.(If you’re curious how Babel deals with the more complicated cases of break/continue, labelled break/continue, and return, try it out at https://babeljs.io/repl.)
"It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical."
And not including sum types despite having a sum-type-shaped hole in the language (`if err != nil`).
And some of the discussion about "why no generics" seemed kind of divorced from existing PL knowledge on the topic.
Go is just simply badly designed, relying on hard-coded functionality a lot.
Go also has some really weird stuff in it, such as named return values.
Frankly, the lack of sum types hurts the most. The language would just be a lot better with a unifying Result type in the library. And don't give me any of that "oh, they tried to keep the language simple!" stuff.
Intuitively, sum types are laughably simple. Everyone understands "It's one of these possible values, so you need to check which one it is and then handle that situation." They are more simple than enums on a conceptual level! Sum types are just not how C-programmers think about the world.
There is a lot of discussion of sum types in Go at https://go.dev/issue/19412.
I also vehemently disagree with them, and I think that the way code is factually written in practice is on my side: People who propose sum types commonly refer to the Option<T> or Result<S,E> types in Rust. These are types which are almost exclusively used as return types.
Interface types are the opposite. They're used as input types, and almost never to distinguish between a concrete, finite list of alternatives. They are used to describe a contract, to ensure that an input type fulfills a minimal set of requirements, while keeping the API open enough that other people can define their own instances.
The fact that Go in fact does not use interface types for its error handling is a pretty good argument in favor of that, I'd say.
The thing is, at this point it doesn't matter, sadly. Adding sum-types to the language now would be unsatisfying. You would really need proper integration as the primary tool for error handling in the standard library (and the ecosystem), and that's unlikely to happen, even less likely than a Go 2.0.
EDIT: Just to make it clear, I think not wanting to add sum types to the language is understandable at this point. The real shame is that they were not in the language from the beginning.
(Separately, I'm not entirely sure what you mean when you say that Go doesn't use interface types for its error handling, since the language-defined error type is in fact an interface type.)
Frankly, I'm just the type of person who doesn't understand why it is possible to silently drop error values in Go (even accidentally), see
f, err := os.Open("foo.txt")
g, err := os.Open("bar.txt")
if err != nil {
return
}
while the language is simultaneously very strict about eg. unused imports.It seems like a pretty severe flaw for a language that takes pride in its explicit error handling, and a deep dive into why this flaw was acceptable to the creators would be really interesting.
For now though, instead of sum types we ended up with features such as named returns (???). I imagine some of the complexity here was about not wanting to introduce an explicit tuple-type, since a Result<S, E>-type doesn't compose with multiple returns. (I feel like there should be some workaround here. Maybe anonymous structs + some sort of struct/tuple unpacking, but I could see it getting gnarly.)
> I'm not entirely sure what you mean when you say that Go doesn't use interface types for its error handling, since the language-defined error type is in fact an interface type.
What I meant is that this specific usecase of sum-types (ie. error-unwrapping) is not something that interfaces in Go are used for. Error-handling in Go is done via multiple return values. This goes against the common claim that "sum types and interfaces are too similar/have the same uses", and should count for something, considering that explicit error handling is a big component of Go.
I'm not personally concerned about examples like os.Open, where the result must be used. Sure, you can ignore the error. But a failure will show up very quickly. I'm not saying that this is not an issue at all, but I believe it's a less important one.
Part of the reason for Go's behavior is the idea that "errors are values" (https://go.dev/blog/errors-are-values). And part of it is that the simple fmt.Println("Hello, world") doesn't require any error handling. And part of it is the desire to make error handling clear and explicit in the program, even if the result is verbose.
Rob Pike and Ken Thompson are theory-unaware. Sure. Yes. This position is good and defensible.
I misspent a few thousand hours of my life on the 9fans mailing list long ago when Pike was very active on it, and my non-joking assessment is that ever since he finished his PhD or shortly after, Pike has probably felt he knows all he needs to learn about programming-language design except for the things he and the people in his immediate social environment invent.
Bell Labs was never good at designing programming languages. Did you know that in the Bourne shell (and possibly in all the other shells) you can have a statement of the form $foo = bar which will assign bar to the variable whose name is the value of foo? (Emacs Lisp, an old language, has the same functionality in the form of a function named "set", but most Emacs Lisp programmers know to avoid it.) Well, I found that statement in a shell script written by one the Bell Labs guys, and the shell script was not doing anything fancy like interpreting a programming language or defining a new PL feature (not that it is sane to do either of things in a Unix shell).
None of what I say is more than a wisp of a reason not to choose Golang IMHO.
I think Go is a perfectly fine language, and I respect the goal to stay clear of complexity, but when looking at any particular Go feature, then it's easier to explain the decision with "They are used to thinking in terms of C-idioms." than with any particular brilliance or awareness of PL-theory.
Sum types have been discussed since before the initial open source release of the language, at least according to some of the issue threads such as https://github.com/golang/go/issues/19412
If they're laughably simple, then please contribute a proposal for how to add them to the language. You'll find no one is really fighting against the concept of sum types.
The thing which I am personally salty about is that they weren't there to begin with, and that the language wasn't built with them in mind.
Could an LLM coupled with a constraint solver create the perfect language (for particular domains, e.g., performance)? Or, just use Rust ;-)?
In this particular case I imagine the issue was uncovered some time in the 1970s, which is when lexical scoping came to the fore in Scheme (in the US) and ML (in Europe). It's a fairly natural problem to run into if you're either thinking deeply about the interaction between closures (which capture environments) and loop constructs, or if you're programming in a language that supports these features.