Toward Go 2
blog.golang.org
blog.golang.org
> I can't answer a design question like whether to support
> generic methods, which is to say methods that are
> parameterized separately from the receiver.
I work on the Dart language. Dart was initially designed with generic classes but not generic methods. Even at the time, some people on the team felt Dart should have had both.We proceeded that way for several years. It was annoying, but tolerable because of Dart's optional type system -- you can sneak around the type checker really easily anyway, so in most cases you can just use "dynamic" instead of a generic method and get your code to run. Of course, it won't be type safe, but it will at least mostly do what you want.
When we later moved to a sound static type system, generic methods were a key part of that. Even though end users don't define their own generic methods very often, they use them all the time. Critical common core library methods like Iterable.map() are generic methods and need to be in order to be safely, precisely typed.
This is partially because functional-styled code is fairly idiomatic on Dart. You see lots of higher-order methods for things like manipulating sequences. Go has lambdas, but stylistically tends to be more imperative, so I'm not sure if they'll feel the same pressure.
I do think if you add generic types without generic methods, you will run into their lack. Methods are how you abstract over and reuse behavior. If you have generic methods without generic classes, you lose the ability to abstract over operations that happen to use generic classes.
A simple example is a constructor function. If you define a generic class that needs some kind of initialization (discouraged in Go, but it still happens), you really need that constructor to be generic too.
`range` is generic, and by virtue of that is a builtin which only works with a subset of the also builtin magically generic types.
The reason why Go "does not feel the same pressure" has nothing to do with its imperative style[0] it is because they special-cased a few generic structures as builtin very early on, unlike, say, Java (which only had arrays as a typed datastucture).
[0] Java and C# are could hardly be more imperative, hell Java is only just adding anonymous functions
For instance, you have two functions. One takes `Function<Type, Type>` and the other takes `UnaryOperator<Type>`. Giving the "same" lambda to both functions will result in two anonymous inner types, one implementing both interfaces.
They also sucked as an interface (for developers to use), resulting in them only being used when no other option was available (Comparable) or for cute hacks (double-brace initialisation).
My opinion is that Dart's type system is the optimal type system. It allows for statically typed API surface, but at the same time, I can still add some dynamic voodoo under the surface and enable productive meta-programming.
> As a far as I can tell, an "optional type system" is
> just an unsound type system with a marketing name.
It is a term with a specific meaning. The key bit separating it from both gradual typing and a static type system with a dynamic type (like Scala or C#) is that in an optionally typed language, the type system has zero runtime effect. If you were to take a source file and mechanically strip out all of the type annotations, the resulting program behaves identically to the original one.Dart 1.0 was an optionally typed language. Dart 2.0 is not -- we've gone to a mandatory static type system (with lots of inference and a dynamic type). The limitations of optional typing are simply too onerous, and it's just not what users expected or want.
> Any decent static type system would allow one to
> progressively increase the level of typing, the weakest
> form being using a Variant type to hold all other types.
That's the dream of optional typing and gradual typing. In practice, I have yet to see a language that does this harmoniously. It sounds simple, but things quickly get very complex when you start working with generic types and first-class functions because the boundary between the untyped and typed code gets very strange. To keep the guarantees you expect inside your typed code, you have to figure out how to handle untyped data that flows into it. That gets nasty very quickly when that untyped data may be an instance of a generic class or a function.This would be solved in a conventional static type system by forcing the user to eliminate the variant, and forcing the user to deal with the case that it has an unexpected value.
So I cannot see why even C# dynamic is necessary, except to perhaps save some thinking, but this isn't a good thing IMHO.
The semantic differences for OLE2 Variants vs C# dynamic mostly only exist in implementation protocols.
Makes little difference, either way you need a magical type since it has to allow any method call at compile time, and then requires enough RTTI that it can check for and dispatch method calls at runtime. Whether you call it Variant or dynamic has no relevance.
GHC also considers Dynamic->Dynamic and Int->Int to be incompatible types, which is not quite what one would want in a gradualy typed setting.
Haskell also sidesteps the difficulties that come with mutability in a gradualyly typed setting.
Sort of. If you wrap a polymorphic type inside a monomorphic one then it can handle it fine, for example
data Mono = Mono (forall a b. a -> b -> a)
(But perhaps you know this)> The problem munificent was talking about is that for more complex types you cannot write that type checking predicate you want, which would check if a value belongs to the given type and return a variant-free version of the object.
The complexity of the type should not matter to a sound type system. Assume a "fromVariant" function that unpacks the variant, it has a type: forall a. Variant -> Maybe a. Maybe is either Nothing or Just a, depending on whether the variant does indeed contain what you expect it to contain.
> if you have a reference to an array of integers in the typed part of the code you need to protect against untyped parts of the code inserting non-integers inside the array.
This "protection" is exactly what static type systems do. "Untyped code" in the context of using a static type system, is code that works with variants. So we need to pack the array of integers into a variant, where they will be suitably tagged by the runtime. The "untyped code" is free to write whatever it wants into a variant, so we must unpack it with runtime checks. The static type system forces us to unpack and assert its contents, before we can assign a specific type to it.
> The only solution is to insert typechecks in the reads and/or writes to the array
I am not sure what you mean here. When the array is packed into a variant then it can indeed be modified into anything by "untyped" code. But it should always be tagged correctly, which is the responsibility of the runtime. It will need to be re-asserted when unpacked into a specific type.
> A similar problem happens with functions
Functions should be values like any other, so my points apply to them too.
The point I have been trying to make, is that a conventional static type system will already allow for "gradual typing". I cannot see a need to use an unsound type system in order to achieve this goal.
Anyway, going back to the topic, I'll try to explain why your idea doesn't work.
> assume a fromVariant function
What I was trying to say on my other post is that you can only create such a function for primitive types (numbers, booleans, strings, etc). And perhaps also for objects, if your language has something analogoud to an instanceof operator.
But for the higher order stuff like functions and mutable references you cannot write such a fromVariant function so easily! Suppose I write down a dynamically typed implementation of the identity function and pass it to some typed code that expects an int->int function. When we pass the function to the typed side we will call fromVariant on it but the best we can do at first is check if the value we have is a function (and not an integer, etc). There is no way that fromVariant can also check the function signature, to guarantee that the dynamically typed implementation will always return an integer when given an integer.
One way to solve this problem is for fromVariant to return a wrapped version of the dynamic function that checks if the returned value is of the expected type. But these wrappers can slow down the program a lot and they also mess up with pointer equality...
> the arrays
The basic problem with the arrays is similar to the one with function. The statically typed code can't blindly trust that the array will always contain the type it expect because at any moment the untyped portion of the code could try to insert junk values into the array. You will either need to do a runtime check when the dynamic code writes to the array (which slows down the dynamic code) or you need to add runtime checks when the static code reads from the array (which makes it slower than it would be in a fully static language).
Things also get extra tricky if the array contains non primitive values (functions, other arrays, etc)
---
If you are interested I can send you links to a couple of gradual typing papers. As munificent said, there is currently lots of ongoing research about this and we all wish it were as simple as you were suggesting it would be :)
I was trying to show that "gradual typing" should be possible in any reasonable static type system, without resorting to unsoundness. I do concede that there is probably a definition of the term "gradual typing" out there that I am not strictly adhering to.
> Suppose I write down a dynamically typed implementation of the identity function and pass it to some typed code that expects an int->int function. When we pass the function to the typed side we will call fromVariant on it but the best we can do at first is check if the value we have is a function (and not an integer, etc).
If the best we can do is tag it as a function, then fromVariant would unpack it to a function with type Variant -> Variant, which represents all dynamically-typed functions. When we call this function, we indeed would need need to assert the outputs.
> One way to solve this problem is for fromVariant to return a wrapped version of the dynamic function that checks if the returned value is of the expected type
Agreed. Although the danger of this is that you have a bomb waiting to go off inside some unsuspecting piece of typed code. The alternative to to acknowledge that it is a partial function in the type, e.g. the Variant -> Variant function becomes Int -> Maybe Int, or similar. The point is that we are solving these problems using conventional static typing. You haven't yet convinced me that this doesn't work.
> You will either need to do a runtime check when the dynamic code writes to the array (which slows down the dynamic code) or you need to add runtime checks when the static code reads from the array (which makes it slower than it would be in a fully static language).
It sounds like you are concerned with performance and efficiency, that may indeed be an issue, but it's orthogonal to the type system. My own position is that if one wants performance, then don't write untyped code!
> If you are interested I can send you links to a couple of gradual typing papers. As munificent said, there is currently lots of ongoing research about this and we all wish it were as simple as you were suggesting it would be
Thanks for the offer, I am definitely interested. I suspect that the outstanding issues are around usability and efficiency. I stand by my original point that "gradual typing" should be possible in any sufficiently expressive static type system.
The basic goal of gradual typing is that you can have your program be fully untyped or fully typed or somewhere in between and it will behave in a sensibly in all cases. Adding types to a program does not change the result of the program, except that sometimes adding types may cause new type errors to show up.
> the best we can do is tag it as a function, with type Variant -> Variant
This is very limiting. With this restriction, you cannot assign an accurate static type to a function with a dynamically typed implementation, which means that you can't use dynamically typed libraries from inside statically typed code.
Similarly, you cannot use statically typed objects inside the dynamically typed code because all objects that can be touched by the dynamically typed part of the code must have all their fields typed with the Variant type.
> Although the danger of this is that you have a bomb waiting to go off inside some unsuspecting piece of typed code
Technically the bomb is inside the dynamically typed part of the code :) One of the nice things you can get with a well though out gradual typing system is a guarantee that the line number in any of the type error messages will point to the dynamically typed part of the code. We call this blame tracking.
> It sounds like you are concerned with performance and efficiency, that may indeed be an issue, but it's orthogonal to the type system
Yeah, but one of the big challenges with gradual typing today is that i we try to be flexible (letting programmers add types wherever they want) and sound (with a soud type system, blame tracking, etc) all the implementations so far have had very bad performance, often running much much slower than the untyped version of the code would (with factors like 5x or 10x slower being common).
When the performance gets this bad it effectively means that your only options are to have fully typed code or fully untyped code, because almost anything in between will run up super slow. Which kind of ruins the whole point of being able to gradually add types to your program.
> Thanks for the offer, I am definitely interested.
You can get very far by searching google scholar for "gradual typing. One good place to start out would be Siek and Taha's original paper on gradual typing for functional languages[1] and Sam Tobin-Hochstadt's PHD thesis on Typed Racket[2] (which used to be called Typed Scheme back then). For an example of the challenges of gradual typing in practice, this recent paper [3] might be interesting.
[1] https://cs.colorado.edu/~siek/pubs/pubs/2006/siek06:_gradual...
[2] http://www.ccis.northeastern.edu/racket/pubs/dissertation-to...
> With this restriction, you cannot assign an accurate static type to a function with a dynamically typed implementation, which means that you can't use dynamically typed libraries from inside statically typed code.
It depends what you mean by an accurate type. A accurate type for adapting your example would be to expose Variant -> Variant as Int -> Maybe Int, thus capturing the partiality. I concede that Int -> Maybe Int cannot be used in a function that expects Int -> Int, but that is how it should be! An untyped language cannot provide me with a function of Int -> Int.
> It depends what you mean by an accurate type.
The gradual typing literature tends to prefer having partial functions (that might raise well-behaved type errors at runtime) because it eases the transition between typed and untyped programs. You can add or remove type annotations without having to simultaneously add and remove tons of explicit pattern matches against Maybe. But this isn't the part I was talking about.
The key issue is that when you cast a function from (Variant->Variant) to (Int->Maybe Int) you need to add a small wrapper around the dynamic function that converts the input from Int to Variant and converts the output from Variant to Maybe Int. In theory it is simple but in practice all those wrappers can add up really quickly...
Functions are the simplest case where these higher-order type-conversion issues pop up but in my experience things are actually the most thorny when it comes to mutable objects and arrays. The extra type checks and conversions around every property read and write can add a ton of overhead. And from a semantics point of view you need to be careful because if your implementation is based around wrappers it could end up messing with object identity (like Javascript's === operator or Python's "is")
If you wanted to add variants to Dart, you'd either need to add this runtime information, or limit the Dart typesystem to be no more and no less than what the JavaScript typesystem already stores dynamically.
If Dart wants to distinguish ints and doubles, it needs to keep track of that on its own; if it has a JavaScript variable x and needs to know calculates x/2, it won't know whether to round it as if it were integer arithmetic, or return a floating-point value. Usually it is enough to do this at compile time (the function calculating x/2 knows whether it's halving a Dart-level double or a Dart-level int), and so there's no need to make x an object that remembers its own Dart type.
Also, even if JavaScript distinguished ints and doubles, a higher-level language is likely to want to have multiple kinds of things with an int representation: bools, enums, bitfields, Unicode scalar values, etc. Again, ideally that information is tracked at compile time and the knowledge of what to do with an int representation is inserted into the compiled code, and so no metadata needs to be stored at runtime.
An important aspect of type annotations, for example, seems to be that they help document the API of a module, regardless of whether they're statically checked. This is especially relevant with respect to maintenance tasks.
Conversely, the benefits of static type checking to get correct code initially seem to be a lot murkier. There seems to be a cost associated with static types and that is that it takes longer to write code; time that can be spent in a dynamically typed language on other validation work (remember that types do not even remotely capture all software defects).
This means that it is definitely worth exploring the benefits of optional/gradual/soft typing vs. static typing and/or runtime type checking vs. compile-time type checking.
Conversely, the assumption that static types are an unalloyed good is not currently borne out by the evidence (in part, of course, that is because the evidence is inconclusive).
But this is not the only cost that matters, indeed might not even be a cost.
I've gone from being neutral about static vs. dynamic types to being pro-static types -- and the change happened when maintenance became a bigger part of my job.
Writing new code is now far less important to me than looking at a small part of a large system and trying to understand what is and is a not possible at that point. Static typing does not make this easy, but dynamic typing makes it far more difficult.
I'm not saying otherwise. My point is that there's no objective way to proclaim one better than the other. This depends on application domain, economic constraints, engineering constraints, what you're doing, and so forth.
Writing Ada software that controls a pacemaker has totally different requirements than exploratory programming in Jupyter that mostly deals with integers, floats, and arrays and matrices thereof, for example.
Very true. But any analysis that emphasizes writing code over maintaining it will systematically bias itself in favor of dynamic typing.
Interestingly I have had the converse debate with some of my colleagues, who have learned to hate Python because they keep having to debug existing systems. I try to tell them that it is an excellent language for the kind of one-off data-analysis that I did when I was a scientist.
They don't believe me, because here among software engineers, seemingly innocent 400 line scripts keep growing into giant, decade old, 100kloc typeless monstrosities.
This is not what the studies do. There is no emphasis on anything. They look at how people do on a number of different tasks with different typing options and report the results.
Also, it's not just dynamic vs. static typing. Gradual and soft typing is also of interest, because it allows you to turn dynamically typed code into statically typed code without a complete rewrite.
The sweet spot for me at the moment is a language which is fundamentally static but has good type inference so that you don't actually have to mention types all the time.
Haskell's pretty good at this, although because it's possible to write many Haskell programs without actually mentioning a single type anywhere it can be a pain to read the results. Rust deliberately constrained type inference so that you have to have type annotations on functions, which makes sure your interfaces don't get too murky when reading the code.
I'll go with the idea in another reply that static typing really starts to show its benefit in long-lived code bases under maintenance.
Certainly a static type checker makes refactoring a lot easier, in my experience.
Note that I wrote statically checked. You can also check them at runtime (basically as a form of Design by Contract), which is what Dart does at the moment.
Runtime type checks allow you to greatly simplify the difficult parts of the type system or make it more expressive.
People often forget how many expressiveness shortcuts we take so that our type systems keep working out. We have integers (which are in actually practice often just elements of a finite ring), but introduce natural numbers and suddenly a lot of static type checks stop working out. How about interactions between the natural numbers with and without zero? Integers modulo your wordsize vs. infinite precision integers? Can you statically check overflow or underflow? Dart has both `num`, `int`, and `double` types, and `int` and `double` are subtypes of `num`, but `int` is not a subtype of `double`; readAsBytesSync() returns an `Uint8List` that conforms to `List<int>`. From a mathematician's perspective, computer science type systems are usually painfully simplistic. They usually capture something about the representation of the data, not its actual mathematical properties. Their role is often is to prevent undefined behavior, not to capture the actual semantics. Computer science typing often tends to be opportunistic rather than semantically oriented, based on what's easily possible rather than what one wants to express (see the entire family of covariance issues).
As an extreme example, consider GAP's [1] type system. We're dealing with computer algebra here, and some of the types simply cannot be captured at compile-time. Any type annotations would have to be checked at runtime, other than the trivial ones (and even integers are basically a subtype of the rationals or any polynomial ring with integer coefficients).
If I want to do effective implementations of matrices and vectors over finite fields, I pack several finite field elements into a machine word and extract them; no static type system in existence has even remotely the power to statically typecheck these operations for me, unless layered over a ton of unsafe code, and then only for some languages.
From the computer science side of things (I have a background in formal methods), type systems are underpowered specification languages that just pick a small subset of a formal spec that happens to be convenient to prove. They can express only a subset of program behavior (compare what types in even (say) Haskell do to a full-blown formal specification in Z) and often not even the interesting ones. As a result, I don't particularly stress out over whether checks only occur at compile time. Runtime type checking is simply a different tradeoff in that you move from the set of types that you can build a sound type system around to predicates that can be expressed at runtime. It's simply a different set of tradeoffs.
Perhaps. As someone who has used Python and Haskell for roughly equal amounts of time in my professional career I definitely find Haskell faster to write in.
(1) creating abstractions that are typed correctly is easy
(2) the existing abstractions you're building from are correctly typed
(3) the properties that the type system proves are valuable enough to reward the effort
Whether (1) is true depends on your type system. If it's very simple (e.g. dynamically typed), it's trivially true. If it's rich and expressive, it can sometimes be impossible to express your intent without obfuscating your types (e.g. using pointers and typecasts or polymorphism or some other loophole); and with some type systems, it's plain impossible to express some valid programs.
Whether (2) is true is a social problem. You need to have enough sensible people in your community such that you get to use List<T> rather than ObjectList with typecasts, or what have you. What's available in the runtime libraries often translates into what's commonly used for APIs in third party modules, and if the library of types isn't rich enough, APIs will use loopholes or typecasts or other mechanisms that obfuscate types. Third party code turns into a kind of firewall that stops your types (and the information, proofs, they convey) from flowing from one module to another in your program.
Whether (3) is true also depends on your type system, but also on your application domain. It's in direct opposition to (1); the more you can prove with your type system, the harder it normally is to write code that proves exactly the right things, and neither more nor less (the risk is usually too little type information flowing through). If the properties being proved are important to your application domain, then that gives a big boost to type systems that can prove those properties. If it's just because it gives you warm fuzzies, and testing is what proves interesting things because interesting properties (in your domain) are harder to prove with a type system, then rich type systems are less useful.
Personally, I was once a strong proponent of rich expressive type systems. But I eventually learned that they can become a kind of intellectual trap; programmers can spend too much time writing cleverly typed confections and less time actually solving problems. In extremis, layers of abstraction are created to massage the type system, often in such an indirect way that the value of the properties being proven are lost owing to the extra obfuscation. Now, I see much more value in a strong type system for languages like Rust, but much less value at higher levels of abstraction. That's because many of the properties being proven by Rust (like memory safety) are already proven in high-level dynamic languages through GC and lack of pointers and unsafe typecasts. Whereas application-level properties being proven through type systems are seldom valuable.
Documentation and tooling are much stronger arguments for types in high-level languages than proofs; ironically, this means that the type systems don't need to be sound, as long as they're usually OK in practice!
(1) I started porting our high-performance, concurrent cuckoo hashing code to Go about 4 years ago. I quit. You can probably guess why from the comments at the top of the file about boxing things with interface{}. It just got slow and gross, to the point where libcuckoo-go was slower and more bloated than the integrated map type, just because of all the boxing: https://github.com/efficient/go-cuckoo/blob/master/cuckoo.go
(my research group created libcuckoo.)
Go 1.9 offers a native concurrent map type, four years after we looked at getting libcuckoo on go -- because fundamental containers like this really benefit from being type-safe and fast.
(2) I chose to very tightly restrict the initial set of operations we initially accepted into the TensorFlow Go API because there was no non-gross way that I could see to manipulate Tensor types without adding the syntactic equivalent of the bigint library, where everything was Tensor.This(a, b), and Tensor.That(z, q). https://github.com/tensorflow/tensorflow/pull/1237 and https://github.com/tensorflow/tensorflow/pull/1771
I love go, but the lack of generics simply causes me to look elsewhere for certain large classes of development and research. We need them.
Note that, at least in its current form, the native concurrent map type uses interface{} for all keys and values, and therefore offers no type safety:
https://github.com/golang/go/blob/master/src/sync/map.go
See also: https://github.com/golang/go/issues/18177
All of Go's built-in pseudo-generic types (e.g. maps) require special support from the parser. I'm not sure if they plan on doing that for sync.Map as well, but this is clearly an area that could benefit from generics.
For (1), for curiosity sake, I tried benchmarking the cuckoo.go file you posted. I'm curious if these numbers are in line with what you found. The first test I did was a Rand test, Putting 10,000 random string values under random string keys, then Getting the same 10,000 keys, then Getting 5000 more random keys. The first line below uses default code you posted, which uses
type keytype string
type valuetype string
The second line specializes the code to just using the string type directly for keys and values. The third line uses interface{} instead for values, which is the code style HN doesn't like. BenchmarkRandInsert-8 100 13474408 ns/op
BenchmarkStringStringRandInsert-8 100 13585071 ns/op
BenchmarkStringVoidStringRandInsert-8 100 14126666 ns/op
That third line shows the cost of casting to/from interface{}. The difference in the code was about as you might expect, with the "void" example needing a cast on each put and a cast on each get.If I use uint32 for value instead, I get:
BenchmarkStringIntRandInsert-8 200 9431584 ns/op
BenchmarkStringVoidIntRandInsert-8 100 10485785 ns/op
The 9.4ms result is using a version of your code hand-specialized for string key and uint32 values. The 10.4ms result uses interface{} for values.I ran a sequential-insert test as well, with keys and values generated sequentially instead of randomly. The results are much the same, though the cost of casting to/from interface{} seems to get mostly lost in the noise.
BenchmarkSequentialInsert-8 100 12185272 ns/op
BenchmarkStringStringSequentialInsert-8 100 12233946 ns/op
BenchmarkStringVoidStringSequentialInsert-8 100 12543980 ns/op
BenchmarkStringIntSequentialInsert-8 200 8823747 ns/op
BenchmarkStringVoidIntSequentialInsert-8 200 8709242 ns/op
I also tried some tests using uint32 as the key type. This int key specialized version is 3x faster than any of the string key versions, but it's not really a fair comparison... a small part of the code is specific to string keys (getinthash), so it's not as obvious what the generic equivalent of that function would be. The keys need to be Hashable or some such, not just interface{}. I'm also allocating and formatting random strings in one case, vs just picking random numbers in the other case.I didn't find the interface{} casting in any of the above code too horribly "gross", just a bit ugly, but that's entirely subjective. Yes, all of the cuckoo versions seemed about 30% slower than the corresponding builtin map type, but my benchmark numbers don't show if that is the price of boxing, of supporting concurrency, or maybe function call overhead, or something else. I'm guessing you did more detailed benchmarks that point to the source of the slowdown.
One last comment: keytype and valuetype really need to be exported, don't they? I wasn't able to use the library without changing them to exported symbols.
Overloading is usually not a "generics" concern in most mainstream languages[1]. If you do a "type class"/"Rust-style-traits" (or even multiclasses) thing then it sort of naturally takes a front seat. Incidentally, Haskell fucked this one up horribly in some respects, see the "Num" type class.
Of course, e.g. Java could theoretically add an interface called Addable to complement Comparable which would sort-of allow generic "+", but they've chosen to not opt for that. The fact that the "first parameter" is privileged is also a bit of a detriment to such syntax conveniences. (I can expand on this, but it's bascially because that for "X op Y" and "Y op X" to work, they both need to know of each other. This problem can be solved by type classes, but cannot be solved by overloading, AFAIK.)
[1] If you do have overloading from the outset (like Java, IIRC), then you're constrained because you need to be able to "pick the most specific overload" in a "generic" context.
I guess the performance issue has something to do with some missing optimizations. Hopefully it will get better.
Edit: Also https://github.com/golang/go/issues/19361
> For example, I've been examining generics recently, but I don't have in my mind a clear picture of the detailed, concrete problems that Go users need generics to solve. As a result, I can't answer a design question like whether to support generic methods, which is to say methods that are parameterized separately from the receiver. If we had a large set of real-world use cases, we could begin to answer a question like this by examining the significant ones.
This is a much more nuanced position than the Go team has expressed in the past, which amounted to "fuck generics," but it puts the onus on the community to come up with a set of scenarios where generics could solve significant issues. I wonder if Go's historical antipathy towards this feature has driven away most of the people who would want it, or if there is still enough latent desire for generics that serious Go users will be able to produce the necessary mountain of real-world use cases to get something going here.
See also all of the "container" portion of the stdlib: https://golang.org/pkg/container/
All of those should be generic, but aren't.
Clearly there's already sufficient demand for generic collections just based on the stdlib and the fact sync.Map was seen as needed.
They aren't because the prevailing attitude is "what do you need generics for? Just do a cast!"
The issue is the Go developers are unwilling to consider generics until someone can come up with a problem that absolutely, positively cannot be solved without generics. And of course there isn't one.
Go is turing complete already, so all problems are already able to be solved. :)
[1]: https://groups.google.com/forum/#!topic/golang-nuts/RKymTuSC... [2]: https://github.com/robpike/filter
I think even more damning then "these should be generic" is that there should be more of them. The real problem is the lack of generics inhibits the introduction of more data structures because they are hard to use.
It is true that arrays and maps/hashes take you a long way, but there's a lot of other useful data structures in the world. And despite the fact they may look easy at first, data structure code tends to be notoriously difficult to write both correctly and with high performance.
I believe your impression of the Go team's position has been corrupted (likely unintentionally) by intermediaries.
If you scroll to the bottom, you can see 4 large proposals written by Ian over the years.
C# isn't mentioned in any of the previous proposals, only Java, C++ and C.
Do you need a full-on JIT, or just a runtime? [1]
[0]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-g...
[1]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-g...
The AS/400 uses a kernel level JIT, binaries are only JITted at installation time or explicitly via the command line.
The Lisp environments also only used JIT once.
Some implementations of Oberon would use bytecode instead of native code and JIT on module load.
There are many other examples.
https://en.wikipedia.org/wiki/Eiffel_(programming_language)#...
Eiffel is a very powerfull language with good tooling, just failed adoption, because licenses were too expensive and software industry still has issues valuing quality.
There is also an interesting anecdote about use of Eiffel in the book, here:
https://en.wikipedia.org/wiki/Object-Oriented_Software_Const...
That section shows the the 1st edition of the book did use Eiffel (as I said above).
So combining the easiness of interactive development with performance when it was time to ship the product.
It was also available before Java was a thing.
This is what I always kind of missed with Java and .NET, the previous generation of languages (Eiffel, Modula-3, Oberon(-2)) all had a mix of interactive development and AOT compilation for shipping release builds.
The Eiffel compiler required four distinct sequential processes (note: not phases. I really mean that four programs needed to be run in sequence, and each of these four programs implemented who knows how many passes). Eiffel generated C++, huge executables, was incredibly slow (even with contract stuff turned off).
It was very hard to debug, with gdb routinely giving up on the complex .o generated. It was a very verbose language that implemented all known features under the sun, a bit like Ada and Scala.
Eiffel didn't take off for some very solid reasons.
(Haven't worked much on Java recently, but did use it a fair amount some years ago.)
Isn't Java roughly equally verbose as Eiffel?
>that implemented all known features under the sun
At least based on looking at the Wikipedia article for Eiffel, it does not seem to have implemented functional programming, unless the article is out-of-date, or unless the agent mechanism supports FP somehow (not sure about that last bit).
This was not an inherent problem of the language, but one of the implementation.
I know this because I wrote a compiler for an Eiffel dialect for my Ph.D. thesis and there have been other Eiffel compilers without these shortcomings.
Also, EiffelStudio – which seems to be what you are talking about – generated C code, not C++ and had the option to generate an intermediate representation instead that could be mixed with compiled code.
> It was a very verbose language that implemented all known features under the sun, a bit like Ada and Scala.
It was actually pretty minimalist, not at all like Ada or Scala.
Code was verbose because (1) programmers were encouraged to annotate methods with contracts and (2) exactly because it was minimalist and didn't have a lot of alternative ways of expressing the same functionality.
Edit: There are two hard parts about writing an Eiffel compiler.
1. Getting the semantics of feature renaming during inheritance right. If you aren't careful, it's easy to introduce errors during this step.
2. Doing separate compilation for mutually dependent classes. This is where much of the complexity of the EiffelStudio compiler comes from. Java solved the problem by simply not allowing separate compilation in those cases. If class A has a member of type B and B has a member of type A, they need to be compiled together.
Everything else that's involved in writing an Eiffel compiler does not require more than than what you learn in your typical compiler course. (Though, obviously, experience will get you better results.)
I think that must be what I was referring to by my mention about melting or freezing in a nearby comment. Didn't remember the technical details. Interesting feature. Wonder if other languages have something like that.
(Edit: Reading over it again, I realize that this may be read as criticism. To clarify, while I disagree with Meyer on some things, this isn't about disagreement. He is a teacher at heart – in fact, a very good teacher – and you can see some of his habits for keeping students engaged sometimes carry over into his writing.)
You don't see this approach really anymore, because these days it's so cheap to just generated unoptimized native code if compilation speed is an issue.
Obviously, if you have a JIT-compiler, then something similar happens internally in that some methods will be compiled, while others will be left in IR form (and sometimes, methods will even revert to using their IR). This was technology from back when CPU speed was still expressed in MHz.
OCaml had and still has both a bytecode and a native code format (for similar reason), but they cannot be mixed. OCaml's bytecode still sees use for (1) the bootstrap process, which allows you to build OCaml from scratch, (2) to run OCaml code on oddball architectures that the native compiler doesn't support, (3) to run code in the timetraveling debugger, and (4) when you need compact rather than fast executables.
OCaml's bytecode interpreter makes use of the fact that OCaml is statically typed and therefore doesn't incur nearly as much of a performance penalty as bytecode interpreters for dynamically typed languages.
[1] https://www.eiffel.org/doc/eiffelstudio/Melting%20Ice%20Tech...
>Obviously, if you have a JIT-compiler, then something similar happens internally in that some methods will be compiled, while others will be left in IR form (and sometimes, methods will even revert to using their IR).
I was going to say (in my parent comment to which you replied) that maybe that Eiffel's Melting Ice tech is something like Java JIT tech, but wasn't sure if they were the same thing.
The others already replied why.
Why on earth would one need to use gdb, given the nice graphical debugger of EiffelStudio?
Also it was way faster than code produced by Java, which I remind only got a JIT with version 1.3 and to this day only third party commercial JDKs do offer support for AOT to native code.
Java is also quite verbose. Verbose languages are quite good for large scale development.
On real life the majority of code is read not written, so I rather have it verbose and comprehensible than trying to make sense of a line full of hieroglyphs.
That sounds like unrelated to the semantics of the language.
I seem to remember that there was some intermediate form of language translation too - something called melting or freezing or some such (might have been two different things). This was for EiffelStudio, probably, not generic Eiffel stuff.
>This is what I always kind of missed with Java and .NET, the previous generation of languages (Eiffel, Modula-3, Oberon(-2)) all had a mix of interactive development and AOT compilation for shipping release builds.
Interesting. Didn't know that that generation had both. As an aside, I had read a good description about Modula-2 (not -3) in a BYTE article (IIRC, years ago), and it seemed to me at the time that it was a good language. Having had a good amount of Pascal background before doing a lot of C, I was interested to try out Modula-2, but probably didn't have access to any compiler for it at the time. Later I read that Delphi's feature of units for modularity may have been inspired by Modula-2's modules, along with the concept of separate compilation.
The OOP features in Turbo Pascal are based on Object Pascal, which was designed at Apple for their Lisa and Mac OS systems programing language, with input from Niklaus Wirth.
Turbo Pascal was the reason why most PC developers never had too much love for Modula-2, because by Turbo Pascal 4.0 we had all the goodies from Modula-2 with case insensitive keywords.
Also, the compilers were more expensive than Turbo Pascal ones.
Incidentally Martin Odersky was the author of Turbo Modula-2, the short lived Modula-2 compiler sold by Borland.
I usually thank my technical school for having us go through Turbo Basic and Turbo Pascal before getting into Turbo C.
We were using Turbo Pascal 5.5 (I already knew 3.0 and 4.0) and 6.0 was still hot out of Borland's factory, which is why I've always been a fan of strong typed systems programming.
Turbo Pascal was the Pascal I'd used too, for a good while. Loved that environment. Really fast dev time and a fun language to work in.
In the end, Eiffel stuck with covariance and a type system that is technically unsound, because the simplicity of the type system outweighed any hypothetical gains from making it fully sound.
Practical problems with Eiffel's generics arise mostly from the fact that, like Java, constrained genericity inherently relies on nominal subtyping, which makes it less expressive than it could be.
[1] https://www.cs.princeton.edu/courses/archive/fall98/cs441/ma...
https://en.wikipedia.org/wiki/Generic_programming#Genericity...
I may not be able to throw more light on it than that, sorry, since I'm not an expert on these areas, just interested.
As other respondents to the GP comment show, it's not at all hard to come up with that ostensibly elusive picture if you're attending to it. So I suppose the obvious takeaway is that they're still not really attending to it. Which is fine, Go clearly has its niche and its fans, and people who don't want a pretty good Blub have other options.
Finer resolution would be if fusiongyro had characterized the golang team as saying "fuck off developers who want generics," which wouldn't have been as polite as the phrase "I don't have in my mind a clear picture of the detailed, concrete problems that Go users need generics to solve," but whose semantics are otherwise equivalent.
I suppose the argument I'm making does indeed depend on the premise that essential semantics of "fuck" can be conveyed in other ways. If you like, we could explore this by example.
He's explicitly rude. Sets up a strawman argument with the developer who can't imagine programming without generics. Mocks the people who want generics. Gasps in faux disbelief that it could be solved by types.
And then, when he goes in for the kill, to show once and for all that generics are pointless garbage:
He attacks type hierarchies and inheritance... completely missing the point by conflating parametric polymorphism and subtype polymorphism.
I think "Fuck generics" does a decent job of summarazing the rudeness, misinformation and ill-thought out arguments that the article contains about generics.
If you think you know how I ought to spend my time, come over and mow my lawn while I expound on the problems of dev entitlement culture.
I'm fine with RSC, Commander Pike, and those other benevolent dictators continuing to make much better decisions that I certainly ever would and more importantly to be super careful with what they add to the language. I write Swift, JS, objC, and Go. Go is the preferred language because it's simple and I have the entire SDK in my head.
I use Go a lot. I can't say i've ever run into not having generics being a huge deal.
Let's face it, for most programmers out there, generics is for type safe collections. If slices and maps weren't built into Go then i'd prolly have moved to something else.
That said i'd like to see more built in data structures
Actually you very much can. It's the perfect word to describe certain attitudes, even if the person didn't use the word. It just translates to "who cares", "we don't need no stinking Generics", etc.
It can also convey many other things: http://reallifeglobal.com/how-use-word-fuck/
And that's just in your first sentence.
My impression has always been that the Go core team is very open to the possible addition of generics, but also very wary of the very real downsides that generics have. Nothing I've ever seen from Rob or other core team members contradicts that. I'm open to citations if you can give any.
In contrast, a lot of Go users and proponents seem to be vehemently against generics, for wildly differing reasons. Some seem to be against them just for the lulz, others for crazy conspiracy theory or obscure philosophical reasons.
Me, I miss generics sometimes, but not enough to get riled up about it. I hope the Go folks come up with a clean, simple, and efficient way to get them, but until then I'm happy with the features Go has.
That sure is a concern when just importing fmt increases your binary size from 900k to 1.6M…
If I am debugging some C++, and I find myself looking at templated function, then I can't easily see what types the template parameters are. Also it becomes hard to navigate to method calls etc.
C++ is a particularly bad example, because template metaprogramming is essentially typeless. But any form of polymorphism introduces these problems to some extent.
To be fair, that includes the run-time polymorphism already available in Go. Thus if generics allow us to replace some `interface{}` hacks with properly typed middle-men (even if they compile down to `interface{}`) then that would be genuine benefit.
More often I am navigating through source code comparing it to logs, stack traces and other evidence that I can grab of what went wrong in production.
The source-level is important, because although I said "debugging" in my comment, what that very often comes to is first figuring out the intent of some other engineers from the thing they wrote -- and what they wrote is the source code, not the program state.
For code generation you can do what Java does and erase generics (i.e. ignore them), in which case it will be no slower than code generation for manually erased code, which is what programmers have to write now. For good run-time performance you'd want to do specialisation, which may take longer but not much longer if the compiler is clever enough not to recompile the same method many times. C# does that and its implementation is actually fast enough for a JIT, let alone ahead of time compilation.
Yeah, god forbid someone is not absolute in their statements, but gives the benefit of the doubt. I'd say it only counts as "weasel" word when you don't say what you mean to say or want to hide your true intention/reality. But, for all the "maybe" etc, the parent made clear what he thinks: that the Go team doesn't care about Generics. He's not going two ways about it, for this to qualify as "weasel" wording.
>My impression has always been that the Go core team is very open to the possible addition of generics, but also very wary of the very real downsides that generics have.
Ten years on, and just some a handful of posts and "proposals" (not bigger than 1000-2000 words) written, for something that should have been there for the start (and for other languages with even less resources, like Julia, Nim, Rust, Crystal, Haxe, etc, has been), does not look like "very open".
I'd get the "We have a certain philosophy about Go, with which Generics don't match". Or "we don't like them, deal with it". But this back and forth, and ifs and buts ad inifinutum borders on the passive-aggresive treatment of the issue.
The "very real downsides" are the same old engineering tradeoffs every language had to make. "We'll only add them when we can have our cake and eat it too" is a weasel approach.
>Nothing I've ever seen from Rob or other core team members contradicts that. I'm open to citations if you can give any.
If by citations you mean "all talk but no walk" -- and even the talk being always hesitant and amounting to "we'll see", "if there is some magic way" (and the perennial "we're always open to it"), then yes, that has been a constant for a decade.
Now, I've just written ~3-5K lines of Go, and my company has just a few K or 10K lines of Go in production (mostly a C shop). But while I like a lot (static compilation for easy deployment, concurrency, standard library, go fmt, etc), one of the things that stops me using it more is the Generics situation.
My specific problem when i realized it was an actual problem was when i tried to connect to a sql databse and working with a proper ORM.
I would need one for each and every struct(table).
These days there is a tool that can generate all those struct methods: https://github.com/vattle/sqlboiler
So from the ORM perspective we (as the community) have worked around it.
Now Repository<T> and for us around 200,000 lines of code are gone and only one narrow set of tests to execute.
class Collection<T implements Entity> {
void insert(T entity) {
String vals = entity.props.map(escapeSql).join(",");
String qs = entity.props.map(x => "?").join(",");
PreparedStatement p = db.prepare("insert into %s (%s) values (%s);", this.tableName, qs, vals);
db.submit(p);
}
}This code (okay I made the use line up because it's not on the website and I'm lazy, you do need one though):
use some::stuff::for::the::dsl;
let versions = Version::belonging_to(krate)
.select(id)
.order(num.desc())
.limit(5);
let downloads = version_downloads
.filter(date.gt(now - 90.days()))
.filter(version_id.eq(any(versions)))
.order(date)
.load::<Download>(&conn)?;
is completely, statically typed, with zero runtime overhead. Generics + type inference makes sure that everything is valid, and if you screw it up, you get a compiler error (which honestly are not very nice at the moment).Thanks to generics, all of this checking is computed at compile time. The end resulting code is the equivalent of
let results = handle.query("SELECT version_downloads.*
WHERE date > (NOW() - '90 days')
AND version_id = ANY(
SELECT id FROM versions
WHERE crate_id = 1
ORDER BY num DESC
LIMIT 5
)
ORDER BY date");
but you get a ton of checking at compile time. It can even, on the far end of things, connect to your database and ensure things like "the version_downloads table exists, the versions table exists, it has a crate_id column, it has a num column", etc.You can absolutely create an ORM _without_ generics, but it cannot give you the same level of guarantees at compile time, with no overhead.
It also heavily relies on generic types to ensure that everything is well-formed, and to provide zero overhead.
There's two variants on a single way: basically, a "schema.rs" file contains your schema. You can write it out, and (with the help of migrations) it will update it when you create a new migration, or, you can have it literally connect to the DB at compile time and introspect the schema.
> Also, type checking is negligible from a performance perspective, so I the "zero overhead" is of minimal interest.
Ah, but it's not "the type checking is fast", it's the "diesel can generate hyper-optimized outputs that don't do runtime checks". Like, as an interesting example, Diesel can (well, will be, this has been designed but not implemented yet) actually be _faster_ than a literal SQL statement. Why? The SQL literal is in a string. That means, at runtime, your DB library has to parse the string, check that it's correct, convert it to the databases' wire format, and then ship it off. Thanks to strong generics, since the statements are checked at compile time, none of those steps need to be done at runtime; the compiler can pre-compile that DSL into postgres' wire format directly.
The reason this hasn't actually been implemented yet is that it's admittedly a tiny part of the overall time, and the Diesel maintainers have more pressing work to do. I use it as an example because it's an easier one to understand than some of the other shenanigans that Diesel does internally. It couldn't remove this kind of run-time overhead without generics.
Few typesystems come close to that (Coq is one of the few languages where the system might be advanced enough), but generally you want to completely remove entire classes of errors.
How (well) does diesel deal with migrations? I've seen other ORMs fall down the stairs trying to deal with schema modification over time.
How (well) does diesel deal with migrations? I've seen other ORMs fall down the stairs trying to deal with schema modification over time.
"diesel migraiton run" will run all pending migrations, there's revert/redo as well. "diesel migration generate" can generate these files and directories for you; it doesn't yet (that I know of) have helpers to write the SQL though (like Rails does for its migrations).
On deploy, it runs the migrations first https://github.com/rust-lang-nursery/thanks/blob/master/buil...
I believe there are some interesting improvements coming down the line in the future, but for now, it works pretty well.
1. Look at uses of cast operators in a codebase. How many of those would be eliminated by making the surrounding operation generic?
2. Go through the language spec and see how many features could be moved out of the language and into the core library if generics were supported.
In C++, on the other hand, separate compilation of template for each instantiation is effectively part of language semantics, because it has numerous observable effects.
The Go team needs to stop trying to control what people do with their language. If people come up with crazy implementations, it's their own problem. It's up to the Go community to provide alternatives to prevent those crazy implementations from becoming standardized in Go programming culture. Refusing to implement generics because programmers might abuse them is paternalistic and counterproductive.
Some people are turned away from C++ after seeing random bits of crazy language/template misuse, although when used sanely C++ could've served them nicely for their job.
Same thing is going to happen to Swift very soon.
https://engineering.kitchenstories.io/comfortable-api-reques...
Those function signatures are definitely not... self-documenting, I'd say.
/jk 2
a) generic programming is a new thing
b) any of those people you mention aren't using go for some things and other things where this feature is desired
c) that the feature will 'at best' be at level X
d) that level X won't be enough to satisfy some use cases
and a whole host of other things, not least of which the fact that go itself was invented to suit some purpose after other languages existed, and has been successful for many users
Any new language is already playing catch-up to reach the state of the art. Go is 10 years old and in those 10 years has made astonishingly little progress at implementing basic features such as error handling, because the designers cannot escape their bubble.
The most sensible thing to do is to kill Go, not to improve it. The root problem is not the lack of this or that feature, but the bizarre beliefs of the people running the project.
Care to expand this? It's pretty clear that you don't find Go suitable for your own use cases, but I've found it to be an extremely productive language, despite the fact that it may have a couple of warts.
Look, even allowing type aliases to become part of an interface declaration would be a HUGE win. You can't currently write portable arbitrary data structures without reimplementing them with a code generator. Ugh!
40, nearing on 45:
> Milner, R., Morris, L., Newey, M. "A Logic for Computable Functions with reflexive and polymorphic types", Proc. Conference on Proving and Improving Programs, Arc-et-Senans (1975)
https://news.ycombinator.com/item?id=8756683
They just weren't ready to make any of the significant tradeoffs other languages have to do in order to support them.
Whether that's a good outcome, I'm not fully sure - but there's definitely a lot of research in that area.
Yeah as pointed out in that very thread by pjmlp:
As usual, it lacks discussion about generics in:
Eiffel
Ada
Modula-3
D
MLton
.NET
Rust
Haskell
OCaml
Sather
BETA
CLU
Oh wait, they're pointing out that none of these is even remotely being discussed in the article hinting that they did not, in fact, and much prefer taking down strawmen they built from some sort of daguerreotypes of Java and C++.> They just weren't ready to make any of the significant tradeoffs other languages have to do in order to support them.
Ah yes, the ever so convenient but never actually clarified "tradeoffs", which only ever happen for generics but oddly enough apparently don't happen for any other feature.
> there's definitely a lot of research in that area.
That there is, would be nice if Go's designers ever bothered actually using it.
The idea that the monomorphization/boxing tradeoff can be dodged by not having generics in the language is a fallacy. In Go, today, you as a programmer already have to decide which side of the tradeoff you want every time you write something that could be generic. It's just that the compiler doesn't help you at all.
And even that is not the whole story, just because you reify generics does not mean you have to monomorphise everything, Microsoft's C# compiler only monomorphises value types.
Also Go may box in other contexts than interfaces depending on escape analysis (according to https://golang.org/doc/faq#stack_or_heap).
Does that mean writing a separate version of an algorithm for each type or types that you want it to handle? Like a sort for ints, a sort for floats, etc., even if the logic is otherwise the same (or almost)?
Not a PL design or theory expert, just interested.
There is also the approach taken by Swift, where values of generic type are unboxed in memory, and reified type metadata is passed out of band. There's no mandatory monomorphization.
Also the Swift optimizer can clone and specialize generic functions within a module, eliminating the overhead of indirection through reified metadata.
The compiler specializing/monomorphizing as an optimisation shows that Swift has a hybrid approach that tries to balance the trade-offs of both (like many other languages, like Haskell, and, with explicit programmer direction, Rust), but the two schemes still fall into the broad categories of monomorphizing vs dictionary passing, not something fundamentally different.
Hybrid approaches in which the compiler (or JIT!) decides whether to monomorphize or use vtables are also possible.
I don't know exactly what the problem is for Go. There are tradeoffs, e.g., just with the type system: impredicative, stratified or predicative quantification, implicit subtyping or explicit type instantiation, value restrictions, variance handling for mutable inductive types, Hindley-Milner or bidirectional typechecking, etc. There are more tradeoffs with the implementation. Fortunately, these are all well understood by now.
However, it is also true that many mainstream languages famously got generics wrong. What's most infurating about this situation is that a lot of research just gets ignored. If the question is really "should Go have generics whose design is based on C++ templates and/or Java generics" then the only sane answer is a resounding no.
Yeah, I find it strange how languages are trending at a glacially slow pace to having the same features that strongly typed functional programming languages have had for literally decades. It's like we're going to be using actual functional programming languages eventually but the only way it'll happen is to very slowly change what everyone is currently using in small steps.
Static types, immutability, type inference and non-null variables are popular design choices right now but they've been in functional programming languages for nearly 50 years. I'm still waiting for inductive types and pattern matching to turn up in a mainstream language and seeing them talked about like they're new concepts.
Source: got to interact with the community between the C# 1.0 and 4.0 releases (2.0 added generics, 3.0 added lambdas, neither feature was considered of any use to a Productive Developer up to the day when Microsoft officially announced them).
That isn't true inside Microsoft. Many of the people who work on C# are the same academic wanks that work on Scala or F#. C# has a different user base from those languages though, so they still have to be careful what they add to the language, and many language features are planned 3 or 4 versions in advance.
No, that was not intended as included in "the C# community". Hell, SPJ used to work at Microsoft (he may still do, but he used to).
> the people who work on C# are the same academic wanks that work on Scala or F#
I'm sure you mean wonks, but I liked the typo.
> C# has a different user base from those languages though, so they still have to be careful what they add to the language, and many language features are planned 3 or 4 versions in advance.
I have no issue with the evolution of C# rate or otherwise, only with a number of its users.
C# has a specific audience, and the language designers cater to them pretty well. I really really like C# as a language, and I don't mind delayed access to certain features that I already like from other languages.
You probably have a beef with some C# users not because of their choice of language, but with the field they work in (primarily enterprise) tends to breed a certain kind of attitude that other techies don't like very much.
(says a man planning to purchase his 5th Macbook Pro later this year)
There are good reasons to use C#, so when it gets a new feature that other languages have had for years, well, it is newsworthy.
Now when will javascript get pattern matching?
C# and Java are slowly catching up :)
Human behaviour is strange when you expect rationality. Sour grapes is such a pervasive cognitive bias that one has to wonder why it exists since it's obviously irrational. I think it's likely that it presents a major advantage in the psychology of group cohesion.
The numeric tower of lisps is the canonical use case of what generic functions are good for.
Smalltalk, the second greatest language after Lisp, is also good example.
It seems like you’d have valuable feedback given that it’s a “showstopper” for you.
What use cases do they need other than the extremely large body of public knowledge on the matter, and why would one more example change anyone's mind?
To me this represents the epitome of the insular and outwardly hostile attitude that the Go team has toward good ideas elsewhere in the CS world. Would it really make a difference to the team if I or anyone else were to hunt down specific examples and present them with problems solved with generics and stronger type systems?
I doubt it.
... but, hey, just to indulge you, a few off the top of my head:
- Generic containers in libraries (aka not built-in)
- Parametricity to restrict user-supplied implementations of interfaces; not quite as valuable in a language with casts, etc., but still reasonably valuable.
- To give a sound account for the currently-magic built-in types.
That should be enough, frankly, but I'm sure I could come up with more if I were to spend 5 more minutes on it...
Can we stop pretending that they don't know of any compelling use cases now?
"Find us examples and show us so we can ponder this further" is incredibly condescending after we did that 5 years ago and they decided to stall (or to use the author's term, "wait") on the issue. Honestly I think it might be too late for generics to be added, because the large body of existing interfaces aren't generic and likely would have a very high transition cost.
It's actually a classic bulshitting tactic by people who have no actual argument. Effectively, it's "well, we'll defer a choice until we can PROVE that decision X is PERFECT in EVERY way."
That's not to say: IF there's a real, quantifiable doubt, then by all means, continue to discuss (but preferably set time limits), but to do that you MUST set out precisely what the problems are, what potential solutions could (and couldn't) be, etc. etc. Not just vague "this feels wrong" objections.
The Go team have been long criticized for choosing the option that fits Google, but not the rest of the world. This seems like their attempt to think about what others are doing with the language, beyond their insular experience, so they don't end up with something that fits Google perfectly but falls apart everywhere else.
If they don't take the time to learn how people intend to use generics in Go, the best solution for Google, and Google alone, is what we will get.
It's not unreasonable to ask "how do we implement this in the best way?", but I'll note a) that's not what was asked, and b) I have a hard time believing that code at Google is so incredibly "special" that they need a special kind of generics. Also, if Google is such a unique snowflake why not ask the Google people directly rather than the "Go community"?
They asked for use cases from a wide range of people to ensure they implement it in the best way. Subtly different, but essentially the same request.
> I have a hard time believing that code at Google is so incredibly "special" that they need a special kind of generics.
I didn't say they need special generics. I said the approach that works best at Google may not be the best approach for the population at large. If Google is best served by C++-style generics, while the community as a whole would be better served by Haskell-style generics, why just jump in and do it the C++ way before seeing how others might want to use it?
> Also, if Google is such a unique snowflake why not ask the Google people directly rather than the "Go community"?
Because they are trying to avoid the mistake of using one datapoint like Go has struggled with in the past? They know what works in Google, but that doesn't necessarily work for everyone else. See: Package dependencies, among many.
Phrasing is important, and obviously (from the reactions of me and others in the thread) the phrasing was way off and perceived as condescending and lazy.
> I didn't say they need special generics. I said the approach that works best at Google may not be the best approach for the population at large. If Google is best served by C++-style generics, while the community as a whole would be better served by Haskell-style generics, why just jump in and do it the C++ way before seeing how others might want to use it?
OK, so you said they don't need a special generics, but then say that they do. I must not be understanding what you're trying to say. (If this is about choosing trade-offs, then see the other poster who talked about trade-offs. Executive summary: Not doing generics is also a trade-off.)
Also: ASK GOOGLE.
> Because they are trying to avoid the mistake of using one datapoint like Go has struggled with in the past? They know what works in Google, but that doesn't necessarily work for everyone else. See: Package dependencies, among many.
You can't have it both ways. Either Google is important enough to it in a way that works for them, or the community is more important and they get to choose.
Anyway, I'm done with this conversation. I think we may be seeing this from viewpoints that are so different that it's pointless to continue.
I'm not sure how to argue constructively with someone who says "I'm not saying X, but..." and then immediately states a rephrasing of "X". I'm sure that's not what you think you are doing, but that's the way I'm seeing it, FWIW.
Maybe, but I don't know that we should be attacking someone's poor communication ability. I'm sure I've misunderstood at least one of your points too. Let's just focus on what was actually asked for: Use-case examples.
> OK, so you said they don't need a special generics, but then say that they do.
There isn't an all encompassing 'generics'. Generics is a broad category of different ways to achieve reusable statements across varying types, in a type-safe manner. To try and draw an analogy, it is kind of like functional and imperative programming. Both achieve the function of providing a way to write programs, but the paradigms differ greatly. Each with their own pros and cons.
If imperative programming is the best choice for Google, that doesn't mean the community wouldn't be better served by functional programming, so to speak. And when it comes to generics, there are quite a few different paradigms that can be used, and not easily mixed-and-matched. They are asking for use-cases to determine which generics paradigm fits not only the needs at Google, but the needs everywhere.
> Also: ASK GOOGLE.
The Go team is Google. They have asked Google. Now they are asking everyone else. I'm not sure how to make this any more clear.
> Either Google is important enough to it in a way that works for them, or the community is more important and they get to choose.
In the past Google was seen as the most important, and they have been widely criticized for it. This is them moving in a direction that favours the community. And they are still being criticized for it... Funny how that works.
Yes, thank you. Everybody in this thread already knows that. PICK ONE.
(EDIT: I should also add: Since Go is structually typed and has mutable variables, that should be a good indication of what to do and what not to do. See e.g. Java arrays, variance and covarince.)
> If imperative programming is the best choice for Google, that doesn't mean the community wouldn't be better served by functional programming, so to speak. And when it comes to generics, there are quite a few different paradigms that can be used, and not easily mixed-and-matched. They are asking for use-cases to determine which generics paradigm fits not only the needs at Google, but the needs everywhere.
And now you're trying to bring imperative vs. functional into this?
I think IHBT... and this really is my last comment in this thread. Have a good $WHATEVER.
They are picking one, based on the use-cases that will be given to them in the near future. I don't understand what your point is here.
> Since Go is structually typed and has mutable variables, that should be a good indication of what to do and what not to do.
That may be true, but the worst case scenario is that they learn nothing from the examples they get. You don't have to provide any if you feel it is a useless endeavour. If it gives them comfort, so be it.
> And now you're trying to bring imperative vs. functional into this?
It wasn't clear if you understood what I meant by there being different ways to provide generics. I struggled to write it as eloquently as I had hoped and the analogy was meant to bridge what gaps may have existed.
It's almost like communication can be difficult. Where have I see that before? Hmm...
> It's almost like communication can be difficult.
We can agree about that! :)
> Where have I see that before? Hmm...
Not sure what you mean (ironically?).
Have a good night :).
It's not rocket science.
And because it's 2017.
i almost feel this is like asking bjarne what use cases there are for 'classes' in c-with-classes. it's a structural language change that (depending on implementation details) can have sweeping ramifications on how one writes code.
type parameterization allows for the expression of algorithms, idioms and patterns wholly or partially separate from concrete type details. i'm sure others can better sell the topic though.
- performance wise: no dynamic dispatch, means no virtual function call, means faster code.
- type safety: let say I want a data structure that can store anything but everything in it must be the same type. You just can't do that with Go's current type system without introspection (which is cumbersome and really slow).
Generics already exists in Go : maps, slices and channels and generic, and it was mandatory for performance reason. The problem of not having generics is that the community can't build its own implementation of useful data structures : for instance, until the last release Go lacked a thread-safe map. That's fine, let's use a library for that … Nope, can't implement that because, “no generics”.
Generics are rarely useful in your own code, but they allow people to create good abstractions in their libraries. Without generics, the ecosystem growth is limited.
No, you can't, because generics don't provide the key feature of interfaces: run-time dynamism.
You could make a case that static polymorphism and dynamic polymorphism could be accomplished by a single mechanism, as traits do in Rust, but you can't say generics are "better" than interfaces, since they solve a different set of problems, with different implementation strategies, despite related theoretical foundations.
> The problem of not having generics is that the community can't build its own implementation of useful data structures
This is not true either, although it is true that it is harder to do this sort of thing in Go, the result may in fact not be as fast as you can achieve with monomorphisation, and you may have to write more code by hand.
The "trick" is the Go model is to provide an interface (which yes, potentially unnecessarily uses a dynamic mechanism) to implement indirect operations. For example the Swap function of https://golang.org/pkg/sort/#Interface enables https://golang.org/pkg/container/heap/#Interface - which is a reusable data structure. Compiler-level static-generics for slices and arrays, combined with indirecting through array indexes, enables you to avoid boxing when instantiating the heap interface. The data structure can be instantiated with an interface once, so there is no dynamic lookup for heap operations, but there is still a double indirect call.
Yes, this is less convenient than proper generics, but this doesn't mean you can't do these things.
Furthermore, I've never actually needed to do this outside of sorting (which is, absolutely, is annoying) in large projects. Almost every time I need a specialized data structure, I usually need it for performance reasons, and so hand-specializing for my needs tends to be worthwhile anyway.
> Generics are rarely useful in your own code, but they allow people to create good abstractions in their libraries.
I'd like to see something like templates or generics for Go, but I definitely don't want Java-style "fake" generics without code specialization. Furthermore, I found that the vast majority of generics-heavy libraries back in my C# days were more trouble than they are worth, despite meaningful opportunities for better performance with value types. Copy/paste almost always resulted in a better outcome.
Yep, assembly is less convenient than Go, but it doesn't mean you can't write programs using assembly.
Why do we need anything beyond assembler?
Then you'll wish you had generics. :)
It's a small superset of Go that allows you to call map/filter/reduce methods on any slice. It's not true generics (you can't define your own generic functions or types) but it covers the most common use case for me: declarative data processing.
If Go wants to be more sophisticated, there are other sophisticated languages I can use out there like Haskell, Rust, F#, Scala, etc. The problem I have learned in my career is "sophistication" usually gets in the way of getting things done because now you have all these clever developers full of hubris creating these novel implementations that nobody else in the world understands, and your project fails but hey -- at least you have a dope, terse, generic implementation -- totally rad.
Depending how they're implemented, you might not have to use them (even via libraries). Not using the language altogether is a bit dramatic.
This is my beef with JavaScriot these days... They introduced a slew of new, in my opinion insane, control structures, and while I can avoid them in my code, they are still slowly polluting my dependencies, and adding obscure runtime requirements for my projects.
I remember when Java started to have generics in 1.5. It was very hard to go back to 1.4 after having used that. I feel that I just can't program without them any longer.
Nah, this is pretty much the same answer they've been giving all along, lots of handwaving about how generics are so cutting edge that no one can figure out how to use them effectively yet ("we just can't wrap our heads around this crazy idea!")
In other words, don't count on them in Go 2.
It doesn't seem to me that there's a shortage of people pointing out real-world use cases at all, and I'm looking from the outside in.
basically it goes like: > If google isn't screaming about it, it's not important. > If google has a workaround, then it's fixed.
Choosing is hard, and while they could improve I think the Go maintners have done a pretty good job, and are willing to admit mistakes.
Admitting a mistake on "generics" would probably go a long way towards credibility for the team.
No matter how many excellent decisions they've made that have delicately balanced opinions and tradeoffs, this one hasn't gone over well, and it's very widely known.
In Rust, this was handled the right way. The Result type forces you to at least actively dispose of the error.
In Go, it's just way too easy to leave an error unhandled.
Sort of: by default, an unused value of the `Result` type produces a compiler warning. Like all warnings in Rust, this category of warnings can be promoted to a hard error for all users with a single line of code. And unlike other languages where compiler warnings are uniformly ignored, there is a strong culture in Rust of making sure that code compiles warning-free (largely due to the fact that the compiler is rather selective about what to issue warnings about, to help avoid useless false positives and warning spew (there is a separate project, called "clippy", for issuing more extensive warnings)).
In both cases I have to either write a handler immediately after the call, or I'm going to fail and toss it back up to whoever called me.
Errors-should-be-values has always seemed like a bizarre statement to me. Like, fine, but they're still errors - their core feature will be that I stop what I want to do and handle them. And in that regard I much prefer exceptions (at least Python style exceptions) because most of the time I just want to throw the relevant context information up to some level of code which knows enough to make a decision on handling them. The thing I really want is an easy way to know all the ways something can fail, and what information I'm likely to get from that, so I can figure out how I want to handle them.
The main difference as I see it is that in Rust, you can't get the success value if there's an error, so you're forced by the compiler to handle any error if there is one. With tuples, you're free to just look at one element regardless of the value of the other.
It seems to me to be the same "Generics is some foreign concept we just heard of, and we're not sure how to implement them in Go yet, but we'll consider it if we chance upon some magical optimal way" that has been the official party line of Go since forever...
Even the "we are asking you for proposals" part has been there since a decade or so...
This is such a troll'ish statement, but I'll respond anyways because maybe you actually are just that uninformed. Without generics any number of libraries that utilize generic function blocks - Func [1] in C#, Callable [2] in Java, etc., and do things like return the function's type parameter as a method result, would not be possible. This is exceedingly common, at least in libraries. If you want to know how common, let me refer you to my friend http://google.com.
Just because something isn't valuable to you, personally, doesn't mean it's not valuable. As in all aspects of life, not everyone is you.
1. https://msdn.microsoft.com/en-us/library/bb549151(v=vs.110)....
2. https://docs.oracle.com/javase/7/docs/api/java/util/concurre...
Rust has generics and traits/typeclasses, and the result is my Rust code uses those features extensively and the presence of those features greatly influences the designs. Similarly, when I write Java, I design with inheritance in mind. I would have trouble showing real world use cases for inheritance in my Rust code, because Rust doesn't have inheritance and so the designs don't use inheritance-based patterns.
Essentially, how does one provide real world evidence for the utility of something that's only hypothetical? You can't write working programs in hypothetical Go 2-with-generics, so examples are always going to be hypothetical or drawn from other languages where those tools do exist.
On the latter part I'm fairly cynical these days, since I'm presently on a team where the lead embraced a Scala-DSL heavy, functional design and the net result has been a total loss of project velocity because when we need to prototype a new requirement, the push back has been paraphrasing "oh, could you not do that in <core product> and handle it somewhere else?" - so we end up with a bunch of shell scripts cobbled together to do so.
Of course nothing can't be done without them. With or without generics or inheritance the language is still Turing complete.
The question is, do you want to write type safe code? which is memory safe? which has bound-checked array? or not? Assembly makes all that stuff unnecessary as well. This is not a good argument, especially when Go std lib is getting all these type unsafe API using interface {} everywhere. That is precisely what generics are for, to allow writing parametric functions (sort) or type safe containers instead of sync.Map like in the std lib.
If you care about type safety and code re-use then generic programming is a necessity. What do you think append, copy or delete are? these are generic functions. All people are asking is the ability to define their own in a type safe fashion.
Are these use cases Russ Cox don't know they exist?
The argument about assembly is a red herring. Let's compare:
1) In addition to writing your business functions, learn these additional control structures to obtain safety
2) When writing your business functions, also use these well reviewed functions that enforce type and memory safety when moving code across interfaces.
3) Port all of your code to assembly, write you own memory safety control structures from scratch.
You see the difference? The choice in your mind between "add control structures to the language" and "do everything painstakingly by hand" but we are advocating a third option, which is: use well reviewed libraries written with only the basic control structures.
The reason I am advocating that is the more control structures you have the harder it is to analyze code. You end up slowly moving your codebase to a point where only people with deep deep knowledge of an advanced programming language can read it.
I think culturally, in 2017, programmers underestimamte how much can be done with just functions and literals and well written helpers.
The reason for that is we are rewarded (psychologically and professionally) for learning new control structures, but not as often for writing better code using the beginner structures.
They sure are useful to readability and maintainability.
[0] which can obviously be used to replace looping constructs anyway
Users who have heavy need for genetics have already moved away from Go.
* type-safe containers (linked lists, trees, etc.)
* higher order functions (map, reduce, filter, etc.)
* database adapters (i.e. something like `sql.NullColumn<T>` instead of half a dozen variations on `sql.NullWhatever`)
* 99% of functions that accept or return `interface{}`
I appreciate that they seem open to the idea for v2, but "the use case isn't clear" is absurdly out of touch with the community. Using `interface{}` to temporarily escape the type system and copy/pasting functions just to adjust the signature a bit were among the first go idioms I learned, and they both exist solely to work around the lack of generics.> Using `interface{}` to temporarily escape the type system
tbh if you program Go correctly, you use interface{} pretty rarely.
It's a bit like saying "if you program C correctly, you use (void *) pretty rarely.". that's not the case, Go maintainers themselves keep on adding API with interface {} everywhere. Are you saying they are not programming Go correctly?
For loops do not get the job done easily enough. I've lost count of the number of times I've had to do contortions in order to count backwards inclusive down to zero with an unsigned int. With a proper iterator API, it's trivial.
Furthermore, for loops are a pain to optimize. They encourage use of indices everywhere, which results in heroic efforts needed to eliminate bounds checks, effort that is largely unnecessary with higher level iterators. Detecting the loop trip count is a pain, because the loop test is reevaluated over and over, and the syntax encourages complicated loop tests (for example, fetching the length of a vector over and over instead of caching it). For loop trip count detection is one of the major reasons why signed overflow is undefined in C, and it's a completely self-inflicted wound.
I'm generally of the opinion nowadays that adding a C-style for loop to a language is a design mistake.
There was a nice paper at this years POPL which (in my opinion) allows you to substantiate this claim.
The paper is "Stream Fusion to Completeness", by Oleg Kiselyov, Aggelos Biboudis, Nick Palladinos, and Yannis Smaragdakis. The actual question in the paper is how to compile away stream operations to imperative programs using for/while loops and temporary variables. On the other hand, if we look at the output of the compiler we can see how complex it is to express natural stream programs using only for/while loops.
For instance, even very simple combinations of map/fold/filter create while loops and make it difficult to detect the loop trip count afterwards (in fact, you need an analysis just to detect that the loop terminates). If you combine several such streams with zip, the resulting code makes a nice entry for obfuscated code contests. Finally, if you use flatMap the resulting control flow doesn't fit into a for/while loop at all...
So for/while loops are not simpler than stream processing primitives and unless you explicitly introduce temporaries (i.e., inline the definition of map/fold/etc.) you quickly end up with very complicated control flow.
If you want to show something else is easier to use, you'd have to do a user study, and even that's not going to be universally applicable since it depends on the previous experiences of the user population being tested. It's why these things tends to be debated as a matter of taste.
They all show that the natural expression is through declarative programming and that for loop are never what comes naturally.
Regardless, these are simply cases of people writing bad code, and nobody is claiming map/reduce/filter is a panacea for bad code.
Functional composition/chaining works best with small, well-named single purpose functions that compose/chain together into more complex functionality (with appropriate names at every non-trivial level of chaining/composition). You can't easily compose/chain imperative loops this way (at least not without wrapping them in functions that take data, and returns transformed data, by which point you might as well use map/reduce/filter to transform the data to begin with to get rid of the impedance mismatch).
There is a germ of truth here - sometimes list comprehensions make code harder to understand than if you just wrote an if statement or a loop - but you've overgeneralized. I don't think anyone on the core Go team seriously thinks that map and reduce are bad ideas when Google relies so heavily on MapReduce (and its successors).
Python's comprehensions are among the most powerful, easy to use, concise capabilities of any language I've used. You can address the abuse issue with coding guidelines: no more than 2 deep. Done.
It's not incomprehensible, it's just phrased a different way from what you're used to.
A lot of programming boils down to mutating local or global state, by executing lines of code -- each of which mutating the contents of some variable somewhere -- in order, one at a time. But this is just one style of programming, and is not the only one. When using map, filter, reduce etc. we, instead, construct a path through which data flows, where each line modifies some aspect of the data, as it flows through the path. This is essentially what Functional Reactive Programming is, although you can get the same effect using a pure language and function composition.
What would be incomprehensible?
As opposed to code that just uses chained map, reduce etc as opposed to nested or sequential for loops.
One is a difference is skill, clarity etc, the other is a difference in style (imperative vs fp).
How easily are they abused?
Because it's the opinion of the programming community, including the brightest programmers out there, that FP, and map, reduce and filter are totally fine and dandy.
My more cynical reaction is that someone who doesn't understand the benefit of generics or can't even come up with examples where generics are superior to their alternative should not be allowed anywhere near a language design discussion.
But even so, even with all the shortcomings, once Java 5 was released people migrated to usage of generics, even if generics in Java are totally optional by design.
My guess to why that happens is that the extra type safety and expressivity is definitely worth it in a language and without generics that type system ends up staying in your way. I personally can tolerate many things, but not a language without generics.
You might as well use a dynamic language. Not Python of course, but something like Erlang would definitely fit the bill for Google's notion of "systems programming".
The Go designers are right to not want to introduce generics though, because if you don't plan for generics from the get go, you inevitably end up with a broken implementation due to backwards compatibility concerns, just like Java before it.
But just like Java before it, Go will have half-assed generics. It's inevitable.
Personally I'm sad because Google had an opportunity to introduce a better language, given their marketing muscle. New mainstream languages are in fact a rare event. They had an opportunity here to really improve the status quo. And we got Go, yay!
I've mostly moved on to Clojure for programming backend services. I still use Go for the occasional small utility program due to its speed and ease of deployment, though.
I've never once even considered the fact that I might need generics because I've never run into an unsolvable problem or an extremely inelegant engineering solution. I see a lot of people claiming that the lack of generics is a big problem but no one is actually showing a concrete case where generics are necessary.
C doesn't have generics but we never have this discussion when we talk about C.
That is a trivial example where generics help. And yes, the endless boxing and runtime type assertion does make your code slower eventually (see the comment from the libcuckoo authors).
This is one of the reasons why the container and sort stdlb libraries are such a joke. They're not generic so they have to special case every different type. With generics, the entire sort package would just be a function that takes []Ordinal and sorts it. Container would similarly only require one implementation of each data structure (because they would be generic).
I find it hard to believe that you've programmed "scores of Go" and you've never hit any of these issues. I also have programmed plenty of Go, and these problems are just frustrating.
But again, I still severely doubt you've never run into a problem with Go's lack of generics. I ran into it after about 3 months of working with Go. Maybe you didn't find the work-arounds problematic, but that doesn't mean that the original limitation (no generics) doesn't cause problems. You just choose to accept the work-arounds as the "right way" of doing things.
If you haven't already, I would recommend looking at Rust or another modern language that has generics to see what sort of benefits you can get out of it that you can't directly get with Go. Personally the fact that if T implements an interface I won't let you use []T as []I is pretty silly IMO. Rust lets you do that with the From trait, and you can even do it even more generically with the From trait using a generic T.
Sure it does, a light weight version was introduced in C11.
http://en.cppreference.com/w/c/language/generic
Besides, C gets an excuse because the language was designed before generics were even invented, and with pre-processor macros is it possible to implement a poor man's generic system.
So, at runtime, Go 1 code needs to be able to work with Go 2 types. That will impose some restrictions, I imagine.
Interesting. Are there any other languages where that is allowed? I'm assuming that by "codebase" you mean all the code that gets compiled into one program or library (for compiled languages), or is part of one program or library (for interpreted languages). And I don't mean the case where only a common subset of Lang X v1 features and Lang X v2 features are used in the same codebase (because in that case, there is no issue). That latter case is possible, for example, in Python, and probably some many languages too. In fact by the definition of the case, it should be possible in all languages.
Correct.
> Are there any other languages where that is allowed?
One would be Racket, a Lisp which allows you to pick a syntax flavour using the #lang directive: https://docs.racket-lang.org/guide/Module_Syntax.html#%28par...
>>Interesting. Are there any other languages where that is allowed?<<
C / C++
Perl 5/6
The various language feature pragmas in Haskell.
Use site variance can be really powerful when you're dealing with invariant data structures like Lists. You can then treat them like covariant or contravariant depending on a particular method call at hand.
But, I don't know of one, and maybe that's because the Go team is right, some tradeoffs need to be made, and they did, and so Go is what it is. You can't add all the other great features you want and eat the Go cake too.
Disclaimer: I'm no language design expert. Just thinking this from the fact that I've yet to hear of such a language.
KDE 4 was released in 2008 and it was supposed to be ported to Windows. We're in 2017 and KDE 5 is still far from being considered stable on Windows. And I'm sure you can also think of many examples of programs promised and never delivered or underdelivered.
It was ported to Windows. I cannot check the remarks about stability because I use Linux, but it sounds plausible. There's only a handful (maybe 2 or 3) developers working on Windows support in their spare-time AFAIK. I would guess that KDE had anticipated more Windows developers joining the project as it progressed towards maturity.
That's always the problem with open-source projects: It's very hard to do planning and forecasting with a bunch of volunteers. Even if they commit to a roadmap, there will always be someone who has to step down because of private issues (new job, new child, etc.). Go is in a much better position since Google has headcount assigned to it (again, AFAIK).
(Technically, there's also Ocamlnet's multicore library, but that's too low-level for most people.)
> a functional language
Has closures.
Has pattern matching.
Has algebraic data types (however variants can't have generic parameters not present on the data type itself, but you can use trait objects to do that).
Functions that don't take &mut parameters, & parameters with types with interior mutability and don't call functions that do I/O are pure (although they may not terminate and may abort).
> optional lazy constructs
"Automatic" lazy costructs are usually unnecessary and thus almost never used, but can be implemented as a library using macros.
Can also just use closures and call them explicitly.
> great polymorphism
Has trait/typeclass polymorphism with either monomorphization or dynamic dispatch.
Structures can't be inherited, but composition gives equivalent results and is the proper way to do it.
There have been several proposals for inheriting structures though, so maybe it will be available at some point, despite not being advisable.
> statically typed with inference
Yep.
> generics
Yup.
Not higher kinded, no constant integer parameters and not variadic yet, but those features should be available in the future.
> great concurrency story
Rust is the only popular language supporting safe concurrency without giving up the ability to share memory between threads.
> an efficient GC
It turns out that a GC is unnecessary, and that reference counting is enough, which is what Rust provides (although it's still relatively rarely used).
If you insist on a tracing GC, there are/have been several attempts to add it, and maybe there will be a mature one in the future.
In particular, there is a "rust-gc" crate, and Servo has a working integration with the SpiderMonkey GC, which I suppose should be possible for others to use with some work.
> compiles quickly
Work in progress (incremental compilation, more parallelization).
> self contained binaries
Yes (or optionally using shared libraries).
> simple and effective tooling which takes only seconds to setup
rustup can install the compiler and package manager in one line.
If you want an IDE, you'll have to install that too separately. Work in progress on improving IDE functionality and ease of use.
> giving you perfomance that equals java and can rival C
Performance is at least theoretically the same as C since the generated assembly code is conceptually the same (other than array bounds checking, which can be opted out from with unsafe code).
> low memory footprint
Same as C.
Rust uses glibc by default, which must be dynamically linked. It's usually the only thing that's dynamic about Rust binaries, but it does mean that compiling on on old CentOS box is a decent idea if you want a wide range of compatibility.
Alternatively, you can use MUSL, which works like this:
$ rustup target add x86_64-unknown-linux-musl
$ cargo build --target=x86_64-unknown-linux-musl
Boom, now libc is statically linked too.The default is that all Rust code is statically linked, but since you may link C code as well, that may or may not be statically linked. It's done by packages, so there's no real default. Many of them default to static and provide the option of dynamic.
That's why there's not really a guide, that's really all there is to it.
What I'm not happy with is language designers seemingly ignoring the state of the art even when it comes to picking low-hanging fruit.
First, I'm not looking for a language. Programming languages are part of my field. I was making an observation rather than expressing a need. If there's a language that's not totally obscure or esoteric, I've probably written some serious code in it at some time.
Second, I know and use Scala (among other languages) and have done so since the 1.x versions, but the JVM dependency is too often a problem (and Scala Native is not yet mature).
> To minimize disruption, each change will require
> careful thought, planning, and tooling, which in
> turn limits the number of changes we can make.
> Maybe we can do two or three, certainly not more than five.
> ... I'm focusing today on possible major changes,
> such as additional support for error handling, or
> introducing immutable or read-only values, or adding
> some form of generics, or other important topics
> not yet suggested. We can do only a few of those
> major changes. We will have to choose carefully.
This makes very little sense to me. If you _finally_ have the opportunity to break backwards-compatibility, just do it. Especially if, as he mentions earlier, they want to build tools to ease the transition from 1 to 2. > Once all the backwards-compatible work is done,
> say in Go 1.20, then we can make the backwards-
> incompatible changes in Go 2.0. If there turn out
> to be no backwards-incompatible changes, maybe we
> just declare that Go 1.20 is Go 2.0. Either way,
> at that point we will transition from working on
> the Go 1.X release sequence to working on the
> Go 2.X sequence, perhaps with an extended support
> window for the final Go 1.X release.
If there aren't any backwards-incompatible changes, why call it Go 2? Why confuse anyone?---
Additionally, I'm of the opinion that more projects should adopt faster release cycles. The Linux kernel has a new release roughly every ~7-8 weeks. GitLab releases monthly. This allows a tight, quick iterate-and-feedback loop.
Set a timetable, and cut a release with whatever is ready at the time. If there are concerns of stability, you could do separate LTS releases. Two releases per year is far too short, I feel. Besides, isn't the whole idea of Go to go fast?
True, but C# also has nominal types, overloading, and explicit interface implementation. Adding generics without breaking existing code without those features looks very difficult to me.
Think SML/OCaml/Ada/Modula-3 (modules parameterized by modules) or Haskell (typeclasses).
>If you finally have the opportunity to break backwards-compatibility, just do it.
I think Russ explained pretty clearly why this is a bad idea. Remember Python 3? Angular 2? We don't want that to happen with Go 2.0.
>Additionally, I'm of the opinion that more projects should adopt faster release cycles.
I am of the opposite opinion. In fact, I consider quick releases to be harmful. Releases should be planned and executed very carefully. There are production codebases with millions of lines of Go code. Updating them every month means that no progress will be made at all. The current pace is very pleasant, as most of the codebases I work with can benefit from a leap to newer version every six months.
The problem with Python 2/3 is that Python 3 didn't add enough new features to make people want to move to 3.
The problem with Angular 2 is that it just didn't know what it wanted to be.
If Go2 doesn't break enough yet still break it will be no different from Python 2/3 fiasco.
Go has serious design issues, the go team ignoring them hindered Go adoption pace at first place.
As an aside, since you're making the distinction, can you have meaningful benefit without breakage? Eg, you're specifically separating the two - so can you have significant improvements without breakage?
It would seem that pretty much any language change, from keyword changes to massive new features, breaks compatibility.
Sure, in two ways:
(1) Performance improvements with no semantic changes.
(2) New opt-in features that don't break existing code (such as where code using the new feature would have just been syntactically invalid in the old version, so the new feature won't conflict with any existing code.)
There be no reason for SemVer minor versions if you couldn't make meaningful improvements while maintaining full backward-compatibility.
I think I'm simply wrong here. I was envisioning "breaking" as being incompatible with Go1. If Go2 was a superset of Go1, it would be allow Go1 code to run flawlessly in Go2 and still allow any new keywords/features.
My assumptions were incorrect, and writing my reply to you sussed it in my head. Thank you for your reply, sorry for wasting your time :)
One way is by having a new language feature which does not interact with anything else in the old version of the language, i.e. is orthogonal.
Break early and then stabilize for a long time; just make sure everybody knows the plan.
Which emphasises even more the fact that large private codebases really dislike breaking changes.
I hate generics. also, I hate exceptions.
Too many people are wanting "magic" in their software. All some people want is to write the "Happy Path" through their code to get some Glory.
If it's your pet project to control your toilet with tweets then that's fine. But if it's for a program that will run 24/7 without human intervention then the code had better be plain, filled with the Unhappy Paths and boring.
Better one hour writing "if err" than two hours looking at logs at ohshit.30am.
there's nothing magic about exceptions, too, it's that it's harder than necessary to use them correctly and that's why it's not as big of a deal to not have them in go - as evidenced by this thread.
The fact that Go has had this much success without generics is mind-boggling to me.
Go is quite similar to Limbo, by some of the same authors, how much success did it have outside AT&T?
No, it was adopted quickly because it's a simple, script-like language producing static binaries that didn't require any runtime, highly portable with built-in cross-compilation, requires very little resources, is able to perform pretty damn good and has an extensive standard library (although not perfect) supporting many modern technologies.
That are a lot of boxes it ticks off. I remember wanting to start hobby projects with friends where the end-result would preferably be a native binary, and the limit was that I was the only-one with a C/C++ background. Now anyone with some scripting background can pick up Go in a few days, without having to worry too much about a build system, portability, cpu architectures, performance, memory leaks, ...
Sure, Google was the initial push, but it only really took off once projects like Docker showed it's potential.
To paramet(e)rize the types (of a class or function or method), just like you parametrize the arguments of a function or method.
Update: searched for and found the article that mentions that:
https://dlang.org/blog/2016/08/30/ruminations-on-d-an-interv...
Excerpt (italics mine):
[ Walter: We nailed it with arrays (Jan Knepper’s idea), the basic template design, compile-time function execution (CTFE), and static if. I have no idea what the thought process is in any repeatable manner. If anything, it’s simply a dogged sense that there’s got to be a better way. It took me years to suddenly realize that a template function is nothing more than a function with two sets of parameters –compile time and run time–and then everything just fell into place.
I was more or less blinded by the complexity of templates such that I had simply missed what they fundamentally were. There was also a bit of the “gee, templates are hard” that predisposes one to believe they actually are hard, and then confirmation bias sets in.
I once attended a Scott Meyers presentation on type lists. He took an hour to explain it, with slide after slide of complexity. I had the thought that if it was an array of ints, his presentation would be 2 minutes long. I realized that an array of types should be equally straightforward. ]
The design can go badly wrong, of course, but it can also go wonderfully right. Generics are a very important feature to consider for the language.
I have no opinion on exceptions with regard to Go specifically. I think they serve a good purpose in other languages but are often misused.
I advice to read about Human Factors and Complex Systems engineering. You will be surprised.
I don't hate generics, but I don't require them either. If the Go guys can make them work well and fit into the language i'd be fine with it, but I don't want them to feel pressured into throwing something out there for the language purists. I doubt they feel pressured. They've been around the block and have as good a sense as any group i've ever seen for what to include in the language.
Go is not the language to use if you want to write clever code, but it's great when you write code for businesses that must run 24x7.
You mean like memory and resource management code? Oh wait.. you use a garbage collected language. lol
This is sampling bias at work. The people who need generics have long since given up on Go and no longer even bother participating in Go-related discussions, because they've believe it will never happen. Meanwhile, if you're still using Go, you must have use cases where the lack of generics is not a problem and the existing language features are good enough. Sampling Go users to try and find compelling use cases for adding generics is not going to yield any useful data almost by definition.
/raises hand
I like when tools are good, but I've basically written off Go as a tool for generating unsustainable code right now (and a big part of it is the odious options, either type-unsafety or code generation, for things that are trivially handled by parametricity). If things change, I'll be happy to revisit it, but the described thought process just ignores my existence. And that's fine from my perspective, because I don't need Go, but I'd still like it to be better in case I do need to use it.
For me, user provided data structures are the only thing that comes to mind (sync.Map for example) in my production use of Go. But even then, my pain (time spent, errors made) due to a lack of generics is low.
You may have written off Go. But if you're willing, I bet the Go team would still like to read your production code use-cases that caused you to do so.
User-provided data structures are a big one. (A language where I have to copypaste to have typesafe trees is a bad language.) But, beyond that, I build stuff to be compiler-verified. Here's a trivial example that I did yesterday that I straight-up can't do in Go, but did in C# as part of a object graph rehydration routine (where object A required pointers to object B from a different data set, and since it's 2017 we aren't using singletons so the JSON deserializer takes in converters that look up objects based on keys in object A's JSON representation):
public interface IKeyed<T> { T Key { get; } }
Just being able to do that on an object is powerful. C# has reified types; I know what T is at runtime. (I can't specialize on the type in C#, but I can fake it.) But I also know what it is at compile-time, and so I can't accidentally pass `IKeyed<String>` and `IDictionary<Int32, DependentObject>` to the same function because type constraints, yo.I don't really care about fast code, because computers are all future-computers-from-beyond-the-moon. If I'm using a statically-typed language, I care about correct. Duplicated code is code that will--not may--lead to bugs. State I have to maintain in my head (like "this is an int map, talking to an int-keyed object set") is state that will--not may--lead to bugs.
If you're a statically-typed language that isn't helping me avoid bugs, you might as well not exist. You don't have to be whomping around stuff like Scala's Shapeless--I would argue that you shouldn't--but you have to provide at least a basic level of sanity and the difficulty of expressing stuff outside of parameterized types (even when there are workarounds) makes it not worth the hassle. I'll cape up for damned near everything in at least some context, from dynamic languages like Ruby and ES6 to Java (well, Kotlin) or C# to Modern C++ to Scheme or a Lisp. My no-buenos on programming languages are limited pretty exclusively to C and Go because both languages encourage me, encourage all of us who use them, to write bad code.
My use case was/(still is) writing a spreadsheet where a user can enter different stuff in a cell and I want to store the value in an underlying data type. I now ended up storing everything as string because I couldn't figure out an efficient way to implement it.
My goal would have been to have one generic type that can store the cell types text and number which I can store in an array. This generic type could then implement an interface method like format() which calls a different format method depending on the type the cell has.
I played around with interface and reflect.TypeOf but lost too much performance compared to just storing everything in a string. Strings on the other hand now fill up my memory with stuff I don't need - at least that is my impression.
I don't have programming experiences so maybe I misunderstand the discussion or just overlooked a way to solve my issue in an efficient way. So sorry if the example I mentioned something easily doable in go.
FWIW generics wouldn't help you with that, "sum types" would. A sum type is a souped-up enum which can store associated data alongside the "enum tag", so you can have e.g.
enum Foo {
Bar(String),
Baz(u8, u32),
Qux,
}
and at runtime you ask which "value" is in your enum: match foo {
Bar(s) => // got a Bar with as string inside
Baz(n, _) => // got a Baz, took the first number
Qux => // Got a Qux, it stores no data
}
(and in most languages the compiler will yell at you if you forget one of the variants).So for your use case you'd have e.g.
enum Value {
Boolean(bool),
Integer(u32),
String(String),
// etc…
}
> I played around with interface and reflect.TypeOf but lost too much performance compared to just storing everything in a string.Maybe try a struct with an explicit tag rather than reflection? e.g.
type ValueType int
const (
TYPE1 ValueType = iota
TYPE2
TYPE3
// … one for each concrete type you want to wrap
)
struct Value {
type ValueType
value interface {}
}
and then you can switch value.type {
case TYPE1:
value.value.(Type1)
case TYPE2:
value.value.(Type2)
// etc...
I'd expect reflect.TypeOf to be very expensive, this is a check + a cast.That depends on the language, and enums being sum types also depends on the language.
* Rust and Swift enums are sum types (they can hold data and every variant can hold different stuff), there is also a ton of (mostly functional) languages with sum types not called enum: Haskell, F#, OCaml, … there are also languages which replicate them via other structures (sealed classes in Kotlin, case classes in Scala).
* java enums (a bare "enum" is the C one) can hold data but every variant must hold data of the same type so you'd need up/down casts
* C++, C# and C enums can't hold data
Go does not have enums.
Docker...yeah. We're stuck with it there. And their historical security posture doesn't make me super excited, but...yeah.
I hate when you're right. But you usually are.
"Here is a concrete example of a task I must accomplish with <design involving HKTs>, and the lack of HKTs prohibits me from doing this in such a way that I must completely re-think my approach.
(Concrete example explained)
I am of the opinion that lacking HKTs prevents me from having any kind of elegant and extensible design which will hold strong for years as the code around it changes."
Not that HKTs and TypeClasses aren't interesting, but without that kind of motivation, it's quite difficult to justify the incredibly large cost of implementing them well. And that cost isn't just in the code and testing of the compiler. This cost also surfaces in tooling, documentation, and mindshare. It could also have an unknown effect on the ethos of F#, which for better or for worse, does bring with it a bit of a somewhat intangible quality that many people like.
Personally, I think Golang should implement generics. But I'm sympathetic with the views expressed by Russ Cox here. And I don't think it's just sampling bias.
Rust, for example, has been thinking about HKTs for a while, and they might not fit well in the language. Rust wants to solve the problems that HKTs solve, and it looks like the solution is converging to ATCs (associated type constructors).
It's taking a while, but Rust is newish, and it isn't taking anywhere near as long as Go is taking to get generics.
Not implementing generics, then suggesting that it would be nice to have examples of generics being used in the wild… You had it coming, obviously.
Now what's the next step, refusing to implement generics because nobody uses it?
> Every major potential change to Go should be motivated by one or more experience reports documenting how people use Go today and why that's not working well enough.
My goodness, it looks like that is the next step. Go users have put up with the absence of generics, so they're not likely to complain too loudly at this point (besides, I hear the empty interface escape hatch, while not very safe, does work). More exacting developers have probably dismissed Go from the outset, so the won't be able to provide those experience reports.
I think you misunderstood. Clearly he meant to ask for examples from the real world that lack generics, but shows how adding them would improve the system.
I don't think many such examples will emerge.
First, you have the empty interface escape hatch. It's cumbersome, but it works. The real reason why the repeated use of this escape hatch (instead of proper generics), is lack of compile time type safety, which is replaced by dynamic checks. This introduces elements of dynamic typing that generics could have avoided. This has a cost, which unfortunately is hard to asses.
Second, Go users tolerate the absence of generics for some reason. Maybe they don't really need it, but then they don't have a compelling use case. Maybe they do need them, but didn't realise the limitations of the language would hurt them down the line, but are they competent enough to articulate why generics would be better? They made quite the blunder when they chose Go, after all.
That said, he also wrote this:
> For example, I've been examining generics recently, but I don't have in my mind a clear picture of the detailed, concrete problems that Go users need generics to solve.
Of course he doesn't: Go doesn't have generics. Go users found other ways, thus proving they didn't really need generics. And generics users don't use Go…
There's at least one high profile example: the standard library itself. It must have been obvious by the time they designed the standard library, before the language was out. They had to have feedback then, just look at the trail of rage against the absence of generics.
They ignored it then —I have no idea why. I'm not sure they'll listen now.
In the early days I was enthusiastic enough that I tried to provide support for the os.user implementation on Windows.
Nowadays I just advocate Go for C use cases, where the use of a tracing GC is not an hindrance as a way for us to enjoy safer computing.
Oh, and the code of the new syncmap.Map is full of unsafe.Pointer as workaround for lack of generics, while achieving good performance.
https://stackoverflow.com/questions/43368604/constant-struct...
https://stackoverflow.com/questions/18342195/how-to-declare-...
This is a remarkable oversight which makes it impossible to write purely-functional code with Go. We also see this same problem in most other imperative languages, with organizations going to great lengths to emulate const data:
https://facebook.github.io/immutable-js/
Const-ness in the spirit of languages like Clojure would seem to be a relatively straightforward feature to add, so I don't really understand the philosophy of leaving it out. Hopefully someone here knows and can enlighten us!
But take a look at this article about the design goals of Go: https://talks.golang.org/2012/splash.article Look especially at section 4, "Pain Points". That is what Go is trying to solve. So what the Go team is asking for, I suspect, is concrete ways that the lack of generics hinders Go from solving those problems.
You say those aren't your problems? That's fine. You're free to use Go for your problems, but you aren't their target audience. Feel free to use another language that is more to your liking.
Note well: I'm not on the Go team, and I don't speak for them. This is my impression of what's going on - that there's a disconnect in what they're asking for and what the comments here are supplying.
(And by the way, for those here who say - or imply - that the Go team is ignorant of other languages and techniques, note in section 7 the casual way they say "oh, yeah, this technique has been used since the 1970s, Modula 2 and Ada used it, so don't think we're so brilliant to have come up with this one". These people know their stuff, they know their history, they know more languages than you think they do. They probably know more languages than you do - even pjmlp. Stop assuming they're ignorant of how generics are done in other languages. Seriously. Just stop it.)
for foo := range(bar)
Instead of: for _, foo := range(bar)
When you just want to iterate over the contents of a slice and don't care about the indices. Failing to unpack both the index and the value should be a compile error.But if `bar` is a map or a channel, then that short syntax is very handy.
For comparison's sake, you don't see many PHP users complaining about the difference between `foreach($bar as $foo)` and `foreach($bar as $_ => $foo)`.
Improvements to package management is probably the highest item on my wishlist for Go 2.
The work is in progress and will most likely be available for general purpose use way before Go2.
Check it out: https://github.com/golang/dep
The alternative is often to just use interface{}, which hurts performance and type safety.
I'm curious as to what metric you use for "developer productivity", and the values of said metric you can cite for the 3 languages you list.
Working with batch processing I'm often changing my maps to lists or hashes multiple times during discovery. Go makes me rewrite all my code each time I change the variable type.
The only time I even notice it is if I have to write a method like `AcceptInt(), AcceptString(), AcceptBool()` and etc.
I enjoy generics in Rust, I'm just not sure what I'm doing differently in Go that causes me to not miss them.
When I'm doing text processing for example, I pass the same strings/dicts/hashes through dozens of functions for cleaning, sorting, organising, benchmarking, comparing, etc..
It's not just in->save->out CRUD work.
Now we have at least vendor. But this alien-style fixed paths is really annoying - no other language forces such GOPATH crap. Please remove this limitation from Go 2!
The conventions and resolution approaches that are needed to accomplish this often end up being opaque, less flexible, and harder to work with.
I much prefer the approach of C, C++, and even PHP to some extent, where the interaction with the filesystem isn't hidden.
When using C or C++, it's trivial to reference a globally-installed library, such as the standard libraries of such languages. It's also simple to reference third-party libraries, or even to include local copies of third-party libraries within one's own source tree. Relative or even absolute paths can easily be specified when referencing external code.
This also gives so much more flexibility with regards to how a project is laid out, and how it pulls in other code or libraries it depends on. I can follow an approach that works for me, for my project, for my team, for my version control system, for my development environment, for my deployment environment, and so on.
I want the language to conform to my needs. I don't want to have to modify my behavior and my environments to conform with what the language's developers deem to be the "right way" of doing things, especially when this isn't compatible with my needs.
Maybe this means there's slightly less consistency with how libraries and dependencies are handled across projects and libraries, but that's a cost that I'm willing to pay, and in practice it actually isn't that much of an issue when using languages like C and C++.
I'd much rather spend a few seconds specifying "-I" and "-L" and "-l" options when compiling or linking than trying to remember a bunch of conventions or how the high level package names end up mapping to the installed modules or libraries.
I'm not saying that the C approach is perfect, or that I'd want other languages to use a C preprocessor like approach of actually combining separate source files just prior to compilation or even execution.
But I would really like it if modern languages didn't try to hide the existence of files and directories so much, and didn't try to force conventions on me. I'd rather deal with the very small cost of explicitly telling the compiler where to find dependencies rather than relying on conventions or opaque resolution processes.
No problem at all with very other language, except with Go - not possible!
I want to open cmd.exe (Win) or bash (Linux) and cd to the directory and build. And not configure a GOPATH in the environment variables of the current user or pretend it to the batch script, set it in IntelliJ Go plugin, etc. It so unflexible, so arkward, and there is no reason for this crap at all. So please fix it for Go 2. Thx
Give me cargo for go either way.
$ cd projects/foo
$ export GOPATH=$PWD
$ cd src/github.com/company/foo
$ go build'go get github/user/project && cd $GOPATH/github/user/project'.
And to avoid dependency issues, I use one of the many vendoring tools. Currently 'govend'.
That's the issue, and it's a big design flaw - do away with this GOPATH
I don't have go/src/ checked in. Is that what you mean? If you do that, you are doing it wrong. I have many, many go/src/$org/$project directories, each of them have their own source control (most of which is different git repos). If I am working on my stuff, cd $GOPATH/src/github.com/sethgrid/$project. That is the repo under source control. Do any development I want, push up any changes to that repo. Rinse and repeat. I can then cd $GOPATH/src/github.com/sethgrid/$project2 and do the same. When I want to pull down $project3, I just `go get github.com/sethgrid/$project3` or manually clone it and I can cd over to it and work on it separately.
Maybe I'm not understanding your criticism.
Personally I hate Go's imports, and I think the GOPATH is a terrible, terrible idea. Yet, I quite like the vendor directory, and I expect the new `dep` tool to be the final touch.
Now my only problem is that I often forget which projects of mine are in my GOPATH, so I'm afraid to delete them -_-
To support the uncommon edge case, src/foo/vendor/dep is completely different to src/bar/vendor/dep.
Your src/foo code can't pass src/foo/vendor/dep types to src/bar code expecting src/bar/vendor/dep types. Even if the deps are identical checkouts. Code can become uncompilable by the act of vendoring a dependency.
You have two completely different sets of globals. Code can stop working by the act of vendoring a dependency.
The initialization runs twice. So well known packages like glog will panic if two packages has it vendored because they both try to update settings in the standard library.
GOPATH would be preferable, but none of the tools for managing packages at that level support pinning. So you dare to be developing more than one package at a time, you end up with a single monolithic repository representing your GOPATH and pin your dependencies in src/, or end up with symlink hacks and other unmaintainable tricks.
Eg, `import "foo/lib"` pulls seamlessly from vendor, which is a really nice UX if people want to vendor.
With that said, I still think a proper package manager Ala Rust Cargo.toml will be better. Here's hoping Go's `dep` solves this crap basket :)
Solving the GOPATH issue would be letting me "git clone" a Go project to a directory of my choosing and still use the tools.
But well, I trust the core team to make the best choices.
What I'd like is some kind of automatic error propagation, so any unhandled error will return from function wrapped with some special class with enough information to find out what happened.
Collections?
It also really makes me wonder if I am living in some sort of alternate reality. I am a professional programmer working at a large company and I am pretty sure that 95% of my colleagues (myself included, as difficult as it is for me to admit) have no idea what a reified generic is. I have run into some problems where being able to define custom generic containers would be nice, but I don't feel like that has seriously hindered my ability to deliver safe, functional, and maintainable software.
What I appreciate most about Go is that I am sure that I can look at 99% of the Go code written in the world and I can understand it immediately. When maintaining large code bases with many developers of differing skill levels, this advantage can't be understated. That is the reason there are so many successful new programs popping up in Go with large open-source communities. It is because Go is accessible and friendly to people of varying skill levels, unlike most of the opinions expressed in this thread.
> I am a professional programmer working at a large company and I am pretty sure that 95% of my colleagues (myself included, as difficult as it is for me to admit) have no idea what a reified generic is.
I didn't know what it was called either until I saw them mentioned here, but now that I know about it, I get why it would be useful. Before that, I kind of assumed all languages with generics would also allow you to access the type information at runtime.
> I have run into some problems where being able to define custom generic containers would be nice, but I don't feel like that has seriously hindered my ability to deliver safe, functional, and maintainable software.
I don't mean any disrespect, but this is a very good example of the Blub Paradox: http://wiki.c2.com/?BlubParadox
Interesting read, and I admit there is some of that. But my point isn't to say that those features aren't useful or powerful, but rather that with the constraint of working with a large group of programmers of varying skills, simplicity has more value than power (as long as we can deliver software that meets our requirements). It is similar to point 4 in the "Problems with the Blub Paradox" section.
Not including feature X in programming language Y will also ensure that no-one who primarily uses Y will ever come to understand or appreciate X.
The blub paradox basically represents the opinion that it is always better for languages to include more powerful features.
I think that goes too far, working primarily in Scala I am seeing firsthand how much diminishing returns extra complexity in the language can have, but I'm certainly of the opinion that Go leans too heavily toward handicapping the language in pursuit of simplicity.
Same with generics. It's easy to point to simple examples. It hard to ensure that terrible metaprogramming monstrosities don't arise.
It's possible to write bad code with go as it is, but it's actually difficult to make such bad code inscrutable.
In my experience, most people who think they need reified generics don't really understand them and can actually do fine with much safer concepts expressed on an erased runtime.
If it were a topic of gender or race instead of programing language I am sure this behavior would be called out as mansplaining/whitesplaining.
I do wish some term like 'PLsplaining' be used for this infuriating behavior of calling Go/PHP or whatever programmers clueless.
What are you and your co-workers using aside from Go?
We also do some Python and Javascript for scripts and web work.
No you really can't. You can look at any given line of code and tell me what it does, where as it might take more time to parse any given line of Haskell, Scala, or OCaml longer.
However, expressivity in a large application pays a dividend tenfold, because the main challenge of reading code is not any given line, it is understanding the application architecture, the frameworks, the data flow and how it all works together.
> That is the reason there are so many successful new programs popping up in Go with large open-source communities.
Successful, or popular? Just look at Docker. Certainly popular, but check the bug tracker or talk to anyone with real world experience using it at scale and you'll be hearing a completely different story.
Haha, thanks.
If there's a Go2 there will be a lot of these headlines for blog posts/rants.
It doesn't matter if it is true or not, but that phrase is wonderful.
For example, in Go it's difficult to implement state machines in a clean way. Another is handling events and timeouts is easier with Reactive programming. Distributed programming is easier with Erlang or Akka. Don't wait for problem reports in the Go community. Look at the problems in other PLs and proactively improve Go.
Turning: func (f Foo) name() string
Into: func name(f Foo) string
Callable like this: f.name() or name(f)
Extending foreign structs from another package should be possible too, just without access to private fields.
Other than that, if-as-expression would be nice to have, too.
This post really frustrates me, because the lengthy discussion about identifying problems and implementing solutions is pure BS. Go read the years worth of tickets asking for monotonic time, and see how notable names in the core team responded. Pick any particular issue people commonly have with golang, and you'll likely find a ticket with the same pattern: overt dismissal, with a heavy moralizing tone that you should feel bad for even asking about the issue. It's infuriating that the same people making those comments are now taking credit for the solution, when they had to be dragged into even admitting the issue was legitimate.
The attitude of Go community cannot be separated from the patronizing tone of Go maintainers. In fact it stems directly from the people @ Google working on Go. All the bullshit "you don't need that with go"™ comes directly from Pike,Cox and co. It's fine to be opinionated, but just admit these are opinions instead of engaging into the bullshit they engaged in for years when dismissing this or that problem as irrelevant.
Pike said "Go design" is done. Except that a language which design "is done" is a dead one.
Guess C is done for.
Original (~1970) K&R (~1980) ANSI C (~1990) C99 (~2000) C11 (~2010)
I would not be surprised to see a C22.
Many small quality of life improvements: stdint.h stdbool.h, inline, restrict, allowing variable declaration within the code, snprintf, variadic macros...
Most of those were accessible through compiler extensions but having them in the standard means that you know it'll work everywhere on any compliant implementation.
Linux definitely isn't conservative when it comes to the language, not only does it use modern features of the language but it even relies on GCC extensions and even sometimes on the compiler's optimizer to produce the correct code: https://unix.stackexchange.com/questions/153788/linux-cannot...
I'm not super familiar with CPython but it also doesn't appear to limit itself to C89, see for instance this random file I opened: https://github.com/python/cpython/blob/master/Python/future....
You can see that `const char *feature` is declared in the middle of some code. It also uses "inline" in several places.
They decided to ditch clang/c2 integration work, and are planning to actually fully support C99 and not only what is required by ANSI C++.
https://www.reddit.com/r/cpp/comments/6mqd2e/why_is_msvc_usu...
https://www.reddit.com/r/cpp/comments/6mqd2e/why_is_msvc_usu...
Go gets frequent updates as well.
You're basically saying "well, it's still subject to logic errors." Well, that's a nonargument, because you can say that about 100% of languages in existence. That is not a criticism you can levy just against BEAM langs. And we were never arguing that BEAM magically takes care of your logic bugs. Just that it seems to handle the "set of all bugs that are not logic bugs" better.
I also apologize for erlang/elixir fans being rabid and insufferable. ;) A lot of us have dealt with many other langs (I spent years on Ruby and other OO langs) and we're enjoying the unexpected (to us) benefits of this new/old world, that's all. Joe Armstrong is basically Einstein AFAIC...
For example what if you screw up and forget to encode that it SHOUDN'T return OK?
you know that many other frameworks also have these safeguards right? Java server frameworks have been doing it forevever. You can code dirty if you want and deal with no issues or you can be error prone. Hell you can do the same thing in go. You can catch panics. It's not pretty but you can definitely do it.
I forgot one can only point out strengths of erlang not problems with humans coding in general. all hail erlang.
As for Erlang's problems, it does have a fair number of warts and special cases, but the biggest of them all is that it's still a niche language. We actually had to dump Erlang for new projects in favor of Go. That may sound ridiculous, but filling positions involving Erlang was next to impossible (even with candidates without any prior knowledge of the language).
You can wrap all your code in an unchecked exception and never again have to worry about errors. It's bad practice obviously but there are many situations in which you don't care what error occurred but that an error itself occurred.
<>, err = <>
if err != nil { return nil, err }
<>, err = <>
if err != nil { return nil, err }
Is not what software engineers should spend time on, definitely.Not automating ubiquitous trivial propagations with at least explicit "rie <expr>" (rie for "return if error") is a complete engineering fail under any philosophy.
Oh, I have an idea. If ", err" part is missing from lvalue, insert that mantra automagically under #pragma ARIE=on. This workaround will give designers some stats to embrace.
But hey! Your editor can easily boilerplate all that... boilerplate!
I think Go will continue to grow until we finally have metrics that say that coding something in Go might get you fast-running code but will be slower to code and hell to maintain.
Meanwhile, well-written C code does this the "wrong" way; every external call has to be done like
if (read(fd, buf, len) < 0) {
hopefully remember to free some things;
return NULL;
}
Why do you think explicit error handling is a Pythonic practice? if err != nil {
return err
}
So it's more like 7,7 million to 17,6 million (the top fifth "the" is from comments, so it doesn't count)As much as people complain about it, explicitly checking errors like this (and checking them all) is almost essential for production quality enterprise code and any code running on mission critical systems (and to most project managers, all systems are mission critical).
And fwiw I still prefer go's style of error checking to languages that implement massive try/catch blocks.
If you were to code just this "happy path" in Go, it would actually be the unhappy path, because you'd have silent errors resulting in indeterminate state (read: horrible bugs to troubleshoot). If you believe programming is mostly about managing all possible state (as I do), then Go's strategy is way too explicit. In Erlang/Elixir/BEAM's case, as soon as state goes off the rails (read: encounters something the programmer didn't anticipate), it's logged and restarted by its supervisor process almost instantly. (and there are of course strategies for repeated restarts)
That doesn't fix the error, and it doesn't imply the program will work correctly.
Defer to pmarrek on what they meant, but to me it's an issue of practical programming.
In a choice between "I will tell you what to do about errors" vs "I will assume you only crash and restart on any error", I've found the later to be far more efficient.
The former generally leads to a rat's nest of never ending error specialization as unexpected or rare stuff bubbles up in UAT or down the road in prod.
Which isn't to say there's a right answer. There's always going to be particular situations where of course you should use one or the other.
But on the whole, as a philosophical default, fail-and-recycle-on-all-errors is a helluva lot easier to spec, code, test, and maintain for me.
the erlang strategy is to make it possibly to continue from a known good state when the latter happens where chances are it won't happen again. the errors that happen all the time you will encounter early/often enough that you can work out how to handle/fix them
the result of this strategy is you pretty much never write code that handles the unexpected. it turns out that's a lot of code to not have to write
The thing is, there will always be the "unknown unknown" bugs, the ones you didn't even think to anticipate, and Erlang/Elixir/BEAM will always win out in a behavior contest on those vs. in Go, because it is built to anticipate any possible error, not just the possibilities you're aware of.
Coroutines seem like something that is more complex to me than generics.
That must be because a lot of function calls are followed by:
if err != nil {
return err
}
https://stackoverflow.com/questions/18771569/avoid-checking-...The worst I've seen is people logging the error message but not the exception itself (so losing the stack trace), or causing another exception to be thrown (and losing the stack trace or cause of the original).
For example, in Java, I can make as many method calls I want inside of a try block, and catch an exception from any one of them in the catch block. In Go, I would need the equivalent of one catch block per method call, in the form of "if err != nil { return err }". Why repeat yourself?
If you want to pass on an error in Go, its easy to do accidentally -- just omit "if err != nil". Both types of exception handling models enable this kind of behavior. If you are going to do it, you might as well not repeat yourself while you are doing it.
Because you can put more meaningful error messages due to the granularity of the checking. This has made debugging things a lot easier at times.
I'm not saying Go's error handling is better than Java's. But I've written some relatively large and complicated projects in Go (like the Linux $SHELL and REPL scripting language I'm currently coding this very moment) and error handling really is the least of the problems I've been having. Sure, Go's idioms do get in my way from time to time. But on balance Go's idioms save me more time than it wastes. Which is why I keep coming back to Go despite having used more than a dozen other languages in anger. But that's my workflow and my personal preference. Others will find the reverse to be true and I wouldn't be complaining that their language should be more like Go.
Frankly I think people waste too much time comparing languages. Just try something and if it doesn't work, move on. Don't assume your personal preference is a benchmark for how all languages should be designed as there will be a million other developers whose personal preference will exactly oppose you.
But in most cases it is unnecessary since you always have the line number and type of exception printed out as part of the stack trace.
There is also the switch case you need to make when catching an error, like tcp connection then dns resolution and so on, you have to know every time any kind of sub errors.
His anecdote about Google/Amazon using leap smears to solve the problem is telling. I suspect that they were unable to see outside their own bubble to think about how others may be impacted.
> We did what we always do when there's a problem without a clear solution: we waited
The original problem described the issue and the potential consequences very well and the problem didn't change between the initial report and when cloudflare hit it. It was only until a Serious Industrial User (to borrow a term from Xavier @ OCaml) got bit in a very public way that they actually began thinking about what a clear solution would look like.
And this is exactly why the Go designers don't understand language design, and how this ignorance shines through every single place in their language.
Language design is about compromises. There is never a perfect solution, only one that satisfies certain parameters while compromising others. You, the designer, are here to make the tough choice and steer the language in a direction that satisfies your users.
Besides, characterizing their position as "We waited" is very disingenuous. First of all, this is more stalling than waiting, and second, waiting implies they at least acknowledge the problems, which the Go team famously never does. Read how various issues are answered and then summarily closed with smug and condescending tones.
As for acknowledging, well, they have always been very clear about their position. It makes no sense to spend a decade answering the same question over and over with a long and elaborate response which the person asking has already seen and dismissed. I can understand them becoming condescending after a decade of facing people who act with a deliberately obtuse and holier-than-thou attitude.
It's not like they have been lazy - every release of Go has had a large amount of improvements that matter. Working on generics would have meant sacrificing a (probably large) amount of them.
(for the record, I dearly miss generics too!)
Go is great for server-side web applications. The libraries for that are all there and well debugged. Anything else, not so much.
It appeared to me that the golang devs believed so strongly in the superiority of leap second smearing that waiting for everyone to adopt it was better than compromising their API or the implementation of time.Time.
I couldn't fit any kinds of thank yous into the talk, except for the credit to the overall community in the first few minutes, because I had a lot to say and only 25 minutes. It's possible I should have written an extended blog post that was more than just the talk, but I didn't - the blog post is just the talk, as it says.
If they implemented everything everyone asked for they'd have another screw-up like C++ or Java.
C++ is a whole other animal. The whole system of a Go program is typically very penetrable. You can trace the code down to the bottom of the stdlib or even the compiler builtins in seconds with ease (thanks to the excellent Guru and vim-go). In my experiences of C++, I doubt how many C++ programmers have ever had a look to the implementation of STL or iostream, which is hard to do ergonomically. Although some people occasionally call the opaqueness encapsulation and see it a good practice (which I don't agree). There are lots of cross compilation and static linking problems that don't arise with Go. There is the problem that one C++ code base will look completely different from others because no one in the C++ world follows conventions the way people in the golang world do.
Compared to them Go is a very well thought out and elegant language.
But interestingly, not one I would choose with which to embark on a project that I expected to require more than 2000 lines of code (as a ballpark scale measurement).
Go seems to be an excellent glue language. It's very suitable for making a tool that does /this/ and only /this/, putting it in a docker container and running 500 of it at once, linking other parts of my stack together.
I would never use it to write a RDMS, because (as someone who has admittedly been following the situation only loosely) the language maintainers don't seem interested in solving other-people problems. They seem to be making a language for them, and I can respect and understand that, but it certainly gives me pause to think about using it for anything big. If I run into a problem that Go can't solve, will it be possible to persuade the language developers to give a shit? Situation unclear.
No you don't. But even if I granted that Golang has a bright future (I don't think it does) by any objective measure C++ and Java are more successful than Golang. LOC, number of devs, performance, install base, etc. Pick a measure other than current hype (not even peak hype) and either Java or C++ trounces Golang.
> I've used all three languages.
I have too. I program golang full time and have for 3 years. I'd switch tomorrow to either of the other languages if I could wave a magic wand (and I'm a fair hater of both).
> and runs in a JVM
I desperately miss the JVM (any of the ones I've used). The amount of sophistication and polish in comparison to the Golang runtime is embarrassing. Debugger support, operational sophistication, IDES, tooling, basically everything is better on any of the JVMs I've used in comparison to the golang runtime.
>Java has mile long class hierarchies, massive try-catch blocks, indentation as thick as my neck
And go has ridiculous copy/paste libraries, horrendous concurrency edge cases, terrible error handling, worse third party libraries and no story around packages.
> There are lots of cross compilation and static linking problems that don't arise with Go.
Because dependency management is not possible in golang. It is literally the worst story in any language I've used in 20 years. You have 2 choice in golang half baked vendoring or a monorepo where you have all of your code in one place.
> Compared to them Go is a very well thought out and elegant language
No, compared to them Go is a young language. There aren't any big projects in Golang yet. We shall see if there ever will be. My theory is that either none will ever happen as some other language will be a better choice, or golang will adapt and all the things people claim are benefits (simplicity, default tooling) will go away, crushed under the reality of complex projects being complex.
"Just as we at Google did not at first understand the significance of handling leap second time resets correctly, we did not effectively convey to the broader Go community the significance of handling gradual code migration and repair during large-scale changes."
Our fault, both times.
I want to be clear I'm thankful for the effort everyone has put into golang. Making something people care about enough to love or hate is hard. Stewarding a FOSS project can be a very negative thankless experience as well. While I'm being critical above, there are also lots of examples of golang folks, core or otherwise, making really constructive and productive comments and contributions. I don't mean to trivialize that or ignore it.
(Sorry just discovered this song a few days ago)
in particular, i think it could help with the method dispatch, but probably not with the memory allocation ( although go already uses interfaces pretty extensively).
I like Go, but the maintainers have a maddeningly stubborn attitude towards generics and package managers and won't ease up even with many voices asking for these features.
This is absurd. Waiting to fix a known language design issue until a production outage of a major customer is a failure of process, not an achievement. The fact that the post presents this as a positive aspect of Go's development process is beyond comprehension to me.
Shows the value of constantly being transparent and supporting open source projects.
Please don't refuse to compile just because there are unused imports.
Please do warn, and loudly say it's NON-CONFORMANT or whatever will embarass me enough from sharing my piece of Go code with someone else.. but.. can I please just run my code, in private, when experimenting?
A more modern implementation: https://github.com/munificent/vigil
Since we have version control now, I don't see any threat from an optional capacity for the compiler to destructively mutate the source as an optional feature. Let the same toolchain that converts input to output suggest improvements to the input.
If you're working with inexperienced programmers (and some experienced but bad programmers), you should be certain that they WILL let that code garbage there if the compiler allow it.
Having custom functionality in editors to work around this apparently dogmatic design decision in the core language.. is also just absurd.
> godep helps build packages reproducibly by fixing their dependencies
In any case, every language has a hoop to jump through. Good luck finding one without.
From my experience I think we should talk about container formats because they make 80% of what we would like to have generics for. Actually, go feels as if it has only two container data structures: Slices and Maps. And both feel as if they are pretty hard coded into the language.
Yes, I am sure there are more container formats and it is possible to build your own, but I think it is not easy enough.
That's inane and insane. Java generics do almost nothing, they don't even exist at runtime in a language with (much like Go) heavy runtime semantics. Here's what Java's generics do: get typechecked, and insert implicit casts.
> Yes, I am sure there are more container formats
First, you forgot Channels and arrays (which are distinct from slices) as well as "range" to iterate over them generically, second there are more containers than that in Go's own standard library: https://golang.org/pkg/container/, https://github.com/golang/go/issues/18177
> it is possible to build your own, but I think it is not easy enough
What does that mean? It is not easy enough what? To write containers? To have generics?
2. and it requires significant expense of explicit casts which are not just sanity checks (which is what they are in java, due to type system weaknesses)
Actually, I meant it is not easy enough to build container formats (maybe due to the lack of generics).
Btw. I will not continue this conversation as I am tired of getting insulted (inane and insane), getting downvoted for merely asking questions and you editing your post without labeling it. Just saying.
You didn't ask a question. You didn't include a single question mark.
What you said was a jumble of misleading and wrong words.
I regularly use generics in other languages for tasks unrelated to "containers"/data structures. They allow me to write less code and have the compiler check my mistakes.
Containers are frequently where people start with parameterized types, but once you are in the habit of using them actively that's rarely where you stop.
Eh. Many people don't ever go beyond that and it's fine, containers-type structures (to which I include stuff like futures or option types) and functions working on those are by far the most common use case.