31

For example, I want to show a list of buttons from 0,0.5,... 5, which jumps for each 0.5. I use a for loop to do that, and have different color at button STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

At this case there should be no rounding errors as each value is exact in IEEE 754.But I'm struggling if I should change it to avoid floating point equality comparison:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

On one hand, the original code is more simple and forward to me. But there is one thing I'm considering : is i==STANDARD_LINE misleads junior teammates? Does it hide the fact that floating point numbers may have rounding errors? After reading comments from this post:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

it seems there are many developers don't know some float numbers are exact. Should I avoid float number equality comparisons even if it is valid in my case? Or am I over thinking about this?

Kilian Foth
  • 107,706
  • 45
  • 295
  • 310
ocomfd
  • 5,652
  • 8
  • 29
  • 37
  • 24
    The behavior of these two code listings is not equivalent. 3/2.0 is 1.5 but `i` will only ever be whole numbers in the second listing. Try removing the second `/2.0`. – candied_orange Jan 31 '18 at 07:52
  • 29
    If you absolutely need to compare two FPs for equality (which isn't required as others pointed out in their fine answers since you can just use a loop counter comparison with integers), but if you did, then a comment ought to suffice. Personally I've been working with IEEE FP for a long time and I'd still be confused if I saw, say, direct SPFP comparison without any kind of comment or anything. It's just very delicate code -- worth a comment at least every time IMHO. –  Jan 31 '18 at 08:04
  • Direct equality/inequality comparison, that is! I work in computer graphics so I often see code (not like this) depending on underlying IEEE standards, but always with a comment when direct equality/inequality comparisons are made with SPFP at least. –  Jan 31 '18 at 08:09
  • 14
    Regardless of which you choose, this is one of those cases where a comment explaining the how and why is absolutely essential. A later developer may not even consider the subtleties without a comment to bring them to their attention. Also, I'm heavily distracted by the fact that `button` doesn't change anywhere in your loop. How is the list of buttons accessed? Via index into array or some other mechanism? If it's by index access into an array, this is another argument in favor of switching to integers. – jpmc26 Jan 31 '18 at 12:00
  • 3
    I strongly support @jpmc26. The typical means to communicate non-obvious implementation choices or -restraints is to write a comment. (I am sure you are aware of it; why don't you present it as one alternative?) I would strongly object to version 2 which is an obfuscation. On the other hand it's not clear from the contrived example whether your real-world problem really needs float comparisons, and where the constants come from, and if they could at some point change (say, DIFF to 0.3). I suspect that the float comparison is not really needed, but if it is, do it properly (with epsilons). – Peter - Reinstate Monica Jan 31 '18 at 12:23
  • Even though your code may be problem free, remember that the young 'uns will see your code and think that floats are fine. You are an example to others whether or not you want to. – Haakon Løtveit Jan 31 '18 at 13:27
  • http://floating-point-gui.de/errors/comparison/ https://randomascii.wordpress.com/2012/02/11/they-sure-look-equal/ – Berin Loritsch Jan 31 '18 at 17:15
  • 10
    Write that code. Until someone thinks that 0.6 would be a better step size and simply changes that constant. – tofro Jan 31 '18 at 17:15
  • Your second example would be much improved by adding a constant `lineSize = 0.5` and multiplying by that rather than dividing by two – Richard Tingle Feb 01 '18 at 07:14
  • 13
    _"...mislead junior developers"_ You will mislead senior developers also. Despite the amount of thought you have put into this, they will assume that you didn't know what you were doing, and will likely change it to the integer version anyway. – GrandOpener Feb 01 '18 at 07:20
  • 1
    I don't see a list of buttons here. Just a single button. – paparazzo Feb 01 '18 at 08:16
  • You could change the loop to a while loop, which avoids having the float as counter. – allo Feb 01 '18 at 15:41
  • Side note, are you sure you don't want to use something like [BigNumber](https://github.com/MikeMcl/bignumber.js/) instead? My firm recently got bit by JavaScript numbers being stupid and we found this library to be splendid. – corsiKa Feb 01 '18 at 15:50
  • 2
    It is bad code period. In general, "Easy to understand" is 3rd in line for the most important attributes of code. "On-Time" and "It Works" are the only things more important. Requiring other developers to be aware of obscure trivia, such as some floating point numbers are exact when used on a system that uses this specific representation format and the data is this specific size, seems to me to be the opposite of "Easy to Understand". – Dunk Feb 01 '18 at 21:24
  • I don't agree with most of this commentary. If your code relies on a specific property such as 0.5d being exact, *write a comment* to that effect. Nothing wrong with the code *per se.* – user207421 Feb 02 '18 at 04:25
  • Note that this code is brittle if for any reason it needs to be refactored to another value not having these properties. Consider the future you who forgot you did this. – Thorbjørn Ravn Andersen Oct 17 '22 at 07:42

9 Answers9

117

I would always avoid successive floating-point operations unless the model I'm computing requires them. Floating-point arithmetic is unintuitive to most and a major source of errors. And telling the cases in which it causes errors from those where it doesn't is an even more subtle distinction!

Therefore, using floats as loop counters is a defect waiting to happen and would require at the very least a fat background comment explaining why it's okay to use 0.5 here, and that this depends on the specific numeric value. At that point, rewriting the code to avoid float counters will probably be the more readable option. And readability is next to correctness in the hierarchy of professional requirements.

Kilian Foth
  • 107,706
  • 45
  • 295
  • 310
  • 50
    I like "a defect waiting to happen". Sure, it might work *now*, but a light breeze from someone walking by will break it. – AakashM Jan 31 '18 at 10:39
  • 11
    For example, suppose the requirements change so that instead of 11 equally spaced buttons from 0 to 5 with the "standard line" on the 4th button, you have 16 equally-spaced buttons from 0 to 5 with the "standard line" on the 6th button. So whoever inherited this code from you changes 0.5 to 1.0/3.0 and changes 1.5 to 5.0/3.0. What happens then? – David K Jan 31 '18 at 12:49
  • 9
    Yeah, I'm uncomfortable with the idea that changing what *seems* to be an arbitrary number (as "normal" as a number could be) to another arbitrary number (that *seems* equally "normal") actually introduces a defect. – Alexander Jan 31 '18 at 15:06
  • 9
    @Alexander: right, you'd need a comment that said `DIFF must be an exactly-representable double that evenly divides STANDARD_LINE`. If you don't want to write that comment (and rely on all future developers to know enough about IEEE754 binary64 floating point to understand it), then don't write the code this way. i.e. don't write the code this way. Especially because it's probably not even more efficient: FP addition has higher latency than integer addition, and it's a loop-carried dependency. Also, compilers (even JIT compilers?) probably do better at making loops with integer counters. – Peter Cordes Feb 01 '18 at 12:04
41

As a general rule, loops should be written in such a way as to think of doing something n times. If you're using floating point indices, it is no longer a question of doing something n times but rather running until a condition is met. If this condition happens to be very similar to the i<n that so many programmers expect, then the code appears to be doing one thing when it is actually doing another which can be easily misinterpreted by programmers skimming the code.

It's somewhat subjective, but in my humble opinion, if you can rewrite a loop to use an integer index to loop a fixed number of times, you should do so. So consider the following alternative:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

The loop works in terms of whole numbers. In this case i is an integer and STANDARD_LINE is coerced to an integer as well. This would of course change the position of your standard line if there happened to be roundoff and likewise for MAX, so you should still strive to prevent roundoff for accurate rendering. However you still have the advantage of changing parameters in terms of pixels and not whole numbers without having to worry about the comparison of floating points.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179
Neil
  • 22,670
  • 45
  • 76
  • 3
    You may also want to consider rounding instead of flooring in the assignments, depending on what you want. If the division is supposed to give an integer result, floor might give surprises if you stumble upon numbers where the division happens to be slightly off. – ilkkachu Jan 31 '18 at 12:51
  • 1
    @ilkkachu True. My thoughts were that if you're setting 5.0 as the maximum pixel amount, then through rounding, you'd preferably like to be on the lower side of that 5.0 rather than slightly more. 5.0 would effectively be a maximum. Though rounding may be preferable according to what you need to do. In either case, it makes little difference if the division creates a whole number anyway. – Neil Jan 31 '18 at 12:53
  • 4
    I strongly disagree. The best way to stop a loop is by the condition that most naturally expresses the business logic. If the business logic is that you need 11 buttons, the loop should stop at iteration 11. If the business logic is that the buttons are 0.5 apart until the line is full, the loop should stop when the line is full. There are other considerations that can push the choice toward one mechanism or the other but absent those considerations, choose the mechanism that closest matches the business requirement. – Reinstate Monica Jan 31 '18 at 19:04
  • Your explanation would be completely correct for Java/C++/Ruby/Python/... But Javascript doesn't have integers, so `i` and `STANDARD_LINE` only look like integers. There's no coercion at all, and `DIFF`, `MAX` and `STANDARD_LINE` are all just `Number`s. `Number`s used as integers should be safe below [`2**53`](http://2ality.com/2013/10/safe-integers.html), they're still floating point numbers, though. – Eric Duminil Feb 01 '18 at 08:17
  • @EricDuminil Yes, but this is the half of it. The other half is readability. I do mention it as the principle reason for doing it this way, not for optimization. – Neil Feb 01 '18 at 08:40
21

I agree with all the other answers that using a non-integer loop variable is generally bad style even in cases like this one where it will work correctly. But it seems to me that there's another reason why it's bad style here.

Your code "knows" that the available line widths are precisely the multiples of 0.5 from 0 up to 5.0. Should it? It seems like that's a user-interface decision that might easily change (e.g., maybe you want the gaps between available widths to become larger as the widths do. 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0 or something).

Your code "knows" that the available line widths all have "nice" representations both as floating-point numbers and as decimals. That also seems like something that might change. (You might want 0.1, 0.2, 0.3, ... at some point.)

Your code "knows" that the text to put on the buttons is simply what Javascript turns those floating-point values into. That also seems like something that might change. (E.g., perhaps some day you'll want widths like 1/3, which you probably wouldn't want to display as 0.33333333333333 or whatever. Or perhaps you want to see "1.0" instead of "1" for consistency with "1.5".)

These all feel to me like manifestations of a single weakness, which is a sort of mixing of layers. Those floating-point numbers are part of the internal logic of the software. The text shown on the buttons is part of the user interface. They should be more separate than they are in the code here. Notions like "which of these is the default that should be highlighted?" are user-interface matters, and they probably shouldn't be tied to those floating-point values. And your loop here is really (or at least should be) a loop over buttons, not over line widths. Written that way, the temptation to use a loop variable taking non-integer values disappears: you'd just be using successive integers or a for...in/for...of loop.

My feeling is that most cases where one might be tempted to loop over non-integer numbers are like this: there are other reasons, entirely unrelated to numerical issues, why the code should be organized differently. (Not all cases; I can imagine some mathematical algorithms might be most neatly expressed in terms of a loop over non-integer values.)

8

One code smell is using floats in loop like that.

Looping can be done in a lot of ways, but in 99.9% of the cases you should stick to an increment of 1 or there will definitely be confusion, not only by junior developers.

Pieter B
  • 12,867
  • 1
  • 40
  • 65
  • I disagree, I think integer multiples of 1 are not confusing in a for-loop. I wouldn't consider that a code smell. Only fractions. – CodeMonkey Feb 02 '18 at 07:12
3

Yes, you do want to avoid this.

Floating point numbers are one of the biggest trap for the unsuspecting programmer (which means, in my experience, almost everybody). From depending on floating point equality tests, to representing money as floating point, it's all a big quagmire. Adding up one float upon the other is one of the biggest offenders. There are whole volumes of scientific literature about things like this.

Use floating point numbers exactly in places where they are appropriate, for example when doing actual mathematical calculations where you need them (like trigonometry, plotting function graphs etc.) and be super careful when doing serial operations. Equality is right out. Knowledge about which particular set of numbers is exact by IEEE standards is very arcane and I would never depend on it.

In your case, there will, by Murphys Law, come the point where management wants you to not have 0.0, 0.5, 1.0 ... but 0.0, 0.4, 0.8 ... or whatever; you you will be immediately borked, and your junior programmer (or yourself) will debug long and hard until you find the problem.

In your particular code, I would indeed have an integer loop variable. It represents the ith button, not the running number.

And I would probably, for the sake of extra clarity, not write i/2 but i*0.5 which makes it abundantly clear what is going on.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Note: as pointed out in the comments, JavaScript does not actually have a separate type for integers. But integers up to 15 digits are guaranteed to be accurate/safe (see https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), hence for arguments like this ("is it more confusing/error prone to work with integers or non-integers") this is appropriately close to having a separate type "in spirit"; in daily use (loops, screen coordinates, array indices etc.) there will be no surprises with integer numbers represented as Number as JavaScript.

AnoE
  • 5,614
  • 1
  • 13
  • 17
  • I'd change the name BUTTONS to something else - there are 11 buttons after all and not 10. Maybe FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Apart from that, yes, that's how you should do it. – gnasher729 Jan 31 '18 at 23:22
  • It is true, @EricDuminil, and I have added a bit about this into the answer. Thank you! – AnoE Feb 01 '18 at 11:19
1

I don't think that either of your suggestions is good. Instead, I would introduce a variable for the number of buttons based on the maximum value and the spacing. Then, it is simple enough to loop over the indices of the button themselves.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

It might be more code, but it is also more readable and more robust.

Jared Goguen
  • 200
  • 1
  • 1
  • 8
0

You can avoid the whole thing by computing the value you are showing rather than using the loop counter as the value:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
Arnab Datta
  • 191
  • 8
0

I would display buttons at positions numbered first = 0 to last = 10, starting at start = 0.0 and ending at end = 5.0.

So button #3 is red. And button #i is displayed at start + (end - start) / (last -first) * (i - first).

Now you have zero problems if you change the number of buttons, their numbering, their positions a distances.

The original code works right now because of which constants were used. But it can break in many ways if you change the constant. There is no guarantee that 3*0.4 == 1.2, for example. (Bizarrely you can prove that x+x+x == 3 * x, but that would break for the next button).

gnasher729
  • 42,090
  • 4
  • 59
  • 119
-1

Floating point arithmetic is slow and integer arithmetic is fast, so when I use floating point, I would not use it unnecessarily where integers can be used. It is useful to always think of floating point numbers, even constants, as approximate, with some small error. It is very useful during debugging to replace native floating point numbers with plus/minus floating point objects where you treat each number as a range instead of a point. That way you uncover progressive growing inaccuracies after each arithmetic operation. So "1.5" should be thought of as "some number between 1.45 and 1.55", and "1.50" should be thought of as "some number between 1.495 and 1.505".

J.Farges
  • 7
  • 1
  • 5
    The performance difference between integers and floats is important when writing C code for a small microprocessor, but modern x86-derived CPUs do floating point so fast that any penalty is easily eclipsed by the overhead of using a dynamic language. In particular, does not Javascript actually represent _every_ number as floating-point anyway, using the NaN payload when necessary? – leftaroundabout Jan 31 '18 at 23:04
  • 1
    "Floating point arithmetic is slow and integer arithmetic is fast" is a historical truism that you should not retain as gospel moving forward. To add to what @leftaroundabout said, it's not just true that the penalty would be almost irrelevant, you may well find floating-point operations to be *faster* than their equivalent integer operations, thanks to the magic of autovectorizing compilers and instruction sets that can crunch large amounts of floats in one cycle. For this question it's not relevant, but the basic "integer is faster than float" assumption hasn't been true for quite a while. – Jeroen Mostert Feb 01 '18 at 11:01
  • 1
    @JeroenMostert SSE/AVX have vectorised operations for both integers and floats, and you may be able to use smaller integers (because no bits are wasted on exponent), so _in principle_ one might often still squeeze out more performance from highly-optimised integer code than with floats. But again, this isn't relevant for most applications and definitely not for this question. – leftaroundabout Feb 01 '18 at 11:12
  • 1
    @leftaroundabout: Sure. My point wasn't about which one is *definitely faster* in any given situation, just that "I know FP is slow and integer is fast so I'll use integers if at all possible" isn't a good motivation even before you tackle the question of whether the thing you're doing needs optimization. – Jeroen Mostert Feb 01 '18 at 11:19
  • Putting a button on a screen takes ages compared to calculating its position. So performance is a complete non-argument. – gnasher729 Oct 16 '22 at 20:48