Диапазоны и поплавки Haskell

Почему поведение нотации диапазона Haskell отличается от поплавков, чем для целых чисел и символов?

Prelude> [1, 3 .. 10] :: [Int]
[1,3,5,7,9] 
Prelude> [1, 3 .. 10] :: [Float]
[1.0,3.0,5.0,7.0,9.0,11.0]
Prelude> ['a', 'c' .. 'f']
"ace"

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

Ответ 1

Синтаксис [e1, e2 .. e3] - действительно синтаксический сахар для enumFromThenTo e1 e2 e3, который является функцией в классе Enum.

Стандарт Haskell определяет его семантику следующим образом:

Для типов Int и Integer функции перечисления имеют следующее значение:

  • Последовательность enumFrom e1 - это список [e1,e1 + 1,e1 + 2,…].
  • Последовательность enumFromThen e1 e2 - это список [e1,e1 + i,e1 + 2i,…], где приращение i равно e2 − e1. Инкремент может быть равен нулю или отрицательный. Если приращение равно нулю, все элементы списка являются то же самое.
  • Последовательность enumFromTo e1 e3 - это список [e1,e1 + 1,e1 + 2,…e3]. Список пуст, если e1 > e3.
  • Последовательность enumFromThenTo e1 e2 e3 - это список [e1,e1 + i,e1 + 2i,…e3], где приращение i равно e2 − e1. Если приращение положительный или нулевой, список заканчивается, когда следующий элемент будет больше, чем e3; список пуст, если e1 > e3. Если приращение отрицательный, список заканчивается, когда следующий элемент будет меньше, чем e3; список пуст, если e1 < e3.

Это в значительной степени то, что вы ожидаете, но экземпляры Float и Double определяются по-разному:

Для Float и Double семантика семейства enumFrom задается правилами для Int выше, за исключением того, что список заканчивается, когда элементы становятся больше e3 + i∕2 для положительного приращения i, или когда они становятся меньше e3 + i∕2 для отрицательных i.

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

Вы можете обойти это, перечислив с помощью целых чисел и преобразовать в Float после.

Prelude> map fromIntegral [1, 3 .. 10] :: [Float]
[1.0,3.0,5.0,7.0,9.0]

Ответ 2

Хорошо, Хеннинг Махольм уже сказал это в своем комментарии, но он не объяснил, почему это действительно лучшее решение.

Прежде всего, говоря: имея дело с плавающей точкой, мы всегда должны знать о возможных ошибках округления. Когда мы пишем [0.0, 0.1 .. 1.0], мы должны знать, что все эти числа, кроме первого, не будут в точном месте десятых. Где нам нужна эта определенность, мы не должны использовать поплавки вообще.

Но, конечно, есть много приложений, где мы довольствуемся разумной уверенностью, но нуждаемся в высокоскоростных. То, что плавает отлично. Одним из возможных применений такого списка было бы простое трапециевидное числовое интегрирование:

trIntegrate f l r s = sum [ f x | x<-[l,(l+s)..r] ] * s - (f(l)+f(r))*s/2

давайте протестируем это: trIntegrate ( \x -> exp(x + cos(sqrt(x) - x*x)) ) 1.0 3.0 0.1 = > 25.797334337026466
по сравнению с 25.9144 ошибка менее одного процента. Конечно, не точный, но присущий методу интеграции.

Предположим теперь, что диапазоны float были определены так, чтобы всегда заканчиваться при пересечении правой границы. Тогда было бы возможно (но мы не можем быть уверены в этом!), Что в сумме вычисляются только 20 значений, а не 21, потому что последнее значение x оказывается равным 3.000000. Мы можем имитировать этот

bad_trIntegrate f l r s = sum [ f x | x<-[l,(l+s)..(r-s)] ] * s - (f(l)+f(r))*s/2

то получим

bad_trIntegrate ( \x -> exp(x + cos(sqrt(x) - x*x)) ) 1.0 3.0 0.1

= > 21.27550564546988
Urgh!

Это не имеет ничего общего со скрытием проблем с плавающей запятой. Это просто метод, помогающий программисту легче обойти эти проблемы. Фактически, противоположный результат [1, 3 .. 10] :: Float помогает запомнить эти проблемы!