Fonksiyonel programlamada kişi matematiksel yasalarla modülerliğe nasıl ulaşır?


11

Bu soruda , işlevsel programcıların programlarının düzgün çalıştığından emin olmak için matematiksel kanıt kullanma eğiliminde olduklarını okudum . Bu, birim testinden çok daha kolay ve hızlı geliyor, ancak bir OOP / Birim Testi arka planından geldiğini hiç görmedim.

Bana açıklayabilir ve bana bir örnek verebilir misiniz?


7
"Bu, birim testinden çok daha kolay ve daha hızlı geliyor". Evet, sesler. Gerçekte, çoğu yazılım için pratik olarak imkansızdır. Ve neden başlık modülerlikten bahsediyor, ancak doğrulama hakkında konuşuyorsunuz?
Euphoric

@ EOPhoric OOP'ta Birim Testi, yazılımın bir bölümünün doğru çalıştığını doğrulamak için testler yazıyorsunuz, aynı zamanda endişelerinizin ayrıldığını doğrulamak ... yani modülerlik ve tekrar kullanılabilirlik ... Bunu doğru anlarsam.
leeand00

2
@Euphoric Yalnızca mutasyonu ve mirası kötüye kullanırsanız ve kusurlu tip sistemlere sahip dillerde çalışıyorsanız (yani, var null).
Doval

@ leeand00 Sanırım "doğrulama" terimini kötüye kullandınız. Modülerlik ve tekrar kullanılabilirlik, yazılım doğrulaması ile doğrudan kontrol edilmez (tabii ki, modülerlik eksikliği, yazılımın bakımını ve yeniden kullanılmasını zorlaştırabilir, bu nedenle hataları ortaya çıkarır ve doğrulama işlemini başarısız hale getirir).
Andres F.

Modüler bir şekilde yazılmışsa, yazılım parçalarını doğrulamak çok daha kolaydır. Böylece fonksiyonun bazı fonksiyonlar için doğru çalıştığına dair gerçek kanıtlara sahip olabilirsiniz, diğerleri için birim testleri yazabilirsiniz.
grizwako

Yanıtlar:


22

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:

  1. 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.

  2. 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 .

  1. Bunun heighttemel duruma istisnalar atmadığını kanıtlayın ( Empty). 0 döndürmek bir istisna atamaz, bu yüzden işimiz bitti.
  2. 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.

8
  1. Kod hakkında mantık yürütmek çok daha kolaydır şey değişmez . Sonuç olarak, döngüler daha çok özyineleme olarak yazılır. Genel olarak, özyinelemeli bir çözümün doğruluğunu doğrulamak daha kolaydır. Genellikle böyle bir çözüm, sorunun matematiksel bir tanımına çok benzer şekilde okunacaktır.

    Bununla birlikte, çoğu durumda gerçek bir resmi doğruluk kanıtı yürütmek için çok az motivasyon vardır. İspatlar zordur, çok (insani) zaman alır ve düşük bir YG'ye sahiptir.

  2. Bazı fonksiyonel diller (özellikle ML ailesinden), C tarzı bir tip sistemin çok daha eksiksiz garantiler sağlayabilen son derece etkileyici tip sistemlerine sahiptir (ancak jenerikler gibi bazı fikirler ana dilde de yaygın hale gelmiştir). Bir program tip kontrolünden geçtiğinde, bu bir tür otomatik kanıttır. Bazı durumlarda, bu bazı hataları algılayabilir (örneğin özyinelemede temel durumu unutmak veya bir kalıp eşleşmesinde belirli durumları ele almayı unutmak).

    Öte yandan, bu tür sistemler karar verilebilmeleri için çok sınırlı tutulmalıdır . Dolayısıyla bir anlamda esneklikten vazgeçerek statik garantiler kazanıyoruz - ve bu kısıtlamalar “ Haskell'de çözülmüş bir soruna monadik bir çözüm ” çizgisi boyunca karmaşık akademik makalelerin var olmasının bir nedenidir .

    Hem çok liberal dillerden hem de çok kısıtlı dillerden zevk alıyorum ve her ikisinin de kendi zorlukları var. Ama birisinin “daha ​​iyi” olacağı durum böyle değil, her biri farklı bir görev için daha uygun.

