Hacker News new | past | comments | ask | show | jobs | submit login
An Introduction to Generics (go.dev)
220 points by mfrw on March 25, 2022 | hide | past | favorite | 63 comments



An example of something that I couldn’t do with earlier non-generic versions of Go: implement the lodash functions in Go with a nice DX[1]. I kept using []T though, and the article has a good example of how to improve that to work with slice based type, not just slices.

So many simple daily use methods like map, forEach, any, all, reduce etc feel much better now.

On the whole generics here seem neat, well thought out, easy to understand and usable, while still allowing you to completely ignore them if you want. Most declarations fit neatly into libraries, with usage often never even having to mention or even know about type declarations (See the examples on the lib). That was a nice change from Java - the inference is pretty nice and neat from what I’ve seen so far.

I’m sure we’ll find it being pushed to its limits, like with ORMs and multi chaining, and that might be ugly, but on the whole I’m very satisfied with this addition.

[1]: https://GitHub.com/sudhirj/slicy


Please don't slavishly copy Java/JS idioms. Go is not a language with a JIT and does not have a GC designed to handle lots of garbage; it assumes you won't allocate unless you need to.

For example instead of

    func Drop[T any](array []T, n int) []T {
        if n > len(array) {
            n = len(array)
        }
        return array[n:]
    }
Consider

    func Drop[T any](array []T, n int) []T {
        if n < len(array) {
            return array[n:]
        }
        return nil
    }
Which can collect the underlying array sooner.

Instead of your Chunk implementation (which, I want to offer, is better than most), consider an iterative zero-alloc version:

    func Chunk[T any](array []T, n int) (chunk, remaining []T) {
        return Take(array, n), Drop(array, n)
    }
(And not performance-related, but instead of indices in `FindIndex` etc., consider returning pointers; they can then generalize more easily to non-slice sequences, or anonymous slices.)

