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.
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:
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.
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.
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.
* 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
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.
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.
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.
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.
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.
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).
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.
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"
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.
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.
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.
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.
62 comments
[ 0.17 ms ] story [ 106 ms ] thread[1]: https://about.sourcegraph.com/go/gophercon-2019-generics-in-... [2]: https://go-review.googlesource.com/c/go/+/187317/
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
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:
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.
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...
And I didn't find how to write a contract which requires a type must have some specified fields in this draft.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.
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.
* 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:
Or 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!
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
This is a bad idea. Imagine a generic hash map, we surely want it to work with any value type.
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.
Is the problem you are describing different?
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.
Or, perhaps, not enough, but informative to the same degree?
You can substitute generics that conform to a contract by their contract.
Curious what you consider "abuse", given the standard library itself uses generics in all sorts of ways?
I said it's hard to read.
And it's probably filled with bugs.
>And it's probably filled with bugs.
All nontrivial code is probably filled with bugs.
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).
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.
>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 >?
How about:
mmm... I like more the third :)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.
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.
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.
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
but how would it be implemented? Type N only promises that it it implements the Node interface which is just but those Edges might not be concrete type E.So instead you must resort to
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...
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 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.