1 पॉइंट द्वारा GN⁺ 2024-07-22 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Rust का Pin async/await द्वारा बनाए गए Future के अंदर self-referential state को सुरक्षित तरीके से संभालने के लिए पेश किया गया एक बुनियादी तत्व है
  • async Future हर await point पर state सहेजता है, इसलिए किसी object के अंदर एक field उसी object के दूसरे field को reference कर सकता है और वह self-referential type बन सकता है
  • move constructor, offset pointer, ?Move डिज़ाइन क्रमशः runtime tracking cost, compile-time feasibility, और मौजूदा API के साथ backward compatibility समस्याओं के कारण अपनाए नहीं जा सके
  • अंतिम डिज़ाइन Pin है, जो pointer को wrap करके target को pinned typestate में डालता है; Unpin auto trait की वजह से ज़्यादातर types पहले की तरह move हो सकते हैं
  • Pin की कठिनाई immutability की अवधारणा से ज़्यादा library type की सीमाओं से आती है; reborrowing, Pin::set, pinned projection, और Drop के साथ interaction इसकी usability को काफी घटाते हैं

Pin की ज़रूरत वाली समस्या

  • Rust async ecosystem में Pin और pinning core foundation हैं, लेकिन async Rust सीखने वालों के लिए यह अब भी कठिन और गलतफहमियों से भरा क्षेत्र है
  • Pin का उद्देश्य users को केवल safe Rust से खुद self-referential types बनाने देना नहीं है
    • इसका मकसद compiler द्वारा async functions से बनाए गए self-referential Future या tokio जैसे runtimes द्वारा unsafe code से बनाए गए self-referential types को सुरक्षित तरीके से manipulate करने देना है
  • उदाहरण में async fn bar, foo(&mut z).await point पर z और z को reference करने वाले Foo Future को उसी Future state में साथ-साथ store करना पड़ता है
    • उस समय Future object के अंदर एक field उसी object के दूसरे field को reference करता है
    • ऐसा Future type self-referential type बन जाता है
  • object ऐसी state में जाने के बाद move हो जाए, तो internal reference पुरानी memory location की ओर point करता है, और वह location dead memory हो सकती है या किसी दूसरी value के लिए reused हो सकती है
  • Pin से पहले Rust में ownership होने पर या mutable reference होने पर object को move किया जा सकता था, इसलिए किसी खास point के बाद move रोकने को express करने का तरीका चाहिए था

जो approaches समाधान नहीं बन सके

  • move constructor

    • move constructor वह तरीका है जिसमें value move होने पर destructor की तरह code चलता है और self-referential pointers को नई location के हिसाब से ठीक करता है
    • Rust में pointers सिर्फ move होने वाली value के “अंदर” ही नहीं होते; उदाहरण के लिए, वे अपनी state की ओर point करने वाले pointers के vector में भी हो सकते हैं
    • ऐसे सभी pointers को track करने के लिए अंततः garbage collection जैसी runtime memory management चाहिए होगी
    • Rust ने शुरुआती चरण में move constructor न रखने का फैसला किया था, और बहुत-सा unsafe code इस assumption पर निर्भर करता है कि values को सिर्फ memory copy से move किया जा सकता है
    • move constructor को बाद में जोड़ना breaking change बन जाता
  • offset pointer

    • offset pointer वह तरीका है जिसमें self-reference को normal reference के बजाय self-referential object address के base पर offset के रूप में compile किया जाता है
    • compile time पर हमेशा यह तय नहीं किया जा सकता कि कौन-सा reference self-reference है
    • अलग-अलग branches में वही value कभी अपने object के अंदर point कर सकती है और कभी बाहर
    • इसे संभालने के लिए references को offset और reference के enum जैसे रूप में compile करना पड़ेगा; async/await पर काम करते समय इसे अव्यावहारिक माना गया

