सामग्री पर जाएँ
Topos Uranos

सारांश

इससे पहले के दो पाठों ने सिखाया कि फ़ंक्शन क्या है और उसका डेटा कैसे भीतर आता और बाहर जाता है; यह पाठ सिखाता है कि कौन-से फ़ंक्शन लिखे जाएँ, यह कैसे तय किया जाए। विधि क्रमिक परिशोधन है: एक छोटा main लिखा जाता है जो ऐसे फ़ंक्शनों को कॉल करता है जो अभी मौजूद नहीं हैं, हर एक अपने अनुबंध के साथ, और फिर हर एक में तब तक उतरा जाता है जब तक ऐसे हिस्से न मिल जाएँ जो कुछ पंक्तियों में लिखे जा सकें। पाठ एक अच्छे इंटरफ़ेस की कसौटियाँ तय करता है, “एक फ़ंक्शन, एक काम” का नियम और वे संकेत जो उसके उल्लंघन को उजागर करते हैं, और ओवरलोडिंग, जो अलग-अलग टाइपों पर एक ही काम को एक ही नाम देती है। इन कसौटियों से वह इकाई 4 के उन एल्गोरिदमों को फिर से लिखता है जो main के भीतर रहते थे, और उनका अलग-अलग परीक्षण करता है। सबको एक सूत्र में बाँधने वाली अवधारणा ऊपर से नीचे का विघटन है: अच्छी तरह डिज़ाइन किया गया प्रोग्राम अपने ही विनिर्देश की तरह पढ़ा जाता है।

अधिगम उद्देश्य

  1. क्रमिक परिशोधन से किसी समस्या को उपसमस्याओं में विघटित करना और विघटन वृक्ष लिखना।
  2. बॉडी लिखने से पहले हर फ़ंक्शन का इंटरफ़ेस (नाम, पैरामीटर, रिटर्न मान और अनुबंध) डिज़ाइन करना।
  3. इकाई 4 के किसी एकाश्म प्रोग्राम को संसक्त फ़ंक्शनों के समूह के रूप में फिर से लिखना, और हर एक का परीक्षण हेतु main से अलग-अलग परीक्षण करना।
  4. किसी डिज़ाइन को “एक फ़ंक्शन, एक काम” के नियम और छिपे प्रभावों की अनुपस्थिति की कसौटी पर परखना।
  5. किसी फ़ंक्शन को ओवरलोड करना, यह पहले से बताना कि हर कॉल कौन-सा संस्करण चुनती है, और समझाना कि कोई कॉल अस्पष्ट क्यों होती है।

क्रमिक परिशोधन

समस्या के कथन से कुछ पंक्तियों वाले main तक

इकाई 4 के प्रोग्राम पूरे के पूरे main के भीतर लिखे गए थे। जब तक वे एक स्क्रीन में समा जाते हैं, यह सहा जा सकता है; परंतु इस तरह लिखा गया दो सौ पंक्तियों का प्रोग्राम माँग करता है कि सब कुछ एक साथ दिमाग़ में रखा जाए, और वह केवल पूरे का परीक्षण स्वीकार करता है। क्रमिक परिशोधन, या ऊपर से नीचे का डिज़ाइन, काम का क्रम उलट देता है: पहले समस्या की भाषा में लिखा जाता है कि प्रोग्राम क्या करता है, ऐसे फ़ंक्शनों की कॉलों के अनुक्रम के रूप में जो अभी मौजूद नहीं हैं; फिर उनमें से हर फ़ंक्शन को एक नई, छोटी समस्या माना जाता है, और प्रक्रिया तब तक दोहराई जाती है जब तक इतने सरल फ़ंक्शन न मिल जाएँ कि उनकी बॉडी कुछ भी विघटित किए बिना लिखी जा सके।

