What's the meaning of the new tilde token ~ in Go? - go

Go introduces the new token ~.
~T means the set of all types with underlying type T
However, I could not understand it, ask someone to help explain.
The following is an example.
type Ordered interface {
Integer | Float | ~string
}

In Go generics, the ~ tilde token is used in the form ~T to denote the set of types whose underlying type is T.
It was also called "approximation" constraint element in the generics proposal, which explains what it's good for in plain language:
Listing a single type is useless by itself. For constraint satisfaction, we want to be able to say not just int, but “any type whose underlying type is int”. [...] If a program uses type MyString string, the program can use the < operator with values of type MyString. It should be possible to instantiate [a function] with the type MyString.
If you want a formal reference, the language spec has placed the definition of underlying types in its own section:
Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.
This covers the very common cases of type literals and other composite types with bound identifiers, or types you define over predeclared identifiers, which is the case mentioned in the generics proposal:
// underlying type = struct literal -> itself -> struct { n int }
type Foo struct {
n int
}
// underlying type = slice literal -> itself -> []byte
type ByteSlice []byte
// underlying type = predeclared -> itself -> int8
type MyInt8 int8
// underlying type = predeclared -> itself -> string
type MyString string
The practical implication is that an interface constraint whose type set has only exact elements doesn't allow your own defined types:
// hypothetical constraint without approximation elements
type ExactSigned interface {
int | int8 | int16 | int32 | int64
}
// CANNOT instantiate with MyInt8
func echoExact[T ExactSigned](t T) T { return t }
// constraints.Signed uses approximation elements e.g. ~int8
// CAN instantiate with MyInt8
func echo[T constraints.Signed](t T) T { return t }
As with other constraint elements, you can use the approximation elements in unions, as in constraints.Signed or in anonymous constraints with or without syntactic sugar. Notably the syntactic sugar with only one approx element is valid:
// anonymous constraint
func echoFixedSize[T interface { ~int8 | ~int32 | ~int64 }](t T) T {
return t
}
// anonymous constraint with syntactic sugar
func echoFixedSizeSugar[T ~int8 | ~int32 | ~int64](t T) T {
return t
}
// anonymous constraint with syntactic sugar and one element
func echoFixedSizeSugarOne[T ~int8](t T) T {
return t
}
As anticipated above, a common use case for approximation elements is with composite types (slices, structs, etc.) that need to have methods. In that case you must bind the identifier:
// must bind identifier in order to declare methods
type ByteSeq []byte
func (b ByteSeq) DoSomething() {}
and now the approximation element is handy to allow instantiation with ByteSeq:
// ByteSeq not allowed, or must convert func argument first
func foobar[T interface { []byte }](t T) { /* ... */ }
// ByteSeq allowed
func bazquux[T interface { ~[]byte }](t T) { /* ... */ }
func main() {
b := []byte{0x00, 0x01}
seq := ByteSeq{0x02, 0x03}
foobar(b) // ok
foobar(seq) // compiler error
foobar([]byte(seq)) // ok, allows inference
foobar[[]byte](seq) // ok, explicit instantiation, then can assign seq to argument type []byte
bazquux(b) // ok
bazquux(seq) // ok
}
NOTE: you can not use the approximation token with a type parameter:
// INVALID!
type AnyApprox[T any] interface {
~T
}

There is not just the new token, but the new syntax for interfaces. You can declare an interface with type constraints in addition to method constraints.
To satisfy an interface, the type must satisfy both the method constraints and the type constraints.
From the docs:
An interface representing all types with underlying type int which implement the String method.
interface {
~int
String() string
}
For a type to have an "underlying type" of int, that means the type takes the following form:
type SomeType int
And to satisfy the method constraint, there must be declared a method with the specified signature:
func (v SomeType) String() string {
return fmt.Sprintf("%d", v)
}

Related

Unable to assign untyped int to generic struct field [duplicate]

