1 poin oleh GN⁺ 2024-07-22 | 1 komentar | Bagikan ke WhatsApp
  • Pin di Rust adalah elemen dasar yang diperkenalkan untuk menangani state yang self-referential secara aman di dalam Future yang dibuat oleh async/await
  • Future async menyimpan state di setiap titik await, sehingga sebuah field di dalam satu objek bisa menjadi tipe self-referential yang merujuk ke field lain dalam objek yang sama
  • Desain move constructor, offset pointer, dan ?Move masing-masing tidak diadopsi karena masalah biaya pelacakan runtime, kemungkinan kompilasi, dan kompatibilitas mundur dengan API yang sudah ada
  • Desain akhirnya adalah Pin, yang membungkus pointer dan memasukkan targetnya ke pinned typestate; berkat auto trait Unpin, sebagian besar tipe tetap bisa dipindahkan seperti sebelumnya
  • Kesulitan Pin bukan berasal dari konsep immutability itu sendiri, melainkan dari keterbatasan tipe library; reborrowing, Pin::set, pinned projection, dan interaksi dengan Drop sangat menurunkan kemudahan penggunaannya

Masalah yang Membuat Pin Diperlukan

  • Dalam ekosistem async Rust, Pin dan pinning adalah fondasi utama, tetapi bagi orang yang mempelajari async Rust, ini masih merupakan area yang sulit dan sering disalahpahami
  • Tujuan Pin bukan agar pengguna bisa membuat tipe self-referential sendiri hanya dengan Rust yang aman
    • Tujuannya adalah memungkinkan Future self-referential yang dibuat compiler dari fungsi async, atau tipe self-referential yang dibuat runtime seperti tokio dengan kode unsafe, untuk dimanipulasi dengan aman
  • Pada contoh async fn bar, di titik foo(&mut z).await, z dan Future Foo yang merujuk ke z harus disimpan bersama dalam state Future yang sama
    • Pada titik ini, satu field di dalam objek Future merujuk ke field lain di dalam objek yang sama
    • Tipe Future seperti ini menjadi tipe self-referential
  • Jika objek dipindahkan setelah memasuki state seperti ini, referensi internalnya akan menunjuk ke lokasi memori lama, yang bisa saja sudah menjadi memori mati atau digunakan kembali untuk nilai lain
  • Sebelum Pin, Rust memungkinkan objek dipindahkan jika ada ownership atau mutable reference, sehingga diperlukan cara untuk menyatakan larangan pemindahan setelah titik tertentu

Pendekatan yang Tidak Menjadi Solusi

  • move constructor

    • move constructor adalah cara menjalankan kode seperti destructor saat sebuah nilai dipindahkan untuk memperbaiki pointer self-referential ke lokasi baru
    • Di Rust, pointer tidak selalu hanya berada “di dalam” nilai yang dipindahkan; misalnya, pointer juga bisa berada dalam vector pointer yang menunjuk ke state miliknya sendiri
    • Untuk melacak semua pointer seperti ini, pada akhirnya dibutuhkan manajemen memori runtime yang mirip garbage collection
    • Sejak awal Rust memutuskan untuk tidak memiliki move constructor, dan banyak kode unsafe bergantung pada asumsi bahwa nilai dapat dipindahkan hanya dengan menyalin memori
    • Menambahkan move constructor belakangan akan menjadi breaking change
  • offset pointer

    • offset pointer adalah cara mengompilasi self-reference sebagai offset relatif terhadap alamat objek self-referential, bukan sebagai referensi biasa
    • Pada waktu kompilasi, tidak selalu mungkin menentukan apakah sebuah referensi adalah self-reference
    • Bergantung pada cabang eksekusi, nilai yang sama bisa menunjuk ke bagian dalam objeknya sendiri atau ke luar objek
    • Untuk menangani ini, referensi harus dikompilasi menjadi bentuk seperti enum berisi offset dan reference; pada masa pengerjaan async/await, pendekatan ini dinilai tidak realistis

