3 पॉइंट द्वारा GN⁺ 2025-05-18 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • अगर फ़ंक्शन के अंदर मौजूद 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 की तरफ ऊपर ले जाना

  • अगर किसी फ़ंक्शन के अंदर if condition है, तो पहले यह देखना उपयोगी हो सकता है कि क्या उस 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 की संभावना खुलती है
  • यही pattern micro और macro, दोनों स्तरों पर काम करता है
    • TigerBeetle की संरचना data plane में object batches को एक साथ process करके, control plane के decision cost को amortize करने का तरीका है
  • for नियम की मुख्य प्रेरणा performance है, लेकिन यह expressiveness में भी मदद कर सकता है
    • jQuery elements के collections पर काम करता था और अतीत में सफल रहा
    • abstract vector spaces की भाषा, coordinate-wise equations के समूहों की तुलना में, कई बार सोचने के लिए बेहतर tool हो सकती है

1 टिप्पणियां

 
GN⁺ 2025-05-18
Hacker News टिप्पणियाँ
  • मेरा अजीब-सा सोचने का मॉडल यह है कि संभावित states और program flow का एक tree होता है, और conditionals उस tree की pruning करते हैं
    जितना जल्दी हो सके pruning करना बेहतर है, ताकि कम branches पर ही काम करना पड़े
    आखिरकार मैं चाहता हूँ कि function दो में से किसी एक चीज़ पर focus करे: program tree को traverse करना या असली काम करना

    • यह नजरिया programming language theory या lambda calculus की small-step semantics में दिखने वाली चीज़ों से अच्छी तरह मेल खाता है
      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 अनुशंसित
    • मेरा सोचने का मॉडल उस ठोस दुनिया से मेल बिठाना है जिसमें मौजूदा लिखा जा रहा code रखा है
      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 नहीं किया गया
    • एक adjacent model फेंककर देखें तो, classes nouns हैं और functions verbs हैं
    • यह इतना अजीब model नहीं है, और अगर इसे आखिर तक ले जाएँ तो असल में यह Prolog के execution model के काफी करीब पहुँचता है
  • ज्यादा सामान्य 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 करना बेहतर है

    • यह लगभग validate मत करो, parse करो जैसी ही बात है: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
    • तो core logic को समझते समय कौन-सी assumptions की जा सकती हैं, यह और धुंधला नहीं हो जाता? क्या आप हर call chain को एक-एक करके follow कर verify करना पसंद करते हैं?
  • “अगर function के अंदर if condition है, तो सोचें कि क्या उसे caller की तरफ ले जाया जा सकता है” जैसी ढीली-ढाली guess के counterexamples बहुत ज्यादा हैं
    अगर function 37 जगहों से call होता है, तो क्या हर जगह if statement 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 dispatch
    method dispatch को loop के बाहर निकालने की technique भी design के flow के खिलाफ जा सकती है
    उदाहरण के लिए, canvas object को raster image से भरने के लिए image pixels पर loop चलाते हुए canvas.putpixel(x, y, color) call करना आप नहीं चाहेंगे। image को canvas या उसके rectangular area पर blit करने वाला method होना चाहिए

    • अगर function 37 जगहों से call होता है, तो इस case में मतलब शायद यह होगा कि function को true/false branches implement करने वाले दो functions में बाँटा जा सकता है, और उन्हें क्रमशः 21 और 16 जगहों से call कराया जा सकता है
    • यहाँ key word consider है
      यह post tagged union जैसी चीज़ों का उपयोग करते समय खास तौर पर सामने आने वाली काफी specific design problem को target करती है
    • अगर function 37 जगहों से call होता है तो code refactor करना चाहिए, लेकिन फिर भी जवाब है “context पर निर्भर करता है”
      DRY सही जवाब जैसा लगता है, लेकिन असली code examples देखे बिना निर्णय नहीं हो सकता
      अगर यह library function है, तो उसकी position special है। वह ownership boundary पर है, data domains को cross करता है, और DDD की भाषा में कहें तो bounded context को पार करता है। इसलिए उसे अपना क्षेत्र खुद protect करना चाहिए
      EnterCriticalSection में entry पर strong validation, if condition तक शामिल validation, valid code path का संकेत देता है और इसे domain boundary माना जाना चाहिए
      इसके उलट, application लिखते समय अगर किसी सामान्य application function में if statement है, तो उसे सुरक्षित रूप से बाहर धकेला जा सकता है
      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...

    • समाधान judgment और execution को अलग करना है, और यह concept Bertrand Meyer से मिला है
      if (weShouldDoThis()) { doThis(); }
      यह functional core और imperative shell का complement या उसका हिस्सा है
      checks को अलग रखने से test करना आसान होता है, और अगर complexity की चिंता हो तो check के अंदर हर clause को function में निकाल सकते हैं
    • code scanner reports को gospel की तरह न मानें; उन पर शक करना चाहिए
      खासकर Sonar वास्तविक bugs नहीं बल्कि “code smells” report करता है
      ऐसी “bug नहीं” वाली items को ठीक करते समय नए errors आने का risk 0 से बढ़कर 0 से ज्यादा हो जाता है, और actual production issues संभालने वाले developers का समय बर्बाद हो सकता है
    • मेरे अनुभव में यह अक्सर local optimum होता है
      “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 पर उसे भयानक तरीके से खोलना पड़ सकता था
    • पूरी तरह readable बड़े function पर शिकायतें देखने के बाद से मुझे code complexity scanner हमेशा नापसंद रहे हैं
      logic एक जगह होना कहीं ज्यादा readable होता है, और उसे तभी तोड़ना चाहिए जब details की वजह से big picture छूटने लगे
    • कल LLM thread में किसी ने पूछा था, “coding में लोग कौन-सा और अविश्वसनीय tool अपनाते हैं”, अब जवाब मिल गया
  • कभी-कभी 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 के अंदर कैसे रखा जाए?

    • शायद बिना checks वाला function लिखें, फिर एक wrapper function रखें जो केवल checks करे और internal function को call करे
    • असल में सवाल में ही जवाब है
      अगर 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 हो सकता है, या PUT style 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 किया जा सकता है

    • लगता है आपने नाम उलट दिए हैं, या दिए गए reasons conclusion को support नहीं करते
      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

    • तुरंत उसी author की The Wrong Abstraction याद आ गई
      branch को for loop के अंदर डालना एक ऐसी abstraction है जो कहती है, “for loop नियम है, और 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 करना चाहिए

    • printInvoice function को 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 जैसा लगता है
    • “सब कुछ नीचे धकेलकर readability बढ़ाने” की बात करते हुए arrow antipattern दिखाना अच्छा नहीं है
      इसके बजाय ऐसा करना चाहिए
      if(!printerReady){ return; }
      if(!printerHasInk){ return; }
      if(!printerHasPaper){ return; }
      if(!invoiceFormatIsPortrait){ return; }
      यह बढ़ते हुए arrow से कहीं ज्यादा readable है
      हालांकि loop को अपने function में डालना और बाकी assumptions भी पहले ही handle कर लेना अच्छा है
    • यह PC का printer driver भी हो सकता है और printer की internal circuitry भी, इसलिए लोगों की राय अलग हो सकती है
      paper न होने पर printer को खुद अपने wheels बेकार में घुमाने नहीं देना चाहिए। वह check मैं function के अंदर रखूँगा
    • Elixir side में होता तो उस function का नाम शायद maybe_print_invoice जैसा रखता, और यह मुझे कहीं ज्यादा पसंद है