3 पॉइंट द्वारा GN⁺ 2025-05-18 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • किसी फ़ंक्शन के अंदर मौजूद if स्टेटमेंट को कॉल साइट पर ऊपर ले जाने से कोड की जटिलता कम करने में मदद मिलती है
  • condition check और branch handling को एक जगह केंद्रित करने से duplication और अनावश्यक branch checks को आसानी से पहचाना जा सकता है
  • enum dissolving refactor का उपयोग करके उसी condition के कोड में जगह-जगह फैल जाने की समस्या को रोका जा सकता है
  • batch operations आधारित for loops performance सुधारने और दोहराए जाने वाले काम को optimize करने में प्रभावी होते हैं
  • if को ऊपर, for को नीचे ले जाने वाले pattern को मिलाकर code readability और efficiency दोनों को साथ में बढ़ाया जा सकता है

दो संबंधित नियमों पर एक संक्षिप्त नोट

  • अगर किसी फ़ंक्शन के भीतर if condition मौजूद है, तो यह सोचना बेहतर है कि क्या उसे फ़ंक्शन के call site पर ले जाया जा सकता है
  • उदाहरण की तरह, फ़ंक्शन के अंदर precondition जाँचने के बजाय, उस condition check की ज़िम्मेदारी call site को देना या type (या assert) के ज़रिए precondition को guarantee करना अधिक उपयुक्त है
  • precondition checks को ऊपर ले जाने (Push up) का तरीका पूरे कोडबेस को प्रभावित करता है और कुल मिलाकर अनावश्यक condition checks की संख्या कम करता है

control flow और conditionals का केंद्रीकरण

  • control flow और if statements कोड की जटिलता और bugs के मुख्य कारण होते हैं
  • conditionals को call site जैसे ऊपरी स्तर पर केंद्रित करके branch handling को एक फ़ंक्शन में समेटना, और वास्तविक काम को सीधी रेखा वाले (straight-line) subroutines को सौंपना उपयोगी pattern है
  • जब branches और control flow एक ही जगह इकट्ठा होते हैं, तो duplicate branches और अनावश्यक conditions को पहचानना आसान हो जाता है

उदाहरण:

  • जब f फ़ंक्शन के भीतर nested if मौजूद हो, तो dead code (Dead Branch) को पहचानना आसान होता है
  • लेकिन अगर branches कई फ़ंक्शनों (g, h) में बिखरी हों, तो इसे समझना कठिन हो जाता है

enum dissolving refactor

  • अगर कोड में वही conditional branch enum आदि के रूप में समाई हुई हो, तो condition को ऊपर खींचकर branch और काम को अधिक स्पष्ट रूप से अलग किया जा सकता है
  • इस तरीके को लागू करने से वही condition कोड में बार-बार दोहराए जाने की समस्या से बचाती है

उदाहरण:

  • f, g फ़ंक्शनों और enum E में एक ही branching condition व्यक्त की गई स्थिति को
  • एक ऊपरी conditional branch के ज़रिए पूरे कोड को सरल बनाया जा सकता है

data-oriented thinking और batch operations

  • अधिकांश प्रोग्राम कई objects (entities) पर चलते हैं। critical path (Hot Path) में performance अक्सर बहुत सारे objects को प्रोसेस करने से तय होती है
  • batch की अवधारणा लाकर objects के set पर operations को default बनाना, और single-object operations को special case की तरह संभालना बेहतर होता है

उदाहरण:

  • frobnicate_batch(walruses) जैसी batch processing function को default रखा जाए,

  • और individual objects को for loop के जरिए संभाले जाने वाले special case में बदला जा सकता है

  • performance optimization के दृष्टिकोण से यह तरीका महत्वपूर्ण है; बड़े पैमाने के काम में यह startup cost कम करता है और ordering flexibility बढ़ाता है

  • SIMD operations (struct-of-array आदि) का उपयोग भी संभव है, जिससे किसी विशेष field को bulk में प्रोसेस करने के बाद पूरा काम आगे बढ़ाया जा सकता है