Daha sonra, kanıtların ve birim testinin birbirinin yerine kullanılamayacağı belirtilmelidir. Her ikisi de programın doğruluğuna sınır koymamıza izin veriyor:

  • Test, doğruluk üzerinde bir üst sınır oluşturur: Bir test başarısız olursa, program yanlıştır, hiçbir test başarısız olursa, programın test edilen vakaları ele alacağından eminiz, ancak yine de keşfedilmemiş hatalar olabilir.

    int factorial(int n) {
      if (n <= 1) return 1;
      if (n == 2) return 2;
      if (n == 3) return 6;
      return -1;
    }
    
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(3) == 6);
    // oops, we forgot to test that it handles n > 3…
    
  • İspatlar doğruluk üzerinde daha düşük bir sınır oluşturur: Bazı özellikleri kanıtlamak imkansız olabilir. Örneğin, bir işlevin her zaman bir sayı döndürdüğünü kanıtlamak kolay olabilir (tip sistemlerinin yaptığı budur). Ancak sayının her zaman olacağını kanıtlamak imkansız olabilir < 10.

    int factorial(int n) {
      return n;  // FIXME this is just a placeholder to make it compile
    }
    
    // type system says this will be OK…
    

1
"Belirli özellikleri ispatlamak imkansız olabilir ... Ama sayının her zaman <10 olacağını kanıtlamak imkansız olabilir." Programın doğruluğu sayının 10'dan küçük olmasına bağlıysa, bunu kanıtlayabilmeniz gerekir. Tür sisteminin (en azından bir ton geçerli programı dışlamadan) yapamayacağı doğrudur - ancak yapabilirsiniz.
Doval

@Evet Evet. Ancak, tür sistemi yalnızca kanıt için bir sistem örneğidir. Tür sistemleri çok görünür bir şekilde sınırlıdır ve bazı ifadelerin gerçekliğini değerlendiremez. Bir kişi çok daha karmaşık kanıtlar yapabilir, ancak kanıtlayabileceği şeylerle sınırlı olacaktır . Hala geçilemeyen bir sınır var, sadece daha uzakta.
amon

1
Katılıyorum, sadece örnek biraz yanıltıcı olduğunu düşünüyorum.
Doval

2
Idris gibi bağımlı olarak yazılan dillerde, 10'dan düşük döndüğünü kanıtlamak bile mümkün olabilir
Ingo

2
@Doval'ın ortaya çıkardığı endişeyi ele almanın daha iyi bir yolu, bazı problemlerin kararsız olduğunu (örneğin durma problemi), kanıtlamak için çok fazla zaman gerektirdiğini veya sonucu kanıtlamak için yeni matematiğin bulunması gerektiğini belirtmek olabilir. Benim kişisel görüşüm, eğer bir şey doğru olduğu kanıtlanırsa, bunu birim test etmeye gerek olmadığını netleştirmeniz gerektiğidir . Kanıt zaten bir üst ve alt sınır koyar. İspatların ve testlerin birbirinin yerine geçememesinin nedeni, ispatın yapılması çok zor olabileceği ya da doğrudan yapılması imkansız olabilmesidir. Ayrıca testler otomatikleştirilebilir (kod değiştiğinde).
Thomas Eding

7

Burada bir uyarı sözü olabilir:

Diğerlerinin burada yazdıkları genel olarak doğru olsa da - kısacası, gelişmiş tip sistemlerin, değişmezliğin ve referans şeffaflığının doğruluğa çok katkısı vardır - fonksiyonel dünyada test yapılmadığı durum böyle değildir. Aksine !

Çünkü Quickcheck gibi otomatik ve rastgele test senaryoları üreten araçlarımız var. Yalnızca bir işlevin uyması gereken yasaları belirtirsiniz ve ardından hızlı kontrol, bu yasaları yüzlerce rastgele test durumu için kontrol eder.

Görüyorsunuz, bu bir avuç test vakasında önemsiz eşitlik kontrollerinden biraz daha yüksek.

AVL ağacının uygulanmasına ilişkin bir örnek:

--- A generator for arbitrary Trees with integer keys and string values
aTree = arbitrary :: Gen (Tree Int String)


--- After insertion, a lookup with the same key yields the inserted value        
p_insert = forAll aTree (\t -> 
             forAll arbitrary (\k ->
               forAll arbitrary (\v ->
                lookup (insert t k v) k == Just v)))

--- After deletion of a key, lookup results in Nothing
p_delete = forAll aTree (\t ->
            not (null t) ==> forAll (elements (keys t)) (\k ->
                lookup (delete t k) k == Nothing))

Aşağıdaki gibi okuyabileceğimiz ikinci yasa (veya mülk): Tüm keyfi ağaçlar tiçin aşağıdakiler geçerlidir: tboş değilse , ko ağacın tüm anahtarları için k, silmenin sonucu olan ağaca bakmayı tutacaktır. kadlı tsonuç olacaktır Nothing(: bulunamadı gösterir).

Bu, mevcut bir anahtarın silinmesi için uygun işlevselliği kontrol eder. Mevcut olmayan bir anahtarın silinmesini hangi yasalar yönetmelidir? Ortaya çıkan ağacın kesinlikle sildiğimiz ağaçla aynı olmasını istiyoruz. Bunu kolayca ifade edebiliriz:

