Okay, so first off, floating point numbers are the worst and you should never use them.
But if you are, they say programmers should be aware of coercion rules. “ints promote to floats”, they say. “And floats promote to doubles.”
You probably had to go to a school and pay money for somebody to put that on a slide so that you could forget it and look it up later. So look at this code:
int fs, n1, n2;
double C = ((double)fs * 256 * n1 * n2) / 24000000;
int c0 = C;
int c2 = 10000;
int c1 = C * c2 - (c0 * c2);
(this code is converting a floating point value into a fixed-point value with a (256 * n1 * n2) / 24M
scaler)
fs
is an int, but it gets cast to double - and multiplied by a bunch of ints, then divided by an int. Looks good, right?
But the fact that you see it on this blog means something’s up. Does a double * int
turn into a double or an int? What about a double / int
?
It turns out the above code is fine. However, this code is not:
// fs, n1, n2 are all u32
let C = (fs * 256 * n1 * n2) / 24_000_000;
let c0 = C;
let c2 = 10_000;
let c1 = C * c2 - (c0 * c2);
Obviously this is a problem in transliterating the code to Rust. This code is nestled inside code to initialize an analog-to-digital converter, the only way to detect that something was off was noticing that the sample rate of the final product was ~10% off.
There’s at least two aspects to this. One is, in the process of converting code from one language to another (or in general, rewriting/refactoring any code), unit tests are king. I’ve said it before, but a common first step to refactoring old code bases is to write unit tests for that codebase as-is.
In this case, a simple test that we get the expected c0
, c1
, and c2
values for a given fs
would suffice to catch the error.
The other aspect to this is coercion rules. At work, we often speak of minimizing “tribal knowledge” - that is, things that you just Need To Know in order to do your job. The location of the test suite. Review practices. Code architecture. But there’s a language side to this too - coercion rules are something you just Need To Know, and so working in a language requires that developers spend that brainpower on that. In order to read the C++ code above, you need to understand that int c0 = C;
is truncating the floating point value C
, even though there is nothing in that line of code that says that. The last line, int c1 = C * c2 - (c0 * c2)
is even worse - it’s performing a subtle operation which can only be understood by referencing the types of those variables, which are spread across multiple lines. You have to take a second to think about why C * c2
and c0 * c2
aren’t the same, even though c0 = C
.
Fortunately even within C/C++, some compilers will throw a warning on that line, saying that you need to explicitly cast.
Rust is in the bucket of languages where this:
let C = (some f64 expression);
let c0: u32 = C;
is a compilation error. The above code would ultimately have to be written:
// fs, n1, n2 are all u32
let scaler = 256 * n1 * n2;
let C = (fs * scaler) as f64 / 24_000_000.0;
let c0 = C as u32;
let c2 = 10_000;
let c1 = (C * c2) as u32 - c0 * c2;
Notice the last line - it’s much more of a mouthful, there’s a lot more parenthesis, it’s a lot more to type. But it says what’s happening on that line, without having to move your eyeballs across all the preceeding lines: C * c2
is being converted to an int, so it will truncate, and then we subtract off the integer portion.
We could improve it further with some different variable names:
let integer_portion = C as u32;
let denominator = 10_000;
let numerator = (C * denominator) as u32 - integer_portion * denominator;
Of course, the original code is written to match a PDF document somewhere on the Internet, so within the context of that document it may be logical to keep those names. But, the moral remains: When rewriting code, use unit tests - additionally, anytime a language does something for you implicitly, it’s bound to bite you - and the choice of variable names can help the readability of an algorithm.
This blog series updates every week at Programming for Maintainability