#What's the "correct" way to implement "setter" and "getter" methods with "union" types?

21 messages · Page 1 of 1 (latest)

tiny path
#

(Yes I know that Go doesn't have setters, getters and classic union types. And that generics aren't allowed for methods)

In TypeScript I have the following setup:

export class Stat {
  protected _min?: number | Stat;

  get min(): undefined | number | Stat {
    return this._min;
  }

  set min(newMin: undefined | number | Stat) {
    // some code
  }
}
// Which makes it easy to get and set min value.
// somewhere after:
foo.min = 10;
foo.min = bar;
if (foo.min === undefined) {
  doX();
} else if (foo.min instanceof Stat) {
  doY();
} else {
  doZ();
}

What's the "correct" way to implement this in Go? Both of my current ideas feel clunky. I know that "correct" is subjective, but still.
I also don't like that I essentially have no compile-time safety for SetMin method

Option 1 - any

type Stat struct {
  min any
}

func (s *Stat) Min() any {
  return s.min
}

func (s *Stat) SetMin(newMin any) {
  // some code
}

// somewhere after:
foo.SetMin(10)
foo.SetMin(bar)
switch v := foo.min.(type) {
case nil:
  doX()
case *Stat:
  doY()
case float64:
  doZ()
}

Option 2 - struct in struct:

type StatBoundary struct {
  number float64
  stat *Stat
}

type Stat struct {
  min *StatBoundary
}

func (s *Stat) Min() *StatBoundary {
  return s.min
}

func (s *Stat) SetMin(newMin any) {
  // some code
}

// somewhere after:
foo.SetMin(10)
foo.SetMin(bar)
if foo.min == nil {
  doX()
} else if foo.min.stat != nil {
  doY()
} else {
  doZ()
}

Or maybe have several SetMin methods?

func (s *Stat) SetMin(newMin float64) {
  // some code
}
func (s *Stat) SetMinStat(newMin *Stat) {
  // some code
}
// somewhere after:
foo.SetMin(10)
foo.SetMin(bar)
foo.SetMinStat(nil)
crystal flower
#

given there are indeed no union type and unless you want to go the unsafe route (you could, if memory was precious) I would just have your 2 types in 2 different fields?

I also wouldn't use any (that's too wide, you could pass a completely unrelated type and then ... panic)

So it leaves you with 2 setters (given indeed there is no generic for same type...

though depending what it's for maybe actually having 2 types would be better)

tiny path
#

Ok so 2 setters, I saw that approach in Go somewhere. 2 fields in Stat struct. And.. 2 getters? MinNumber() and MinStat()?
something like:

if foo.MinStat() != nil {
  doX()
} else if foo.MinNumber() != math.Inf(-1) {
  doY()
} else {
  doZ()
}
crystal flower
#

you could potentially have a common interface for what get returns, depend what you'd do next with the returned thing

#

I don't know TS so...

random spoke
#

also why you want the union type include itself

#

You are not providing enough informations, so the only thing I can guess is you want to "inherit" another Stat if the min was not set to a specific value. Therefore, I'd do this:

type Stat struct {
  parent  *Stat
  minFlag bool
  min     float64
}

func (s *Stat) Min() (v float64, avaliable bool) {
  if s.minFlag {
    return s.min, true
  }
  if s.parent == nil {
    return 0, false
  }
  return s.parent.Min()
}

func (s *Stat) SetMin(v float64) {
  s.minFlag = true
  s.min = v
}

func (s *Stat) Parent() *Stat {
  return s.stat
}

func (s *Stat) SetParent(parent *Stat) {
  // maybe do some cycle check here depends on if the input is trustable
  s.parent = parent
}
tiny path
# random spoke why you want a union type?
maxHealth = &Stat{}
maxHealth.setMin(0)
maxHealth.setMax(100)
health = &Stat{}
health.setMin(0)
health.setMax(maxHealth)

Stats have base value, min/max boundaries and final value (that depends on base + some modifiers and then capped by boundaries)

random spoke
#

although I don't understand why you give both maxHealth and health a boundary?

#

or in realistic your Stat looks like

type Stat struct {
  min   float64
  max   float64
  value float64
}

?

tiny path
#

maxHealth stat can be affected by modifiers like -10, +25% etc. So it changes depending on current effects. Boundaries ensure that it remains in safe range, cause it shouldn't be less than 0 for example.

random spoke
tiny path
random spoke
#

You can defined an interface like this

type Float64Value interface {
  Float64() float64
}

type ConstantFloat64 float64

func (v ConstantFloat64) Float64() float64 {
  return v
}

type Stat struct {
  min       Float64Value
  max       Float64Value
  value     Float64Value
  modifiers []func(float64) float64
}

func NewStat() *Stat {
  return &Stat{
    min:   ConstantFloat64(0),
    max:   ConstantFloat64(0),
    value: ConstantFloat64(0),
  }
}

func (s *Stat) Float64() float64 {
  v := s.value.Float64()
  for _, mod := range modifiers {
    v = mod(v)
  }
  return min(max(v, s.min.Float64()), s.max.Float64())
}
#

where both *Stat and ConstantFloat64 implemented the Float64Value interface

#

so you can freely pass a constant float64 after you wrapped it with the ConstantFloat64 type, or you also can directly set min/max/value to a *Stat.

#

I do not know if the value is necessary to become "unioned" but if it's not you can still keep it as basic float64

tiny path
#

Interesting approach, thank you, I'll try it this way

random spoke
#

You are welcome.
Whenever you think you need to use union to solve something, you can find the common properties they share. For example, in above float64 and Stat both provide a float64 number to participate in some calculations. Then you can define a interface based on their common operations/properties, and eventually do operation on the interface instead of type switch on various stuffs.