Go's updated generic proposal (Contracts)
github.com
github.com
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.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.
func New(compare func(generic.K, generic.V) int) *Map(generic.K, generic.V) 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) boolInitial reading of this document makes me feel it is really over complicated compared with Dart.
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');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.
* 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
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.
Import “stuffWithGeneric{
thing(K, V) -> (egg, bacon),
thing(K, V) -> (int, string),
}”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?
func thing(a car+plane)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?
Thing<A<B, C<D>, E>>()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.
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).
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'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 >?
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.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 :)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.
[1]: https://about.sourcegraph.com/go/gophercon-2019-generics-in-... [2]: https://go-review.googlesource.com/c/go/+/187317/
func Print(type T)(s []T)
could be written as func Print(type T implements SomeInterface)(s []T)
What am I missing?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...
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.
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.