Yan etkiler, sınırsız kalıtım ve null
her 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 - Empty
hiçbir bilgi Node
taşımayan bir değer ve ilk ve son olan 3 tuple taşıyan değerler elemanları tree
s 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 max
ve 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 - Empty
ağaçlar için bir tanım ve Node
ağ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
, value
ve rightChild
sı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();
height
Doğru uyguladığımızı nasıl kanıtlayabiliriz ? Biz kullanabilir yapısal indüksiyon oluşur: Kanıtlayacak 1. height
eden (s) taban durumunda doğrudur tree
(tip Empty
yinelemeli çağrılar için varsayılarak) 2. height
doğ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 Empty
ağ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 height
getiri , 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 max
sonlandı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 height
sonlandı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 height
istisnalar atmadığını göstermek .
- Bunun
height
temel duruma istisnalar atmadığını kanıtlayın ( Empty
). 0 döndürmek bir istisna atamaz, bu yüzden işimiz bitti.
- Bunun
height
temel dışı vaka ( Node
) için istisna oluşturmadığını kanıtlayın . Bir kez daha bildiğimizi +
ve max
istisnalar 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.