Go generics: type constraint for map keys? - go

In the code below, I define a generic linked list. Go1.18 is happy to use an instance of the list as a key to a map. However, the last line, when uncommented, doesn't compile; I get the error:
Cons[int] does not implement comparable
Is there a weaker type constraint I can use that picks out those types that can be used as keys, or is this intended, or is it a compiler bug?
package main
import "fmt"
type List[X any] interface {
isList()
}
type Cons[X any] struct {
Data X
Next List[X]
}
func (Cons[X]) isList() {}
type Nil[X any] struct{}
func (Nil[X]) isList() {}
func id[X comparable](x X) X { return x }
func main() {
x := Cons[int]{5, Nil[int]{}}
m := map[List[int]]string{}
m[x] = "Hi" // succeeds
fmt.Println(m[x]) // prints "Hi"
// fmt.Println(id(x)) // fails
}

Go 1.20 (February 2023)
comparable is the correct catch-all constraint for map keys.
All types that are comparable as per the Go spec, even if the comparison may panic at run time, can satisfy the comparable constraint. Your code will compile as expected in 1.20.
This finally fixes the inconsistency in previous Go version about spec-comparable types vs comparable types. See below for details.
Go 1.18 and 1.19
The predeclared comparable constraint is the correct constraint for map keys, however it can be instantiated only by strictly comparable types, i.e. types that support == and != (condition for being used as map keys) but won't panic at run time. This excludes interfaces1.
This is mentioned here: https://go.dev/ref/spec#Type_constraints
The predeclared interface type comparable denotes the set of all
non-interface types that are comparable. Specifically, a type T
implements comparable if:
T is not an interface type and T supports the operations == and != 2
T is an interface type and each type in T's type set implements comparable
Even though interfaces that are not type parameters can be compared (possibly causing a run-time panic) they do not implement comparable.
This is an important gotcha, because basic interface types normally do support the equality operators — what is compared is their dynamic types/values.
Therefore, your interface List[X] can be used as a map key directly, as in map[List[int]]string{}, but it does not implement comparable because it has an infinite type set (it has no terms, so any type implements it). And Cons doesn’t implement it either because it has a field of type List[X]. There is no "weaker" constraint for this.
Consider that constraints that embed comparable are also valid for map keys, so if you really need the method isList() in the function body, you can define a constraint like this, and have your lists-that-are-map-key structs implement that, instead of declaring an interface field:
// may use this as a constraint
type List interface {
comparable
isList() bool
}
1: the quote from the specs hints there are interface types that implement comparable, but it's effectively not possible to instantiate comparable with any interface at all: interfaces with only methods have an infinite type set, and interfaces with type terms can't be used anywhere except as constraints.
2: this rule actually doesn't cover non-interface types that support ==, like type S struct { data any }, but these types still can't instantiate comparable https://go.dev/play/p/N-pmE0XC-hB. This is a bug in the spec.

Related

Why can't I assign a embedded struct to a parent struct in go?

