Использование более узкого языка не просто перемещает целевые посты от правильной реализации к правильной спецификации. Трудно сделать что-то очень неправильное, но логически непротиворечивое; вот почему компиляторы ловят так много ошибок.
Арифметика указателей в том виде, в котором она обычно формулируется, не имеет смысла, поскольку система типов на самом деле не означает, что она должна означать. Вы можете полностью избежать этой проблемы, работая на языке сборки мусора (обычный подход, который заставляет вас также платить за абстракцию). Или вы можете более точно определить, какие типы указателей вы используете, так что компилятор может отклонить все, что является непоследовательным или просто не может быть доказано, что оно правильно написано. Это подход некоторых языков, таких как Rust.
Построенные типы эквивалентны доказательствам, поэтому, если вы напишете систему типов, которая забывает об этом, то все виды не работают. Предположим на некоторое время, что когда мы объявляем тип, мы фактически имеем в виду, что мы утверждаем правду о том, что находится в переменной.
- int * x; // Ложное утверждение. х существует и не указывает на int
- int * y = z; // только true, если доказано, что z указывает на int
- * (х + 3) = 5; // только true, если (x + 3) указывает на int в том же массиве, что и x
- int c = a / b; // только true, если b ненулевой, например: "nonzero int b = ...;"
- обнуляемый int * z = NULL; // nullable int * - это не то же самое, что int *
- int d = * z; // Ложное утверждение, потому что z обнуляется
- if (z! = NULL) {int * e = z; } // Хорошо, потому что z не нуль
- бесплатно (у); int w = * y; // Ложное утверждение, потому что y больше не существует в w
В этом мире указатели не могут быть нулевыми. Разыменования NullPointer не существует, и указатели не нужно нигде проверять на нулевое значение. Вместо этого «nullable int *» - это другой тип, значение которого может быть извлечено либо в ноль, либо в указатель. Это означает, что в тот момент, когда начинается ненулевое предположение, вы либо регистрируете свое исключение, либо переходите к нулевой ветви.
В этом мире ошибок массива вне границ тоже не существует. Если компилятор не может доказать, что он находится в границах, попробуйте переписать, чтобы компилятор мог это доказать. Если этого не произойдет, вам придется вручную ввести предположение в этом месте; компилятор может найти противоречие с этим позже.
Кроме того, если у вас не будет указателя, который не инициализирован, у вас не будет указателей на неинициализированную память. Если у вас есть указатель на освобожденную память, то он должен быть отклонен компилятором. В Rust существуют разные типы указателей, чтобы можно было ожидать таких доказательств. Существуют исключительно собственные указатели (то есть: без псевдонимов), указатели на глубоко неизменяемые структуры. Тип хранилища по умолчанию является неизменным и т. Д.
Существует также проблема применения фактической четко определенной грамматики для протоколов (которая включает элементы интерфейса), чтобы ограничить область входной поверхности в точности тем, что ожидается. Суть «правильности» в следующем: 1) избавиться от всех неопределенных состояний 2) обеспечить логическую согласованность . Сложность попадания во многом связана с использованием крайне плохого инструмента (с точки зрения правильности).
Именно поэтому две худшие практики - это глобальные переменные и переходы. Эти вещи мешают помещать пре / пост / инвариантные условия вокруг чего-либо. Это также, почему типы так эффективны. По мере того, как типы становятся сильнее (в конечном счете, используя зависимые типы, чтобы принять во внимание фактическое значение), они становятся сами по себе конструктивными доказательствами правильности; несогласованные программы не скомпилируются.
Имейте в виду, что это не просто глупые ошибки. Речь идет также о защите базы кода от умных проникновений. Будут случаи, когда вам придется отклонить отправку без убедительного сгенерированного машиной доказательства важных свойств, таких как «следует формально указанному протоколу».