Assignability question in golang specification - go

While reading go specification "Assignability" section, I tried to execute a couple of examples to get a better understanding of the topic, and now I can't get what am I doing wrong in my code.
According to the specification,
One of the cases when a value x is assignable to a variable of type T is as follows:
x's type V and T have identical underlying types and at least one of V or T is not a defined type.
Defined type specification states that
Type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.
But when I try to run following code, the build fails:
func main() {
type Defined int32
var d Defined
var i int32
d = i
}
The output is:
cannot use i (type int32) as type Defined in assignment
Meanwhile, the similar example with composite literal works fine:
func main() {
type MyMap map[string]int
var x MyMap
var y map[string]int
x = y
}
playground

Also from the spec:
https://golang.org/ref/spec#Numeric_types
To avoid portability issues all numeric types are defined types and thus distinct

Since type Defined int32 defines a new type, d and i don't have identical types; hence, the first clause x's type is identical to T from the assignability spec isn't applicable. The second clause x's type V and T have identical underlying types and at least one of V or T is not a defined type is not applicable as the types of both i and d are defined types. As the remaining clauses from the assignability spec do not apply in this situation, the assignment fails. Changing type Defined int32 to type Defined = int32 (which aliases a type) fixes the error.
x = y due to the T is an interface type and x implements T clause from the assignability spec is applicable.

Related

Go error: cannot use generic type without instantiation

