Home > database >  Generic function to set field of different structs used as map values
Generic function to set field of different structs used as map values

Time:12-13

Having structures with common fields...

type Definition struct {
        Id string
        ...
}
type Requirement struct {
        Id string
        ...
}
type Campaign struct {
        Id string
        ...
}

...I have multiple functions like this:

func fillDefinitionIds(values *map[string]Definition) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}
func fillRequirementIds(values *map[string]Requirement) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}
func fillCampaignIds(values *map[string]Campaign) {           
        for key, value:=range *values { // Repeated code
                value.Id=key            // Repeated code
                (*values)[key]=value    // Repeated code
        }                               // Repeated code
}

I would like to have a single function, generalizing the access with generics (or interfaces, whatever), kind of...

func fillIds[T Definition|Requirement|Campaign](values *map[string]T) {           
        for key, value:=range *values {
                value.Id=key
                (*values)[key]=value
        }                                
}

Of course, this gives value.Id undefined (type T has no field or method Id). I've been able many times to overcome similar issues, but this time I can't find a solution for this.

How can be this set of functions be abstracted as a single one?

CodePudding user response:

Compose your structs with a struct that includes the common field(s), and define a setter method on that common type:

type Base struct {
    Id string
}

func (b *Base) SetId(id string) {
    b.Id = id
}

type Definition struct {
    Base
}
type Requirement struct {
    Base
}
type Campaign struct {
    Base
}

Then define the interface constraint as a union of pointer types, and specify the setter method. You must do this, because generics field access isn't available in the current version of Go.

type IDer interface {
    *Definition | *Requirement | *Campaign
    SetId(id string)
}

func fillIds[T IDer](values map[string]T) {
    for key, value := range values {
        value.SetId(key)
        values[key] = value
    }
}

Example: https://go.dev/play/p/fJhyhazyeyc

func main() {
    m1 := map[string]*Definition{"foo": {}, "bar": {}}
    fillIds(m1)
    for _, v := range m1 {
        fmt.Println("m1", v.Id) 
        // foo
        // bar
    }

    m2 := map[string]*Campaign{"abc": {}, "def": {}}
    fillIds(m2)
    for _, v := range m2 {
        fmt.Println("m2", v.Id)
        // abc
        // def
    }
}

CodePudding user response:

Generics are for cases where the same code works for any number of types, like:

func Ptr[T any](v T) *T {
    return &v
}

You're wanting to use generics to actually modify specific fields in a number of different types, then generics aren't really the way to go. That's essentially not what they're intended to be used for, and golang already has features that allow you to do just that: Composition and interfaces.

You have identified shared fields, great, so create a type and embed it where needed:

type Common struct {
    ID    string
}

type Foo struct {
    Common
    FooSpecificField  int64
}

type Bar struct {
    Common
    BarOnly    string
}

Now add a setter on the common type:

func (c *Common) SetID(id string) {
    c.ID = id
}

Now all types that embed Common have an ID field, and an setter to go with it:

f := Foo{}
f.SetID("fooID")
fmt.Println(f.ID) // fooID
b := Bar{}
b.SetID("barID")
fmt.Println(b.ID) // barID

To accept a map of all types that allow you to set an ID, all you really need to do is make fillIds accept the required interface:

type IDs interface {
    SetID(string)
}

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k, v := range vals {
        v.SetID(k)
        vals[k] = v
    }
    return vals
}

Because setters should, by definition, be pointer receivers, you could probably write the same function even shorter:

func fillIDs(vals map[string]IDs) map[string]IDs {
    for k := range vals {
        vals[k].SetID(k)
    }
    return vals
}

Using interfaces indicates that this function wants to interact with the objects you pass to it, through a known/defined interface. Using generics indicates that you're expected to provide data that will be used as a whole. Setting fields is not using data as a whole, hence I'd argue generics aren't the right tool for the job. Generics can be very powerful, and extremely useful in certain cases. A while back, I posted a review on code review about generics to create a concurrent-safe map. That's an excellent use-case for generics, so much so that I ended up implementing such a type in response and put it up on github

I thought I'd mention this that I don't oppose generics at all. They can be very useful. The thing I object to is the over-use of the feature, which can -and often does- lead to code that is smelly, and harder to read/maintain.

  • Related