There is a small but important tangle of issues around the interaction between min and max operations and Comparable and FloatingPoint protocols and the IEEE 754 standard that I would like to attempt to resolve.
This is a subtle subject. I will attempt to give a good overview of all the issues involved. For the purposes of this discussion, the behavior of comparisons (==, !=, etc) is out of scope. I am focused on min and max operations. Please read the entire post and attempt to stay focused.
IEEE 754 (2008)
The 2008 revision of 754 defined the following four operations (these are IEEE 754 operations, not Swift functions, but I am translating the interface from IEEE 754 pseudocode into Swift for familiarity):
func minNum<T>(_ x: T, _ y: T) -> Tfunc maxNum<T>(_ x: T, _ y: T) -> Tfunc minNumMag<T>(_ x: T, _ y: T) -> Tfunc maxNumMag<T>(_ x: T, _ y: T) -> T
These are defined to return the lesser / greater / lesser in magnitude / greater in magnitude of x and y if neither operand is NaN, the number operand if one argument is a quiet NaN, and a quiet nan if either operand is a signaling NaN or both operands are quiet NaNs.
These definitions are mostly sensible for pairwise operation, but they have a fatal flaw if you attempt to extend them to more than two arguments or use them in reductions: they are not associative in the presence of signaling NaNs. Example:
minNum(minNum(1, .signalingNaN), 2) = minNum(.nan, 2) = 2
minNum(1, minNum(.signalingNaN, 2)) = minNum(1, .nan) = 1
FloatingPoint in Swift 3 ... 4.2
The FloatingPoint protocol binds IEEE 754; these operations are surfaced as the static functions minimum, maximum, minimumMagnitude and maximumMagnitude.
They are not surfaced as the min and max free functions defined on Comparable (which always returns the first argument if either is NaN), nor as the fmin and fmax free functions defined by the platform C module (which bind the C stdlib fmin and fmax functions).
IEEE 754 (201x)
Because of the flaw that I discuss above, the revision of IEEE 754 currently in preparation will remove the minNum, maxNum, minNumMag, and maxNumMag operations entirely. In their place, there are eight recommended—but not required—operations added to clause 9:
func minimum(_ x: T, _ y: T) -> Tfunc maximum(_ x: T, _ y: T) -> Tfunc minimumMagnitude(_ x: T, _ y: T) -> Tfunc maximumMagnitude(_ x: T, _ y: T) -> Tfunc minimumNumber(_ x: T, _ y: T) -> Tfunc maximumNumber(_ x: T, _ y: T) -> Tfunc minimumMagnitudeNumber(_ x: T, _ y: T) -> Tfunc maximumMagnitudeNumber(_ x: T, _ y: T) -> T
minimum and maximum return NaN if either argument is NaN, and the lesser/greater of the two arguments otherwise. The Magnitude operations compare magnitudes. The Number operations discard NaNs, preferring to return number arguments.
These new operations have the virtue of being associative, which makes them suitable for use in reductions. They are unfortunately not required; as recommended operations, they may change somewhat in the next revision of IEEE 754 (202x), so we should exercise some caution in adopting them.
(I'm going to talk only about minimum from here on out; everything also applies to maximum). For those keeping score at home, we have considered five different "minimum" operations at this point. The one bound to Comparable.min is not ideal because it isn't commutative in the presence of NaN. The one bound to FloatingPoint.minimum has been explicitly abandoned from IEEE 754, and is probably the worst of the bunch.
Swift 5 ...
I suggest the following (again, I am only discussing minimum; all of this applies equally to maximum):
- Add a customization point for binary
minonComparablethat lets us override the behavior forFloatingPoint. Use this for the implementation of n-arymin, so that the behavior matches. - Deprecate
FloatingPoint.minimum, marking it renamed toFloatingPoint.minimumNumber, which matches the IEEE 754 recommended operation (the semantics of the two are identical except for signaling NaNs, which are effectively a bug in the current Swiftminimum, so this is a very reasonable replacement). - Deprecate the
fminfree function. We don't need another name for this operation floating around to confuse things, and it's a holdover from the C stdlib.
The remaining questions after doing these three things are:
- Should we provide a binding for the new IEEE 754
minimumoperation? - What should the semantics of
Comparable.minbe onFloatingPoint?
The obvious thing to do is to have Comparable.min implement the new IEEE 754 minimum operation. This is pretty reasonable, because it satisfies most of the properties that we want for Comparable.min: it is associative, and it is commutative (up to representation when x == y). This would be a completely fine solution, I think.
There is one additional property, however, which is desirable for the min free function; that the sets {x, y} and {min(x,y), max(x,y)} are the same. This cannot be satisfied by either of the IEEE 754 recommended notions of minimum. I can think of at least one other option that we may want to consider: make NaNs a preconditionFailure when used in Comparable.min, and repurpose FloatingPoint.minimum to bind the IEEE 754 minimum operation.
This would leave us with three notions each of minimum and maximum; the two recommended IEEE operations, as static funcs on FloatingPoint, and a third definition which only allows values that are non-exceptional under Comparable. This may be a better solution, or it may be the pathway to a rabbit hole of ever-increasing complexity that we're better off ignoring.