Do you know the three core designs of Go generics?

Posted by kylera on Wed, 05 Jan 2022 17:11:52 +0100

Hello, I'm fried fish.

Go1. Generics in 18 is a hot topic, although I have written many articles on the design and thinking of generics before. However, because the proposal for generics had not been finalized before, a complete introduction was not written.

Now it has basically taken shape. Fried fish will take you to find out the Go generics. The content of this paper mainly involves three concepts of generics, which is very worthy of our in-depth understanding.

As follows:

  • Type parameter.
  • Type constraints.
  • Type derivation.

Type parameter

Type parameter, this noun. Unfamiliar friends are confused at first sight.

Generic code is written using abstract data types, which we call type parameters. When a program runs generic code, type parameters are replaced by type parameters. That is, the type parameter is a generic abstract data type.

Simple generic example:

func Print(s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

The code has a Print function, which prints out each element of a fragment. The element type of the fragment, here called T, is unknown.

Here comes a point to do generic syntax design, that is: how should the generic type parameters of T be defined?

In the existing design, it is divided into two parts:

  • Type parameter list: the type parameter list will appear in front of the general parameters. To distinguish between type parameter lists and general parameter lists, type parameter lists use square brackets instead of parentheses.
  • Type parameter constraint: just as conventional parameters have types, type parameters also have meta types, which are called constraints (described further later).

The complete examples are as follows:

// Print can print elements of any fragment.
// Print has a type parameter T and a single (non type) s, which is a fragment of the type parameter.
func Print[T any](s []T) {
    // do something...
}

In the above code, we declare a function Print, which has a type parameter t, and the type constraint is any, which is expressed as any type. The function is the same as interface {}. His input variable s is a slice of type T.

After the function is declared, we need to specify the type of type parameter when calling the function. As follows:

    Print[int]([]int{1, 2, 3})

In the above code, we specified the type parameter passed in as int, and passed in [] int{1, 2, 3} as the parameter.

Other types, such as float64:

    Print[float64]([]float64{0.1, 0.2, 0.3})

It's also a similar statement. Just follow the suit.

Type constraint

After that, let's talk about "constraints". Type constraints must be specified in all type parameters before they can be called complete generics.

The following is divided into two parts to explain in detail:

  • Define function constraints.
  • Define operator

Why type constraints

In order to ensure that the caller can meet the program demands of the receiver and ensure that the functions, operators and other features applied in the program can run normally.

Generic type parameters and type constraints complement each other.

Define function constraints

Problem point

Let's take a look at the examples provided by Go officials:

func Stringify[T any](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String()) // INVALID
    }
    return ret
}

The purpose of this method is that any type of slice can be converted into the corresponding string slice. But there is a problem in the program logic, that is, its input parameter T is of any type, which can be passed in.

If the String method is called internally, it will naturally report an error, because it may not be implemented only for types such as int and float64.

You said to define valid type constraints, like the above example, how to implement them in generics?

If the incoming party is required to have built-in methods, an interface must be defined to restrict it.

Single type

Examples are as follows:

type Stringer interface {
    String() string
}

Apply in generic methods:

func Stringify[T Stringer](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Then put the Stringer type in the original any type to realize the demands required by the program.

Multiple types

In the case of multiple type constraints. Examples are as follows:

type Stringer interface {
    String() string
}

type Plusser interface {
    Plus(string) string
}

func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = p[i].Plus(v.String())
    }
    return r
}

The same rules as regular input and output parameter type declarations.

Define operator constraints

After the definition of function constraint is completed, the remaining big bone to gnaw is the constraint of "operator".

Problem point

Let's look at the official example of Go:

func Smallest[T any](s []T) T {
    r := s[0] // panic if slice is empty
    for _, v := range s[1:] {
        if v < r { // INVALID
            r = v
        }
    }
    return r
}

After the above function example, we can quickly realize that this program can't run successfully at all.

The input parameter is of any type. The internal program obtains the value according to the slice type, and the internal operator comparison is carried out. If it is really slice, each value type may be different.

