Yan etkiler, sınırsız kalıtım ve nullher türden üye olması nedeniyle OOP dünyasında bir kanıt çok daha zordur . Çoğu kanıt, her olasılığı kapsadığınızı göstermek için bir indüksiyon prensibine dayanır ve bu 3 şeyin hepsi bunu kanıtlamayı zorlaştırır.
Diyelim ki tamsayı değerler içeren ikili ağaçlar uyguluyoruz (sözdizimini daha basit tutmak için, hiçbir şeyi değiştirmemesine rağmen genel programlamayı buna getirmeyeceğim.) Standart ML'de bunu şöyle tanımlarım bu:
datatype tree = Empty | Node of (tree * int * tree)
Bu tree, değerleri tam olarak iki çeşit (veya sınıf, OOP kavramıyla karıştırılmaması gereken) olarak adlandırılan yeni bir tür sunar - Emptyhiçbir bilgi Nodetaşımayan bir değer ve ilk ve son olan 3 tuple taşıyan değerler elemanları trees ve orta elemanı bir int. OOP'taki bu bildirgeye en yakın yaklaşım şöyle görünecektir:
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;
}
}
}
Uyarı ile Tree tipi değişkenler asla olamaz null.
Şimdi ağacın yüksekliğini (veya derinliğini) hesaplamak için bir işlev yazalım maxve iki sayıdan daha büyük olan bir işleve erişimimiz olduğunu varsayalım :
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
heightİşlevi vakalara göre tanımladık - Emptyağaçlar için bir tanım ve Nodeağaçlar için bir tanım var . Derleyici kaç ağaç sınıfı olduğunu bilir ve her iki durumu da tanımlamazsanız bir uyarı verir. Sentezleme Node (leftChild, value, rightChild)fonksiyonu imza değişkenlere 3-tuple değerlerini bağlanır leftChild, valueve rightChildsırası ile biz işlev tanımında bunlara atıfta böylece. OOP dilinde böyle yerel değişkenleri bildirmeye benzer:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
heightDoğru uyguladığımızı nasıl kanıtlayabiliriz ? Biz kullanabilir yapısal indüksiyon oluşur: Kanıtlayacak 1. heighteden (s) taban durumunda doğrudur tree(tip Emptyyinelemeli çağrılar için varsayılarak) 2. heightdoğru, kanıtlamak height(temel olmayan durum için doğru s ) (ağaç aslında a olduğunda Node).
Adım 1 için, bağımsız değişken bir Emptyağaç olduğunda işlevin her zaman 0 döndürdüğünü görebiliriz . Bu, bir ağacın yüksekliğinin tanımı ile doğrudur.
Adım 2 için işlev geri döner 1 + max( height(leftChild), height(rightChild) ). Yinelemeli çağrıların gerçekten çocukların yüksekliğini döndürdüğünü varsayarsak, bunun da doğru olduğunu görebiliriz.
Ve bu da kanıtı tamamlar. Adım 1 ve 2 tüm olasılıkları bir araya getirmiştir. Bununla birlikte, hiçbir mutasyonumuz, null'umuz olmadığı ve tam olarak iki çeşit ağaç olduğunu unutmayın. Bu üç koşulu ortadan kaldırırsanız, kanıt pratik olmasa bile çabucak daha karmaşık hale gelir.
EDIT: Bu cevap zirveye yükseldiğinden, daha az önemsiz bir kanıt eklemek ve yapısal indüksiyon biraz daha ayrıntılı bir şekilde kapsamak istiyorum. Üstü bunu kanıtladı eğer heightgetiri , onun dönüş değeri doğrudur. Yine de her zaman bir değer döndürdüğünü kanıtlamadık. Bunu da kanıtlamak için yapısal indüksiyonu kullanabiliriz (veya başka bir mülk.) Yine, 2. adımda, özyinelemeli çağrıların, doğrudan ağacı.
Bir işlev iki durumda bir değer döndüremez: bir istisna atarsa ve sonsuza dek dönerse. İlk olarak, herhangi bir istisna atılmazsa, fonksiyonun sona erdiğini kanıtlayalım:
Kanıtlamak (istisnalar atılmadıysa), temel durumlar ( Empty) için işlevin sonlandığını kanıtlayın . Koşulsuz olarak 0 döndürdüğümüz için sona erer.
Temel olmayan durumlarda işlevin sona erdiğini kanıtlayın ( Node). Orada burada üç işlev çağrıları var: +, max, ve height. Bunu biliyoruz +ve maxsonlandırıyoruz çünkü dilin standart kütüphanesinin bir parçası ve bu şekilde tanımlanıyorlar. Daha önce de belirtildiği gibi, kanıtlamaya çalıştığımız mülkün, acil alt ağaçlarda çalıştığı sürece özyinelemeli çağrılarda doğru olduğunu varsaymaya izin veriyoruz, bu nedenle heightsonlandırma çağrıları da.
Bu kanıtı sonuçlandırır. Birim testi ile sonlandırmayı kanıtlayamayacağınızı unutmayın. Şimdi geriye kalan tek şeyin heightistisnalar atmadığını göstermek .
- Bunun
heighttemel duruma istisnalar atmadığını kanıtlayın ( Empty). 0 döndürmek bir istisna atamaz, bu yüzden işimiz bitti.
- Bunun
heighttemel dışı vaka ( Node) için istisna oluşturmadığını kanıtlayın . Bir kez daha bildiğimizi +ve maxistisnalar atmadığımızı varsayın . Yapısal indüksiyon, özyinelemeli çağrıların da atmayacağını varsaymamızı sağlar (çünkü ağacın hemen çocukları üzerinde çalışın.) Ama bekleyin! Bu işlev özyinelemeli, ancak kuyruk özyinelemeli değil . Yığını patlatabiliriz! Deneme kanıtımız bir hatayı ortaya çıkardı. Kuyruk özyinelemeli olarak değiştirerekheight düzeltebiliriz .
Umarım bu kanıtların korkutucu veya karmaşık olması gerekmediğini gösterir. Aslında, kod yazdığınızda, kafanızda gayri resmi olarak bir kanıt oluşturdunuz (aksi takdirde, sadece işlevi yerine getirdiğinize ikna olmazsınız.) Boş, gereksiz mutasyon ve sınırsız kalıtımdan kaçınarak sezgilerinizi kanıtlayabilirsiniz kolayca düzeltin. Bu kısıtlamalar düşündüğünüz kadar sert değildir:
null bir dil hatasıdır ve bunu ortadan kaldırmak koşulsuz olarak iyidir.
- Mutasyon bazen kaçınılmaz ve gereklidir, ancak düşündüğünüzden çok daha az sıklıkta ihtiyaç duyulur - özellikle de kalıcı veri yapılarınız olduğunda.
- Sınırsız sayıda (işlevsel anlamda) / alt sınıflara (OOP anlamında) karşı sınırsız sayıda derse sahip olmak, tek bir cevap için çok büyük bir konudur . Orada bir tasarım ticareti olduğunu söylemek yeterli - doğruluk olasılığı ve uzatma esnekliği.