Home > Enterprise >  Why does maps.Keys() in Go specify the map type as M?
Why does maps.Keys() in Go specify the map type as M?

Time:04-19

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 literal L 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.

  • Related