DEV Community

Cover image for Generics in Go 1.18
Nitin Singh
Nitin Singh

Posted on

Generics in Go 1.18

With the Go 1.18 release, which is planned to be released in the February of 2022, many new features are waiting for us. And few major features are

  • Workspaces
  • Fuzzing Test
  • Generics

Go 1.18 Beta 1 is available, with generics

Notes

  • Golang hasn't yet announced a stable version, therefore it is not advisable to use it in production.
  • Due to addition of generics go 1.18-beta is approx. 18% slower Read more

Now the moment has come when Go programming language has its most awaiting and official release feature of Generics which is going to out in sometime around Feb 2022. The concepts and examples in this article are explained by Robert Griesemer in GopherCon.

There were lots of talks in past about Generics in Go about detail design and prototype information where Go contributors finalise the syntax and how to represent the constraints as interfaces read more in earlier part (Generics & Go). But still the go team were not happy and now they have figure out the major issues which they were dealing with. This time it is very clean and 100% backward compatible with existing programs and has some room to grow and improve.

Whats exactly is new - if you step back and look from Burj khalifa's top point of view, there are just 3 things

  1. Type parameter for function and types - visible sign of generosity
  2. Type set defined by interfaces - there is different view of interfaces, they are now not just able to define sets of methods but also sets of Types
  3. Type inference - which hopefully should smoothen up the generic code and make it little light weight.

Let's discuss them briefly to cover more details.

Type Parameters


Type parameter lists

 [P, Q constraint1, R constraint2]
Enter fullscreen mode Exit fullscreen mode

Type parameter list looks very much like ordinary parameter lists with square bracket instead of parentheses. It is customary to start type parameters with upper-case to emphasise that they are types.

Type parameter gives you the ability to parameterise a function or a typed with types

Lets take an example with Generic max function

For eg. a basic max function for float64 arguments

func max(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}
Enter fullscreen mode Exit fullscreen mode

We can make this function generic by replacing our float64 type with a Type parameter(in this case T)

func max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Enter fullscreen mode Exit fullscreen mode
  • The type parameter T, declared in a type parameter list, takes the place of float64.
  • And because we have new type T here, we also have to declare that happens in Type parameter list which marked in bold.

Calling generic function

To Call the generic max function, we provide the type and ordinary arguments.

When we want to use this max function for int arguments we have to call it off course with an int arguments but we also have to provide the respective type which is int in this case.

m:= max[int](2,3)
Enter fullscreen mode Exit fullscreen mode

Instantiation

Providing this type argument to your function is called Instantiation. Thats fairly important part in Generics programming.

Instantiation happening in 2 steps

  1. Substitute type argument for type Parameter.
    All the provided type arguments are substituted for the respected type parameters

  2. Check the type arguments implements their constraints.
    After the substitution has happened, the compiler checks that each type argument has implements it respective constraints. And what that exactly means will check in later columns.

But Instantiation fails if step 2 fails and in this case your program not gonna be valid.

Instantiating generic max

fmax := max[float64]
m := fmax(2.74,3.95)
Enter fullscreen mode Exit fullscreen mode

Going back to our original example we can instantiate this generic max function just by itself without actually calling it. Like this we provided float64 type argument and then we get a non generic max function which is essentially our original max function and we call it as any ordinary function.

max[float64](2.74,3.95) is evaluated as (max[float64])(2.74,3.95).

Instantiation produces a non-generic function.

Another example of Generic Binary Tree

Just how to see it look for types because we can parameterise types. Of course as well here is an example of generic binary tree.

  type Tree [T interface{}] struct{
    left, right  *Tree[T]
    data T
  }

  func (t *Tree[T]) Lookup(x T) *Tree[T]

  var stringTree Tree[String]

// Types can have parameter lists, too.
// Method declare the  respective type parameter with the receiver
Enter fullscreen mode Exit fullscreen mode

Here data type is generic we use the type T again and we can now create an instance of a string Tree for instance by instantiating this generic tree with string type and we can also have methods on Generic types.

Type Sets


Let's talk about the kind of types that a type parameter can be instantiated with.

Types of value parameters

An ordinary parameter lists has a type for each parameter.
In this case we have to type in float64 and this type tells us what kind of values are valid as arguments for a and b so this float64 is a set of values. Note the type parameter list each parameter type also has a type and this type is kind of a meta type and it tell us what kind of types are valid for this parameter and this meta type define sets of types and we call this type constraint.

Type Constraints

func max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}
Enter fullscreen mode Exit fullscreen mode

Interface defines method sets

Recently in Go specs , an interface define a set of methods if we have an interface with method A(), B(), C(), then we say that Interface define has a method set with A, B and C.
And every type that implements those methods actually implements that interface but there is a different view.
We could look at the set of types that implements those methods then say that the interface actually defines that set of type.

Interface defines method sets

Interfaces also defines types sets

