Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

There is a huge misconception here which really should be clarified in the title. Erlang has an "exactly equal" or strict equality operator, called =:=, which is different from just usual equality, which is ==. The usual equality operator will keep having +0.0 = -0.0, and floating point arithmetic will keep behaving as you would expect... It's that we no longer have +0.0 =:= -0.0, which is a very different thing (and frankly, makes sense - there should be some special operator that can differentiate between these two values).


Honestly - I rather dislike this change. I'd much rather have 0 & -0 be precisely equal to each other and have some obscure library function like `isNegativeZero` for the extremely rare cases they'd need to be differentiated. 0 and -0 being distinct values is honestly just an accident of convenience with how floats are stored - there are two bit patterns that result in a value of 0 but because they're different patterns we've assigned an unwarranted amount of distinction to them.


> 0 and -0 being distinct values is honestly just an accident of convenience with how floats are stored

That's not at all true. The inclusion of negative zero in ieee 754 was a deliberate design choice, and if it were considered desirable that zero should be unsigned, then the bit pattern assigned to -0 would not have been so assigned. See 'much ado about nothing's sign bit'[0], by one of the principal authors of ieee 754.

Whereas -0 and +0 are not operationally equivalent, operational equivalence is an important and useful relation; see the very pattern matching example linked from the OP[1].

0. https://people.freebsd.org/~das/kahan86branch.pdf

1. https://erlangforums.com/t/total-term-order/2477/41


That is a phenomenal title.


unfortunately this scan of kahan's chapter is missing some pages


the thread explains in the very first message that the existing semantics caused a subtle compiler bug in a way that makes it clear that you are not just wrong about the semantics of ieee-754 floating point but also about the pragmatics of how this affects real programs


> 0 and -0 being distinct values is honestly just an accident of convenience with how floats are stored

This is actually a valid distinction because these values can represent different values.

Floating point values are not numbers, but ranges of possible values: float -0 is a number in the range (-FLT_TRUE_MIN / 2, 0] and float +0 is a number in the range [0, FLT_TRUE_MIN).

IMO if I were to redesign IEEE 754 I would add a third float 0 value that always represents exactly 0 and redefine float -0 and float +0 as open limits, but that would probably make everything a lot more complicated.


I don't think it's just that.

1/0 is infinity but 1/(-0) is -infinity


Rigorously division by zero is "undefined" in mathematics.


You can totally define division by zero.

"There are mathematical structures in which [dividing by zero] is defined. [...] However, such structures do not satisfy every ordinary rule of arithmetic (the field axioms)." https://wikipedia.org/wiki/Division_by_zero

You're familiar with the ordinary rules of arithmetic (ORA!) applied to the real numbers. However, there are many number systems that follow different rules.

The rules of arithmetic for floats, wraparound ints, elliptic curves, and asymptotic analysis are all different from ORA! (and each other) but they are useful in their own ways.


Shouldn't both of those be NaN? Does Erlang have a NaN?


The reason division by zero can be a number is that floating-point numbers aren't individual numbers but ranges of numbers. So 0.0 isn't really the integer 0, it is the range of numbers between 0 and the smallest representable non-zero number. So division by 0.0 may be division by zero, or it may be division by a very small number, in which case it would approach infinity.

The same goes for the floating-point infinity: it doesn't represent the concept of infinity, it is a placeholder for every number between the largest representable number and infinity. That's how dividing a very large number by a very small number can result in infinity, a number which is really not a number.

This is the philosophy by which IEEE floating-point numbers were designed, and it's the explanation behind which negative zeroes and infinities make sense in floating point.

The way I find it easiest to reason about is by taking a graph of an asymptote and rounding it to the nearest units. You somehow need a way to say "there is an asymptote here!" even though you might not have all the units, and so you introduce infinities and negative zeroes to maintain as much precision as possible.


this isn't true. floats don't have semantics of internals. they have semantics of exact points that do math with rounding (otherwise the result of subtracting 2 floats would sometimes be more than 1 float)


Moreover the spec defines rounding modes (rtnz, rtg, rtfz rtl) that are designed to exactly do interval arithmetic


IEEE754 has a separate +Infinity and -Infinity to go along with SNaN and QNaN.

For instance:

- as parent said 1/0 is Inf.

- as parent said 1/(-0) and log(0) is -Inf.

- sqrt(-1) and 0/0 is NaN.

[1] https://www.gnu.org/software/libc/manual/html_node/Infinity-...


>both of those be NaN