I have below code try to assign embed struct to its parent struct. There are two set of structure: Guider is the parent struct, DataBlock extends from it. The method func Put(x Guider) accept a parameter with type Guider. It works when I pass a DataBlock variable.
However, the other case is Mock extends from zerolog.Event, but it fails to pass the parameter on the method Test(e zerolog.Event)
I got the following error:
cannot use m (variable of type Mock) as type zerolog.Event in argument to Test
Why are these two cases works differently? How can I make them both work?
package main
import (
"fmt"
"github.com/rs/zerolog"
)
type Guider interface {
Guid() string
}
type FSEntity struct {
guid string
}
func (e FSEntity) Guid() string {
return e.guid
}
func Put(x Guider) {
fmt.Printf("%+v\n", x)
}
type Mock struct {
zerolog.Event
}
func Test(e zerolog.Event) {
}
//Child struct:
type DataBlock struct {
FSEntity
data []byte
}
func main() {
myVar := DataBlock{}
myVar.guid = "test"
myVar.data = []byte("moar test")
Put(myVar) // it works
m := Mock{}
Test(m) // it doesn't work. cannot use m (variable of type Mock) as type zerolog.Event in argument to Test
}
First, a couple of definitions:
Polymorphism
Polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.
Subtyping
Subtyping (also subtype polymorphism or inclusion polymorphism) is a form of type polymorphism in which a subtype is a datatype that is related to another datatype (the supertype) by some notion of substitutability, meaning that program elements, typically subroutines or functions, written to operate on elements of the supertype can also operate on elements of the subtype
Inheritance
In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation.
Object composition
Object composition and object aggregation are closely related ways to combine objects or data types into more complex ones.
Golang follows composition over inheritance principle, e.g. it doesn't support inheritance. So when you're saying
Mock extends from zerolog.Event
you actually mean that Mock includes zerolog.Event struct.
The way Golang implements polymorphism is interface. All types that implement some interface can be used in its place. It's what you see when use Guider.
However, it doesn't work for simple structs. zerolog.Event is a struct inside Mock.
So, normally, Test function should accept some interface as a parameter, and both mock and real event should implement this interface. However, it looks like zerolog doesn't provide interface for Event. So instead you should access the Event field of you struct. Example
Put(myVar) is legal because myVar is a DataBlock which contains (not inherits from and not implements) an FSEntity which in turn implements the Guider interface.
Since Put accepts a Guider, the reference to myVar is compatible, by virtue of the anonymous FSEntity field it contains which implements Guider. The implementation of Guider on FSEntity is (in effect) elevated to the containing struct (providing a means of delegating interfaces). This only occurs if the contained field is anonymous.
But in the case of Test(m), the function accepts a zerolog.Event which is a struct type, not an interface. As such, there is no "delegation" possible. Test() must be passed a zerolog.Event and in this scenario, this requires that you use the type name of the anonymous field:
Type(m.Event)
Some bonus info:
If DataBlock contained two anonymous fields which both implemented Guider then implicit delegation/elevation cannot take place; golang does not know which of the contained implementations should be delegated to/elevated (if any). In that scenario you must again use the name of the field that you wish to pass to the Put() function:
// given...
type Foo string
func (f Foo) Guid() string {
return string(f)
}
// and...
type DataBlock struct {
FSEntity
Foo
data []byte
}
// then...
Put(myVar) // is now illegal
// and must instead use either/or:
Put(myVar.FSEntity)
Put(myVar.Foo)
Whether implicit or explicit, the crucial distinction is that it is a field of the DataBlock (myVar) that is passed to Put(), not myVar itself.
If you want to pass the DataBlock to Put(), using a Guider interface, then DataBlock must itself implement the Guider interface.
Take this with a grain of salt, since I'm not familiar with zerolog package.
Your Guider is an interface, which might have any underlying type as long as Guid() method is satisfied. I assume this is happening through DataBlock containing FSEntity, which itself implements Guid() method, therefore satisfies MIGHT the interface.
On the other hand, I don't know what methods should be implemented to satisfy zerolog.Event or if it's even an interface, or a struct straight up. If it's an interface, you might need to implement it's required methods to be able to use DataBlock as zerolog.Event type. You might want/need to dig into that direction for some very specific answers.

How to define type without operations allowed on the underlying type?

If I define a type definition
type X int64
Why is it that I can then do
var x X = 123
x = x + 1
The x behaves as if it was the underlying int64, which I don't want. Or that it allows operations on the underlying type to be performed on this new type?
One of the reasons I'd define a new type is to hide the underlying type and define my own operations on it.
Creating a new defined type will dissociate any methods on the underlying type, but it does not dissociate functionality with operators such as + - / *.
A defined type may have methods associated with it. It does not inherit any methods bound to the given type
You should base your type on an underlying type with the desirable operators. For example, if you don't want to have arithmetic operators, you can derive from a struct.
If your type still needs the arithmetic capabilities of an int64 for internal reasons, you can hide it as an un-exported field in the struct. For example:
type X struct {
number int64
}