p_delete_nonexistant = forAll aTree (\t ->
                          forAll arbitrary (\k -> 
                              k `notElem` keys t ==> delete t k == t))

Bu şekilde test yapmak gerçekten eğlenceli. Ayrıca, hızlı kontrol özelliklerini okumayı öğrendikten sonra, makine test edilebilir özellikleri olarak hizmet ederler .


4

Bağlantılı cevabın "matematiksel yasalar yoluyla modülerliğe ulaşmak" ile ne anlama geldiğini tam olarak anlamıyorum, ama bence ne anlama geldiğine dair bir fikrim var.

Functor'a göz atın :

Functor sınıfı şu şekilde tanımlanır:

 class Functor f where
   fmap :: (a -> b) -> f a -> f b

Test senaryoları ile değil, yerine getirilmesi gereken birkaç yasa ile birlikte gelir.

Tüm Functor örnekleri aşağıdakilere uymalıdır:

 fmap id = id
 fmap (p . q) = (fmap p) . (fmap q)

Şimdi diyelim ki uygula Functor( kaynak ):

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

Sorun, uygulamanızın yasalara uygun olduğunu doğrulamaktır. Bunu nasıl yapıyorsun?

Bir yaklaşım test senaryoları yazmaktır. Bu yaklaşımın temel sınırlaması, davranışı sınırlı sayıda durumda doğruladığınızdır (8 parametreli bir işlevi kapsamlı bir şekilde test etmek iyi şanslar!) Ve bu nedenle, testlerin geçmesi, testlerin geçmesinden başka bir şey garanti edemez.

Diğer bir yaklaşım, fiili tanıma dayalı matematiksel akıl yürütmeyi, yani bir kanıtı kullanmaktır (sınırlı sayıda vakadaki davranış yerine). Buradaki fikir, matematiksel bir kanıtın daha etkili olabileceğidir; ancak bu, programınızın matematiksel kanıtlara ne kadar uygun olduğuna bağlıdır.

Yukarıdaki Functorörneğin yasaları yerine getirdiğine dair gerçek bir resmi kanıt aracılığıyla size rehberlik edemem , ancak kanıtın nasıl görünebileceğinin bir taslağını sunacağım:

  1. fmap id = id
    • Eğer sahipsek Nothing
      • fmap id Nothing= Nothinguygulamanın 1. kısmına göre
      • id Nothing= Nothingtanımına göreid
    • Eğer sahipsek Just x
      • fmap id (Just x)= Just (id x)= Just xuygulamanın 2. kısmına göre, daha sonraid
  2. fmap (p . q) = (fmap p) . (fmap q)
    • Eğer sahipsek Nothing
      • fmap (p . q) Nothing= Nothingbölüm 1'e göre
      • (fmap p) . (fmap q) $ Nothing= (fmap p) $ Nothing= Nothingbölüm 1'in iki uygulaması ile
    • Eğer sahipsek Just x
      • fmap (p . q) (Just x)= Just ((p . q) x)= Just (p (q x))bölüm 2'ye göre, daha sonra.
      • (fmap p) . (fmap q) $ (Just x)= (fmap p) $ (Just (q x))= Just (p (q x))ikinci bölümün iki uygulaması ile

-1

"Yukarıdaki koddaki hatalara dikkat edin; sadece doğru olduğunu kanıtladım, denemedim." - Donald Knuth

Mükemmel bir dünyada, programcılar mükemmeldir ve hata yapmazlar, bu yüzden böcek yoktur.

Mükemmel bir dünyada, bilgisayar bilimcileri ve matematikçiler de mükemmeldir ve hata yapmazlar.

Ama biz kusursuz bir dünyada yaşamıyoruz. Bu yüzden hata yapmamak için programcılara güvenemeyiz. Ancak , bir programın doğru olduğuna dair matematiksel bir kanıt sunan herhangi bir bilgisayar bilimcisinin bu kanıtta herhangi bir hata yapmadığını varsayamayız. Bu yüzden kodunun çalıştığını kanıtlamaya çalışan kimseye dikkat etmem . Birim testleri yazın ve kodun spesifikasyonlara göre davrandığını gösterin. Başka hiçbir şey beni hiçbir şeye ikna etmeyecek.


5
Birim testlerinde de hatalar olabilir. Daha da önemlisi, testler sadece hataların varlığını gösterebilir - asla yokluğunu gösteremez. @Ingo'nun cevabında söylediği gibi, büyük akıl sağlığı kontrolleri yapıyorlar ve kanıtları güzel bir şekilde tamamlıyorlar, ancak bunların yerini almıyorlar.
Doval
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.