В мире ООП доказательство намного сложнее из-за побочных эффектов, неограниченного наследования и null
принадлежности к любому типу. Большинство доказательств основаны на принципе индукции, чтобы показать, что вы рассмотрели все возможности, и все три из этих вещей затрудняют доказательство.
Допустим, мы реализуем двоичные деревья, которые содержат целочисленные значения (для упрощения синтаксиса я не буду вносить в это общее программирование, хотя это ничего не изменит.) В стандарте ML я определил бы это как это:
datatype tree = Empty | Node of (tree * int * tree)
Это вводит новый тип с именем tree
, значения которого могут входить ровно в две разновидности (или классы, которые не следует путать с концепцией ООП класса) - Empty
значение, которое не несет никакой информации, и Node
значения, которые содержат 3-кортеж, первый и последний элементы tree
s и средний элемент которых является int
. Ближайшее приближение к этому объявлению в ООП будет выглядеть так:
public class Tree {
private Tree() {} // Prevent external subclassing
public static final class Empty extends Tree {}
public static final class Node extends Tree {
public final Tree leftChild;
public final int value;
public final Tree rightChild;
public Node(Tree leftChild, int value, Tree rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
}
}
С оговоркой, что переменные типа Tree никогда не могут быть null
.
Теперь давайте напишем функцию для вычисления высоты (или глубины) дерева и предположим, что у нас есть доступ к max
функции, которая возвращает большее из двух чисел:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Мы определили height
функцию по кейсам - есть одно определение для Empty
деревьев и одно определение для Node
деревьев. Компилятор знает, сколько классов деревьев существует, и выдает предупреждение, если вы не определили оба случая. Выражение Node (leftChild, value, rightChild)
в сигнатуре функции связывает значения 3-кортежа переменных leftChild
, value
и , rightChild
соответственно , таким образом , мы можем обратиться к ним в определении функции. Это похоже на объявление локальных переменных, таких как это на языке ООП:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Как мы можем доказать, что мы реализовали height
правильно? Мы можем использовать структурную индукцию , которая состоит из: 1. Доказать, что height
это правильно в базовом случае (ах) нашего tree
типа ( Empty
) 2. Предполагая, что рекурсивные вызовы height
верны, доказать, что height
это верно для неосновного (и) случая (ов) ) (когда дерево на самом деле Node
).
На шаге 1 мы видим, что функция всегда возвращает 0, когда аргумент является Empty
деревом. Это правильно по определению высоты дерева.
Для шага 2 функция возвращается 1 + max( height(leftChild), height(rightChild) )
. Предполагая, что рекурсивные вызовы действительно возвращают рост дочерних элементов, мы видим, что это также правильно.
И это завершает доказательство. Комбинированные шаги 1 и 2 исчерпывают все возможности. Заметьте, однако, что у нас нет ни мутаций, ни нулей, и существует ровно две разновидности деревьев. Уберите эти три условия, и доказательство быстро станет более сложным, если не непрактичным.
РЕДАКТИРОВАТЬ: Так как этот ответ поднялся до вершины, я хотел бы добавить менее тривиальный пример доказательства и немного подробнее рассмотреть структурную индукцию. Выше мы доказали, что если height
возвращает , его возвращаемое значение является правильным. Мы не доказали, что оно всегда возвращает значение. Мы также можем использовать структурную индукцию, чтобы доказать это (или любое другое свойство). Опять же, на шаге 2 мы можем предположить, что свойства являются рекурсивными вызовами, если все рекурсивные вызовы работают с прямым потомком объекта. дерево.
Функция может не вернуть значение в двух ситуациях: если она выдает исключение, и если она зацикливается вечно. Сначала давайте докажем, что если не сгенерировано никаких исключений, функция завершается:
Докажите, что (если не выдается никаких исключений) функция завершается для базовых случаев ( Empty
). Поскольку мы безоговорочно возвращаем 0, он завершается.
Докажите, что функция заканчивается в неосновных случаях ( Node
). Там три вызовов функций здесь: +
, max
и height
. Мы знаем это +
и max
заканчиваем, потому что они являются частью стандартной библиотеки языка, и они определены таким образом. Как упоминалось ранее, мы можем предположить, что свойство, которое мы пытаемся доказать, является истинным для рекурсивных вызовов, если они работают с непосредственными поддеревьями, поэтому вызовы также должны height
завершаться.
Это завершает доказательство. Обратите внимание, что вы не сможете доказать завершение модульным тестом. Теперь осталось только показать, что height
не генерирует исключений.
- Докажите, что
height
не генерирует исключения в базовом случае ( Empty
). Возвращение 0 не может выдать исключение, поэтому мы закончили.
- Докажите, что
height
не вызывает исключение в неосновном случае ( Node
). Предположим еще раз, что мы знаем +
и max
не бросаем исключения. И структурная индукция позволяет нам предполагать, что рекурсивные вызовы также не будут генерироваться (потому что работают с непосредственными дочерними элементами дерева.) Но подождите! Эта функция является рекурсивной, но не хвостовой . Мы могли бы взорвать стек! Наше попытанное доказательство обнаружило ошибку. Мы можем исправить это, изменив height
хвостовую рекурсию .
Я надеюсь, что это показывает, что доказательства не должны быть страшными или сложными. Фактически, всякий раз, когда вы пишете код, вы неофициально создаете доказательство в своей голове (в противном случае вы не будете уверены, что просто реализовали функцию.) Избегая нулевого значения, ненужных мутаций и неограниченного наследования, вы можете доказать, что ваша интуиция исправить довольно легко. Эти ограничения не так суровы, как вы думаете:
null
это языковой недостаток, и избавиться от него безусловно хорошо.
- Мутация иногда неизбежна и необходима, но она нужна гораздо реже, чем вы думаете, особенно если у вас есть постоянные структуры данных.
- Что касается конечного числа классов (в функциональном смысле) / подклассов (в смысле ООП) по сравнению с их неограниченным количеством, то это слишком большой вопрос для одного ответа . Достаточно сказать, что здесь есть компромисс между дизайном - доказуемость правильности и гибкость расширения.