golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.38k stars 17.7k forks source link

proposal: math: Reflect, Least, Greatest #60274

Open jimmyfrasche opened 1 year ago

jimmyfrasche commented 1 year ago
package math

func Reflect[T Number]() Kind
type Kind uint
func (Kind) Size() int
func (Kind) Ordered() bool
func (Kind) Float() bool
func (Kind) Complex() bool
func (Kind) Signed() bool
func (Kind) Unsigned() bool
func (Kind) Integer() bool

func Least[T Ordered]() T
func Greatest[T Ordered]() T

func Convert[S, T Number](T) (S, bool)

Reflect returns a Kind bitset that provides information about the properties of a numeric type. The bool returning methods correspond with the constraints and Size returns 8, 16, etc. Since this is about the properties of the type not its identity Reflect[int]() == Reflect[int64]() on platforms where int is 64 bits.

Least (Greatest) return the element that is <= (>=) every element in T. For example, Greatest[float64]() is math.Inf(0), Least[uint]() is 0, and Greatest[int16] is math.MaxInt16.

Convert performs the conversion between numeric types S and T and the boolean reports whether the conversion was lossless, returning false if there was any truncation or wrapping.

Future versions of the language may allow Reflect to be written using type switches but this would still provide a more convenient interface.


For convenience, I've assumed #52427 is accepted and that Unsigned, Signed, Integer, Float, and Complex have been moved into package math.

I have also assumed that these additional constraints are defined:

type Ordered Integer | Float
type Number Ordered | Complex

The Ordered constraint is similar to that in package cmp except that it does not include ~string. The package qualification should be sufficient to disambiguate.

If this api is too much or too different for math, it, and all the constraints, could go into a new package, math/number or the like.

fzipp commented 1 year ago

It's possible to get this information with real reflect but that is heavyweight.

Why would this one be less heavyweight? What would it do differently?

jimmyfrasche commented 1 year ago

Unlike reflect.Type, math.Type would only have finitely many values. With language support the body would probably look something like:

switch T.(type) {
case ~uint8:
  return uint8Type

Even if it needs to resort to skullduggery and reach-into-the-runtime or teach-the-compiler-a-trick magic to work around the current inability to write that in Go itself, I'd imagine that it would still retain the essence of being a simple lookup.

earthboundkid commented 1 year ago

At this point, math contains almost all float64 stuff. I think if there's a package with generic math stuff, it should go in a new package, and the old math package can be considered implicitly "math/float64s". Maybe math/numeric.Types/Reflect()/Min()/Max().

jimmyfrasche commented 1 year ago

If it needs a new package, that's fine by me. math does already have untyped constants for all the Min/Max values, though.

zephyrtronium commented 1 year ago

I created https://github.com/zephyrtronium/number as a demonstration of the proposed Reflect function. It does perform faster than reflection, but unless there is some reason to believe the layout of internal/abi.Type will change in the future, I don't think it needs to be in the standard library.

jimmyfrasche commented 1 year ago

First, super cool!

Second, I do think even if the abi remains stable I'd feel uneasy relying on a third party dep making those kind of assumptions and playing those kinds of shenanigans.

Third, I think regardless of implementation having it part of the stdlib is worthwhile to have it in a centralized place, especially one that can be updated in lockstep if something like, for example 128 bit ints get added to the language.

jimmyfrasche commented 1 year ago

Here's a small example of use. It provides a fully correct generic variadic min. It works correctly even if len(vs) == 0 and uses math.Min instead of < for floating point types:

func Min[T notComplex](vs ...T) T {
  min := math.Greatest[T]()

  if math.Reflect[T]().Float() {
    // T = ~float32 | ~float64
    // so these are always lossless conversions
    min := float64(min)
    for _, v := range vs {
      min = math.Min(min, float64(v))
    }
    return T(min)
  }

  for _, v := range vs {
    if v < min {
      min = v
    }
  }
  return min
}
fzipp commented 1 year ago

With language support the body would probably look something like:

switch T.(type) {
case ~uint8:
  return uint8Type

You can't switch on a generic type like that. If you're proposing a language change ("With language support ...") you should say so.

jimmyfrasche commented 1 year ago

@fzipp I was saying that IF there were such a language change the code would, then, in that hypothetical scenario, just be a big ole switch.

jimmyfrasche commented 1 year ago

@zephyrtronium a more concrete argument against relying on the internals is that alternate Go implementations may not use the same representation internally but if it's in std each such implementation can wire it in appropriately.

jimmyfrasche commented 1 year ago

If it does get its own package, it could hold the various number-y constraints, too. Something like:

package number

type Signed ~int | int8 | ⋯
type Unsigned ~uint | uint8 | ⋯
type Integer Signed | Unsigned
type Float ~float32 | ~float64
type Ordered Integer | Float
type Complex ~complex64 | ~complex128
type Type Ordered | Complex

type Kind uint
// A bunch of methods on Kind
func Reflect[T Type]() Kind

func Least[T Ordered]() T
func Greatest[T Ordered]() T

That gives you a lot of what you need to write generic numeric algorithms in one place without filling math up with a bunch of definitions.

jimmyfrasche commented 1 year ago

A nice to have would be (using the definition of Type in the previous post, not the one in the first post)

func Convert[S, T Type](T) (v S, lossless bool)
func CanConvert[S, T Type](T) (lossless bool)

that returns true if converting back to T produces something == to the original value and false if there was any rounding/overflow/etc. That's easy to check by doing the reverse conversion but it's awkward and doesn't read well, imo.

zephyrtronium commented 1 year ago

@jimmyfrasche I feel like the actual proposal here is drifting substantially, so I can't tell whether I'm +1. It might be to everyone's benefit to update the title and first post with exactly the API you're suggesting.

jimmyfrasche commented 1 year ago

Sorry. Reflect/Least/Greatest are the main thing.

I'm fine for those going in math. If they go in their own package then that package could be a good place to put the numeric constraints as well.

The Convert/CanConvert functions are semi-related functionality that seem like a good fit with Reflect/Least/Greatest, wherever those end up.

I'll think about how best to clear that all up.

golightlyb commented 1 year ago

Can you clarify, for a float, is Least math.Inf(-1) or minus math.MaxFloat32/64? I don't think it matters too much which, just think it should be documented.

jimmyfrasche commented 1 year ago

math.Inf(-1) otherwise that would be < than the result of Least, which does not seem useful.

jimmyfrasche commented 1 year ago

updated the proposal

crisman commented 1 year ago

Related NaN min/max issue in #60616

jimmyfrasche commented 1 year ago

Overlap with #50019 that proposes just Greatest/Least (under different names) and contains some useful and relevant discussion