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

Make min/max use isless even for Real #51356

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

timholy
Copy link
Member

@timholy timholy commented Sep 17, 2023

In #23155, we made the generic min/max use isless rather than <. This is motivated by the observation that the implementation

min(a, b) = cmp(a, b) ? a : b

implies that cmp satisfies the same properties as isless (either cmp(a, b) in which case we return a, otherwise either cmp(b, a) or they are equal, in which case returning b is correct).

However, this is not true for Real types, including AbstractFloat (due to NaN). Yet currently the default for Real uses <.

Part of the motivation here is that many packages seem to subtype Real when, theoretically, perhaps they shouldn't. Prominent examples include IntervalArithmetic.Interval and ForwardDiff.Dual. An example using intervals that would fail is the following: min(1..3, 2..4): the two intervals have non-empty intersection, so it's not/shouldn't be true that 1..3 < 2..4, in which case 2..4 would be returned (which is wrong by any measure).

To be honest, I don't expect this to be mergeable, as I bet it is breaking. But I think it's worth seeing if it passes tests (I haven't tried locally) and perhaps a PkgEval run.

In #23155, we made the generic `min/max` use `isless` rather than `<`.
This is motivated by the observation that the implementation

    min(a, b) = cmp(a, b) ? a : b

implies that `cmp` satisfies the same properties as `isless` (either
`cmp(a, b)` in which case we return `a`, otherwise either `cmp(b, a)`
or they are equal, in which case returning `b` is correct).

However, this is not true for `Real` types, including AbstractFloat
(due to `NaN`).

Part of the motivation here is that many packages seem to subtype
`Real` when, theoretically, perhaps they shouldn't. Prominent examples
include `IntervalArithmetic.Interval` and `ForwardDiff.Dual`.
@brenhinkeller brenhinkeller added the maths Mathematical functions label Sep 17, 2023
@timholy
Copy link
Member Author

timholy commented Sep 18, 2023

@nanosoldier runtests()

@nanosoldier
Copy link
Collaborator

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

@simeonschaub
Copy link
Member

Does this mean min(1., NaN) would be 1. whereas max(1., NaN) is still NaN? (ref #7866 which I ironically stumbled upon through https://2pi.dk/2016/05/ieee-min-max)

I wonder whether with this change we could get rid of the "bad zero" shenanigans in

isbadzero(::typeof(max), x::AbstractFloat) = (x == zero(x)) & signbit(x)
though

@timholy
Copy link
Member Author

timholy commented Sep 21, 2023

@nanosoldier runtests(["Pyehtim", "FMI", "RemoteSensingToolbox", "ControlSystemsMTK", "GeometricIntegrators", "FourierSpaces", "OptimKit", "Controlz", "GeoIO", "MCPTrajectoryGameSolver", "Test", "TaijaPlotting", "QuantumCumulants", "FiniteVolumeMethod", "LoopVectorization"])

@timholy
Copy link
Member Author

timholy commented Sep 21, 2023

Does this mean min(1., NaN) would be 1. whereas max(1., NaN) is still NaN?

There are two methods higher in the priority list:

julia> methods(min, Tuple{T,T} where T<:Real)
# 3 methods for generic function "min" from Base:
 [1] min(x::T, y::T) where T<:Union{Float32, Float64}
     @ Base.Math math.jl:894
 [2] min(x::T, y::T) where T<:AbstractFloat
     @ Base.Math math.jl:878
 [3] min(x::T, y::T) where T<:Real
     @ promotion.jl:533

That said, you raise an interesting point. TBH I'm not really an expert on the nuances of < vs isless; naively I would have expected that either < or isless would be implemented, not both. Perhaps the right way to solve this would be to only have < but use a trait to encode whether the order is partial or total, and then dispatch on that for algorithms. (Any algorithm that requires a total order, but one is not available, could MethodError.) But of course that's not within the realm of possibility at the present time.

In other words, the way that I'd like for this to work is to implement min/max using isless, which implies a total order, and then any other types that don't have a total order need to come up with their own implementation of min/max---and they get notified that this is required via the MethodError about missing an implementation of isless. That would avoid the bizarre behavior you noted.

@nanosoldier
Copy link
Collaborator

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maths Mathematical functions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants