Home > Software engineering >  Generic type which isn't of an interface type
Generic type which isn't of an interface type

Time:11-08

I'm trying to work with generics to create a parameterised type which can be of:

T, *T, T[], map[interface{}]interface{}

Where: T is of comparable type, but is not an interface.


I've attempted to formulate this through constrained typeset, but this fails due to MisplacedTypeParam compiler error:

type myType[T comparable] interface {
    T | *T | T[] | map[interface{}]interface{}
}

I also have the issue when using reflect, that getting the reflect.Kind or reflect.Type of an interface will return the value's type underlying the interface, which means I haven't figured out how to assert the type is not an interface.


From this, I am wondering what the best alternative way to represent such a type would be?


This is my work in progress (https://github.com/mcwalrus/go-jitjson) and its main parts:

type JitJSON[T any] struct {
    data []byte
    val  *T
}

func (jit *JitJSON[T]) Unmarshal() (T, error) {
    if jit.val != nil {
        return *jit.val, nil
    }

    var val T
    if jit.data == nil {
        return val, nil
    }

    jit.val = &val
    err := json.Unmarshal(jit.data, jit.val)
    if err != nil {
        return val, err
    }

    return *jit.val, nil
}

CodePudding user response:

Unfortunately you cannot** implement this simply as a type constraint. Therefore, you must rely on run-time checks.

You may implement the type check through reflect, but this may be redundant in your application, as the standard library json already produces an appropriate error when trying to marshal or unmarshal an incompatible data type.

If you do want to use reflect to inspect the exact type of a parameterized type, here's the naive but incorrect way to do it:

func f[T any]() {
    var zero T
    typ := reflect.TypeOf(zero)
    fmt.Println(typ)
}

This works for most types, except for interface types, for which it will always report typ as <nil>. This is because of how assignment of interface values works in Go. Here is the correct way to do it:

func f[T any]() {
    var zero T
    typ := reflect.TypeOf(&zero).Elem()
    fmt.Println(typ)
}

This is necessary because interface values, at runtime, only carry type information of the concrete type which they reference. By taking the pointer to an interface variable, taking the type of the pointer, then taking the element of the pointer type, the actual type of the interface variable can be extracted.


** I can't seem to find a concise explanation of why this can't be done, but here are some difficulties:

  • comparable cannot be used in a type union
  • type constraints cannot be composed
  • type constraints cannot be defined recursively
  • type constraints cannot be negated
  • there is no interface which matches structs in general

CodePudding user response:

As of Go 1.19 the constraint comparable already can be implemented only by strictly comparable types, i.e. types for which == and != are guaranteed to not panic at run time.

So this does exclude interfaces. However it also excludes other types that aren't strictly comparable but also not interfaces either, e.g. [5]any or struct{ Data any }.

I don't know if this is good enough for you. Otherwise, there is no way to define a constraint type set in terms of subtractions, i.e. in set notation, A \ B. From the spec:

An interface type is specified by a list of interface elements. An interface element is either a method or a type element, where a type element is a union of one or more type terms. A type term is either a single type or a single underlying type.

If you need to exclude some kinds of types from your computation you might have to fall back to reflection. Consider also that if your goal is to unmarshal JSON, using any constraint and *T might be acceptable (== checks if two pointers point to the same variable). After all, you are supposed to pass an addressable value to json.Unmarshal:

// code simplified for illustrative purposes
func (jit *JitJSON[T]) Unmarshal() (T, error) {
    var val T
    err := json.Unmarshal(jit.data, &val) // needs a pointer anyway
    if err != nil {
        return val, err
    }
    jit.val = &val
    return *jit.val, nil
}
  • Related