Search code examples
gogenericsfunctional-programming

Getting the underlying type of slice passed as type argument to a type parameter in the receiver of a method


I am creating a data structure using Go generics.

The use case is to write a Reduce function that takes a slice of a defined type, applies a reduce function to each element of the slice and returns a value of the defined type.

type MyType[T any] struct{
    I T
}

// I want to write something like this
func (m *MyType[[]T]) Reduce(init T, fnReduce func(T, T)T) (*MyType[T]) {
    acc := init
    for _, v := range m.I {
        acc = fnReduce(acc, v)
    }
    return &MyType[T]{I : acc}

}

Go compiler does not allow to give the type parameter to the receiver as: m *MyType[[]T]

What is a way to solve this?


Solution

  • If you want to have a method on a slice to reduce it, define MyStruct as a slice of T:

    type MyType[T any] []T
    
    func (m MyType[T]) Reduce(init T, fnReduce func(acc, elem T) (result T)) T {
        res := init
        for _, v := range m {
            res = fnReduce(res, v)
        }
        return res
    }
    

    Here an example on the Playground.


    In case you need to be able to add other fields to MyStruct apart from the slice itself, a struct with a field containing the slice is also an option. (New func to populate the private slice field from outside the package.)

    func NewMyType[T any](sl []T) MyType[T] {
        return MyType[T]{sl: sl}
    }
    
    type MyType[T any] struct {
        sl []T
    }
    
    func (m MyType[T]) Reduce(init T, fnReduce func(acc, elem T) (result T)) T {
        res := init
        for _, v := range m.sl {
            res = fnReduce(res, v)
        }
        return res
    }
    

    On the issue stated in the comment:

    1. First idea is to call a reduce function whenever the need is identified to reduce the data. This works if there is a place where we know the data is of type slice. If that is never the case (all code is generic), then have a look at option 2).
    func reduce[T any](sl []T, init T, fnReduce func(acc, elem T) (result T)) T {
        res := init
        for _, v := range sl {
            res = fnReduce(res, v)
        }
        return res 
    }
    
    1. The second idea is to have a old fashioned interface that is implemented differently for different data types.
    type MyI[T any] interface {
        Reduce(init T, reduce func(acc, elem T) (result T)) T
    }
    
    type MySliceType[T any] struct {
        data []T
    }
    
    func (m MySliceType[T]) Reduce(init T, reduce func(acc, elem T) T) T {
        res := init
        for _, v := range m.data {
            res = reduce(res, v)
        }
        return res
    }
    
    type MyType[T any] struct {
        data T
    }
    
    func (m MyType[T]) Reduce( T,  func(acc, elem T) T) T {
        return m.data
    }
    

    We have a common Reduce function here, just in case we don't have a slice it just return m.data. If we have a slice, it actually applies the reduce function.

    (The example doesn't make too much sense, but I hope the idea comes across.)