F# Floating point ranges are experimental and may be deprecated

后端 未结 6 1785
猫巷女王i
猫巷女王i 2020-12-19 15:27

I\'m trying to make a little function to interpolate between two values with a given increment.

[ 1.0 .. 0.5 .. 20.0 ]

The compiler tells m

6条回答
  •  遥遥无期
    2020-12-19 16:05

    TL;DR: F# PowerPack's BigRational type is the way to go.


    What's Wrong with Floating-point Loops

    As many have pointed out, float values are not suitable for looping:

    • They do have Round Off Error, just like with 1/3 in decimal, we inevitably lose all digits starting at a certain exponent;
    • They do experience Catastrophic Cancellation (when subtracting two almost equal numbers, the result is rounded to zero);
    • They always have non-zero Machine epsilon, so the error is increased with every math operation (unless we are adding different numbers many times so that errors mutually cancel out -- but this is not the case for the loops);
    • They do have different accuracy across the range: the number of unique values in a range [0.0000001 .. 0.0000002] is equivalent to the number of unique values in [1000000 .. 2000000];

    Solution

    What can instantly solve the above problems, is switching back to integer logic.

    With F# PowerPack, you may use BigRational type:

    open Microsoft.FSharp.Math
    // [1 .. 1/3 .. 20]
    [1N .. 1N/3N .. 20N]
    |> List.map float
    |> List.iter (printf "%f; ")
    

    Note, I took my liberty to set the step to 1/3 because 0.5 from your question actually has an exact binary representation 0.1b and is represented as +1.00000000000000000000000 * 2-1; hence it does not produce any cumulative summation error.

    Outputs:

    1.000000; 1.333333; 1.666667; 2.000000; 2.333333; 2.666667; 3.000000; (skipped) 18.000000; 18.333333; 18.666667; 19.000000; 19.333333; 19.666667; 20.000000;

    // [0.2 .. 0.1 .. 3]
    [1N/5N .. 1N/10N .. 3N]
    |> List.map float
    |> List.iter (printf "%f; ")
    

    Outputs:

    0.200000; 0.300000; 0.400000; 0.500000; (skipped) 2.800000; 2.900000; 3.000000;

    Conclusion

    • BigRational uses integer computations, which are not slower than for floating-points;
    • The round-off occurs only once for each value (upon conversion to a float, but not within the loop);
    • BigRational acts as if the machine epsilon were zero;

    There is an obvious limitation: you can't use irrational numbers like pi or sqrt(2) as they have no exact representation as a fraction. It does not seem to be a very big problem because usually, we are not looping over both rational and irrational numbers, e.g. [1 .. pi/2 .. 42]. If we do (like for geometry computations), there's usually a way to reduce the irrational part, e.g. switching from radians to degrees.


    Further reading:

    • What Every Computer Scientist Should Know About Floating-Point Arithmetic
    • Numeric types in PowerPack

提交回复
热议问题