Totally NOT. NaN is defined as 0.0/0.0. That's it.


IIRC division by zero throws an exception.


There is no negative zero


1/0 is undefined and 1/-0 is also undefined.

what we have is lim(x->0+) 1/x = +inf and lim(x->0-) 1/x = -inf


this is wrong. ieee defines them


not in ieee-754, only in reality


Numbers are abstract concepts in general. There is no fundamental reality only "real" numbers.


the question of whether mathematical objects like 53 are more or less real than the historically contingent universe is indeed very interesting

however, it is irrelevant to the fact that a commenter was disagreeing because they had confused ieee-754 division with the division operation on so-called 'real' numbers


They're still ==.

=:= (=== in elixir) Somewhat means "and has the same binary form"

Fwiw, julia made the exact same choices.


What about the N number of NaN values that are treated the same, but are physically different values?


So, in Erlang it will be the case that:

+0.0 == -0.0 is true, I guess because they are very close

+0.0 =:= -0.0 is false, I guess because they have different bits

A =:= B for two different numbers of course, because they have different bits

How does == work for other very close floating point values in Erlang? Is there some inconsistency here?


It is not about being very close. IEEE says "Comparisons shall ignore the sign of zero" and erlang (or rather CPU instructions erlang outputs for float == float) respects that

While =:= is not something mandated by IEEE. Erlang implements such an operator on floats and it decides to compare false for 0.0 & -0.0. So it probably compares bits/does integer comparisons on same data


Ah, so == is compliant, and =:= is extra. Makes sense!


The only other distinction between == and =:= is for integers and floats. Integers are not =:= to the corresponding float, but are ==, so it has nothing to do with closeness.

-0.0 is a weird dumb non mathematical value that IEEE 754 put in (for example the standardized value for sqrt(-0.0) is not even justifiable based on their choices). I think they didn't want 1/(1/-Inf) to be Inf, or something.

In the end think of erlang's "==" as "equal in value" and "=:=" as "equal in representation in memory".

Now if you really want to raise the hackles of someone using Erlang, should ask why integers and floats don't have a well ordering, and what you should do when you sort them.


Reading the article is what clarifies things.

HN is full of people who think the world needs their hot take based on a headline, unfortunately.


"Please don't sneer, including at the rest of the community." It's reliably a marker of bad comments and worse threads.

https://news.ycombinator.com/newsguidelines.html


I read the first word of your reply and would like to counter you with “no! writing!”


i don't use Erlang (mainly Julia), but this change makes a lot of sense. in Julia, there is the == operator (value equality) and the === operator (value egality). 0.0 == -0.0,because they have the same "value". but they aren't identical (the bits are different), so !(0.0 === -0.0).


> there should be some special operator that can differentiate between these two values).

What for though?

I was expecting as I learned Rust that I would soon find the == operator (which is just the predicate PartialEq::eq) wasn't quite enough and need some === type operator (Rust doesn't have one), but I never did. Turns out actually in most cases equality is what I cared about.


There is an example in the article about compiler optimization, and they state that other similar instances may exist.


The example for the compiler just seems like it wants a property that types don't necessarily even have (Equivalence), so my instinct would be that it's probably just broken and this is the tip of the iceberg or if the compiler does care about this property it should know floats don't have Equivalence and thus 0.0 == -0.0 isn't cause to perform this optimisation.


it's using the =:= operator you say it should be using, not the == operator you incorrectly say it is using

however, that operator was originally defined incorrectly, and now they are fixing that bug


So far the only example I've been given of this operator is that it's what the compiler uses to decide equivalence, which is a situation where the correct answer was categorically no. The belief seems to be that this code is fine if we mistakenly unify cases where we think the IEEE floating point number had the same representation, and I don't buy that this is better than unifying -0.0 and 0.0 in the example, I think it'll be the exact same outcome.


i think the belief is rather that the compiler needs to reliably distinguish between -0 and +0 except where it explicitly invokes arithmetic comparison, and so the semantics of both =:= and unification should change to distinguish -0 from +0

