A Proposal for Adding Generics to Go(blog.golang.org) |
A Proposal for Adding Generics to Go(blog.golang.org) |
Which then creates the problem of developers who insist on writing infinitely extensible generics with indecipherable bounds.
Just repeat code. You'll be fine.
If you find yourself repeating a LOT of code because Go does not support generics, maybe stop and think about your design. Putting generics in as an escape hatch will do you more than good, guaranteed
- https://news.ycombinator.com/item?id=20576845 (2019 draft)
- https://news.ycombinator.com/item?id=20541079 (also 2019 draft)
- https://news.ycombinator.com/item?id=23543131 (2020 draft, i.e. the base version of the current draft)
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.
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.
Go's built-in collections are actually generic and Java's generic-less awkwardness was mostly about collections.
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.
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.
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
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.)
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.
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.
I'm only pulling in k8s.io/api, k8s.io/apimachinery, and k8s.io/go-client though.
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".
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.
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.
Writing APIs to deal with futures, etc, is much easier when you can chain parameterised functions together.
// Print has a type parameter T and has a single (non-type)
// parameter s which is a slice of that type parameter.
func Print[T any](s []T) { ... }
Call: Print[int]([]int{1, 2, 3})
Above, "any" is really just a synonym for "interface{}". You can have more restrictive type constraints on parameterized types by specifying other Go interfaces. This is vaguely similar to how Rust does it, and quite different from the C++ approach."This design does not support template metaprogramming or any other form of compile time programming."
[1] https://go.googlesource.com/proposal/+/refs/heads/master/des...
The `any` constraint is a synonym for the `interface{}` constraint.
When dealing with collections. It's maddening to have to keep duplicating basic functions like getting the keys from a map, or checking if a slice contains a given item.
It's pretty much always giant, complex class hierarchies or a bunch of reflection (or both).
That's why I feel generics are so important.
You can build complicated messes with any programming paradigm. It's a matter of discipline to use the tool correctly. Don't hate on generics, but rather the unskilled use of them (which I frankly see far less than abuse of other patterns/paradigms/language features).
The biggest negative with generics is compile time, but the clarity and conciseness of generics is worth it for me.
Over engineered how?
I fear that Go will eventually turn into something where we look back and realize we've lost something important by gaining a lot of less importants. The impulse to change things is just too strong these days. C89 has done just fine unchanged for 30 years.
All I want is a C+=1 I can rely on for the next 30 years.
For C+=1, I'd look at Zig; it unfortunately lacks the excellent corporate support that Go enjoys.
To me, Go looks much like early Java, only with a much better concurrency and saner "OOP". If anything, generics made Java better in many ways, without sacrificing performance or usability. It took 7 years for Java; it's going to take closer to 10 years for Go, bur better late than never.
We can argue endlessly about which metrics of success are most important.
Zig looks cool. I've seen it mentioned a few times over the years. Looks like manual memory management is the default, yeah? That's important IMO if you're really trying to replace C. Rust is great but I just can't iterate fast enough (yet). Does Zig offer an optional GC?
EDIT: Also I never said Go is a good C replacement. But I'm finding it useful for some of those tasks and suspect it will be less useful for them down the road.
https://go.googlesource.com/proposal/+/refs/heads/master/des...
I, however, discovered Rust in the meanwhile. It has generics. And it is not too complex either and has quite a few other bonuses.
I'm a fairly late-comer to generics, I never programmed seriously in C++, I avoided generics in Java initially, and I wrote a lot of code in less statically-typed languages. Ever since the first serious talk of generics in Go 2.0 I've endeavored to educate myself and I now am very strongly in favor of them.
However, as someone who reads other people's Go code, I'm not a huge fan. One of the greatest things about Go is that a developer can usually one-shot read and understand almost anybody's code because there's a simplicity "forcing-function" applied to everything. To lose that would be a shame.
you know what's hard to read? hand-rolled for loops & select blocks combined to wrangle concurrency.
the no1 benefit of parametric polymorphism in Go is going to be abstracting over concurrency
Is there any concise history of these attempts (including this one)? I'd like to understand the gist of these proposals and what ultimately derailed them.
By themselves these proposals are pretty inscurtable.
They also switched from parenthesis () to square brackets [], thankfully.
The previous one was confusing as F and the antithesis of the simplicity Go claims it abides by. It felt very much like a plot to add generics without ever using the word generics anywhere and looking too much like Java/C#/...
The current proposal is basically what you'd expect from generics in a programming language, but a bit more limited.
It took basically 10 years, a generation of developers, to quell the opposition against generics in Go, to end up with generics...
They might even have unknowingly followed the ADA implementation except that Go's type inference makes them even easier to use.
> To use a generic type, you must supply type arguments. This is called instantiation.
https://go.googlesource.com/proposal/+/refs/heads/master/des...
This is basically how generics as packages in ADA works. I would add that ADA solved many existing problems in Go decades ago...
https://en.wikibooks.org/wiki/Ada_Programming/Generics
> The generic procedure can be instantiated for all the needed types.
Now all Go needs to do is to look at how Ada tasks work in order to fix every single issue with Go routines...
IMO This is something that can't be done by a committee of so called industry players.
From skimming here: https://go.googlesource.com/proposal/+/master/design/go2draf...
They don't feel like generics in for example Java (my Java is rudimentary), but rather like an abstraction over interfaces.
Can anyone elaborate on this?
Go is a perfectly fine middleware language, but no C replacement. (Rust does much better in that regard).
Simplicity: yes, but in a senseful manner. Omitting generics is not senseful to me.
Basically, I want fast statically typed python with better package management. Or other way to put it, I want Go with classic OOP and Exceptions.
- Python's OOP, stdlib, exceptions
- Go's fast compilation and binaries
- Rust's package manager (never used but I heard it's amazing)
- Java's third-party packages & tooling
- F#'s expressiveness, FP, type system (never used it, only read a few intro articles, but it looks very nice?)
I'm tyring to move on from Python but finding it difficult to decide what to learn next.
Really? This is how you form your opinions?
† Deliberate use of a neutral adjective.
After many years chasing the newest, shiniest and best tools I can't appreciate stability and a large and useful standard lib enough. I finally understood the importance of the boring stack.
I don't need generics in Go, but I'm happy they are coming. Especially for collection methods.
It's strange that they don't consider Print[T](x T) instead of Print[T any](t T). The "any" could just be omitted without loss of anything. Especially since repeated types with the same constraints indeed DO omit it! Print2[T1, T2 any]
The question of what the syntax should look like has been beaten to death.
Wait... But why?gif
Go -> Guava -> Java
I mean, seriously,
func F[T any](p T) { ... } template<typename T>
void F(T p) { ... }As soon as you see Enterprise Go, it's over.
But yeah, I like to think of Go as a Java for the new millennium.
We're a Java shop and lots of people hate some of the newer changes to the language. And how OOP focused it is. I think Go would be a better fit because it seems to match the philosophy of our team more. But don't really think it's worth the switch for us.
FWIW, I don't think this impulse is there with the Go team. Go progresses quite slowly, from what I've seen as a user over the past 18 months. Generics have been in discussion for a long time with multiple implementations and no real rush to "just ship it".
The worst thing we can have on any HN thread is a debate about the virtues of Rust vs. Go. They are different languages with somewhat different long-term goals and very definitely different short-term goals, and these threads are never interesting in anything but a sort of sporting event spectator way.
I will just say that while there are a lot of things I like more about Rust than about Go, generics in Rust come at a cognitive cost. They're infectious; they don't get used the way people say they need them for Go ("I need to be able to sort arbitrary things and have sets of arbitrary types"); they're as fundamental to Rust as interfaces are to Go. It adds a lot of additional indirection.
I'd say: If you can code in C#/TS (or anything like) + go, then it only depends if you have a free weekend.
Also, Rust struck me as quite a lot more challenging before non-lexical lifetimes and rust-analyzer, so it's possible that you're responding to outdated criticism.
Consider initialization: C++ has dozens of different ways of initializing objects that in turn interact in complicated ways with move semantics and references and so forth. Rust's initialization story by contrast is straightforward: you just make the thing you want to make using struct or enum literals and maybe wrap those initializers in functions if you want.
- 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.
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#
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?
Like you said, We stop the go routine by telling it to exit the function, it's quite straightforward I think. Not more than a channel and switch case (+ context)
If you use vanilla Rust, it honestly feels quite high level, almost like Ruby or Python.
Most of the time your IDE's messages (or "cargo check" output) is sufficient to find all the things the compiler will complain about.
I usually find that once I've fixed all of those, I compile it once and it just works.
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.type A[T] int
Is that a parameterized type named A with a type parameter T, or is it a definition of an array named A whose length is T?
Separately, it's nice that type parameter lists use the same syntax as non-type parameter lists.
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.
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.
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.
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.
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 think one think that could seriously be improved is high-quality explanations of Rc, RefCell, etc, and where and why they are used.
I have found myself piecing together explanations from various books, Reddit threads, etc just to try to wrap around these.
I also think a lot of it has to do with the culture of the languages. Kotlin is a pretty nice language, but using it for Android still makes me want to hit my computer with a hammer because the over-abstraction of the Java ecosystem is maddening.
The big issue is that the ecosystem is kind of stuck; it's freaking 2021 and we're still targeting Java 8. Also the type system could be better, null references are everywhere even though there is Optional<T> (I wish it had optional strict null checks a la Typescript, but I'm not sure how realistic that is), and the fact that you can't have List<int> because generics won't work on primitives is just boneheaded.
But yeah, the language isn't the "Dog extends Animal" bullshit they teach you in school, at least not anymore.
Go doesn't suffer from this yet because it's too new and the philosophy is different which keeps some of it at bay, but it's only a matter of time. Entropy always wins.
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. 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.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.
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.
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
However you can litter your code with unwrap and clone to reach the finish line quickly, but then you lose the two main value props of the language and likely lose performance and runtime consistency over other languages.
That's why is better to learn Rust coming from a C++ background.
(And in my experience this mental model can also be of value even in languages were memory management is handled by the language runtime)
Generics in Go are vastly different than templates in C++. They might be used for similar things, but whereas Go's generics actually build up on Go's structural typing, templates are ... something completely different again.
I mean; C++ templates are Turing complete. They are in the same ballpark as Scala's type machinery. And I say that with adoration.
that's not a very high milestone to achieve. Even java generics are turing complete (https://arxiv.org/abs/1605.05274)
The older I get, the more icky I find subtyping. It just makes things messy...
template<SomeConstraint T>
void F(T p) { ... }
or just void F(SomeConstraint auto p) { ... }
like this for instance: https://gcc.godbolt.org/z/hPM38TThe only real cases I can see is creating new data structures, (for instance if you wanted to create your own map type).
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 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, …).
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?
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!
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.
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.
Edit: formatting
* calloc would a;so accept a typename T and count and return T[count]*
* Generic containers could be implemented as something closer to C++ templates (or preprocessor macros on steroids, like T4, as you suggest).
IIRC, historically void* didn't exist, and it used char*. It added void as a unit type, and absent historical baggage could add... let's call it "noreturn" as a bottom type:
#define NULL ((noreturn*)0)
noreturn* malloc(size_t);
void free(void*);
noreturn exit(int); // never returns
int main() {
// where (T)... is any expression of type T
void x = (T)...; // throw away the value
T y = (noreturn)...; // a nonterminating expression
void* p = (T*)...; // pointers convertible *to* void*
T* q = (noreturn*)...; // pointers convertible *from* noreturn*
*(noreturn*)...; // notionally, this should always fault
*(void*)...; // read zero bytes, so always fine
}Rust is a sane C++: Zero-overhead abstractions, not afraid of language complexity.
Go is a modern C: Simplicity, stability.
They have some overlap in what they're best at, but they both take on unique and important missions that both need to be targeted, in our rapidly expanding universe of software engineering.
(edit: formatting)
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.
Generic maps as sets are only ok for membership checks.
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.
[0]: https://rust-lang.github.io/hashbrown/hashbrown/hash_set/ind...
* 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 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!)
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.
Basically I get the high level abstractions and package management that I expect from Python, while inheriting a set of tools that help finally realize some of the high performance, zero-copy idealism of C++ (slices, lifetimes).
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?
The fact that something is a type variable makes it clear the that type of that thing doesn't matter.
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.
I don't know why you're all so scared of a little repetition ;)
if _, ok := wasTouched[thing]; !ok {
touch(thing)
wasTouched[thing] = struct{}{}
}
is way uglier than if !wasTouched[thing] {
touch(thing)
wasTouched[thing] = true
}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.
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.
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.