Now each type that is in element of type set implements that interface and it's pretty obvious that these 2 views are essentially identical. That means for each set of methods we can imagine this typically infinite set of types and now we just saying interface defines that infinite set of types.

Interfaces also defines types sets

And if you want to check if type implements the interface we just check wether that type is in element of that set of types. But this view as a type that has some advantages because we can now control it in new and different ways, for instance we can add types explicitly to this set.

A type set with 3 types

A type set with 3 types

And in order to make this happen go developers have extended the syntax for interfaces exactly to be able to add additional types, for instance here we have an example of an interface with 3 types int, string and bool and this interface defines the set of exactly those 3 types.

interface {
    int|string|bool
}
Enter fullscreen mode Exit fullscreen mode

Another way of saying this is that these three types implement this interface.

// constraints.Ordered

package constraints

type Ordered interface{
    Integer|Float|~string
}

// Ordered defines the set of all Integer, floating point, and string types.
// The > operator is supported by every type in this type set.
Enter fullscreen mode Exit fullscreen mode

Now we can go back to our previous example remember the constraints.Ordered interface this is the declaration of that interface. Here this declarations states that order is the set of all integers floating-points and string types. Vertical pipe expresses in union of these types in this case type sets.

And Integer and Float are interfaces that define types of themselves, they are similarly defined in the same package.
No, there are no methods on this interface, usually we do not care about a specific type like we don't have a
set that just contains int or string type. We usually care about all the string types and all the int types and that is what is new token Tilde is for.

Tilde string in this case denotes the set of types that consists of all types with the underlying type string(all string types).

With Go 1.18, we still want to be able to specify methods and still to be able to do that with interfaces and you can embed our other interfaces as usual but will also be able to embed arbitrary types and unions of this forum and Tilde type elements.

The 2 function of type constraint

// 1. The type set of constraint is the set of valid type arguments
 // 2. If all types in the constraint support an operation, that operation may be used with respective type parameter.*
Enter fullscreen mode Exit fullscreen mode

Now uses a constraint that type set defined by a constraint interface defines exactly the type that are permissible as arguments for the respective type parameter.
And within a generic function for operands of type parameter type only the operations that are permitted for all the types in the respective typeset are permitted on those operands.

Constraint literals

[S interface{~[]E}, E interface{}]

// It is common to write constraint literals (inline)
Enter fullscreen mode Exit fullscreen mode

Interfaces use that's constraints may be given names (such as constraints.Ordered) or they may be used directly in line inside type parameter list for instance here we have a type parameter list that declares 2 type parameter S and E.

 [S interface{~[]E}, E interface{}] 
Enter fullscreen mode Exit fullscreen mode

so here S is defining is the type parameter that requires a slice type/ arbitrary slice type whose element type is not further constraint.

[ S ~[]E, E interface{} ] 

// in constraint position, interface {E} may be written as  E  for type elements E.
Enter fullscreen mode Exit fullscreen mode

This is a fairly common scenario because it is so common go people do introduce some syntactic sugar, that allows you to leave a way the interface curly braces around it

 [ S ~[]E, E any ] 

// The new pre-declared identifier any is an alias for interface{}.
Enter fullscreen mode Exit fullscreen mode
  • And finally because the empty interface appears quite often in type parameter list and for that matter in Go code, develops of Go introduce a new pre-declare identifier any that is an alias for empty interface(interface{}) and so this any identifier can be used anywhere instead of the empty interface.
  • Interface that define types are powerful new construct and they really make constraints work in GO but for now such interfaces may only be used inside type parameter list in constraint position.
  • But it's not hard to see how such interface is useful in general for arbitrary variables and may be that something that we would do in the future.

Type inference


// Calling max with type inference

func max[T constraints.Ordered] (a, b T) T

var a, b, m float64
m = max[float64](a, b)

m = max(a, b)

// Passing type arguments leads to more verbose code.
Enter fullscreen mode Exit fullscreen mode

Type inference mechanism

  • For this, going back to our max example with type parameters. When comes to need to pass type arguments which can make for rather verbose code and frequently these type arguments that can actually be deduced from the ordinary parameter that are arguments which are passed in a function call.

  • So here we have our generic max function again we pass 2 argument a and b of type float64 and by comparing those type against that parameter types T, after comparing T with float64 we can infer that the type for T must be float64.

  • Thats what the compiler does and if you can do that successfully then we can simply the way the type argument which lead to a function call for max that looks like any ordinary function call (no type parameter or anything)

Releasing exact inference are pretty complicated but using it is not. Type inference either succeed or it fails, if its succeed type parameters can be left away and always good that generic function call looks like any ordinary function call, if its fails then the compiler will complain and we simply provide those type arguments

Today people developing Go are try to strike a balance between the complexity and inference power for type inference. On today there is some room to fine tune and it will refine overtime. Fact will be that more programs won't need type arguments, the important point is the programs that don't need type arguments today they won't need type arguments tomorrow.

Top comments (0)