62 comments

[ 0.17 ms ] story [ 106 ms ] thread
Excellent example of clear and concise writing, introducing a subject.
Whatever you think of Go as a language, its documentation is outstanding.
Honestly it just reminds me of Rust's traits (granted I didn't read the entite spec.), could this come with operator overloading too?
It could in future but this document is staying away from overloading. I doubt Go will ever support it really but this design does not do anything to prevent it in future.
I've been doing some work in Dart/Flutter recently and I'm finding it to be a nice mix of Java/JavaScript/TypeScript/Golang. It has generics which work pretty well, although I've already found a very small bug in it [1].

Initial reading of this document makes me feel it is really over complicated compared with Dart.

[1] https://github.com/dart-lang/sdk/issues/37626

Which part is overcomplicated compared with Dart?
One thing I really like about Dart is the lack of interface objects, they are implicit. The `contract` proposal feels like having to write interfaces. The inference in Dart is really nice to work with as the compiler gets it right most of the time.

The other thing is the documentation is really easy to grok and come up to speed on:

https://dart.dev/guides/language/language-tour#generics

https://dart.dev/guides/language/sound-dart

Edit: Additionally, mocking (with Mockito-Dart) becomes trivial, which is great for unit testing:

  class Thing {
    String boo;
  }

  class MockedThing extends Mock implements Thing {}

  when(thing.boo).thenReturn('ack');
I have knowledge in Python and never developed in Go, so I can't comment the Design choices. On the form, in my humble opinion, the PEP style is easier to read : presenting rational and constraints before the proposed solution.

It would mean moving (and a bit of rewording) "Discarded ideas", "Comparison with Java", "Comparison with C++" before "Design" and after "Background".

As a reader, I prefer to be presented with the problem first (with the constraints and previously explored ideas), then be presented the logical, "obvious", proposed solution.

It is not a proposal, it is a draft.

The new draft improves the contract part. But generic declaration part still looks some verbose. There are many repetitiveness, such as the Map example: https://github.com/golang/proposal/blob/4a54a00950b56dd00964...

    func New(type K, V)(compare func(K, K) int) *Map(K, V)
    func (m *Map(K, V)) Insert(key K, val V) bool
    func (m *Map(K, V)) find(key K) **node(K, V)
    func (m *Map(K, V)) Insert(key K, val V) bool
    func (m *Map(K, V)) Find(key K) (V, bool)
    func (m *Map(K, V)) InOrder() *Iterator(K, V)

And I didn't find how to write a contract which requires a type must have some specified fields in this draft.
I would believe Generics are highly reusable code, you shouldn't have to write and read them often. The verbosity you lost in those few lines are earned every time another piece of code use them.

Also you cannot just criticize without also proposing a solution :)

> Also you cannot just criticize without also proposing a solution :)

Why not? There can be valid criticism without there necessarily being a better solution. That's actually the whole story of generics in Go. Countless proposals, ideas, and criticism. So far, there is no solution.

Generics are highly reusable is true.

But people don't read and write them often because of the language design.

Most Hindley Milner typed languages like ML and Haskell by default are generic by default and it's not harder or more complex than non-generic code.

Thinking of a function 'swap'. The signature should just be swap : (a, b) -> (b, a) instead of swap: <T, U>(x: (T, U)) => (U, T). The language design in most of languages which discourage people using generics.

In fact, non-generic code usually introduce couplings to the argument type, making things harder to change, and violates the least power principle.

This works nice for something as simple as a swap. But more complicated use-cases have requirements. I want those explicitly stated rather than inferred by a compiler. Abstracting the actually used features behind names like U: String, T: Map is can keep complexity hidden. Or would you prefer the compiler to auto-generate them for you from usage? (like U: must have ChatAt(int)->Char, Length->int; T: must have put(U), get(U), Length->Int) I certainly prefer names. And those have to be declared somewhere.
I hate this. I really wish generics were not adding to function signature, but instead to types themselves.

  func New(compare func(generic.K, generic.V) int) *Map(generic.K, generic.V)
It occurred to me that, though verbose, this syntax leaves the door open for specialization of methods, eg

    func (m *Map(K, V)) Insert(key K, val V) bool
    func (m *Map(K string, V string)) Insert(key K, val V) bool
    func (m *Map(K int, V int)) Insert(key K, val V) bool
I wish there was a way to have generics without:

* letting people being able to abuse them. C++ cough cough.

* making it clear to people reading the code what is happening.

I’ve noticed a few things:

* associated types in Rust are much clearer than generics

