Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What would generics in Go be?

Tags:

generics

go

There's been a lot of criticism about Go recently because it doesn't have support for generics. What exactly does that mean? How would you explain that to someone coming from a dynamically typed language like Ruby where this isn't a familiar concept?

like image 295
Simpleton Avatar asked Jan 17 '14 21:01

Simpleton


People also ask

Does Go have generics now?

With the release of generic programming in Go, we can now write a min() function that works for both integer and floating-point types without having to explicitly write them based on the types. The Go generics design basically entails allowing types and function declarations to have optional type parameters.

How are generics implemented in Go?

One key characteristics of Go generics implementation is they only partially use monomorphization, a technique used in languages like C++, D, or Rust to compile generic code. In a nutshell, monomorphization consists in replicating a function's implementation to specialize it for distinct types.

Does Go support generic programming?

In other words, interface types in Go are a form of generic programming. They let us capture the common aspects of different types and express them as methods. We can then write functions that use those interface types, and those functions will work for any type that implements those methods.

Why does Go not have generic types?

As we are all aware Go has no Generics, it was designed with simplicity in mind and Generics as mentioned above is considered to add complexity to the language. The same goes for Inheritance, Polymorphism and some other features that the top object-oriented languages showed when Go was created.


3 Answers

Update Q1 2022

Generics are officially supported with Go 1.18

Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.
This includes major - but fully backward-compatible - changes to the language.

See nwillc/genfuncs for (lots of) examples:

// Keys returns a slice of all the keys in the map.
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, len(m))
    var i int
    for k, _ := range m {
        keys[i] = k
        i++
    }
    return keys
}

2014:

William B. Yager blog post reminds why the "generic" part present in Go is not enough:

You can write generic functions easily enough.
Let's say you wanted to write a function that printed a hash code for objects that could be hashed. You can define an interface that allows you to do this with static type safety guarantees, like this:

type Hashable interface {
  Hash() []byte
}

func printHash(item Hashable) {
   fmt.Println(item.Hash())
}

Now, you can supply any Hashable object to printHash, and you also get static type checking. This is good.

What if you wanted to write a generic data structure?
Let's write a simple Linked List. The idiomatic way to write a generic data structure in Go is:

(here is just the start)

type LinkedList struct {
   value interface{}
   next *LinkedList
}

func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
   return &LinkedList{value, oldNode}
}

The "correct" way to build generic data structures in Go is to cast things to the top type and then put them in the data structure. This is how Java used to work, circa 2004. Then people realized that this completely defeated the purpose of type systems.

When you have data structures like this, you completely eliminate any of the benefits that a type system provides. For example, this is perfectly valid code:

node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4}) 

So that is why, if you want to retain the benefit of type system, you have to use some code generation, to generate the boileplate code for your specific type.

The gen project is an example of that approach:

gen generates code for your types, at development time, using the command line.
gen is not an import; the generated source becomes part of your project and takes no external dependencies.


Update June 2017: Dave Cheney detailed what Generics for Go would mean in his articles "Simplicity Debt" and "Simplicity Debt Redux".

Since Go 2.0 is now actively discussed at the core team level, Dave points out what Generics involve, and that is:

  • Error handling: Generic would allow a monadic Error handling, meaning you need to understand monad on top of the rest: that enable handling computational pipeline instead of checking errors after each function call.
    But: you need to understand monad!
  • Collections: facilitate custom collection types without the need for interface{} boxing and type assertions.
    But that leaves the question of what to do with the built in slice and map types.
  • Slicing: Does it go away, if so, how would that impact common operations like handling the result a call to io.Reader.Read?
    If slicing doesn’t go away, would that require the addition of operator overloading so that user defined collection types can implement a slice operator?
  • Vector: Go’s Pascal-like array type has a fixed size known at compile time. How could you implement a growable vector without resorting to unsafe hacks?
  • Iterator: what you really want to be able to do is compose iterators over database results and network requests.
    In short, data from outside your process—and when data is outside your process, retrieving it might fail.
    In that case you have a choice, does your Iterable interface return a value, a value and an error, or perhaps you go down the option type route.
  • Immutability: The ability to mark a function parameter as const is insufficient, because while it restricts the receiver from mutating the value, it does not prohibit the caller from doing so, which is the majority of the data races I see in Go programs today.
    Perhaps what Go needs is not immutability, but ownership semantics.

As Russ Cox writes in "My Go Resolutions for 2017":

Today, there are newer attempts to learn from as well, including Dart, Midori, Rust, and Swift.

The latest discussion is Go issue 15292: it also references "Summary of Go Generics Discussions".

like image 20
VonC Avatar answered Nov 15 '22 11:11

VonC


In a dynamically typed language, you don't care what type of list it is, just that it's a list. However, in a statically typed language, you do care what type of list it is because the type is "a list of A" where "A" is some type. That is, a list A is a different type from list B.

So when you speak of generics, calling some function of type A -> B each item of a list with a foreach means that the list must be a list A. But... if you use generics, then you don't have to declare what A is, you can just have it be filled in at a later date. Thus, you establish the contract whereby given a list C and a function A -> B, A === C in order for it to compile. This reduces boilerplate considerably.

In Go, given the lack of generics and the ability to declare such a type contract, you have to write a function that operates on a list of int, a list of double, a list of string, etc. You can't just define things in a "generic" manner.

like image 102
wheaties Avatar answered Nov 15 '22 09:11

wheaties


Go 1.18 introduced generics (beta release)

You don't have to be left wondering now. Generics are a reality in Go. The draft release notes of Go 1.18 officially announce the introduction of type parameters into the language in a backward-compatible way.

The current set of specifications for type parameters can be found in the type parameters proposal authored by Ian Lance Taylor and Robert Griesemer.

These specs are already implemented into the language at tip, which you can run right now on the Gotip Playground.

A very high level overview

(source)

  • Functions can have an additional type parameter list that uses square brackets but otherwise looks like an ordinary parameter list: func F[T any](p T) { ... }.
  • These type parameters can be used by the regular parameters and in the function body.
  • Types can also have a type parameter list: type M[T any] []T.
  • Each type parameter has a type constraint, just as each ordinary parameter has a type: func F[T Constraint](p T) { ... }.
  • Type constraints are interface types.
  • The new predeclared name any is a type constraint that permits any type.
  • Interface types used as type constraints can embed additional elements to restrict the set of type arguments that satisfy the constraint:
  1. an arbitrary type T restricts to that type
  2. an approximation element ~T restricts to all types whose underlying type is T
  3. a union element T1 | T2 | ... restricts to any of the listed elements
  • Generic functions may only use operations supported by all the types permitted by the constraint.
  • Using a generic function or type requires passing type arguments.
  • Type inference permits omitting the type arguments of a function call in common cases.

Example of a generic function

func echo[T any](v T) T {
    return v
}

Note that any is a type alias of interface{}

Example of a generic struct type

type Foo[T any] struct {
    val T
}

Example of an interface constraint with type set and approximation types

type FloatingPoint interface {
    ~float32 | ~float64
}

Interface constraints can also be parametrized, for example a constraint that restricts any type to its pointer counterpart:

type Ptr[T any] interface {
    *T
}
like image 25
blackgreen Avatar answered Nov 15 '22 10:11

blackgreen