If one is slice and the other is int, how to compare the values of operators?

Approximate element

Some students may think of overloaded operators, but Think too much, Go language has no plan to support. To this end, a new design is made, that is, it is allowed to limit the type range of type parameters.

The syntax is as follows:

InterfaceType  = "interface" "{" {(MethodSpec | InterfaceTypeName | ConstraintElem) ";" } "}" .
ConstraintElem = ConstraintTerm { "|" ConstraintTerm } .
ConstraintTerm = ["~"] Type .

Examples are as follows:

type AnyInt interface{ ~int }

The type set declared above is ~ int, that is, all types of int (such as int, int8, int16, int32, int64) can meet the conditions of this type constraint.

The underlying type is int8, for example:

type AnyInt8 int8

That is, within the matching range.

Joint element

If you want to further narrow down the qualified type, you can use it in combination with the separator. The usage is:

type AnyInt interface{
 ~int8 | ~int64
}

You can limit the type set to int8 and int64.

Implement operator constraints

Based on the new syntax, combined with new concept union and approximate elements, the program can be transformed to realize the matching of operators in generics.

The declaration of type constraints is as follows:

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

The application is as follows:

func Smallest[T Ordered](s []T) T {
    r := s[0] // panics if slice is empty
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

After ensuring that the values are basic data types, the program can run normally.

Type derivation

Programmers write code, a certain degree of laziness is inevitable.

In certain scenarios, you can avoid explicitly writing out some or all type parameters through type derivation, and the compiler will recognize them automatically.

It is suggested that it is best to specify complex functions and parameters, otherwise students reading code will be more troublesome, and the guarantee of readability and maintainability is also an important point in the work.

Parameter derivation

Function example. As follows:

func Map[F, T any](s []F, f func(F) T) []T { ... }

Common code snippets. As follows:

var s []int
f := func(i int) int64 { return int64(i) }
var r []int64

Specify two type parameters explicitly. As follows:

r = Map[int, int64](s, f)

Only the first type parameter is specified, and the variable f is inferred. As follows:

r = Map[int](s, f)

Without specifying any type parameters, let both be inferred. As follows:

r = Map(s, f)

Constraint derivation

The magic is that type derivation is not limited to this, even constraints can be derived.

Function examples are as follows:

func Double[E constraints.Number](s []E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

The derivation case based on this is as follows:

type MySlice []int

var V1 = Double(MySlice{1})

MySlice is a slice type alias for int. The type of variable V1 is derived by the compiler as [] int, which is not MySlice.

The reason is that when comparing the two types, the compiler will recognize the MySlice type as [] int, that is, the int type.

To achieve "correct" derivation, the following definitions are required:

type SC[E any] interface {
    []E 
}

func DoubleDefined[S SC[E], E constraints.Number](s S) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

Based on this derivation case. As follows:

var V2 = DoubleDefined[MySlice, int](MySlice{1})

As long as the explicit type parameter is defined, the correct type can be obtained. The type of variable V2 will be MySlice.

What if you don't declare constraints? As follows:

var V3 = DoubleDefined(MySlice{1})

The compiler deduces through function parameters, and can also make it clear that the type of variable V3 is MySlice.

summary

In today's article, we introduce three important concepts of generics:

  • Type parameter: abstract data type of generic type.
  • Type constraint: ensure that the caller can meet the program demands of the receiver.
  • Type derivation: avoid explicitly writing out some or all type parameters.

The content also involves new concepts such as joint element, approximate element, function constraint, operator constraint and so on. In essence, they are new solutions based on three major concepts, one ring after another.

Have you learned Go generics? How about the design? Welcome to discuss:)

If you have any questions, you are welcome to feedback and exchange in the comment area. The best relationship is mutual achievement. Your praise is Fried fish The greatest driving force of creation, thank you for your support.

The article is continuously updated. You can search [fried fish in your brain] on wechat. This article GitHub github.com/eddycjy/blog It has been included. You can watch it when learning Go language Go learn maps and routes , welcome to Star reminder.

reference resources

Topics: PHP Java Go Back-end