Home > Blockchain >  When is the tilde not necessary in Go generics?
When is the tilde not necessary in Go generics?

Time:05-12

With Golangs new generics we have the tilde operator ~ which will match the underlying type. In what case is it valid to NOT match the underlying type? I'm trying to understand why the current behavior with the tilde is not the default behavior. It seems unnecessary to support both.

For example, why would you write

interface { int }

and not

interface { ~int }

What benefit to you would it be to write a method that is so strict that it could not accept something like

type MyInt int

Why is the tilde behavior not the default, and thus the language would not require another operator?

CodePudding user response:

Not using the ~ operator means you only accept the listed exact types. Why should this matter?

You may want to use the values of the exact types to set to other variables and else type conversion would be required. And because the saying goes "new type, new method set". New types having the same underlying type have their own method sets.

You may want the "original" behavior of the value, which may change if it has a different method set.

For example, let's say you want to print the number like this:

type Num interface{ ~int }

func foo[T Num](v T) {
    fmt.Println(v)
}

If MyInt has a String() string method:

type MyInt int

func (m MyInt) String() string { return "bar" }

The output might not be what foo() would want, because the fmt package checks if a printed value has a String() string method, and if so, it is called to acquire its string representation:

foo(1)
foo(MyInt(1))

This will output (try it on the Go Playground):

1
bar

If you only allow int:

type Num interface{ int }

You can still call foo() with a value of type MyInt, using a type conversion:

foo(1)
x := MyInt(1)
foo(int(x))

And output will be what foo() wants, not what MyInt would want (try this one on the Go Playground):

1
1

Yes, this would also be possible if foo() itself would do the conversion, but this clearly documents you want a pure int, with int's behavior, and not something that is an int with a different, custom behavior.

CodePudding user response:

Why is the tilde behavior not the default, and thus the language would not require another operator?

Because if the approximation would be the default unconditionally you could not express the fact that your polymorphic function requires an int and not a MyInt. You would then have to introduce an operator like strict and write %int. Nothing gained.

CodePudding user response:

Why is the tilde behavior not the default

Because it would be confusing and semantically unsound to write a function like func Foo[T int](v T) that accepts type parameters that are not int. Then the meaning of int in interface constraints would not be the same as everywhere else. (More on this discussion)


What benefit to you would it be to write a method that is so strict [...]

Indeed if the constraint includes only one exact type, using type parameters is moot. If the type set of the constraint has cardinality 1, you should just remove the type parameter.

A function like:

func Foo[T int](v T)

can only ever be instantiated with exactly int, so it can (and should!) be simply written with regular arguments:

func Foo(v int)

When the type set cardinality is N, which includes single tilde types, but also unions, makes it basically impossible to write exhaustive type switch, since using ~ in case statements is not allowed (yet?):

func Foo[T ~int | ~string](v T) {
    switch t := any(v).(type) {
        case int: // ok
        case string: // ok

        // how to match other possible types then? 
    }
} 

In this particular case, an exhaustive type switch can be written only if the constraint includes exact types:

func Foo[T int | string](v T) {
    switch t := any(v).(type) {
        case int: // ok
        case string: // ok

        default:
            panic("should not occur")
    }
}

This should not arise frequently in practice: if you find yourself switching on the type parameter, you should ask yourself if the function really needs to be generic. However the use case is relevant when designing your code.

  • Related