+1
One thing I wonder: Can't we do this as well?
guard !other.isZero else {
return self.isAlmostZero(tolerance: tolerance)
}
+1
One thing I wonder: Can't we do this as well?
guard !other.isZero else {
return self.isAlmostZero(tolerance: tolerance)
}
I like this proposal, though it is a bit dissonant that a positive value can be almostZero
while simultaneously not being almostEqual(to: .leastNonzeroMagnitude)
:
let a = Double.leastNonzeroMagnitude
let b = 1.0e-9
b.isAlmostZero() // true
b.isAlmostEqual(to: a) // false
I understand why this is the case, but itâs still strange.
⢠⢠â˘
Also, while looking over the implementation, I find that the following initializer (which already exists on FloatingPoint
and has nothing to do with the current proposal except that it is used there and I noticed it) has a misleading name:
init(sign: FloatingPointSign, exponent: Exponent, significand: Self)
I get that it is a useful operation, but when reading it in source code:
let scaledOther = Self(sign: .plus,
exponent: -1,
significand: other)
At the point of use this really looks like it should produce a value whose exponent is negative one, and whose significand is copied from other
. But what it actually does is add negative one to the existing exponent of other
.
I know, too late to change and not relevant to the current proposal, but I wanted to mention it anyway.
Well, it can be used to do that, it can just also be used to do some other tricks, like this one =) If you pass a significand in [1,2), it does exactly what it says on the tin. Passing "out of range" significands like this was deliberately not disallowed because it could be made fully defined and allows nice party tricks.
The addition of isAlmostZero
seems a little odd to me; just looking at some code, I think that I would expect isAlmostEqual(to: 0)
to behave the same as isAlmostZero()
. Is it possible that to: 0
could be a special case for isAlmostEqual
?
In addition to personally supporting the isApproximately
spelling, looking at the alternatives considered, I feel that introducing an approximate equality operator makes a great deal of sense, as it seems likely that isAlmostEqual
would be most commonly used without a manually-specified threshold. However, I feel that the above suggestion to unify isAlmostEqual
and isAlmostZero
is necessary for an operator to make logical sense.
Translate this into Latin and it can go on IEEE 754's family crest.
+1
This would be a great addition! Will â be added to Swift's current list of operators?
Big +1, I have written the naive version many times in the past.
Yes
Yes. I have a slight preference for a combinations of alternatives mentioned by others in this thread, but havenât thought through the implications. Iâm curious if there's something I'm missing in this alternative:
isApproximately
extension FloatingPoint {
public func isApproximately(
_ other: Self,
relativeTolerance: Self = Self.ulpOfOne.squareRoot(),
zeroAbsoluteTolerance zeroTolerance: Self = Self.ulpOfOne.squareRoot()
) -> Bool {
if self.isNaN || other.isNaN { return false }
if self.isInfinite { return self == other }
if other == 0 {
_precondition(zeroTolerance > 0,
"zeroAbsoluteTolerance should be greater than zero")
return abs(self) < zeroTolerance
}
_precondition(relativeTolerance >= .ulpOfOne && relativeTolerance < 1,
"relativeTolerance should be in [.ulpOfOne, 1).")
...
}
}
Reasons for 1 API preference:
other
might be dynamically zero and, IIUC, this means callers would frequently have let closeEnough = other == 0 ? x.isAlmostZero() : x.isAlmostEqual(to: other)
, which seems obnoxious (and error prone).What are the main arguments for two separate APIs over one with two parameters?
I don't recall.
Brief reading
+1.0000000000000002 for all the reasons already listed :). This is a problem I've always known about but never bothered to solve with anything more than the naive abs(x - y) < threshold
solution. This feels like a perfect candidate for inclusion in the standard library. Also minorly in favor of the isApproximately
naming suggestion, but support the proposal either way.
Any easy proposal to support.
With regards to isApproximately
versus isAlmostEqual
, one advantage of the latter is that it would appear in autocomplete lists for the word equal, but I wonder if that would matter much in practice. As an API, isAlmostEqual(to:)
matches the existing isEqual(to:)
.
+1
This is a wonderful thing to have as a language level feature. Swift is a wonderful language for numeric use and this provides a sensible solution to an issue even casual users of floating point math run into.
I have one question on the semantics of the tolerance argument. A common form for tolerance equations is:
abs(self - other) < scale * relativeTolerance + absoluteTolerance
In the proposal isAlmostEqual(to:tolerance:)
holds the absoluteTolerance
from that equation at 0
and takes an argument for relativeTolerance
. I wonder if we should expose both as arguments?
If we only expose one as an argument I wonder if absoluteTolerance
is actually more what a common user expects the argument to control? With absoluteTolerance: 10e-7
and relativeTolerance
held at a sensible default like the one in the proposal the user gets values within the 10e-7
they requested plus an allowance for machine representation. That might be what they want a decent amount of the time?
The proposal's isAlmostZero(absoluteTolerance:)
is nice and specific about what its tolerance argument is talking about.
Overall very pleased to see the language growing in this direction.
+1 for sure. But I also wonder why we need the extra .isAlmostZero()
instead of catching the zero-equality of the argument and returning the alternate evaluation, especially given @Michael_Ilseman 's #3 reason... If I don't know that what I'm comparing to == 0
how can I know to call the .isAlmostZero()
function?
+1 with sympathy for comments of compiler warnings for anti-patterns by @nrith and @allevatoâs comments. I also have sympathy for the âis approximatelyâ naming suggestion by @bwm003.
+1 based on a read-through of the proposal and no particular floating point expertise other than general use. A nice and useful addition.
There is some subtlety involved in understand why zero requires a special case. Having read the proposal, I would now know to use isAlmostZero()
. However, my alternate-universe self that didn't read this thread would be unlikely to use isAlmostZero()
even if he saw it in the API; he'd just remember the general case and stick a 0 in there.
Much of the motivation for this proposal centers on the fact that developers try to make these approximate floating point comparisons but usually do it wrong. I would extend that logic to the zero case in this proposal as well: isAlmostEqual(to: x)
when x == 0
should just call the zero-specific function. Yes, that probably creates inconsistencies around zero, but it's better than doing the clearly-wrong thing.
I too prefer isApproximately()
to isAlmostEqual(to:)
. Just as an English word, "almost" suggests "not quite as much as" rather than "in the neighborhood of", which is a bit off. "Nearly" might address that issue, but "approximately" is most unambiguous of all.
Note that if we change the signature to:
isAlmostEqual(to:relativeTolerance:absoluteTolerance)
and default absoluteTolerance
to something more than zero then we don't need isAlmostZero
anymore. The problem with comparing to zero is that relativeTolerance
by itself isn't enough.
If you have both absolute and relative terms then reasonable defaults I would suggest people to try are relativeTolerance = 2 * Self.ulpOfOne
and absoluteTolerance = Self.ulpOfOne.squareRoot()
.
+1 to the inclusion of this proposal. Iâm also in favour of isApproximately
or approximatelyEquals
over isAlmostEqual
. Iâll leave discussion of the semantics of the method to those with more expertise, other than to say what has been explained makes sense to me.
Off-topic, but I canât resist: if Iâm remembering my high school Latin right, something like âsit cognobilis quamquam etiam peregrinus remanetâ might be a better translation - literally âalthough it may be understandable, nevertheless even now it remains strange.â Google Translate generally does a very poor job with Latin; what it got is not quite nonsense but has numerous grammatical issues and word choice problems - e.g. âmirumâ means âstrange and marvellousâ, which I donât think is the sense of the word meant here.
Edit: or correction by the way of @nrithâs now-deleted post in case it isnât reposted: âcognobile sit, quamquam etiam inusitum estâ
IIUC, you're proposing a hybrid approach. Is this the alternative discussed in the proposal?
Why not a single function using a hybrid relative/absolute tolerance? Because--as a library--we do not know the scaling of user's inputs a priori, this will be wrong more often than right. The scale invariance (up to underflow) of the main
isAlmostEqual
function is a highly-desirable property, because it mirrors the way the rest of the floating-point number system works.
What's your take on this, do you disagree and if so why?
Also, what are the reasons that 2 * Self.ulpOfOne
is a better a relative tolerance in contrast to the proposal's?
Yes, it is indeed that alternative. Thank you for pointing that out and sorry for missing that in the proposal.
The assertion that scale invariance only works for the relative and not the absolute tolerance is of course true. The hybrid approach with appropriately set absolute and relative tolerances can be robust for a portion of the floating point range. For the stats/ML work I'm personally involved in that has been a good trade off, but I can see how a language's standard library might want a more pure approach.
Even if we have two functions I'd suggest that we cover more use cases by exposing both relativeTolerance
and absoluteTolerance
as arguments in isAlmostEqual()
and defaulting absoluteTolerance
to 0
.
My suggested tolerances were to be taken as a pair. When absolute tolerance is zero I like the relative tolerance in the proposal. I'm afraid that I don't have a better reason for my particular suggestions than that they are somewhat commonly used (e.g. Brent's root finding algo) and have worked for me in the past.
+1, I have always used my own flawed versions of this function
Yes. It provides a solid default answer to a real world problem.
Perhaps there should be API to get the suggested tolerance value. That way it could be used with existing tolerance within APIs that other frameworks (XCTAssertEqualWithAccuracy).
The spelling I think I like the best is:
x.approximateEqualityTolerance
x.isApproximately(3.0)
x.isApproximatelyZero()
Quick reading ... but years of doing it the wrong way. :]
What a great addition. +1 from me too. I hope this won't have any stdlib private API in its implementation as I would want to copy this extension into my code base because otherwise I won't be able to reach to it on older Swift versions (my project is locked to Swift 5 stdlib).