Generics: Pass map with derived types

Issue

In the following example foo and bar are basically of the same type: map[uint32]string.
Nevertheless go1.18beta complains that: M2 does not match map[K]V.

Is it even possible to get equal to accept both of these maps? Do I need to change the signature of equal or the declarations of the maps itself?

package main

import "fmt"

func equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool {
    if len(m1) != len(m2) {
        return false
    }
    for k, v1 := range m1 {
        if v2, ok := m2[k]; !ok || v1 != v2 {
            return false
        }
    }
    return true
}

type (
    someNumericID uint32
    someStringID  string
)

func main() {
    foo := map[uint32]string{
        10: "bar",
    }

    bar := map[someNumericID]someStringID{
        10: "bar",
    }

    if equal(foo, bar) == true {
        fmt.Println("Maps are the same")
    } else {
        fmt.Println("Maps are not the same")
    }
}

Solution

Is it even possible to get equal to accept both of these maps?

Yes, but you have to differentiate the key and value types, as they are not the same. This is what the fixed function could look like:

func equal[K1, K2 ~uint32, V1, V2 ~string](m1 map[K1]V1, m2 map[K2]V2) bool {
    if len(m1) != len(m2) {
        return false
    }
    for k, v1 := range m1 {
        if v2, ok := m2[K2(k)]; !ok || V2(v1) != v2 {
            return false
        }
    }
    return true
}

In particular, both keys and value type parameters are constrained to the respective approximate elements ~uint32 and ~string in order to allow the conversions m2[K2(k)] and V2(v1) in the function body. This is required to compare values (including map indexing) that don’t have the same type, but have the same underlying type.

The above solution forgoes type parameters M1 and M2 on the map types — due to what appears to be a compiler bug; see comments for details —, but since you are not actually using those types in the function body, nor in return values, they are not strictly needed.

Playground: https://gotipplay.golang.org/p/Y8C_8ilsXUg


If you want to understand why your first example failed, here’s a breakdown. The relevant paragraph in the language specs is Type inference.

  1. In equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2), the type params M1 and M2 have the same constraint ~map[K]V.
  2. When you call the function without explicit instantiation, the compiler tries to infer the type parameters from the types of the supplied arguments. In short, it infers K and V from M1, so equal(foo, bar) where foo is map[uint32]string results in K = uint32 and V = string.
  3. The instantiated constraint is then M1, M2 ~map[uint32]string
  4. Now there’s no more type params to infer, so it just type-checks bar against the instantiated constraint. Is the underlying (~) type of bar the same as map[uint32]string? No. Even if the underlying types of keys and vals are the same, the underlying type of the entire map is exactly map[someNumericID]someStringID.
  5. Instantiation of equal with args foo and bar fails.

This becomes more apparent if instead of relying on type inference, you instantiate equal with explicit type args. By specifying just M1 and M2 (remember that they have the same constraint): equal[map[uint32]string, map[uint32]string](foo, bar) then bar obviously doesn’t match.

Answered By – blackgreen

Answer Checked By – Mary Flores (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.