व्यावहारिक उदाहरण और सुझाए गए patterns

  • FFT आधारित polynomial multiplication की तरह, कई बिंदुओं पर एक साथ computation संभव बनाकर performance को अधिकतम किया जा सकता है
  • conditionals को ऊपर और loops को नीचे ले जाने वाले नियम साथ-साथ लागू किए जा सकते हैं

उदाहरण:

  • loop के भीतर एक ही condition expression को बार-बार जाँचने के बजाय, condition को loop के बाहर निकालने से loop के अंदर branching कम होती है और optimization तथा vectorization आसान हो जाती है
  • यह approach TigerBeetle design जैसे बड़े सिस्टम data plane में भी उच्च efficiency सुनिश्चित करती है

निष्कर्ष

  • if statements (conditionals) को ऊपरी स्तर (call site, control layer) पर और for loops को निचले स्तर (operation layer, data processing layer) पर ले जाने वाले pattern को मिलाकर code readability, efficiency, और performance तीनों को बेहतर बनाया जा सकता है
  • abstract vector space के दृष्टिकोण से सोचना (set-level operations) बार-बार branching करने की तुलना में बेहतर problem-solving tool है
  • संक्षेप में: if ऊपर, for नीचे!

1 टिप्पणियां

 
GN⁺ 2025-05-18
Hacker News राय
  • मेरा एक अलग तरह का मानसिक मॉडल यह है कि अलग-अलग state या program flow मिलकर एक tree structure बनाते हैं। conditional statements इस tree की branches को prune करने का काम करते हैं। मैं चाहता हूँ कि जितना जल्दी हो सके pruning कर दूँ ताकि बाद में process करनी पड़ने वाली branches कम हों। मैं ऐसी स्थिति से बचना चाहता हूँ जहाँ हर branch को एक-एक करके evaluate और handle करने के बाद आखिर में पूरी branch को एक साथ काटना पड़े। थोड़ा अलग नज़रिये से देखें तो conditionals "ज़रूरी नहीं काम को पहचानने की प्रक्रिया" हैं, और loops "असल काम" हैं। आख़िरकार मैं चाहता हूँ कि मेरा function या तो program tree को traverse करने पर ध्यान दे, या फिर वास्तविक काम करने पर
    • मैं अपना एक समानांतर मॉडल रखना चाहूँगा। classes noun हैं, और functions verb
    • मेरा मानसिक मॉडल उस दुनिया के हिसाब से ढलता है जिसमें वह ठोस code मौजूद है जो मैं लिख रहा हूँ। यह domain characteristics, existing code patterns, data pipeline के stages, performance profile आदि पर निर्भर करता है। शुरू में मैंने ऐसे rules या heuristics बनाने की कोशिश की थी, लेकिन बहुत code लिखने के बाद समझ आया कि ऐसे abstract rules वास्तव में ज़्यादा मायने नहीं रखते। कई बार किसी function का नाम या एक-दो अक्षर तय कर देने से उस "द्वीप जैसे code" के भीतर ही rule सही लगता है, लेकिन असली codebase में आम तौर पर functions को यूँ ही नहीं जोड़ा गया होता। उदाहरण के लिए "duplication और dead condition" की बात आती है, लेकिन वह ऐसा rule है जो इस सुविधाजनक मान्यता पर टिका है कि वह function सिर्फ़ एक ही जगह call होता है। असल में कई बार वे दूसरी वजहों से अलग रखे जाते हैं
    • मुझे यह काफ़ी अच्छा मॉडल लगता है
  • एक ज़्यादा सामान्य नियम यह है कि conditionals को input source के जितना संभव हो उतना पास रखा जाए। program में बाहर से आने वाले entry points (दूसरी services से आने वाले data सहित) को जितनी जल्दी हो सके पहचानना, और core logic तक पहुँचने से पहले — ख़ासकर resource-heavy हिस्सों तक पहुँचने से पहले — जितनी ज़्यादा guarantees बनाई जा सकें, यही मुख्य बात है। इसे types में explicitly व्यक्त करना भी बहुत अच्छा है
    • क्या ऐसा करने से core logic समझते समय यह जानना मुश्किल नहीं हो जाता कि कौन-कौन सी assumptions लागू हैं? क्या फिर पूरे code की call chain देखनी नहीं पड़ती?
  • “अगर कोई if condition किसी function के अंदर है, तो सोचो क्या उसे caller की तरफ़ ले जाया जा सकता है” — इस सलाह के बहुत ज़्यादा counterexamples हैं। अगर कोई function 37 जगहों से call होता है, तो क्या हर call site पर वही if statement दोहरानी होगी? जैसे getaddrinfo या EnterCriticalSection जैसे functions के लिए क्या इस तरह if बाहर ले जाने को कह सकते हैं? मुझे लगता है कि ऐसा बदलाव सिर्फ़ तब सोचा जा सकता है जब function सिर्फ़ एक-दो जगह से call हो, और वह निर्णय function की concern के बाहर हो। एक तरीका यह है कि सिर्फ़ conditionals करने वाले function को helper function delegate कर दिया जाए। और जब condition को loop के बाहर ले जाना ज़रूरी हो, तब caller को सीधे low-level condition helper इस्तेमाल करने दिया जाए। लेकिन इस पूरे विचार के केंद्र में “optimization” है। optimization अक्सर बेहतर program design से टकराती है। संभव है कि बेहतर design यह हो कि caller को condition के बारे में जानने की ज़रूरत ही न पड़े। ऐसा dilemma OOP में भी बार-बार आता है। “if” से दिखने वाला निर्णय असल में method dispatch से हो सकता है। इस dispatch को loop के बाहर निकालना भी design principles से टकरा सकता है। उदाहरण के लिए canvas पर image draw करते समय हर बार putpixel को call करने की बजाय blit जैसे method का इस्तेमाल करना
    • अगर कोई function 37 जगहों से call होता है तो code refactoring की ज़रूरत तो है ही। उस सवाल का जवाब यही है: यह depend करता है। DRY सही जवाब जैसा लगता है, लेकिन फ़ैसला असली example code देखकर करना चाहिए। अगर यह library है, तो ownership boundary होने की वजह से हर तरफ़ को अपना data और responsibility संभालनी चाहिए। EnterCriticalSection जैसे function के लिए entry point पर मज़बूत validation (conditional सहित) सही है। लेकिन application code में if को caller की तरफ़ ले जाना ठीक हो सकता है। library या core code में control flow को edges की तरफ़ ले जाना उचित है। अपने domain के भीतर control flow को edges पर रखना अच्छा है। लेकिन ऐसे नियम हमेशा सिर्फ़ idiomatic होते हैं, इसलिए किसी ऐसे व्यक्ति को context के हिसाब से फ़ैसला लेना चाहिए जो स्थिति को समझता हो
  • “dissolving enum refactor” का उदाहरण मूलतः polymorphism pattern है। match statement को polymorphic method call से बदला जा सकता है। इस तरीके का उद्देश्य शुरुआती condition branching तय होने के समय और वास्तविक behavior execute होने के समय को अलग करना है। case distinction को object (यहाँ enum value) या closure अपने साथ रखता है, इसलिए हर call point पर उसे बार-बार दोहराने की ज़रूरत नहीं होती। अगर case distinction बदलता है, तो सिर्फ़ branching point बदलना पड़ता है; जहाँ असली action होता है, वहाँ बदलाव की ज़रूरत नहीं होती। इसका downside यह trade-off है कि हर case के behavior branch को सीधे देखने की सुविधा कम हो जाती है, और code level पर case list पर dependency बनती है
  • कुछ मामलों में मुझे conditionals को function के अंदर रखना पसंद है। इससे जानबूझकर caller को function call order में गलती करने से रोका जा सकता है। उदाहरण के लिए जहाँ idempotency guarantee चाहिए, वहाँ पहले यह check करना कि काम पहले ही हो चुका है या नहीं, और नहीं हुआ तो करना। अगर इस condition को call site पर ले जाएँ, तो idempotency की guarantee तभी रहेगी जब हर caller उस प्रक्रिया का सही पालन करे, यानी abstraction यह guarantee नहीं दे पाएगा। ऐसे मामलों में यह दर्शन कैसे लागू करना चाहिए, यह जानना चाहता हूँ। एक और उदाहरण है: जब database transaction के अंदर checks की एक series पूरी करने के बाद काम करना हो, तब उन checks को कहाँ रखा जाए
    • लगता है आपने अपने सवाल का जवाब ख़ुद ही दे दिया। अगर condition को call site पर ले जाते हैं तो function अब idempotent नहीं रहता, और स्वाभाविक रूप से guarantee भी नहीं दे सकता। अगर idempotency guarantee देने के लिए हर function में state management logic डालनी पड़ रही है, तो शायद आप काफ़ी अजीब code लिख रहे हैं, और बहुत ज़्यादा business logic एक ही function में ठूँस रहे हैं। idempotent code मोटे तौर पर दो तरह का होता है। पहला, जहाँ data model या operation ख़ुद idempotent हो। ऐसे में process order की ज़्यादा चिंता नहीं करनी पड़ती। दूसरा, जहाँ ज़्यादा जटिल business operations के लिए idempotent abstraction बनानी पड़ती है। वहाँ rollback या abstraction on atomic apply जैसी जटिल logic चाहिए होती है, और वह ऐसी बात नहीं जिसे एक simple function में समेट दिया जाए
    • एक तरीका यह भी है कि बिना checks वाला internal function बनाया जाए, और बाहर wrapper function checks करने के बाद internal function को call करे
  • code complexity scanners आख़िरकार if statements को नीचे धकेलने की प्रवृत्ति वाले tools हैं। लेकिन इस लेख में उल्टा if statements को ऊपर, यानी higher-level function में ले जाने की सलाह दी गई है। ऐसा करने से जटिल branching logic को एक single function में centrally handle किया जा सकता है, और असली concrete काम subroutines को सौंपा जा सकता है
    • समाधान “decision” और “execution” को अलग करना है। यह विचार मैंने Bertrand Meyer से सीखा। जैसे if (weShouldDoThis()) { doThis(); } इस तरह, और हर check को अलग function में निकाल देने से testing और complexity management आसान हो जाता है
    • code scanners की reports पर गंभीरता से शक करना चाहिए। sonarqube वगैरह असली bugs के अलावा “code smell” भी अंधाधुंध report करते हैं। इस तरह “समस्या न होने वाले code” तक को ठीक करने की कोशिश में नए bugs पैदा होने का ख़तरा बढ़ जाता है, और सचमुच ज़रूरी मुद्दों पर समय बर्बाद होता है
    • ऐसी optimization अक्सर “local optimum” ही होती है। यानी जैसे ही नई requirements या exception cases आते हैं, branching logic को loop के बाहर भी चाहिए होता है। उस हालत में अगर loop के अंदर और बाहर दोनों जगह branching मिल जाए, तो समझना मुश्किल हो जाता है। अगर पूरा भरोसा हो कि condition सिर्फ़ loop के अंदर ही चाहिए, तो उसे वहीं रखो; नहीं तो बेहतर है कि design को थोड़ा लंबा ही सही, लेकिन ज़्यादा समझने लायक बनाया जाए। Haskell इस्तेमाल करते समय मुझे यह अनुभव हुआ है। जब logic को सबसे concise और optimized local optimum रूप में ढालते हैं, तो requirements में बहुत छोटा सा बदलाव भी design की मंशा दिखाने के बजाय सिर्फ़ logic को छोड़ देता है, और छोटे बदलाव पर भी code unrolling काफ़ी बढ़ जाता है
    • code complexity scanners मुझे हमेशा से खटकते रहे हैं। वे आसानी से पढ़े जा सकने वाले बड़े function पर भी आपत्ति जताते हैं। logic को एक जगह रखने से overall context समझना आसान होता है, लेकिन functions में बाँटते समय यह ध्यान रखना पड़ता है कि असली context खो न जाए
    • कल LLM पर एक thread में “ऐसे अविश्वसनीय tools जिन्हें developers फिर भी स्वीकार कर लेते हैं” की बात हुई थी। अब मुझे जवाब पता है…
  • कुछ मामलों में उल्टा जाकर SIMD का इस्तेमाल करना चाहिए। उदाहरण के लिए AVX-512 आदि में branching वाले code को branchless code में vector mask registers के ज़रिए handle किया जा सकता है। उदाहरण के लिए for loop के अंदर if statement को manage करना, for loop के बाहर if रखने से आसान हो सकता है, और memory access efficiency भी बेहतर हो सकती है। एक ठोस उदाहरण लें: अगर किसी operation में odd होने पर +1 और even होने पर -2 करना हो, तो सामान्यतः हर loop iteration में branch लेनी पड़ेगी, लेकिन SIMD के साथ vector processing से 16 int को एक साथ process किया जा सकता है, और branch भी नहीं रहती। अगर compiler सही तरह vectorize कर दे, तो वह मूल code को branchless optimized version में बदल देगा
    • जो before code दिया गया है, वह शायद लेख के तर्क से पूरी तरह मेल नहीं खाता; बल्कि optimized SIMD version ही लेख की बात के ज़्यादा क़रीब लगता है। उदाहरण में for loop के अंदर का if data-dependent branch है, इसलिए उसे आसानी से ऊपर नहीं खींचा जा सकता। अगर algorithm की संरचना if (length % 2 == 1) { ... } else { ... } जैसी होती, जहाँ condition loop के बाहर की हो, तब उसे for loop के ऊपर ले जाना स्पष्ट रूप से सही होता। SIMD version में if पूरी तरह ग़ायब हो गया है, और इस तरह का pattern शायद लेख के लेखक को भी पसंद आता
    • मेरे दिमाग़ में भी तुरंत ऐसा code आया जो for loop के elements की values के आधार पर branch करता है। क्या किसी को पता है कि compiler के लिए ऐसे code को auto-vectorize करना कितना कठिन होता है? उसकी सीमा क्या है, यह जानना दिलचस्प होगा
  • व्यक्तिगत रूप से मुझे नहीं लगता कि यह कोई “अच्छा” नियम है। कुछ जगह लागू हो सकता है, लेकिन context पर इतना निर्भर करता है कि कोई साफ़ निष्कर्ष निकालना मुश्किल है। यह अंग्रेज़ी spelling rules जैसा लगता है — exceptions इतने ज़्यादा हैं कि इसे सच में नियम कहना कठिन है
  • (2023) उस समय की चर्चा का लिंक (662 points, 295 comments) https://news.ycombinator.com/item?id=38282950
  • Sandi Metz की 99 Bottles of OOP में मैंने इससे मिलती-जुलती बात पढ़ी थी। यह मेरी शैली नहीं है, लेकिन branching logic को call stack के सबसे ऊपर ले जाना उपयोगी हो सकता है, इससे मैं सहमत हूँ। ख़ासकर उस codebase में जहाँ flags को कई layers के पार भेजा जाता था, यह बात और भी ज़्यादा महसूस हुई। https://sandimetz.com/99bottles
    • उसी लेखक का “The Wrong Abstraction” लेख तुरंत याद आ गया। for loop के अंदर की branching ऐसा abstraction बनाती है जहाँ “for नियम है, branching behavior है।” लेकिन जैसे ही नई requirements आती हैं, यह abstraction टूटने लगती है, और code को समझना कठिन हो जाता है क्योंकि उसमें ज़बरदस्ती parameters जोड़ने पड़ते हैं या exception handling बढ़ानी पड़ती है। अगर शुरुआत से बिना abstraction के code लिखा गया होता, तो नतीजा शायद ज़्यादा स्पष्ट और maintainable होता। https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction