HN user

Merovius

729 karma
Posts15
Comments236
View on HN

Using conventional commits gives you a framework and mechanism that undoubtedly improves contribution semantics.

I do not want to contribute to a project using conventional commits. I have consistently found, that I am unable to decide what the "type" of a commit even is and I feel unnaturally caged in into how I would split up commits, by having them be restricted to types (it doesn't help that the conventional commits I've seen appear to decide the type by fair dice roll).

Discouraging contributions does not "undoubtedly improve contribution semantics".

When I said "you need a runtime implementation of generics" I did not mean "you need dynamic dispatch". I meant "you need a type-checker and at least limited code generator at runtime".

Similar to what java does.

Yes, if you completely forego the static implementation, you can deal with just one. Then all code is slow.

Java can get away with it, because it has a JIT compiler – so it isn't subject to the same "no runtime code generation" design restriction Go is. That also means, it can run on fewer platforms than Go.

add a new type of interface that is only for use in generic constraints

Yes, that would be feasible. It has been ruled out, because we really don't want to have interfaces that can not be used as types. They currently exist, but the goal is to remove that restriction, not to add more of the same.

I'll note that either way, at that point we are firmly outside the realm of this particular thread. It's no longer what I was responding to.

I'm not sure what you mean. Perhaps you are referring to the reflect package? In that case, yes, that exists. But it is limited in its power (for example, it doesn't allow to create types with methods – precisely because of the difficulties we are talking about) and a comparatively frequent source of bugs. If anything, it provides pretty strong evidence for the problems with this approach.

More specifically, it is because of interface type assertions – the fact that if you have a value of some interface type (e.g. `any`), you can dynamically assert that it is another interface type (e.g. `io.Reader`). A good example of that is `io.Copy`: https://cs.opensource.google/go/go/+/refs/tags/go1.26.3:src/...

This aspect is what prevents you from statically knowing which interface-implementations you need to generate for a specific concrete type. There could always be new ones added at runtime.

FWIW I found, so far, that bringing up dyn-compatibility to Rust people was very useful in helping them understand why Go's interfaces won't ever have generic methods.

The one additional piece of information you need is that in Go, all interfaces are supposed to be trait objects. The exception are union-elements, but that's really a restriction the Go team is trying to remove, not a model to base more features on.

but the reason against using runtime reflection is mostly that it's slow.

More specifically, it is that it would introduce surprising performance cliffs – code becoming surprisingly slow due to seemingly unrelated changes.

Though BTQH I think an even more important argument is that you would need to have effectively two generics implementations, one working at runtime and one working at compile time. That's a lot of complexity, with surprising failure modes if these two are not bug-compatible.

But that runtime reflection is exactly how you would work around it today.

I think the overwhelming majority of people will "work around it" by just not trying to use generic methods.

To be clear, I have no skin in the game here. I thought the point you made sounded plausible and as I have zero experience or expertise, I wouldn't argue against it.

I just thought it's ridiculous - and kind of funny - to deny making the claim you literally made. I'm not sure you have a lot of legs to stand on, accusing others of "anti-science thinking" and a "failure of reading comprehension" when asking us to ignore the clear, textual evidence of that contradiction.

For it to be a "narrative", there would need to be an additional claim that this specific case and context, which is factual, generalizes to most unrelated cases.

