Home > Enterprise >  Syntax for using a generic type as struct field
Syntax for using a generic type as struct field

Time:09-23

I am trying to define a table type in Golang using structs. This is what I have at the moment.

type Column[T any] struct {
    Name   string
    Values map[int]T
}

I would like to use this column type to define a table like so,

type Table struct {
//Following line returns an error
    Columns map[string]Column

Go's compiler is throwing an error that I need to instantiate the generic type Column.

Can anyone help me with the syntax for creating it?

CodePudding user response:

You need to propagate the type from top level structure:

type Table[T any] struct {
    Columns map[string]Column[T]
}

See playground

CodePudding user response:

So what you're trying to do do is essentially have a number of columns, with values in different types, and collect them all in a Table type. Using plain old generics, you can do something like Ado Ren suggested:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Table[T any] struct {
    Columns map[string]Column[T]
}

However, as I suspected, you want your Table to be able to contain multiple columns, of different types, so something like this would not work:

sCol := Column[string]{
    Name: "strings",
}
uiCol := Column[uint64]{
    Name: "Uints",
}
tbl := Table[any]{
    Columns: map[string]any{
        sCol.Name:  sCol,
        uiCol.Name: uiCol,
    },
}

The reason, if you look at what this implies, is that the type for tbl doesn't make sense, compared to the values you're assigning. The any (or interface{} type means that what tbl is initialised to - and what it expects - is this (I replaced the Column type with an anonymous struct so the type mismatch is more obvious):

Table[any]{
    Columns: map[string]struct{
        Name   string
        Values map[int]any // or interface{}
    }{
        "Strings": {
            Name:   "Strings",
            Values: map[int]string{}, // does not match map[int]any
        },
        "Uints": {
            Name:   "Uints",
            Values: map[int]uint64{}, // again, not a map[int]any
        },
    }
}

This is essentially what is going on. Again, and as I mentioned in a comment earlier, there's a reason for this. The whole point of generics is that they allow you to create types and write meaningful functions that can handle all types governed by the constraint. With a generic Table type, should it accept differently typed Column values, that no longer applies:

func (t Table[T any]) ProcessVals(callback func(v T) T) {
    for _, col := range t.Columns {
        for k, v := range col.Values {
            col.Values[k] = callback(v)
        }
    }
}

With a type like Table[uint64], you could do stuff like:

tbl.ProcessVals(func(v uint64) uint64 {
    if v%2 == 1 {
        return v*3   1
    }
    return v/2
})

But if some of the columns in table are strings, then naturally, this callback makes no sense. You'd need to do stuff like:

tbl.ProcessVals(func (v interface{}) interface{} {
    switch tv := t.(type) {
    case uint64:
    case int64:
    case int:
    case uint:
    default:
        return v
    }
})

Use type switches/type assertions to make sense of each value, and then process it accordingly. That's not really generic... it's basically what we did prior to the introduction of go generics, so why bother? At best it makes no difference, at worst you'll end up with a lot of tedious code all over the place, as callbacks are going to be written by the caller, and their code will end up a right mess.


So what to do?

Honestly, this question has a bit of the X-Y problem feel about it. I don't know what you're trying to do exactly, so odds are you're just going about it in the sub-optimal way. I've been vocal in the past about not being the biggest fan of generics. I use them, but sparingly. More often than not, I've found (in C templates especially), people resort to generics not because it makes sense, but because they can. Be that as it may, in this particular case, you can incorporate columns of multiple types in your Table type using one of golangs more underappreciated features: duck-type interfaces:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Col interface {
    Get() Column[any]
}

type Table struct {
    Columns map[string]Col
}

func (c Column[T]) Get() Column[any] {
    r := Column[any]{
        Name:   c.Name,
        Values: make(map[int]any, len(c.Values)),
    }
    for k, v := range c.Values {
        r.Values[k] = v
    }
    return r
}

Now Table doesn't expect a Column of any particular type, but rather any value that implements the Col interface (an interface with a single method returning a value of Column[any]). This effectively erases the underlying types as far as the Table is concerned, but at least you're able to cram in whatever Column you like.

Demo

Constraints

If relying on a simple interface like this isn't to your taste (which, in some cases, is not desirable), you could opt for a more controlled approach and use a type constraint, in conjunction with the above Get method:

type OrderedCols interface {
    Column[uint64] | Column[int64] | Column[string] // and so on
}

type AllCols interface {
    // the OrderCols constraint, or any type
    Column[any] | OrderedCols
}

With these constraints in place, we can make our Table type a bit safer to use in the sense that we can ensure the Table is guaranteed to only contain actual Column[T] values, not something else that just happens to match the interface. Mind you, we still have some types to erase:

type Table[T AllCols] struct {
    Columns map[string]T
}

tbl := Table[Column[any]]{
    Columns: map[string]Column[any]]{
        c1.Name: c1.Get(), // turn in to Column[any]
        c2.Name: c2.Get(),
    },
}

Demo here

  • Related