Catatan penerjemah: catatan tertanggal 13 Mei 2014, sehingga beberapa detail, termasuk kode sumber, mungkin tidak sesuai dengan keadaan saat ini. Jawaban atas pertanyaan mengapa penerjemahan dari pos yang sudah lama dibutuhkan adalah nilai dari isinya untuk membentuk pemahaman tentang salah satu konsep dasar bahasa Rust, seperti kelancaran.
Seiring waktu, saya menjadi yakin bahwa akan lebih baik untuk meninggalkan perbedaan antara variabel lokal yang bisa berubah dan tidak berubah di Rust. Setidaknya banyak orang skeptis tentang masalah ini. Saya ingin menyatakan posisi saya di depan umum. Saya akan memberikan berbagai motif: filosofis, teknis dan praktis, serta beralih ke pertahanan utama sistem saat ini. (Catatan: Saya melihat ini sebagai Rust RFC, tetapi memutuskan bahwa nadanya lebih baik untuk posting blog dan saya tidak punya waktu untuk menulis ulang sekarang.)
Penjelasan
Saya menulis artikel ini dengan cukup meyakinkan dan percaya bahwa garis yang saya pertahankan akan benar. Namun, jika kita tidak selesai mendukung sistem saat ini, ini tidak akan menjadi bencana atau sesuatu seperti itu. Ini memiliki kelebihan, dan secara keseluruhan saya merasa cukup menyenangkan. Saya hanya berpikir kita bisa memperbaikinya.
Dalam satu kata
Saya ingin menghapus perbedaan antara variabel lokal yang tidak dapat diubah dan berubah-ubah dan ganti nama &mut
pointer ke &my
, &only
atau &uniq
(tidak ada bedanya bagi saya). Kalau saja tidak ada kata kunci mut
.
Motif filosofis
Alasan utama saya ingin melakukan ini adalah karena saya percaya ini akan membuat bahasa lebih konsisten dan mudah dimengerti. Pada dasarnya, ini akan mengarahkan kita kembali dari berbicara tentang kemampuan berubah menjadi berbicara tentang menggunakan alias (yang akan saya sebut "berbagi," lihat di bawah).
Variabilitas menjadi konsekuensi dari keunikan: "Anda selalu dapat mengubah segala sesuatu yang memiliki akses unik untuk Anda. Data bersama biasanya tidak berubah, tetapi jika perlu, Anda dapat mengubahnya menggunakan semacam jenis Cell
."
Dengan kata lain, dari waktu ke waktu, menjadi jelas bagi saya bahwa masalah dengan balap data dan keamanan memori muncul ketika Anda menggunakan keduanya alias dan kemampuan berubah-ubah. Pendekatan fungsional untuk memecahkan masalah ini adalah menghilangkan mutabilitas. Pendekatan Rust akan menghapus penggunaan alias. Ini memberi kita cerita yang bisa diceritakan dan itu akan membantu kita mengetahuinya.
Catatan tentang terminologi: Saya pikir kita harus merujuk pada penggunaan alias sebagai pemisahan ( catatan penerjemah: selanjutnya, di mana-mana alih-alih "aliasing" digunakan "berbagi" dalam arti "pemisahan" atau "kepemilikan bersama", karena tidak ada "penggunaan alias", tidak ada "nama samaran" yang memberikan pemahaman tentang apa yang dipertaruhkan ). Di masa lalu, kami menghindari ini karena referensi multi-utasnya. Namun, jika / ketika kita mengimplementasikan rencana paralelisasi data yang saya usulkan, maka konotasi ini tidak sepenuhnya tidak tepat. Bahkan, mengingat hubungan erat antara keamanan memori dan balap data, saya benar-benar ingin mempromosikan konotasi ini.
Motif pendidikan
Saya pikir aturan saat ini lebih sulit untuk dipahami daripada yang seharusnya. Tidak jelas, misalnya, bahwa &mut T
tidak menyiratkan kepemilikan bersama. Selain itu, penunjukan &mut T
menyiratkan bahwa &T
tidak menyiratkan mutabilitas, yang tidak sepenuhnya akurat, karena jenis seperti Cell
. Dan tidak mungkin untuk menyetujui apa yang memanggil mereka ("tautan yang bisa berubah / tidak dapat diubah" adalah yang paling umum, tetapi ini tidak sepenuhnya benar).
Sebaliknya, jenis seperti &my T
atau &only T
tampaknya menyederhanakan penjelasan. Ini adalah tautan unik - tentu saja, Anda tidak dapat memaksa keduanya untuk menunjuk ke tempat yang sama. Dan mutabilitas adalah hal ortogonal: ia datang dari keunikan, tetapi juga berlaku untuk sel. Dan tipe &T
adalah kebalikannya, tautan bersama . RFC PR # 58 memberikan sejumlah argumen serupa. Saya tidak akan mengulanginya di sini.
Motif praktis
Saat ini, ada kesenjangan antara pointer yang dipinjam, yang dapat dibagikan atau dapat berubah + unik, dan variabel lokal yang selalu unik, tetapi dapat dapat berubah atau tidak berubah. Hasil akhir dari ini adalah bahwa pengguna harus memposting iklan mut
pada hal-hal yang tidak dapat diedit secara langsung.
Variabel lokal tidak dapat dimodelkan menggunakan referensi
Fenomena ini terjadi karena tautan tidak ekspresif seperti variabel lokal. Secara umum, ini mencegah abstraksi. Biarkan saya memberi Anda beberapa contoh untuk menjelaskan apa yang saya maksud. Bayangkan bahwa saya memiliki struktur lingkungan yang menyimpan pointer ke penghitung kesalahan:
struct Env { errors: &mut usize }
Sekarang saya dapat membuat instance dari struktur ini (dan menggunakannya):
let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; }
OK, sekarang bayangkan saya ingin memisahkan kode yang memodifikasi env.errors
menjadi fungsi yang terpisah. Saya mungkin berpikir bahwa karena variabel env
tidak dinyatakan sebagai dapat diubah, saya dapat menggunakan tautan &
immutable:
let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1;
Tapi ini tidak benar. Masalahnya adalah bahwa &Env
adalah tipe kepemilikan bersama ( catatan penerjemah: seperti yang Anda ketahui, lebih dari satu referensi objek yang tidak berubah dapat ada pada suatu waktu ), dan karenanya env.errors
muncul di ruang yang memungkinkan kepemilikan terpisah dari objek env
. Agar kode ini berfungsi, saya harus mendeklarasikan env
sebagai dapat diubah dan menggunakan tautan &mut
( catatan penerjemah: &mut
) untuk memberi tahu kompiler bahwa env
unik dalam kepemilikan, karena hanya satu referensi objek yang dapat berubah yang dapat ada pada suatu waktu dan ras data dikecualikan, tetapi mut
karena Anda tidak dapat membuat referensi yang dapat diubah ke objek yang tidak dapat diubah ):
let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env);
Masalah ini muncul karena kita tahu bahwa variabel lokal itu unik, tetapi kita tidak bisa memasukkan pengetahuan ini ke dalam referensi pinjaman tanpa membuatnya bisa berubah.
Masalah ini terjadi di sejumlah tempat lain. Sejauh ini kami telah menulis tentang ini dengan cara yang berbeda, tetapi saya terus dihantui oleh perasaan bahwa kita berbicara tentang istirahat, yang seharusnya tidak boleh.
Ketik memeriksa penutupan
Kami harus mengatasi batasan ini dalam hal penutupan. Penutupan sebagian besar terbuka dalam struktur seperti Env
, tetapi tidak cukup. Ini karena saya tidak ingin mengharuskan variabel lokal dinyatakan mut
jika mereka digunakan via &mut
dalam penutupan. Dengan kata lain, ambil beberapa kode, misalnya:
fn foo(errors: &mut usize) { do_something(|| *errors += 1) }
Ekspresi yang menggambarkan penutupan sebenarnya akan membuat instance dari struktur Env
:
struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize }
Lihat tautan &uniq
. Ini bukan sesuatu yang bisa dimasuki pengguna akhir. Ini berarti pointer "unik tetapi tidak selalu bisa berubah". Ini diperlukan untuk melewati pemeriksaan tipe. Jika pengguna mencoba menulis struktur ini secara manual, ia harus menulis &mut &mut usize
, yang pada gilirannya akan mengharuskan parameter errors
dinyatakan sebagai mut errors: &mut usize
.
Penutupan dan prosedur yang tidak dibungkus
Saya memperkirakan bahwa pembatasan ini merupakan masalah untuk penutupan yang tidak dibungkus. Biarkan saya menguraikan desain yang saya pertimbangkan. Pada dasarnya, idenya adalah ungkapan ||
sama dengan beberapa tipe struktural baru yang mengimplementasikan salah satu ciri Fn
:
trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); }
Jenis yang tepat akan dipilih sesuai dengan jenis yang diharapkan, pada hari ini. Dalam hal ini, konsumen penutupan dapat menulis satu dari dua hal:
fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }
Kami ... mungkin ingin memperbaiki sintaks, mungkin menambahkan gula seperti FnMut(usize) -> usize
, atau simpan | usize | -> usize, dll. Itu tidak begitu penting, penting bahwa kita akan melewati penutupan dengan nilai . Harap perhatikan bahwa sesuai dengan aturan DST (Jenis Dinamis yang Terkini) saat ini, diizinkan untuk meneruskan jenis berdasarkan nilai sebagai argumen ke FnMut<usize, usize>
, oleh karena itu argumen FnMut<usize, usize>
adalah DST yang valid dan bukan masalah.
Selain : proyek ini tidak lengkap, dan saya akan menjelaskan semua detail dalam pesan terpisah.
Masalahnya adalah bahwa tautan &mut
diperlukan untuk memanggil penutupan. Karena penutupan dilewati oleh nilai, pengguna lagi harus menulis mut
tempat yang tampak tidak pada tempatnya:
fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); }
Ini adalah masalah yang sama seperti pada contoh Env
atas: apa yang sebenarnya terjadi di sini adalah bahwa FnMut
hanya menginginkan tautan unik , tetapi karena itu bukan bagian dari sistem tipe, ia meminta tautan yang dapat diubah .
Sekarang kita mungkin bisa menyiasatinya dengan cara yang berbeda. Salah satu opsi yang bisa kita lakukan adalah ke ||
sintaks tidak akan berkembang menjadi "tipe struktural tertentu", melainkan menjadi "tipe struktural atau penunjuk ke tipe struktural, seperti yang ditentukan oleh inferensi tipe". Dalam hal ini, penelepon dapat menulis:
fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); }
Saya tidak ingin mengatakan bahwa ini adalah akhir dari dunia. Tetapi ini adalah langkah maju dalam distorsi yang terus meningkat yang harus kita lalui untuk menjaga kesenjangan antara variabel lokal dan referensi.
Bagian API lainnya
Saya tidak melakukan studi mendalam, tetapi, tentu saja, perbedaan ini merambah di tempat lain. Misalnya, untuk membaca dari Socket
, saya memerlukan pointer yang unik, jadi saya harus menyatakannya bisa berubah. Karenanya, terkadang ini tidak berhasil:
let socket = Socket::new(); socket.read()
Secara alami, menurut saran saya, kode seperti itu akan berfungsi dengan baik. Anda masih akan menerima pesan kesalahan jika Anda mencoba membaca dari &Socket
, tetapi kemudian akan membaca sesuatu seperti "tidak mungkin untuk membuat tautan unik ke tautan bersama", yang secara pribadi saya anggap lebih bisa dimengerti.
Tapi bukankah kita perlu mut
untuk keamanan?
Tidak, tidak sama sekali. Program karat akan sama baiknya jika Anda baru saja menyatakan semua binding sebagai mut
. Compiler sangat mampu melacak variabel lokal mana yang berubah pada waktu tertentu - justru karena mereka lokal ke fungsi saat ini. Apa yang benar-benar diperhatikan oleh sistem tipe adalah keunikan.
Makna yang saya lihat dalam aturan penerapan mut
, dan saya tidak akan menyangkal bahwa ia memiliki nilai, utamanya adalah mereka membantu menyatakan niat. Yaitu, ketika saya membaca kode, saya tahu variabel mana yang dapat dipindahkan. Di sisi lain, saya juga menghabiskan banyak waktu membaca kode C ++ dan, sejujurnya, saya tidak pernah memperhatikan bahwa ini adalah batu sandungan utama. (Hal yang sama berlaku untuk waktu saya habiskan membaca kode di Jawa, JavaScript, Python, atau Ruby.)
Juga benar bahwa saya terkadang menemukan bug karena saya mendeklarasikan variabel sebagai mut
dan lupa mengubahnya. Saya pikir kita bisa mendapatkan manfaat yang sama dengan lainnya, pemeriksaan yang lebih agresif (misalnya, tidak ada variabel yang digunakan dalam kondisi loop yang berubah dalam tubuh loop). Saya pribadi tidak ingat berhadapan dengan situasi yang berlawanan: yaitu, jika kompiler mengatakan bahwa sesuatu harus dapat diubah, itu pada dasarnya selalu berarti bahwa saya lupa kata kunci mut
suatu tempat. (Pikirkan: kapan terakhir kali Anda merespons kesalahan kompiler tentang perubahan yang tidak valid dengan melakukan sesuatu selain merestrukturisasi kode untuk membuat perubahan itu valid?)
Alternatif
Saya melihat tiga alternatif untuk sistem saat ini:
- Yang saya sajikan di mana Anda hanya membuang "ketidakmampuan" dan hanya melacak keunikan.
- Satu tempat Anda memiliki tiga jenis referensi:
&
, &uniq
dan &mut
. (Seperti yang saya tulis, ini sebenarnya jenis sistem yang kita miliki saat ini, setidaknya dari sudut pandang pemeriksa pinjaman.) Opsi yang lebih ketat, di mana variabel non-mut selalu dianggap terpisah. Ini berarti Anda harus menulis:
let mut errors = 0; let mut p = &mut errors;
Anda perlu mendeklarasikan p
sebagai mut
, karena jika tidak, variabel akan dianggap terpisah, meskipun itu adalah variabel lokal, dan karenanya mengubah *p
tidak diperbolehkan. Apa yang aneh dalam skema ini adalah bahwa variabel lokal TIDAK mengizinkan kepemilikan terpisah, dan kami tahu pasti, karena ketika Anda mencoba untuk membuat aliasnya, itu akan bergerak, destructor akan memulai, dll. Artinya, kita masih memiliki konsep "dimiliki", yang berbeda dari "tidak memungkinkan kepemilikan yang terpisah."
Di sisi lain, jika kita menggambarkan sistem ini, mengatakan bahwa kemampuan berubah diwarisi melalui &mut
pointer, tanpa gagap tentang kepemilikan bersama, ini mungkin masuk akal.
Dari ketiga ini, saya pasti lebih suka No. 1. Ini adalah yang paling sederhana, dan sekarang saya paling tertarik dengan bagaimana kita dapat menyederhanakan Rust dengan mempertahankan karakternya. Kalau tidak, saya memberikan preferensi kepada yang kita miliki saat ini.
Kesimpulan
Pada dasarnya, saya menemukan bahwa aturan saat ini mengenai mutabilitas memiliki beberapa nilai, tetapi harganya mahal. Mereka adalah sejenis abstraksi yang mengalir: yaitu, mereka menceritakan sebuah kisah sederhana, yang ternyata ternyata tidak lengkap. Hal ini menyebabkan kebingungan ketika orang beralih dari pemahaman awal, di mana &mut
mencerminkan bagaimana kemampuan berubah, menjadi pemahaman penuh: kadang-kadang mut
hanya diperlukan untuk memastikan keunikan, dan terkadang mutabilitas dicapai tanpa kata kunci mut
.
Selain itu, kita harus bertindak dengan hati-hati untuk mempertahankan fiksi, yang mut
menunjukkan mutabilitas, bukan keunikan. Kami telah menambahkan kasus khusus bagi peminjam untuk memeriksa penutupan. Kita harus membuat aturan tentang &mut
mutability lebih kompleks secara umum. Kita harus menambahkan mut
pada penutupan agar kita dapat memanggilnya, atau membuat sintaksis penutupan menjadi kurang jelas. Dan sebagainya.
Pada akhirnya, semuanya berubah menjadi bahasa yang lebih kompleks secara keseluruhan. Alih-alih hanya memikirkan kepemilikan bersama dan keunikan, pengguna harus berpikir tentang kepemilikan bersama dan sifat berubah-ubah, dan keduanya entah bagaimana kacau.
Saya pikir itu tidak layak.