Kebutuhan pinned typestate

  • Future self-referential tidak harus selalu tidak bisa dipindahkan sejak awal; selama siklus hidupnya, ia harus bisa dipindahkan bebas sampai suatu titik tertentu, lalu tidak boleh dipindahkan setelah titik itu
    • Saat Future sedang dikombinasikan dengan Future lain, ia harus tetap bisa dipindahkan
    • Setelah ditempatkan di lokasi tempat ia akan dipoll, ia tidak boleh dipindahkan lagi
  • Model Ralf Jung menambahkan state ketiga untuk Future self-referential, yaitu pinned typestate, selain typestate “owned” dan “shared” yang sudah ada
  • Ketika sebuah objek masuk ke pinned typestate, objek itu tidak boleh dipindahkan lagi
    • Lebih tepatnya, memori objek tersebut tidak boleh dibuat tidak valid tanpa terlebih dahulu menjalankan destructornya
    • Secara praktis, ini bisa dipahami sebagai persyaratan agar objek tidak dipindahkan ke lokasi baru
  • Sebagian besar tipe tidak dapat memuat self-reference, sehingga pinned typestate tidak memiliki makna khusus bagi tipe-tipe tersebut
    • Untuk tipe seperti ini, sebaiknya mereka bisa keluar dari batasan pinning dan kembali dapat dipindahkan
  • Model formal rinci untuk pinned typestate dijelaskan dalam tulisan Ralf Jung, A Formal Look at Pinning

Mengapa Desain ?Move Gagal

  • Sebelum Pin, sempat dicoba desain berbasis trait baru bernama Move
    • Sebagian besar tipe mengimplementasikan Move
    • Tipe yang dapat memuat self-reference tidak mengimplementasikan Move
    • Ketika sebuah referensi dibuat ke nilai dari tipe yang tidak mengimplementasikan Move, nilai tersebut masuk ke pinned typestate dan tidak bisa lagi dipindahkan
  • Pendekatan ini intuitif karena mengaitkan waktu pembuatan referensi dengan transisi ke pinning untuk menjamin keamanan
    • Bahkan sempat diimplementasikan di sebuah branch compiler
  • Keterbatasan mendasarnya adalah ada kasus ketika kita ingin sementara mereferensikan nilai yang nantinya akan menjadi self-referential, tetapi belum ingin melakukan pinning
    • Misalnya, kita mungkin ingin menyimpan nilai sementara di Option, lalu mengeluarkannya dengan Option::take
  • Masalah yang lebih besar adalah kompatibilitas mundur
    • Move tidak bisa dibuat sebagai auto trait
    • Sebab, sudah ada API stabil seperti mem::swap yang berasumsi bahwa nilai selalu bisa dipindahkan dari mutable reference
  • Cara menambahkannya sebagai ?Move juga tidak kompatibel mundur karena associated type
    • Tempat menambahkan bound ?Trait pada associated type adalah di definisi trait
    • Jika bound associated type pada trait yang sudah ada dilonggarkan, kode yang bergantung pada bound tersebut bisa rusak
    • Banyak operasi dasar terkait dengan associated type, termasuk associated future type milik IntoFuture, Target milik DerefMut, tipe return fungsi, item iterator, nilai return index operator, dan nilai return operator aritmetika
  • Edition juga tidak dapat menyelesaikannya dengan mudah
    • Sebab, agar crate dari edition berbeda bisa dikombinasikan, interface trait harus tetap sama

Desain Pin

  • Desain akhirnya mengekspresikan pinned typestate bukan sebagai properti tipe objek, melainkan sebagai state yang dibuat oleh pointer khusus
  • Pin adalah wrapper type yang membungkus pointer
    • Ia juga dapat membungkus built-in reference type
    • Ia juga dapat membungkus smart pointer yang didefinisikan library seperti Box
  • Pin memasukkan target yang ditunjuk pointer tersebut ke pinned typestate, dan target itu tidak boleh lagi dipindahkan
  • Untuk meminimalkan perubahan, desain ini diimplementasikan sebagai API library, bukan fitur compiler
    • Kode yang benar-benar perlu mengubah objek pinned harus mengaksesnya melalui API unsafe
    • Dalam hal ini, kode tersebut harus memberikan jaminan bahwa objek tidak dipindahkan melalui mutable reference biasa
  • Karena bagi sebagian besar tipe tidak ada perbedaan bermakna antara state pinned dan state biasa, auto trait Unpin ditambahkan
    • Jika sebuah tipe tidak mungkin self-referential, mutable reference dapat diperoleh dari pinned pointer tanpa unsafe
    • Objek yang mengimplementasikan Unpin aman untuk dipindahkan keluar dari Pin
  • Karena pinning hanya berlaku pada pinned pointer, reference biasa yang unpinned tetap bekerja dengan tipe yang bukan Unpin
  • Penjelasan tambahan tersedia di dokumentasi standar untuk tipe Pin dan modul pin
  • Keunggulan terbesar desain ini adalah dapat ditambahkan tanpa merusak kode yang sudah ada
    • API yang dapat memindahkan data yang direferensikan, seperti swap, membutuhkan mutable reference
    • Jika sebuah objek dipin dengan Pin, API semacam itu tidak lagi dapat dipanggil pada objek tersebut
    • Karena pinned typestate hanya berlaku pada pinned reference khusus, jaminan kompatibilitas mundur seluruh bahasa Rust tidak dilanggar

