Is it pride that let's "industry" languages ignore decades of PL theory and research? The "design by committee" problem? Or something else entirely?
So, in fact, industry didn't ignore PL theory and research at all.
Instead of belly aching about everyone who did it wrong, why not explain what is wrong and why you think so.
I mean, I think think Rust didn't do it perfect because I often want to use Box<dyn MyTrait> which is a hassle because I need to mark every function ?Sized if I want to pass a reference to my `x: &T...where T: MyTrait + ?Sized` and I don't enjoy that.
Sometimes, reality bites.
Even C++ arguably falls into that category to an extent. Its template system definitely could have been designed better, and using Haskell-like type signatures like Rust does is arguably a better approach… but Rust trait signatures tend to be more complex and less elegant than Haskell ones, because the simple and elegant ones have runtime overhead that Rust, like C++, isn’t willing to accept. (For example, see functions being a trait as opposed to a type.) And even Rust struggles to match C++ in metaprogramming expressivity, which also helps C++ reduce runtime overhead. Upcoming Rust features like `const fn` and specialization will help with that, but both have been ‘upcoming’ for many years…
And it's fine if the error messages are helpful, but this added implicit complexity to the language rightly gives folks the heebie-jeebies (se the recent comments for Rust's GAT patches)
The other problem is incentive and design approach. The academic approach to PL design is to carefully develop the type system and understand the way the rules interact, proving things like soundness, decidability, etc. the industry approach to software development is MVP. These two approaches are incompatible and lead to different language designs, because you can't slap a carefully designed system on top of a recklessly developed MVP that has been bumped incrementally over a long time. At least not without major time consuming rewrites.
FWIW I've used Rust for a bit and I disagree that they "got it right". I'm not saying Rust is bad, I like the language. But it's not as if their generics implementation does not have issues. It's just that they are different issues.
Its not academia, the grade is getting fast funding or revenue because any language is fine and any performance is good enough because the bottlenecks are somewhere else or irrelevant
> ...
> This is especially important for environments like the Go playground, which regularly compiles untrusted code.
To be clear here, the tradeoff being made is that it is more important that the compiler is fast than that it is able to tell programmers about clear bugs in their code.
I see the justification of the playground as unconvincing too. You can run 'for {}' in the playground, and you'll get "timeout running program". They already have solved the problem for the playground because obviously at runtime a go program can take infinite time, and it doesn't seem like it's any harder to also constrain compilation in the same way.
The actual cost that the go compiler being fast has is that other parts of go are slow. In other languages, a macro will execute as part of compilation, and incremental compilation will work with it. In go, if you want a macro, you write a separate go program that generates code, write a "//go:generate" comment, and then have no way of knowing when to run it, no way of having incremental compilation with it, and it invariably ends up being slower than the compiler expanding it itself would have been.
Now, if you want to know if you have an impossible type set, you'll run "go vet" (which likely doesn't do this validation, I haven't checked) or one of the various linting tools, it'll have to re-parse and re-compile all the code, it won't cache, and it'll ultimately give you the same answer the compiler could have given you.
I suppose this is superior in one way: when performing such generation and validation, you can do it against only your own code, rather than against third party code you import, while if it were part of the compiler directly it would typically end up running against all compilation units, even third party ones....
In practice though, even for "slow to compile" languages, I find that I spend more time finding and fixing bugs than I do waiting for compilation, especially with incremental compilation. On the other hand, with go, I spend less time total compiling code, more time total fixing bugs, and more time total running golangci-lint and various other bits of code-generation.
I spend exactly 0 time worrying that some adversarial third-party dependency I import will make my compile-times exponential, both in go where it can't happen by design, but also in other languages where it could happen, but you know, doesn't.
You run it every time you do a commit (at least), and then you check in the changes.
Also, you probably run it during development by using a Makefile that will automatically run it whenever the input changes.
This means that downstream packages never have to run it. They don't even have to have the code generation tool installed. This means a package maintainer can use any code generation tool they like.
It was probably done that way due to experience with Bazel, where doing a build means you need to build and run code generators written in multiple programming languages. It does indeed slow things down.
In this case, you know to run "go generate" because the feature you just added doesn't work. (Though I tend to add a CI rule to make sure that the generator in CI produces the file that is checked into the repo. Because, yeah, you can forget, and it's nice for the computer to double check that.)
The actual tradeoff being made here is more like "it's better to give a good error message in 1s than a great error message in 10s." That doesn't seem obviously wrong to me.
1. Macros are just one use case replaced by go:generate step.
2. go:generated files are usually not the most moving parts while developing (except of course while you write the generator or its input. In fact go:embed has allowed to move some generated files to compile time and have not seen the need to replace go:generate with go:embed for those. (Does anyone has a compile benchmark?)
2. The Go compiler already uses caches and we can expect more and more caching will happen in future releases.
So I'm really not concerned by the compile time of files produced by go:generate instead of macros.
I'm not sure that's entirely true. Or it seems at least misleading.
"Fast" here means "guaranteed to finish before the heat death of the universe", in the worst case.
And as I'm trying to explain in the post, it is easier to explain clear bugs to the programmer, if you don't have to rely on solving NP-complete problems. NP-complete problems are notoriously hard to report errors for, which the user understands.
My rust experience denies this.
I've never seen a concrete type in an interface before, so I decided to try it:
package main
type A interface {
int
M()
}
func f(x A) { // line 8
x.M()
}
It is indeed accepted if it's there by itself, but it is rejected if you try to use the interface, which is what function f does: ./main.go:8:10: interface contains type constraints
I think this ends up being a very boring reason for rejection and is somewhat irrelevant to the post that you can't actually put concrete types in interface{} definitions. It looked weird to me so I had to try it.Since this is in the context of generics, I think the author might have been thinking about embedding types like comparable, which are interfaces (comparable is interestingly defined as `type comparable interface { comparable }`). You can, of course, put interfaces in interfaces in general (see io.ReadCloser and friends).
For the comparable case:
type A interface {
comparable
M()
}
func p(x A) {
x.M()
}
This gets its own error: ./main.go:8:10: interface is (or embeds) comparable
Very clever, because yeah, there is nothing stopping you from writing `func f(x comparable) {}`. Except this explicit check.I don't know what the point of this comment is, since 99% of the article is not about this code block, but I guess I found it interesting.
> interface is (or embeds) comparable
Currently, not all interface types may be used as value types. Such types include comparable.
It is very possible that all interface types could be used as value types in future Go versions, and comparable might be able to be used as value types as early as Go 1.20.
You can't use it as a type, but you can use it as a constraint: https://go.dev/play/p/LnmudHIjAoQ
Of course, you can then not actually instantiate that function (as the type set is empty). But using concrete types in interfaces is very possible. And it's indeed why the |-operator was introduced. It allows you to use operators: https://go.dev/play/p/I3eMDBwIcHA
You can put concrete types in interface definitions as of 1.18. See the spec: https://go.dev/ref/spec#Interface_types
Checking type sets is a work for a linter. But the developer should have caught the problem with a simple unit test.
I'm glad that the specification relieves the compiler writers from that the non-empty check task.
Linting can happen asynchronously in CI.