pinned typestate की requirements

  • self-referential Future को शुरुआत से हमेशा immovable होना ज़रूरी नहीं; वह lifecycle के किसी point तक स्वतंत्र रूप से move हो सकता है, लेकिन किसी खास point के बाद move नहीं होना चाहिए
    • Future को दूसरे Future के साथ compose करते समय उसे movable होना चाहिए
    • poll होने के दौरान जहां वह live रहेगा, वहां place हो जाने के बाद उसे आगे move नहीं होना चाहिए
  • Ralf Jung का model मौजूदा “owned” और “shared” typestate के बाद self-referential Future के लिए तीसरी state, pinned typestate, जोड़ता है
  • object pinned typestate में चला जाए तो उसे फिर कभी move नहीं करना चाहिए
    • और अधिक सटीक रूप से, पहले destructor चलाए बिना उस object की memory को invalidate नहीं करना चाहिए
    • व्यावहारिक रूप से इसे object को नई location पर न ले जाने की requirement माना जा सकता है
  • ज़्यादातर types self-reference शामिल नहीं कर सकते, इसलिए pinned typestate का उनके लिए खास मतलब नहीं होता
    • ऐसे types को pinning की constraints से बाहर आकर फिर से move हो सकना बेहतर है
  • pinned typestate का detailed formal model Ralf Jung के A Formal Look at Pinning में दिया गया है

?Move डिज़ाइन क्यों विफल हुआ

  • Pin से पहले Move नाम के नए trait पर आधारित डिज़ाइन आज़माया गया था
    • ज़्यादातर types Move implement करते
    • जो types self-reference शामिल कर सकते थे, वे Move implement नहीं करते
    • Move implement न करने वाले type की value का reference बनाते ही वह value pinned typestate में चली जाती और फिर move नहीं हो सकती
  • यह तरीका intuitive था, क्योंकि reference बनाने के point और pinning transition को जोड़कर safety guarantee की जाती थी
    • वास्तव में इसे compiler branch में implement भी किया गया था
  • मूल सीमा यह थी कि कभी-कभी ऐसी value को थोड़ी देर reference करना होता है जो बाद में self-referential बनेगी, लेकिन अभी pin नहीं करना होता
    • उदाहरण के लिए, value को थोड़ी देर Option में store करके फिर Option::take से निकालना चाह सकते हैं
  • इससे बड़ी समस्या backward compatibility थी
    • Move को auto trait नहीं बनाया जा सकता था
    • क्योंकि mem::swap जैसी stable APIs पहले से मौजूद थीं, जो mutable reference से value को हमेशा move कर सकने पर निर्भर करती हैं
  • ?Move के रूप में जोड़ने का तरीका भी associated type के कारण backward compatible नहीं था
    • trait के associated type में ?Trait bound जोड़ने की जगह trait definition होती है
    • मौजूदा trait के associated type bound को relax करने से उस bound पर निर्भर code टूट सकता है
    • IntoFuture का associated future type, DerefMut का Target, function return types, iterator item, index operator return value, arithmetic operator return value आदि कई basic operations associated type से जुड़े हैं
  • edition से भी इसे आसानी से हल नहीं किया जा सकता था
    • क्योंकि अलग-अलग editions के crates को साथ compose करने के लिए trait interface समान रहना ज़रूरी है

Pin डिज़ाइन

  • अंतिम डिज़ाइन pinned typestate को object type की property नहीं, बल्कि special pointer द्वारा बनाई गई state के रूप में express करता है
  • Pin pointer को wrap करने वाला wrapper type है
    • built-in reference type को भी wrap कर सकता है
    • Box जैसे library-defined smart pointer को भी wrap कर सकता है
  • Pin उस pointer के target को pinned typestate में डालता है, और target को फिर move नहीं करना चाहिए
  • बदलावों को कम रखने के लिए यह डिज़ाइन compiler feature नहीं, बल्कि library API के रूप में implement किया गया है
    • pinned object को वास्तव में modify करने वाला code unsafe API से access करना पड़ता है
    • इस समय यह guarantee देनी पड़ती है कि normal mutable reference के जरिए object move नहीं होगा
  • ज़्यादातर types में pinned state और normal state के बीच कोई meaningful difference नहीं होता, इसलिए Unpin auto trait जोड़ा गया
    • अगर type self-referential नहीं हो सकता, तो pinned pointer से unsafe के बिना mutable reference पाया जा सकता है
    • Unpin implement करने वाले object को Pin से बाहर move करना safe है
  • pinning केवल pinned pointer पर लागू होती है, इसलिए normal unpinned reference Unpin न होने वाले types के साथ भी काम करते रहते हैं
  • अतिरिक्त व्याख्या standard docs के Pin type और pin module में दी गई है
  • इस डिज़ाइन का सबसे बड़ा फायदा यह था कि इसे existing code तोड़े बिना जोड़ा जा सकता था
    • swap जैसी APIs, जो referenced data को move कर सकती हैं, mutable reference मांगती हैं
    • object को Pin से pin करने पर ऐसी APIs उस object पर आगे call नहीं की जा सकतीं
    • pinned typestate सिर्फ special pinned reference पर लागू होती है, इसलिए Rust language की overall backward compatibility guarantee नहीं टूटती