This question already has answers here:
Assigning a value literal to a struct field of a generic type without running into an IncompatibleAssign error
(1 answer)
Convert a type (int, float etc) to `T` [go1.18]
(2 answers)
How to declare and use a struct field which can store both string and int values?
(4 answers)
Closed 4 months ago.
Given a generic struct:
type R2[IDTYPE comparable] struct {
ID IDTYPE
IsActive bool
}
Implementing an interface:
type Storable interface {
Store(ctx context.Context) error
}
I would expect the following definition to work:
func (r R2[int]) Store(ctx context.Context) error {
r.ID = 123 // not allowed
// ...
return nil
}
However, the method definition is not allowed. The error is:
'123' (type untyped int) cannot be represented by the type IDTYPE (int)
Is it not yet possible to do this kind of generic field assignment in Go?
Addendum:
On go playground the error is:
cannot use 123 (untyped int constant) as int value in assignment
And converting to int(123) does not work. The error in this case is:
cannot use comparable(123) (untyped int constant 123) as int value in assignment
Instantiation must happen at the type level, not on a method level, and methods can't introduce new type parameters, see How to create generic method in Go? (method must have no type parameters)
This means when you want to use R2, you then have to choose type arguments for the type parameters, and the methods can't change those, you're "stuck" with the types you choose on R2's instantiation.
Also note that since the constraint for IDTYPE is comparable, which may be string for example, the integer 123 cannot be assigned to the ID field in all cases because it may have a type of string.
If you want / must handle multiple concrete types for the IDs, generics is not the right choice. Interfaces may be used instead:
type R2 struct {
ID any
IsActive bool
}
Also note that the receiver must be a pointer if you wish to modify the receiver (e.g. fields of a struct).
If you wish to restrict the values stored in ID to comparable, use a (generic) function for it.
Here's how you can do it:
type R2 struct {
ID any
IsActive bool
}
func (r *R2) Store(ctx context.Context) error {
setID(r, 123)
return nil
}
func setID[ID comparable](r *R2, id ID) {
r.ID = id
}
Testing it:
r := &R2{}
var s Storable = r
s.Store(context.TODO())
fmt.Println(r)
Which outputs (try it on the Go Playground):
&{123 false}
This provides flexibility (you can set any comparable values to the ID field using setID()), and provides compile-time safety: attempting to set an incomparable value will result in a compile-time error such as this:
setID(r, []int{1}) // Error: []int does not implement comparable

Go generics: syntax of generic type constraints

In the Go Language reference, on the section regarding Type parameter declarations, I see [P Constraint[int]] as a type parameter example.
What does it mean?
How to use this structure in a generic function definition?
It's a type parameter list, as defined in the paragraph you linked, that has one type parameter declaration that has:
P as the type parameter name
Constraint[int] as the constraint
whereas Constraint[int] is an instantiation of a generic type (you must always instantiate generic types upon usage).
In that paragraph of the language spec, Constraint isn't defined, but it could reasonably be a generic interface:
type Constraint[T any] interface {
DoFoo(T)
}
type MyStruct struct {}
// implements Constraint instantiated with int
func (m MyStruct) DoFoo(v int) {
fmt.Println(v)
}
And you can use it as you would use any type parameter constraint:
func Foo[P Constraint[int]](p P) {
p.DoFoo(200)
}
func main() {
m := MyStruct{} // satisfies Constraint[int]
Foo(m)
}
Playground: https://go.dev/play/p/aBgva62Vyk1
The usage of this constraint is obviously contrived: you could simply use that instantiated interface as type of the argument.
For more details about implementation of generic interfaces, you can see: How to implement generic interfaces?

interface contains type constraints: cannot use interface in conversion

type Number interface {
int | int64 | float64
}
type NNumber interface {
}
//interface contains type constraints
//type NumberSlice []Number
type NNumberSlice []NNumber
func main() {
var b interface{}
b = interface{}(1)
fmt.Println(b)
// interface contains type constraints
// cannot use interface Number in conversion (contains specific type constraints or is comparable)
//a := []Number{Number(1), Number(2), Number(3), Number(4)}
//fmt.Println(a)
aa := []interface{}{interface{}(1), interface{}(2), interface{}(3), 4}
fmt.Println(aa)
aaa := []NNumber{NNumber(1), NNumber(2), NNumber(3), 4}
fmt.Println(aaa)
}
why the Number slice a couldn't be initialized like that?
NumberSlice and NNumberSlice look like similarly, but what mean type constraints, it looks strange grammar
The language specifications explicitly disallow using interfaces with type elements as anything other than type parameter constraints (the quote is under the paragraph Interface types):
Interfaces that are not basic may only be used as type constraints, or as elements of other interfaces used as constraints. They cannot be the types of values or variables, or components of other, non-interface types.
An interface that embeds comparable or another non-basic interface is also non-basic. Your Number interface contains a union, hence it is non-basic too.
A few examples:
// basic: only methods
type A1 interface {
GetName() string
}
// basic: only methods and/or embeds basic interface
type B1 interface {
A1
SetValue(v int)
}
// non-basic: embeds comparable
type Message interface {
comparable
Content() string
}
// non-basic: has a type element (union)
type Number interface {
int | int64 | float64
}
// non-basic: embeds a non-basic interface
type SpecialNumber interface {
Number
IsSpecial() bool
}
In the initialization of the variable a, you are attempting to use Number in a type conversion Number(1), and this is not allowed.
You can only use Number as a type parameter constraint, i.e. to restrict the types allowed for instantiation of a generic type or function. For example:
type Coordinates[T Number] struct {
x, y T
}
func sum[T Number](a, b T) T {
return a + b
}

Generic Structs with Go

