вторник, 20 июня 2017 г.

Целочисленные типы

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

Целочисленные типы должны быть разделены на две группы:
  1. Ограниченное число предопределённых типов, подходящих для большинства задач, и к которым применимы встроенные арифметические операции: +, -, *, /, %.
  2. Типы из обычных или псевдо-разделов, для работы с которыми необходимо использовать функции из тех же разделов с соответствующими названиями: add, sub, mul, div, mod и т.д.

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

Предопределённые типы

В первую группу должны войти:
НазваниеДиапазон допустимых значений
byte 0 .. (28 - 1)
int -(231 - 1) .. (231 - 1)
longint -(263 - 1) .. (263 - 1)

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

int и longint - знаковые, симметричные относительно 0 типы. Равенство по модулю максимального и минимального значений упрощает учёт корректных вычислений, в частности, гарантирует, что смена знака и значение по модулю всегда выполнимы без переполнения. Это важней желания вместить на одно допустимое значение больше, как это позволяют наиболее распространённые ныне платформы из-за использования дополнительного кода для кодирования отрицательных чисел. В большинстве случаев минимальное отрицательное даже не нужно, но непропорционально своему значению требует избыточного внимания в случае необходимости достижения полной корректности.

Главным целочисленным типом является int, так как он представляет хороший компромисс по предоставляемому диапазону чисел, достаточного для большинства задач, и возможностью эффективной обработки. В частности, количество элементов массива не может превышать максимального значения int. longint нужен как из-за того, что в современных условиях нередки ситуации, где диапазона 32-битных значений всё же не хватает, например, для кодирования времени в наносекундах или обработки больших файлов, так и потому что расширенный диапазон естественным образом возникает при вычислениях в полном диапазоне int.

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

В арифметических выражениях, в которых смешиваются операнды разных целочисленных типов, операнды меньшего типа рассматриваются как эквивалентные операнды наибольшего. Вычисления выражений для операндов в byte неявно происходят в диапазоне int, для int и longint в диапазоне longint. Контроль переполнения происходит как в выражениях, так и при сохранении значения. Если диапазон приёмника меньше диапазона типа выражения, требуется явное преобразование типа. Если такой же или больше, то не требуется, но контроль переполнения выполняется в любом случае. Такое правило позволяет уменьшить вероятность переполнений и не требует избыточных явных приведений типа.

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

Защитный языкJava
var (a, b, c int; d longint; f bool)

a = max(int) / 2; 
b = a + 2; 
c = 300;

f = a + b < c; // false
d = a * b; // 1152921504606846975
c = b * 2 / 3 // 715827883
int a, b, c; long d; boolean f;

a = Integer.MAX_VALUE / 2;
b = a + 2;
c = 300;

f = a + b < c; // true
d = a * b;     // -1
c = b * 2 / 3; // -715827882

Типы предопределённых разделов

Вторую группу целочисленных типов должны представить типы из состава специальных разделов, отличающихся знаковостью и разрядностью гарантированных диапазонов. Разделы должны предоставлять почти одинаковые наборы функций для работы с собственными целочисленными типами. Имена разделов можно вывести из синтаксического уравнения - имя = [u]int(8|16|32|64), где u обозначает беззнаковость, а число, естественно - разрядность типа. Диапазоны допустимых значений определяются по формулам - [0 .. 2n-1] для беззнаковых и [-2n-1 .. 2n-1-1] для чисел со знаком, так как они должны быть закодированы через двоичное дополнение.

Объявления разделов:
type (t) // сам целочисленный тип

var (min, max t) // минимальное и максимальное значение

// Группа функций, которые трактуют переполнение как ошибку.
// Если при их выполнении не было явно получено состояние корректности,
// то в случае переполнения они завершают выполнение программы.
func add(addend1,  addend2 t)    (sum t);
func sub(minuend,  subtrahend t) (difference t);
func mul(factor1,  factor2 t)    (product t);
func div(divident, divisor t)    (ratio t);
func mod(divident, divisor t)    (remainder t);

// Группа процедур, для которых переполнение сопровождается воображаемым
// отбросом старших разрядов.
proc mod_add(addend1, addend2,    > sum t)        (overflow bool);
proc mod_sub(minuend, subtrahend, > difference t) (overflow bool);
proc mod_mul(factor1, factor2,    > product t)    (overflow bool);

// Функции для преобразования к основным типам и обратно. Поскольку при
// этом может возникнуть переполнение, то функции также подразумевают
// явное взятие корректности, либо завершение работы.
func to_byte   (value? t) (result byte);
func to_int    (value? t) (result int);
func to_longint(value? t) (result longint);

func from_byte   (value byte)    (result t);
func from_int    (value int)     (result t);
func from_longint(value longint) (result t);

// Функции интерпретации стандартных типов как соответствующих их
// диапазону типов из разделов противоположной знаковости и наоборот.

// в разделе int8:
func as_byte(value t) (result byte);
func as_t(value byte) (result t);

// в разделе uint32:
func as_int(value t) (result int);
func as_t(value int) (result t);

// в разделе uint64:
func as_longint(value t) (result longint);
func as_t(value longint) (result t);

Битовые операции для встроенных целочисленных типов не предусмотрены как неуместные. Вместо них вводятся:

  1. Встроенная функция для возведения в степень func pow(v, n int) (res int) или отдельная операция v^n. Выбор будет сделан позднее.
  2. Типы-множества, которые можно приводить к целочисленным и обратно и о которых подробней будет рассказано в отдельной заметке.

Дополнительные материалы:

  1. Danger – unsigned types used here!
  2. INT14-C. Avoid performing bitwise and arithmetic operations on the same data.