Masalah Kemudahan Penggunaan Pin

  • Pin memenuhi kebutuhan dengan cara yang kompatibel mundur, tetapi begitu pengguna menanganinya secara langsung, muncul tebing kompleksitas
  • Salah satu penjelasannya adalah bahwa untuk mengubah objek pinned dibutuhkan kode unsafe
    • Namun masalah ini tidak boleh dilebih-lebihkan
    • Dengan Pin::set, kita dapat melakukan assignment ke objek pinned secara aman
    • Kode yang benar-benar perlu mengubah objek pinned umumnya adalah kode hasil compiler yang menurunkan fungsi async menjadi Future, dan jarang ditulis langsung oleh pengguna
  • Penjelasan bahwa Pin sulit karena perilakunya bersifat kondisional juga bukan akar masalahnya
    • Rust memiliki fitur yang berperilaku berbeda bergantung kondisi, tetapi tetap membuat pemahaman lebih mudah
    • non-lexical lifetimes adalah contoh ketika lifetime dapat berakhir pada titik berbeda di setiap cabang kondisi
  • Masalah utamanya adalah Pin merupakan tipe library murni, sedangkan reference type biasa adalah tipe bawaan bahasa yang mendapat berbagai dukungan sintaks dan sugar
    • Fitur-fitur yang terasa alami pada referensi biasa hilang pada pinned reference
    • Mental model yang dibangun pengguna berdasarkan perilaku referensi yang diterima compiler menjadi rusak pada pinned reference

reborrowing dan Pin::as_mut

  • Mutable reference biasa &mut T tidak mengimplementasikan Copy, tetapi bisa diteruskan beberapa kali sebagai argumen yang sama
    • Ini karena compiler secara implisit melakukan reborrowing, seolah-olah memasukkan &mut *x alih-alih x
  • Karena Pin<&mut T> adalah tipe library biasa dan tidak mengimplementasikan Copy, ia tidak memiliki kemudahan ini
    • Jika Pin<&mut T> digunakan dua kali atau lebih, bisa muncul error penggunaan nilai setelah move, atau error lifetime yang lebih sulit dipahami
    • Kita harus secara eksplisit memanggil Pin::as_mut untuk melakukan reborrow
  • Pada mutable reference biasa, assignment dapat dilakukan langsung dengan dereference dan assignment operator, tetapi pada Pin, kita harus mempelajari metode set
    • Banyaknya API khusus seperti ini muncul karena Pin adalah tipe library tanpa dukungan sintaks bahasa

pinned projection dan Drop

  • pinned projection adalah masalah mendapatkan pinned reference ke field sebuah objek dari pinned reference ke objek tersebut
    • projection berarti mengakses field dari sebuah objek
  • Ini jauh lebih sulit daripada akses field pada referensi biasa, sehingga crate pihak ketiga seperti pin-project-lite digunakan
    • Crate seperti ini mengharuskan kita mempelajari API baru yang kompleks, termasuk macro
  • Interaksi terburuk terjadi antara pinned projection dan trait Drop
    • Drop::drop menerima mutable reference biasa
    • Jika sebuah tipe memiliki field self-referential, lalu field tersebut di-pin-project dan dipoll, kemudian destructor memindahkan field itu, jaminan pinning bisa dilanggar
    • Misalnya, jika di dalam destructor Future tersebut di-pin di stack lalu dipoll, jaminan pinning yang sudah ada akan dilanggar
  • Crate seperti pin-project-lite menangani masalah ini dengan membatasi kemampuan mendefinisikan destructor
    • Secara praktis ini berjalan, tetapi menambahkan kompleksitas yang harus didokumentasikan ketika menjelaskan jaminan pinning
    • Karena Drop sudah stable sebelum Pin, diperlukan workaround