(And please don't call slices arrays.)


Thanks, Will keep this in mind when I do the perf edits and benchmarks. And yeah, didn’t want to confuse slices arrays, but it’s weirder to call them slices.


There is already an experimental slices package btw. It will be part of the standard packages once the finalize it.

https://pkg.go.dev/golang.org/x/exp/slices


Yeah, I’m using and delegating to it when possible. I don’t expect it to be as exhaustive as the lodash set though.


Everyone has strong opinions about language features. Personally, I'm happy that we had an imperative, statically typed mainstream language without generics for a long time. It really showed how much you can achieve without it in practical terms. Obviously, interfaces and built-in generic types like maps & slices, helped compensate a lot, but it's a quite simple feature set. I feel like I have a much clearer view today on when generics really shine compared to other alternatives. I hope, going forward, that responsible idioms around generics will be established in the Go community.


We already had this language: the early versions of Java (1.4 and before) had no generics. Go always has annoyed me because the core language team comes across as repeating all the language design mistakes of early Java and other languages that treat their users like children.


What mistakes are you thinking of? To me Go and Java seems quite different as fas far as imperative languages go, but I don't know Java and it's history deeply.


The old Java collection APIs were hampered by a lack of generics, so they tend to work in terms of the Object class (basically Java’s equivalent to Go’s interface{} except nominally rather than structurally typed). This means that the type system can’t encode and propagate constraints on the types of objects in collections, forcing casting and other things that lead to runtime errors. I’ve never personally seen a usable collections API in a statically-typed language without generics.


Additionally to the lack of generics, all references/pointers in Java/Go can be null/nil (so you can't express that a reference points to something) and all fields are zero-initialized (so having every field set to zero must be a valid state for your object). The former was - in reference to ALGOL W - called the billion dollar mistake.


Hehe, yes that's a classic.


> Personally, I'm happy that we had an imperative, statically typed mainstream language without generics for a long time. It really showed how much you can achieve without it in practical terms.

Didn't C already teach us this decades ago?


    type Ordered interface {
        Integer|Float|~string
    }
I kind of despise the keyword / syntax design choice of the tilde (~) signifying "anything where the underlying type is e.g. a string".

Usually a ~ signifies a bitwise-NOT operation*.

It reminds me of something more like a Ruby design mentality, where optimizing for code terseness is chosen. The nice thing about the crazy syntax in Ryby is that at least the chosen symbol is unique and doesn't conflict with meaning in other similar languages.

I realize the context where it's used is not part of the logic-execution pipeline, but it still forces me to contort my mind and special case this concept, and map it against what is effectively a namespace collision between Go and other C-style languages.

Naturally it's too late for Go (Generics are fully baked and released), but would have been nice to land with something more intuitive or at least less collision-prone, even if only to avoid the ambiguity.

Inevitably, the introduction of generics means increased opportunities for new kinds of complexity, and this choice needlessly increases the cognitive load when trying to read and reason about a go program.

I know it's the the end of the world, but I find it somewhat of a bummer for a technology I was previously so enthusiastic about.

* edit: Thank you Jtsummers for the correction- it is bitwise-NOT, I had mistakenly written bitwise-OR.


> Usually a ~ signifies a bitwise-OR operation.

That sounded wrong to me, I recalled it being a NOT, and double checked. Turns out my recollection was correct (it's also not something I ever used much), it is the bitwise-NOT in C.

That actually makes it a bit more awkward, in my mind, as I initially parsed the example as "not string". However, ~ is also often used to mean "about" or "approximately". In a text exchange:

"How many people will be at dinner?"

"~5"

Meaning "about 5 people" or "approximately 5 people". So this also works in that sense, ~string can be read as "something in the neighborhood of a string" or "something like a string".

https://en.wikipedia.org/wiki/Bitwise_operations_in_C


~ is the similarity sign as mathematical symbols.


Agreed, this is a slightly better way to think about it, yet still seems out of place for Go. Where else does Golang diverge from what an operator means in C?


For one, in the operator in question: bitwise negation in Go is expressed using prefix ^ rather than ~ as in C. Go also differs from C in operator precedence, which I would argue is a significantly larger change than the visible syntax for an operator.


++ and -- are also statements rather than expressions (and =, but lots of languages diverge from C on this).


Well, * can already mean any of "pointer to" (in type context), "dereference" (as a unary prefix operator) or "multiplication" (as a binary operator)...


> Usually a ~ signifies a bitwise-NOT operation*.

Only in expression context. C doesn't assign any meaning to ~ in type definition context, nor does it have an existing way to represent the concept of "any type reducible to X". Since it doesn't diverge from C in either syntactic or semantic meanings when considering context, I don't see this being an issue in practice. Any potential confusion argument you can lay onto ~ could also be used against |, but I don't see any mention of that being a problem.

I can't "disagree" with your reaction -- that would be silly even if I wanted to -- but I claim that you would likely stop suffering from increased mental effort as you read and write this syntax over time.


If you ask most programmer, ~ is definitely not a bitwise-NOT operation.


It's the bitwise-NOT operator in C, java, and python just off the top of my head, I think most people are familiar with it


In Go we use ^ for bitwise-NOT.


Surely that is xor? Ah I looked it up and it is xor, but unary ^ is NOT. Interesting overloading.


We must be careful to not conflate our distaste of highly abstract OOP code with generics. Generics and OOP are orthogonal. If you've used a map or a slice in Go, you've used generics. If you've used a union in C, you (sort of) have used generics.

I bet most complaints we hear about generics in Java, have little to do with generics themselves and mostly to do with inheritance, abstract classes, and so on.


Most "complaints" about generics in Java have to do with type erasure. It's a tradeoff at the end of the day, it opened up the JVM to host many high quality languages like Scala, Kotlin, Clojure, etc. Haskell also does type erasure as far as I'm aware. Furthermore, the JVM is now getting generic specialization for value types, so types like HashMap<int, Object> become viable without the cost of boxing.

Inheritance based languages enable covariance and contravariance in generic types, something that comes in handy in many occasions.


“Haskell also does type erasure as far as I'm aware.” It’s not exactly an applicable term. Indeed Haskell doesn’t let runtime bother with a lot of information that it can provably dispense of. But there’s no laxity tradeoffs as in Java, Haskell won’t let you use a wrong type in a container


> Haskell won’t let you use a wrong type in a container

Could you elaborate? Java should throw a runtime exception for mismatches.


The key word in your post is "runtime". Haskell will prevent that bug at compile time instead.


Actually, I was thinking of covariant arrays. The compiler should prevent issues at compile time for generic collections, same as Haskell.


That’s false. Java’s generics are sound, you can’t introduce runtime type errors without casting (which is also true of haskell)


As a pointless pedantic nit, Java's type-system is unsound: https://io.livecode.ch/learn/namin/unsound

But realistically, the above doesn't matter.

What actually matters is that you often _do_ have to cast at runtime in java, so it's somewhat common to hit type errors at runtime in practice. Haskell's type-system makes runtime type casting both less necessary, and vanishingly rare, meaning such runtime cases are practically never hit.

The above is at least true from my personal anecdotal experience.


As far as I know that unsoundness “hack” no longer work.

Hm, could you please show me where you had to cast? Long ago, some resource/service lookup did require casting, but my anecdotal experience is that there really is no common area where I have to cast anything anymore.


If a language erases all types, like Haskell, it's fine. If a language doesn't erase any types, like C#, it's fine. If a language erases types in some places but not others, like Java, you get all sorts of awful problems.


Could you provide examples of such? I'm not aware that the Java team mentioned any when positing the primitive specialization design.


No type definitions for methods :(


Quite typical of golang pushing more complexity onto the user in order to save complexity in the compiler, then claim that golang is a "simple" language shrugs.


While it’s fun to hate on Go and the language has indeed made some bizarre design choices, the fact that there’s even a market for Go shows that there’s some major deficiencies in other languages.

If Go is really so bad, why don’t Go programmers use e.g. Rust? I’m sure they’d cite things like:

- Compilation time is much faster in Go.

- Complexity. Rust is a highly complex language even for me as a C++ programmer. For those who are new to compiled languages they are certainly going to choose Go over Rust

- GC definitely has development time benefits over a borrow checker if you can afford the extra CPU headroom and memory usage

- Go’s concurrency model is more appealing to many people

The real question is, why is there not a “Go but designed by sane people” language?


golang doesn't really compete with rust, but it's more like a faster python/ruby/js. You can see this by the companies that have moved over from those languages to golang. Hype definitely plays a role, but also the perception that it's quick to pick up, and does decently well when it comes to performance.

> The real question is, why is there not a “Go but designed by sane people” language?

The fact is that the JVM and .NET platforms would almost surely do better for anything that golang is considered for, but there's a lot of hype and companies want to attract programmrs.


The real question is why not just use Java, C#, D, Haskell, Js or the myriad other managed languages?

Rust is complex due to the inherent complexity of being a low-level language.


Let’s not be so simple minded. Users bear some of the cost of complex compilers, too. And complex languages, even if they make writing some code simpler, ALWAYS have a cost. It’s a balance, there’s not an obvious right answer.


Methods exist to implement interfaces. Interfaces cannot have generic methods because those would need to be instantiated at runtime, which would require runtime codegen.


Why do you need runtime codegen? What exactly needs to be "instantiated"? Ultimately the runtime representation of a type parameter comes down to sizes and offsets. Why not have the caller pass those values into the generic method?



Ugh yeah. The ability to check at runtime if a value satisfies an interface, combined with structural typing...

So now the static expressivity of the type system is compromised in the name of runtime introspection. Reminiscent of how when Java added generics, runtime introspection ended up totally blind to them due to erasure.

This seems like the result of not taking care to account for the possible future addition of generics when originally designing the language — it was always well understood that they’d be a likely later addition to the language. The ability to check if a value satisfies an interface at runtime doesn’t seem all that critical, although I could be missing something, I’m not a regular user of the language.


It's not as common as unwrapping to concrete types, but still frequently used.

One place it's absolutely critical, and certainly the most common by number-of-calls even if people don't think about it, is `fmt` functions that check to see if the type implements `String() string`.

An idiom found in many data-shuffling libraries is to accept a small interface like `io.Writer`, but have fast-paths if the type also implements other methods. E.g. https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/io...

It's a little annoying but I don't think it's as bad as Java's erasure. We're still very early in idiomatic Go-with-generics so maybe we'll see a huge impact from this limitation later, but most times I see people wanting generic method parameters, they've got a design in mind which would be horribly inefficient even if it was valid. If you are going to box everything and generate lots of garbage, you might as well write Java to begin with.


For the case of coercing a specifically a function argument to a different interface, there is a solution that isn't stupidly inefficient, but it's impossible to support for the general case of coercing any interface value to a different interface. I expect 90% of the time a function coerces and interface value to a different interface, that value is one of its arguments, but to support that case for generic interfaces but not the other 10% would be a really ugly design. I think you'd also have issues with treating functions that coerce their arguments to generic interfaces as first class functions.

    package p1
    type S struct{}
    func (S) Identity[T any](v T) T { return v }

    package p2
    type HasIdentity[T any] interface {
     Identity(T) T
    }

    package p3
    import "p2"
    // Because parameter v might be (and in this case is) coerced into
    // p2.HasIdentity[int], this function gets a type annotation in the compiler
    // output indicating that callers should pass a p2.HasIdentity[int] interface
    // reference for v if it exists, or nil if it doesn't, as an additional
    // argument.
    func CheckIdentity(v interface{}) {
     if vi, ok := v.(p2.HasIdentity[int]); ok {
      if got := vi.Identity(0); got != 0 {
       panic(got)
      }
     }
    }

    package p4
    import (
     "p1"
     "p3"
    )
    func CheckSIdentity() {
     p3.CheckIdentity(p1.S{})
    }


I'm not sure this approach would correctly handle the case where the function is oblivious to generics but receives an object where a generic method must be used to satisfy the interface. E.g. a function taking an `any` parameter, checking if it's an `io.Writer`, passed something with a `Write[T any]([]T) (int, error)` method. Intuitively you would expect that to "materialize" with `byte` and match. And as the FAQ says, if it doesn't, then what's the point of generic method arguments since methods exist primarily to implement interfaces?


It would be up to the caller which knows the concrete type of the value getting passed as `any` to provide an interface object with a function pointer to the concrete instantiation of `Write[T]` for T=byte. If the immediate caller also doesn't know the concrete type, it would also get that from its caller. It's ultimately very fragile, since there are definitely cases where at no point in the caller chain does anyone statically know the concrete type (like if this value is pulled from a global variable with an interface type).

I think it would be terrible to include in the language because of the inconsistencies, but it is possible to make the two examples you listed as typical cases of interface->interface coercion work with generic methods, ugly as it may be.


Nice find.


As someone working to see where generics would benefit my actually-existing go projects and not starting with the assumption that something like lodash would be good, this has not been a problem at all.

"No way to require pointer methods" has been much more frustrating to work around. Even the part that works ("Pointer method example") sucks right now because type-type inference was disabled shortly before the 1.18 release pending some bugs. Hopefully once the basic ergonomics are back this can be reconsidered.


I really appreciated Go for its simple rules. It doesn't need these new features. You just surround the pieces.


You aren't forced to use these features. You can ban in your own code and just reap the benefits of libraries written with generics. You were already benefiting from generic code in the language (maps, slices, channels, etc.) anyway.


Generics feel like OOP creeping back into popular programming culture.

Everything just goes in a big circle. OOP is the bees knee's for like 20 years and then everyone decides that OOP is the devil for unnecessary abstraction. Then everyone gets all exited about functional programming, but they miss some of the convenience of abstract classes and inheritance.

After developers make sufficient noise, we wind up with effectively the same OOP functionality *GASP. We just avoid words like "class"....

EDIT: You guys are right about generics in FP. I guess my larger point is that lots language paradigms provide the same features just with different names and backlash is silly.


Generics are wildly, WILDLY popular in functional programming - far more so than in imperative flavours of OOP, really. Basically every functional data structure is full of things like List[A].map(func: A => B): List[B]

Also, it’s really not “OOP or functional”, there are tonnes of languages mixing OOP and functional very effectively (Scala is both very functional and very object oriented, but honestly most languages are a mix to some degree). Functional vs. imperative is more of a divide.


And yet, FP remains less popular overall than OOP (personally, I love love FP :).

Many of the people targeting the most popular FP language of all time aren't writing the Javascripts because it's their first choice. Just look at the pretty WILD popularity of Typescript, an OOP bolt-on language which transpiles to Javascript.

The simpler (in some ways) imperative programming model still dominates developer mindset.


TypeScript bringing a half decent type system to JavaScript is orthogonal to OOP

JavaScript is OOP already. TypeScript just provides better typing like what you would find in Haskell, Ocaml or other functional languages


You are technically 100% correct, however the bultin object system in Javascript is so bad that a reasonable argument can be made that it's not particularly accessible or ergonomic to many of the folks who actually write it for a living.

I know lots of folks here on HN are super highly competent, and this contrasts with my IRL experience with colleagues who are less passionate about programming and just want to do their job- these folks aren't going to be teaching lessons in proper usage of objects in JS.


It’s hard to tell, but are you referring to prototypical object inheritance here? If so, JavaScript has long since implemented more traditional class definition ergonomics, such that explicit prototyping is almost largely unused in modern codebases.

In truth, even classes are becoming increasingly uncommon in JavaScript codebases, and most object usage in JS these days is struct-like, where the ergonomics are pretty similar (or identical) to other languages.


Generics have been used in functional programming before OOP and classes were but a twinkle in a researchervs eye


They've also been in procedural languages for over 40 years. Ada started with generics and the first spec was published in 1983.


Object-orientation has nothing to do with generics. Generics are static typing v2, object-orientation is an entire programming paradigm encompassing program design, idioms, the fundamental way you think about flow, etc., and doesn't require static typing v1 either. JS, the most object-oriented language under the sun, doesn't have generics; C, the least, does.


Generics has nothing to do with OOP




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: