Sebuah bukti jauh lebih sulit di dunia OOP karena efek samping, warisan tak terbatas, dan null
menjadi 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 tree
yang nilainya dapat datang tepat dua varietas (atau kelas, tidak menjadi bingung dengan konsep OOP kelas) - Empty
nilai yang tidak membawa informasi, dan Node
nilai - nilai yang membawa 3-tuple yang pertama dan terakhir elemen adalah tree
s 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 max
fungsi 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 height
fungsi berdasarkan kasus - ada satu definisi untuk Empty
pohon dan satu definisi untuk Node
pohon. 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
, value
dan rightChild
masing-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 height
dengan benar? Kita dapat menggunakan induksi struktural , yang terdiri dari: 1. Buktikan yang height
benar dalam kasus dasar dari tree
tipe kita ( Empty
) 2. Dengan asumsi bahwa panggilan rekursif height
adalah benar, buktikan bahwa height
itu 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 Empty
pohon. 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 height
kembali , 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 max
mengakhiri 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 height
mengakhiri juga.
Itu menyimpulkan buktinya. Perhatikan bahwa Anda tidak akan dapat membuktikan pemutusan dengan uji unit. Sekarang yang tersisa adalah menunjukkan bahwa height
tidak ada pengecualian.
- Buktikan bahwa
height
tidak melempar pengecualian pada kasus dasar ( Empty
). Mengembalikan 0 tidak dapat menghasilkan pengecualian, jadi kami selesai.
- Buktikan bahwa
height
tidak melempar pengecualian pada case non-base ( Node
). Asumsikan sekali lagi bahwa kita tahu +
dan max
tidak 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 height
menjadi 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.