Close enough

Perl 6 has some amazing numerical features, including builtin complex number computations. You might never need these, but they permit a bit of math geekry that I find quite pleasing. That pleasure is especially strong in Euler’s Formula:

$$
e^{i\pi} = -1
$$

It’s even more pleasing when I move everything to one side (and this will become important to Perl 6 later):

$$
e^{i\pi} + 1 = 0
$$

This tiny formula has everything you need to know about everything (almost). There’s the natural base, the imaginary unit, the ratio of a circle’s circumference to its diameter, and the multiplicative and additive identities. Curiously how all of that works out.

When I try this in the Perl 6 REPL I almost got the right answer, but not quite:

$ perl6
> e**(i*pi)
-1+1.22464679914735e-16i
> e**(i*pi) == -1
False

That’s right. I almost get the right answer. Most of you are probably used to these minor imprecisions and the practical programming considerations that come with them.

That’s not a problem because Perl 6 anticipates these situations with the =~=, or the approximately-equal operator:

> e**(i*pi) =~= -1
True

So, close enough is good enough for horseshoes, hand grenades, and now Perl 6.

I can also use (U+2245, ᴀᴘᴘʀᴏxɪᴍᴀᴛᴇʟʏ ᴇqᴜᴀʟ ᴛᴏ) version of the operator:

> e**(i*pi) ≅ -1
True

The $*TOLERANCE variable controls how almost “almost” is. By default it’s 1e-15. That’s really small but its still larger than the 1.22464679914735e-16i extra bit I saw.

I need to be careful though. The =~= looks at the relative difference instead of the absolute difference. Here’s something you might not expect to be approximately equal since the values differ by 1 (which you think is much larger than 1e-15):

> 9999999999999999 =~= 9999999999999998
True

It’s their relative difference that matters though. That’s the absolute value of their difference divided by the least absolute value of the two:

$$
\frac{| x – y |}{ min( |x|, |y| ) }
$$

Turn that into Perl 6 (I really like that infix min operator):

> sub relative-difference ( $x, $y ) { abs( $x - $y ) / ( abs($x) min abs($y) ) }
> relative-difference( 999999999999999, 999999999999998 ).fmt('%e')
1.000000e-15
> relative-difference( 999999999999999, 999999999999997 ).fmt('%e')
2.000000e-15

This works because very small differences in the large numbers might be less significant as very small differences in small numbers?

Huh?

It sounds like double talk, but this takes into account the size of the numbers when looking at the tolerance. Consider the case at the small end. Any value smaller than 1e-15 is within 1e-15 of 1e-15, but that isn’t very helpful. The relative difference takes the sizes of the values into account. Thus, this is false because the relative difference is almost an order of magnitude:

> 1e-15 =~= 1e-16
False
> relative-difference( 1e-15, 1e-16 ).fmt('%e')
9.000000e+00

This way, the tolerance value doesn’t overwhelm the comparison as you get close to the tolerance itself.

That’s not the end of the story, though. Why is this one different? These are the same values with one moved over to the other side of the comparison:

> 1e-15 - 1e-16 =~= 0
True

There’s a problem with relative difference. As the smaller value gets much closer to zero, the relative difference goes to infinity:

> relative-difference( 1e-15, 1e-17 ).fmt('%e')
9.900000e+01
> relative-difference( 1e-15, 1e-18 ).fmt('%e')
9.990000e+02
> relative-difference( 1e-15, 1e-19 ).fmt('%e')
9.999000e+03
> relative-difference( 1e-15, 1e-21 ).fmt('%e')
9.999990e+05

The absolute difference in all of those are less than 1e-15, but the relative difference are very high. So, when one side of my comparison is exactly 0, the =~= switches to the absolute difference. That’s why 1e-15 - 1e-16 =~= 0 is True.

This means you can get different answers depending on the structure of the comparison:

> 9999999999999999 - 9999999999999998 =~= 0 # absolute
False
> 9999999999999999 =~= 9999999999999998     # relative
True

If I check that the difference is close to zero, I compare the absolute difference. If I compare two non-zero values to each other, I compare their relative difference.

Although it doesn’t matter in this case, that’s one of the pleasures of moving the 1 to the right:

> e**(i*pi) + 1 =~= 0
True

And, here’s one final tidbit. You can change the tolerance. If you want two and two to be five, select the right tolerance level:

> my $*TOLERANCE = 0.21; 2 + 2 =~= 5      # relative
True
> my $*TOLERANCE = 0.21; 2 + 2 - 5 =~= 0  # absolute
False
> my $*TOLERANCE = 1.1; 2 + 2 - 5 =~= 0   # absolute
True

Now I want the string version of this.

Leave a Reply

Your email address will not be published. Required fields are marked *