Pin की usability समस्याएं

  • Pin ने requirements को backward-compatible तरीके से पूरा किया, लेकिन जैसे ही user इसे सीधे handle करता है, complexity cliff पैदा हो जाती है
  • एक explanation यह है कि pinned object को modify करने के लिए unsafe code चाहिए
    • हालांकि इस समस्या को बढ़ा-चढ़ाकर नहीं देखना चाहिए
    • Pin::set से pinned object में safely assignment किया जा सकता है
    • वास्तव में pinned object को modify करने वाला code आमतौर पर compiler-generated code होता है, जो async function को Future में lower करता है; users द्वारा सीधे लिखे जाने के मामले कम हैं
  • यह explanation भी core reason नहीं है कि Pin इसलिए कठिन है क्योंकि यह conditional तरीके से behave करता है
    • Rust में ऐसे features हैं जो conditions के आधार पर अलग behavior रखते हुए भी समझना आसान बनाते हैं
    • non-lexical lifetimes इसका उदाहरण हैं, जहां conditional branches के हिसाब से lifetime अलग-अलग points पर खत्म हो सकती है
  • core problem यह है कि Pin एक pure library type है, जबकि normal reference type language built-in type है जिसे कई syntax supports और sugar मिलते हैं
    • normal references में स्वाभाविक रूप से मिलने वाली capabilities pinned reference में गायब हो जाती हैं
    • compiler द्वारा स्वीकार किए जाने वाले reference behavior के आधार पर user ने जो mental model बनाया होता है, वह pinned reference में टूट जाता है

reborrowing और Pin::as_mut

  • normal mutable reference &mut T, Copy implement नहीं करता, फिर भी उसे same argument के रूप में कई बार pass किया जा सकता है
    • क्योंकि compiler implicit रूप से reborrowing करता है, जैसे x की जगह &mut *x डाल दिया गया हो
  • Pin<&mut T> normal library type है और Copy implement नहीं करता, इसलिए यह सुविधा नहीं मिलती
    • Pin<&mut T> को दो या अधिक बार use करने पर move के बाद value use करने की error आ सकती है, या समझने में और कठिन lifetime error आ सकती है
    • reborrow करने के लिए explicit रूप से Pin::as_mut call करना पड़ता है
  • normal mutable reference में dereference और assignment operator से सीधे assign किया जा सकता है, लेकिन Pin में set method सीखना पड़ता है
    • ऐसी special APIs बढ़ने की वजह यह है कि Pin language syntax support के बिना library type है

pinned projection और Drop

  • pinned projection का मतलब है object के pinned reference से उस object के field का pinned reference पाने की समस्या
    • projection का अर्थ object से field तक access करना है
  • यह normal reference field access से कहीं कठिन है, इसलिए pin-project-lite जैसे third-party crate का उपयोग होता है
    • ऐसे crates में macro सहित complex नई APIs सीखनी पड़ती हैं
  • सबसे खराब interaction pinned projection और Drop trait के बीच होता है
    • Drop::drop normal mutable reference लेता है
    • अगर किसी type में self-referential field हो, और उस field पर pin project करके poll करने के बाद destructor में उसी field को move कर दिया जाए, तो pinning guarantee टूट सकती है
    • उदाहरण के लिए, destructor के अंदर उस future को stack पर pin करके poll करने से existing pinning guarantee violate हो जाती है
  • pin-project-lite जैसे crates destructor define करने की क्षमता को restrict करके इस समस्या को handle करते हैं
    • practical तौर पर यह काम करता है, लेकिन pinning guarantee समझाते समय documentation में complexity जोड़ता है
    • Drop, Pin से पहले stable था, इसलिए workaround की जरूरत थी

मौजूदा मूल्यांकन और अगले सुधार की दिशा

  • Pin ने arbitrary references शामिल करने वाले async functions को safe self-referential objects में compile करना संभव बनाया
    • references Rust users के code लिखने के basic तरीके का अहम हिस्सा हैं, इसलिए इसके बिना async/await की usability काफी कम हो जाती
  • साथ ही Pin को existing Rust के साथ पूरी तरह backward-compatible तरीके से जोड़ा गया
  • Pin high-performance network services और asynchronous programming के दूसरे use cases को support करने वाले ecosystem का core building block बन गया है
  • लेकिन pinned reference को handle करना ordinary reference संभालने से कहीं ज्यादा कठिन है, और Pin सच में complexity cliff बनाता है
  • अगले सुधार की दिशा का core concept pinned places है