i don't know what it would mean to not unify cases where the ieee floating point number had the same representation, assuming that by 'unify' you mean 'paattern-match) (erlang doesn't have full bidirectional unification where run-time data can contain variables which could be instantiated with constants from a pattern). what would the semantics of unification be then? no pattern could ever match a term containing a floating point number?

or perhaps you aren't aware that 'unify' means 'pattern-match' (prolog style) and we are discussing a pattern-matching bug, and by 'unify cases' you just mean 'remove one of two cases'. i think it's very likely that the compiler needs to remove redundant cases that fall out of its optimizer in order to get decent performance, and if the user puts them in their code it's probably a bug


> no pattern could ever match a term containing a floating point number?

No equivalence matching on floating point numbers seems like a much healthier approach, yes. I don't expect people to have huge problems with "x greater than 0.0" or "z less than 16.35" but "p is exactly 8.9" suggests muddled thinking.

The floats are very strange. While machine integers are already a bit weird compared to the natural numbers we learned in school, the floats are much stranger so I expect code which is trying to test equivalence on these values is going to keep running into trouble.

We know that humans, including human programmers, do not appreciate how weird floats really are, so I think telling programmers they just can't perform these matches will have a better outcome than confusing them by allowing this and then it rarely has totally unexpected consequences because a+epsilon was equivalent to b even though mathematically a+epsilon != b


as documented in https://news.ycombinator.com/item?id=35889463 this would prevent you from translating lapack into your ideal language

you have to get rid of order comparisons too, though, or people will just replace a == b with a >= b && a <= b

also it seems like a compiler attempting to determine whether two pieces of code are equivalent (so it can throw one away) needs to be able to test whether two constants in them could ever produce different computational results; this is important for code-movement optimizations and for reunifying the profligate results of c++ template expansion

similarly, a dataflow framework like observablehq needs to be able to tell if an observable-variable update needs to propagate (because it could change downstream results) or not; for that purpose it needs to even be able to distinguish different nans. like the compiler, it needs the exact-bitwise-equality relation rather than ordinary arithmetic equality

where i think we agree is that floating-point arithmetic is probably a bad default for most people most of the time because it brings in all kinds of complexity most programmers don't even suspect

your comment reads to me as 'i don't understand floating point equality and therefore no human does so no human should have access to it'

but there are more things in heaven and earth than are dreamed of in your philosophy, tialaramex


My fear isn't about floating point equality but about equivalence. As we discussed repeatedly, nothing changed for equality, -0.0 = 0.0 just as -0 = 0 in integers. Equality works fine, though it may be surprising in some cases for floating point values because sometimes a != a and b + 1 == b.

"Exact-bitwise-equality" is yet a further different thing from equivalence. Is this what Erlang's =:= operator does? Erlang's documentation described it as "Exactly equal to" which is a very silly description (implying maybe == is approximately equal to), presumably there's formal documentation somewhere which explains what they actually meant but I didn't find it.

Presumably Exact-bitwise-equality is always defined in Erlang? In a language like Rust or C++ that's Undefined in lots of cases so this would be a terrible idea. I still think it's better not to prod this particular dragon, but you do you.


I could also imagine dumping binary to a file and expectingb the hashes to be equal.


What’s the use case for different values?


It's a natural consequence of the IEEE 754 standard for floating point numbers.


To be clear, it was added to the standard because there was user demand for it.

Zero is already special cased in IEEE 754 (along with the rest of the denormals, because of the implied leading mantissa bit), the implementation would be no harder if you just only had a single zero. However, back when the standard was being created, when a single zero value was proposed there was pushback from people in the user community who depended on knowing which side of the zero they underflowed on their own floating point implementations.

Many people think that negative zero is just an engineering artefact. This is not true, it is a feature that was asked for, debated, and then added...

... almost certainly because it was an engineering artefact in a previous system that someone managed to depend on, https://xkcd.com/1172/ style.


> Many people think that negative zero is just an engineering artefact. This is not true, it is a feature that was asked for, debated, and then added...

Allowing zero to be represented with two bit patterns was a feature that was advocated for to make bit operations easier - it was not decided that both zeroes should be distinguishable. This absolutely is an engineering artifact but you're quite correct that it was quite intentional.


i think the rationale in https://people.freebsd.org/~das/kahan86branch.pdf goes well above and beyond 'an engineering artefact in a previous system that someone managed to depend on'


unfortunately this scan is missing some pages


1/Infinity in 754 is 0. 1/(-Infinity) is -0. These numbers are not strictly equal in real number terms.


These numbers are inequal in terms of storage - in real number terms zero has no sign and thus -0 and +0 are the same number. The source of their distinction is also important as it's just a pure point of convenience at the bit level - there are two separate bit patterns for 0 but both numbers are the same number.


+/-0 often indicates an underflow on one side of 0 or the other. Certainly in real terms 1/(infinity) != -1/(infinity).


You are overloading the word “real”. In terms of the mathematics of the reals, there is no difference. In terms of real-life pragmatics, 0 and -0 could be used to differentiate between different outcomes in a way that is sometimes useful.


Infinity and -0 are not in the real numbers, so the expression there makes no sense if you are thinking strictly in the real numbers. If you assign real number bounds to what the floating point numbers mean, the expressions make sense.

In floating point terms infinity tends to indicate overflow (any number that is too big to represent) and the 0's indicate underflows. So in more verbose terms,

1/(positive overflow) = (positive underflow)

While

-1/(positive overflow) = (negative underflow)

In this case, since the positive overflow isn't really infinity and the underflows aren't really 0, they are not equal. In practice, -0 and the 0 can both also arise from situations that produce 0 exactly, too, but this is not that case.

You may be thinking about how lim{x->inf}(1/x) = 0 = lim{x->inf}(-1/x), which is true. Infinity in floating point does not necessarily represent that limit, though, just any number that is too big.

You may also notice that the limit is not in the range of the functions inside the limit. For all real x, 1/x != 0


We are saying the same thing.


Floating point numbers don't represent all possible values, just a subset of values representable by a number of bits used. Therefore, in some cases it's reasonable to treat floating point numbers not as "exact point on a number scale", but rather a range between a number and the next possible representable number.

In the case of +0.0 and -0.0, they can be treated as values between zero and the smallest representable number (about 5.4E-079 for 32 bit floats). It isn't a very common use case since dealing with such small numbers isn't a very common thing, but it is definitely a possibility.


Float stuff like 1.f / (+0.f) = infinity and 1.f / (-0.f) = -infinity.

And maybe complex number shenanigans and multi valued functions? Maybe someone familiar with mathematics can tell us more. :)


