Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: math: MinOf and MaxOf for generic numeric limits #50019

Open
rogpeppe opened this issue Dec 7, 2021 · 14 comments
Open

proposal: math: MinOf and MaxOf for generic numeric limits #50019

rogpeppe opened this issue Dec 7, 2021 · 14 comments

Comments

@rogpeppe
Copy link
Contributor

rogpeppe commented Dec 7, 2021

Currently when writing a function involving a generic numeric type, there's no easy way to determine the maximum or minimum possible value of that type, because the math.Min* and math.Max* constants are all type-specific.

I propose that we add the following functions to the math package:

// MinOf returns the minimum possible value of type T.
func MinOf[T contraints.Integer | constraints.Float]() T

// MaxOf returns the maximum possible value of type T.
func MaxOf[T contraints.Integer | constraints.Float]() T

It's currently not possible to write the above functions without using reflection until #45380
is fixed, but here's an approximation:

https://gotipplay.golang.org/p/0i3Wc7i4dJs

@robpike
Copy link
Contributor

robpike commented Dec 7, 2021

To do this with any kind of efficiency in implementation code, generated code, and execution time seems unlikely. Plus you really want this to be a constant, which won't happen for a function.

So this feels like the wrong approach to me, but the right approach might require some kind of language support if we are to avoid type-asserting from interface{} on the way out.

@robpike
Copy link
Contributor

robpike commented Dec 7, 2021

P.S. That said, I'm not convinced it's worthwhile to do. I know what it's for but will it show up often enough to need so much mechanism?

@Inuart
Copy link

Inuart commented Dec 7, 2021

the right approach might require some kind of language support

IMHO it's essential that somebody writes a crazy idea that everybody can reject. So here I go:

const (
	MaxOf[T number] {
		int: math.MaxInt,
		int8: math.MaxInt8,
		...
	}
	MinOf[T number] {
		int: math.MinInt,
		int8: math.MinInt8,
		...
	}
	...
)

To be used like

func MaxSubSum[T number](arr []T) T {
	best := MinOf[T]
	var current T
	for _, v := range arr {
		current = Max(v, current+v)
		best = Max(best, current)
	}
	return best
}

@rogpeppe
Copy link
Contributor Author

rogpeppe commented Dec 7, 2021

To do this with any kind of efficiency in implementation code, generated code, and execution time seems unlikely. Plus you really want this to be a constant, which won't happen for a function.

In general we don't allow constants to be derived from type parameters (that way lies madness), but I think it could be done with reasonable efficiency if #45380 was implemented.

In that case, the code might look like this:

func MinOf[T constraints.Integer | constraints.Float]() T {
	switch type T {
	case ~int:
		return math.MinInt
	case ~int8:
		return math.MinInt8
	...
	}
}

I have a feeling it wouldn't take too smart a compiler to implement it as a direct lookup from the dictionary, so not as efficient as a constant (we don't allow constants based on type parameters anyway), but one indirection still isn't too bad.

@renthraysk
Copy link

renthraysk commented Dec 7, 2021

Did play around trying to get something similar, though just for constraints.Integer. With inlining they effectively a constant, though the inliner does assign them a higher inlining cost.

https://gotipplay.golang.org/p/Hw4hnZq3imw

Edit: Typo in code, should not have had the go vet warning.

@robpike
Copy link
Contributor

robpike commented Dec 7, 2021

In general we don't allow constants to be derived from type parameters (that way lies madness)

What I was getting at, incoherently, was that type parameters might not be the way to do this. The fact that a problem arises because of them does not necessarily mean the solution requires them too.

@g-talbot
Copy link

What's being asked for here is the Go equivalent of the C++ numeric_limits package. That doesn't seem unreasonable? If I'm implementing a generic function on [T constraints.Signed] I might want to return an error (etc.) instead of overflowing the type. Is the issue that the generated code for func X[T constraints.Signed](v T) T {...} is the same function when making the calls X(int64(1)), X(int32(1)), X(int16(1)), X(int8(1)), etc. because these will fall into the same GC shape?

@jdemeyer
Copy link

This works for signed and unsigned integers:

func MinOfInt[T constraints.Integer]() T {
    var zero T
    minusone := ^zero
    if minusone > 0 {
        return zero // Unsigned
    }
    bits := 8 * unsafe.Sizeof(zero)
    return minusone << (bits - 1) // Signed
}

func MaxOfInt[T constraints.Integer]() T {
    return ^MinOfInt[T]()
}

@csm10495
Copy link

csm10495 commented Apr 28, 2023

I'm kind of new to golang, but this type of functionality seems like something that should be an included battery to a language standard library.

Ultimately I guess the specifics are less important to me, but generic min/max functions make sense somewhere in the standard library.

@ianlancetaylor
Copy link
Contributor

Note that this proposal is not about generic min/max functions. That is #59488. This proposal is about functions to return the minimum and maximum values of numeric types.

@csm10495
Copy link

Note that this proposal is not about generic min/max functions. That is #59488. This proposal is about functions to return the minimum and maximum values of numeric types.

Ah whelp.. sorry folks.

@Merovius
Copy link
Contributor

Merovius commented Jan 10, 2024

I'm wondering a little bit about the use cases for this. Personally, I encountered this in one situation so far: A mathematical algorithm implemented as a generic function, which I wanted to test using a type-parametric helper that could also check some boundary cases. In other words: Nothing that really justifies a language change, but also nothing that requires a constant expression or anything efficient.

For overflow checks, I prefer checking int64(T(v)) == v anyways (assuming that I got v as an int64 out of some decoding function).

When asking on Slack, @justenwalker helped coming up with this, which is not a constant, but seems reasonably efficient:

type Integer interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

func Max[T Integer]() T {
	if ^T(0) > 0 {
		return ^T(0)
	}
	var v T
	bits := 8 * unsafe.Sizeof(v)
	return 1<<(bits-1) - 1
}

func Min[T Integer]() T {
	if ^T(0) > 0 {
		return 0
	}
	var v T
	bits := 8 * unsafe.Sizeof(v)
	return (^v) << (bits - 1)
}

[edit] oh, sorry, just saw that @jdemeyer already posted something like this [/edit]

@Inuart
Copy link

Inuart commented Jan 10, 2024

I'm wondering a little bit about the use cases for this.

Back in 2021 I wrote my use case in this comment. IMHO some algorithms seem to need boundary values to behave correctly (how would you implement MaxSubSum without some MinOf[T] or the mentioned type switch on a parametric type? How do I handle len(arr) == 0?).


bits := 8 * unsafe.Sizeof(v)

I feel like if Min/Max requires unsafe then it would be great to avoid importing the unsafe pkg by hiding it in the stdlib. Also, what about non integers? Would I need to write a separate MaxSubSumFloat too?

@Merovius
Copy link
Contributor

Also, what about non integers?

You might be able to generalize the ideas from the integer function. For example, floats can be distinguished by if T(1) / 2 == 0. float32 and float64 could be distinguished via T(math.SmallestNonzeroFloat32)/2 == 0. Both assuming the compiler would optimize them out. Though there is some difficulty in making sure that the operators are defined on the complete set of numeric types, I'm not sure that works, yet.

I do agree that it's enough subtlety to justify doing it in the stdlib, anyways. i.e. I wouldn't expect anyone to be able to implement these correctly themselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests

11 participants