-1

While writing two methods (one for a slice and one for a string map) I realized the implementation for both methods is identical and the only thing that changes is the prototype of the functions.

I'm trying to avoid repeating it and originally I thought of the following (see the FIXME part):

package main

import (
    "fmt"
    "strings"
)

type SomethingList []*Something
type SomethingMap map[string]*Something

type Something struct {
    ID   string
    Type int
}

func (sl SomethingList) GetIDsString() string {
    return getIDsString(sl)
}

func (sm SomethingMap) GetIDsString() string {
    return getIDsString(sm)
}

func getIDsString(elements interface{}) string {
    var ids []string

    // FIXME: Yes, I know I can't iterate over an interface
    for element = range elements {
        ids = append(ids, element.ID)
    }
    return strings.Join(ids, ",")
}

func main() {
    s1 := Something{ID: "ABC", Type: 1}
    s2 := Something{ID: "DEF", Type: 1}

    sl := SomethingList{&s1, &s2}
    sm := SomethingMap{s1.ID: &s1, s2.ID: &s2}
    fmt.Println(sl.GetIDsString())
    fmt.Println(sm.GetIDsString())
}

The important part is the function getIDsString which basically takes the ID field of the struct and concatenates it's content across all the members of the slice or map.

I realize now after reading a bit about how interfaces work (yes, I'm quite a newbie in Go as is probably obvious already :-)) that this is not going to work, as Go is statically typed and I can't simply change the types on runtime. Also the interfaces don't allow me to iterate. I've been able to get close using a loop that iterates using the Len() method on the reflect.ValueOf and Index() to access each element. But Index() doesn't work on the string map.

What would be the most idiomatic way of solving this without duplicating much code?

Thanks!

Towerman
  • 185
  • 1
  • 4
  • 1
    as of current example with limited number of implementations, duplicate the code and create the appropriate execution path for both types is the best solution. Then, if you are repeating it over and over for many more types, you might want evaluate code gen or reflect indeed. –  Jul 25 '21 at 19:42
  • `But Index() doesn't work on the string map.` see https://stackoverflow.com/a/53159340/4466350 –  Jul 25 '21 at 19:43
  • 1
    maps provide key/value lookup, and array provide ordered lists. You can abstract them to the point of an iterable set, but the value of both types is then lost. – erik258 Jul 25 '21 at 21:28

1 Answers1

0

In general repetition of small part of the code in golang is quite common. But in case you have a large amount of duplicate code, you can have that logic in one structure, and ad hoc transform the second structure to the first one to invoke that logic:

package main

import (
    "fmt"
    "strings"
)

type (
    SomethingList []*Something
    SomethingMap  map[string]*Something

    Something struct {
        ID   string
        Type int
    }
)

func (sl SomethingList) GetIDsString() string {
    ids := make([]string, len(sl))
    for i := range sl {
        ids[i] = sl[i].ID
    }
    return strings.Join(ids, ",")
}

func (sm SomethingMap) GetIDsString() string {
    l := make(SomethingList, len(sm))
    i := 0
    for key := range sm {
        l[i] = sm[key]
        i++
    }
    return l.GetIDsString()
}

func main() {
    s1 := Something{ID: "ABC", Type: 1}
    s2 := Something{ID: "DEF", Type: 1}

    sl := SomethingList{&s1, &s2}
    sm := SomethingMap{s1.ID: &s1, s2.ID: &s2}
    fmt.Println(sl.GetIDsString())
    fmt.Println(sm.GetIDsString())
}

Alternatively you could decouple the creation of IDsString from the structure itself in a following way.

package main

import (
    "fmt"
    "strings"
)

type (
    SomethingList []*Something
    SomethingMap map[string]*Something

    Something struct {
        ID   string
        Type int
    }

    somethingIterator interface {
        ForEach(func(value Something))
    }
)

func (sl SomethingList) ForEach(f func(value Something)) {
    for i := range sl {
        f(*sl[i])
    }
}

func (sm SomethingMap) ForEach(f func(value Something)) {
    for key := range sm {
        f(*sm[key])
    }
}

func GetIDsString(iterator somethingIterator) string {
    var ids []string
    iterator.ForEach(func(value Something) {
        // Some sophisticated logic is here.
        ids = append(ids, value.ID)
    })
    return strings.Join(ids, ",")
}

func main() {
    s1 := Something{ID: "ABC", Type: 1}
    s2 := Something{ID: "DEF", Type: 1}

    sl := SomethingList{&s1, &s2}
    sm := SomethingMap{s1.ID: &s1, s2.ID: &s2}
    fmt.Println(GetIDsString(sl))
    fmt.Println(GetIDsString(sm))
}

Second approach allows to avoid extra intermediate structure creation which could be beneficial for big list/map.

Jaroslaw
  • 636
  • 3
  • 8
  • `Second approach allows to avoid extra intermediate structure creation which could be beneficial for big list/map.` On the other hand, f you have that big of a list, and you have not initialized upfront the length and capcity of the destination storage, you will have more JIT allocations, which is slower. –  Jul 25 '21 at 21:09
  • A reasonable thing to do is to implement such iterator using reflection with fast path support for all base types. but i have not found such package with fast paths. –  Jul 25 '21 at 21:10
  • 1
    or to wait for generics! –  Jul 25 '21 at 21:10
  • Yes, but you can extend interface with the `Count() int` method and use it to optimize the allocation. As i understand the provided method in question is only a demonstration, so instead of creating the ids glued with comma you may want to calculate statistics using `Type` field, thus i have not decided to put `Count() int` method. – Jaroslaw Jul 25 '21 at 21:14