The best example on kahan's webpage is Borda's mouthpiece. Signed zero makes it look right, unsigned creates a singularity


There are actually very clear and technical numerical analysis reasons for all of the weird stuff that happens in IEEE 754. The zero behavior, in particular, is because of this sort of thing.


Preserving the sign in case of arithmetic overflow/underflow.


Because two binary values that are not bit-for-bit equal should have an equality operator that can reflect that without resorting to conversion


there is no such thing as -0.

if you want to say "zero approaching from negative one", fine, say that. but that IS NOT the same thing as -0. zero is just zero, it doesn't have a sign.

some people just want everything to fit in a nice neat box, and sometimes folks, life is not like that. this is an awful change, injecting meaning where there is no room for it.


-0 exists. It's just not a mathematical object. It's an IEEE754 signed-magnitude bit-pattern — one that is distinct from the IEEE754 signed-magnitude bit-pattern we call +0. Both of these bit-patterns encode the semantic meaning of the mathematical object 0; but that doesn't mean that the bit patterns are the same "thing."

Compare/contrast: the JSON document "0" vs the JSON document "0.0". Both encode the same type (JSON only has one Number type) and so the same mathematical object — which is, again, 0 — but these are distinct JSON documents.

And that's the whole point of this change. Erlang == is mathematical equality. Erlang =:= is (encoded bit-pattern) representational equality.

Which is often the kind of equality you care about, if you have canonicalization happening at parse time, and so you want to implicitly assert (usually for added efficiency!) that the values you got after parsing + mathing are not only mathematically equal to some value, but representationally equal to that value.

---

(If this irks you as a mathematician, you could model IEEE754 as its own mathematical group — a lattice, I think? — of which +0 and -0 would be members. Along with with some — but not all — base-2 rationals; and a random assortment of signalling and non-signalling NaNs.)


dealing with the same thing in rust, you can't use float in a lot of equality places where you can use ints, and they are moving to change match so you can't use floats in patterns there either.


groups and semilattices are mutually exclusive; semilattices can't have any inverse elements


> JSON document "0" vs the JSON document "0.0"

nope. both those documents are a STRING type, not a number type.


I can trump your pedantry :)

A JSON document is itself canonically a string / stream of human-writable bytes. (I.e. the JSON specification specifies JSON documents canonically as a string-encoding grammar, not as a set of formal AST-node types like SGML/XML/etc.) When you mention a JSON document in text, you're mentioning an encoded string, so you put it in quotes.

Backticks to represent what you'd type at a REPL: if `"foo"` is a JSON document, then `foo` is the decoding of that JSON document (which will not itself be a JSON document, but rather a primitive or data structure in your programming language — one which happens to have a valid encoding as a JSON document.)


