`if` को ऊपर, `for` को नीचे ले जाना
(matklad.github.io)- अगर फ़ंक्शन के अंदर मौजूद condition branching को caller की तरफ ऊपर ले जाया जाए, और बार-बार होने वाले काम को अलग-अलग calls की बजाय batch operations के रूप में नीचे ले जाया जाए, तो code flow और performance दोनों को साथ में सरल बनाया जा सकता है
ifको ऊपर ले जाने से preconditions को type या assert के ज़रिए सुनिश्चित करना आसान हो जाता है, और फ़ंक्शन के अंदर की duplicate checks तथा अनावश्यक branching कम हो जाती है- अगर branching logic कई functions में बिखरा हो, तो dead conditions या दोहराए गए decisions को ढूँढना कठिन हो जाता है; enum बनाकर फिर match करने वाली संरचना भी उसी branching को data structure के रूप में दोहराने जैसी बन सकती है
forको नीचे ले जाने से कई objects को एक साथ process किया जा सकता है, startup cost amortize होती है, और processing order को rearrange करने या vectorization जैसी optimization की गुंजाइश बनती है- इन दोनों नियमों को साथ में लागू करने पर loop के अंदर condition की दोबारा evaluation और hot loops की branching कम होती है, जिससे control plane के decision cost को data plane की batch processing की ओर सरकाने जैसी संरचना बनती है
if को caller की तरफ ऊपर ले जाना
- अगर किसी फ़ंक्शन के अंदर
ifcondition है, तो पहले यह देखना उपयोगी हो सकता है कि क्या उस condition को caller तक ऊपर ले जाया जा सकता हैOption<Walrus>लेकरNoneहोने पर return करने वाले फ़ंक्शन की तुलना में, सीधेWalrusलेने वाला फ़ंक्शन अधिक सरल रूप होता है- फ़ंक्शन के precondition की जाँच कर “कुछ भी न करने” के बजाय, caller पहले जाँच कर type या assert के ज़रिए उसे सुनिश्चित कर सकता है
- precondition checks को ऊपर ले जाने का यह तरीका पूरे call path में फैल सकता है, और नतीजतन checks की संख्या कम करने की प्रेरणा बनता है
- control flow और
ifअक्सर code complexity और bugs की संभावना बढ़ाते हैं- जटिल branching logic अगर किसी एक upper-level फ़ंक्शन में इकट्ठा हो, और असली काम सीधे-सादे lower-level functions को सौंप दिया जाए, तो यह संरचना पढ़ने में आसान होती है
- जटिल control flow अगर पूरी file में बिखरा होने की बजाय एक screen में दिखने वाले एक ही फ़ंक्शन में इकट्ठा हो, तो duplicate conditions और dead conditions को पहचानना आसान होता है
- “dissolving enum” refactoring तब उपयोगी होता है जब enum creation और match एक ही branching को दोहराते हों
- अगर
f()condition के आधार परE::FooयाE::Barबनाता है, औरg()फिर match करकेfoo()याbar()को call करता है, तो branching दो बार दिखाई देती है - condition को ऊपर खींचने पर यह
main()में उसी condition के आधार पर सीधेfoo(x)याbar(y)call करने जैसा रूप ले लेता है
- अगर
for को batch operations की तरफ नीचे ले जाना
- data-oriented सोच में program आमतौर पर कई objects के समूहों को संभालते हैं, और hot path भी अक्सर बहुत-सी entities को process करने के कारण hot बनता है
- objects के batch का विचार लाना, और scalar version को batch version का special case मानना, एक उपयोगी तरीका है
for walrus in walruses { frobnicate(walrus) }की तुलना मेंfrobnicate_batch(walruses)बेहतर रूप है
- batch processing का मुख्य लाभ performance है
- कई targets को एक साथ संभालने से startup cost amortize की जा सकती है
- processing order को अधिक लचीले ढंग से बदला जा सकता है, और entities को किसी खास क्रम में process करने की ज़रूरत भी कम हो जाती है
- पहले सभी entities के एक field को process करके फिर दूसरे field पर जाने जैसी vectorization या struct-of-array तकनीकें इस्तेमाल की जा सकती हैं
- performance के एक चरम उदाहरण के रूप में Vectorized Interpreters Talk का लिंक दिया गया है
- एक रोचक उदाहरण FFT-आधारित polynomial multiplication है
- कई points पर polynomial को एक साथ evaluate करने का तरीका, हर point पर अलग-अलग कई बार evaluation करने से तेज़ हो सकता है
ifऔरforके नियम साथ में भी लागू किए जा सकते हैं- अगर condition loop के बाहर है, तो
conditionको बार-बार दोबारा evaluate नहीं किया जाता - hot loop से branching हट जाती है
- vectorization की संभावना खुलती है
- अगर condition loop के बाहर है, तो
- यही pattern micro और macro, दोनों स्तरों पर काम करता है
- TigerBeetle की संरचना data plane में object batches को एक साथ process करके, control plane के decision cost को amortize करने का तरीका है
forनियम की मुख्य प्रेरणा performance है, लेकिन यह expressiveness में भी मदद कर सकता हैjQueryelements के collections पर काम करता था और अतीत में सफल रहा- abstract vector spaces की भाषा, coordinate-wise equations के समूहों की तुलना में, कई बार सोचने के लिए बेहतर tool हो सकती है
1 टिप्पणियां
Hacker News टिप्पणियाँ
मेरा अजीब-सा सोचने का मॉडल यह है कि संभावित states और program flow का एक tree होता है, और conditionals उस tree की pruning करते हैं
जितना जल्दी हो सके pruning करना बेहतर है, ताकि कम branches पर ही काम करना पड़े
आखिरकार मैं चाहता हूँ कि function दो में से किसी एक चीज़ पर focus करे: program tree को traverse करना या असली काम करना
expressions reduction rules के हिसाब से बार-बार “rewrite” होते हुए evaluate होते हैं। उदाहरण के लिए
(1 + 2) + 4,3 + 4में और फिर7में बदलता हैइसमें congruence rules होते हैं, जो तय करते हैं कि अगला कौन-सा subexpression evaluate होगा, और computation rules होते हैं, जो सचमुच expression को बदलकर program state बदलते हैं
strict, यानी non-lazy languages में आम तौर पर सभी subexpressions को parent expression से पहले evaluate कराया जाता है, लेकिन conditionals और infinite loops जैसे special constructs अपवाद हैं
conditionals में, congruence rule सभी subexpressions को evaluate करने का निर्देश दे उससे पहले computation rule लागू हो जाता है, और सचमुच expression tree की pruning करता है
[1]: Benjamin C. Pierce की Types and Programming Languages अनुशंसित
domain की विशेषताएँ, मौजूदा codebase के patterns, data pipeline के किस stage में हैं, performance characteristics आदि देखना चाहिए
पहले मैं ऐसे code-organization rules और heuristics बनाने की कोशिश करता था, लेकिन पर्याप्त code लिखने पर यह स्वीकार करना पड़ता है कि abstraction level गलत है और लंबे समय तक इससे चिपके रहने लायक नहीं
यह भी संकेतक है कि ऐसी चर्चाएँ fake function names या एक-अक्षर variables पर निर्भर करती हैं। बाहरी context के बिना “code island” में लगभग कोई भी rule plausible लग सकता है
सिर्फ यह सुविधाजनक assumption करने पर कि
g,hका इकलौता caller है और आगे भी रहेगा, आप कह सकते हैं कि इस rule से dead branch सामने आईअसली codebase में आम तौर पर कोई वजह होती है कि
gऔरhको शुरू में ही merge नहीं किया गयाज्यादा सामान्य rule यह है कि
ifको input के source के करीब रखें: https://gieseanw.wordpress.com/2024/06/24/dont-push-ifs-up-p...मुख्य बात यह है कि वह point खोजें—program में बाहर से आने वाले entry points, दूसरे services से लाए गए data तक शामिल करते हुए—और core logic, खासकर resource-heavy हिस्सों तक पहुँचने से पहले जितनी ज्यादा guarantees बन सकें, वैसा refine करें
संभव हो तो उन guarantees को types में encode करना बेहतर है
“अगर function के अंदर
ifcondition है, तो सोचें कि क्या उसे caller की तरफ ले जाया जा सकता है” जैसी ढीली-ढाली guess के counterexamples बहुत ज्यादा हैंअगर function 37 जगहों से call होता है, तो क्या हर जगह
ifstatement repeat करना चाहिए?अगर वह function
getaddrinfoयाEnterCriticalSectionहै, तो क्या API user कोifबाहर धकेलना चाहिए?मेरे हिसाब से इस transformation पर तभी सोचा जा सकता है जब यह internal function हो, अधिकतम दो जगहों से call होता हो, और वह निर्णय function की concern से बाहर हो
दूसरा तरीका यह है कि function केवल
ifकरे और दो helper functions को call करेअगर caller को loop के अंदर condition को बाहर खींचना हो, तो low-level “condition interpretation helper” इस्तेमाल किया जा सकता है। जिन callers को loop के अंदर-बाहर नहीं, बल्कि सिर्फ एक बार
ifचाहिए, वेifको छिपाने वाला convenience function इस्तेमाल कर सकते हैंहालांकि यह optimization के लिए है, और optimization अक्सर अच्छे program structure से टकराता है
object-oriented में callee के अंदर
ifवाला निर्णय इस रूप में दिखाई देता है कि कौन-सा method call करना है—यानी method dispatchmethod dispatch को loop के बाहर निकालने की technique भी design के flow के खिलाफ जा सकती है
उदाहरण के लिए, canvas object को raster image से भरने के लिए image pixels पर loop चलाते हुए
canvas.putpixel(x, y, color)call करना आप नहीं चाहेंगे। image को canvas या उसके rectangular area पर blit करने वाला method होना चाहिएconsiderहैयह post tagged union जैसी चीज़ों का उपयोग करते समय खास तौर पर सामने आने वाली काफी specific design problem को target करती है
DRY सही जवाब जैसा लगता है, लेकिन असली code examples देखे बिना निर्णय नहीं हो सकता
अगर यह library function है, तो उसकी position special है। वह ownership boundary पर है, data domains को cross करता है, और DDD की भाषा में कहें तो bounded context को पार करता है। इसलिए उसे अपना क्षेत्र खुद protect करना चाहिए
EnterCriticalSectionमें entry पर strong validation,ifcondition तक शामिल validation, valid code path का संकेत देता है और इसे domain boundary माना जाना चाहिएइसके उलट, application लिखते समय अगर किसी सामान्य application function में
ifstatement है, तो उसे सुरक्षित रूप से बाहर धकेला जा सकता हैlibrary या critical code section के अंदर भी
ifको गहरे अंदर की बजाय edge तक ऊपर लाना safe हैअपने domain को manage करो, दूसरों के domain से demand मत करो, और उस domain के अंदर control flow को edge की तरफ ले जाओ—यह सलाह reasonable लगती है
बेशक idiom तो idiom ही है, और real world में इसे ऐसे व्यक्ति को evaluate करना चाहिए जो context समझकर reasonable judgment कर सके
“enum decomposition refactoring” का उदाहरण मूल रूप से polymorphism ही है
matchको enum पर polymorphic method call से बदला जा सकता हैमकसद उस बिंदु को अलग करना है जहां case distinction बनता है, यानी पहला
ifऔर उसी के अनुसारfooयाbarचलाने वाला बिंदुcase distinction को object, यहां enum value या closure, साथ लेकर चलता है, और call site पर उसे फिर दोहराने की जरूरत नहीं होती
यानी अगर case distinction बदलता है, तो सिर्फ उस जगह को बदलना होता है जहां वह distinction बनता है; हर distinction के हिसाब से behavior trigger करने वाली जगहों को बदलने की जरूरत नहीं रहती
हालांकि trade-off है। जहां behavior execute होता है वहां जिन individual cases पर विचार करना है उन्हें सीधे देखना मददगार हो सकता है, लेकिन इसके बदले individual cases की list पर code-level dependency जुड़ जाती है
code complexity scanner अंततः
ifको नीचे धकेलने के लिए मजबूर करता है। लेख इसके उलट सलाह देता हैifको ऊपर लाने पर control flow अक्सर एक function में centralize हो जाता है, और उस function में complex branching logic होता है, लेकिन असली काम straight-line subroutines को delegate होता है⁰ https://docs.sonarsource.com/sonarqube-server/latest/user-gu...
if (weShouldDoThis()) { doThis(); }यह functional core और imperative shell का complement या उसका हिस्सा है
checks को अलग रखने से test करना आसान होता है, और अगर complexity की चिंता हो तो check के अंदर हर clause को function में निकाल सकते हैं
खासकर Sonar वास्तविक bugs नहीं बल्कि “code smells” report करता है
ऐसी “bug नहीं” वाली items को ठीक करते समय नए errors आने का risk 0 से बढ़कर 0 से ज्यादा हो जाता है, और actual production issues संभालने वाले developers का समय बर्बाद हो सकता है
“local” का मतलब है कि यह सिर्फ तब तक सही है जब तक requirements बदलती नहीं हैं या कोई edge case नहीं मिलता जिसके कारण loop के बाहर भी branch की जरूरत पड़ जाए
अगर loop के अंदर और बाहर दोनों जगह branching आ जाए, तो reasoning ज्यादा मुश्किल हो जाती है
अगर आपको काफी भरोसा है कि condition सिर्फ loop के अंदर ही असर करेगी, तो उसे वहीं रख सकते हैं
लेकिन अगर यह सोचना मुश्किल नहीं है कि future requirement में loop के बाहर भी branch की जरूरत पड़ सकती है, तो पहले से वैसी structure design करना बेहतर हो सकता है
code ज्यादा verbose हो सकता है, लेकिन follow करना आसान होगा, और बाद में spaghetti बनने की संभावना भी कम होगी
यही वजह है कि मैंने Haskell छोड़ दिया। Haskell आपको सबसे concise और “local optimum” logic लिखने के लिए प्रेरित करता है, लेकिन वह logic के intent से ज्यादा logic को ही express करने पर झुक जाता है। मामूली requirement change पर उसे भयानक तरीके से खोलना पड़ सकता था
logic एक जगह होना कहीं ज्यादा readable होता है, और उसे तभी तोड़ना चाहिए जब details की वजह से big picture छूटने लगे
कभी-कभी conditional logic को callee के अंदर रखना पसंद करता हूं। यह caller को गलती से गलत order में काम करने से रोकता है
उदाहरण के लिए, अगर आप idempotent operation बनाना चाहते हैं, तो पहले check कर सकते हैं कि काम पहले ही पूरा हो चुका है या नहीं, और नहीं हुआ तो उसे execute कर सकते हैं
अगर उस condition को caller के बाहर धकेल दिया जाए, तो function के सभी callers को idempotency guarantee पाने के लिए खुद verify करना होगा कि वे सही तरीके से call कर रहे हैं, और उस guarantee को abstract नहीं किया जा सकेगा
database transaction के अंदर कोई काम करने से पहले checks की series चलानी पड़ सकती है। इस philosophy को लागू करते हुए checks को transaction boundary के अंदर कैसे रखा जाए?
अगर condition को caller के बाहर धकेलते हैं, तो वह function अब idempotent नहीं रहता, इसलिए जाहिर है वह guarantee नहीं दे सकता
हालांकि अगर individual function को state management implement करके idempotency देनी पड़े, तो structure काफी अजीब होने की संभावना है, और लगता है कि एक ही function में बहुत ज्यादा logic हो रहा है
idempotent code आम तौर पर दो categories में आता है
एक, data model और perform किया जा रहा operation खुद inherently idempotent हो। यह stateless operation हो सकता है, या
PUTstyle operation जिसमें input data में record करने लायक पूरा state मौजूद होदूसरा, ज्यादा complex business operation, जहां rollback किया जाता है या partial failure से state खराब न हो यह guarantee करने वाली atomic application abstraction देकर idempotent abstraction बनाई जाती है
पहले case में operation order check करने की जरूरत नहीं होती। यह inherently idempotent है, इसलिए बस फिर से perform कर दें
दूसरे case में simple abstraction लागू नहीं की जा सकती। desired work को record करना, उसके complete या fail होने की guarantee देना, और फिर उस completion या failure को permanently persist कराना होता है
इस तरह का logic ऐसा नहीं है जिसे एक function में डालकर दूसरे operations के साथ compose किया जाए
ये सलाहें बहुत opinionated हैं, इसलिए इन्हें rule of thumb की तरह treat नहीं करना चाहिए
मुझे लगता है कि यहां कोई rule of thumb है ही नहीं। अगर जबरन बनाना हो तो शायद उल्टा कहूंगा
ifको DRY की वजह से नीचे धकेलना चाहिएअगर performance allow करे, तो
forको ऊपर लाने पर विचार करना चाहिए। इससेfilter/map/reduceऔर function composition का इस्तेमाल करके चुना जा सकता है कि किन objects पर कौन-सा behavior apply करना है, और असल में code को vectorize किया जा सकता हैifको नीचे धकेलना आम तौर पर vectorization को रोकता हैलेख में बताए गए cases वे हैं जहां DRY नहीं है, खासकर जब type internally tagged हो और similar branches stack के नीचे कई functions में फैलनी पड़ें
मुझे भरोसा नहीं है कि यह पालन करने लायक कोई “अच्छा” rule है
कभी-कभी ऐसा हो सकता है, लेकिन यह इतना context-dependent है कि conclusion निकालना मुश्किल है
यह कुछ वैसा लगता है जैसे “c के बाद को छोड़कर i, e से पहले आता है” वाला rule। exceptions इतनी ज्यादा हैं कि असल में rule न होने जैसा ही है
Sandi Metz की 99 Bottles of OOP से इसका एक मिलता-जुलता version मिला था
कुल मिलाकर यह मेरी style नहीं है, लेकिन ऐसे codebase पर काम करते समय जहाँ बहुत सारे flags कई layers नीचे pass कर दिए गए हों, logic के branching points को call stack में ऊपर ले जाने वाली बात बहुत convincing लगी
https://sandimetz.com/99bottles
branch को
forloop के अंदर डालना एक ऐसी abstraction है जो कहती है, “forloop नियम है, और branch behavior है”लेकिन नई requirements का उस abstraction को तोड़ देना बहुत आम है
तब workaround करना पड़ता है, और resulting code में ऐसी abstraction रह जाती है जो कुछ cases में लागू होती है और कुछ में नहीं, या फिर उसे हर जगह लागू कराने के लिए abstraction में ढेर सारे parameters ठूँस दिए जाते हैं, जिससे उसे follow करना मुश्किल हो जाता है
अगर शुरुआत में वह abstraction बनाई ही न गई होती, तो resulting code शायद modify और understand करने में ज्यादा आसान होता
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
code readability के लिए सब कुछ नीचे धकेल देना बेहतर होता है
printInvoice(invoice, options)if(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}}से कहीं बेहतर हैloops के लिए भी इसी तरह
printInvoices(invoices)for(invoice of invoices){ printInvoice(invoice) }से बेहतर हैआखिरकार code readability बहुत महत्वपूर्ण है, लेकिन encapsulation उससे भी ज्यादा महत्वपूर्ण है, इसलिए दोनों को ठीक से mix करना चाहिए
printInvoicefunction को invoice print करना चाहिएअगर नाम से बताए गए conditions में से कोई एक false हो और इसलिए invoice print न किया जा सके, तो क्या होगा?
exception throw किया जा सकता है, या sentinel value या error type return किया जा सकता है, लेकिन उस case में क्या करना चाहिए यह तुरंत साफ नहीं होता
खासकर Java या C++ जैसी languages में, जहाँ normal control flow के लिए exceptions का इस्तेमाल पसंद नहीं किया जाता और monadic errors आम नहीं हैं, second style के करीब structure करना बेहतर हो सकता है
हालांकि अगर portrait format error को दर्शा नहीं रहा है, तो उसे invoice printer को handle करना चाहिए
encapsulation मुख्यतः long-term code readability, local refactoring और changeability, और सिर्फ local objects की चिंता करते हुए global behavior reason करने की क्षमता के लिए एक tool लगता है
इसलिए readability और encapsulation की तुलना करके किसी एक को ज्यादा महत्वपूर्ण मानना category error जैसा लगता है
इसके बजाय ऐसा करना चाहिए
if(!printerReady){ return; }if(!printerHasInk){ return; }if(!printerHasPaper){ return; }if(!invoiceFormatIsPortrait){ return; }यह बढ़ते हुए arrow से कहीं ज्यादा readable है
हालांकि loop को अपने function में डालना और बाकी assumptions भी पहले ही handle कर लेना अच्छा है
paper न होने पर printer को खुद अपने wheels बेकार में घुमाने नहीं देना चाहिए। वह check मैं function के अंदर रखूँगा
maybe_print_invoiceजैसा रखता, और यह मुझे कहीं ज्यादा पसंद है