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

На языках, которые я тестировал, - (x div y ) не равно -x div y; Я тестировал // в Python, / в Ruby, div в Perl 6; C имеет аналогичное поведение.

Это поведение обычно соответствует spec, так как div обычно определяется как округление результата деления, однако оно не имеет большого смысла с арифметической точки зрения, поскольку оно приводит к тому, что div ведет себя по-другому, в зависимости от на знаке, и это вызывает путаницу, такую как этот пост о том, как это делается на Python.

Есть ли какое-то конкретное обоснование этого дизайнерского решения, или просто div определяется таким образом с нуля? По-видимому, Guido van Rossum использует аргумент согласованности в блоге, в котором объясняется, как это делается на Python, но вы также можете иметь согласованность, если вы решите округлить.

(Вдохновленный этим вопросом PMurias в IRC-канале # perl6)

Ответы

Ответ 1

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

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

По этой причине операции с целым числом, как правило, обертываются при переполнении (для добавления, умножения и сдвига-левого) и округляются к отрицательной бесконечности при создании неточного результата (для деления и сдвига-право). Оба эти элемента являются простым усечением на своем конце целого числа в двоичной арифметике с двумя дополнениями; самый простой способ обработки углового футляра.

В других ответах обсуждается связь с оператором остатка или модуля, который язык может предоставлять наряду с делением. К сожалению, у них есть это в обратном направлении. Остальное зависит от определения деления, а не наоборот, в то время как модуль может быть определен независимо от деления - если оба аргумента оказываются положительными, а разделение округляется, они работают одинаково, поэтому люди редко замечают.

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

Ответ 2

В идеале мы хотели бы иметь две операции div и mod, удовлетворяющие для каждого b>0:

  1. (a div b) * b + (a mod b) = a
  2. 0 <= (a mod b) < b
  3. (-a) div b = -(a div b)

Это, однако, математическая невозможность. Если бы все вышеизложенное было правдой, мы бы

1 div 2 = 0
1 mod 2 = 1

так как это единственное целочисленное решение для (1) и (2). Следовательно, в силу (3)

0 = -0 = -(1 div 2) = (-1) div 2

которое в силу (1) влечет

-1 = ((-1) div 2) * 2 + ((-1) mod 2) = 0 * 2 + ((-1) mod 2) = (-1) mod 2

(-1) mod 2 < 0 что противоречит (2).

Следовательно, нам нужно отказаться от некоторого свойства среди (1), (2) и (3).

Некоторые языки программирования отказываются (3) и делают div округленным вниз (Python, Ruby).

В некоторых (редких) случаях язык предлагает несколько операторов деления. Например, в Haskell мы имеем div,mod удовлетворяющий только (1) и (2), аналогично Python, и мы также имеем quot,rem удовлетворяющий только (1) и (3). Последняя пара операторов округляет деление к нулю, по цене возврата отрицательных остатков, например, мы имеем (-1) 'quot' 2 = 0 и (-1) 'rem' 2 = (-1).

С# также отказывается (2) и позволяет % вернуть отрицательный остаток. Когерентно целое деление округляется к нулю. Java, Scala, Pascal и C, начиная с C99, также принимают эту стратегию.

Ответ 3

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

Ответ 4

В Википедии есть отличная статья, в том числе история, а также теория.


Пока язык удовлетворяет свойству евклидова деления, что (a/b) * b + (a%b) == a, как разделение полов, так и усечение деления являются когерентными и арифметически разумными.


Конечно, люди любят утверждать, что человек явно прав, а другой явно ошибается, но у него больше характер священной войны, чем разумная дискуссия, и обычно она больше связана с выбором их раннего предпочитаемого языка, чем что-либо остальное. Они также часто склонны спорить в первую очередь о своих выбранных %, хотя, вероятно, имеет смысл выбрать / сначала, а затем просто выбрать % которое соответствует.

  • Полы (например, Python):
    • Не менее авторитет, чем предлагает Дональд Кнут.
    • % следуя знаку делителя, очевидно, что примерно 70% всех студентов угадывают
    • Оператор обычно читается как mod или modulo а не remainder.
    • "C делает это", что даже не верно. 1
  • Усечение (например, C++):
    • Делает целочисленное деление более согласованным с поплавковым делением IEEE (в режиме округления по умолчанию).
    • Это реализует больше процессоров. (Не может быть правдой в разное время в истории.)
    • Оператор читается modulo а не по remainder (хотя это фактически противоречит их точке).
    • Свойство деления концептуально больше относится к остатку, чем к модулю.
    • Оператор читает mod а не modulo, поэтому он должен следовать разграничению Fortran. (Это может показаться глупым, но, возможно, это был ключ для C99. См. Эту тему.)
  • "Евклид" (например, Pascal- / полы или усечения в зависимости от знаков, поэтому % никогда не отрицателен):
    • Никлаус Вирт утверждал, что никого не удивляет позитивный mod.
    • Раймонд Т. Буте позже утверждал, что вы не можете реализовать евклидово подразделение наивно с любым из других правил.

Некоторые языки предоставляют оба. Как правило, как в Ada, Modula-2, некоторые Lisps, Haskell и Julia, они используют имена, связанные с mod для оператора в стиле Python, и rem для оператора C++. Но не всегда - Fortran, например, называет то же самое modulo и mod (как упоминалось выше для C99).


Мы не знаем, почему Python, Tcl, Perl и другие влиятельные языки сценариев в основном выбирают напольные покрытия. Как отметил в этом вопросе, Гвидо ван Россум объясняет, почему ему нужно было выбрать один из трех последовательных ответов, а не почему он выбрал тот, который он сделал.

Тем не менее, я подозреваю, что влияние C было ключевым. Большинство языков сценариев (по крайней мере на начальном этапе) реализованы на C и заимствуют их инвентарь оператора из C. C89. Определенный реализацией % явно нарушен и не подходит для "дружественного" языка, такого как Tcl или Python. И C называет оператор "мод". Поэтому они идут с модулем, а не с остатком.


1. Несмотря на то, что говорит вопрос, и многие люди используют его в качестве аргумента, C фактически не имеет подобного поведения для Python и друзей.C99 требует усечения деления, а не настила.C89 разрешен либо, а также разрешен либо вариант мода, поэтому нет гарантии свойства деления, и нет способа написать переносимый код, выполняющий целочисленное деление со знаком.Это просто сломано.

Ответ 5

Как сказала Паула, это из-за остатка.

Алгоритм основан на евклидовом подразделении.

В Ruby вы можете написать эту перестройку дивиденда с последовательностью:

puts (10/3)*3 + 10%3
#=> 10

Он работает так же в реальной жизни. 10 яблок и 3 человека. Хорошо, вы можете вырезать одно яблоко в три, но выйти за пределы целых чисел.

С отрицательными числами сохраняется также последовательность:

puts (-10/3)*3 + -10%3 #=> -10
puts (10/(-3))*(-3) + 10%(-3) #=> 10
puts (-10/(-3))*(-3) + -10%(-3) #=> -10

Фактор всегда округляется вниз (вниз вдоль отрицательной оси), и напоминание следует:

puts (-10/3) #=> -4
puts -10%3 #=> 2

puts (10/(-3)) #=> -4
puts 10%(-3) # => -2

puts (-10/(-3)) #=> 3
puts -10%(-3) #=> -1