not sure what you hoped to accomplish with that. yes all documents are a byte array. this is not the same thing a the STRING type. this JSON document:

    0
and this JSON document:

    0.0
might compose a different stream of bytes, but by your own admission, they get parsed as the same type and value, 0. the only way to distinguish the difference in a parsed document, would be to present a string type as the input document:

    ["0", "0.0"]
but even then, you're only retaining difference in meaning after parsing, BECAUSE the values are strings, not numbers. so your comments have only proven that some byte arrays are different, and some resultant strings are different than others. so congrats?


JSON defines absolutely no semantics for numbers, only the syntax. Some implementations do handle 0 and 0.0 differently.

See also: the problems with stuffing a high magnitude integer like 2^60+7 into a JSON document, and then unpacking it in JS using the standard web browser API. The number can't be represented exactly with the standard JS number type, but it's a valid JSON document, and so the implementation has to make a choice.


> they get parsed as the same type and value, 0

In Python the first becomes an int, the second a float.


who said anything about Python?


You made a claim about parsing JSON, I gave a counterexample.


> so your comments have only proven that some strings are different than others

Er... yes? What I'm trying to do here, is to provide an intuition for how IEEE754 bit patterns should be mentally modeled. They're byte-arrays that encode a semantic type, like a JSON document is a byte-array that encodes a semantic type.

And, just like `"0"` and `"0.0"` are different encodings of the same JSON-semantic Number, IEEE754 -0 and IEEE754 +0 are different encodings of the same mathematically-semantic number; namely, zero.

But, unlike with JSON — and I hope this should be obvious — there's no such thing as "decoding" IEEE754 to something else. The bit-patterns in IEEE754 just are... themselves. There's no more-canonical encoding to move them into that will erase the difference between IEEE754 -0 and +0, or between regular numbers and denormals, or between all the different NaNs. They're all distinct IEEE754 objects.

Which means that, unlike with JSON Numbers, where you just decode them to canonicalize them and can forget about the fact that there are multiple equally-canonical JSON encodings for any given decoded value... you can't just forget about there being multiple distinct IEEE754 objects with the same mathematical meaning.

You don't operate on JSON documents in their encoded form. But you do operate on IEEE754 bit-patterns in their encoded form. You don't turn them into "numbers" in some abstract sense, do math, and then turn them back into bit-patterns. They're bit patterns the whole time.

Every time the FPU does an IEEE754-math operation, it has to know what to do with those distinct equally-canonical encodings, applying logic to them such that they produce mathematically-semantically-correct — but potentially still bit-pattern distinct! — results. Your FPU isn't attempting to simulate mathematics over the field of the rationals (in the way that an arbitrary-precision decimal library would); it's doing specific IEEE754 operations that move an IEEE754-bit-pattern-typed register from containing a value representing one position on the IEEE754 lattice, to a value representing a different position on the IEEE754 lattice. Where some of those position-values of the lattice may be interpreted, by humans, to represent a mathematical number. (Non-uniquely, because -0 and denormals!)

There is no other domain to move the math into. You're stuck in encoded non-canonical bit-pattern land. And as such, it's helpful to be able to talk about the particular encoded non-canonical bit-patterns themselves, compare them for bit-pattern equality, and so forth. Because what those bit-patterns are can determine things like how long an operation takes, or how well a vector of said bit-patterns compresses, or whether external systems will be able to decode the results, or whether you'll still have any data at if you try to hard-cast your FP32 to an FP16.

(And don't get me started on the fact that computer "integers" aren't mathematical integers! At least IEEE754 is a standard, such that `float` means "position on the IEEE754 FP32 lattice" on every architecture targeted by programming languages with FP types, and when an arch doesn't have that support, it's shimmed in with a runtime IEEE754-correct FPU support library. The C machine-integer types — not just `int`, all of them — aren't even well-defined as being a position on a two's-complement ring vs a position on a signed-magnitude ring! Any program that ANDs two machine-integers together and expects a particular bit-pattern to result, is completely non-portable to many historical and esoteric architectures! Which comes up more than you'd think, because mathematical proof-assistants are effectively "esoteric architectures.")


> there's no such thing as "decoding" IEEE754 to something else.

Huh? You can totally take the raw bytes in an IEEE754 floating point number and parse them into something else (say, a decimal type) the same way you can take the raw bytes of a JSON document and parse it into a struct.

It's a lossy encoding for reals.


There isn’t in the set of real numbers, but there is in the set of floating point numbers.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: