* the "time" package hooks into the Go scheduler.
* the "reflect" package needs type information out of the runtime.
* the "net" package hooks into the Go scheduler a bit. But way less than it used to. I think you might even be able to do this all yourself outside of std nowadays.
What else?
Our rough plan going forward is to actually move a bunch of the stdlib elsewhere but ship copies/caches of tagged versions with Go releases. So maybe "encoding/json" is really an alias for "golang.org/std/encoding/json" and Go 1.13.0 ships with version 1.13.0 of golang.org/std/encoding/json. But if you need a fix, feature, or optimization sooner than 6 months when 1.14.0 comes out, you can update earlier.
1. The constructs like "x <- chan" and "x, ok <- chan" (and map indexing and so on) are only available to builtin types.
It's impossible to write a thing like "x, ok <- chan" where it either panics or doesn't depending on if the caller wanted two or one return values.
2. generic types, like 'map' and 'chan'.
3. 'len' can't be implemented for a user type, e.g. "len(myCustomWorkQueue)" doesn't work.
4. range working without going through weird hoops (like returning a channel and keeping a goroutine around to feed it)
5. Reserving large blocks of memory up front (e.g. as 'make' can do for slices and maps) in an efficient manner (e.g. make(myCustomWorkQueue, 0, 20) doesn't work)
It really doesn't. Maps and channels are not part of the stdlib. They're part of the language, but not the library.
Similarly, range is part of the language, not the stdlib.
If the stdlib was privileged, "container/*" in it would be generic, but it's not.
Consider that make, map, chan etc are just language features/specs just like int64, string, func, main etc
But higher level data structures like lists, sets, maps etc. are all implemented using those primitives under the hood. So, especially in a "systems language", I would sort of expect them to just be libraries in the stdlib, not language features[1]. But if you think otherwise (or see a gap in my reasoning) I'd love to hear why!
[1] Except maybe some syntactic sugar for list/dict/set literals.
> But higher level data structures like lists, sets, maps etc. are all implemented using those primitives under the hood. So, especially in a "systems language", I would sort of expect them to just be libraries in the stdlib, not language features[1].
Maybe! But if those higher order structures are closer to language features than libraries -- and, specifically to this conversation, if they leverage parts of the runtime that aren't available to mere mortals -- is it a strictly worse outcome? I don't know. Some evidence might suggest yes. But I think it's at best arguable.
In Go you don't have generics as a primitive with which to build those things.
As a result, you don't have the building blocks for anyone to build a usable data structure unless it's baked into the core of the language.
> Should [be just libraries]? I think you are asserting facts that are not in evidence.
I tried to describe why I feel that go's collections should be libraries and not builtins, and hopefully understand why GP seemed like they were questioning that idea.
EDIT: Yes, something has changed. They always said "no, but maybe later" and now it's "ok how about now". That is a big change and I am just asking what's the reason for it.
So, yeah, it was always "on the table" in some hazy, we'll see later, way, but not like that.
You might ask, why not pick a solution earlier and then improve it over time? However with a popular language it's not that easy. People are writing lots of code. If you start early, you can paint yourself in a corner and end up with a system that you really can't improve very much because it introduces incompatible changes. Note the extremely painful (and drawn out!) incompatible changes with Perl 6 and Python 3. If you make the wrong choice early, you might still take over a decade to find an opportunity to replace it. And since the state of the art in language design moves very slowly, it took a long time before they could see something that they felt they wouldn't regret choosing.
This is part of the material linked to from this thread. It answers all your questions. You will see that some of the people who are directly replying to you are very much involved in this effort.
Edit: I tried editing the previous message but HN actually automatically made a reply instead....
Also inevitably, Java will get value types (structs) at some point.
Just like with Go Generics, people will complain the feature is bolted on. After some time it is just one of the many quirks of a complex language.
I think it was Stroustrup who said: there are two kinds of programming languages, the ones people hate and the ones nobody uses.
v2 is allowed to Break Stuff. So obviously they're going to consider the thing that people have shouted for most in Go... generics and error handling (and package management but they found a way of doing that earlier).
Simply, Go has probably accumulated all the developers it is going to accumulate without something drastically changing.
At this point, I can't think of any programmers around me who now want to learn Go.
But yeah, if Go just added a generic map/filter mechanism (Python's list comprehensions (in turn inherited from Haskell) look very nice), that would cover about %75 of the use cases I want Generics for. Add a generic mechanism to use the range operator with user-defined types, and we're at about 95%.
Do they? I find them hard to read and hard to chain. I tend to prefer basic map, filter, reduce methods
OTOH, I never used nested map/remove-if constructs in Lisp, because that, too, can get rather annoying to read.
The idea is that when people say "generics", they usually just mean map/filter/reduce. So ply just bakes those into the language directly instead of adding new generic programming facilities.
In my work I don't think I could live without these higher level abstractions anymore. It enables me to write clear and concise code that helps me keeping accidental complexity under control so that I can focus on the essence of what I'm trying to codify. Performance on the other hand is not a primary concern to me, given it stays above some acceptable threshold.
I appreciate how different tasks would require different approaches. I'm interested in which direction go goes (no pun intended).
I'd say that to attain simplicity and clarity, you have to limit the use of abstraction. OTOH there's no way to avoid abstraction at all; programming is all about abstraction. If the means of abstraction are not powerful enough, copy-paste programming and boilerplate proliferate, making code less maintainable and harder to understand.
Dylan was too long winded for my taste, as are most modern attempts.
I agree the introduction of generics is a big worry not because of what good programmers will do with it, but because of what people more enamoured with pretty abstractions than code that does work will do with it. I don’t particularly want to live inside someone else’s creation for weeks just to understand it. The existing culture of no nonsense and minimal abstraction should help police that though.
I’m not that keen on the syntax of contracts either, but we’ll see what comes out at the end, that’s not so important - the idea of contracts looks like a good one in theory at least, but it feels like they could have used interfaces instead somehow (I’m probably missing something there).
The errors proposal is interesting as it looks like try/catch at first glance but importantly doesn’t leak that error across function/lib boundaries as exceptions do, so it’s a natural extension of the existing approach.
I'm not sure I understand the need for contracts really. If a function with type argument is compiled, the caller should use specific types to call it, so the compiler can determine whether said type has the required methods used in the generic function. Therefore the compiler knows everything it needs to fail with a compile-time error without any extra ugly "contract" syntax. What am I missing? Are some types only resolved at runtime?
So the lessons from a number of PL implementations show that this kind of information needs to be explicitly encoded in codes, and that's the whole point of having "contract", "type class", "concept" or whatever else it's called. Compilers may be able to infer technical details of type, but at least for now it's not sophisticated enough to infer the original intention of the writer.
Since Go doesn't have standalone object files / binary libraries, it's not going to produce confusing link-time errors for this.
template <typename T>
void foo(T bar)
{
bar.hardToSpell(); //1. Error here?
}
struct Bar() {
void ahrdotspell(); //2. Error here?
}
Bar buzz;
foo(buzz); //3. Error here?
In this particular case it's easy to deduce that something is misspelled and error 2 is the real error but the compiler is probably going to tell you it is error 1. And once you get into more complex programs the problem might be that the Bar class is not simply supported by the generic function or that the generic class is implemented incorrectly.If you instead could add an interface constraint on T (like for example C#/Java/Typescript allows) it would be clear from the contract that this interface defines all the required functions on T if Bar doesn't implement those it is unambiguous that the implementation of the interface is not there.
Then you just have a 4th possible spot for an error. I don‘t see an essential difference between considering the function definition as the reference and having a contract (where typos are possible as well), just the latter being cumbersome and superfluous.
So each of the 3 potential errors can always be pinpointed exactly, as opposed to my example where it could be any 3 (or all 3 simultaneously) of them because "T having a hardToSpell-method" is not part of any public interface description, it's something deep in the implementation of the foo-function (in my example it's not very deep but in real code it usually is).
To iterate my point, it serves as a "contract" between users and the code author. Without this, any user of a generic function will implicitly depend on its implementation, which creates tight coupling. Cryptic error messages or unintentional breakage are just undesired side effects from not having this clear boundary. So, the interface of generic functions needs to be explained to its users anyway; then why not making this understandable to compiler and prevent users from making such mistakes?
There might be a bias in the commentary because the membership of the Go community has self selected to just be those for whom the current constraints are feasible.
When Java's limited, half-crippled version of generics showed up, sourcecode generation died. Pretty much overnight. Why doesn't anyone do it? Because it's a terrible experience compared to having types.
I believe you can get integer parameter types by using something like the `convertible` contract seen here https://go.googlesource.com/proposal/+/master/design/go2draf... (unless I'm mistaking what you are saying)
Most of its quirks are related with compatibility with older versions. If a new version without that handicap was released today, it may look nicer than most people think.
I don't know Fortran, but I'm fairly sure that you just googled "fortran generics" and pasted the first result without checking it :)
https://cug.org/5-publications/proceedings_attendee_lists/20...
It's interesting to consider Python which - generally - has strong community norms against excessive abstractions and too much magic.
There are some notable exceptions. I believe Twisted was regarded as "UnPythonic" and I've heard complaints about asyncio. Interestingly both are related to the same problem space.
I'm sure there are other examples of inscrutable pyramids of abstraction but Python also has many examples of good, readable abstractions.
Maybe it's not abstraction that's the problem but poor judgement? What makes one language sprout AbstractSingletonProxyFactoryBeans and another not? I'm not sure if correlates directly to "ability to create abstractions" - I think there's a human element.
Missing features that enable higher levels of composition and abstraction.
Such as, for example, generics.
If you look at the usual punching bag in these discussions, Spring, you will see that as Java added features, the implementations became simpler.
Java's design hypothesis was: if we take away these features, median programmers won't blow their feet off accidentally. But it turns out that to get things done you need well-above-median programmers to provide tools, APIs, frameworks, leverage and that in turn, they will need to find a way to do it. In the case of Spring that's been marked by a slow retreat from heroics into progressively simpler approaches as and when Java itself allowed for them.
Spring 5, for instance, sets Java 8 as the minimum, and they went through the whole framework refactoring, cleaning up, cutting down and so on wherever that baseline allowed them to.
Disclosure: I work for Pivotal, which sponsors Spring, though on unrelated software. But I've spent a lot of time with Spring folks. They are smart.
On this case, it's the lack or availability of alternatives. You can't take away every feature that isn't the strictest possible OOP and expect people to create expressive and simple code. Remember that those things were created by the best professionals around, and won the selection of best practices and standard open source tools.
People do not write singletons in Python because it has actual static variables. Any moderately complex Python code is full of factories, but people don't even notice because they are actually just a function declaration around the same code that would be there anyway. There is a lot of "writing too the interface" that Java people do with abstract classes, but it comes nearly automatically with duck typing. And for the "bean" part people just use dictionaries.
Go people seem to be taking the wrong lesson from those things.
This is a very valuable observation, relevant outside of Golang, which underlines the pitfalls of abstractions. Design patterns aim to standardize abstractions so that other readers of the same code can easily understand.
I used to capture the same related principles in what I called "IKEA code" - simple, elegant code that is cheap to write, easy to understand and use and not meant to last forever. I.e. code and design that is meant to be replaced. I think the concept of "local productivity vs global productivity" is very related and completes the "IKEA code" principles. Rather than suggest one is better than the other, I think it's important to understand the motivations when making a choice.
Generics increase type safety (no more interface{} casts) and reduce code duplication. I don't see how this could possibly throw productivity "out the window". If anything, the effect will be the opposite. The tradeoff with generics has always been programmer time versus compile time and/or execution time. Assuming that programmers don't unnecessarily complicate things for themselves, programmer time will be significantly lessened with the introduction of generics. And there's nothing preventing programmers from burying themselves in a sea of abstraction even in a language without generics.
Not to be snarky, but...
The whole point of LISP-style macros is that you very much can have both. Ultimately, generics will compile down, either via type erasure or a generated specialization, to a "copy-pasted" function (in concept, at least). With a macro, you have ultimate control of how this is achieved.
The problem, of course, is that the people who care how generics are implemented all think their way is the best (leading to a proliferation of slightly-different-and-incompatible implementations), and everyone else just wants something that works fast and reliably.
Bad code can be written in any language. Good code can be written in most languages (yes, even C++...the jury is still out on PHP). Every feature that grants power comes at the cost of potential complexity. What ultimately determines success or failure is the community and best practices that develop around these features.
In this regard, I think Go will be "o.k." even with generics.
> Compared to a half-as-useful Java codebase where I have to carry a dictionary first to learn 200 new english words for complicated abstractions and spend a month to get "assimilated", this was absolute bliss
You're making a good argument for standard generics and their machinery included as part of the standard library as opposed to bolted on.
Java incrementally baked generics in relatively late in its life along with a culture of a lot of ad hoc machinery to cover up the lack of useful lambdas.
The wisdom of Go putting these in the stdlib is you get their benefit universally without having to wade through a learning process with a uniquely flawed and ad hoc hierarchical ontology for each project.
Having such universal tooling is a sign that your language is powerful enough to lift out and abstract real problems. That's a good thing! Don't mistake Java's legacy issues for issues Golang might have. It probably won't end up with the same problems because it starts with a better feature set.
From my current perspective I think fixing some things related to Go Interfaces and Container types (Maps/Slices) should make them even more usable than they are now and making them pretty much the thing people want to use Generics for. Currently, it looks like hell to build something with the empty interface and using a lib which is built around the empty interface doesn't look nice either.
Using Go maps is sometimes awkward too as they require some weird attribute 'comparable' which some types posses and others don't. Why??? I mean why can't the Go authors use their own language construct and use an Interface like:
interface Comparable {
Comparable(interface{}) bool
}
Similarly, I find it awkward that we still can't use Interface Slices[1]. I mean I understand it isn't simple to implement but certainly not impossible.One thing on my todo list is writing an experience report with better examples. It's priority just got up-voted ;-)
In Java, I can't count the number of times I've debugged either correctness bugs or 'merely' horrible performance bugs based on incorrect implementations of Equals or Hashcode methods. Implementing a non-commutative Equals method is a colossal footgun, and incredibly irritating to debug, and yet somehow it happens time and again.
And there are other, subtler, arguably "correct" and yet but extremely unwise things one can when do given Java-like maps implemented on the Equals and Hashcode interface methods. For example, one can define two types T1 and T2 which both claim to be (commutatively) equal, and define some interface TX which is implemented by T1 and T2. Using a Map<TX,Object> and inserting members by T1 and T2 now has an interesting form of semi-defined behavior: if inserting "equal" keys, whichever type was used first will be persisted. Imagine doing this in some concurrent map, and imagine that T1 is 8 bytes and T2 is 400 bytes; enjoy debugging those occasional OOMs.
I've been very happy not to ever have this particular problem when defining the key types in my Go maps.
Kotlin and Scala solve the problem pretty decently. Equals and hashCode are automatically generated for data/case classes. I've never encountered a messed up equals in Scala and only very rarely felt the need to override the generated equals, and then you make a very conscious choice to do it.
C# does it even better, there is a IEquatable<T> interface that defines the `bool Equals(T other)` method. It partially solves the irritating universal equality.
If I would design Java today Object would have no methods and there would be a stdlib interface defining `equals(other: T)`, and data classes would automatically implement that interface
This polymorphic equality is the default in some functional languages, like OCaml. It's fine for simple cases, but you really do need overloadable equality. Equality as a method in OO is a problem because of the asymmetry though, and often an even bigger problem because such languages typically permit pervasive null.
The problems you mentioned:
Classes could change equals to accept subclasses. Then subclasses could not alter equality themselves.
T implementing `equals(other: U)` for some U solves a lot of other use-cases, but could make equality asymetrical.
Reference equality would still exist, but would not be the default, e.g. Java's `equals` is Scala's `==`, and Java's `==` is Scala's `eq` operator (which you almost never use)
So I completely agree, that having to implement basic attributes like 'equals' or 'comparable' is no good idea. Instead I would favor some logic which assumes some natural situation and lets you implement a custom logic if you want to do it.
Nevertheless, I think those problems just show some of the inconsistencies Go comes with. Don't get me wrong, I love Go and I like the way the Go devs work. It is just that I am worried that we will end up with Go 2.0 adding Generics without solving the issues within the otherwise pretty nice Interface system.
C++ and Java generics are nothing alike, so there's no such thing as C++/Java-style generics.
Templates are basically a form of copy/paste style substitution with fancy bits. When you instantiate std::vector<char> in C++, the compiler creates an entirely new class with "char" substituted in everywhere the type variable appears. This class has nothing in common with std::vector<MyStruct> - there's no common superclass, the compiled code may or may not be shared depending on low level details of your toolchain and the types involved, etc. That in turn means if you want to write a function that works for any std::vector you must also use templates, and your function will also be substituted, and your final binary will also have many similar-but-different copies of the compiled code, etc. However because of how C++ templates work you can achieve some pretty astonishing things with them. In particular you're getting code compiled and tuned to the exact data type it was instantiated with, so you can go pretty fast with certain data layouts.
Java generics seem superficially similar but in fact are different. In (erased) generics, a List<Foo> and List<Bar> are exactly the same class at runtime. They're both just List and both just contain objects. The compiled code is the same, you can cast between them if you know what you're doing, etc. Likewise if you write a generic function that works for any list, there's only one copy of that function. Generics have different tradeoffs: they're less powerful (you can't write a regex library with them for instance), and they don't auto-tune themselves to particular data types ... at least not until Project Valhalla comes along ... but they're backwards compatible with pre-generic libraries at a binary level and they avoid an explosion of compiled code bloat, which is a common problem with templates.
Just where is the line between "tedious but workable" and "broken?"
There are a lot of interfaces in the existing library that one could consider quite abstract as well. Reader? Writer? error? Another thing worth noting is that many/most C++ codebases manage to avoid the type of generic proliferation you're talking about.
Java is actually a much better example of where generics are used rarely and well. Containers are parameterised by exactly one thing - the type of what they contain. You don't parameterise them with different allocation strategies or comparators. Type inference means generics are often automatic, but still catch mistakes for you. And whilst nothing stops people making bad abstractions in any language, the standard library sets a pretty good example by and large.
That said, Kotlin's generics are better still and probably the best implementation I know of.
The main reasons template errors suck so much in C++ are 1) lack of a proper mechanism to constrain instantiations (hopefully addressed by Concepts) and 2) The general mess that is function overloading and automatic type conversions.
For example, the classic "template hell" error message people usually encounter when starting to use C++ is failing to properly overload operator<< (example here[1]). That error message could easily be as simple as "No `operator<<` for class C", but instead the compiler needs to inform the user of every possible overload match, every possible type conversions that could cause a match, and templates that could have matched but can't be instantiated along with a reason for the failure. Of course it doesn't help that std::ostream is absurdly abstract, but it's really just making a bad problem worse rather than the fundamental root of the problem
The reason STL errors are so convoluted is because the C++ template language is Turing complete.
With C++14 onwards, it is a matter of developers actually making use of static_assert, constexpr if and enable_if.
Not as comfortable as just using concepts, but they get the job done.
Which are needed and/or allowed for reuse because the template language is Turing complete. More restrictive type-level languages have better error messages because the compiler either understands what you're intending to do or you're limited in the things you can express, full stop.
The only reasonable ORM library I've seen is sqlboiler, which uses code generation.
type Thing interface { ... }
is better, but Thing is still a dynamic type.
Library code might be a little ugly and abstract, but as an end-developer, you usually don't need to worry about it. The code you yourself would see and write is going to have a lot more meaning and be more understandable.
I've rarely had to do anything fancy with generics in my own projects. Where I've seen it be most useful is when I want to offer a flexible public API -- in which case, the choice of writing a bit of generic library code is much less smelly than copy pasta.
But the functions you provide for the code you're writing are a library for someone else.
This is a completely false dichotomy. Just because a feature can be misused doesn't mean it's not legitimately useful. Generics make the language easier for the user, but are harder for the implementors. Most language designers / implementors recognize that this trade-off is worthwhile, and therefore nearly every other popular typed language has them. The Go implementors are the only ones against doing it, because, well, they would have to implement it, and they're concerned that it will make the codebase too complex for them to maintain. (Their sympathizers are against it too, which is still a mystery to me.)
The point isn't that it can't be useful.
In the real world, the amount of code I'm forced to understand that uses overly complicated abstractions is the issue. I'm glad one language finally took the opposite direction.
This may say more about the specific codebases you work in than it does the software world at large. I spend most of my time in a couple of languages that all have generics and don't experience the problem you describe.
Parametric polymorphic code is not hard to read at all - in fact, it is easier to reason about since it is universally quantified for all types.
go can iterate over arrays irregardless of the element type the same way with the same construct using "range", therefore "range" is polymorphic.
The real debate here is should the compiler be "privileged" or should there be API in userland to implement these behavior.
> Java, C#, Visual Basic .NET and Delphi have each introduced "generics" for parametric polymorphism.
Neither of those contradict my assertion that most programmers have never done work in a language where the type system can make guarantees that they can trust.
Outside of Java syntax noise, I really have my doubts that Java's parametric polymorphism is too complicated for the average programmer to understand.
public static <A> A identity(A a) { return a; }How many languages provide you a way to guarantee at the type level that the behavior of a function cannot be influenced by instantiation of type variables? I don't believe that to be true of anything that runs in the JVM or in .NET, for instance, as runtime type information is always available. C++ has dynamic_cast. What common language actually has parametricity expressed in the type system?
Saying Java doesn’t have parametric polymorphism because you can cheat is like saying Haskell isn’t pure because of unsafePerformIO.
A programmer can write Java in any language if they're willing to work hard enough at it.
They might try to please different crowds with Go 2. At the end of the day, they might just loose everyone.
I'm sure there are plenty of Go apps with bad architecture, where a tiny dose of generics would greatly help.
I've seen the situation in C++ where templates led to excessive templates, simply because they were "neat." As a result, I found myself debugging code that had no source code whatsoever.
But then, after seeing the examples, e.g. https://github.com/albrow/fo/tree/master/examples/, I could see the picture of what the current codebase would become after its introduced. Lots of abstract classes such as "Processor", "Context", "Box", "Request" and so on, with no real meaning.
Few months back, I tried to raise a PR to docker. It was a big codebase and I was new to Golang, but I was up and writing code in an hour. Compared to a half-as-useful Java codebase where I have to carry a dictionary first to learn 200 new english words for complicated abstractions and spend a month to get "assimilated", this was absolute bliss. But yes, inside the codebase, having to write a copy-pasted function to filter an array was definitely annoying. One cannot have both I guess?
I believe Golang has two different kinds of productivity. Productivity within the project goes up quite a lot with generics. And then, everyone tends to immediately treat every problem as "let's create a mini language to solve this problem". The second type of productivity - which is getting more and more important - is across projects and codebases. And that gets thrown out of the window with generics.
I don't know whether generics is a good idea anymore. If there was an option to undo my vote in the survey...