Live data from Hacker News

Generic interfaces

go.dev

61–70 of 78 posts

Re: Generic interfaces

#61
post #31

Earlier quoted context omitted.

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 ty…

C++ templates are duck typed at compile time. 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.)

> C++ templates are duck typed at compile time.

This is not really true after C++ 20. C++ templates can leverage concepts that specify compile-time constraints and type checking on template parameters during template argument substitution

Re: Generic interfaces

#62
post #14

I find C++ templates simpler than Go generics. With C++, you can at-least get to a design solution. With Go generics: oops this is not possible, oops that is not possible - all because of strange language limitations. Go's Generics are a crippled implementation - they don't really deserve the feature title of 'generics'. (Its like saying you support regex, but don't support groups and repeat operators and you can onl…

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 ty…

> 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.

No longer true after C++ 20. When you leverage C++20 concepts in templates, type-checking happens in the template body more precisely and earlier than with unconstrained templates.

In the below, a C++ 20+ compliant compiler tries to verify that T satisfies HasBar during template argument substitution, before trying to instantiate the body

    template 
      requires HasBar
    void foo(T t) {
      t.bar();
    }
The error messages when you use concepts are also more precise and helpfully informative - like Rust generics

Re: Generic interfaces

#63
post #7

If I'm being honest, the magic of Go was lost when generics were introduced. It now feels akin to Java, which I guess was inevitable and for anyone to really take it seriously maybe it needed to get here. But I am not a fan of generics. While that level of abstraction and composability is clever, it also lends itself to more complexity and systems that can be harder to concretely understand. Just an opinion that I kn…

I was so excited when generics were going to release, and, tbh, I've barely used them. It's made some code easier to express correctly in the type system in rare cases.

I don't think I'd agree it's made the language "Java-like". That sounds like more of an indictment of the author of the code you're reviewing ;)

Re: Generic interfaces

#65
post #62

Earlier quoted context omitted.

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 ty…

> 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. No longer true after C++ 20. When you leverage C++20 concepts in templates, type-checking happens in the template body more precisely and earlier than with unconstrained templates. In the below, a C++ 20+ compliant compiler tries to verify that T satisfies HasBar…

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?

Re: Generic interfaces

#66

How is this better than rewriting containers for specific element types? Go was supposed to be simple and I can't understand any of this rubbish.

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?

Re: Generic interfaces

#67
post #62

Earlier quoted context omitted.

> 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. No longer true after C++ 20. When you leverage C++20 concepts in templates, type-checking happens in the template body more precisely and earlier than with unconstrained templates. In the below, a C++ 20+ compliant compiler tries to verify that T satisfies HasBar…

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?

You need to have something that uses those templates. In your godbolt example, add a struct S

    struct S {
      bool M() { return true; }
    };


    int main() {
      S s;
      foo(s); // this now will check foo
    }
Now you will get compile errors saying that the constraint is not satisfied and that there is no matching function for call to 'bar(S&)' at line 14.

Re: Generic interfaces

#68
post #67

Earlier quoted context omitted.

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?

You need to have something that uses those templates. In your godbolt example, add a struct S struct S { bool M() { return true; } }; int main() { S s; foo(s); // this now will check foo } Now you will get compile errors saying that the constraint is not satisfied and that there is no matching function for call to 'bar(S&)' at line 14.

> 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...

Re: Generic interfaces

#69
post #67

Earlier quoted context omitted.

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?

You need to have something that uses those templates. In your godbolt example, add a struct S struct S { bool M() { return true; } }; int main() { S s; foo(s); // this now will check foo } Now you will get compile errors saying that the constraint is not satisfied and that there is no matching function for call to 'bar(S&)' at line 14.

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.

Re: Generic interfaces

#70
post #67

Earlier quoted context omitted.

You need to have something that uses those templates. In your godbolt example, add a struct S struct S { bool M() { return true; } }; int main() { S s; foo(s); // this now will check foo } Now you will get compile errors saying that the constraint is not satisfied and that there is no matching function for call to 'bar(S&)' at line 14.

> 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 als…

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

  template void foo(Dummy);
This can be done at the consumer side as well. I don't see a big deal of this. Dummy checks are common in Go too. For example, to check if a type satisfies an interface.

   var _ MyInterface = (*MyType)(nil)
   var _ SomeInterface = GenericType[ConcreteType]{}
After all, Go checks that a type implements an interface only at the point where you assign or use it as that interface type.

Thanks for your blog post. Unfortunately, the intentional limitations make the design space a massive headache and many times lead to very convoluted API. I would actually make the argument that it explodes complexity - for the developer, instead of constraining it.

Post reply on HN