उदाहरण के लिए यह समस्या लें। पूर्णांक n≥1n \geq 1 पूर्ण कहलाता है यदि उसके उचित भाजकों (वे धनात्मक भाजक जो nn से भिन्न हैं) का योग nn के बराबर हो, न्यून यदि योग छोटा हो और प्रचुर यदि बड़ा हो: 28=1+2+4+7+1428 = 1 + 2 + 4 + 7 + 14, अतः 28 पूर्ण है; 8, जिसके लिए 1+2+4=71 + 2 + 4 = 7, न्यून है; 12, जिसके लिए 1+2+3+4+6=161 + 2 + 3 + 4 + 6 = 16, प्रचुर है। ऐसा प्रोग्राम चाहिए जो nn पढ़े और उसके उचित भाजकों का योग और उसकी श्रेणी लिखे। डिज़ाइन का पहला स्तर main है, और वह सबसे पहले लिखा जाता है:

C++ प्रोग्राम
#include <cstdint>#include <iostream>#include <print>#include <string>​enum class Kind { deficient, perfect, abundant };​// n के धनात्मक भाजकों का योग।// पूर्व-शर्त: n >= 1।// पश्च-शर्त: n के सभी धनात्मक भाजकों का योग लौटाता है, n समेत।std::int64_t divisorSum(int n);​// उचित भाजकों के योग के अनुसार n की श्रेणी।// पूर्व-शर्त: n >= 1।// पश्च-शर्त: योग n से छोटा, बराबर या बड़ा होने के अनुसार deficient, perfect या abundant लौटाता है।Kind classify(int n);​// किसी श्रेणी का नाम, उसे लिखने के लिए।// पूर्व-शर्त: कोई नहीं।// पश्च-शर्त: “न्यून”, “पूर्ण” या “प्रचुर” लौटाता है।std::string kindName(Kind kind);​int main(){    int n{};    std::cin >> n;    std::println("{} के उचित भाजक: योग {}", n, divisorSum(n) - n);    std::println("{} एक {} संख्या है", n, kindName(classify(n)));}
लिंकर के संदेश
/usr/bin/x86_64-linux-gnu-ld.bfd: main.o: in function `main':main.cpp:27:(.text+0x22a): undefined reference to `divisorSum(int)'/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:28:(.text+0x3fd): undefined reference to `classify(int)'/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:28:(.text+0x40d): undefined reference to `kindName[abi:cxx11](Kind)'collect2: error: ld returned 1 exit status

प्रोग्राम बिना किसी चेतावनी के कंपाइल होता है, क्योंकि हर कॉल अपनी घोषणा से मेल खाती है, और कंपाइलर केवल यही जाँचता है, जैसा पाठ फ़ंक्शन ने सिखाया। विफल होती है लिंकिंग: लिंकर परिभाषाएँ खोजता है और उन्हें नहीं पाता (“undefined reference”, यानी बिना परिभाषा का संदर्भ; जुड़ा हुआ [abi:cxx11] वह आंतरिक चिह्न है जो GCC उन फ़ंक्शनों पर लगाता है जो std::string लौटाते हैं)। यह स्थिति ठीक वही है जो इस बिंदु पर डिज़ाइन की स्थिति है: इंटरफ़ेस तय हो चुके हैं, बॉडी नहीं। यदि ऐसा है, तो main अभी से प्रोग्राम के विनिर्देश की तरह पढ़ा जाता है, और तीन घोषणाएँ बाक़ी काम हैं, हर एक आरंभिक समस्या से छोटा।

विघटन वृक्ष

दूसरा स्तर हर घोषणा में उतरता है। kindName तीन स्थितियों वाला निर्णय है। classify divisorSum पर टिका है: कोई संख्या पूर्ण होती है जब उसके सभी भाजकों का योग 2n2n हो। और divisorSum भाजकों को (d,n/d)(d, n/d) के जोड़ों में खोजने वाला लूप है, जो पाठ संख्यात्मक एल्गोरिदम से आता है और n\sqrt{n} पर रुकता है। परिणाम को वृक्ष के रूप में बनाया जाता है: जड़ पूरी समस्या है; हर नोड की संतानें वे उपसमस्याएँ हैं जो उसे हल करती हैं; पत्तियाँ वे संक्रियाएँ हैं जो भाषा पहले से देती है।

उदाहरणपूर्णांकों के वर्गीकारक का वृक्ष

चित्र वृक्ष को स्तर-दर-स्तर उसी क्रम में खोलता है जिसमें उसे डिज़ाइन किया गया।

प्रमाण

  1. पहला स्तर पूरी समस्या है, जो main के ज़िम्मे है और अभी विघटित नहीं हुई।

  2. दूसरा स्तर पाँच संक्रियाओं वाला main है: पढ़ना, जोड़ना, वर्गीकृत करना, नाम देना और लिखना। हर नोड एक कॉल है; लाइब्रेरी की कॉलें पहले से मौजूद हैं, बाक़ी घोषित हैं और लंबित हैं।

  3. तीसरा स्तर classify को विघटित करता है, जो divisorSum को कॉल करने और तुलना करने के सिवा कुछ नहीं करता। वही फ़ंक्शन वृक्ष में दो बार आता है, और एक ही बार लिखा जाता है: पुनःउपयोग कोड लिखने से पहले वृक्ष में दिख जाता है।

  4. अंतिम स्तर पत्तियाँ हैं: विभाज्यता की जाँच और जोड़ों की सीमा, जो भाषा की संक्रियाएँ हैं। उन तक पहुँचते ही परिशोधन समाप्त हो जाता है, क्योंकि विघटित करने को कुछ नहीं बचता।

वृक्ष पूरा होने पर परिभाषाएँ नीचे से ऊपर की ओर लिखी जाती हैं, ताकि हर फ़ंक्शन पहले से लिखे फ़ंक्शनों पर टिके:

C++ प्रोग्राम
#include <cstdint>#include <iostream>#include <print>#include <string>​enum class Kind { deficient, perfect, abundant };​// n के धनात्मक भाजकों का योग।// पूर्व-शर्त: n >= 1।// पश्च-शर्त: n के सभी धनात्मक भाजकों का योग लौटाता है, n समेत।std::int64_t divisorSum(int n){    std::int64_t sum{0};    for (int d{1}; d <= n / d; ++d) {        if (n % d == 0) {            sum += d;            if (d != n / d) {                sum += n / d;            }        }    }    return sum;}​// उचित भाजकों के योग के अनुसार n की श्रेणी।// पूर्व-शर्त: n >= 1।// पश्च-शर्त: योग n से छोटा, बराबर या बड़ा होने के अनुसार deficient, perfect या abundant लौटाता है।Kind classify(int n){    const std::int64_t proper{divisorSum(n) - n};    if (proper < n) {        return Kind::deficient;    }    if (proper == n) {        return Kind::perfect;    }    return Kind::abundant;}​// किसी श्रेणी का नाम, उसे लिखने के लिए।// पूर्व-शर्त: कोई नहीं।// पश्च-शर्त: “न्यून”, “पूर्ण” या “प्रचुर” लौटाता है।std::string kindName(Kind kind){    if (kind == Kind::deficient) {        return "न्यून";    }    if (kind == Kind::perfect) {        return "पूर्ण";    }    return "प्रचुर";}​int main(){    int n{};    std::cin >> n;    std::println("{} के उचित भाजक: योग {}", n, divisorSum(n) - n);    std::println("{} एक {} संख्या है", n, kindName(classify(n)));}
प्रोग्राम का इनपुट
28
आउटपुट
28 के उचित भाजक: योग 2828 एक पूर्ण संख्या है

परिणाम की तीन विशेषताएँ विधि के कारण हैं। पहली, नीचे का हिस्सा लिखते समय main नहीं बदला। दूसरी, योग std::int64_t के रूप में लौटाया जाता है (<cstdint> से, वह 64 बिट का पूर्णांक जिसका उपयोग पाठ पुनरावर्तन भी करता है), क्योंकि nn के लिए जो int के अधिकतम मान के निकट हो, उसके भाजकों का योग उस मान से अधिक हो सकता है, और इंटरफ़ेस में यह तय कर देना कॉल करने वाले सभी की रक्षा करता है। तीसरी, हर फ़ंक्शन अलग से जाँचा जाता है: divisorSum(28) को 56 देना चाहिए, divisorSum(1) को 1 और divisorSum(36) को 91, जहाँ जोड़ा (6,6)(6, 6) एक ही बार गिना जाता है; पाठ का अंतिम अनुभाग इन जाँचों को एक प्रोग्राम में बदल देता है।