I had a function implemented to obtain the keys in a map (a few versions actually, for different types), which I updated to use generics in Go 1.18. Then I found out that the experimental library was extended to include that functionality, and while my implementation was nearly identical, the function declaration has some differences that I would like to better understand.
Here is my original generic version (I renamed the variables to match the standard library, to better highlight was is actually different):
func mapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
And here is the standard-library version:
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
As you can see, the main difference is the extra M ~map[K]V
type parameter, which I omitted and directly used map[K]V
for the function's argument type. My function works, so why would I need to go through the extra trouble of adding a third parameterized type?
As I was writing my question, I thought I had figured out the answer: to be able to invoke the function on types that are really maps under the covers, but are not directly declared as such, like maybe on this DataCache
type:
type DataCache map[string]DataObject
My thinking was that this probably required the ~map
notation, and the ~
can only be used in a type constraint, not in an actual type. Only problem with this theory: my version works fine on such map types. So I am at a loss as to what it's useful for.
CodePudding user response:
Using a named type parameter in function signatures is mostly relevant when you need to accept and return defined types, as you correctly guessed, and as @icza answered here with respect to the x/exp/slices
package.
Your remark that "tilde types" can only be used in interface constraints is also correct.
Now, almost all functions in the x/exp/maps
package do not actually return the named type M
. The only one that actually does is maps.Clone
with signature:
func Clone[M ~map[K]V, K comparable, V any](m M) M
However declaring the signature without the approximate constraint ~map[K]V
still works for defined types thanks to type unification. From the specs:
[...], because a defined type
D
and a type literalL
are never equivalent, unification compares the underlying type of D with L instead
And a code example:
func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
type Dictionary map[string]int
func main() {
m := Dictionary{"foo": 1, "bar": 2}
k := Keys(m)
fmt.Println(k) // it just works
}
Playground: https://go.dev/play/p/hzb2TflybZ9
The case where the additional named type parameter M ~map[K]V
is relevant is when you need to pass around an instantiated value of the function:
func main() {
// variable of function type!
fn := Keys[Dictionary]
m := Dictionary{"foo": 1, "bar": 2}
fmt.Println(fn(m))
}
Playground: https://go.dev/play/p/hks_8bnhgsf
Without the M ~map[K]V
type parameter, it would not be possible to instantiate such a function value with defined types. Of course you could instantiate your function with K
and V
separately like
fn := Keys[string, int]
But this is not viable when the defined map type belongs to a different package and references unexported types:
package foo
type someStruct struct{ val int }
type Dictionary map[string]someStruct
and:
package main
func main() {
// does not compile
// fn := Keys[string, foo.someStruct]
// this does
fn := maps.Keys[foo.Dictionary]
}
Though, this seems a rather exoteric use case.
You can see the final playground here: https://go.dev/play/p/B-_RBSqVqUD
However keep in mind that x/exp/maps
is an experimental package, so the signatures may be changed with future Go releases, and/or when these functions get promoted into the standard library.