Доказ набагато складніший у світі OOP через побічні ефекти, необмежене успадкування та nullчленство кожного типу. Більшість доказів покладаються на принцип індукції, щоб показати, що ви охопили всі можливості, і всі 3 з цих речей ускладнюють доведення.
Скажімо, ми реалізовуємо двійкові дерева, які містять цілі значення (задля спрощення синтаксису, я не вношу в це загальне програмування, хоча це нічого не змінить.) У стандартному ML я б визначив це як це:
datatype tree = Empty | Node of (tree * int * tree)
Це вводить новий тип під назвою tree, значення якого може містити рівно два різновиди (або класи, не плутати з концепцією класу OOP класу) - Emptyзначення, яке не містить інформації, і Nodeзначення, які несуть 3-кортеж, перший і останній елементами є trees, середнім елементом якого є an 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;
}
}
}
З застереженням, що змінних типу Дерево ніколи не може бути 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відповідно , таким чином , ми можемо звернутися до них у визначенні функції. Це схоже на оголошення локальних змінних на зразок цієї мови на мові OOP:
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 комбінують усі можливості. Зауважте, однак, що у нас немає ні мутації, ні нулів, і є рівно два сорти дерев. Заберуть ці три умови, і доказ швидко ускладнюється, якщо не недоцільно.
EDIT: Оскільки ця відповідь піднялася до вершини, я хотів би додати менш тривіальний приклад доказу та трохи детальніше висвітлити структурну індукцію. Вище ми довели, що якщо heightповертається , його повернене значення є правильним. Ми не довели, що це завжди повертає значення. Ми можемо використовувати структурну індукцію, щоб довести це теж (або будь-яку іншу властивість.) Знову на кроці 2 нам дозволяється припускати властивості рекурсивних викликів до тих пір, поки рекурсивні виклики працюють на прямому дочірньому дерево.
Функція може не повернути значення у двох ситуаціях: якщо вона кидає виняток і якщо вона циклічно назавжди. Спершу докажемо, що якщо жодних винятків не буде, функція припиняється:
Доведіть, що (якщо немає винятків) функція припиняється для базових випадків ( Empty). Оскільки ми беззастережно повертаємо 0, воно припиняється.
Доведіть, що функція припиняється в неосновних випадках ( Node). Там три викликів функцій тут: +, maxі height. Ми це знаємо +і maxприпиняємо, оскільки вони є частиною стандартної бібліотеки мови і вони визначені саме так. Як згадувалося раніше, ми можемо вважати, що властивість, яку ми намагаємося довести, є вірною для рекурсивних дзвінків, якщо вони працюють на безпосередніх підрядках, тому дзвінки також heightприпиняються.
Це завершує доказ. Зауважте, що ви не зможете довести припинення за допомогою одиничного тесту. Тепер все, що залишилося, - це показати, що heightне кидає винятків.
- Доведіть, що
heightне викидає виключень у базовому випадку ( Empty). Повернення 0 не може кинути виняток, тому ми закінчили.
- Доведіть, що
heightне викидає виняток у неосновному випадку ( Node). Припустимо ще раз, що ми знаємо +і maxне кидаємо винятків. І структурна індукція дозволяє припустити, що рекурсивні дзвінки також не будуть кидатись (тому що діяти на безпосередніх дітей дерева.) Але зачекайте! Ця функція є рекурсивною, але не хвостовою рекурсивною . Ми могли б підірвати стек! Наші спроби виявили помилку. Ми можемо виправити це, змінивши його heightна рекурсивний хвіст .
Я сподіваюся, що це свідчить про те, що докази не повинні бути страшними або складними. Насправді, кожного разу, коли ви пишете код, ви неофіційно створювали доказ у своїй голові (інакше ви не переконаєтесь, що просто реалізували цю функцію.) Уникаючи нульової, непотрібної мутації та необмеженої спадщини, ви можете довести свою інтуїцію виправити досить легко. Ці обмеження не такі суворі, як ви могли б подумати:
null є мовним недоліком і безумовно добре це усунути.
- Мутація іноді неминуча і необхідна, але вона потрібна набагато рідше, ніж ви думаєте, особливо, коли у вас є стійкі структури даних.
- Що стосується обмеженої кількості класів (у функціональному сенсі) / підкласів (у розумінні ООП) проти необмеженої їх кількості, то це тема занадто велика для однієї відповіді . Досить сказати, що там є дизайн дизайну - доцільність правильності та гнучкість розширення.