One day I came across some code that looked like this (paraphrased):
let sign__old f = assert (Float.is_finite f); if String.is_prefix (Float.to_string f) ~prefix:"-" then `Negative else `Nonnegative
Naturally, I replaced it with the following:
let sign__new f = assert (Float.is_finite f); if Float.is_negative f then `Negative else `Nonnegative
A few days later, this caused an issue in prod. How is it possible? These two functions are obviously equivalent, right?
It turns out there is exactly one edge case for which these two functions behave differently: -0., aka negative float zero.
Positive vs Negative Zero
In the IEEE floating point standard, numbers are represented as sign and magnitude. This means it is technically possible to have both a positive and a negative zero. While these two values are numerically equal, both are treated as valid floats, and they behave differently when passed into different functions.
In this case,
sign__new sees negative zero as
`Nonnegative, because it is not numerically smaller than zero, despite having a negative sign. On the other hand,
Float.to_string (-0.) produces
sign__old thinks it is
I think it’s likely uncommon that the existence of negative zero leads to bugs in code, because typically programs see these two values as having the same behavior. In my case, the code is constructing an AST that represents the float value. The old code produces a unary negation applied to positive zero immediate, while the new code proces a negative zero immediate. This change caused an exception in prod because the code generation and parsing process no longer round trips.
The first time I learned about negative zero, I thought it was a misfeature. But it actually makes sense, considering infinities are also signed. With all four values representable, we can have
1/-inf = -0,
1/-0 = -inf and so on, which is nice.
With the bug identified, the fix is easy: use
Float.ieee_negative instead of
If you enjoyed this post, try another: Precisely Compare Ints and Floats