Can't use reflect.Type as comparable with generics in go 1.18 [duplicate]

In the code below, I define a generic linked list. Go1.18 is happy to use an instance of the list as a key to a map. However, the last line, when uncommented, doesn't compile; I get the error:
Cons[int] does not implement comparable
Is there a weaker type constraint I can use that picks out those types that can be used as keys, or is this intended, or is it a compiler bug?
package main
import "fmt"
type List[X any] interface {
isList()
}
type Cons[X any] struct {
Data X
Next List[X]
}
func (Cons[X]) isList() {}
type Nil[X any] struct{}
func (Nil[X]) isList() {}
func id[X comparable](x X) X { return x }
func main() {
x := Cons[int]{5, Nil[int]{}}
m := map[List[int]]string{}
m[x] = "Hi" // succeeds
fmt.Println(m[x]) // prints "Hi"
// fmt.Println(id(x)) // fails
}
Go 1.20 (February 2023)
comparable is the correct catch-all constraint for map keys.
All types that are comparable as per the Go spec, even if the comparison may panic at run time, can satisfy the comparable constraint. Your code will compile as expected in 1.20.
This finally fixes the inconsistency in previous Go version about spec-comparable types vs comparable types. See below for details.
Go 1.18 and 1.19
The predeclared comparable constraint is the correct constraint for map keys, however it can be instantiated only by strictly comparable types, i.e. types that support == and != (condition for being used as map keys) but won't panic at run time. This excludes interfaces1.
This is mentioned here: https://go.dev/ref/spec#Type_constraints
The predeclared interface type comparable denotes the set of all
non-interface types that are comparable. Specifically, a type T
implements comparable if:
T is not an interface type and T supports the operations == and != 2
T is an interface type and each type in T's type set implements comparable
Even though interfaces that are not type parameters can be compared (possibly causing a run-time panic) they do not implement comparable.
This is an important gotcha, because basic interface types normally do support the equality operators — what is compared is their dynamic types/values.
Therefore, your interface List[X] can be used as a map key directly, as in map[List[int]]string{}, but it does not implement comparable because it has an infinite type set (it has no terms, so any type implements it). And Cons doesn’t implement it either because it has a field of type List[X]. There is no "weaker" constraint for this.
Consider that constraints that embed comparable are also valid for map keys, so if you really need the method isList() in the function body, you can define a constraint like this, and have your lists-that-are-map-key structs implement that, instead of declaring an interface field:
// may use this as a constraint
type List interface {
comparable
isList() bool
}
1: the quote from the specs hints there are interface types that implement comparable, but it's effectively not possible to instantiate comparable with any interface at all: interfaces with only methods have an infinite type set, and interfaces with type terms can't be used anywhere except as constraints.
2: this rule actually doesn't cover non-interface types that support ==, like type S struct { data any }, but these types still can't instantiate comparable https://go.dev/play/p/N-pmE0XC-hB. This is a bug in the spec.

How to write several implementation of the same method that have a different signature