1 टिप्पणियां

 
GN⁺ 2024-07-22
Hacker News की राय
  • मुझे हमेशा लगता रहा है कि Pin को आधिकारिक docs में साफ़ तरीके से नहीं समझाया गया है, इसलिए इसे समझना मुश्किल है
    खासकर “Pin यह गारंटी देता है कि object कभी move नहीं होगा” जैसी व्याख्याएँ बहुत मिलती हैं, लेकिन यह सच नहीं है
    यह बात सिर्फ़ तब सही है जब object Unpin न हो; ज़्यादातर सामान्य objects Unpin होते हैं, इसलिए Pin आम तौर पर कुछ नहीं करता
    यह समझने में मुझे बहुत समय लगा, और जिन types T के लिए Pin का सच में मतलब होता है, उनका set काफ़ी खास और अजीब है; मुझे लगता है docs इस बात पर पर्याप्त ज़ोर नहीं देते

    • अच्छा feedback है, और अच्छा होगा अगर docs इस हिस्से को और स्पष्ट करें
      बेशक जिन types को आप वास्तव में pinned state में handle करेंगे—futures और streams—उनके ऐसे खास objects होने की संभावना कहीं ज़्यादा है
      फिर भी मुझे लगता है कि पिछले कुछ वर्षों में docs बहुत बेहतर हुए हैं
      यह लेख लिखते समय जब मैंने देखा, तो हैरानी हुई कि वे काफ़ी सही points पर focus कर रहे थे; मुझे याद है कि 2019 के आसपास वे std API docs की बजाय Rust reference docs में जाने लायक contract specification की तरफ़ कहीं ज़्यादा झुके हुए थे
  • मुझे लगता है users को Pin मुश्किल लगने की वजह यह है कि Pin अपने-आप में कोई meaning नहीं रखता
    यह language के दूसरे wrappers से अलग है; अगर कोई exception हो तो वह AssertUnwindSafe जैसा है, जिसे उसके मूल उद्देश्य के लिए लगभग कोई इस्तेमाल नहीं करता
    जब आपके पास Pin<&mut InnerType> हो, तो language या standard library में मौजूद Pin अकेले यह नहीं बताता कि आप क्या कर सकते हैं और क्या नहीं
    बस अगर InnerType को Unpin घोषित किया गया है, तो इसका मतलब है कि आप वह सब कर सकते हैं जो एक सामान्य pointer से कर सकते हैं
    इसके बजाय Pin “meaning खुद लेकर आओ” वाले तरीके से काम करता है, और InnerType provider pinned object को safely manipulate करने के लिए internally unsafe methods और APIs अलग से बनाता है
    Pin का अपना उद्देश्य ऐसे pointer देना है जिनमें &mut replacement, Box से निकालकर move करना जैसी intrinsic capabilities कम हों, ताकि inner type उनके ऊपर extra capabilities को safely allow कर सके
    मुझे लगता है इसी meaning की अस्पष्टता लोगों को सबसे ज़्यादा confuse करती है, और मुझे भी इसे समझने में काफ़ी समय लगा
    structural fields और non-structural fields की concept बस “यह field सामान्य data है, लेकिन वह field अपने-आप pinned रहना चाहने वाले object को रखता है” जैसे common access patterns को संभव बनाने की व्यवस्था है

    • Pin का meaning है। जब तक target type Unpin implement नहीं करता, इस pointer का target फिर कभी move नहीं किया जा सकता
      और ज़्यादा सटीक कहें तो इसका मतलब है कि destructor चलाए बिना target को invalidate नहीं किया जा सकता, और move के problematic होने की वजह भी यही है
      कुछ अधिकार छोड़ने पर self-referential values store करने के अधिकार जैसे दूसरे अधिकार मिलते हैं
      components के बीच contracts आम तौर पर इसी तरह काम करते हैं
      इसी तरह reference के ज़रिए mutate करने का अधिकार छोड़ने पर आप उसी reference को alias भी बना सकते हैं
      जब भी इस बात के बारे में सोचता हूँ, तो हालांकि यह बिल्कुल अलग और कहीं भारी विषय है, मुझे फिल्म Lincoln की line याद आती है: “अगर हम कानून का पालन करें, Alex, यहाँ तक कि स्वतंत्रता खोने तक पालन करें — जैसे दमन करने की स्वतंत्रता — तो शायद हम ऐसी दूसरी स्वतंत्रताएँ खोज लें जिन्हें पहले नहीं जानते थे”
      लेकिन मैं सहमत हूँ कि safe code में ऐसे अधिकारों को सीधे इस्तेमाल न कर पाना educational रूप से problem है
      क्योंकि pinned reference से क्या किया जा सकता है, इसे “compiler ने जो poll method बनाया है उसे call करो” के अलावा आसानी से दिखाना मुश्किल है
  • मैंने कई सालों तक Rust में professionally develop किया है, लेकिन सच कहूँ तो Pin को इतना अच्छी तरह नहीं समझता
    theory पता है, लेकिन कब इस्तेमाल करना है इसकी intuition ज़्यादा नहीं है
    Pin का इस्तेमाल असल में कुछ ऐसा है: “कुछ try किया तो compiler ने शिकायत की, फिर यह-वह pin किया और compile हो गया”
    रोज़मर्रा की coding में यह अब तक ऐसा obstacle नहीं बना कि सच में बैठकर गहराई से समझना पड़े

    • मेरे साथ भी यही है। यह उन सबसे common cases में से एक है जहाँ बात “बस unsafe से बचो, और smart compiler लोगों ने पहले ही सब solve कर दिया है इसके लिए शुक्रगुज़ार रहो” जैसी होती है
      इसके उलट C++ में मैं अक्सर “वे चीज़ें जिन्हें नहीं जानता लेकिन इस्तेमाल करना ही पड़ता है” वाले किनारे पर चलता था और मगरमच्छों का शिकार हो जाता था
  • पढ़ाते समय, अगर यह साफ़ करना हो कि Unpin items पर Pin का असर नहीं पड़ता, तो उन real-world analogies का इस्तेमाल करना अच्छा होगा जहाँ चीज़ों को जगह पर पकड़ने के लिए बना tool भी असर नहीं करता
    Velcro hooks चिकनी सतह पर नहीं चिपकते: Pin → Velcro, Unpin → चिकनी सतह
    magnet non-magnetic materials पर असर नहीं करता: Pin → magnet, Unpin → non-magnetic/glass/brass
    glue non-stick surface पर नहीं चिपकता: Pin → glue, Unpin → non-stick
    इससे यह साफ़ हो जाता है कि “Velcro” चीज़ को जगह पर fix करता है, लेकिन अगर चीज़ “चिकनी” है तो Velcro mechanism का उस पर असर नहीं होता
    Rust ecosystem में naming के माहौल को देखते हुए, अगर trait names को magnet और non-magnetic जैसी दिशा में बदला गया होता तो सुंदर होता

    • लेकिन चिकनी object को Velcro से चिपकाया नहीं जा सकता, और लकड़ी magnet को पकड़कर नहीं रख सकती
      मुझे लगता है Unpin का मतलब है कि object किसी भी समय pinned होने के लिए तैयार है
      कल रात article पढ़ा था, लेकिन यह पहले ही भूल गया हूँ कि pinning के लिए कोई correction step चाहिए या नहीं
      इसलिए T: Pin + !Unpin मेरे हिसाब से ऐसे कागज़ जैसा है जिसे सिर्फ़ staple से fix किया जा सकता है, और T: Pin + Unpin हुक लगी तस्वीर जैसा है जिसे कील पर टांगा जा सकता है और फिर हुक को नुकसान पहुँचाए बिना वापस उतारा जा सकता है
  • “value identity” शब्द इस लेख में कहीं भी परिभाषित नहीं है और Mojo docs में भी मुझे नहीं मिला, इसलिए यह साफ़ नहीं है कि Modular किस आधार पर कहता है कि Mojo उस समस्या को हल करता है जिसे Pin हल करना चाहता है
    मैं भी यह दावा नहीं कर रहा कि मुझे जवाब पता है, लेकिन Dave Abrahams की एक शानदार प्रस्तुति याद आती है, जिन्होंने Chris Lattner के साथ Swift के value semantics पर काम किया था
    प्रस्तुति का शीर्षक “Value Semantics: Safety, Independence, Projection, & Future of Programming” है
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • यह साफ़ है कि Mojo ने किसी अर्थ में Swift के value semantics की अवधारणा विरासत में ली है, लेकिन Rust में भी उसी अर्थ में value semantics मौजूद है
      Rust में references भी first-class types हैं, जबकि Swift और मेरी नज़र में Mojo references को सिर्फ़ parameter passing mechanism के रूप में अनुमति देते हैं
      लगता है Mojo ने Swift के inout parameters को extend करके immutable reference passing mechanism भी जोड़ा है
      अगर objects के अंदर references store करने की अनुमति न हो, तो Rust जिस तरह का code compile करता है वैसा implement नहीं किया जा सकता, इसलिए इससे “self-referential struct” समस्या तो हल हो जाती है
      लेकिन उद्धृत paragraph Mojo के बारे में बिल्कुल यह बात नहीं कह रहा, इसलिए इसका मतलब क्या है यह काफ़ी confusing है
  • मेरी नज़र में समस्या यह है कि अगर किसी value का &mut reference आपके पास है, तो mem::swap/replace जैसी चीज़ों से उस value को move किया जा सकता है
    लेकिन असल में ऐसा करना बहुत कम ज़रूरी होता है
    अगर इसकी अनुमति न होती, तो self-referential values के &mut reference लेना पूरी तरह safe होता लगता है
    शायद ज़रूरत पड़ने पर reference के ज़रिए move को explicitly opt-in करने का कोई तरीका हो सकता था, और अगर swap और replace को unsafe बना दिया जाता तो शायद इस पूरी समस्या से बचा जा सकता था
    काश कोई इस design space को explore करे

    • सही है। जब इस समस्या पर काम हो रहा था, तब Aaron Turon ने कहा था कि &mut बहुत ज़्यादा शक्तिशाली है
      अगर &mut अपने अंदर की value को move करने का अधिकार न देता, तो पूरा design कहीं ज़्यादा सरल होता
      अगले लेख में मैं इस बारे में बात करूँगा
      Rust को backward compatibility बनाए रखनी है और यह पहले ही तय कर चुका है कि &mut से values move की जा सकती हैं, लेकिन अगर पुराने फैसलों से बंधे न हों, तो निश्चित रूप से बहुत साफ़ design संभव है
    • यह बात सही है, लेकिन scalable नहीं है। इससे इतना सारा existing code टूट जाता कि शुरुआत में ही यह मुश्किल से संभव होता
      mem::swap mutable reference के ज़रिए value move करने का बस एक तरीका है, और भी बहुत सारे तरीके हैं
      Option::take एक example है जिसे मैं काफ़ी बार use करता हूँ, और अगर यह unsafe होता तो वाकई अजीब होता
  • यह background story देखकर अच्छा लगा। WithoutBoats पहले से ही बहुत timely topics—async iterators, poll, pin—पर काफ़ी active discussions कर चुका है
    https://news.ycombinator.com/from?site=without.boats
    कम ही communities होंगी जो किसी language की बारीक internals में इतनी गहराई से publicly उतरती हैं, और इसे देखना काफ़ी मज़ेदार है

    • शानदार तो है, लेकिन इसका मतलब यह भी है कि language development बहुत धीमा है
      async अभी भी आधा-अधूरा है और बहुत complex है
      यह मैं पिछले 3 साल से हफ़्ते में 40 घंटे Rust code लिखने के अनुभव से कह रहा हूँ
  • आप Rust जैसी किसी भाषा की कल्पना कर सकते हैं जिसमें move constructor हों, बनने वाले सभी Future subtypes opaque हों और अपने-आप heap पर allocate हो जाते हों
    तब user के पास उसे destroy करने का कोई तरीका नहीं होगा, वह opaque होगा और heap में किसी दूसरी जगह होगा, इसलिए उसे move करने का भी तरीका नहीं होगा, और Pin की जरूरत खत्म हो सकती है
    move constructor होने का मतलब है कि move को conceptually destroy करने के बाद फिर से create करना माना जाता है

    • Pin डेटा की अपनी property से ज्यादा एक state है
      इसका अच्छा असर यह है कि execution से पहले Futureों को merge और inline किया जा सकता है
      यह Rust की immutability से भी मिलता-जुलता है। immutable memory जैसी कोई चीज नहीं, सिर्फ immutable reference होते हैं
    • अगर सभी futures heap पर allocate हों, तो move constructor की जरूरत नहीं है
      लेकिन तब हर async function call पर अलग allocation होगा, और यह memory locality के लिए बहुत खराब है
      किसी तरह का virtual stack इससे कहीं बेहतर होगा, लेकिन stack को by default छोटा optimize करना हो तो आखिरकार garbage collection की जरूरत पड़ेगी
    • अंदाजा है कि यह कितना breaking change होगा, फिर भी अच्छा होता अगर Rust इसे सीधे अपनाता और std में Copy जैसी level पर language में built-in Move trait जोड़ता
      Move ऐसा function define करे जो value को memory के एक address से दूसरे address पर move करे, और जिन structs में impl Move न हो उन्हें move न होने दे
      लगभग हर type पर #[derive(Move)] लगाया जाएगा, और इसके लिए bytes copy करने वाला simple move function implement करना काफी होगा
      लेकिन ऐसा करने से self-referential types, futures, और ज्यादा complex move behavior की जरूरत रखने वाली कई चीजों के लिए रास्ता खुल जाएगा
      असल में Copy और Clone के फर्क को reflect करते हुए इसे दो traits में बांटना शायद ज्यादा समझदारी होगी
      एक marker trait जो compiler को बताए कि bytes को बस उठा कर ले जाना ठीक है, और दूसरा custom “move constructor” implementation की अनुमति देने वाला
      Pin समझना बहुत मुश्किल है, इसलिए काश Move होता
      एक complex concept double negative, और कभी-कभी triple negative में लिपटा हुआ है। fn(...) जैसी चीज देखते ही “ये क्या है?” लगता है, और unsafe pin projection तक पहुंचते-पहुंचते बात छूट जाती है
      कब safe है और कब unsafe, समझ नहीं आता, और बस हाथ खड़े कर देने का मन होता है
      Move-रहित Rust से Move वाले Rust में जाना असुविधाजनक होगा
      अब तक लिखी गई लगभग हर struct में #[derive(Move)] जोड़ना पड़ेगा, और std के साथ भी यही होगा
      पुराने editions में जिन types को pinned नहीं किया गया है, उनके लिए compiler को Move trait implementation infer करना पड़ेगा
      mechanically यह संभव होगा, बस काम बहुत ज्यादा होगा
      async Rust बहुत खराब है। खासकर लगभग हर दूसरी language के future/promise से तुलना करें तो
      किसी दिन कोई Rust के memory safety model को बेहतर बनाएगा और Move trait व बेहतर futures वाली नई Rust-जैसी system language बनाएगा
      निजी तौर पर, मैं Rust के macro system की जगह compile-time execution भी चाहूंगा
      मुझे Rust पसंद है, और टीम ने सालों में जो भी काम किया है वह भी पसंद है
      लेकिन जिस language का मुझे सच में इंतजार है, वह Rust के बाद आने वाली language है
      वही ideas, लेकिन Rust की गलतियों से सीखी हुई language; और ऐसी बेहतर Rust-शैली की language कैसी दिखेगी, यह धीरे-धीरे ज्यादा साफ होता जा रहा है
      सच में इंतजार है
  • WithoutBoats का एक और बेहतरीन लेख है
    सच कहूं तो यह Rust में उन हिस्सों में से एक लगता है जिनका async runtime के अंदर abstraction में दबे रहना अच्छी बात है
    फिर भी custom Future implementation के अलावा Pin असल में कहां इस्तेमाल होता है, यह जानने की उत्सुकता है

    • मुझे भी उत्सुकता है
      wording देखकर लगता है कि शायद इसे FFI के अंदर इस्तेमाल किया जा सकता है
      उदाहरण के लिए अगर कोई extern function *mut T return करके pointer लेता है, तो लगता है कि उसे Pin<&mut T> में wrap करके बेहतर semantics दिए जा सकते हैं
      लेकिन लेख कहता है, “pinned type state के बारे में एक और तथ्य यह है कि यह ज्यादातर types के लिए पूरी तरह irrelevant है। अगर किसी type की value कभी self-reference contain नहीं कर सकती, तो उसे pin करना बेकार है”
      मैं FFI में अभी बहुत beginner हूं, इसलिए समझना चाहता हूं कि इसे safe Rust में wrap करने का सबसे अच्छा तरीका क्या है
    • FFI में ऐसे मामले होते हैं जहां C API items को reference नहीं बल्कि pointer के रूप में expose करता है, इसलिए उन्हें move नहीं करना चाहिए
      address पर निर्भर system types के साथ interact करते समय भी ऐसा होता है
      उदाहरण के लिए कुछ operating systems के mutex/futex में kernel docs कहते हैं कि user-space lock object का address initialization के बाद बदलना नहीं चाहिए, इसलिए मेरी जानकारी में std Pin के equivalent कुछ इस्तेमाल करता है
      अजीब बात यह है कि lock न होने पर भी address नहीं बदलना चाहिए
      आम तौर पर condition सिर्फ locked रहने के दौरान होती है, और उस case में referenced target को move नहीं किया जा सकता, इसलिए Pin की जरूरत नहीं होती
  • असली समस्या यानी threads की अक्षमता को ठीक न करने के लिए बहुत बड़ा काम किया जा रहा है, ऐसा लगता है
    सभी async code, बिना किसी अपवाद के, state management के लिए ढेर सारी syntactic sugar के साथ lightweight threads लागू करने का एक hack है
    Rust जैसी language में यह ऐसी भारी complexity जोड़ देता है जिसकी मूल रूप से ज़रूरत नहीं होनी चाहिए
    threads की efficiency और scalability की समस्याएँ ठीक कर दी जाएँ तो यह सब गायब हो जाएगा
    बस, छू-मंतर
    यह कुछ वैसा ही है जैसे Java जैसी languages में null “ट्रिलियन-डॉलर mistake” था
    एक design decision, या यहाँ design की गैर-मौजूदगी, के कारण बहुत बड़ी complexity पैदा हो जाती है

    • threads किसी समझदार तरीके से cancellation support नहीं करते
      cancellation network applications और GUI में बहुत उपयोगी है
      threads ऐसा बनाना मुश्किल कर देते हैं कि CPU और network दोनों का पर्याप्त उपयोग हो, लेकिन कोई भी जरूरत से ज्यादा occupy न हो
      जब आप thread pools के बीच काम pass करना शुरू करते हैं, तो आप futures को दोबारा implement करने की राह पर चल पड़े होते हैं
      वरना callback/event के साथ काम करना पड़ता है, जहाँ code टुकड़ों में बिखर जाता है, और async/await वही syntactic sugar बनने की कोशिश कर रहा था
      cancellation और timeout का alternative Go की तरह Context object को पूरे code में पिरोना है, लेकिन फिर समस्या आती है कि leaf code मासूमियत से ऐसी function call कर देता है जो Context का ठीक से पालन नहीं करती
      यह async code के अंदर non-async functions वाली समस्या से बस थोड़ा ही बेहतर है
    • “threads की efficiency और scalability समस्याएँ ठीक करने” के लिए Linux kernel को फिर से लिखना संभव है या नहीं, इस पर संदेह है; और अगर संभव भी हो, तो Pin को काम कराने वाले Rust experts और ऐसा काम कर सकने वाले kernel experts का समूह शायद एक जैसा नहीं होगा
      इसलिए जिज्ञासा है कि ठोस रूप से क्या किया जाना चाहिए था
      क्या बस हाथ खड़े कर के कहना चाहिए था, “कभी न कभी कोई Linux को ठीक करके threads को जादुई रूप से तेज बना सकता है, इसलिए हम अपनी language में async नहीं जोड़ेंगे”?
    • concurrent processes के साथ synchronize करने वाली functions और न करने वाली functions को अलग से चिह्नित करना वास्तव में अच्छी बात है
    • दुर्भाग्य से user-space boundary पार करने में “thread” कितना भी हल्का हो, cost लगती है
      साथ ही, operating system को सभी async work का scheduler बना देने से हर runtime को operating system scheduler इस्तेमाल करना पड़ेगा, इसलिए अलग-अलग तरह के scheduler designs संभव नहीं रहेंगे
    • Rust ने जो हानिकारक decisions लिए हैं, वे दिखाते हैं कि पिछले mistakes को किसी भी कीमत पर आगे बढ़ाने की culture गहराई तक जमी हुई है
      ऐसा लगता है कि “preferred approach” executable नहीं है यह सामने आने के बाद भी cost/benefit का दोबारा मूल्यांकन नहीं किया जाता
      “हमें feature X चाहिए, नतीजों से मतलब नहीं” language design में शायद ही कभी जीतने वाली चाल होती है