One of Go's core strengths is simplicity. I've personally witnessed both C and Python programmers jump right into Go projects with minimal assistance. Learning new packages is absurdly easy because the code is so simple and documentation practically generates itself. I worry that widespread use of this feature would negatively impact the language in that regard.
Lacking a true "generics" feature has had an influence that people tend to overlook, which is that the Go community is less reliant on dependencies. One of Go's proverbs is "A little copying is better than a little dependency" [0]. The ecosystem is better documented and healthier because of it, avoiding the require("left-pad") world of the dependency abyss.
The language lends itself to being used for smaller, more concise code bases where things are written with specific and narrowly-defined purpose. It implicitly discourages "Architecture Astronauts" [1] and that strength shouldn't be overlooked or sacrificed for a few new container options.
[0] https://go-proverbs.github.io/
[1] https://www.joelonsoftware.com/2001/04/21/dont-let-architect...
The relative lack of "dependency hell" in golang compared to other languages is because of the great standard library. Golang also never had an official/proper (imo) solution for package management until just this past year or so (although people worked around that a bit)
I like the generics proposal, but it seems like a valid concern.
I also remember reading some one said their Software has increased complexity due to Go's Simplicity. ( Or something similar along the line )
It really is a balancing act. And it seems there is no one size fit all solution.
Which way are you meaning this. The way I read it was that because GO was simple, we were able to build more complex systems. But you seemed to indicate that was a bad thing?
Yes making complex that can be made simple is the way you want to do things, but some things simply are not simple, and thus require complex software.
I predicted at the time that people would take away the wrong conclusion—"dependencies are bad"—from the left-pad incident. Sadly that prediction seems to have come true.
The more dependencies you have, the more likely you are to get one that's going to cause a problem down the line, be it due to conflicts with other libraries, subtle bugs that take years to manifest, or someone deleting something from upstream package manager (as was the case with left-pad).
What I mainly took away from the left-pad incident was that if people will add dependencies for code that is so simple that it takes less than a minute to write(1), then what else will they add dependencies for? I was a bit surprised at the sheer poor engineering judgement involved in adding massive amounts of dependencies so carelessly when they were so fragile as they proved to be in NPM.
Raise your hands those who check all dependencies before every release? Did you look at all the change logs? Did you check for issues, security vulnerabilities, or that it may be time to upgrade to a newer version (or not)? Did you follow all the transitive dependencies and do the same? Do you routinely evaluate each dependency with an eye to replacing it? No? Yes?
Using known good implementations that you know to be maintained, is good. Overdoing creates new problems. Making a culture of it creates a community of fragile, ratty code.
Dependencies are bad if you overdo them because with scale problems tend to morph into new kinds of problems.
(1) You usually do not need to do all of what left-pad does. Usually you need one specific case. And once you have done it a few times (for a few different cases), it becomes like a kata you can write in seconds. It is much faster than adding a dependency. Not least because adding a dependency actually means you should spend enough time reviewing the dependency - in depth the first time, and perhaps just checking the change logs or commit logs on subsequent versions.
As with most things - it is a trade-off.
I’m more worried about eccentricities in the implementation that we’ll get out of the golang team. Then again, maybe we’ll be pleasantly surprised.
I primarily program in Go. While I think often HN makes too big a deal of Go's support for generic types currently being (more or less) limited to slices, maps, and channels (those go pretty far for a lot of programs, especially when combined with interfaces), I don't get the passionate anti-generics attitude this post seems to have attracted either. I'm wary of architecture astronauts just like everyone else, but I think at least the Go team could probably use generics judiciously in the standard library in way that would benefit most programs (like using sync.Map without casting to interface{}, or doing math on float32 without needing to do float32(math.Sin(float32(x))), etc).
In those worlds you're more likely to move data between logical units in JSON or Protobufs (both of which Go is good for) than to need proper generics so I don't understand the push from this subset of the community.
As far as container types and data structures, look at the number of well built storage solutions that don't seem encumbered by the lack of generics at all like: boltdb, bleve, BadgerDB, InfluxDB, etc.
You rarely know all of your requirements up front, and walking back a language change a year or more into your project is almost always prohibitively expensive. At that point, you find yourself with a handful of equally-bad language-hybridization options. One of Go's objectives is scalability--as Go projects mature, they should not find themselves boxed in by the language. In the case of generics specifically, you have a slightly better option than language hybridization or changing languages outright which is generating generic code, but that's only slightly better (because now you need code generation built into your CI/CD pipeline which is inherently worse than a naive generic type system implementation and it also imposes a high cost on all clients of your generic code, since they will also have to adopt your same generic build system as the standard Go toolchain isn't your-generic-build-system-aware).
So, people switch to new simpler languages. The way you remove things from a language is to create a new one.
Then these new simpler languages become popular, and then people want their critical favorite bit added. And eventually the language is no longer simpler. And people switch to another new language, and the cycle repeats.
Because we couldn't convince people to just use the existing complex language if they want that. Because, really, this is all a struggle to influence other developers. These days it's not enough to work the way that is best for you, you need to find ways to harness a critical mass of other developers, and at the same time contain the mess and damage that a huge number of miscellaneous developers out in the world can do to your ecosystem.
It kinda sucks. Where possible, I like to use older, less popular, more minimal: libraries, frameworks, tools.
- Interfaces gather methods. The concrete type referenced through an interface doesn't need to know about the interface. Interfaces can be used to decouple operations from the details of the thing being operated upon.
- Contracts gather methods. The concrete type referenced through a contract doesn't need to know about the contract. Contracts can be used to decouple operations from the details of the thing being operated upon.
Disjunction: Interfaces are resolved at runtime and result in indirect calls. Contracts are resolved at compile time and their contract-nature is lost in object code, they just become direct calls. Oh and contracts can do operators.
It feels to me like the risk here is not more syntax but less orthogonality. There will be two features competing to be the standard way to do the contract-like things.
However, they operate at different times. Interfaces are a runtime abstraction and contracts are at compile time. Interfaces are dynamic and contracts are static. This manifests itself in important ways:
* Contracts give you a way to enforce homogeneity where interfaces permit heterogeneity. Say you want a function that takes a list of objects that you can call `Foo()` on. If you define that function as accepting a list of some Fooer interface, then the list you get at runtime may contain a mixture of all sorts of different types of objects, all of which implement Fooer. If your function excepts a list of a generic type implementing a Fooer contract then at runtime you get a list of objects all of the same type, one which supports supports Fooer.
* Because of the above, there is a runtime overhead to working with interfaces. When you pass a concrete type to something expecting in interface, that value needs to be boxed. When invoke a method on it, you need some amount of dynamic or virtual dispatch at runtime. With contracts, you can generate code at compile time that calls directly into the chosen type because you know at compile time exactly which concrete type has been chosen.
So I think you're right to point out the similarities. But I think having both and having them be similar is a good thing. It lowers the cognitive load while giving more things users can express. I don't think the features will compete any more than interfaces and generics compete in other languages.
There are analogues of this elsewhere. C++ basically recapitulated this same history decades ago. Initially it only had virtual methods but no generics. You would see collection libraries that tried to define collections of arbitrary types using interfaces, but it never quite worked right. A list of ints isn't really a subclass of List<T> even though there is some polymorphism going on. Templates resolved that.
Likewise, Java added generics because interfaces were not sufficiently expressive.
Rust has traits and trait objects. The two features are very similar and reuse a lot of mechanism, but also let you do different things. Traits are basically like contracts and are instantiated and specialized at compile time with no runtime overhead. Trait objects are like interfaces or vtables. They give you runtime polymorphism at the cost of some performance.
This seems like it would be solvable by simply allowing interfaces to constrain generic parameters. Something like:
func foo(type A Comparable, B OtherIface, C /* unconstrained */)(a A, b B) C {
...
}
You'd need to allow for type parameters on interfaces, of course: func lookup(type A Indexable(B), B Hashable)(set A, idx B) {
...
}
But there's no need, IMO, to separate interfaces and contracts.* Regarding performance: a compiler could devirtualize the virtual/interface calls if it can prove there are no necessary dynamic dispatches
* contracts additionally differ from interfaces in that the former are generic over multiple types while interfaces are only generic over one type.
Just to add some more data points to your disjunction section:
When talking about composite types using interfaces or type variables, the difference becomes more apparent:
func Concat(type T stringer)(ts ...T) stringer
vs func Concat(ts ...fmt.Stringer) string
On the type-parameterized case once you pick the concrete type, all the elements of the slice must be of the same type.On the other hand this means that you can use existing slices. One of the most common questions I found when teaching Go to newcomers is "why can't I convert a slice of X where X implements Y into a slice of Y"?
i.e.:
type sint int
func (s sint) String() string { return fmt.Sprint(int(s)) }
func demo(vs []fmt.Stringer) {
for _, v := range vs {
fmt.Println(v.String())
}
}
func main() {
a := []sint{1, 2, 3}
s := make([]fmt.Stringer, len(a))
for i := range a {
s[i] = a[i]
}
demo(s)
}
with contracts you can just pass a concrete instance of the composite type: func demo(type T fmt.Stringer)(vs []T) {
for _, v := range vs {
fmt.Println(v.String())
}
}
func main() {
a := []sint{1, 2, 3}
demo(a)
}
So, even putting aside runtime optimizations, there are perfectly valid use cases where you either want e.g. "a slice of any value as long as each value implements a given method" or "a slice of a concrete type which implements a given method".The former allows you to lift your value into the abstraction (e.g. you can pass your implementation of an io.Writer anywhere some code expects one). This is generally unidirectional; e.g. the external code doesn't give you back an instance of your own type (because in order to do it, you'd have to employ a runtime type assertion to get it back).
The latter allows you to consume some abstraction on top of your data. This code can produce new values of your types (without hacks like factory interfaces) and the compiler knows about that.
But what worries me is that it's an extremely subtle, implementation-understanding distinction. They are still extremely similar and their dissimilarity seems to be of the sort that will bite people. Beginners definitely, and sometimes even pros.
The overlap seems to be a bit of "essential complexity" - if you support both compile-time and runtime dispatch, then the programmer has to choose which to use, and there are many cases where either would work.
Using interfaces for generic type constraints feels very natural, even though there are some rough edges you can bump into when trying to do overly clever stuff.
It’s surprising to me that Go wouldn’t just use interfaces as “contracts” in the same way. Why add a whole new class of entities?
I won't deny they're not orthogonal, nor that we're probably going to see some confusion about which to use when from beginners, nor that there are probably interfaces in the wild that really ought to be contracts (I'm pretty sure I've got a couple myself, though I haven't fully checked until the proposal is stabilized), but neither do they quite stand in for each other. Perhaps if this was in the language from day one, more work would be put in to finding a way to make just one "interface/contract" feature with some kind of (probably confusing) parameter in it that lets it serve both purposes, but doing that today doesn't seem to be on the table.
func convert(Type2, Type1 ConvertibleTo(Type2))(a Type1, b Type1){…}
However, that wouldn’t work as well if you wanted multiple methods with different receiver types, e.g. if you wanted to require convertibility in both directions.
This seems fine to me, I don't feel that these features compete in the same way you're suggesting. With interfaces, the receiver type is not constrained; with contracts, it is explicitly provided. Contracts are resolved at compile-time while interfaces are resolved at runtime. That doesn't seem like a stretch, but I'll have to dig in more with the proposal.
If anything, extending the range of const compile-time capabilities is a good thing.
1. This is a long proposal. Excluding the implementation/issues/comparison parts, it clocks in at 7700 words. Compared to the 27k-word long Go spec, this is epic. I know the proposal is more verbose than the eventual spec (if accepted), but still... 2. There are some valid points in Nate Finch's criticism of the previous proposal (https://npf.io/2018/09/go2-contracts-go-too-far/). Be it philosophical arguments or just plain syntax ones - like writing `func (...)(...)(...)` in function declarations.
I don't know. I'd like some form of generics in the language, but I'm not sure if complex designs like this are likely to be embraced by the community.
And in Java it didn't stop there; the people (person?) behind generics moved on to create Scala, a language with even more features and a notoriously complicated 25-step compilation process (see https://typelevel.org/scala/docs/phases.html) (compare with Java's six phases https://www.javatpoint.com/compiler-phases and Go's... three? Not sure, I found https://getstream.io/blog/how-a-go-program-compiles-down-to-...).
edit: Actually most of my knowledge comes from https://news.ycombinator.com/item?id=9622417, I had bookmarked it.
What does this have to do with anything? Scala is complex to compile for reasons far, far beyond generics. Are generics somehow guilty by association because an engineer who was once involved in an implementation of them went on to build some other complex thing?
1. you don't care about performance (you simply 'box' everything),
2. you don't care about executable size (you simply specialise every generic definition to their concrete use cases -- this is what C++ compilers do),
3. you don't do reflection on types in generic definitions.
As "cinnamonheart" mentions in a sibling post, ML's generics are straightforward, and remain, despite hailing from the 1970, even today a shining example of programming language design. Unfortunately, Scala had to violate all three points above to maintain compatibility with Java, and the JVM.
Java and Scala are two very different languages, it doesn't makes sense to compare them. What you're suggesting is basically a slippery slope fallacy.
But it is not really written a s a technical proposal, more an explanation with plenty of detail and abundant examples. The important information is kind of drowned out by the noise.
But other than that it is not really that complex. Generics aren't easy, but they are not something that needs to be invented. Implementation options and tradeoffs are well understood.
The duplication introduced by having both concepts and interfaces is unfortunate though.
Regarding syntax:
No language has really managed to provide a really good syntax for generics, imo. The default <T> is often ugly. Haskell chose to have them declared in a separate function header which is much nicer, but also annoying when having to modify two lines, etc.
I think this is a reasonable tradeoff. With special syntax highlighting for `(type T)` it will be easy enough to parse visually.
The proposal does contain some particularly unfortunate examples though.
Spoiler alert: It won't.
If you think the Go community had a strong reaction with `if err != nil`, just wait for this one.
Genius level play from Ian and Robert.
"As the term generic is widely used in the Go community, we will use it below as a shorthand to mean a function or type that takes type parameters. Don't confuse the term generic as used in this design with the same term in other languages like C++, C#, Java, or Rust; they have similarities but are not the same."
To the contrary, it’s actually helpful to use the same word for something with roughly the same uses (eg generic type-safe container classes) even if the details are very different.
Go uses curly braces. Lots of similarly braced languages (Java, Kotlin, Scala, Typescript, ...) have generics and type parameters. Maybe generics is not a bad name for this.
What they're proposing here are basically trait bounded generics. They just don't want to use the word "generics" because of the following attitude:
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt."
But you know what, go is easy to learn, and that's been awesome in my experience. I can (and have) hired people who have never written go, and they're productive in a week or two.
Did you mean ad-hoc polymorphism?
Java went down this route, ah, we will remove operator overloading, that will make it simpler, because nobody can ever write a function named equals which does not do what you expect it to... and the end result is horrible and does not prevent people from doing amazingly dumb things.
>The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
If Google, with its intensive algorithmic interviews, can't manage to hire people capable of understanding something like:
func max(T: Comparable)(a, b T) {
if a < b {
return b
} else {
return a }
}
}
And why we'd prefer that over having to rewrite the function for int8, int16, int32, int64, uint8, uint16..., then what hope is there for anybody else?A knife that's sharp and unweildly makes me nervous even in the hands of an expert. Keep unweildly constructions out of go!
All the forces around (and especially outside but still influencing it) Go seem to push it alongside the same path. So far the Go designers are resisting bloating the language, but i wonder for how long that will be the case. There is this mindset that a language can never be good enough and stick with its existing features but instead it must always get new features - which ends on an inevitable bloat. This sort of need for featuritis is like locusts moving from one project to another.
I don't think operator overloading is the best example to give here. It can mask things to make it less clear what's going on. I think Kotlin strikes a good balance, where they overloaded math operators for things like `BigInteger` without going overboard.
Other than that, Java has way more features than anything golang even hopes to offer. Golang literally dumbed down the language to such an extent that many people find it frustrating to work with. Whereas Java is getting some very cool features in the near future (pattern matching, record types, etc.)
I always think about it like this: When designing a tool, you can make one that prevents behavior from users on the low end of the bell curve, but only by also preventing behavior on the high end as well. I think about this a lot in the context of teaching. The kind of classes that ensure no child gets left behind also tend to prohibit any child from racing ahead.
Most software doesn't need scientists, just bricklayers. And they must lay those bricks fairly easily and without making it undecipherable for their supervisors.
func myBlah(thing interface{}){
switch thing.(type) {
case int:
// handle my int
case string:
// handle my string
default:
// default handling
}
}[0] https://github.com/go-fed/activity/blob/master/streams/gen_t...
// proposed
func Print2(type T1, T2)(s1 []T1, s2 []T2) { ... }
vs // less noise
func Print2(s1 []'T1, s2 []'T2) { ... }
This proposal does -not- seem to be informed by the same 'outer space' mindset that gave us the Go language. A reminder that mere capitalization in Go language informs visibility and access.Beyond mere syntax issues, the semantics of generics itself is adding huge complexity to what is a very capable and accessible 80% language.
> func Print2(s1 []'T1, s2 []'T2) { ... }
where would you add the claim that T1 and T2 must obey some named "contract"?
I did miss generics when I first started writing Go, but after doing so for about a year at work and 2 years before that, I don't find me missing them at all.
Of the 12 examples at the end of the draft, there is only one real contract used, `comparable`, which could be easily done without contracts (but with generics) by passing in an extra argument `lessThanOrEqual func(T, T) bool`.
The other kind of contract is harder to do without contracts, as it is the contract which combines all numeric types. However, most of the examples would be possible to write, albeit a little more messily, with a parameter `toDouble f(T) double`. Alternatively, we could go the route of baking in a few special contracts into the language, such as a numeric contract, just like how map and slice are baked in polymorphic structures.
What do we gain from writing code this way? It leaves the Go language with more freedom from the future. Everyone agrees Go needs polymorphism. So add polymorphism. But as we can't agree on contracts, we can just leave it out. We'll get 90% of the benefits upfront, and then we can wait and see what are the actual edge cases we run into with contracts, and decide where to go from there.
Please provide some examples. For example, I don't think polymorphism in direct variable references is all that good.
It leaves the Go language with more freedom from the future. Everyone agrees Go needs polymorphism. So add polymorphism.
This statement confuses me. Go has polymorphism.
If you need a little bit of behavior for those types, such as for a TreeMap data structure or a Sort function, Go's higher order functions make this possible without contracts, by passing in functions. The draft's Containers section uses this approach to construct a TreeMap. The SliceFn function, described in the Sort section, also uses this approach:
func SliceFn(type Elem)(s []Elem, f func(Elem, Elem) bool) {
Sort(sliceFn(Elem){s, f})
}
Although this does use method overloading via contracts in implementation (via Sort), it doesn't need it. I expect that Go's existing interfaces (which yes, are a form of polymorphism, mea culpa) + unbounded parametric polymorphism are enough to solve 90% of Go's current issues.The more complex cases are still possible to handle with parametrically polymorphic structs containing functions. Yes, that's bad code, but we'd be able to look for this use case, judge how often it occurs, and use it's use cases as data to inform further discussions on contracts.
The only case I struggle to easily deal with is the numeric case. My solution doesn't allow generic numeric functions, although I think there's room to explore that in more detail.
polymorphism is often distinguished into ad-hoc polymorphism (interfaces) and parametric polymorphism (generics) – i'm guessing GP was referring to the latter?
Huh? This couldn't be further from the truth.
If you have contracts/generics then you need to have un-compiled code as part of your exports. Which is a huge break from the current approach.
If the latter approach is taken then no un-compiled code needs to be exported.
So you might ask what I am suggesting. AFAIK there are two major problems preventing interfaces to be used as generics:
1. There is no way to require the methods of builtin types aka operators.
2. Compile-time type safety is very limited
The reason why interfaces have no access to operators and other language features is that the language is kinda rough around the edges. And I really don't want to hurt anybody with this statement. I love the language, but I think there are some things that aren't perfect.
For example, just imagine every array/slice would have a method (e.g. Position(int)) that would be the equivalent of the square brackets and the square brackets would be just some syntactic sugar like
arrayA[0] == arrayA.Position(0)
Same thing for operators like + x := 1
x + 2 == x.Addition(2)
It would eliminate a lot of cases that make contracts necessary. In essence, it would mean that you could actually address built-in types in interfaces.The second problem is related to the type parameter list. In my opinion, it would be better to simply put those types on extra lines like:
type T Generic
func Print(s []T) {
// function body
}
I mean, there should be no problem in making them available for more than one function and it would definitely clean up the function signature and look more Go-like. generic func(T, U) Map(slice []T, mapper func(T) U) []U
For "contracts": type Set generic interface(T comparable) {
Push(T)
Pop() T
}
Every time you want to refer to a generic type you'd have to use the generic keyword, which encourages use of concrete types: type IntSet generic Set(int)
This is more or less how Modula-3 does it. Generics have dedicated syntax with a "generic" keyword.Another alternative that I would like is to have angle brackets < > for better readability, so that definition and call would look as follows:
// 1
func Print2<type T1, T2>(s1 []T1, s2 []T2) { ... }
Print2<int,int>( ... )
// 2
var v Vector<int>
// 3
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
Sorry, if it gives you C++ nightmares.I'm disappointed they didn't go with «» or similar to set apart type parameters. The answer in the doc is that they "couldn't bring ourselves to require non-ascii", but there's an easy way to handle this without _requiring_ non-ascii characters: let "(type …)" and "«…»" be syntactically identical, and have gofmt change the former to the latter. Basically everyone uses gofmt all the time, so all the code everyone reads would use the more visually distinctive syntax, but it would be easy to type using ascii only.
Anyone else find this limitation a bit disappointing? Seems like a somewhat arbitrary restriction that limits the usefulness of this feature. I hope it doesn't take another 10 years for this to be changed...
Composing contracts is a slightly more verbose fix for allowing multiple contracts for a given type (just make a composed contract and specify that).
Using composed contracts, allowed:
func Foo(type T PrintStringer)(s T) {...}
I read this as, while a function can have multiple type parameters, only one contract can be specified in total.Not allowed (function uses "setter" and "stringer"):
func Bar(type B setter, S stringer)(box B, item S) {...}
Maybe I'm misunderstanding though.In other languages with parametric polymorphism, the real re-use comes in by allowing functions like Bar to be used for any combination of "constraint implementing" types.
func thing(stuff _.T, stuff2 _.k)
Or
func thing(stuff g.K)
And so on.Also want to point out that there is a LOT of Golang code that is being shipped and used in the world and none of it uses generics. Do we really need them?
Granted, that community in average probably contains more "Go fans". There was no lack of criticism at the try proposal over there though.
1: https://www.reddit.com/r/golang/comments/cifdwf/the_updated_...
But, there are some aspects that will make code appear more complex or challenging to read. It will certainly make it more challenging for beginners.
Appearance of the code with type switches and issue with the idea of Iterator and doing something two different ways were the only parts that stood out to me besides increased complexity
I know where this specific proposal is coming from, but I feel that Robert and Ian are being pushed by the constant noise made by people coming from other languages, people who like those who made the horrible "try" proposal, seem to be trying really hard to ruin Go by turning it into yet another complex monster.
Not a day goes by without someone making a new proposal that is trying to change the very thing that made Go so unique and lovable!
All the proposals that has been made so far exists in several of the other popular programming languages. Use one of those if you really want the added complexity - leave Go just the way it is!
[1] (https://news.ycombinator.com/item?id=20555477)
edit: changed "strict" to "static"
I find it especially unorthogonal that you can type switch on generic parameters like they are interfaces. If I have a method that takes interface parameters, should I just always use generic (unboxed) parameters instead?