Evaluasi Saat Ini dan Arah Perbaikan Berikutnya

  • Pin memungkinkan fungsi async yang memuat referensi apa pun dikompilasi menjadi objek self-referential yang aman
    • Referensi adalah bagian penting dari cara dasar pengguna Rust menulis kode, sehingga tanpa ini, kemudahan penggunaan async/await akan jauh menurun
  • Pada saat yang sama, Pin ditambahkan dengan cara yang sepenuhnya kompatibel mundur dengan Rust yang sudah ada
  • Pin telah menjadi komponen dasar ekosistem yang menopang layanan jaringan berkinerja tinggi dan berbagai use case pemrograman asinkron lainnya
  • Namun menangani pinned reference jauh lebih sulit daripada menangani ordinary reference, dan Pin memang menciptakan tebing kompleksitas
  • Konsep utama untuk arah perbaikan berikutnya adalah pinned places

1 komentar

 
GN⁺ 2024-07-22
Pendapat Hacker News
  • Saya selalu merasa Pin sulit dipahami karena tidak dijelaskan dengan jelas di dokumentasi resmi
    Khususnya, banyak penjelasan seperti “Pin menjamin objek tidak akan pernah berpindah”, padahal itu tidak benar
    Itu hanya benar ketika objek bukan Unpin, dan kebanyakan objek biasa adalah Unpin, jadi Pin biasanya tidak melakukan apa-apa
    Butuh waktu sangat lama bagi saya untuk memahami ini, dan menurut saya himpunan tipe T yang membuat Pin benar-benar bermakna itu cukup khusus dan aneh, tetapi dokumentasinya tidak cukup menekankan hal tersebut

    • Masukan yang bagus, dan akan baik jika dokumentasinya memperjelas bagian ini
      Tentu saja, tipe yang dalam praktik akan diperlakukan sebagai sesuatu yang dipin, yaitu future dan stream, jauh lebih mungkin merupakan objek khusus seperti itu
      Meski begitu, menurut saya dokumentasinya sudah jauh membaik dalam beberapa tahun terakhir
      Saat saya memeriksanya ketika menulis artikel ini, saya terkejut karena fokusnya sudah pada poin yang cukup tepat, dan seingat saya sekitar 2019 dokumentasi API std jauh lebih condong ke spesifikasi kontrak yang lebih cocok masuk ke dokumen referensi Rust
  • Menurut saya alasan pengguna menganggap Pin sulit adalah karena Pin itu sendiri tidak punya makna
    Ini berbeda dari wrapper lain dalam bahasa ini; kalau ada pengecualian, mungkin hanya AssertUnwindSafe, yang hampir tidak pernah dipakai untuk tujuan aslinya
    Ketika ada Pin<&mut InnerType>, Pin saja di dalam bahasa atau standard library tidak memberi tahu apa yang bisa dan tidak bisa dilakukan
    Hanya saja, jika InnerType dinyatakan sebagai Unpin, artinya semua hal yang bisa dilakukan dengan pointer biasa juga bisa dilakukan
    Sebaliknya, Pin bekerja dengan cara “bawalah maknanya sendiri”, yakni penyedia InnerType membuat metode dan API tambahan yang secara internal unsafe untuk memanipulasi objek yang dipin dengan aman
    Tujuan Pin sendiri adalah menyediakan pointer dengan kemampuan bawaan yang lebih sedikit, seperti mengganti lewat &mut atau mengeluarkan lalu memindahkan dari Box, agar tipe di dalamnya dapat mengizinkan kemampuan tambahan di atasnya secara aman
    Menurut saya ambiguitas makna inilah yang paling membingungkan orang, dan saya sendiri butuh waktu cukup lama untuk memahaminya
    Konsep tentang field struktural dan nonstruktural hanyalah mekanisme untuk memungkinkan pola akses umum seperti “field ini data biasa, tetapi field itu berisi objek yang ingin dipin sendiri”

    • Pin punya makna. Selama tipe target tidak mengimplementasikan Unpin, target dari pointer ini berarti tidak akan pernah bisa dipindahkan lagi
      Lebih tepatnya, artinya target tidak bisa dibuat tidak valid tanpa menjalankan destruktornya, dan itulah alasan mengapa pemindahan menjadi masalah
      Jika melepaskan hak tertentu, kita memperoleh hak lain, seperti hak untuk menyimpan nilai yang mereferensikan dirinya sendiri
      Kontrak antar-komponen umumnya bekerja seperti ini
      Demikian pula, jika melepaskan hak untuk mengubah lewat referensi, kita sekaligus bisa membuat referensi itu memiliki alias
      Setiap kali memikirkan hal ini, saya teringat dialog dalam film Lincoln, meski itu topik yang sama sekali berbeda dan jauh lebih berat: “Jika kita tunduk pada hukum, Alex, jika kita tunduk sampai kehilangan kebebasan — misalnya kebebasan untuk menindas — mungkin kita akan menemukan kebebasan lain yang sebelumnya tidak kita kenal”
      Namun saya setuju bahwa fakta bahwa hak-hak seperti itu tidak bisa digunakan langsung dalam kode aman adalah masalah dari sisi edukasi
      Sebab sulit menunjukkan dengan mudah apa yang bisa dilakukan dengan referensi yang dipin selain “memanggil metode poll yang dibuatkan compiler”
  • Saya sudah beberapa tahun mengembangkan secara profesional dengan Rust, tetapi jujur saja saya tidak begitu memahami Pin
    Saya paham teorinya, tetapi tidak punya banyak intuisi tentang kapan harus menggunakannya
    Penggunaan Pin pada dasarnya terasa seperti “saya mencoba sesuatu lalu compiler mengeluh, kemudian saya pin ini-itu sampai akhirnya bisa dikompilasi”
    Dalam coding harian, sejauh ini Pin belum pernah benar-benar menjadi hambatan yang membuat saya harus duduk dan memahaminya secara mendalam

    • Saya juga begitu. Ini salah satu kasus paling umum yang masuk kategori “hindari saja unsafe, dan bersyukurlah orang-orang compiler yang pintar sudah menyelesaikan semuanya”
      Sebaliknya di C++, saya sering berjalan di tepi rawa “hal-hal yang tidak saya pahami tetapi wajib saya pakai”, lalu dimakan buaya
  • Saat mengajarkannya, untuk memperjelas bahwa item Unpin tidak terpengaruh oleh Pin, mungkin bagus memakai analogi dunia nyata tentang benda yang tetap tidak terpengaruh meskipun memakai alat yang dibuat untuk menahannya di tempat
    Kait velcro tidak menempel pada permukaan licin: Pin → velcro, Unpin → permukaan licin
    Magnet tidak memengaruhi bahan nonmagnetik: Pin → magnet, Unpin → nonmagnetik/kaca/kuningan
    Lem tidak menempel pada permukaan antilengket: Pin → lem, Unpin → antilengket
    Dengan begitu jelas bahwa “velcro” memang menahan benda di tempatnya, tetapi jika benda itu “licin”, ia tidak terpengaruh oleh mekanisme velcro
    Mengingat suasana penamaan di ekosistem Rust, rasanya indah jika nama trait-nya diganti ke arah seperti magnet dan nonmagnetik

    • Namun benda licin tidak bisa ditempeli velcro, dan kayu tidak bisa ditahan oleh magnet
      Saya rasa Unpin berarti objek siap dipin kapan saja
      Saya membaca artikelnya tadi malam, tetapi saya sudah lupa apakah peminjaman butuh tahap penyesuaian
      Jadi menurut saya T: Pin + !Unpin seperti kertas yang hanya bisa dipasang dengan staples, sedangkan T: Pin + Unpin lebih mirip lukisan yang memiliki kait, bisa digantung pada paku lalu diturunkan lagi tanpa merusak kaitnya
  • Istilah “identitas nilai” tidak didefinisikan di mana pun dalam tulisan ini dan saya juga tidak menemukannya di dokumentasi Mojo, jadi dasar Modular mengatakan bahwa Mojo menyelesaikan masalah yang ingin diselesaikan Pin tidak jelas
    Saya juga tidak mengklaim tahu jawabannya, tetapi saya teringat presentasi bagus dari Dave Abrahams, yang mengerjakan semantik nilai Swift bersama Chris Lattner
    Judul presentasinya adalah “Value Semantics: Safety, Independence, Projection, & Future of Programming”
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • Jelas bahwa Mojo dalam suatu arti mewarisi konsep semantik nilai Swift, tetapi Rust juga memiliki semantik nilai dalam arti yang sama
      Rust memiliki referensi sebagai tipe kelas satu, sedangkan Swift dan, menurut saya, Mojo hanya mengizinkan referensi sebagai cara meneruskan parameter
      Mojo tampaknya memperluas parameter inout Swift dengan juga menyediakan cara penerusan referensi immutable
      Jika referensi tidak bisa disimpan di dalam objek, maka kode semacam yang dikompilasi Rust tidak bisa diimplementasikan, sehingga ini memang menyelesaikan masalah “struct yang mereferensikan dirinya sendiri”
      Namun paragraf yang dikutip sama sekali tidak mengatakan hal seperti itu tentang Mojo, jadi cukup membingungkan maksudnya
  • Menurut saya masalahnya adalah jika ada referensi &mut ke suatu nilai, nilai itu bisa dipindahkan dengan hal seperti mem::swap/replace
    Namun dalam praktiknya jarang sekali perlu melakukan itu
    Kalau itu tidak diizinkan, memiliki referensi &mut ke nilai yang mereferensikan dirinya sendiri tampaknya akan sepenuhnya aman
    Mungkin bisa saja ada cara untuk secara eksplisit memilih pemindahan melalui referensi hanya saat diperlukan, dan jika swap serta replace dibuat unsafe, mungkin seluruh masalah ini bisa dihindari
    Saya berharap ada yang mengeksplorasi ruang desain ini

    • Betul. Saat mengerjakan masalah ini, Aaron Turon menggambarkan &mut sebagai terlalu kuat
      Jika &mut tidak memberi wewenang untuk memindahkan nilai di dalamnya, keseluruhan desainnya akan jauh lebih sederhana
      Saya berencana membahas hal ini di tulisan berikutnya
      Rust harus menjaga kompatibilitas mundur dan sudah memutuskan bahwa nilai bisa dipindahkan dari &mut, tetapi jika tidak terikat oleh keputusan masa lalu, jelas ada desain yang jauh lebih rapi yang mungkin dibuat
    • Ini benar, tetapi tidak skalabel. Karena akan merusak terlalu banyak kode yang ada, sejak awal hal itu mungkin sulit diterapkan
      mem::swap hanyalah salah satu cara memindahkan nilai melalui referensi mutable, dan ada sangat banyak cara lain
      Option::take adalah contoh yang cukup sering saya gunakan, dan akan terasa sangat aneh jika itu unsafe
  • Senang melihat cerita latar ini. WithoutBoats sudah banyak melakukan diskusi aktif tentang topik yang sangat tepat waktu seperti iterator asynchronous, poll, dan pin
    https://news.ycombinator.com/from?site=without.boats
    Sepertinya tidak banyak komunitas yang menggali detail internal bahasa sedalam ini secara terbuka, dan menyenangkan untuk diikuti

    • Keren memang, tetapi itu juga berarti pengembangan bahasanya sangat lambat
      Async masih setengah matang dan sangat kompleks
      Ini saya katakan sebagai orang yang selama 3 tahun terakhir menulis kode Rust 40 jam seminggu
  • Kita bisa membayangkan bahasa mirip Rust yang memiliki move constructor, semua subtipe Future yang dihasilkan bersifat opaque, dan otomatis dialokasikan di heap
    Dengan begitu, pengguna tidak punya cara untuk merusaknya, dan karena opaque serta berada di tempat lain di heap, tidak ada cara untuk memindahkannya, sehingga Pin bisa jadi tidak diperlukan
    Adanya move constructor berarti secara konseptual perpindahan adalah penghancuran lalu pembuatan ulang

    • Pin lebih merupakan status daripada properti data itu sendiri
      Ini memberi efek baik: Future bisa digabungkan dan di-inline sebelum dieksekusi
      Mirip juga dengan immutability di Rust. Bukan ada memori immutable, melainkan hanya referensi immutable
    • Jika semua future dialokasikan di heap, move constructor tidak diperlukan
      Namun itu berarti setiap pemanggilan fungsi asynchronous menimbulkan alokasi terpisah, dan ini sangat buruk bagi lokalitas memori
      Sejenis stack virtual akan jauh lebih baik daripada itu, tetapi untuk mengoptimalkan stack agar pada dasarnya kecil, pada akhirnya diperlukan garbage collection
    • Bisa dibayangkan betapa destruktifnya perubahan itu, tetapi akan sangat bagus jika Rust menghadapinya langsung dan menambahkan trait Move ke std, yang tertanam di bahasa pada level mirip Copy
      Move mendefinisikan fungsi untuk memindahkan nilai dari satu alamat memori ke alamat lain, dan membuat struct tanpa impl Move tidak bisa dipindahkan
      Hampir semua tipe cukup diberi #[derive(Move)], yang mengimplementasikan fungsi pemindahan sederhana berupa menyalin byte
      Namun dengan ini, jalan akan terbuka untuk tipe self-referential, future, dan banyak hal lain yang membutuhkan perilaku move yang lebih kompleks
      Dalam praktiknya, mungkin lebih masuk akal membaginya menjadi dua trait yang mencerminkan perbedaan antara Copy dan Clone
      Yang satu adalah marker trait yang memberi tahu compiler bahwa byte boleh dipindahkan begitu saja, sedangkan yang lain mengizinkan implementasi “move constructor” kustom oleh pengguna
      Pin terlalu sulit dipahami, jadi akan bagus jika ada Move
      Konsep yang rumit dibungkus dalam negasi ganda, kadang negasi tiga lapis. Saat melihat sesuatu seperti fn(...), rasanya “apa ini?”, dan ketika sampai ke pin projection yang unsafe, saya mulai tersingkir
      Saya tidak tahu kapan aman dan kapan tidak aman, jadi akhirnya lepas tangan saja
      Berpindah dari Rust tanpa Move ke Rust dengan Move akan merepotkan
      Hampir semua struct yang pernah ditulis sejauh ini harus ditambahi #[derive(Move)], dan std juga sama
      Untuk semua tipe yang belum di-pin di edition lama, compiler harus menginfer implementasi trait Move
      Secara mekanis mungkin bisa dilakukan, hanya saja pekerjaannya banyak
      Async Rust itu mengerikan. Apalagi jika dibandingkan dengan future/promise di hampir semua bahasa lain
      Suatu hari nanti seseorang akan memperbaiki model memory safety Rust dan membuat bahasa sistem baru yang mirip Rust, dengan trait Move dan future yang lebih baik
      Secara pribadi, saya juga ingin ada eksekusi waktu kompilasi sebagai pengganti sistem macro Rust
      Saya menyukai Rust, dan saya juga menyukai semua pekerjaan yang telah dilakukan timnya selama bertahun-tahun
      Namun bahasa yang benar-benar saya nantikan adalah bahasa setelah Rust
      Idenya sama, tetapi bahasa itu belajar dari kesalahan Rust, dan semakin jelas seperti apa bahasa bergaya Rust yang lebih baik itu nantinya
      Saya benar-benar menantikannya
  • Satu lagi tulisan bagus dari WithoutBoats
    Sejujurnya, ini salah satu bagian yang membuat saya merasa lega karena di Rust ia diabstraksikan dan terkubur di dalam runtime asynchronous
    Meski begitu, selain implementasi Future kustom, saya penasaran di mana Pin benar-benar dipakai

    • Saya juga penasaran
      Dari cara diekspresikan, sepertinya bisa dipakai di dalam FFI
      Misalnya jika fungsi extern mengembalikan *mut T lalu menerima pointer, membungkusnya dengan Pin<&mut T> tampak bisa memberi semantik yang lebih baik
      Namun artikel itu mengatakan, “fakta lain tentang status tipe yang di-pin adalah bahwa ia sama sekali tidak relevan bagi kebanyakan tipe. Jika nilai suatu tipe tidak akan pernah bisa berisi referensi ke dirinya sendiri, mem-pin nilai itu tidak ada gunanya”
      Saya masih sangat pemula dalam FFI, jadi ingin memahami cara terbaik membungkusnya dengan Rust yang aman
    • Dalam FFI, ada kasus ketika C API mengekspos item sebagai pointer, bukan referensi, sehingga item itu tidak boleh dipindahkan
      Ini juga berlaku saat berinteraksi dengan tipe sistem yang bergantung pada alamat
      Misalnya, pada mutex/futex di sebagian sistem operasi, dokumentasi kernel mengatakan bahwa objek lock di user space tidak boleh berubah alamat setelah diinisialisasi, jadi setahu saya std menggunakan sesuatu yang setara dengan Pin
      Yang unik adalah alamatnya tidak boleh berubah bahkan saat tidak sedang terkunci
      Biasanya syarat itu hanya berlaku selama sedang terkunci, dan dalam kasus itu objek yang sedang direferensikan tidak bisa dipindahkan, sehingga Pin tidak diperlukan
  • Ini terlihat seperti pekerjaan besar-besaran demi menghindari perbaikan masalah yang sebenarnya, yaitu inefisiensi thread
    Semua kode asynchronous, tanpa pengecualian, adalah hack untuk mengimplementasikan thread ringan dengan banyak syntactic sugar untuk manajemen state
    Dalam bahasa seperti Rust, ini menambahkan kompleksitas luar biasa yang semestinya tidak perlu ada
    Jika masalah efisiensi dan skalabilitas thread diperbaiki, semua ini akan hilang
    Hilang begitu saja
    Mirip dengan bagaimana null dalam bahasa seperti Java adalah “kesalahan bernilai triliunan dolar”
    Satu keputusan desain, atau dalam kasus ini ketiadaan desain, menimbulkan kompleksitas yang sangat besar

    • Thread tidak mendukung pembatalan dengan cara yang masuk akal
      Pembatalan sangat berguna dalam aplikasi jaringan dan GUI
      Thread membuat sulit untuk memanfaatkan CPU dan jaringan secara memadai sekaligus tanpa membuat salah satunya terlalu banyak didominasi
      Begitu mulai memindahkan pekerjaan antar thread pool, Anda sudah masuk ke jalur mengimplementasikan ulang future
      Atau Anda akan bekerja dengan callback/event, yang membuat kode terpecah-pecah, dan async/await memang dimaksudkan menjadi syntactic sugar untuk hal itu
      Alternatif untuk pembatalan dan timeout adalah menyelipkan objek Context ke seluruh kode seperti di Go, tetapi ini menimbulkan masalah ketika kode di ujung secara naif memanggil fungsi yang tidak benar-benar mematuhi Context
      Ini hanya sedikit lebih baik daripada masalah fungsi non-asynchronous di dalam kode asynchronous
    • Saya meragukan apakah mungkin menulis ulang kernel Linux untuk “memperbaiki masalah efisiensi dan skalabilitas thread”, dan bahkan jika mungkin, himpunan pakar Rust yang membuat Pin bekerja mungkin tidak sama dengan himpunan pakar kernel yang bisa melakukan hal seperti itu
      Jadi saya penasaran, secara konkret apa yang seharusnya dilakukan
      Apakah seharusnya mereka angkat tangan dan berkata, “suatu hari nanti mungkin ada yang memperbaiki Linux sehingga thread secara ajaib menjadi cepat, jadi kami tidak akan menambahkan async ke bahasa kami”?
    • Menandai secara terpisah fungsi yang melakukan sinkronisasi dengan proses konkuren dan yang tidak, sebenarnya adalah hal yang baik
    • Sayangnya, melintasi batas user space tetap berbiaya, seberapa ringan pun “thread” itu
      Selain itu, jika sistem operasi dijadikan scheduler untuk semua pekerjaan asynchronous, maka setiap runtime harus memakai scheduler sistem operasi, sehingga berbagai desain scheduler menjadi tidak mungkin
    • Keputusan-keputusan merugikan yang dibuat Rust menunjukkan budaya yang sangat mengakar untuk memaksakan kesalahan sebelumnya dengan cara apa pun
      Bahkan setelah “pendekatan yang disukai” terbukti tidak dapat dijalankan, mereka tampaknya tidak mengevaluasi ulang biaya/manfaatnya
      “Saya ingin fitur X, tidak peduli akibatnya” jarang menjadi langkah yang menang dalam desain bahasa