22 ms·
When to use generics
- Groxx 5y ago>For type parameters, prefer functions to methods >The Tree example illustrates another general guideline: when you need something like a comparison function, prefer a function to a method. >We could have defined the Tree type such that the element type is required to have a Compare or Less method. This would be done by writing a constraint that requires the method, meaning that any type argument used to instantiate the Tree type would need to have that method. Yes yes a thousand times yes. If you have an interface with a single method, or something with a single varying piece of behavior, please strongly consider accepting a function instead. Going from method to function is trivial - pass `thing.method`. Going the other way is impossible at runtime, it requires package-level code at compile-time. This is true for many/most? languages, but it feels like a lot of Go libraries have forgotten it, and they're sometimes a huge unnecessary pain to deal with for that reason.
- cube2222 5y agoAgreed. I almost never use sort.Sort in practice, sort.Slice is just so much more usable. Also, based on the recent blog post[0] a method-based approach will be a tiny bit slower than the function-based approach. Not that it matters for most code though. [0]: https://planetscale.com/blog/generics-can-make-your-go-code-slower https://planetscale.com/blog/generics-can-make-your-go-code-...
- staticassertion 5y ago> This means that a time.Time pointer has the same GCShape as an uint64, a bytes.Buffer and a strings.Builder. > In the current implementation of Generics in 1.18, every runtime invocation of a generic function will transparently receive as its first argument a static dictionary with metadata about the arguments being passed to the function. > That’s right, after the monomorphization step, the generated function shape needs to take as a runtime input the virtual method tables for all its generic arguments. What an odd design. I'm curious to read the justification for this. I assume it's faster compile times.
- shp0ngle 5y agoYeah, exactly that. Faster compile times.
- kgeist 5y agoAnd also smaller binaries. For comparison, our templates-heavy C++ project's binary is 1 GB in debug mode and 200 MB in release mode. It takes 10-20 seconds just to link.
- deleted 5y ago[deleted]
- staticassertion 5y agoAll of that gets removed though
- nyanpasu64 5y agoAgreed. If I could retroactively alter Rust's design, I'd remove non-marker traits with methods, and instead have functions and data structures take method or vtable pointers. In case of static dispatch, the method pointers would be passed as compile-time const generic parameters. This allows picking different ordering operators for different trees or sorts defined on the same type, instead of requiring wrapper types defining custom comparison operators, or having individual types or functions add comparator parameters in an ad-hoc fashion (https://doc.rust-lang.org/std/primitive.slice.html#method.sort_by https://doc.rust-lang.org/std/primitive.slice.html#method.so...).
- twanvl 5y agoThe ergonomics of this are really bad without some kind of implicit parameters and trait resolution. Without trait methods, ad-hoc overloading becomes impossible. You would have to explicitly specify what + operator and what == operator you use everywhere. After all, there could be multiple different additions or comparisons defined for a type, so which one do you mean by `+`? Another problem is that a data structure's invariant can depend on the trait implementation. For example a tree is balanced with a comparison. This means that the comparison has to be stored in the struct, with the corresponding runtime overhead. With traits you know that there is only a single implementation for any particular type, so if the trait v-table is an argument to every method call there is no risk of different implementations being used at different times.
- zozbot234 5y ago> This means that the comparison has to be stored in the struct, with the corresponding runtime overhead. The runtime overhead is solvable in principle by making the comparison a const-generic parameter of the data structure type. But to do this properly requires dependent types, because the type of that const-generic parameter is taken from an earlier parameter in the same definition. It's a dependent sum type, sometimes called a dependent record.
- nyanpasu64 5y ago
- azth 5y ago> If you have an interface with a single method, or something with a single varying piece of behavior, please strongly consider accepting a function instead. Java neatly solves this by means of Functional Interfaces. Interfaces that have a single method related to the generic type parameter can be called by using lambda syntax. Take for example sorting: <T> void sort(T[] a, Comparator<? super T> c) A `Comparator<T>` has a method `int compare(T o1, T o2)`, which allows the caller to invoke `Arrays.sort` by passing a lambda: Arrays.sort(list, (l1, l2) -> l1.compareTo(l2));
- Groxx 5y agoI don't think I'd call this "neatly"... more "a weird but pragmatic hack to improve an incredibly common pattern that had no concise option prior to java 8". And it kinda encourages continuing the "keep declaring a lambda that could have just been a function reference if the API had used a function instead" pattern, which is not great. But I do very much like it. It improves Java's ergonomics quite a lot.
- layer8 5y agoWhat I like about Java functional interfaces is that they give a place to specify an interface contract. For example, with Comparator, it specifies that instances must implement a total ordering. Purely structural function types don't have that affordance. A slightly problematic point, however, is that you can assign a method reference from one functional interface to a variable of another functional interface based purely on structural typing, even if they happen to have incompatible interface contracts. For example, if you had a PartialOrder functional interface with the same parameter/return types as Comparator, then you could assign PartialOrder::compare to a Comparator variable although the semantics aren't compatible. Whereas nominal typing prevents you from assigning a PartialOrder to Comparator (no "duck typing"), Java circumvents the nominal type system here for method references, with the result that one may assign incompatible implementations without noticing.
- watwut 5y ago> keep declaring a lambda that could have just been a function reference Yes, but those are less readable, too much syntax and not enough javadoc/names on them.
- suzuki 5y ago> If you have an interface with a single method, or something with a single varying piece of behavior, please strongly consider accepting a function instead. Agreed. And if you prefer methods to functions, you can define methods on such a function type! In my LINQ in Go (https://github.com/nukata/linq-in-go https://github.com/nukata/linq-in-go), I define a higher order function type: type Enumerator[T any] func(yield func(element T)) and several methods on it, for example, // Where creates an Enumerator which selects elements by appling // predicate to each of them. func (loop Enumerator[T]) Where(predicate func(T) bool) Enumerator[T] { return func(yield func(T)) { loop(func(element T) { if predicate(element) { yield(element) } }) } } Naturally, if you need another type parameter for a method, you must define it as a function. For example, // Select creates an Enumerator which applies f to each of elements. func Select[T any, R any](f func(T) R, loop Enumerator[T]) Enumerator[R] { return func(yield func(R)) { loop(func(element T) { value := f(element) yield(value) }) } } Now, with the function which converts a slice to an Enumerator, // From creates an Enumerator from a slice. func From[T ~[]E, E any](x T) Enumerator[E] { return func(yield func(E)) { for _, element := range x { yield(element) } } } you can write the following: seq := Select(func(e int) int { return e + 100 }, From([]int{ 7, 8, 9, })).Where(func(e int) bool { return e%2 != 0 }) seq(func(e int) { Println(e) }) // Output: // 107 // 109
- beastman82 5y agoI'm curious how parametric polymorphism got the name generics?
- deleted 5y ago[deleted]
- whateveracct 5y agoIt's extra confusing because Haskell has capital-G Generics which are not at all this :)
- recursive 5y agoWhich is totally on-brand for Haskell.
- wk_end 5y agoThis lead me down an interesting path. So, first of all, my (possibly incorrect) understanding is that pure parametric polymorphism doesn't allow you to specify constraints - in Haskell terms, `a -> IO a` is a case of parametric polymorphism, but `Showable a => a -> IO a` or whatever isn't (entirely) - you've got ad hoc polymorphism in there too. So just calling it "parametric polymorphism" would be inaccurate. My assumption was that they took the terminology from Java, but as I looked more into it I started to doubt this. Java called them Generics because the implementation in Java 5 was descended from a research project called "Generic Java". The Generic Java paper [0] uses the term - and the term "Genericity" - freely but doesn't give an original source for it at a glance. They do note that "Modula 3, Ada 95, Eiffel, and Sather" all support them though. Wikipedia has an article on "Generic programming" [1] which notes that "[they] are known as generics in Ada, C#, Delphi, Eiffel, F#, Java, Nim, Python, Go, Rust, Swift, TypeScript and Visual Basic .NET", so this isn't exclusive to Go. It cites a paper from 1989 called "Generic programming" as an origin of the term, although it also claims that "Ada has had generics since it was first designed in 1977–1980", and indeed "generic" is a keyword in Ada 83 [2]. The "Generic programming" paper is written about Ada [3]; my hunch is that Ada's where the term comes from. I'd be curious to see if anyone can find an earlier use of that term. [0] https://homepages.inf.ed.ac.uk/wadler/gj/Documents/gj-oopsla.pdf https://homepages.inf.ed.ac.uk/wadler/gj/Documents/gj-oopsla... [1] https://en.wikipedia.org/wiki/Generic_programming https://en.wikipedia.org/wiki/Generic_programming [2] https://en.wikibooks.org/wiki/Ada_Programming/Ada_83 https://en.wikibooks.org/wiki/Ada_Programming/Ada_83 [3] http://stepanovpapers.com/genprog.pdf http://stepanovpapers.com/genprog.pdf
- candiddevmike 5y agoSo far, I'm planning on using them to replace interface{}. I wrote a stupid simple test assert library that abuses []interface{} for inputs/outputs with the assumption that a panic is fine because it'll make the test fail.
- kccqzy 5y agoTL;DR use it only when you need parametric polymorphism > If a function has parameters with those types, and the function code doesn’t make any particular assumptions about the element types, then it may be useful to use a type parameter. In some languages when a type variable is universally quantified the language simply doesn't allow you to make assumptions about these types. This is of course the right thing to do. > Another case where type parameters can be useful is when different types need to implement some common method, and the implementations for the different types all look the same. Looking the same is a clear indication that your code isn't making assumptions about the type. So this is just a restatement of the above point. Overall this is very good advice. At the risk of sounding elitist, I think this is something everyone should already know, regardless of whether they use Go or not. Of course the article is aimed at beginners, so there's nothing wrong with that, but at HN we can just use the proper terminology such as parametric polymorphism or a universally quantified type variable.
- uluyol 5y agoI think it's also aimed at the C++ crowd where generics = static dispatch and interfaces = dynamic dispatch and people try to eek out the extra performance.
- eru 5y agoKeep in mind that C++ doesn't provide for parametric polymorphism, one of the more important uses of generics. Parametric polymorphism gives you guarantees about your program for free. C++'s templates can't do that. In C++ even if the types look generic, your code can make assumptions about the types. Eg your generic code from some type 'a' to some type 'a' can implement the identify function for most types, but implement cosine when given a float. In something like Haskell (or Java, I think?) that sneakiness is not possible without clearly telling the user in the type. See eg https://bartoszmilewski.com/2014/09/22/parametricity-money-for-nothing-and-theorems-for-free/ https://bartoszmilewski.com/2014/09/22/parametricity-money-f...
- kaba0 5y ago
- enw 5y agoLast sentence covers it well: > Another way to say this is that you should avoid type parameters until you notice that you are about the write the exact same code multiple times. (The above will make design pattern astronauts shudder)
- eru 5y agoIt will also make eg Haskeller's shudder. Haskell (and languages with parametric polymorphism in general) can give you lots of guarantees for free when using generic types. Even if at runtime you only ever use your code with one specific type. See eg https://bartoszmilewski.com/2014/09/22/parametricity-money-for-nothing-and-theorems-for-free/ https://bartoszmilewski.com/2014/09/22/parametricity-money-f... To give a super simple example: if in Haskell you see a function with type 'a -> a', you know that it can only be the identify function, because it's generic over 'a', so the code can't do anything specific. In contrast, if you see something like 'float -> float' almost anything can happen (that doesn't involve side-effects). It might be identity, it might be sine, it might be square-roots, etc.
- lgas 5y agoWell, technically `float -> float` is still the identity function, but your point stands for `Float -> Float`.
- eru 5y agoIn my defense, I used pseudo-code that just happens to look a bit like Haskell. ;)
- Dylan16807 5y ago> To give a super simple example: if in Haskell you see a function with type 'a -> a', you know that it can only be the identify function, because it's generic over 'a', so the code can't do anything specific. So I can't have any conditionals or pattern matching based on a parameter's type, or provide default implementations of any functions?
- orblivion 5y agoAll of the "when it's useful" examples are either of `any` or `comparable`. There are no examples restricted to an interface (other than `comparable` if that counts). Only the "when it's not useful" examples include a generic restricted to an interface, `[T io.Reader]`. Since the language allows for restricting to an interface, and this is the language's official blog, surely there must be a recommended use case for it.
- Macha 5y agoI don't think the Go developers wanted to implement generics, but it was something they took a lot of heat for, and so while they relented and implemented the feature, they want to use their position to suggest "best practices" to limit their use.
- icholy 5y agoThe author of the blogpost has been making proposals for generics since Go 1.0 was released. Contrary to what some of the smooth-brains on here think, generics weren't implemented because of their incessant crying.
- pstuart 5y agojinx! you owe me a coke.
- zelphirkalt 5y agoUltimately a language lives through its users. If no one had complained about the lack of generics, the Go devs would probably not have liftet a finger to implement is, as per their original design.
- icholy 5y agoIt's like you didn't even read what I said...
- pstuart 5y ago
- MichaelMoser123 5y agoI used generics as an exercise: for a circular buffer [1] is that ok? The exercise project is a grep that greps through the content of archives [2] i needed a circular buffer to keep the last n lines for displaying the context of a match. [1] https://github.com/MoserMichael/rzgrep/blob/master/src/cbuf/cbuf.go https://github.com/MoserMichael/rzgrep/blob/master/src/cbuf/... [2] https://github.com/MoserMichael/rzgrep https://github.com/MoserMichael/rzgrep
- pansa2 5y ago> I used generics as an exercise: for a circular buffer [1] is that ok? I’d say that’s an example of “type parameters can be useful for general purpose data structures”.
- icholy 5y agoDon't make T a pointer.
- Groxx 5y agoFor a collection like this, definitely agreed. If the users of that circular buffer want to use pointers, they can just pass a pointer type. No need to force it on all cases.
- ByteJockey 5y agoI think they're using it so that they can use nil as the zero value to indicate the buffer is empty in cases where the zero value is a valid result. I feel like the comma ok idiom is a better idea here.
- Someone 5y agoI think so, too, but only because that’s idiomatic go. In general, it’s not a good idea to have functions that logically return a value or an error return a value and an error. If you use go, that’s what you do, though. I see that that was changed already (https://github.com/MoserMichael/rzgrep/commit/764b9ad61a5dbe6b23c97ce739ed770199e222c5 https://github.com/MoserMichael/rzgrep/commit/764b9ad61a5dbe...) I think I would not return “ctx.data[0]” for the value returned in case of error, though. That runs the risk of users processing items multiple times if they forget to check for errors. I would use either - a fresh new instance of T - a special instance of T created once (could be passed in to NewCBuf as a value with, to the user, obviously incorrect contents) The first is cleanest, but may not be performant if creating T’s is costly. The second may be risky if the returned items are mutable and the caller changes them.
- jamal-kumar 5y agoIMHO the biggest deciding use case for me is whether or not it's more performant than any other possible solution. So far I haven't seen this to be the case in other people's benchmarks. [1] [1] https://planetscale.com/blog/generics-can-make-your-go-code-slower https://planetscale.com/blog/generics-can-make-your-go-code-...
- turminal 5y agoBy that metric you're almost always better off writing specialized code all the time and duplicating stuff around a lot. (In all languages, not just go)
- mcronce 5y agoThis is only the case* when there is actually something to be gained by specializing the implementation of a data type or function for a given type. In my experience that isn't particularly common. * Excluding Go's implementation of generics, which are frequently less performant than even vtable dispatch, let alone manually copied implementations.
- jamal-kumar 5y agoIs that so? It kind of sounds like there's cases to consider where there's actually a performance gain, but in certain use cases in languages with implementations of this feature in a different way... From the article I linked: "Historically, monomorphization has been the design of choice for implementing Generics in systems languages such as C++, D, or Rust. There are many reasons for this, but it all boils down to trading longer compile times for significant performance gains in the resulting code. When you replace the type placeholders in generic code with their final types before the compiler has performed any optimization passes, you create an exciting universe of optimizations that are essentially impossible when using boxed types. At the very least, you get to de-virtualize function calls and get rid of virtual tables; in the best case scenario, you get to inline code, which in turn enables further optimizations. Inlining code is great. Monomorphization is a total win for systems programming languages: it is, essentially, the only form of polymorphism that has zero runtime overhead, and often it has negative performance overhead. It makes generic code faster. So, as somebody who works on performance for large Go applications, I admit I was not particularly excited about Generics in Go, really. I was excited about monomorphization, and about the potential for the Go compiler to perform optimizations that it simply can’t do when it’s dealing with interfaces. Cue my disappointment: The Generics implementation in Go 1.18 does not use monomorphization… at least, not quite."
- shpongled 5y ago> Let’s start with a general guideline for programming Go: write Go programs by writing code, not by defining types I find this interesting - I think I typically do the reverse: I generally start by thinking of my program in terms of types and then write code to perform operations/transformations on those types (obviously this doesn't work 100% of the time). That being said, I have never written any Go, but I can't imagine I would approach it differently.
- obphuscate 5y agoI generally think type first in Java and most other languages purely by convention - it is just a social and programmatic norm. That being said, Go's conciseness lends itself to creating working code quickly and generally the type system aims to just be "good enough", broadly trying to force you to get working code first and invent the types as they are opportune. This is also why (aside from fear of hurting compilation speed) that generics were only just added after many years; which some people, myself included before I actually used Go and loved it - think is needed to use a language for anything beyond "toy" purposes (you don't actually need generics most of the time). In practice this works out really well (especially since with Go you never need to extend interfaces, you automatically are known to implement an interface implicitly by just possessing the same function signatures an interface possesses), and is liberating and freeing: I don't go down a rabbit hole of thinking I need to support future cases and define AbstractAnimal, AbstractMammal extends AbstractAnimal, Dog extends AbstractMammal. This also highlights the other side benefit - since you aren't thinking what each object's operations are you can use composition and define the traits that a Dog possesses, and lo and behold later, since those trait interfaces are decoupled you don't have to repeat them if you realize Snake can implement Hiss() just like Cat can even though they both might traditionally extend and inherit down orthogonal branches (where you would have to duplicate or do wonky shenanigans like attach it to AbstractAnimal and return UnsupportOperation where not actually implemented). The bottom line is it forces you to not even have to think about YAGNi because you compose only what is needed to accomplish each task on the fly.
- kaba0 5y agoThat’s not much different than just using interfaces/traits though (with the added benefit or hindrance depending on person of Graph::walk may be different than Animal::walk)
- tastysandwich 5y agoI code in Golang daily. Generics are awesome! Some simple examples: // returns a pointer to v func ToPointer[T any](v T) *T { return &v } // returns a given default value if v is nil, otherwise v func DefaultValue[T any](v *T, defaultVal T) T { if v == nil { return defaultVal } return *v } Although a quick grep shows I mainly use these when writing tests. I hope they allow type aliases on generics soon. in package a: type ASomething[T any] struct {} in package b: type BSomething = a.ASomething // doesn't work type BSomething[T any] = a.ASomething[T any] // doesn't work I found a need for this recently when updating a bunch of code in a common package I have to use generics. The above type alias would have meant I didn't have to update any code that uses that common package - the transition to generics "under the hood" would have been seamless. Oh well, nothing sed can't handle.
- xigoi 5y agoNot knowing much about Go: Wouldn't ToPointer create a dangling pointer, because `v` is on the stack? Or is it automatically copied to the heap?
- cyphar 5y agoGo has escape detection, so any returned pointer to a stack allocated variable becomes heap allocated. The code posted is a fairly common way of taking a stack allocated primitive type and turning it into a heap allocated one.
- eru 5y agoOr coming from the opposite side: you can say that Go has escape analysis, so that it can detect when variables can safely be allocated on the stack. The stack is an implementation detail that's purely about optimization. (I don't know for sure whether it's safe to thing like this in Go. It's safe to think like this in most Lisps, OCaml or Haskell.)
- Dylan16807 5y ago
- kgeist 5y agoI can only remember three cases in our codebase (30+ microservices) where we suffer without generics: 1) we have to write many duplicate functions just to check if an element is inside a slice 2) slices are not covariant, so we have many duplicate functions just to convert something like []MyObject to []MyInterface 3) we have to resort to using slices/maps for most data structures, it's mostly good enough, but the intention is less clear (for example, using a map for a set, or a slice for a queue) and it's less performant than more specialized data structures Looks like most of it will be covered by the new slices package and various third-party collections packages which are going to proliferate. We don't use 1.18 in production yet however, because of poor tooling support yet (including linters).
- sudhirj 5y agoMy personal experience with generics was to port a parallel circular queue library (used to take interface{} and require a type cast at every use) add an experiment to build all the Lodash slice and collection functions in Go. Happy to say that both worked really well. Took me less than an hour to understand the system enough for these purposes, and the implementation hides neatly in the library - the code that uses the library has absolutely no mention of anything generic. Having generics also helped switch to cleaner implementation inside as well, because I could now use maps as well without type casts. Looks like a solid useful system - I could do everything I wanted, all the complexity stays in the library, and changes were pretty minimal and easy to understand. Would recommend. https://github.com/sudhirj/cirque https://github.com/sudhirj/cirque https://github.com/sudhirj/slicy https://github.com/sudhirj/slicy
- eyelidlessness 5y agoSo… use generics the same way they’re used in other languages? I’m trying so hard to watch this evolution in Go with an assumption there’s careful consideration of something other languages have gotten wrong and it seems like the whole result is… use generics within the language constraints in ways that aren’t problematic. I mean, Go already isn’t TypeScript (which does this great despite complaints). Go on, Go folks. Use generics. If you’re using them wrong you’re gonna get much more direct criticism than general guidelines to use the platform.
- GeneralMayhem 5y agoSome of it is definitely trying to walk back the refrain that Go is simple and simple is good, which implies that anything that isn't currently in Go is complicated and therefore bad. If you repeat that too many times, and people buy into it, it becomes hard to roll out any new feature without being a heretic. But - there are some things about Go generics that are particularly weird. The biggest one is that, in a lot of cases, they can actually just be another way to write runtime polymorphism, not static polymorphism as in languages like C++ (https://planetscale.com/blog/generics-can-make-your-go-code-slower https://planetscale.com/blog/generics-can-make-your-go-code-...).
- paskozdilar 5y ago> But - there are some things about Go generics that are particularly weird. The biggest one is that, in a lot of cases, they can actually just be another way to write runtime polymorphism, not static polymorphism as in languages like C++. Isn't static polymorphism the whole point of generics? Runtime polymorphism was already possible through interfaces and type switches. Also you can't type switch a generic variable, so runtime polymorphism with generics seems literally impossible.
- excuses_ 5y agoI wouldn't say it's the whole point but very important one. For simple types the current Go implementation does produce monomorphic functions but for others, for example interfaces and other pointer types, it does pass around a dict with metadata in runtime. Check out this excellent blog post: https://planetscale.com/blog/generics-can-make-your-go-code-slower https://planetscale.com/blog/generics-can-make-your-go-code-....
- Taryns 5y ago[dead]
- MrYellowP 5y agoCan someone please explain to me of what use generics actually are and how they create faster code or improve the typing-workflow? I'm serious, I have no idea. Googling it, I get: > In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. As someone who believes that OOP is bullshit and making things worse instead of better, can someone please put the above paragraph into humanly understandable words which translate into actual practical gains and improvements? Like ... actual use cases, maybe? Thanks!
- Genmutant 5y agoAn use case that comes up quite often is building a List / Map / Set implementation. You want to have it to enforce to (typesafe) take only objects of the same type, and when taking it out again, get the object in the same type back. And you want the compiler to complain, when to try to put a String in a List<int> (a list of strings), or try to asign the object you get out of the list to a boolean. And obviously you don't want to write a specific List implementation for every Type, like a StringList, DoubleList, IntList,... especially as every method in it would basically work the same.
- CrociDB 5y agoInterestingly enough, putting _in Go_ on the title would make it less generic and target a better audience for the link.
- sirwhinesalot 5y agoUse them when you need them... If you're not sure if you need them, you don't. If you see yourself using reflection to implement a datastructure, you already know that you do. I don't get the nonsense around generics, it's so obvious to me...