* one letter generics are “overly generic” and do not describe enough.

* the declaration of the type really adds verbosity

So what can a language do?

1. Maybe change the syntax to this one:

  func eat(food []generic.Type)
Or

  func eat(food []g::type)
Or something that doesn’t involve adding more <> or ()

2. Forbid one letter generic or force a description of the generic or even better: force listing all the types that are currently using this generic NEXT to the generic!

  g.type -> {egg, bacon}
  func eat(food []g.type)
But then you can’t use a generic from another package... but maybe it’s a good limitation?

Or force a generic to have a description, which is what golang is doing with interfaces. The problem is that we can’t combine difference interfaces like in Rust/ocaml

"or even better: force listing all the types that are currently using this generic"

This is a bad idea. Imagine a generic hash map, we surely want it to work with any value type.

True. I’m trying to find a solution to the biggest problem of generics which is imo that you can’t understand how it’s being used when reading one.

It’s fine if the function doesn’t do much. But if it’s nesting generics, or doing anything complex, then not using a union of interfaces make it really hard to understand code.

I’d like if Golang would add unions of interfaces. But generics? I’m not sure if it’s a good idea.

Or maybe if you import a package that have some generic functions, you would need to add a declaration that list what the types using these generics are.

  Import “stuffWithGeneric{
    thing(K, V) -> (egg, bacon),
    thing(K, V) -> (int, string),
  }”
Imagine you have a non-generic function, for example, `parseJsonData(string)`, how would you understand how it is being used? It can be called with all sorts of input.

Is the problem you are describing different?

That’s true. Usually the name of the argument indicates how it is used. Without it you are lost.

I feel like what really tricks me is this kind of code:

parseJsonData(t T)

Now neither the type nor the argument name help you understanding the purpose of the function.

In this case wouldn't the name of a contract be enough?

Or, perhaps, not enough, but informative to the same degree?

The one-letter "naming convention" used for generic types by most developers is very unfortunate.
Why? There are usually only one or two of them, it's like using `i` for loops instead of `indexOfElement`.
Type parameters are more like function arguments, not loop variables. There is often one or two arguments too but they are not as local (contained) as loop variables.
This is a problem with template metaprogramming, not with generics. Code using generics isn’t less obvious than code using only specialized types. On the contrary: You work with strong typing instead of using interface {} everywhere.
People use interface{} everywhere because union interfaces don’t exist in Golang. If union interfaces were a thing you could be writting

  func thing(a car+plane)
You can substitute all generically typed items with the type interface {}. That's how much is known about the object - basically nothing.

You can substitute generics that conform to a contract by their contract.

> without... letting people being able to abuse them. C++ cough cough.

Curious what you consider "abuse", given the standard library itself uses generics in all sorts of ways?

(comment deleted)
For example. Typical C++ code:

  Thing<A<B, C<D>, E>>()
I know the syntax, but what about this is "abuse" of generics exactly?
It makes the code unreadable.
That's not people abusing the syntax, that's the syntax being intertwined with the semantics. Mind you, a similar thing can pop up in a language like C#, it just happens less often.
And? Care to elaborate?
It makes the code unreadable to others.
The population of C++ developers who manage to write and ship code using templates far more complex than that would suggest otherwise.
who said that code is hard to write?

I said it's hard to read.

And it's probably filled with bugs.

If it isn't hard to write, it's easy enough to read to be useful, and understanding the domain and the problem being solved makes reading it even easier. Just about any production code is hard to read.

>And it's probably filled with bugs.

All nontrivial code is probably filled with bugs.

  Thing<A<B, C<D>, E>> 
except for a different bracket syntax, it is the type level equivalent of:

  Thing(A(B, C(D)), E)
There absolutely nothing unreadable with that. Mind you, I find <> brackets somewhat ugly, but that's subjective and plenty of languages have made the same choice.

To be more constructive, C++ templates have two major pain points: template specialization means that the template machinery is a full term rewriting engine, so arbitrary computations can be expressed in it. The other issue is that it is hard to specify template constraints (or concepts, it type classes or traits or whatever term you prefer).

A new language can eskew specialization (but note that Rust, that wants to compete in the same space as C++ has added, or is adding them), and there is no good reason not to have constraints in a new language (the design space is well explored).

I think you misplaced a ) there.

  Thing<A<B, C<D>, E>>
Is the type level equivalent of:

  Thing(A(B, C(D), E))
Not:

  Thing(A(B, C(D)), E)