Studying Go generics, I'm running into an error I can't seem to untangle. I've boiled it down to the simplest code:
type opStack[T any] []T
func main() {
t := make(opStack)
// t := new(opStack)
t = append(t, 0)
fmt.Println(t[0])
}
In playground, this bonks at the make() call (and similarly on the new call that's commented out) with the following error message:
cannot use generic type opStack[T any] without instantiation
But make() is an instantiating function. So, I expect I'm missing some syntactical subtlety. What is Go complaining about and what's the needed correction?
Whenever you use a parametrized type, including anywhere a type argument is required, like in the built-in make, you must replace the type parameters in its definition with actual types. This is called instantiation.
t := make(opStack[int], 0)
t = append(t, 0)
A generic type must be instantiated also if you use it as a type argument to another generic type:
type Data[T any] struct {
data T
}
d := Data[opStack[int]]{ data: []int{0, 1, 2} }
You can instantiate with a type parameter, for example in function signatures, fields and type definitions:
type FooBar[T any] struct {
ops opStack[T]
}
type OpsMap[T any] map[string]opStack[T]
func echo[T any](ops opStack[T]) opStack[T] { return ops }
The relevant quotes from the language specs are (currently) in two different places, Type definitions:
If the type definition specifies type parameters, the type name denotes a generic type. Generic types must be instantiated when they are used.
and Instantiations
A generic function or type is instantiated by substituting type arguments for the type parameters. [...]
In other programming languages, "instantiation" may refer to creating an instance of an object — in Go the term specifically refers to replacing type params with concrete types. In my view, the usage of the term is still consistent, although in Go it doesn't necessarily imply allocation.
Note that you may call generic functions without explicit type arguments. Instantiation happens there too, simply the type arguments might all be inferred from the function arguments:
func Print[T, U any](v T, w U) { /* ... */ }
Print("foo", 4.5) // T is inferred from "foo", U from 4.5
Inference used to work also in generic types, with the restriction that the type parameter list had to be non-empty. However this feature has been disabled, so you must supply all type params explicitly.
type Vector[T any] []T
// v := Vector[int]{} -> must supply T
type Matrix[T any, U ~[]T] []U
// m := Matrix[int, []int]{} -> must supply T and U
because you want
t = append(t, 0)
the data type can be int or float group.
this code should work
package main
import "fmt"
func main() {
type opStack[T any] []T
t := make(opStack[int], 0) // You must initialize data type here
t = append(t, 0)
fmt.Println(t[0])
}

Go type definition operation "inheritance"?

The Go language specification describes type definition as follows:
A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it. The new type is called a defined type. It is different from any other type, including the type it is created from.
I have two questions about this description:
What does "operation as the given type" mean, and what is the scope of "operation"(i.e. what counts as operation)? Say I define type A int[] and type B map[string]int, does "same operation" means I can use indexing on variables of type A and key-related operations on variables of type B?
I don't quite understand this description, why is the new type different with its underlying type while keeping the operations? So, the only difference is that they have different methods?
Look at the meaning of "operation as the given type" with context:
"A type definition creates a new, distinct type with the same operation as the given type."
And yes, this means if you could use the index operator on the original type, you can also index the new type. If you could apply the + addition operator on the original type, you can also apply it on the new type. If you could apply the <- receive operator on the original type (e.g. a bidirectional channel), you can also apply on the new type etc.
Basically everything you (may) do with a value is an operation. Applying operators on it, passing it to a function, calling a method on it. Whether an operation is allowed / valid is determined by the value's type, and whether a method call is valid depends on if the method set of the value's type contains the given method.
The new type is different because the type definition creates a new, named type, and Spec: Type identity:
Two types are either identical or different.
A defined type is always different from any other type.
The new type is different by definition. The new type will have zero methods "inherited" from the original type, which comes handy when you don't want the new type implementing certain interfaces, for details and examples, see Inheritance syntax. What is the difference?
You may of course add new methods to your new type. It may be unwanted to add methods to the existing type, or it may be impossible (e.g. because the old type may be a builtin type or may be defined in a package not under your control, and methods can only be added in the defining package).
Type identity (being different) also plays a role in assignability. E.g. a value of unnamed type can be assigned to a variable of named type if the underlying types match, but a value of named type cannot be assigned to a variable of another named type even if the underlying types match (without explicit conversion).
For example:
type Foo []int
type Bar Foo
var i []int = []int{1}
var f Foo = i // OK, []int is unnamed type
var b Bar = f // Not OK, Foo is a named type
Note that the "operations allowed" will have greater significance in the upcoming Go versions, as generics is added to the next (1.18) release, where you use constraints for type parameters, and what types may be used as type arguments for those type parameters is restricted by the operations that can be applied on certain types. For example if we have a simple generic add() function:
func add[T constraints.Ordered | constraints.Complex](a, b T) T {
return a + b
}
We may call it with int, float64, complex64 for example. But if we have our own defined type:
type MyInt int
Since the same operations can be applied on values of MyInt than that of int, we can also use MyInt as a type argument for the above T type parameter:
fmt.Println(add(1, 2)) // add[int]
fmt.Println(add(1+2i, 3+4i)) // add[complex64]
// Yes, below works too, it will be string concatenation
fmt.Println(add("1", "2")) // add[string]
fmt.Println(add(MyInt(1), MyInt(2))) // add[MyInt]
Output will be (try it on the Go Playground):
3
(4+6i)
12
3
The new type keeps the operations from the underlying type. But as it is a different, new type you can then add more operations to it. For example, I've added an "addall" method for type A
package main
import "fmt"
type A []int
type B map[string]int
func (a A) alladd(increment int) A {
for i := 0; i < len(a); i++ {
a[i] = a[i] + increment
}
fmt.Println(a[0])
return a
}
func main() {
var x A
x = append(x, 122)
x[0] = x[0] + 1
var y B = make(B)
y["ABC"] = 999
x = x.alladd(27)
fmt.Println(x, y)
}
https://go.dev/play/p/Od1_-SXk_uO

Go type automatically converting when it seems like it shouldn't

Sorry for the ambiguous title. I'm not getting a compiler error when I believe that I should, based on creating a new type and a function that takes an argument of that type.
The example:
package search
//Some random type alias
type Search string
//Takes a string and returns Search
func NewSearch(s string) Search {
return Search(s)
}
//This is where things are getting weird
//Returns an int just for arbitrary testing
func PrintSearch(s Search) int{
return 5
}
Now my assumption would be, if I created an object using NewSearch, I would be able to pass it to PrintSearch and have everything run as expected, but if I passed PrintSearch a primitive string, it should not compile. I am not experiencing this behavior.
The main code:
package main
import (
"fmt"
".../search" //no need to type the whole path here
)
func main() {
SearchTerm := search.NewSearch("Test")
StringTerm := "Another test"
fmt.Println(search.PrintSearch(SearchTerm)) // This should print 5
fmt.Println(search.PrintSearch(StringTerm)) // This should throw a compiler error, but it is not
}
It seems like if I write the type and the function in the same package as main, everything works as I'd expect? As in, it throws a compiler error. Is there something I've missed about cross-package type coercion?
We can simplify this example a bit further (playground):
package main
type Foo string
type Bar int
func main() {
var f Foo = "foo"
var b Bar = 1
println(f, b)
}
This is explained in the spec's assignability section.
A value x is assignable to a variable of type T ("x is assignable to
T") in any of these cases:
x's type is identical to T.
x's type V and T have identical underlying types and at least one of V or T is not a named type.
T is an interface type and x implements T.
x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.
x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
x is an untyped constant representable by a value of type T.

In golang why I can't use the struct as nested struct type?

The following code (play):
func main() {
buf := bytes.NewBuffer(make([]byte, 0))
rw := bufio.NewReadWriter(bufio.NewReader(buf), bufio.NewWriter(buf))
var r *bufio.Writer
r = rw
}
Gives the following compile-time error:
cannot use rw (type *bufio.ReadWriter) as type *bufio.Writer in assignment
What I expected is use a struct as a nested struct type. But if I declare r as io.Reader, this will be ok, so should I move to interface?
bufio.NewReadWriter() returns a concrete type, a pointer to a struct and bufio.Writer is also a concrete type, a struct. Neither *ReadWriter and *bufio.Writer is an interface!
In Go there is no automatic type conversion, you cannot assign a value of different concrete type to a variable.
You have 2 options:
Since bufio.ReadWriter embeds *bufio.Writer, you can simply refer to it and use that in the assignment:
var r *bufio.Writer
r = rw.Writer
Or you can declare r to be an io.Writer (it is an interface type) so that you can assign rw to it because rw implements io.Writer:
var r io.Writer
r = rw
Although I don't think creating r in this case is particularly useful because whenever you would use r you could also use rw.
Check out Go spec: Assignability:
A value x is assignable to a variable of type T ("x is assignable to T") in any of these cases:
x's type is identical to T.
x's type V and T have identical underlying types and at least one of V or T is not a named type.
T is an interface type and x implements T.
x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.
x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
x is an untyped constant representable by a value of type T.
None of the cases apply to your code, so it is an invalid assignment.
When r is declared to be io.Writer, it is the the following case and therefore it is valid:
T is an interface type and x implements T.
Different type can't assign, GO do not support extension.

What is the difference between type conversion and type assertion?

What is the main differences between :
v = t.(aType) // type assertion
v = aType(t) // type conversion
Where should I use type assertion or type conversion ?
A type assertion asserts that t (an interface type) actually is a aType and t will be an aType; namely the one wrapped in the t interface. E.g. if you know that your var reader io.Reader actually is a *bytes.Buffer you can do var br *bytes.Buffer = reader.(*bytes.Buffer).
A type conversion converts one (non-interface) type to another, e.g. a var x uint8 to and int64 like var id int64 = int64(x).
Rule of thumb: If you have to wrap your concrete type into an interface and want your concrete type back, use a type assertion (or type switch). If you need to convert one concrete type to an other, use a type conversion.
tl;dr x.(T) asserts that the dynamic value of interface x is T at run time; T(x) converts the type of an expression x to some other type.
Type Assertion
You know that in Go an interface is basically a method set specification, and you can assign to an interface variable any value whose type implements that method set1.
The type assertion written x.(T) asserts that the value stored in the interface x is of type T. You use a type assertion when you want to unbox that value.
One of the most common uses is when you have interface{} and you need to retrieve the concrete value it stores. A typical example, Context values:
func foo(ctx context.Context) {
s := ctx.Value("my_key").(string) // signature is `Value(key interface{}) interface{}`
// do something with s...
}
It is called assertion because at compile time it is not known whether x actually holds the concrete type T, but you assert that it does. That's why the unchecked assertion y := x.(T) panics if x doesn't actually hold a T — you must use the comma-ok assignment v, ok := x.(T) to avoid it.
ctx = context.WithValue(ctx, "my_key", "foo")
s := ctx.Value("my_key").(int) // panic
v, ok := ctx.Value("my_key").(string)
fmt.Println(v, ok) // "foo" true
In addition, when T in x.(T) is an interface itself, the assertion checks that the value stored in x implements T. The outcome is the same as above.
Type Conversion
A type conversion written as T(x) instead "changes the type of an expression to the type specified by the conversion", i.e. changes the type of x to T. An important property of conversions is that they are statically checked2. An invalid conversion simply won't compile:
type Foo string
type Bar int
a := "foo"
fmt.Println(Bar(a)) // cannot convert a (type string) to type Bar
The main condition for a conversion to be valid is assignability between the types involved, but there's several more, including conversions between numerical types, strings and byte/rune slices, directed channels, slices and array pointers, etc.
In simple terms, you use a conversion when you already know what are the types involved, and simply want to change one to the other:
b := []byte("foo") // converts string literal to byte slice
Notes:
1: more formally, when the value's method set is a superset of the interface method set; this is also why the empty interface interface{} can hold any value, because any set is a superset of an empty set.
2: type assertions are also checked at compile time when the type T in x.(T) does not implement the interface. In practice, this won't help you catch errors when x is interface{} since all types implement it.

Resources