curious
curious

Reputation: 121

Perl - Unexpected outcome of while loop

This is a simple program but I am unable to understand logic/work being done by while loop behind the scene.

Problem: Write a program that prints every number from 0 to 1 that has a single digit after the decimal place (that is, 0.1, 0.2, and so on).

So here is my code:

$num = 0;
while ( $num < 1 ) {
  print "$num \n";
  $num = $num + 0.1;
}

If I write it in this way, it is going to print

$num = 0;
while ( $num < 1 ) {
  $num = $num + 0.1;
  print "$num \n";
}

Output:

0.1 
0.2 
0.3 
0.4 
0.5 
0.6 
0.7 
0.8 
0.9 
1 
1.1 

Ideally speaking, 1 and 1.1 should not get printed in both code samples respectively. After printing 0.9 when 0.1 is added to it, it becomes 1.0 i.e. while (1.0 < 1). Hence condition in while loop is false, so it should not print 1 and 1.1. But that is what is happening.

Can someone please explain why while loop is working in this unexpected way i.e. printing 1 and 1.1 even when condition is false.

Upvotes: 2

Views: 265

Answers (3)

ikegami
ikegami

Reputation: 386386

1/10 is a periodic number in binary just like 1/3 is periodic in decimal.

          ____
1/10 = 0.00011 base 2

As such, it can't be represented exactly by a floating-point number.

$ perl -e'printf "%$.20e\n", 0.1;'
1.00000000000000005551e-01

This imprecision is the cause of your problem.

$ perl -e'my $i = 0; while ($i < 1) { printf "%1\$.3f  %1\$.20e\n", $i; $i += 0.1; }'
0.000  0.00000000000000000000e+00
0.100  1.00000000000000005551e-01
0.200  2.00000000000000011102e-01
0.300  3.00000000000000044409e-01
0.400  4.00000000000000022204e-01
0.500  5.00000000000000000000e-01
0.600  5.99999999999999977796e-01
0.700  6.99999999999999955591e-01
0.800  7.99999999999999933387e-01
0.900  8.99999999999999911182e-01
1.000  9.99999999999999888978e-01

Generally speaking, one can solve this by checking if the number is equal to another within some tolerance. But in this case, there's a simpler solution.

$ perl -e'for my $j (0..9) { my $i = $j/10; printf "%1\$.3f  %1\$.20e\n", $i; }'
0.000  0.00000000000000000000e+00
0.100  1.00000000000000005551e-01
0.200  2.00000000000000011102e-01
0.300  2.99999999999999988898e-01
0.400  4.00000000000000022204e-01
0.500  5.00000000000000000000e-01
0.600  5.99999999999999977796e-01
0.700  6.99999999999999955591e-01
0.800  8.00000000000000044409e-01
0.900  9.00000000000000022204e-01

The above solution not only performs the correct number of iterations, it doesn't accumulate error, so $i is always as correct as it can be.

Upvotes: 4

zoul
zoul

Reputation: 104065

Consider this:

use strict;
use warnings;
use v5.10;

my $num = 0;
while ($num < 1) {
    say $num, " (", $num-1, ")";
    $num += 0.1;
}

This outputs:

0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)
1 (-1.11022302462516e-16)

As you can see, due to floating point precision issues the number gained by repeated adding of 0.1 isn’t exactly one, but something slightly less than one, which is why the check succeeds one more time than expected.

Now add this to the imports at the top of the script:

use bignum;

And watch the output change:

0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)

This is because the computation is now done in a high-precision mode and the floating-point error no longer creeps in.

Usually, people get bitten a few times with floating point arithmetic and then learn to do the important part with integers or epsilons. Google, there’s plenty of resources on how to deal with floating point precision errors.

Upvotes: 2

Yunnosch
Yunnosch

Reputation: 26753

In the second version, you print before the loop checks, i.e. it will even print the case which is already beyond the loop condition.

The fact that you get 1.0 as well as 1.1 is explained by 1.0 being slightly higher than the result of adding a lot of 0.1, because of floating point precision.

So what the loop actually sees is

  • 0.9999 < 1.0 ? Yes, print and loop. Printing does some rounding, so what gets printed is 1.0.
  • 1.0999 < 1.0 ? No, but print nevertheless (because loop checkign is done after printing.

So in order to solve, use the first version but start at 0.1 and check against 0.95.

$num = 0.1;
while ( $num < 0.95 ) {
  print "$num \n";
  $num = $num + 0.1;
}

Upvotes: 4

Related Questions