F # Диапазоны с плавающей точкой являются экспериментальными и могут быть устаревшими

Я пытаюсь сделать небольшую функцию для интерполяции между двумя значениями с заданным приращением.

[ 1.0 .. 0.5 .. 20.0 ]

Компилятор говорит мне, что это устарело и предлагает использовать ints, а затем кастинг для float. Но это кажется немного длинным, если у меня есть дробное приращение - нужно ли мне делить свои начальные и конечные значения на мой инкремент, затем несколько раз после? (Yeuch!).

Я где-то где-то видел что-то об использовании последовательных понятий, но не могу вспомнить, как это сделать.

Помогите, пожалуйста.

Ответы

Ответ 1

TL; DR: F # PowerPack BigRational тип - это путь.


Что не так с петлями с плавающей запятой

Как указывали многие, значения float не подходят для цикла:

  • У них есть Round Off Error, так же, как с 1/3 в десятичной форме, мы неизбежно теряем все цифры, начиная с определенного показателя,
  • Они испытывают катастрофическое аннулирование (при вычитании двух почти равных чисел результат округляется до нуля);
  • У них всегда есть ненулевое машина epsilon, поэтому при каждой математической операции ошибка увеличена (если мы не добавляем разные числа так что ошибки взаимно сокращаются - но это не так для циклов);
  • Они имеют разную точность по всему диапазону: количество уникальных значений в диапазоне [0.0000001 .. 0.0000002] эквивалентно количеству уникальных значений в [1000000 .. 2000000];

Решение

Что может мгновенно решить вышеуказанные проблемы, это возврат к целочисленной логике.

С F # PowerPack вы можете использовать тип BigRational:

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

Примечание. Я взял на себя смелость установить шаг 1/3, потому что 0.5 из вашего вопроса на самом деле имеет точное двоичное представление 0,1 b и представляется как +1.00000000000000000000000 * 2 -1; следовательно, он не производит кумулятивной ошибки суммирования.

Выходы:

1.000000; 1.333333; 1.666667; 2.000000; 2.333333; 2.666667; 3.000000; (пропущено) 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; ")

Выходы:

0.200000; 0.300000; 0.400000; 0.500000; (пропущено) 2.800000; 2.900000; 3.000000;

Заключение

  • BigRational использует целочисленные вычисления, которые не медленнее, чем для плавающих точек;
  • округление происходит только один раз для каждого значения (при преобразовании в float, но не в пределах цикла);
  • BigRational действует так, как будто epsilon машины равен нулю;

Существует очевидное ограничение: вы не можете использовать иррациональные числа, такие как pi или sqrt(2), поскольку они не имеют точного представления в виде дробной части. Это не кажется очень большой проблемой, потому что обычно мы не зацикливаемся на рациональные и иррациональные числа, например. [1 .. pi/2 .. 42]. Если мы это сделаем (например, для вычислений геометрии), обычно существует способ уменьшить иррациональную часть, например. переключение с радианов на градусы.


Дальнейшее чтение:

Ответ 2

Интересно, что диапазоны float больше не устаревают. И я помню, что недавно видел вопрос (извините, не смог его отследить), говоря о неотъемлемых проблемах, которые проявляются с диапазонами float, например.

> let xl = [0.2 .. 0.1 .. 3.0];;

val xl : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9]

Я просто хотел указать, что вы можете использовать диапазоны для типов decimal с гораздо меньшим количеством таких проблем округления, например.

> [0.2m .. 0.1m .. 3.0m];;
val it : decimal list =
  [0.2M; 0.3M; 0.4M; 0.5M; 0.6M; 0.7M; 0.8M; 0.9M; 1.0M; 1.1M; 1.2M; 1.3M;
   1.4M; 1.5M; 1.6M; 1.7M; 1.8M; 1.9M; 2.0M; 2.1M; 2.2M; 2.3M; 2.4M; 2.5M;
   2.6M; 2.7M; 2.8M; 2.9M; 3.0M]

И если вам действительно нужно плавать в конце, то вы можете сделать что-то вроде

> {0.2m .. 0.1m .. 3.0m} |> Seq.map float |> Seq.toList;;
val it : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0]

Ответ 3

Как отмечали Джон и другие, выражения диапазона с плавающей запятой не являются численно надежными. Например [0.0 .. 0.1 .. 0.3] равно [0.0 .. 0.1 .. 0.2]. Использование десятичных или Int-типов в выражении диапазона, вероятно, лучше.

Для float я использую эту функцию, она сначала увеличивает общий диапазон 3 раза самым маленьким шагом с плавающей точкой. Я не уверен, является ли этот алгоритм очень надежным. Но для меня достаточно хорошо убедиться, что значение стопа включено в Seq:

let floatrange start step stop =
    if step = 0.0 then  failwith "stepsize cannot be zero"
    let range = stop - start 
                |> BitConverter.DoubleToInt64Bits 
                |> (+) 3L 
                |> BitConverter.Int64BitsToDouble
    let steps = range/step
    if steps < 0.0 then failwith "stop value cannot be reached"
    let rec frange (start, i, steps) =
        seq { if i <= steps then 
                yield start + i*step
                yield! frange (start, (i + 1.0), steps) }
    frange (start, 0.0, steps)

Ответ 4

Попробуйте следующее выражение последовательности

seq { 2 .. 40 } |> Seq.map (fun x -> (float x) / 2.0)

Ответ 5

Вы также можете написать относительно простую функцию для создания диапазона:

let rec frange(from:float, by:float, tof:float) =
   seq { if (from < tof) then 
            yield from
            yield! frange(from + by, tof) }

Используя это, вы можете просто написать:

frange(1.0, 0.5, 20.0)

Ответ 6

Обновленная версия ответа Томаса Петричека, которая компилируется и работает для уменьшения диапазонов (и работает с единицами измерения):  (но это не выглядит так красиво)

let rec frange(from:float<'a>, by:float<'a>, tof:float<'a>) = 
   // (extra ' here for formatting)
   seq { 
        yield from
        if (float by > 0.) then
            if (from + by <= tof) then yield! frange(from + by, by, tof) 
        else   
            if (from + by >= tof) then yield! frange(from + by, by, tof) 
       }

#r "FSharp.Powerpack"
open Math.SI 
frange(1.0<m>, -0.5<m>, -2.1<m>)

ОБНОВЛЕНИЕ. Я не знаю, является ли это новым или если это всегда возможно, но я только что обнаружил (здесь), что этот - более простой - синтаксис также возможен:

let dl = 9.5 / 11.
let min = 21.5 + dl
let max = 40.5 - dl

let a = [ for z in min .. dl .. max -> z ]
let b = a.Length

(Остерегайтесь, там в этом конкретном примере есть:)