Sebuah bukti jauh lebih sulit di dunia OOP karena efek samping, warisan tak terbatas, dan nullmenjadi anggota dari setiap jenis. Kebanyakan bukti bergantung pada prinsip induksi untuk menunjukkan bahwa Anda telah menutupi setiap kemungkinan, dan ketiga hal itu membuat lebih sulit untuk dibuktikan.
Katakanlah kita menerapkan pohon biner yang mengandung nilai integer (demi menjaga sintaksisnya lebih sederhana, saya tidak akan membawa pemrograman generik ke dalam ini, meskipun itu tidak akan mengubah apa pun.) Dalam Standar ML, saya akan mendefinisikan seperti ini:
datatype tree = Empty | Node of (tree * int * tree)
Ini memperkenalkan jenis baru yang disebut treeyang nilainya dapat datang tepat dua varietas (atau kelas, tidak menjadi bingung dengan konsep OOP kelas) - Emptynilai yang tidak membawa informasi, dan Nodenilai - nilai yang membawa 3-tuple yang pertama dan terakhir elemen adalah trees dan yang elemen tengahnya adalah a int. Perkiraan terdekat dengan deklarasi ini di OOP akan terlihat seperti ini:
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;
}
}
}
Dengan peringatan bahwa variabel jenis Pohon tidak pernah bisa null.
Sekarang mari kita menulis fungsi untuk menghitung tinggi (atau kedalaman) dari pohon, dan menganggap kita memiliki akses ke maxfungsi yang mengembalikan lebih besar dari dua angka:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Kami telah mendefinisikan heightfungsi berdasarkan kasus - ada satu definisi untuk Emptypohon dan satu definisi untuk Nodepohon. Kompiler tahu berapa banyak kelas pohon yang ada dan akan mengeluarkan peringatan jika Anda tidak mendefinisikan kedua kasus. Ekspresi Node (leftChild, value, rightChild)dalam fungsi tanda tangan mengikat nilai-nilai dari 3-tupel untuk variabel leftChild, valuedan rightChildmasing-masing sehingga kita bisa merujuk kepada mereka dalam definisi fungsi. Ini mirip dengan mendeklarasikan variabel lokal seperti ini dalam bahasa OOP:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Bagaimana kami dapat membuktikan bahwa kami telah menerapkan heightdengan benar? Kita dapat menggunakan induksi struktural , yang terdiri dari: 1. Buktikan yang heightbenar dalam kasus dasar dari treetipe kita ( Empty) 2. Dengan asumsi bahwa panggilan rekursif heightadalah benar, buktikan bahwa heightitu benar untuk kasus non-dasar ) (ketika pohon itu sebenarnya a Node).
Untuk langkah 1, kita bisa melihat bahwa fungsi selalu mengembalikan 0 ketika argumen adalah Emptypohon. Ini benar dengan definisi ketinggian pohon.
Untuk langkah 2, fungsi kembali 1 + max( height(leftChild), height(rightChild) ). Dengan asumsi bahwa panggilan rekursif benar-benar mengembalikan ketinggian anak-anak, kita dapat melihat bahwa ini juga benar.
Dan itu melengkapi buktinya. Langkah 1 dan 2 menggabungkan semua kemungkinan. Perhatikan, bagaimanapun, bahwa kita tidak memiliki mutasi, tidak ada null, dan ada dua jenis pohon. Singkirkan ketiga kondisi itu dan buktinya cepat menjadi lebih rumit, jika tidak praktis.
EDIT: Karena jawaban ini telah naik ke atas, saya ingin menambahkan contoh yang kurang sepele dari bukti dan menutupi induksi struktural sedikit lebih teliti. Di atas kami membuktikan bahwa jika heightkembali , nilai pengembaliannya benar. Kami belum membuktikannya selalu mengembalikan nilai. Kita dapat menggunakan induksi struktural untuk membuktikan ini juga (atau properti lainnya.) Sekali lagi, selama langkah 2, kita diizinkan untuk mengasumsikan properti memegang panggilan rekursif selama panggilan rekursif semua beroperasi pada anak langsung dari pohon.
Suatu fungsi bisa gagal mengembalikan nilai dalam dua situasi: jika itu melempar pengecualian, dan jika itu berulang selamanya. Pertama mari kita buktikan bahwa jika tidak ada pengecualian yang dilemparkan, fungsi berakhir:
Buktikan bahwa (jika tidak ada pengecualian yang dilemparkan) fungsi berakhir untuk kasing dasar ( Empty). Karena kita mengembalikan 0 tanpa syarat, itu berakhir.
Buktikan bahwa fungsi berakhir pada case non-base ( Node). Ada tiga fungsi panggilan di sini: +, max, dan height. Kami tahu itu +dan maxmengakhiri karena mereka adalah bagian dari perpustakaan standar bahasa dan mereka didefinisikan seperti itu. Seperti yang disebutkan sebelumnya, kami diizinkan untuk menganggap properti yang kami coba buktikan benar pada panggilan rekursif selama mereka beroperasi pada subtree langsung, jadi panggilan untuk heightmengakhiri juga.
Itu menyimpulkan buktinya. Perhatikan bahwa Anda tidak akan dapat membuktikan pemutusan dengan uji unit. Sekarang yang tersisa adalah menunjukkan bahwa heighttidak ada pengecualian.
- Buktikan bahwa
heighttidak melempar pengecualian pada kasus dasar ( Empty). Mengembalikan 0 tidak dapat menghasilkan pengecualian, jadi kami selesai.
- Buktikan bahwa
heighttidak melempar pengecualian pada case non-base ( Node). Asumsikan sekali lagi bahwa kita tahu +dan maxtidak membuang pengecualian. Dan induksi struktural memungkinkan kita untuk menganggap panggilan rekursif tidak akan melempar baik (karena beroperasi pada anak-anak langsung pohon itu). Tapi tunggu! Fungsi ini rekursif, tetapi tidak ekor rekursif . Kita bisa menghancurkan tumpukan! Bukti percobaan kami telah menemukan bug. Kita bisa memperbaikinya dengan mengubah heightmenjadi ekor rekursif .
Saya harap ini menunjukkan bukti tidak harus menakutkan atau rumit. Bahkan, setiap kali Anda menulis kode, Anda secara informal membuat bukti di kepala Anda (jika tidak, Anda tidak akan yakin bahwa Anda baru saja mengimplementasikan fungsinya.) Dengan menghindari nol, mutasi yang tidak perlu, dan warisan tak terbatas, Anda dapat membuktikan bahwa intuisi Anda adalah memperbaikinya dengan cukup mudah. Pembatasan ini tidak sekeras yang Anda kira:
null adalah cacat bahasa dan menghilangkannya adalah baik tanpa syarat.
- Mutasi kadang-kadang tidak dapat dihindari dan perlu, tetapi dibutuhkan jauh lebih jarang daripada yang Anda pikirkan - terutama ketika Anda memiliki struktur data yang persisten.
- Adapun memiliki jumlah kelas yang terbatas (dalam arti fungsional) / subclass (dalam arti OOP) vs jumlah yang tidak terbatas dari mereka, itu adalah subjek yang terlalu besar untuk satu jawaban . Cukuplah untuk mengatakan ada trade design di sana - kemampuan kebenaran versus fleksibilitas ekstensi.