After a whole day spent on debugging...
In C++ we use DBL_MAX as maximum valid double value. Unless INF it is a valid value, we can use it for calculations. In numeric code it is sometimes used as a special marker.
As we do not have a DBL_MAX value in Nim, and as its bit representation is not easy to guess or to google (importC may do) I thought: Well let us just use available system.NaN. That was a big mistake:
# https://stackoverflow.com/questions/5834635/how-do-i-get-double-max
# https://irclogs.nim-lang.org/28-03-2021.html#07:42:47
const
# UninitializedFloat = DBL_MAX # C++ constant
UninitializedFloat = system.NaN # will do?
var x: float = UninitializedFloat
if x == UninitializedFloat:
echo "error, x is uninitialized"
var y = x
if y == x:
echo "equal"
if x > 1e6:
echo "x is too large"
echo "All OK!"
So let us run this program:
$ nim c --gc:arc t.nim
Hint: used config file '/home/salewski/Nim/config/nim.cfg' [Conf]
Hint: used config file '/home/salewski/Nim/config/config.nims' [Conf]
....
CC: stdlib_io.nim
CC: stdlib_system.nim
CC: t.nim
Hint: [Link]
Hint: 21181 lines; 0.368s; 25.633MiB peakmem; Debug build; proj: /tmp/hhh/t.nim; out: /tmp/hhh/t [SuccessX]
salewski@nuc /tmp/hhh $ ./t
All OK!
I was aware about that special NaN behavior 20 years ago, when I wrote some numeric code in Oberon. But unfortunately did not remember.
NaN is a very special floating point value. Comparing a float with NaN gives always false, so testing for NaN does not work as may be expected.
float.high is system.inf.
Had worked better I guess. DBL_MAX is a finite value, which is available from the fenv module linked above. I had seen that module years ago, but was not able to find again...
NaN is a very special floating point value. Comparing a float with NaN gives always false, so testing for NaN does not work as may be expected.
Also .. beware template optimizations and rewriting (in any language ...); Quite a bit of code/template implementations assume "x <= y" can be rewritten "not x > y", but that is not true if either x or y are NaN.
I think the best for uninitialized floats is to use NaNs.
In your example:
proc isUninitializedFloat*(value: float): bool {.inline.} =
# Returns true only if value is NaN.
value != value
...
var x: float = NaN
if x.isUninitializedFloat:
echo "error, x is uninitialized"
...
I've always used NaNs for default uninitialized floating parameters and to signal invalid operations. It works great. Little example in Nim:
proc variance*(values: openarray[float], isSample: bool = true, mean: float = NaN): float =
let count = if isSample: values.len - 1 else: values.len
if count <= 0:
return NaN
var
mean = if mean.isNan: values.mean else: mean
accum = 0.0
for i in values:
let value = i - mean
accum += value * value
return accum / float(count)
# isNan is simply:
proc isNan*(value: float): bool {.inline.} =
value != value
BTW, this website is very interesting: https://float.exposed
isn't that what signalling nan is supposed to be for? ever used that?
something like:
import fenv,strutils
template sNaN:float = cast[float](0xfff0_0000_0000_0001'u64)
proc initOrDont(f: var float) =
if false: f = 1.0
var myF = sNaN
initOrDont(myF)
let x = NaN * NaN
assert fetestexcept(FE_INVALID)==0 #no effect with NaN
let x = myF + 1.0 #triggers on any operation
if fetestexcept(FE_INVALID) != 0: raise newException(ValueError, "sNaN detected")
Hi Shirley,
Yes, but I don't know a single case where the use of sNaNs is more apropiate than qNaNs, maybe for debugging purpouses...
It's also important that the IEEE 754-2008 revision specified the encoding of sNaNs but some architectures are not compatible with the standard.
I prefeer to use quiet NaNs so I can let them propagate through the code when is convenient and launch an exception manually when is really necessary. Simple, compatible and flexible.
Of course, this is only my personal preference.
There is an old, unknown switch in the compiler, --nanchecks:on that can turn the production of a NaN value into an exception. The compiler does not use the hardware's ability to do that as that would change the FPU's state and have severe consequences with interop with other language runtimes.
But I doubt anybody ever used this switch so we should probably simply remove it.
Also, the whole notion of "Not a Number" feels like some sort of broken legacy concept.
It's even worse, imagine a nil value that you cannot check for via x == nil. That's how broken NaN is...
Also, the whole notion of "Not a Number" feels like some sort of broken legacy concept. Like you set a variable type as let v: number but the type system doesn't guarantee that and it also can have a type of "not a number". It's as strange as if you use let v: string and the v also could have a type of "not a string", feels wrong.
nan has it's place, what else should operations like 0/0, Inf/Inf or sqrt(-1) produce? A well defined and unique value is better than some arbitrary constant. And you can trap them on a lot of processors, it just needs to be enabled in fenv (though that has it's downsides, e.g. if you read floats from binary data. Also it's a cpu exception like segfault, so you only get a stacktrace with gdb).