Says who? That seems a very narrow and unusual definition of what makes a "narrative", bent to your purpose. It seems to me, a "narrative" in common parlance just means "telling a story" or "relaying a sequence of events". I honestly have never seen someone use the word to imply generalization (doesn't mean no one ever did, of course).

In any case, given that you responded to a comment talking about the two examples of Texas and Hawaii with an example about California and an "actually", it seems pretty fair to me to say, that you even fulfilled this artificially narrowed definition.

I mean, come on, you have got to admit that you have at least been unclear, if you didn't intend to make this argument. Instead of just defensively flinging insults.

Wow that post is bad. The author clearly never actually attempted to understand what POSWID actually means and where it is coming from. Perhaps, instead of looking at Twitter, they should have opened Wikipedia. Or, better yet, Stafford Beers books (though admittedly, he was a pretty atrocious writer).

The follow-up is slightly better. But still not very convincing, IMO. They get far too stuck on a literal interpretation. Of something that self-describes as a heuristic.

There was no "drilling is good for the environment" narrative.

Oil drilling actually made the water cleaner.

it's a known problem and civilization hasn't collapsed.

Waiting for civilizational collapse to justify regulation seems like an unhelpful standard TBH. In fact, I would argue that it's one of the main problems we are having right now: that we know our current trajectory will lead to civilizational collapse, but aren't willing to change course.

1. I'm not a driver, much less in a country with toll roads. But is it common to have per-vehicle customized toll prices? I would expect to pay a fixed per-car, per-use fee.

2. How is this dependent on privatization? Every car is registered. So it seems pretty easy to enforce taxes on cars. And to do so based on model, weight, whatever you want.

In other words, from what I can tell, making people pay their fair share seems simpler in a public system, if anything. It certainly doesn't require privatization.

FWIW I have little skin in the game, as I said, not a driver, so I would probably benefit both by having to pay less tax and by reducing overall car usage.

Go's Sweet 16 8 months ago

But, like, this is exactly as easy with every single other language that I can think of.

I mean, not exactly. Rust (or rather Cargo) requires you to declare binaries in your Cargo.toml, for example. It also, AIUI, requires a specific source layout - binaries need to be named `main.rs` or be in `src/bin`. It's a lot more ceremony and it has actively annoyed me whenever I tried out Rust.

The second part "Running go install at the root ./.." is actually terrible and risky but, still, trivial with make (a - literally - 50 year old program) or shell or just whatever.

Again, no, it is not trivial. Using make requires you to write a Makefile. Using shell requires you to write a shell script.

I'm not saying any of this is prohibitive - or even that they should convince anyone to use Go - but it is just not true to say that other languages make this just as easy as Go.

Bluesky's architecture was pretty much dictated by the premise that anyone needs to be able to see any post on the entire system, regardless of whether they have any connections with the author. That algorithmic entertainment-style feeds need to exist. You do need that firehose and other expensive infrastructure for that, there's no going around it.

Exactly this (that people want it at least - I don't think that means it needs to exist). And I think there would be a lot less frustration in the discourse of ActivityPub vs. ATproto, if we could collectively agree that you can't get this in a decentralized system. In a dense network, the number of edges scales with the square of the number of nodes. It's just not feasible to have a network that is both dense and has a large number of nodes.

I think "I prioritize virality, recommendation engines and network density, thus accept giving control over the network to a centralized and profit-oriented entity" is an entirely reasonable tradeoff to make. I just don't understand why BlueSky users don't seem to accept that it's the tradeoff they are making.

You could, of course, sell or wind down your company, which would solve all problems outlined here. But this is not an option for most entrepreneurs.

Yes, it is literally an option, you dunce. There is no law requiring you to keep ownership of a business. You might not like that option very well, but it is an option, which is infinitely better than the denizens of the GDR got.

Man, this post got my blood boiling with its callous stupidity.

Uhm correct me if I'm wrong, but… you can sell your stake and leave at will, with zero exit tax? So really, the only thing that the exit tax prevents is the company leaving the country. And you know, every time someone brings up taxing the rich, people object that this would cause capital flight. Well, capital flight is exactly what the exit tax prevents. The capital stays in the country.

And likening that to the Berlin wall, where people literally got shot dead, is honestly pretty disgusting.

AI is really good at generating repetitive patterns, like plain types, or code that implements a certain interface. If you reduce the cost of creating the verbose code [at write time] we can all enjoy the benefit of reduced complexity [at read time] without resorting to generics.

Though humans are very bad at reviewing repetitive patterns. I'd much rather review a single generic implementation, than 10 monomorphic ones, that look the same, but where I can't be sure and actually have to check.

So unless you are making the argument that generated code doesn't require review (compliance auditors would disagree) I would personally still much rather have generics.

Minor nitpick: It isn't all that difficult to come up with type structural/generic edge cases for ANY language compiler where compilation takes forever and times out in a playground. Here is a small program of ~100 lines leveraging Go Generics: https://go.dev/play/p/XttCbEhonXg

Fair point

There are common solutions for the library issue. Authors of libraries for example can force instantiations for a dummy type that checks their concepts.

But that just ensures that the code type-checks for `Dummy`. It doesn't ensure that the code type-checks for any type you can put into `foo`. And that's the point of type constraints: To give you the necessary and sufficient conditions under which a generic function can be used.

That is simply not the case with C++ templates and concepts. That doesn't mean you can't still like them. I'm not trying to talk you out of liking C++ or even preferring it over Go. I'm just trying to explain that C++ concepts where something that we looked at specifically and found that it has properties that we don't like. And that - to us - the fact that Go generics are limited in comparison is a feature, not a bug.

And let's not forget that despite specifically reducing the safety of concepts in this way, the design ended up being NP-complete anyways and you can make a compiler use functionally infinite memory and time to compile a very small program: https://godbolt.org/z/crK89TW9G

For a language like Go, that prides itself on fast compilation times it is simply unacceptable to require a SAT solver to type-check. Again, doesn't mean one has to dislike C++. But one should be able to acknowledge that it is reasonable to choose a different tradeoff.

I would actually make the argument that it explodes complexity - for the developer, instead of constraining it.

The title is a pun. Because it is about the computational complexity of checking constraints.

Just to clarify why this is a problem: it’s possible for foo and bar to be defined in different libraries maintained by different people. Potentially several layers deep. And the author of the foo library tests their code and it compiles and all of their tests pass as and everything is great.

But it turns out that’s because they only ever tested it with types for which there is no conflict (obviously the conflicts can be more subtle than my example). And now a user instantiates it with a type that does trigger the conflict. And they get an error message, for code in a library they neither maintain nor even (directly) import. And they are expected to find that code and figure out why it breaks with this type to fix their build.

Or maybe someone changes one of the constraints deep down. In a way that seems backwards compatible to them. And they test everything and it all works fine. But then one of the users upgrades to a new version of the library which is considered compatible, but the build suddenly breaks.

These kind of situations are unacceptable to the Go project. We want to ensure that they categorically can’t happen. If your library code compiles, then the constraints are correct, full stop. As long as you don’t change your external API it doesn’t matter what your dependencies do - if your library builds, so will your users.

This doesn’t have to be important to you. But it is to the Go project and that seems valid too. And it explains a lot of the limitations we added.

You need to have something that uses those templates.

Exactly. That is what I said:

because you need to know the actual type arguments used, regardless of what the constraints might say.

It is because type-checking concept code is NP complete - it is trivial to check that a particular concrete type satisfies constraints, but you can not efficiently prove or disprove that all types which satisfy one constraint also satisfy another. Which you must do to type-check code like that (and give the user a helpful error message such as “this is fundamentally not satisfiable, your constraints are broken”).

And it’s one of the shortcomings of C++ templates that Go was consciously trying to avoid. Go’s generics are intentionally limited so you can only express constraints for which you can efficiently do such proofs.

I described the details a while back: https://blog.merovius.de/posts/2024-01-05_constraining_compl...

It seems fairly clear to me, that it is preferable to import `rsc.io/omap` over having to implement a self-balancing binary search tree?

As I said in the other comment, I'm not a C++ user, so I'm relying on cargo-culting and copy-paste. But I think gcc disagrees - otherwise this would not compile, as line 14 is provably invalid: https://godbolt.org/z/P8sWKbEGP

Or am I grossly holding this wrong?

C++ templates are duck typed at compile time.

"Compile time" is not the right distinction. This is about "instantiation time". Go's implementation specifically allows to type-check the body and the call separately. That is, if you import a third-party package and call a generic function, all the type checker needs to look at to prove correctness is the signature of the function. It can ignore the body.

This is especially relevant, if you call a generic function from a generic function. For C++, proving that such a call is correct is, in general, NP-complete (it directly maps to the SAT problem, you need to prove that every solution to one arbitrary boolean formula satisfies a different boolean formula). So the designers made the conscious decision to just not do that, instead delaying that check to the point at which the concrete type used to instantiate the generic function is known (because checking that a specific assignment satisfies a boolean formula is trivial). But that also means that you have to (recursively) type-check a generic function again and again for every type argument provided, which can drive up compilation time.

A demonstration is this program, which makes gcc consume functionally infinite amount of memory and time: https://godbolt.org/z/crK89TW9G (clang is a little bit more clever, but can ultimately be defeated using a similar mechanism).

Avoiding these problems is a specific cause for a lot of the limitations with Go's generics.

Look at Haskell type classes or Rust's traits for some classic examples of how to 'type' your generics. (And compare to what Go and C++ are doing.)

Yes, those are a different beasts altogether and the differences between what Go is doing and what Haskell and Rust are doing requires different explanations.

Though it's illustrative, because it turns out Rust also intentionally limited their generics implementation, to solve the kinds of performance problems Go is worried about. Specifically, Rust has the concept of "Dyn compatibility" (formerly "Object safety") which exists because otherwise Rusts goal of zero-cost abstractions would be broken. Haskell doesn't have this problem and will happily allow you to use the less efficient but more powerful types.

(All of this should have the caveat that I'm not an expert in or even a user of any of these languages. It's half-knowledge and I might be wrong or things might have changed since I last looked)

Maybe when you have code that works with the `interface{}` a lot (e.g. unknown JSON data) you'll have a use case for it.

I think in those cases, generics are specifically kind of pointless. Because you will inherently need to use `reflect` anyways. Generics are only helpful if you do know things about your types.

Generics are most useful for people who write special-purpose data structures. And hence for people who need such special-purpose data structures but don't want to implement them themself. The prototypical example is a lock-free map, which you only need, if you really need to solve performance problems and which specific kind of lock-free map you need depends very heavily on your workload. `sync.Map` is famously only really useful for mostly write-once caches, because that's what its optimized for.

The vast majority of people don't need such special-purpose data structures and can get by just fine with a `map` and a mutex. But Go has reach the level of adoption, where it can only really grow further, if it can also address the kinds of use-cases which do need something more specific.

I don't disagree that Go's generics are pretty limited. But I find it a strange complaint, when contrasted with C++ templates. Which, as I understand, are literally not part of the type system and thus there seems to be a far stronger case, that they can not be called generics.

The main difference between Go's generics and C++ templates (and where some of the restrictions come from) is that Go insists that you can type-check both the body of a generic function and the call to it, without one having to know about the other. My understanding is, that with C++ templates (even including concepts), the type checking can only happen at the call-site, because you need to know the actual type arguments used, regardless of what the constraints might say.

And this decision leads to most of the complaints I've heard about C++ generics. The long compile times, the verbose error messages and the hard to debug type-errors.

So, if you prefer C++ templates, that's fair enough. But the limitations are there to address complaints many other people had about C++ templates specifically. And that seems a reasonable decision to me, as well.

In practice, currently, that depends on inlining decisions. If the function taking the function (say `node.insert`) is inlined, then yes. There are also other optimizations, like escape analysis, that matter here: the compiler can prove that the arguments to `node.insert` only escape into the `cmp` passed in. That decision is kept as metadata on `node.insert` even if it is not inlined. So if you pass a method expression to it, it can actually look at that and decide that the arguments don't escape from it either and hence that they don't escape overall. Whereas if you pass a `func` field, it can make no assumptions.

My larger point though, is that with the `func` field the compiler can't optimize things even in principle. A user could always reassign this field (if nothing else using `*t = *new(FuncTree)`). So the compiler has to treat it as a dynamic call categorically. If the `func` is passed as a function, then at least in principle, it can prove that this function can't get modified during the call so can make optimization decisions based on what is being passed to it. For example, even without inlining, a future compiler might decide to compile two versions of `node.insert`, one with general dynamic calls and one specific one for a specific static function.

My philosophy when it comes to API decisions that impact performance is, not to make them too dependent on what the compiler is doing today, but just to take care there is enough information there, that the compiler can do an optimization in principle - which means it either will do it today, or we can make it smarter in the future, if it becomes a problem.