0

I have two structs that are very similar and I would like to create functions that can operate on both of them.

I also have two types that are defined as slices of these two structs. This example is simplified. In reality I have function receivers on those struct types too.

I can't get the generics to work mainly because of two reasons. My slice type seem to work with some operations but not all. It can be used with the len() function for example - but it cant be indexed using brackets: [].

The second issue is that I can't figure out how to extract the struct type from the slice type. E.g if I want to iterate over the slice and pass values to a function how do I type that function to accept the structs that the slice contains?

Can I not achieve this with generics in go? If so what would be the idiomatic way to achieve this?

Here is a simplified example of what I'm trying to achieve:

package main

import (
    "fmt"
    "math"
)

type Year struct {
    Revenue  int
    Earnings int
}

type Years []Year

type Quarter struct {
    Revenue  int
    Earnings int
}

type Quarters []Quarter

func revenueGrowth[T Year | Quarter](current, previous T) {
    return (current.Revenue - previous.Revenue) / math.Abs(previous.Revenue) // ERROR: current.Revenue undefined (type T has no field or method Revenue)
}

func earningsGrowth[T Year | Quarter](current, previous T) {
    return (current.Earnings - previous.Earnings) / math.Abs(previous.Earnings) // ERROR: current.Revenue undefined (type T has no field or method Revenue)
}

// printGrowthPerYear prints the revenue growth for each year
func growthPerYear[T Years | Quarters, U Year | Quarter](reports T, selector func(current, previous U) float64) float64 {
    var sum float64
    for i := 0; i < len(reports)-1; i++ {
        sum = sum + selector(reports[i], reports[i+1]) // ERROR: invalid operation: cannot index reports (variable of type T constrained by Years|Quarters)
    }
    return sum
}

func printAverageGrowthOverYears[T Years | Quarters, U Year | Quarter](estimates, reports T, numOfYears int, selector func(current, previous U) float64) {
    combinedReportsAndEstimates := T{} // ERROR: invalid composite literal type T
    // Start by adding estimates to the combined slice
    for i := 0; i < len(numOfYears); i++ {
        combinedReportsAndEstimates := append(combinedReportsAndEstimates, estimates[i]) // ERROR: invalid operation: cannot index estimates (variable of type T constrained by Years|Quarters)
    }

    // Backfill with the latest reports
    numOfReportsToBackfillWith := numOfYears - len(combinedReportsAndEstimates)
    for i := 0; i < numOfReportsToBackfillWith; i++ {
        combinedReportsAndEstimates[i] = reports[i] // ERROR: invalid operation: cannot index reports (variable of type T constrained by Years|Quarters)
    }

    growth := growthPerYear(combinedReportsAndEstimates, selector)
    fmt.Println(growth / float64(numOfYears))
}

func main() {
    years := Years{
        {Revenue: 1000, Earnings: 100},
        {Revenue: 750, Earnings: 75},
        {Revenue: 500, Earnings: 50},
        {Revenue: 250, Earnings: 25},
    }
    estimatedYears := Years{
        {Revenue: 1250, Earnings: 125},
    }
    fmt.Println("========Years==========")
    printAverageGrowthOverYears(estimatedYears, years, revenueGrowth[Year], 4)  // ERROR: cannot infer U
    printAverageGrowthOverYears(estimatedYears, years, earningsGrowth[Year], 4) // ERROR: cannot infer U

    quarters := Quarters{
        {Revenue: 100, Earnings: 10},
        {Revenue: 75, Earnings: 7},
        {Revenue: 50, Earnings: 5},
        {Revenue: 25, Earnings: 2},
    }
    estimatedQuarters := Quarters{
        {Revenue: 125, Earnings: 12},
    }
    fmt.Println("========Quarters==========")
    printAverageGrowthOverYears(estimatedQuarters, quarters, revenueGrowth[Quarter], 2)  // ERROR: cannot infer U
    printAverageGrowthOverYears(estimatedQuarters, quarters, earningsGrowth[Quarter], 2) // ERROR: cannot infer U
}

I've tried to make the functions generic to work with both Year, Years, Quarter and Quarters.

