Мелочи, из-за которых любишь Perl 6 (и Кобол)

Темы:

Вышла прекрасная статья Кёртиса По (Ovid), где на примере работы с дробными числами показано насколько хорошо продуман Perl 6, вплоть до самых мелочей.

Насколько я могу судить, Perl 6 имеет все шансы взлететь и есть одна приятная фича, о который бы я хотел рассказать.

Но перед тем, как я перейду к Perl 6, хотелось бы рассказать, почему я люблю Кобол. Нет, действительно, в Кобол есть то, о чём вы возможно никогда не слышали: упакованные десятичные. Это формат хранения для чисел, которые мейнфреймы понимали нативно. Когда мейнфреймы пришли в бизнес, они очень активно использовались в бухгалтерии (и до сих пор используются) и числа имели основание 10, а не 2 (внутреннее представление было на основании 2, но отображается на основании 10). Мы не используем упакованные десятичные, мы используем числа с плавающей запятой и в этом заключается проблема, но о ней чуть позже.

В Коболе вы могли декларировать знаковое число с именем TOTAL, имеющим 4 цифры перед десятичной запятой и 2 цифры после запятой:

01 TOTAL PIC S9(4)V9(2)

Не вдаваясь в детали, что это значит, числа сохранялись как упакованные целые. Каждая цифра — это один полубайт (4 бита), а две цифры — один байт. В указанном примере две цифры после десятичной точки сохранялись в одном байте и, если вам требовалось сложить список упакованных десятичных чисел, мейнфрейм использовал инструкцию "Сложить Упакованные". Это не было арифметикой с плавающей запятой. Использовалось основание 10, а не основание 2. Это означало, что в COBOL при вычислении 0.1 + 0.2 - 0.3 мы получали в результате 0 (ноль).

Ну а что в Perl 5?

perl -E 'say .1 + .2 - .3'
5.55111512312578e-17

# 0.0000000000000000555111512312578

Это очень маленькое число, но это не ноль. Может быть вам кажется, что всё в порядке, но это не так. Ноль особенный и вы не можете это игнорировать. Например, деление на "ноль":

$ perl -E 'say 1/(.1 + .2 - .3)'
1.8014398509482e+16

# 18,014,398,509,482,000
# примерно 18 квадриллионов

18 квадриллионов это исключительное число, но всё же не исключение. Если вы умножаете массу Солнца на этот "ноль", вы получите 110 триллионов килограмм, что примерно соответствует массе горы Эверест.

Так почему 0.1 + 0.2 - 0.3 не равняется нулю? Из-за формата плавающей запятой.

В арифметике с плавающей запятой числа берутся по основанию 2, а не 10. Мантисса (число после запятой) — это серия нулей и единиц, представляющая степень двойки (сам термин "плавающая запятая" так называется, потому что число это строка нулей и единиц, а "плавающая запятая" указывает где находится десятичная точка).

Поэтому число 0.625 представляется в бинарном виде как 101. Это 1*1/2 + 0*1/4 + 1*1/8 (обратите внимание, что эти степени двойки умножаются на соответствующее значение бита).

Однако большинство чисел можно лишь приблизительно представить числами с плавающей запятой. Для простоты задействуем 8-битную машину. Число 0.1 — это 00011001. С соответствующими степенями двойки это становится 1/16 + 1/32 + 1/256, or 0.09765625. Число 0.2:

Fractions: 1/8 + 1/16 + 1/128 + 1/256
Bits: 00110011
Result: 0.19921875

Число 0.3:

Fractions: 1/4 + 1/32 + 1/64
Bits: 01001100
Result: 0.296875

0.1 на 32-битной машине:

Fractions: 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + 1/65536
         + 1/131072 + 1/1048576 + 1/2097152 + 1/16777216 + 1/33554432
         + 1/268435456 + 1/536870912 + 1/4294967296
Bits: 00011001100110011001100110011001
Result: 0.0999999998603016

Близко, но не точно в яблочко. (Если вам интересно, вы можете рассчитать самостоятельно с помощью программы, которая есть в 3-й главе книги Beginning Perl).

Арифметика с плавающей запятой, это причина по которой большинство финансовых систем использует целые числа вместо плавающей запятой.

Так как очень просто ошибиться с вычислениями с плавающей запятой, разработчики постоянно сталкиваются с этой проблемой. Одно из преимуществ Кобола в том, что он использует упакованные десятичные и представление чисел на основании 10: вычисления, оперирующие с миллиардами долларов, всегда дадут точный ответ.

Увы, но эта тривиальная вычислительная ошибка — неотъемлемое свойство чисел с плавающей запятой, а не Perl:

$ ruby -e 'puts 0.1 + 0.2 - 0.3'
5.551115123125783e-17

$ python -c 'print .1 + .2 - .3'
5.55111512313e-17

$ echo "puts [expr .1+.2-.3]"|tclsh
5.551115123125783e-17

Этот ответ получается при запуске на 64-битных машинах, для 32-битных ответ может отличаться.

Вернёмся к Perl 6:

$ perl6 -e 'say .1 + .2 - .3'
0

$ perl6 -e 'say 1/(.1 + .2 - .3)'
# Divide by zero in method Numeric at ...

Что? Как это произошло? Нет, это не упакованные десятичные. Вместо них Perl 6 использует рациональные числа, каждое из которых с нумератором и деномератором. Давайте загрузим REPL (консоль компилятора). В Perl 6 всё является объектом и мы можем проинспектировать эти объекты. В сессии ниже метод WHAT выводит тип объекта. (Rat — это сокращение от Rational (рациональный), а термин Rational — это роль, позволяющая объектам вести себя как рациональные числа). Метод nude для роли Rational возвращает двухэлементный список, состоящий из нумератора и деномератора:

$ perl6
> say .3.WHAT
(Rat)
> say .3.numerator
3
> say .3.denominator
10
> say .3.nude.perl
(3, 10)

Это демонстрирует, что Perl 6 использует рациональные числа и выполняет арифметические операции с дробными числами. Арифметика с дробной частью целочисленная. Это означает, что размер машинного слова вашего процессора не имеет значения.

Вот ещё интересный пример:

> say 3.1415927.nude.perl
(31415927, 10000000)

Число π, как известно, иррациональное. Иррациональные числа не могут быть выражены в целочисленном представлении (цифры после десятичной запятой бесконечны). Но, вместо того чтобы полагаться на капризы плавающей запятой, вы можете сами задать нужную вам точность.

Есть ещё много практичных возможностей Perl 6, но это одна из самых фантастических. Вспоминая как раньше сравнивали Кобол с Perl 5, я явно вижу иронию в том, что одна из сильных сторон Кобола есть в Perl 6.

Оригинал статьи