I guess this is illustrative for how it is unreadable.
It is more illustrative that it is inconvenient to type in the HN commento box from mobile.
Seems more like an abuse of one letter names.
Abuse by users is subjective. Still, C++ templates make parsing undecidable.

It's worse than just a Turing-complete preprocessor like, say, Lisp macros. In C++ you cannot decide the type of the next token until you evaluate the (Turing-complete) templates.

This shows that the templates are at least a bit too powerful.

In other languages, you can easily define constraints on generic types. It’s not rocket science, but actively ignoring any advancement since the 70s means that you have to reinvent the wheel constantly.
Golang could do this by implementing unions on interfaces.
I certainly agree with your later example (cars+planes)...but how do union types help with subtyping?
how do generics help with subtyping?
>1. Maybe change the syntax to this one:

>func eat(food []generic.Type) Or

>func eat(food []g::type) Or something that doesn’t involve adding more <> or ()

I don't understand how this would work with multiple generic parameters. Say I have

func merge(a []g::type, b []g::type) []g::type

How do you specify that a, b and the returned value must have the same type and implement the comparison operators?

Also, why are people trying so hard to stay away from < and >?

“type” here is whatever you want it to be

  func merge(a []g::thing, b []g::thing) []g::otherThing
The <> syntax is well accepted, that’s the only thing it has. What’s annoying is that it adds to the function signature when it’s not necessary.
I also dislike the use of fun<T>(a:T b:T)->T because it make look weird.

How about:

    fun(a:$T b:$T) -> $T //to look like templating
    fun(a::T b::T) -> ::T //to look like "a type but depper"
    fun(a:_T b:_T) -> _T //to look like "a type hole"
mmm... I like more the third :)
It is just me or do contracts look a lot like type classes.
Sshhhht! Industrial players don't like to hear that type systems are a thoroughly mapped space. They like to think that their design is different and does not adhere to the laws of type theory. It is also more fun to watch them struggle essentially reinventing research papers from 20+ years ago.

Edit: yes, I am exaggerating a little, but I find it really amazing how PL design is in practice so removed from its theoretical foundations.

> reinventing research papers from 20+ years ago

The first(?) paper on type classes, "Parametric overloading in polymorphic programming languages", is from 1988, so they're reinventing things from at least 31 years ago. I guess in 2050 Go will catchup with today's state of the art.

I don't understand what's the difference between single-parameter contracts and interfaces. Aren't contracts only useful when there's two or more type parameters involved?

Edit: didn't read through the whole thing, I guess contracts can do things that interfaces don't. I think the overlap still makes things a bit awkward though.

If what Generics fundamentally solve is having to type out an almost identical function for many types, why not just have macros?
I think most people would agree that templates are more complicated than generics
I don’t quite get what contracts are needed for? This

    func Print(type T)(s []T)
could be written as

    func Print(type T implements SomeInterface)(s []T)
What am I missing?
You're probably not still looking for an answer, but since I tried to work through this question myself I will answer it, if only for my own benefit:

See the "Mutually referencing type parameters" example. [0] Ignoring the issue of generic types vs functions, suppose you wanted to do shortest path as a fuction using the Node and Edge types described. You could try to write something like

  func ShortestPath(type N implements Node, E implements Edge)(from, to N) []E
but how would it be implemented? Type N only promises that it it implements the Node interface which is just

  Edges() []Edge
but those Edges might not be concrete type E.

So instead you must resort to

  func ShortestPath(from, to Node) []Edge
and use type assertions, which is where we were before generics.

The contracts allow you to constraint the relationship between the concrete types N and E, enabling callers to work with concrete types directly.

[0] https://github.com/golang/proposal/blob/4a54a00950b56dd00964...

> You're probably not still looking for an answer

On the contrary! I think I got it, thanks.

Not sure which version is simpler though, I've certainly never hit the "Mutually referencing type parameters" issue before. I guess using concrete types and avoiding vtable lookups is an advantage that matters to Go?

> I've certainly never hit the "Mutually referencing type parameters" issue before.

I don't think I have either, but according to that document supporting this use case was considered a minimum requirement for generics. I don't know that much about generics but maybe it comes up more often than we realize.

> I guess using concrete types and avoiding vtable lookups is an advantage that matters to Go?

It avoids the vtable lookups and also, in cases like the mutually referencing type parameters, provides compile-type type-checking, which you don't get with the interface-only solution.

I'm also not sure how your proposed solution extends to parameterized types. Certainly a central motivation of generics is make something like LinkedList<T> possible.

For a second I thought it was related to {pre,post}-conditions.