crtv
  • 35
  • 2
  • Does this answer your question? [How can I access a struct field with generics (type T has no field or method)?](https://stackoverflow.com/questions/70358216/how-can-i-access-a-struct-field-with-generics-type-t-has-no-field-or-method) – jub0bs Apr 13 '23 at 10:25
  • @jub0bs I haven't been successful in making this example work by creating interfaces with functions for retrieving each of the properties at least. It fixes parts of it but causes issues elsewhere. The root of the issue for me is how to work with slices of similar looking structs. The solution I have right now is so much code duplication so I would like to make something similar to this example work. – crtv Apr 13 '23 at 11:51
  • you can using interface define GetValue function to make sure 2 struct have same member,but you have define the GetValue function for 4 more lines,i have an example below,you can start fix from that – J CHEN Apr 13 '23 at 14:39

2 Answers2

0

You have a lot of mistakes , i try to figure out what you means

In function printAverageGrowthOverYears i can't figure out what is your real goal

So i Guess It

package main

import (
    "fmt"
    "math"
    "reflect"
)

type M interface {
    GetRevenue() float64
    GetEarnings() float64
}

type Year struct {
    Revenue    float64
    Earnings   float64
    OtherThing string
}
type Quarter struct {
    Revenue      float64
    Earnings     float64
    AnotherThing string
}

func (y Year) GetRevenue() float64 {
    return y.Revenue
}
func (q Quarter) GetRevenue() float64 {
    return q.Earnings
}
func (y Year) GetEarnings() float64 {
    return y.Revenue
}
func (q Quarter) GetEarnings() float64 {
    return q.Earnings
}

type Years []Year
type Quarters []Quarter

func revenueGrowth(current, previous M) float64 {
    return (current.GetRevenue() - previous.GetRevenue()) / math.Abs(previous.GetRevenue())
}

func earningsGrowth(current, previous M) float64 {
    return (current.GetEarnings() - previous.GetEarnings()) / math.Abs(previous.GetEarnings())
}

// printGrowthPerYear prints the revenue growth for each year
func growthPerYear(reports []interface{}, selector func(current, previous M) float64) float64 {
    var sum float64
    for i := 0; i < len(reports)-1; i++ {
        sum = sum + selector(reports[i].(M), reports[i+1].(M))
    }
    return sum
}
func min(a int, b int) int {
    if a < b {
        return a
    }
    return b
}
func printAverageGrowthOverYears(estimates interface{}, reports interface{}, numOfYears int, selector func(current, previous M) float64) {
    estimates_l := reflect.ValueOf(estimates)
    combinedReportsAndEstimates := make([]interface{}, 0)
    for i := 0; i < min(estimates_l.Len(), numOfYears); i++ {
        combinedReportsAndEstimates = append(combinedReportsAndEstimates, estimates_l.Index(i).Interface().(M))
    }
    numOfReportsToBackfillWith := numOfYears - len(combinedReportsAndEstimates)

    reports_l := reflect.ValueOf(reports)
    numOfReportsToBackfillWith = min(reports_l.Len(), numOfReportsToBackfillWith)
    //Here I Guess You Mean This?
    for i := 0; i < numOfReportsToBackfillWith; i++ {
        combinedReportsAndEstimates = append(combinedReportsAndEstimates, reports_l.Index(i).Interface().(M))
    }
    growth := growthPerYear(combinedReportsAndEstimates, selector)
    fmt.Println(growth / float64(numOfYears))

}
func main() {
    years := Years{
        {Revenue: 1000, Earnings: 100},
        {Revenue: 750, Earnings: 75},
        {Revenue: 500, Earnings: 50},
        {Revenue: 250, Earnings: 25},
    }
    estimatedYears := Years{
        {Revenue: 1250, Earnings: 125},
    }
    fmt.Println("========Years==========")
    printAverageGrowthOverYears(estimatedYears, years, 4, revenueGrowth)  // ERROR: cannot infer U
    printAverageGrowthOverYears(estimatedYears, years, 4, earningsGrowth) // ERROR: cannot infer U

    quarters := Quarters{
        {Revenue: 100, Earnings: 10},
        {Revenue: 75, Earnings: 7},
        {Revenue: 50, Earnings: 5},
        {Revenue: 25, Earnings: 2},
    }
    estimatedQuarters := Quarters{
        {Revenue: 125, Earnings: 12},
    }
    fmt.Println("========Quarters==========")
    printAverageGrowthOverYears(estimatedQuarters, quarters, 2, revenueGrowth)  // ERROR: cannot infer U
    printAverageGrowthOverYears(estimatedQuarters, quarters, 2, earningsGrowth) // ERROR: cannot infer U
}
J CHEN
  • 494
  • 3
  • 9
0

just to expand on the answer of "@J Chen", your issue is discussed in this GitHub issue link. I strongly suggest you read the full thread of Questions and Answers to deep dive into your issue and into the possible workarounds.
Another thing you can do is use struct embedding and create a parent with only the two relevant fields. In this scenario, you should be able to achieve what you need without using generics.
If you want to stick to generics, just create an interface with two getter methods in its type set, use it to force the type passed in to satisfy the constraint, and, within the function body, invoke the getter method that must be implemented by the specific struct.
Let me know if you still need help, thanks!

ossan
  • 1,665
  • 4
  • 10