Haskell колеблется и плавает - PullRequest
41 голосов
/ 03 сентября 2011

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

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"

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

Ответы [ 2 ]

35 голосов
/ 03 сентября 2011

Синтаксис [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]
13 голосов
/ 04 сентября 2011

Хорошо, @Henning Makholm уже сказал это в своем комментарии, но он не объяснил, почему это на самом деле лучшее решение.

Первое, что нужно сказать: имея дело с плавающей точкой, мы должны всегда знать о возможных ошибках округления.Когда мы пишем [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 ошибка меньшечем один процент.Конечно, не точно, но это присуще методу интегрирования.

Предположим теперь, что диапазоны с плавающей точкой были определены так, чтобы всегда заканчиваться при пересечении правой границы.Тогда было бы возможно (но мы не можем быть уверены в этом!), Что в сумме вычисляется только 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 помогает запомнить эти проблемы!

...