That being said it would be really nice to have some reusable map type structures that handle GC better than the default maps. Fingers crossed.
That being said it would be really nice to have some reusable map type structures that handle GC better than the default maps. Fingers crossed.
The answer is: A lot.
- read that from start to end exactly zero times,
- picked up generics in a day or two,
- struggled with advanced types twice, half a day each time
- struggled with type erasure twice, also about half a day each time.
Meanwhile generics often saves me a number of minutes pr hour and makes everything cleaner and easier to read.
I used Java before generics but once it arrived I never looked back and neither did anyone i know.
And then all that momentum, all the tools, the books, the conference presentations, the code ... all of it. Pop! It died. Because code generation sucks. It is the worst solution to any problem solvable by a type system.
I thought it was madness, but bringing it up to a very large Golang group and get "nope channels are cheap! That's fine! There's repetition but it's easy to follow"
I've said before, my personal take is use Go, get a feel for the Go mentality, then take it with you to another language.
Go is just too stuck between low level and high level for me personally. I'd rather go under with Rust or over with Kotlin or C#
Dictionary<List<Dictionary<string, object>>>, SortedSet<Dictionary<int, string>>>>
Several thousand out of bounds, missing keys, null reference exceptions, hash collisions and the hair starts to get thin on top. I'm not even sure I'm happy with it for abstract data types.
Please can we keep Go special.
What does any of that have to do with generics?
Do you have any examples?
This is not a problem with generics, but with C#'s lack of discriminated unions and/or tiny-types. Except what on earth are you doing with a dictionary whose keys are lists of dictionaries? I am quite sure someone has not modelled their domain correctly there. That's not something you can blame on the existence of generics - I shudder to imagine how much worse it could have been without generics!
The point is really that it's hard to reason about such things and define if they are appropriate or not for a lot of people. It's a lot of rope to hang yourself with.
It probably would have just become a very long String pretty early on. It would have xml in it, but not always, because nobody is that lucky.
I don't know why you're all so scared of a little repetition ;)
Go's built-in collections are actually generic and Java's generic-less awkwardness was mostly about collections.
class IntList {
private List objList;
public add(int elem) {
objList.add(Integer.of(elem));
}
}
Rest is left as exercise for the readers that never used Java generics, or C++ templates.Generic map/reduce on slices wouldn't hurt too.
OTOH, it's 2021 and look at what we are wishing. My love/hate relationship with golang is like the one I have with Apple.
1. The now-provided interface can more clearly express what the code is intending to do (better names for the operations you're providing than the underlying system has, remove_from_end to pop or dequeue)
2. Hide methods of interacting with the underlying data structures that you don't want people to use (use a C++ vector as a stack, but don't want random access)
3. You can replace the underlying mechanisms at will without impacting the users
If you just wrap a vector in your own vector class and otherwise provide the same operations (or a limited set of operations but for no good reason to restrict usage), sure, that's moronic. But if you wrap a vector class in a "BigNumber" class and provide operations like add, subtract, mod, etc. then value has been added. Same thing with the idea of wrapping a map in a set interface.
For this reason im an advocate of lazy wrapping. Create an abstraction at the last moment, when its painfully obvious what benefit it will provide, when you can see how it ties together disparate pre-existing code blocks, and when you have the highest confidence that it will stick and not need to be unwrapped next week by the senior dev.
I'd offer a different view. Wrapping/abstracting like this should reduce the amount of things a user of the abstraction needs to know. I don't care how Java's BigInteger class works under the hood, only that it does what I need it to do. If I did have to know how it worked to use it, this suggests a failure on the part of whoever created it.
It does increase what the maintainer of the underlying system (including the abstraction) needs to know, but if done in a sane manner this should not be a burden. So we're making a tradeoff. The user gets something simpler, the underlying system maintainer gets something a bit more complex. Or the user gets something more complex and with more boilerplate but the underlying system maintainer gets something simpler (though will be pestered with, "Why don't you offer a generic set yet?" asked for years to come).
> meaning if you have changes which impact multiple layers of wrap, its harder to determine what to change, and to maintain the understandability of each layer.
When this happens, in my experience, it has meant one or more of:
1. The choice of how to wrap/abstract was poorly chosen
2. The choice was made too early (before the problem was properly understood)
3. A major change was made that would've been hard to identify/plan for earlier
I ignore (3) when writing code beyond what's reasonable to plan for. (1) and (2) though mean I mostly agree with this:
> Create an abstraction at the last moment
But rephrased, borrowing the phrase I first saw in some Lean Software book, "last responsible moment." It's not sensible, for instance, to use a map to booleans as a set throughout the project's life and only wrap it at the last moment. If you know it's going to be a set, wrap it early because this offers clarity to your code and reduces boilerplate/noise. If you know you need a stack, and have a vector available, wrap it and hide the random access option. If it later turns out that you also want random access, you can offer it, but if it's been available from the start then users will have abused that and you won't be able to rein it in later (without a lot of effort and heartache).
I mean, you could support only List objects in the language and call it a day because they can be used as anything else. Or only lambdas, for the same reason. At the end of the day, though, having structures for the various ways you want to treat data is helpful. Using the right structure to hold data reduces cognitive load.
Not to defend Go or anything, but that's like saying:
| A Array [with element type = byte/char/u8] and a String are different things
It might be useful to call them different names (of course that would require Go to support generic typedefs for `type Set k = Map k Void`), but they're still fundamentally the same thing.
0: which, to be fair, is not the same thing as a map with bool values.
* each key now has 3 possible states (true, false, and unset) rather than two
* a bool takes 1 byte to store (which may get more problematic due to alignment, I've never checked what the memory layout of go's map is so I don't know how much of a concern it is there)
An empty struct fixes these issues: a key being present means the item is in the set, and an empty struct is zero-sized.
edit: apparently go maps are laid out as buckets of 8 entries with all the keys followed by all the values, so there's no waste due to padding at least.
People argue there are 3 states but it is meaningless in my opinion because you can just ask exists := someMap[someKey] without checking for existence as you do with real maps. Here false is equivalent to not existent.
People new to Go tend to pick map[T]bool or map[T]int because they're used to using bools and ints throughout their code, but struct{} is the correct value type for sets. (That is not to say that a counting set, map[T]int is useless, however. If you need that, use that!)
if _, ok := wasTouched[thing]; !ok {
touch(thing)
wasTouched[thing] = struct{}{}
}
is way uglier than if !wasTouched[thing] {
touch(thing)
wasTouched[thing] = true
}[0]: https://rust-lang.github.io/hashbrown/hashbrown/hash_set/ind...
Generic maps as sets are only ok for membership checks.
Since Go has neither generic functions nor generic typedefs you can't implement a Set with a generic key type on top of map, you have to reimplement all the set operations for each key type you use.
Of course, if you need a concurrent set you're right back in type system hell.
If you want a set of some complex kind of value that contains non-map-indexable types like slices and pointers, then you have build an indirection around it.
A good set implementation needs to support a comparison operation. I really wish this existed for Go maps, too.
https://www.github.com/glouw/ctl
Why Go is so far behind is behind me
So in this light, and with the basic generic data structures supplied by the standard library, it seems to make sense for "user-level" Go code to generally be better-off without generics
Of course the line between "library" and "application" code isn't well-defined (especially if you consider libraries outside of the standard one), which is probably where most of the pain-points are coming in
Though again, Go as a whole seems ill-suited for scaling to larger projects because of lots of other limitations on its type system, reliance on conventions, implicit-defaults, etc. Which makes it well-suited to (and often used for) things like microservices, where each actual codebase is smallish. Codebases like these will tend towards having less "library-like" code anyway, which means they don't need generics as badly. There's synergy here in the language design.
So I guess what I'm saying is: leaving out generics seems like the more "Go-like" direction, will dovetail better with its overall philosophy, etc, and isn't without advantages. But it would also mean kneecapping the language when it comes to certain use-cases that it's never going to be great for anyway. It's the classic "opinionated" vs "everything for everybody" dichotomy
https://fasterthanli.me/articles/aiming-for-correctness-with...
Ctrl+F for "Let's start with Go" to jump to the relevant part
I am aware that fasterthanli.me can be a bit, shall we say... opinionated. Though as you say, his points do stand on their own. I can see the things he points out about the design philosophy of Go's language features and standard library and draw parallels to languages and libraries that I've used firsthand, and had firsthand frustrating experiences with when it came to navigating their magical behavior and lack of enforcement of contracts. But I'll keep an open-mind
That being said, APIs with implicit function that are broken or surprising are painful, but I take this not as an indictment of implicit function, but as an indictment of buggy APIs. I think state and hidden functionality is the essential ingredient of highly useful code.
Ironically, despite all their differences, Rust actually has a similar situation: it's really hard to write the fundamental data-structures in Rust, so they've put a focus on having really good standard-library implementations and people are generally content using those (in Rust's case it's because the borrow-checker makes pointer twiddling hard, but the outcome is similar)
There is basically no limit to the number of data structures possible, nor to the possible implementation details of most of them, all of which can be relevant to the situation at hand.
The stdlib can hardly be expected to implement them all.
And graphs.
> You don't need to be writing your own linked-list or (basic) tree from scratch.
Trees are rarely useful in and of themselves, what's useful is the data structures you're building out of them. And that, in turns, informs a significant number of properties of the tree you'll be using as well as the operations to perform. The stdlib providing "a basic tree" and essentially telling users to get bent would be worse than useless, it would be actively insulting.
Even for the humble "linked list" there are half a dozen possibilities: singly linked? Immutable? Doubly-linked? Circular? Array-of-nodes?
Data structures generally need to be parameterized on the contained types of you don't want to waste the effort of even having a static type system, which makes it impossible to do this right without generics
I find that it really depends a lot on the language you're working in, and how well it does generics.
In Java, I don't use generics much beyond collections, streams, stuff like that. Whenever I try, I tend to trip over its relatively limited implementation of the concept.
In a language like F#, on the other hand, generics are the cornerstone of my business domain modeling. They provide a way to map everything out in a way that is much more concise, readable, type-safe, and maintainable than I find to be possible in many other languages.
I have yet to kick higher-kinded polymorphism's tires in a good context, but I can see where a good implementation of it would move things even further in that direction.
(edit: Disclaimer: This isn't meant to be a statement on Go or the advisability of this proposal. Go isn't really meant for the kinds of applications where I've seen real benefit from generics.)
I've gotten use out of generics in "application code", but I've also been bewildered by overly-complex generics-within-generics-within-generics written by other people in application code. It's hard to be conclusive, but I wouldn't be surprised if they've done more harm than good across application contexts.
To me, that's a shining example of the problems I've run myself into when trying to squeeze much power out of Java-style generics. I never seem to encounter similar problems in F#. Scala, it depends on how successful I am at not losing a boot in the mud.
Generic programming was born in a language whose other pioneering features were algebraic data types and an HM type system. I've never really seen a first-rate example of one that didn't come paired with at least passable examples of the others.
Kotlin does compile much slower than I would like, but at least I only haul in one version of libraries and 0% of it is generated code. Java is basically instant for me on the same codebases.
I'm only pulling in k8s.io/api, k8s.io/apimachinery, and k8s.io/go-client though.
That's interesting, could you explain this more?
Context: I used to use Go a lot, but mostly haven't since Go modules, and I'm curious to know the details of the problem and why it happens.
If your git repositories aren't tagged just so, then go mod throws its hands up and simply invents a whacky snapshot version. Because it can't itself properly determine "earlier version" from "later version" on that snapshot, you often wind up with multiple snapshots from the same repo, not infrequently transmitted through other dependencies.
This is just jolly good fun when it turns out that your dependencies are pulling in incompatible versions of things. Since the official Kubernetes policy for downstream consumers is "we don't care about downstream consumers", it happens more quickly than one would expect.
As much as I have hated playing whack-a-mole with Maven or Bundler, I hate even more playing whack-an-adamantium-and-invisible-with-xray-eyes-mole against go mod.
Does that mean Kubernetes is not following the version tagging policy in its repos? That seems...surprising!
It would mean that you need to create Go compatible tags for your library releases OR adjust your product versions to match Go's assumptions.
The whole implementation of Go modules is a shit show once you get into the weeds.
Either I’m misunderstanding you or you’re mistaken. You can’t have multiple versions of the same module in a go build.
If you never tag v1, you'll never have to deal with it.
It really makes Go look like a joke
1. OCaml
2. Go
3. Java
...
999. TypeScript (any version)
The TypeScript team should consider rewriting the type checker in Rust or Go IMO. It's free, they can do what they want, but the performance is really not good (compared to what it could be) and affects a lot of people.That's exactly what generics are supposed to solve! ;)
No, but seriously, I'll be interested to see if they can pull off maintaining the compiler performance while adding support for this new feature. I've had to hand-write a lot of code that I'm excited about a generics solution automating, but automation can have a price, you're exactly right. I've worked on a C++ codebase before that couldn't physically compile on my machine because it blew stack on template instantiation recursions (issue never noticed because the original developer had a better machine).
It's beautiful that we still import 6yo packages that are clear, concise, and work as needed. The package landscape with generics doesnt seem great.
For many its really not a big deal not having generics, and makes code SO much easier to follow (and compile / debug etc).
There are plenty of languages out there with Generics. I use several of them. I use Go when that suits me, and it's typically for cases where high readability trumps doing a lot of magic with generics. I think only once or twice have I thought to myself, "this would have been better with Generics".
So the argument for not having them now becames either:
(a) they rather not have it available, because you personally don't find it useful
(b) those using generic don't know what they're doing, and only people not using generic are smart, so it's better to not have them to prevent the clueless from being able to use them
I'm not trying to make out that Generics have no place in Computer Science. I was trying to make the case for it being nice that there was a language that didn't have it, and was building on the grandparent saying that he didn't miss it that often, which mirrors my experience with Go.
The fact that something is a type variable makes it clear the that type of that thing doesn't matter.
package foo
type T interface { func Bar() }
func New() T {
return &someotherpackage.ImplPickedAtRuntime{...}
}
Now whenever you see: x := foo.New()
x.Bar()
You have no idea where to read the code for Bar. For maximum fun, ImplPickedAtRuntime should then contain members that were allocated in the same way. What should be a simple M-. then eats up your entire afternoon.Having `Set<Foo>` and `Set<Bar>` is far more readable than `class FooSet` and `class BarSet`, where the code is exactly the same aside from a search-and-replace.
You also run into issues where someone finds a bug in `FooSet`, but doesn't know `BarSet` exists, and forgets to patch both. Now, you have two divergent copy-paste classes.
Generics solve a bunch of real-world problems, in a very simple manner.
The only real cases I can see is creating new data structures, (for instance if you wanted to create your own map type).
There are lots of generics use case you either can't solve at all with interfaces, or you have to contort every which way and usually lose something in the process (type-safety, performances, readability, …).
Half of programming is creating new data structures.
The other half is tranformations (e.g. map, filter, reduce, min, max, etc) which also benefit from being generic.
I can see it for your transformations, but I have seldom seen cases where generics would really help (usually we're talking about comparing complex structure types that will need custom code anyway).
Of course not. Go is Turing complete, and generics do not make it Turing-completer (whatever that may mean)
The question isn’t about what can or cannot be done, but about expressiveness, ease of understanding for humans versus language size (even if you have plenty of disk and RAM, That correlates with buggyness of the compiler) and compilation speed.
There are none, you can always work around them, with the only downside is that you'll move some potential compile-time errors to runtime errors.
But that's the wrong conversation to be having; we could also say "why do we need floats? Everything can be solved with ints too", which is true, but also a lot of work and a poor trade-off. Similar arguments exist for many language features.
So it's a question of trade-offs: how much time will this save people? Will it reduce faults? And what are the costs of adding this? And how do they balance?
I work in Kotlin every day and my laptop never freezes.
I'm sure compile times are shorter in Go (after all, that's one of Go's main selling points). But compiling Kotlin code doesn't seem to be a huge bottleneck in my experience (and I have worked in some other languages that had truly atrocious compile times cough Swift cough). In fact, it usually takes longer for Spring to boot than for the Kotlin code to compile.
The insane amount of time of overhead an Android project has over a "plain Kotlin" from resource packing to dex stuff to desugaring to ProGuard, it made me question if they're speaking in good faith for the rest of the comment...
My ktor projects on the other hand compile incredibly quickly, and with hot reloading it's even more seamless to iterate
When I hear that, the first things that come to mind are things like haskell's IO monad, which forces you to model IO better than go or most other languages, or haskell's other state monads which similarly force you to model state more explicitly.
I think of rust's lifetime and ownership system, which forces you to correctly model the ownership of types and prevents quite a few bad design patterns (which I see constantly in go btw; the number of times I've seen races due to multiple goroutines writing to a struct is large, the number of times I've seen incorrect synchronization that rust would have prevented is large).
I can't think of anything in go that pushes you towards designing your code well in go, especially when compared to languages with more complete type-systems.
No, obviously. I said that Go doesn't give you an escape hatch if you screw up. It does nothing to protect you from screwing up. I specifically said that the challenge was in learning how to not screw up as the language doesn't help you deal with or avoid design mistakes.
I still don't get your contrast to other languages though. Are there specific language features other languages have that let you paper over crappy designs?
Writing APIs to deal with futures, etc, is much easier when you can chain parameterised functions together.