What is the equivalent of this C# code in Go, how can I build it
class ModelX<T>
{
public T Data { get; set; }
}
ModelX<int>
I have tried something like:
type ModelX<T> struct {
ModelY
Data []T
}
m := ModelX<T>
How to do this? Is that possible?
Starting with Go 1.18, you can define generic types:
type Model[T any] struct {
Data []T
}
A generic type must be instantiated1 when used, and instantiation requires a type parameter list:
func main() {
// passing int as type parameter
modelInt := Model[int]{Data: []int{1, 2, 3}}
fmt.Println(modelInt.Data) // [1 2 3]
// passing string as type parameter
modelStr := Model[string]{Data: []string{"a", "b", "c"}}
fmt.Println(modelStr.Data) // [a b c]
}
More info and common gotchas about instantiations: Go error: cannot use generic type without instantiation
If you declare methods on a generic type, you must repeat the type parameter declaration on the receiver, even if the type parameters are not used in the method scope — in which case you may use the blank identifier _ to make it obvious:
func (m *Model[T]) Push(item T) {
m.Data = append(m.Data, item)
}
// not using the type param in this method
func (m *Model[_]) String() string {
return fmt.Sprint(m.Data)
}
An important detail is that — unlike functions2 —, generic types must always supply all3 type parameters upon instantiation. For example, this type:
type Foo[T any, P *T] struct {
val T
ptr P
}
must be instantiated with both types, even if some of them could be inferred:
func main() {
v := int64(20)
foo := Foo[int64, *int64]{val:v, ptr: &v}
fmt.Println(foo)
}
Playground: https://go.dev/play/p/n2G6l6ozacj
Footnotes:
1: Language specs about instantiations: https://golang.org/ref/spec#Instantiations
2: The quote from the specs is "Calls to parameterized functions may provide a (possibly partial) type argument list, or may omit it entirely if the omitted type arguments are inferrable from the ordinary (non-type) function arguments.". This quote excludes parametrized types
3: in early beta releases, the type param list in generic types could be partial; this feature has been disabled.

convert function type in Golang

// Each type have Error() string method.
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
// type error interface {
// Error() string
// }
func (f binFunc) Error() string {
return "binFunc error"
}
func func_type_convert() {
var err error
err = binFunc(add)
fmt.Println(err)
fmt.Println(i)
}
I have two questions about the code above:
I don't know why the Error method executed, when add function was converted into binFunc type?
Why the add function converted result was able to assign to an err error interface variable?
error is an interface:
type error interface {
Error() string
}
This means that any type which has a method: Error() string fulfills the interface and can be assigned to a variable of type error.
binFunc has such a method:
func (f binFunc) Error() string {
return "binFunc error"
}
New developers in Go sometimes find this confusing because they don't realize it's possible to attach methods to more than just structs. In this case binFunc is defined liked this:
type binFunc func(int, int) int
So the way this works is you are allowed to convert any function which has the same signature: (from the spec)
A function type denotes the set of all functions with the same parameter and result types.
So if you create a function add:
func add(x, y int) int {
return x + y
}
You are allowed to convert this into a binFunc:
binFunc(add)
And because of the Error method on binFunc we defined above, we are then able to assign this new binFunc to a variable of type error:
var err error
var bf binFunc = binFunc(add)
err = bf
fmt.Println's behavior is to call .Error() on errors for you:
If an operand implements the error interface, the Error method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).
So to answer your questions:
The Error method is executed because fmt.Println looks for arguments of type error, invokes .Error() and prints the resulting string.
You are allowed to assign binFuncs to err because binFunc has an Error method. You cannot assign add directly to err because it does not have an Error method. But you are allowed to convert add to a binFunc because they have the same function signature, and by doing so you can then assign it to the err variable.
go spec dependencies:
type casting or conversion -> assignability -> type identity
explicit type casting or conversion
binFunc and func(int, int) int have same underlying representation.
binFunc(add)
note, type casting can happen between 2 types that have the same underlying representation. However, their type can be totally different.
type MyInt int
func main() {
var b MyInt = 3
a := int(b)
fmt.Println(a, b)
}
variable assignment
check type identity
based on type identity rule, binFunc is identical to func(int, int) int. So you can do type casting as below:
A named type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types.
func(int, int) int
is type literal, and it's unnamed.
type binFunc func(int, int) int
is a named type.
Predeclared types, defined types, and type parameters are called named types. An alias denotes a named type if the type given in the alias declaration is a named type.
named type is different from others. but here, binFunc is compared with the un-named type: their underlying type literals are structurally equivalent, both func(int, int) int.
var bfunc binFunc = add
check assignability
variable of named type can be assigned with a value of unnamed type providing their underlying type is identical.
You may call this an implicit type conversion here, but it's not accurate. At least golang doesn't call this type conversion because the underlying type/representation is the same.
inspired by this answer
extra words
type assertion only requires type identity. Therefore, the rule is simpler than type assignability.

Resources