I have several implementation of the same method SetRateForMeasure:
package repartition
type Repartition interface {
Name() string
Compute(meters []models.Meter, totalsProd, totalsConso map[string]float64) []models.Meter
SetRateForMeasure(meter models.Meter, measure models.Measure, total float64) float64
}
Then, in my code (in repartition.go), I call it:
rate := repartition.SetRateForMeasure(meter, measure, total)
where repartition is the interface defined before.
Thing is, when I add a new implementation of this method, the arguments of my functions might differ.
For example, the static repartition use a static percentage that is only used in this case.
I end up adding parameters so that I have a common interface to all methods, but it results that there is a lot of unused parameters depending on the implementation.
If I add it to common interface, it will be unused for the other definitions.
I tried to remove this method from my interface definition, but now
rate := repartition.SetRateForMeasure()
is no more defined.
How should I organize my code ?
There is no function overloading in Go, so you cannot declare the same function with different arguments. There's a few ways you can implement this though:
You can add multiple functions with different names and signatures
You can change the function to accept a struct instead of arguments
SetRateForMeasure(args SetRateOptions) float64
type SetRateOptions struct {
Meter models.Meter
Measure models.Measure
Total float64
Percentage *float64 // If nil, use default percentage
... // more parameters as needed
}
Go doesn't support method overriding. You either ​define methods with different names that take different parameters
​ or you can declare the method to accept a parameter struct.
type SetRateParams struct {
Meter models.Meter
Measure models.Measure
Total float64
}
type Repartition interface {
SetRateForMeasure(params SetRateParams) float64
}
Optionally, you can declare params in your structs as pointers, so you can represent "not-provided" semantics with nil instead of using the zero-value. This might be relevant in case of numerical params where 0 could be a valid value.
Using a struct param has also the advantage that you don't have to change all the call sites in case you decide to add an additional param 6 months from now (you just add it to the struct).
There are also worse solutions with interface{} varargs, for the sake of stating what is possible, but unless you loathe type safety, I wouldn't recommend that.

Composition vs inheritance with anonymous struct

I was reading this slideshow, which says:
var hits struct {
sync.Mutex
n int
}
hits.Lock()
hits.n++
hits.Unlock()
How does that work exactly? Seems like hits isn't composed of a mutex and integer, but is a mutex and integer?
It is composition. Using an anonymous field (embedded field), the containing struct will have a value of the embedded type, and you can refer to it: the unqualified type name acts as the field name.
So you could just as easily write:
hits.Mutex.Lock()
hits.n++
hits.Mutex.Unlock()
When you embed a type, fields and methods of the embedded type get promoted, so you can refer to them without specifying the field name (which is the embedded type name), but this is just syntactic sugar. Quoting from Spec: Selectors:
A selector f may denote a field or method f of a type T, or it may refer to a field or method f of a nested embedded field of T.
Beyond the field / method promotion, the method set of the embedder type will also contain the method set of the embedded type. Quoting from Spec: Struct types:
Given a struct type S and a defined type T, promoted methods are included in the method set of the struct as follows:
If S contains an embedded field T, the method sets of S and *S both include promoted methods with receiver T. The method set of *S also includes promoted methods with receiver *T.
If S contains an embedded field *T, the method sets of S and *S both include promoted methods with receiver T or *T.
This is not inheritance in the OOP sense, but something similar. This comes handy when you want to implement an interface: if you embed a type that already implements the interface, so will your struct type. You can also provide your own implementation of some methods, which gives the feeling of method overriding, but must not be forgetten that selectors that denote methods of the embedded type will get the embedded value as the receiver (and not the embedder value), and selectors that denote your methods defined on the struct type (that may or may not "shadow" a method of the embedded type) will get the embedder struct value as the receiver.
It's called embedding, hits is composed of a sync.Mutex and an int. This should be true since there is really no inheritance in Go. This is more of a "has a" rather than an "is a" relationship between the members and the struct.
Read here for a more complete explanation
Quoted from the link
The methods of embedded types come along for free
That means you can access them like hits.Lock() instead of the longer form hits.Mutex.Lock() because the function Lock() is not ambiguous.
See the Go-syntax representation of hits variable:
fmt.Printf("%#v\n", &hits)
// &struct { sync.Mutex; n int }{Mutex:sync.Mutex{state:0, sema:0x0}, n:1}
When you declare the variable, it simply initializes the fields in struct with their default values.
Also, compiler automatically sets the name of the embedded struct as a field. So you can also access like:
hits.Mutex.Lock()
hits.Mutex.Unlock()
And you have access to all methods and exported fields (if any) of sync.Mutex.

Resources