Доказ набагато складніший у світі OOP через побічні ефекти, необмежене успадкування та null
членство кожного типу. Більшість доказів покладаються на принцип індукції, щоб показати, що ви охопили всі можливості, і всі 3 з цих речей ускладнюють доведення.
Скажімо, ми реалізовуємо двійкові дерева, які містять цілі значення (задля спрощення синтаксису, я не вношу в це загальне програмування, хоча це нічого не змінить.) У стандартному ML я б визначив це як це:
datatype tree = Empty | Node of (tree * int * tree)
Це вводить новий тип під назвою tree
, значення якого може містити рівно два різновиди (або класи, не плутати з концепцією класу OOP класу) - Empty
значення, яке не містить інформації, і Node
значення, які несуть 3-кортеж, перший і останній елементами є tree
s, середнім елементом якого є 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
є мовним недоліком і безумовно добре це усунути.
- Мутація іноді неминуча і необхідна, але вона потрібна набагато рідше, ніж ви думаєте, особливо, коли у вас є стійкі структури даних.
- Що стосується обмеженої кількості класів (у функціональному сенсі) / підкласів (у розумінні ООП) проти необмеженої їх кількості, то це тема занадто велика для однієї відповіді . Досить сказати, що там є дизайн дизайну - доцільність правильності та гнучкість розширення.