Rust: Masalah pelacakan untuk `Sifat impl` (RFC 1522, RFC 1951, RFC 2071)

Dibuat pada 27 Jun 2016  ·  417Komentar  ·  Sumber: rust-lang/rust

MASALAH PELACAKAN BARU = https://github.com/rust-lang/rust/issues/63066

Status implementasi

Fitur dasar seperti yang ditentukan dalam RFC 1522 diimplementasikan, namun ada revisi yang masih perlu diperbaiki:

RFC

Ada sejumlah RFC mengenai sifat impl, yang semuanya dilacak oleh masalah pelacakan pusat ini.

Pertanyaan yang belum terselesaikan

Implementasinya juga menimbulkan sejumlah pertanyaan menarik:

  • [x] Apa prioritas kata kunci impl saat mengurai jenis? Diskusi: 1
  • [ ] Haruskah kita mengizinkan impl Trait setelah -> dalam fn jenis atau gula kurung? #45994
  • [ ] Apakah kita harus menerapkan DAG di semua fungsi untuk memungkinkan kebocoran yang aman secara otomatis, atau dapatkah kita menggunakan semacam penangguhan. Diskusi: 1

    • Semantik saat ini: DAG.

  • [x] Bagaimana seharusnya kita mengintegrasikan sifat impl ke regionck? Diskusi: 1 , 2
  • [ ] Haruskah kita mengizinkan menentukan jenis jika beberapa parameter tersirat dan beberapa eksplisit? misalnya fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Beberapa kekhawatiran tentang penggunaan Sifat impl bersarang](https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] Haruskah sintaks dalam impl menjadi existential type Foo: Bar atau type Foo = impl Bar ? ( lihat di sini untuk diskusi )
  • [ ] Haruskah set "mendefinisikan kegunaan" untuk existential type dalam impl hanya merupakan item dari impl, atau menyertakan item bersarang dalam fungsi impl dll? ( lihat di sini misalnya )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Komentar yang paling membantu

Karena ini adalah kesempatan terakhir sebelum FCP ditutup, saya ingin membuat satu argumen terakhir terhadap ciri-ciri otomatis otomatis. Saya menyadari ini adalah menit terakhir, jadi paling-paling saya ingin membahas masalah ini secara resmi sebelum kita berkomitmen pada implementasi saat ini.

Untuk memperjelas bagi siapa saja yang belum mengikuti impl Trait , ini adalah masalah yang saya presentasikan. Sebuah tipe yang diwakili oleh tipe impl X saat ini secara otomatis mengimplementasikan ciri-ciri otomatis jika dan hanya jika tipe konkret di belakangnya mengimplementasikan ciri-ciri otomatis tersebut. Secara konkret, jika perubahan kode berikut dibuat, fungsi akan terus dikompilasi, tetapi penggunaan fungsi apa pun yang bergantung pada fakta bahwa tipe yang dikembalikan mengimplementasikan Send akan gagal.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(contoh sederhana: bekerja , perubahan internal menyebabkan kegagalan )

Masalah ini tidak jelas. Ada keputusan yang sangat disengaja untuk memiliki "kebocoran" ciri otomatis: jika tidak, kami harus meletakkan + !Send + !Sync pada setiap fungsi yang mengembalikan sesuatu yang tidak terkirim atau tidak sinkron, dan kami akan memiliki cerita yang tidak jelas dengan potensi ciri-ciri otomatis kustom lainnya yang tidak dapat diterapkan pada tipe konkret yang dikembalikan fungsinya. Ini adalah dua masalah yang akan saya bahas nanti.

Pertama, saya hanya ingin menyatakan keberatan saya terhadap masalah ini: ini memungkinkan mengubah badan fungsi untuk mengubah API yang dihadapi publik. Ini secara langsung mengurangi pemeliharaan kode.

Sepanjang pengembangan karat, keputusan telah dibuat yang salah di sisi verbositas atas kegunaan. Ketika pendatang baru melihat ini, mereka pikir itu verbositas demi verbositas, tapi ini tidak terjadi. Setiap keputusan, apakah itu untuk tidak memiliki struktur yang secara otomatis menerapkan Salin, atau memiliki semua jenis eksplisit di tanda tangan fungsi, adalah demi pemeliharaan.

Ketika saya memperkenalkan Rust kepada orang-orang, tentu saja, saya dapat menunjukkan kepada mereka kecepatan, produktivitas, keamanan memori. Tapi pergi memiliki kecepatan. Ada memiliki keamanan memori. Python memiliki produktivitas. Apa yang dimiliki Rust mengalahkan semua ini, ia memiliki rawatan. Ketika seorang penulis perpustakaan ingin mengubah algoritma menjadi lebih efisien, atau ketika mereka ingin mengulang struktur peti, mereka memiliki jaminan kuat dari kompiler bahwa itu akan memberi tahu mereka ketika mereka melakukan kesalahan. Dalam karat, saya dapat yakin bahwa kode saya akan terus berfungsi tidak hanya dalam hal keamanan memori, tetapi juga logika dan antarmuka. _Setiap antarmuka fungsi di Rust sepenuhnya dapat diwakili oleh deklarasi tipe fungsi_.

Menstabilkan impl Trait apa adanya memiliki peluang besar untuk melawan keyakinan ini. Tentu, ini sangat bagus untuk menulis kode dengan cepat, tetapi jika saya ingin membuat prototipe, saya akan menggunakan python. Rust adalah bahasa pilihan ketika seseorang membutuhkan pemeliharaan jangka panjang, bukan kode hanya-tulis jangka pendek.


Saya katakan hanya ada "kemungkinan besar" ini menjadi buruk di sini karena sekali lagi, masalahnya tidak jelas. Seluruh gagasan 'ciri-ciri otomatis' di tempat pertama adalah non-eksplisit. Kirim dan Sinkronkan diimplementasikan berdasarkan apa isi struct, bukan deklarasi publik. Karena keputusan ini berhasil untuk karat, impl Trait bertindak serupa juga dapat berhasil dengan baik.

Namun, fungsi dan struktur digunakan secara berbeda dalam basis kode, dan ini bukan masalah yang sama.

Saat memodifikasi bidang struktur, bahkan bidang pribadi, segera jelas bahwa seseorang mengubah konten sebenarnya. Struktur dengan bidang non-Kirim atau non-Sinkronisasi membuat pilihan itu, dan pengelola perpustakaan tahu untuk memeriksa ulang saat PR mengubah bidang struktur.

Saat memodifikasi internal suatu fungsi, jelas sekali bahwa fungsi tersebut dapat memengaruhi kinerja dan kebenarannya. Namun, di Rust, kami tidak perlu memeriksa apakah kami mengembalikan tipe yang benar. Deklarasi fungsi adalah kontrak keras yang harus kita junjung, dan rustc mengawasi kita. Ini adalah garis tipis antara ciri-ciri otomatis pada struct dan dalam pengembalian fungsi, tetapi mengubah internal suatu fungsi jauh lebih rutin. Setelah kita memiliki Future s bertenaga generator penuh, akan lebih rutin lagi untuk memodifikasi fungsi yang mengembalikan -> impl Future . Ini semua akan menjadi perubahan yang penulis perlukan untuk menyaring implementasi Kirim/Sinkronisasi yang dimodifikasi jika kompiler tidak menangkapnya.

Untuk mengatasi ini, kita dapat memutuskan bahwa ini adalah beban pemeliharaan yang dapat diterima, seperti yang dilakukan oleh diskusi RFC awal . Bagian dalam RFC sifat impl konservatif ini memaparkan argumen terbesar untuk membocorkan ciri-ciri otomatis ("OIBIT" adalah nama lama untuk ciri-ciri otomatis).

Saya sudah memberikan tanggapan utama saya untuk ini, tapi ini satu catatan terakhir. Mengubah tata letak struktur bukanlah hal yang umum dilakukan; itu bisa dijaga. Beban pemeliharaan dalam memastikan fungsi terus menerapkan ciri otomatis yang sama lebih besar daripada struktur hanya karena fungsi lebih banyak berubah.


Sebagai catatan terakhir, saya ingin mengatakan bahwa ciri otomatis otomatis bukanlah satu-satunya pilihan. Ini adalah opsi yang kami pilih, tetapi alternatif dari fitur auto opt-out masih merupakan alternatif.

Kami dapat meminta fungsi mengembalikan item yang tidak terkirim/tidak menyinkronkan ke status + !Send + !Sync atau untuk mengembalikan suatu sifat (alias mungkin?) yang memiliki batas tersebut. Ini bukan keputusan yang baik, tapi mungkin lebih baik daripada yang kita pilih saat ini.

Adapun kekhawatiran mengenai ciri-ciri otomatis kustom, saya berpendapat bahwa setiap ciri otomatis baru tidak boleh diterapkan hanya untuk tipe baru yang diperkenalkan setelah ciri otomatis. Ini mungkin memberikan lebih banyak masalah daripada yang dapat saya atasi sekarang, tetapi itu bukan masalah yang tidak dapat kami atasi dengan lebih banyak desain.


Ini sangat terlambat, dan sangat panjang lebar, dan saya yakin saya telah mengajukan keberatan ini sebelumnya. Saya senang bisa berkomentar untuk terakhir kalinya, dan memastikan kami sepenuhnya setuju dengan keputusan yang kami buat.

Terima kasih telah membaca, dan saya harap keputusan akhir membawa Rust ke arah yang terbaik.

Semua 417 komentar

@aturon Bisakah kita benar-benar meletakkan RFC di repositori? ( @mbrubeck berkomentar di sana bahwa ini adalah masalah.)

Selesai.

Upaya implementasi pertama adalah #35091 (kedua, jika Anda menghitung cabang saya dari tahun lalu).

Satu masalah yang saya hadapi adalah dengan kehidupan. Ketik inferensi suka menempatkan variabel wilayah _everywhere_ dan tanpa perubahan pemeriksaan wilayah, variabel tersebut tidak menyimpulkan apa pun selain cakupan lokal.
Namun, tipe konkret _harus_ dapat diekspor, jadi saya membatasinya ke 'static dan secara eksplisit menamai parameter seumur hidup terikat awal, tetapi _never_ salah satu dari mereka jika ada fungsi yang terlibat - bahkan string literal tidak menyimpulkan 'static , itu sama sekali tidak berguna.

Satu hal yang saya pikirkan, yang akan berdampak 0 pada pemeriksaan wilayah itu sendiri, adalah menghapus masa pakai:

  • tidak ada yang mengekspos tipe konkret dari impl Trait harus peduli tentang masa pakai - pencarian cepat untuk Reveal::All menunjukkan bahwa itu sudah terjadi di kompiler
  • sebuah ikatan perlu ditempatkan pada semua tipe konkret impl Trait dalam tipe kembalian suatu fungsi, yang melampaui panggilan fungsi itu - ini berarti bahwa setiap masa pakai, dengan kebutuhan, adalah 'static atau salah satu parameter seumur hidup fungsi - _even_ jika kita tidak tahu yang mana (misalnya "terpendek dari 'a dan 'b ")
  • kita harus memilih varians untuk parameter seumur hidup implisit impl Trait (yaitu pada semua parameter seumur hidup dalam cakupan, sama seperti dengan parameter tipe): invarians paling mudah dan memberikan lebih banyak kontrol ke callee, sementara kontravarian memungkinkan pemanggil melakukannya lebih banyak dan akan memerlukan pemeriksaan bahwa setiap masa pakai dalam tipe pengembalian berada dalam posisi kontravarian (sama dengan parametrisme tipe kovarian alih-alih invarian)
  • mekanisme kebocoran sifat otomatis mengharuskan pengikatan sifat dapat ditempatkan pada tipe beton, dalam fungsi lain - karena kami telah menghapus masa pakai dan tidak tahu ke mana masa pakainya, setiap masa pakai yang terhapus dalam tipe beton harus diganti dengan variabel inferensi baru yang dijamin tidak lebih pendek dari masa pakai terpendek dari semua parameter masa pakai aktual; masalahnya terletak pada kenyataan bahwa sifat impls dapat berakhir membutuhkan hubungan seumur hidup yang lebih kuat (misalnya X<'a, 'a> atau X<'static> ), yang harus dideteksi dan disalahgunakan, karena tidak dapat dibuktikan untuk itu seumur hidup

Poin terakhir tentang kebocoran sifat otomatis adalah satu-satunya kekhawatiran saya, yang lainnya tampak lurus ke depan.
Tidak sepenuhnya jelas pada titik ini berapa banyak pemeriksaan wilayah yang dapat kami gunakan kembali apa adanya. Semoga semua.

cc @rust-lang/lang

@eddyb

Tapi masa hidup _are_ penting dengan impl Trait - mis

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

Saya menyebutkan itu beberapa kali di utas RFC

versi sifat-objek-kurang:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

Ini UB atau tidak tergantung pada definisi as_debug .

@arielb1 Ah, benar, saya lupa bahwa salah satu alasan saya melakukan apa yang saya lakukan adalah hanya menangkap parameter seumur hidup, bukan yang terikat terlambat anonim, kecuali itu tidak benar-benar berfungsi.

@arielb1 Apakah kita memiliki hubungan hidup yang lebih lama yang dapat kita tempatkan di antara masa pakai yang ditemukan dalam tipe beton pra-penghapusan dan masa hidup batas akhir dalam tanda tangan? Jika tidak, mungkin bukan ide yang buruk untuk hanya melihat hubungan seumur hidup dan insta-fail semua langsung atau _tidak langsung_ 'a outlives 'b mana 'a adalah _anything_ selain 'static atau parameter seumur hidup dan 'b muncul dalam tipe konkret dari impl Trait .

Maaf agak lama menulis kembali di sini. Jadi saya sudah berpikir
tentang masalah ini. Perasaan saya adalah bahwa kita, pada akhirnya, harus (dan
ingin) memperluas regionck dengan batasan jenis baru -- saya akan menyebutnya
batasan \in , karena memungkinkan Anda untuk mengatakan sesuatu seperti '0 \in {'a, 'b, 'c} , artinya wilayah yang digunakan untuk '0 harus
baik 'a , 'b , atau 'c . Saya tidak yakin cara terbaik untuk mengintegrasikan
ini untuk menyelesaikannya sendiri -- tentu saja jika set \in adalah singleton
set, itu hanya hubungan yang sama (yang saat ini tidak kita miliki sebagai
hal kelas satu, tetapi yang dapat terdiri dari dua batas), tapi
jika tidak, itu membuat segalanya menjadi rumit.

Ini semua berhubungan dengan keinginan saya untuk membuat himpunan batasan wilayah
lebih ekspresif dari apa yang kita miliki saat ini. Tentu saja seseorang bisa menulis
\in batasan OR dan == . Tapi tentu saja lebih
kendala ekspresif lebih sulit untuk dipecahkan dan \in tidak berbeda.

Bagaimanapun, izinkan saya memaparkan sedikit pemikiran saya di sini. Mari bekerja dengan ini
contoh:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Saya pikir desugaring yang paling akurat untuk impl Trait mungkin adalah
tipe baru:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Sekarang impl Iterator<Item=u32> di foo harus berperilaku sama seperti
FooReturn<'a,'b> akan berperilaku. Ini bukan pertandingan yang sempurna sekalipun. Satu
perbedaan, misalnya, adalah varians, seperti yang dikemukakan eddyb -- I am
dengan asumsi kita akan membuat impl Foo -tipe seperti invarian di atas tipe
parameter foo . Namun, perilaku sifat otomatis berhasil.
(Area lain di mana kecocokan mungkin tidak ideal adalah jika kita menambahkan
kemampuan untuk "menembus" abstraksi impl Iterator , sehingga kode itu
"di dalam" abstraksi mengetahui jenis yang tepat -- maka ia akan mengurutkan
dari memiliki operasi "membuka" implisit yang sedang berlangsung.)

Dalam beberapa hal, kecocokan yang lebih baik adalah dengan mempertimbangkan semacam sifat sintetis:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Sekarang kita dapat menganggap tipe impl Iterator menjadi seperti <() as FooReturn<'a,'b>>::Type . Ini juga bukan pasangan yang sempurna, karena kami
biasanya akan menormalkannya. Anda mungkin membayangkan menggunakan spesialisasi
untuk mencegahnya:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

Dalam hal ini, <() as FooReturn<'a,'b>>::Type tidak akan dinormalisasi,
dan kami memiliki pertandingan yang lebih dekat. Varians, khususnya, berperilaku
Baik; jika kita ingin memiliki beberapa tipe yang "di dalam"
abstraksi, mereka akan sama tetapi mereka diizinkan untuk
normalisasi. Namun, ada masalah: hal-hal sifat otomatis tidak
cukup bekerja. (Kami mungkin ingin mempertimbangkan untuk menyelaraskan berbagai hal di sini,
sebenarnya.)

Bagaimanapun, maksud saya dalam mengeksplorasi potensi desugarings ini bukanlah untuk
menyarankan agar kita menerapkan "Sifat impl" sebagai _actual_ desugaring
(walaupun mungkin menyenangkan...) tapi untuk memberikan intuisi untuk pekerjaan kita. Saya
berpikir bahwa desugaring kedua -- dalam hal proyeksi -- adalah a
cukup membantu untuk membimbing kita ke depan.

Satu tempat dimana proyeksi desugaring ini adalah panduan yang sangat berguna adalah
hubungan "hidup lebih lama". Jika kami ingin memeriksa apakah <() as FooReturn<'a,'b>>::Type: 'x , RFC 1214 memberi tahu kami bahwa kami dapat membuktikannya
selama 'a: 'x _and_ 'b: 'x berlaku. Ini yang saya pikir bagaimana kita inginkan
untuk menangani hal-hal untuk sifat impl juga.

Pada waktu trans, dan untuk ciri-ciri otomatis, kita harus tahu apa XXX
adalah, tentu saja. Ide dasarnya di sini, saya asumsikan, adalah membuat tipe
variabel untuk XXX dan periksa apakah nilai aktual yang dikembalikan
semua bisa disatukan dengan XXX . Variabel tipe itu, secara teori,
beritahu kami jawaban kami. Tapi tentu saja masalahnya adalah tipe ini
variabel dapat merujuk ke banyak wilayah yang tidak berada dalam cakupan di
tanda tangan fn -- misalnya, wilayah tubuh fn. (Masalah yang sama ini
tidak terjadi dengan jenis; meskipun, secara teknis, Anda bisa menempatkan
misalnya deklarasi struct di badan fn dan itu tidak akan dapat dinamai,
itu semacam pembatasan buatan -- seseorang bisa saja bergerak
struktur di luar fn.)

Jika Anda melihat baik pada struct desugaring atau impl, ada
(tersirat dalam struktur leksikal Rust) pembatasan bahwa XXX dapat
hanya beri nama 'static atau masa hidup seperti 'a dan 'b , yang
muncul di tanda tangan fungsi. Itu adalah hal yang kita tidak
pemodelan di sini. Saya tidak yakin cara terbaik untuk melakukannya -- beberapa jenis
skema inferensi memiliki representasi pelingkupan yang lebih langsung, dan
Saya selalu ingin menambahkan itu ke Rust, untuk membantu kami dengan penutupan. Tetapi
mari kita pikirkan delta yang lebih kecil dulu.

Di sinilah batasan \in berasal. Orang bisa membayangkan menambahkan
aturan jenis-periksa yang (pada dasarnya) FR(XXX) \subset {'a, 'b} --
artinya "wilayah gratis" yang muncul di XXX hanya dapat berupa 'a dan
'b . Ini akan diterjemahkan menjadi persyaratan \in untuk
berbagai wilayah yang muncul di XXX .

Mari kita lihat contoh sebenarnya:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Di sini, tipe jika condition benar akan menjadi seperti
Cloned<SliceIter<'a, i32>> . Tetapi jika condition salah, kami akan
ingin Cloned<SliceIter<'b, i32>> . Tentu saja dalam kedua kasus kami akan
berakhir dengan sesuatu seperti (menggunakan angka untuk variabel tipe/wilayah):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Jika kemudian kita instantiate variabel 0 ke Cloned<SliceIter<'2, i32>> ,
kami memiliki '0: '2 dan '1: '2 , atau satu set total hubungan wilayah
Suka:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Jadi nilai apa yang harus kita gunakan untuk '2 ? Kami juga memiliki tambahan
kendala bahwa '2 in {'a, 'b} . Dengan fn seperti yang tertulis, saya pikir kami
harus melaporkan kesalahan, karena 'a maupun 'b tidak ada
pilihan yang benar. Menariknya, jika kita menambahkan batasan 'a: 'b , maka akan ada nilai yang benar ( 'b ).

Perhatikan bahwa jika kita hanya menjalankan algoritma _normal_, kita akan berakhir dengan
'2 menjadi 'body . Saya tidak yakin bagaimana menangani hubungan \in
kecuali untuk pencarian lengkap (walaupun saya bisa membayangkan beberapa yang spesial
kasus).

OK, itu sejauh yang saya dapatkan. =)

Di PR #35091, @arielb1 menulis:

Saya tidak suka pendekatan "tangkap semua kehidupan dalam sifat impl" dan lebih suka sesuatu yang lebih seperti penghapusan seumur hidup.

Saya pikir akan lebih masuk akal untuk berdiskusi di sini. @arielb1 , dapatkah Anda menguraikan lebih lanjut tentang apa yang ada dalam pikiran Anda? Dalam hal analogi yang saya buat di atas, saya kira Anda pada dasarnya berbicara tentang "memangkas" kumpulan masa hidup yang akan muncul baik sebagai parameter pada tipe baru atau dalam proyeksi (yaitu, <() as FooReturn<'a>>::Type alih-alih <() as FooReturn<'a,'b>>::Type atau apa?

Saya tidak berpikir bahwa aturan penghapusan seumur hidup seperti yang ada akan menjadi panduan yang baik dalam hal ini: jika kita hanya memilih masa pakai &self untuk dimasukkan saja, maka kita tidak perlu dapat memasukkan ketik parameter dari Self struct, atau ketik parameter dari metode, karena mereka mungkin memiliki kondisi WF yang mengharuskan kita untuk memberi nama beberapa masa pakai lainnya.

Bagaimanapun, akan sangat bagus untuk melihat beberapa contoh yang menggambarkan aturan yang ada dalam pikiran Anda, dan mungkin keuntungan apa pun darinya. :) (Juga, saya kira kita akan membutuhkan beberapa sintaks untuk mengesampingkan pilihan.) Semua hal lain dianggap sama, jika kita dapat menghindari keharusan memilih dari N masa hidup, saya lebih suka itu.

Saya belum pernah melihat interaksi impl Trait dengan privasi dibahas di mana pun.
Sekarang fn f() -> impl Trait dapat mengembalikan tipe pribadi S: Trait mirip dengan sifat objek fn f() -> Box<Trait> . Yaitu objek tipe pribadi dapat berjalan dengan bebas di luar modul mereka dalam bentuk anonim.
Ini tampaknya masuk akal dan diinginkan - jenisnya sendiri adalah detail implementasi, hanya antarmukanya, tersedia melalui sifat publik Trait bersifat publik.
Namun ada satu perbedaan antara objek sifat dan impl Trait . Dengan objek sifat saja, semua metode sifat dari tipe pribadi bisa mendapatkan tautan internal, mereka masih dapat dipanggil melalui pointer fungsi. Dengan impl Trait metode sifat dari tipe pribadi dapat dipanggil secara langsung dari unit terjemahan lain. Algoritme yang melakukan "internalisasi" simbol harus berusaha lebih keras untuk menginternalisasi metode hanya untuk tipe yang tidak dianonimkan dengan impl Trait , atau menjadi sangat pesimis.

@nikomatsakis

Cara "eksplisit" untuk menulis foo adalah

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Di sini tidak ada pertanyaan tentang ikatan seumur hidup. Jelas, harus menulis seumur hidup terikat setiap kali akan sangat berulang. Namun, cara kita menghadapi pengulangan semacam itu umumnya melalui penghapusan seumur hidup. Dalam kasus foo , elision akan gagal dan memaksa programmer untuk secara eksplisit menentukan masa pakai.

Saya menentang menambahkan penghapusan seumur hidup yang sensitif terhadap @eddyb hanya dalam kasus khusus impl Trait dan bukan sebaliknya.

@arielb1 hmm, saya tidak 100% yakin bagaimana memikirkan sintaks yang diusulkan ini dalam hal "desugarings" yang saya diskusikan. Ini memungkinkan Anda untuk menentukan apa yang tampaknya terikat seumur hidup, tetapi hal yang kami coba simpulkan sebagian besar adalah apa yang tampak seumur hidup dalam tipe tersembunyi. Apakah ini menunjukkan bahwa paling banyak satu masa hidup dapat "disembunyikan" (dan itu harus ditentukan dengan tepat?)

Sepertinya tidak selalu bahwa "parameter seumur hidup tunggal" sudah cukup:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

Dalam hal ini, tipe iterator tersembunyi mengacu pada 'a dan 'b (walaupun _is_ varian di keduanya; tapi saya kira kita bisa menemukan contoh yang invarian).

Jadi @aturon dan saya membahas masalah ini dan saya ingin berbagi. Sebenarnya ada beberapa pertanyaan ortogonal di sini dan saya ingin memisahkannya. Pertanyaan pertama adalah "parameter tipe/seumur hidup apa yang berpotensi digunakan dalam tipe tersembunyi?" Dalam hal (quasi-)desugaring menjadi default type , ini bermuara pada "parameter tipe apa yang muncul pada sifat yang kami perkenalkan". Jadi, misalnya, jika fungsi ini:

fn foo<'a, 'b, T>() -> impl Trait { ... }

akan mendapatkan desugared untuk sesuatu seperti:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

kemudian pertanyaan ini turun ke "parameter tipe apa yang muncul pada sifat Foo dan implikasinya"? Pada dasarnya, ... sini. Jelas ini termasuk set parameter tipe yang muncul digunakan oleh Trait itu sendiri, tetapi parameter tipe tambahan apa? (Seperti yang saya sebutkan sebelumnya, desugaring ini 100% setia kecuali untuk kebocoran ciri-ciri otomatis, dan saya berpendapat bahwa kita harus membocorkan ciri-ciri otomatis juga untuk impls khusus.)

Jawaban default yang kami gunakan adalah "semuanya", jadi di sini ... akan menjadi 'a, 'b, T (bersama dengan parameter anonim yang mungkin muncul). Ini _mungkin_ menjadi default yang masuk akal, tetapi ini bukan _tentu_ default terbaik. (Seperti yang ditunjukkan @arielb1 .)

Ini memiliki efek pada hubungan yang lebih lama, karena, untuk menentukan bahwa <() as Foo<...>>::Type (mengacu pada beberapa contoh tertentu yang buram dari impl Trait ) hidup lebih lama dari 'x , kita harus secara efektif menunjukkan ...: 'x (yaitu, setiap parameter seumur hidup dan tipe).

Inilah sebabnya mengapa saya mengatakan itu tidak cukup untuk mempertimbangkan parameter seumur hidup: bayangkan bahwa kita memiliki beberapa panggilan ke foo seperti foo::<'a0, 'b0, &'c0 i32> . Ini menyiratkan bahwa ketiga masa hidup, '[abc]0 , harus hidup lebih lama dari 'x -- dengan kata lain, selama nilai pengembalian digunakan, ini akan memprolog pinjaman semua data yang diberikan ke dalam fungsi . Tapi, seperti yang ditunjukkan oleh @arielb1 , elision menyarankan bahwa ini biasanya akan lebih lama dari yang diperlukan.

Jadi saya membayangkan bahwa yang kita butuhkan adalah:

  • untuk menyelesaikan default yang wajar, mungkin menggunakan intuisi dari elision;
  • untuk memiliki sintaks eksplisit ketika default tidak sesuai.

@aturon meludahkan sesuatu seperti impl<...> Trait sebagai sintaks eksplisit, yang tampaknya masuk akal. Oleh karena itu, seseorang dapat menulis:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

untuk menunjukkan bahwa tipe tersembunyi sebenarnya tidak merujuk ke 'a atau 'b tetapi hanya T . Atau seseorang mungkin menulis impl<'a> Trait untuk menunjukkan bahwa baik 'b maupun T tidak ditangkap.

Adapun default, sepertinya memiliki lebih banyak data akan sangat berguna -- tetapi logika umum dari elision menyarankan bahwa kita sebaiknya menangkap semua parameter yang disebutkan dalam tipe self , jika berlaku. Misalnya, jika Anda memiliki fn foo<'a,'b>(&'a self, v: &'b [u8]) dan tipenya adalah Bar<'c, X> , maka tipe self akan menjadi &'a Bar<'c, X> dan karenanya kita akan menangkap 'a , 'c , dan X secara default, tetapi tidak 'b .


Catatan terkait lainnya adalah apa arti dari ikatan seumur hidup. Saya pikir batas seumur hidup suara memiliki arti yang ada yang tidak boleh diubah: jika kita menulis impl (Trait+'a) itu berarti bahwa tipe tersembunyi T hidup lebih lama dari 'a . Demikian pula seseorang dapat menulis impl (Trait+'static) untuk menunjukkan bahwa tidak ada pointer yang dipinjam (bahkan jika beberapa masa hidup ditangkap). Saat menyimpulkan tipe tersembunyi T , ini akan menyiratkan ikatan seumur hidup seperti $T: 'static , di mana $T adalah variabel inferensi yang kita buat untuk tipe tersembunyi. Ini akan ditangani dengan cara biasa. Dari sudut pandang pemanggil, di mana tipe tersembunyi, yah, tersembunyi, 'static terikat akan memungkinkan kita untuk menyimpulkan bahwa impl (Trait+'static) hidup lebih lama dari 'static bahkan jika ada parameter seumur hidup yang ditangkap.

Di sini ia berperilaku persis seperti perilaku desugaring:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

Semua ini ortogonal dari inferensi. Kami masih ingin (saya pikir) untuk menambahkan gagasan tentang batasan "pilih dari" dan memodifikasi inferensi dengan beberapa heuristik dan, mungkin, pencarian lengkap (pengalaman dari RFC 1214 menunjukkan bahwa heuristik dengan fallback konservatif sebenarnya dapat membawa kita sangat jauh; Saya tidak mengetahui orang-orang yang mengalami keterbatasan dalam hal ini, meskipun mungkin ada masalah di suatu tempat). Tentu saja, menambahkan batas seumur hidup seperti 'static atau 'a` dapat memengaruhi inferensi, dan karenanya bermanfaat, tetapi itu bukan solusi yang sempurna: untuk satu hal, mereka terlihat oleh pemanggil dan menjadi bagian dari API, yang mungkin tidak diinginkan.

Opsi yang memungkinkan:

Seumur hidup eksplisit terikat dengan penghapusan parameter keluaran

Seperti objek sifat hari ini, objek impl Trait memiliki parameter terikat seumur hidup tunggal, yang disimpulkan menggunakan aturan elision.

Kekurangan: tidak ergonomis
Keuntungan: jelas

Batas seumur hidup eksplisit dengan penghapusan "semua generik"

Seperti objek sifat hari ini, objek impl Trait memiliki satu parameter terikat seumur hidup.

Namun, elision membuat parameter terikat awal baru yang hidup lebih lama dari semua parameter eksplisit:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Kerugian: menambahkan parameter terikat awal

lagi.

Saya mengalami masalah ini dengan impl Trait +'a dan meminjam: https://github.com/rust-lang/rust/issues/37790

Jika saya memahami perubahan ini dengan benar (dan kemungkinannya kecil!), maka saya pikir kode taman bermain ini akan berfungsi:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Baik ThingOne dan ThingTwo mengimplementasikan sifat Thing . build mengatakan itu akan mengembalikan sesuatu yang mengimplementasikan Thing , yang dilakukannya. Namun itu tidak mengkompilasi. Jadi saya jelas salah paham tentang sesuatu.

"Sesuatu" itu harus memiliki tipe, tetapi dalam kasus Anda, Anda memiliki dua tipe yang saling bertentangan. @nikomatsakis sebelumnya telah menyarankan membuat ini bekerja secara umum dengan membuat misalnya ThingOne | ThingTwo sebagai jenis ketidakcocokan muncul.

@eddyb dapatkah Anda menguraikan ThingOne | ThingTwo ? Apakah Anda tidak perlu memiliki Box jika kami hanya mengetahui jenisnya saat run-time? Atau itu semacam enum ?

Ya, itu bisa berupa enum -jenis ad-hoc yang memanggil metode sifat yang didelegasikan, jika memungkinkan, ke variannya.

Aku juga menginginkan hal semacam itu sebelumnya. Enum anonim RFC: https://github.com/rust-lang/rfcs/pull/1154

Ini adalah kasus langka dari sesuatu yang berfungsi lebih baik jika didorong oleh inferensi, karena jika Anda hanya membuat jenis ini pada ketidakcocokan, variannya berbeda (yang merupakan masalah dengan bentuk umum).
Anda juga bisa mendapatkan sesuatu karena tidak memiliki pencocokan pola (kecuali dalam kasus yang jelas-jelas terpisah?).
Tetapi gula delegasi IMO akan "hanya berfungsi" dalam semua kasus yang relevan, bahkan jika Anda berhasil mendapatkan T | T .

Bisakah Anda mengeja bagian lain yang tersirat dari kalimat-kalimat itu? Saya tidak mengerti sebagian besar, dan curiga saya kehilangan beberapa konteks. Apakah Anda secara implisit menanggapi masalah dengan tipe serikat pekerja? RFC itu hanyalah enum anonim, bukan tipe serikat pekerja - (T|T) akan sama bermasalahnya dengan Result<T, T> .

Oh, tidak apa-apa, saya membuat proposal bingung (juga macet di ponsel sampai saya memilah HDD saya yang gagal jadi maaf karena terdengar seperti di Twitter).

Saya menemukan (posisional, yaitu T|U != U|T ) enum anonim menarik, dan saya yakin mereka dapat dicoba di perpustakaan jika kami memiliki generik variadik (Anda dapat menghindari ini dengan menggunakan hlist ) dan const generics (ditto, dengan nomor peano).

Tetapi, pada saat yang sama, jika kami memiliki dukungan bahasa untuk sesuatu, itu akan menjadi tipe serikat pekerja, bukan enum anonim. Misalnya bukan Result tetapi jenis kesalahan (untuk melewati kebosanan pembungkus bernama untuk mereka).

Saya tidak yakin apakah ini tempat yang tepat untuk bertanya, tetapi mengapa kata kunci seperti impl diperlukan? Saya tidak dapat menemukan diskusi (mungkin salah saya).

Jika suatu fungsi mengembalikan Sifat impl, tubuhnya dapat mengembalikan nilai jenis apa pun yang mengimplementasikan Sifat

Sejak

fn bar(a: &Foo) {
  ...
}

berarti "menerima referensi ke tipe yang mengimplementasikan sifat Foo " yang saya harapkan

fn bar() -> Foo {
  ...
}

berarti "mengembalikan tipe yang mengimplementasikan sifat Foo ". Apakah ini tidak mungkin?

@kud1ing alasannya adalah untuk tidak menghapus kemungkinan memiliki fungsi yang mengembalikan tipe berukuran dinamis Trait jika dukungan untuk nilai pengembalian berukuran dinamis ditambahkan di masa mendatang. Saat ini Trait sudah menjadi DST yang valid, hanya saja tidak mungkin mengembalikan DST sehingga Anda perlu mengemasnya untuk menjadikannya tipe berukuran.

EDIT: Ada beberapa diskusi tentang ini di utas RFC yang ditautkan.

Nah, untuk satu, terlepas dari apakah nilai pengembalian berukuran dinamis akan ditambahkan, saya lebih suka sintaks saat ini. Tidak seperti apa yang terjadi dengan objek sifat, ini bukan penghapusan tipe, dan setiap kebetulan seperti "parameter f: &Foo mengambil sesuatu yang menyiratkan Foo , sedangkan ini mengembalikan sesuatu yang menyiratkan Foo " bisa menyesatkan.

Saya mengumpulkan dari diskusi RFC bahwa saat ini impl adalah implementasi placeholder, dan tidak ada impl yang sangat diinginkan. Apakah ada alasan untuk _not_ menginginkan Sifat impl jika nilai yang dikembalikan bukan DST?

Saya pikir teknik impl saat ini untuk menangani "kebocoran sifat otomatis" bermasalah. Sebagai gantinya, kita harus menerapkan pemesanan DAG sehingga jika Anda mendefinisikan fn fn foo() -> impl Iterator , dan Anda memiliki pemanggil fn bar() { ... foo() ... } , maka kita harus mengetik-periksa foo() sebelum bar() (agar kita tahu apa tipe tersembunyinya). Jika hasil siklus, kami akan melaporkan kesalahan. Ini adalah sikap konservatif -- kita mungkin bisa melakukan yang lebih baik -- tetapi saya pikir teknik saat ini, di mana kita mengumpulkan kewajiban sifat-otomatis dan memeriksanya di akhir, tidak bekerja secara umum. Misalnya, itu tidak akan bekerja dengan baik dengan spesialisasi.

(Kemungkinan lain yang mungkin lebih permisif daripada membutuhkan DAG yang ketat adalah dengan mengetik-periksa kedua fns "bersama" sampai batas tertentu. Saya pikir itu adalah sesuatu yang perlu dipertimbangkan hanya setelah kita sedikit mengarsitektur ulang sistem sifat.)

@Nercury saya tidak mengerti. Apakah Anda bertanya apakah ada alasan untuk tidak ingin fn foo() -> Trait berarti -> impl Trait ?

@nikomatsakis Ya, saya bertanya persis seperti itu, maaf bahasanya berbelit-belit :). Saya pikir melakukan ini tanpa kata kunci impl akan lebih sederhana, karena perilaku ini persis seperti yang diharapkan (ketika tipe konkret dikembalikan sebagai ganti tipe pengembalian sifat). Namun, saya mungkin kehilangan sesuatu, itu sebabnya saya bertanya.

Perbedaannya adalah bahwa fungsi yang mengembalikan impl Trait selalu mengembalikan tipe yang sama - pada dasarnya mengembalikan inferensi tipe. IIUC, fungsi yang hanya mengembalikan Trait akan dapat mengembalikan implementasi apa pun dari sifat itu secara dinamis, tetapi pemanggil harus siap mengalokasikan ruang untuk nilai pengembalian melalui sesuatu seperti box foo() .

@Nercury Alasan sederhananya adalah sintaks -> Trait sudah memiliki arti, jadi kita harus menggunakan sesuatu yang lain untuk fitur ini.

Saya sebenarnya melihat orang mengharapkan kedua jenis perilaku secara default, dan kebingungan semacam ini muncul cukup sering. Sejujurnya saya lebih suka fn foo() -> Trait tidak berarti apa-apa (atau menjadi peringatan secara default) dan ada eksplisit kata kunci untuk kasus "beberapa jenis yang diketahui pada waktu kompilasi yang dapat saya pilih tetapi penelepon tidak melihat" dan "objek sifat yang dapat secara dinamis mengirim ke semua jenis yang mengimplementasikan Sifat", misalnya fn foo() -> impl Trait vs fn foo() -> dyn Trait . Tapi jelas kapal-kapal itu telah berlayar.

Mengapa kompiler tidak menghasilkan enum yang menampung semua jenis pengembalian fungsi yang berbeda, mengimplementasikan sifat yang meneruskan argumen ke setiap varian, dan mengembalikannya sebagai gantinya?

Itu akan melewati satu-satunya aturan tipe pengembalian yang diizinkan.

@NeoLegends Melakukan ini secara manual cukup umum, dan sedikit gula untuk itu mungkin bagus dan telah diusulkan di masa lalu, tetapi ini adalah rangkaian semantik ketiga yang sama sekali berbeda dari mengembalikan impl Trait atau objek sifat, jadi tidak sangat relevan dengan pembahasan ini.

@Ixrec Ya saya tahu ini sedang dilakukan secara manual, tetapi kasus penggunaan sebenarnya dari enum anonim karena tipe pengembalian yang dihasilkan kompiler adalah tipe yang tidak dapat Anda jelaskan, seperti rantai panjang iterator atau adaptor masa depan.

Bagaimana semantik yang berbeda ini? Enum anonim (sejauh kompiler menghasilkannya, tidak sesuai dengan enum anonim RFC) karena nilai pengembalian hanya benar-benar masuk akal jika ada API umum seperti sifat yang mengabstraksikan varian yang berbeda. Saya menyarankan fitur yang masih terlihat dan berperilaku seperti Sifat impl biasa, hanya dengan satu-jenis-batas dihapus melalui kompiler yang dihasilkan enum konsumen API tidak akan pernah melihat secara langsung. Konsumen harus selalu hanya melihat 'Sifat tersirat'.

Enum anonim yang dibuat secara otomatis memberi impl Trait biaya tersembunyi yang mudah dilewatkan, jadi itu sesuatu yang perlu dipertimbangkan.

Saya menduga hal "pass-through enum otomatis" hanya masuk akal untuk sifat-sifat aman-objek. Apakah hal yang sama berlaku untuk impl Trait itu sendiri?

@rpjohnst Kecuali ini varian metode aktual dalam metadata peti dan dimonomorfisasi di situs panggilan. Tentu saja, ini mengharuskan perubahan dari satu varian ke varian lain tidak merusak penelepon. Dan ini mungkin terlalu ajaib.

@glaebhoerl

Saya menduga hal "pass-through enum otomatis" hanya masuk akal untuk sifat-sifat aman-objek. Apakah hal yang sama berlaku untuk Sifat impl itu sendiri?

Ini adalah hal yang menarik! Saya telah memperdebatkan apa cara yang tepat untuk "desugar" sifat impl, dan sebenarnya hampir menyarankan bahwa mungkin kita ingin menganggapnya lebih sebagai "struct dengan bidang pribadi" sebagai lawan dari "proyeksi tipe abstrak " penafsiran. Namun, itu tampaknya menyiratkan sesuatu yang mirip dengan turunan tipe baru yang digeneralisasi, yang tentu saja terkenal tidak sehat di Haskell ketika digabungkan dengan keluarga tipe . Saya mengaku tidak memiliki pemahaman penuh tentang ketidaksehatan "dalam cache" ini tetapi sepertinya kita harus sangat berhati-hati di sini setiap kali kita ingin secara otomatis menghasilkan implementasi suatu sifat untuk beberapa tipe F<T> dari impl untuk T .

@nikomatsakis

Masalahnya adalah, dalam istilah Rust

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

tl;dr adalah bahwa turunan tipe baru yang digeneralisasikan (dan sedang) diimplementasikan hanya dengan transmute dalam vtable -- lagi pula, vtable terdiri dari fungsi pada tipe tersebut, dan tipe dan tipe barunya memiliki representasi yang sama , jadi seharusnya baik-baik saja, kan? Tetapi rusak jika fungsi tersebut juga menggunakan tipe yang ditentukan oleh percabangan tingkat tipe pada identitas (bukan representasi) dari tipe yang diberikan -- misalnya, menggunakan fungsi tipe atau tipe terkait (atau dalam Haskell, GADT). Karena tidak ada jaminan bahwa representasi dari tipe - tipe tersebut juga kompatibel.

Perhatikan bahwa masalah ini hanya mungkin karena penggunaan transmute yang tidak aman. Jika itu hanya menghasilkan kode boilerplate yang membosankan untuk membungkus/membuka tipe baru di mana-mana dan mengirimkan setiap metode ke implementasinya dari tipe dasar (seperti beberapa proposal delegasi otomatis untuk Rust IIRC?), maka hasil terburuk yang mungkin terjadi adalah tipe kesalahan atau mungkin ICE. Lagi pula, secara konstruksi, jika Anda tidak menggunakan kode yang tidak aman, Anda tidak akan mendapatkan hasil yang tidak aman. Demikian juga, jika kita membuat kode untuk semacam "passthrough enum otomatis", tetapi tidak menggunakan primitif unsafe untuk melakukannya, tidak akan ada bahaya.

(Saya tidak yakin apakah atau bagaimana ini berhubungan dengan pertanyaan awal saya tentang apakah ciri-ciri yang digunakan dengan impl Trait , dan/atau passthrough enum otomatis, karena kebutuhan harus aman-objek?)

@rpjohnst Seseorang dapat membuat opsi enum case untuk menandai biaya:

fn foo() -> enum impl Trait { ... }

Itu hampir pasti makanan untuk RFC yang berbeda.

@glaebhoerl ya saya menghabiskan beberapa waktu untuk menggali masalah ini dan merasa cukup yakin itu tidak akan menjadi masalah di sini, setidaknya.

Maaf jika itu sesuatu yang jelas tetapi saya mencoba memahami alasan mengapa impl Trait tidak dapat muncul dalam jenis metode sifat yang dikembalikan, atau apakah itu masuk akal sama sekali? Misalnya:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Ini benar-benar masuk akal, dan AFAIK tujuannya adalah untuk membuatnya berfungsi, tetapi belum diimplementasikan.

Ini semacam masuk akal, tapi itu tidak fitur dasar yang sama (ini telah dibahas banyak btw):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Secara khusus, impl Trait di impl Trait for Type tipe terkait RHS akan mirip dengan fitur yang diterapkan hari ini, karena tidak dapat dianggap sebagai Rust yang stabil, sedangkan di trait itu bisa.

Saya tahu ini mungkin terlambat, dan sebagian besar bikeshedding, tetapi apakah telah didokumentasikan di mana saja mengapa kata kunci impl diperkenalkan? Bagi saya sepertinya kita sudah memiliki cara dalam kode Rust saat ini untuk mengatakan "kompiler mengetahui tipe apa yang ada di sini", yaitu _ . Bisakah kita tidak menggunakan kembali ini di sini untuk memberikan sintaks:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Bukan itu yang dilakukan fitur, tipenya bukan yang dikembalikan dari fungsi, melainkan "pembungkus semantik" yang menyembunyikan semuanya kecuali API yang dipilih (dan OIBIT karena itu menyebalkan).

Kami dapat mengizinkan beberapa fungsi untuk menyimpulkan jenis dalam tanda tangan mereka dengan memaksa DAG, tetapi fitur seperti itu tidak pernah disetujui dan tidak mungkin ditambahkan ke Rust, karena akan menyentuh "inferensi global".

Sarankan penggunaan sintaks @Trait untuk menggantikan impl Trait , seperti yang disebutkan di sini .

Lebih mudah untuk memperluas ke posisi tipe lain dan dalam komposisi seperti Box<@MyTrait> atau &@MyTrait .

@Trait untuk any T where T: Trait dan ~Trait untuk some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

Dalam fn func(t: T) -> V , tidak perlu membedakan t atau beberapa v, jadi sebagai sifat.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

masih bekerja.

@JF-Liu Saya pribadi menentang any dan some digabungkan menjadi satu kata kunci/sigil tetapi Anda secara teknis benar bahwa kami dapat memiliki satu sigil dan menggunakannya seperti impl Trait RFC.

@JF-Liu @eddyb Ada alasan mengapa sigil dihapus dari bahasa. Mengapa alasan itu tidak berlaku untuk kasus ini?

@ juga digunakan dalam pencocokan pola, tidak dihapus dari bahasa.

Hal yang ada dalam pikiran saya adalah bahwa sigil AFAIK terlalu sering digunakan.

Syntax bikesheding: Saya sangat tidak senang dengan notasi impl Trait , karena menggunakan kata kunci (font tebal dalam editor) untuk memberi nama suatu jenis terlalu keras. Ingat pengamatan sintaks keras C struct dan Stroustroup (slide 14)?

Di https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 , @konstin menyarankan <Trait> . Ini terlihat sangat bagus, terutama di posisi input:

fn take_iterator(iterator: <Iterator<Item=i32>>)

Saya melihat bahwa itu akan sedikit bertentangan dengan UFCS, tetapi mungkin ini dapat diselesaikan?

Saya juga merasa menggunakan kurung sudut alih-alih sifat impl menjadi pilihan yang lebih baik, setidaknya dalam posisi tipe pengembalian, misalnya:

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait> sintaks konflik dengan obat generik, pertimbangkan:

Vec<<FnOnce() -> bool>> vs Vec<@FnOnce() -> bool>

Jika Vec<FnOnce() -> bool> diperbolehkan, maka <Trait> adalah ide yang bagus, ini menandakan kesetaraan dengan parameter tipe generik. Tetapi karena Box<Trait> berbeda dengan Box<@Trait> , harus melepaskan sintaks <Trait> .

Saya lebih suka sintaks kata kunci impl karena ketika Anda membaca dokumentasi dengan cepat, ini memungkinkan lebih sedikit cara untuk salah membaca prototipe.
Bagaimana menurut anda ?

Saya baru menyadari bahwa saya memang mengusulkan superset ke rfc ini di utas internal (Terima kasih untuk @matklad karena telah mengarahkan saya ke sini):

Izinkan sifat dalam parameter fungsi dan tipe pengembalian untuk digunakan dengan mengelilinginya dengan tanda kurung sudut seperti pada contoh berikut:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

Kompiler kemudian akan memonomorfisasi parameter menggunakan aturan yang sama yang saat ini diterapkan pada obat generik. Jenis kembalian misalnya dapat diturunkan dari implementasi fungsi. Ini berarti bahwa Anda tidak bisa begitu saja memanggil metode ini pada Box<Trait_with_transform> atau menggunakannya pada objek yang dikirim secara dinamis secara umum, tetapi itu masih akan membuat aturan lebih permisif. Saya belum membaca semua diskusi RFC, jadi mungkin sudah ada solusi yang lebih baik yang saya lewatkan.

Saya lebih suka sintaks kata kunci impl karena ketika Anda membaca dokumentasi dengan cepat, ini memungkinkan lebih sedikit cara untuk salah membaca prototipe.

Warna yang berbeda dalam penyorotan sintaks seharusnya berhasil.

Makalah oleh Stroustrup ini membahas pilihan sintaksis serupa untuk konsep C++ di bagian 7: http://www.stroustrup.com/good_concepts.pdf

Jangan gunakan sintaks yang sama untuk generik dan eksistensial. Mereka bukanlah hal yang sama. Generik memungkinkan pemanggil untuk memutuskan apa tipe konkretnya, sementara (subset terbatas ini) eksistensial memungkinkan fungsi dipanggil untuk memutuskan apa tipe konkretnya. Contoh ini:

fn transform(iter: <Iterator>) -> <Iterator>

harus setara dengan ini

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

atau seharusnya setara dengan ini

fn transform(iter: impl Iterator) -> impl Iterator

Contoh terakhir tidak akan dikompilasi dengan benar, bahkan pada malam hari, dan itu sebenarnya tidak dapat dipanggil dengan sifat iterator, tetapi sifat seperti FromIter akan memungkinkan pemanggil untuk membuat sebuah instance dan meneruskannya ke fungsi tanpa bisa untuk menentukan jenis konkret dari apa yang mereka lewati.

Mungkin sintaksnya harus serupa, tetapi tidak boleh sama.

Tidak perlu membedakan salah satu (generik) atau beberapa (eksistensial) dalam nama jenis, tergantung di mana jenis itu digunakan. Saat digunakan dalam variabel, argumen dan bidang struct selalu menerima salah satu dari T, ketika digunakan dalam tipe pengembalian fn selalu mendapatkan sebagian dari T.

  • gunakan Type , &Type , Box<Type> untuk tipe data konkret, pengiriman statis
  • gunakan @Trait , &@Trait , Box<@Trait> dan parameter tipe generik untuk tipe data abstrak, pengiriman statis
  • gunakan &Trait , Box<Trait> untuk tipe data abstrak, pengiriman dinamis

fn func(x: @Trait) setara dengan fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) dapat ditulis dengan sederhana sebagai fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T parameter masih diperlukan di fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } setara dengan struct Foo<T: Trait> { field: T } .

Saat digunakan dalam variabel, argumen dan bidang struct selalu menerima salah satu dari T, ketika digunakan dalam tipe pengembalian fn selalu mendapatkan sebagian dari T.

Anda dapat mengembalikan sifat apa pun, sekarang, di Rust yang stabil, menggunakan sintaks generik yang ada. Ini adalah fitur yang sangat banyak digunakan. serde_json::de::from_slice mengambil &[u8] sebagai parameter dan mengembalikan T where T: Deserialize .

Anda juga dapat mengembalikan beberapa Sifat secara bermakna, dan itulah fitur yang sedang kita diskusikan. Anda tidak dapat menggunakan eksistensial untuk fungsi deserialize, sama seperti Anda tidak dapat menggunakan obat generik untuk mengembalikan penutupan tanpa kotak. Mereka adalah fitur yang berbeda.

Untuk contoh yang lebih familiar, Iterator::collect dapat mengembalikan T where T: FromIterator<Self::Item> , menyiratkan notasi pilihan saya: fn collect(self) -> any FromIterator<Self::Item> .

Bagaimana dengan sintaksnya?
fn foo () -> _ : Trait { ... }
untuk nilai kembali dan
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
untuk parameter?

Bagi saya benar-benar tidak ada saran baru yang mendekati impl Trait dalam keanggunannya. impl adalah kata kunci yang sudah diketahui oleh setiap programmer karat dan karena digunakan untuk mengimplementasikan ciri-ciri, kata kunci itu sebenarnya menunjukkan apa yang dilakukan fitur itu sendiri.

Ya, bertahan dengan kata kunci yang ada tampaknya ideal bagi saya; Saya ingin melihat impl untuk eksistensial dan for untuk universal.

Saya pribadi menentang any dan some digabungkan menjadi satu kata kunci/sigil

@eddyb Saya tidak akan menganggapnya sebagai penggabungan. Ini mengikuti secara alami dari aturan:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Sunting: ini satu arah, bukan isomorfisme.


Tidak terkait: Apakah ada proposal terkait yang juga mengizinkan impl Trait di posisi kovarian lain seperti

~ karatfn foo(panggilan balik: F) -> Rdimana F: FnOnce(impl SomeTrait) -> R {panggilan balik(buat_sesuatu())}~

Saat ini , ini bukan fitur yang diperlukan, karena Anda selalu dapat menetapkan waktu yang konkret untuk impl SomeTrait , yang mengganggu keterbacaan tetapi sebaliknya bukan masalah besar.

Tetapi jika fitur RFC 1522 stabil, maka tidak mungkin untuk menetapkan tanda tangan tipe ke program seperti di atas jika create_something menghasilkan impl SomeTrait (setidaknya tanpa boxing). Saya pikir ini bermasalah.

@Rufflewind Di dunia nyata, hal-hal tidak begitu jelas, dan fitur ini adalah merek eksistensial yang sangat spesifik (Rust memiliki beberapa sekarang).

Tetapi meskipun demikian, yang Anda miliki hanyalah penggunaan kovarians untuk menentukan apa arti impl Trait di dalam dan di luar argumen fungsi.

Itu tidak cukup untuk:

  • menggunakan kebalikan dari default
  • disambiguasi di dalam tipe bidang (di mana any dan some sama-sama diinginkan)

@Rufflewind Sepertinya itu tanda kurung yang salah untuk apa impl Trait itu. Saya tahu Haskell memanfaatkan hubungan ini untuk hanya menggunakan kata kunci forall untuk mewakili universal dan eksistensial, tetapi itu tidak berhasil dalam konteks yang sedang kita diskusikan.

Ambil definisi ini, misalnya:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Jika kita menggunakan aturan bahwa " impl dalam argumen bersifat universal, impl dalam tipe pengembalian adalah eksistensial", maka tipe dari tipe item fungsi foo secara logika adalah ini (dalam notasi tipe yang dibuat-buat):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

Secara naif memperlakukan impl secara teknis hanya berarti universal atau hanya berarti eksistensial dan membiarkan logika bekerja sendiri sebenarnya tidak berhasil. Anda akan mendapatkan ini:

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Atau ini:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Dan tak satu pun dari ini mengurangi apa yang kita inginkan dengan aturan logis. Jadi akhirnya any / some melakukan capture perbedaan penting Anda tidak bisa menangkap dengan satu kata kunci. Bahkan ada contoh yang masuk akal di std mana Anda ingin universal dalam posisi kembali. Misalnya, metode Iterator ini:

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

Dan tidak ada cara untuk menulisnya dengan impl dan aturan argumen/pengembalian.

tl;dr memiliki impl kontekstual menunjukkan baik universal atau eksistensial benar-benar memberikan dua arti yang berbeda.


Untuk referensi, dalam notasi saya, hubungan forall/exists @Rufflewind yang disebutkan terlihat seperti:

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

Yang terkait dengan konsep objek sifat (eksistensial) yang setara dengan generik (universal), tetapi tidak dengan pertanyaan impl Trait .

Yang mengatakan, saya tidak terlalu mendukung any / some lagi. Saya ingin tepat tentang apa yang sedang kita bicarakan, dan any / some memiliki keindahan teoretis dan visual ini, tetapi saya akan baik-baik saja dengan menggunakan impl dengan kontekstual aturan. Saya pikir ini mencakup semua kasus umum, menghindari masalah tata bahasa kata kunci kontekstual, dan kami dapat beralih ke parameter tipe bernama untuk sisanya.

Pada catatan itu, untuk mencocokkan generalitas penuh dari universal, saya pikir kita pada akhirnya akan membutuhkan sintaks untuk eksistensial bernama, yang memungkinkan klausa di mana sewenang-wenang dan kemampuan untuk menggunakan eksistensial yang sama di banyak tempat dalam tanda tangan.

Singkatnya, saya akan senang dengan:

  • impl Trait sebagai singkatan untuk universal dan eksistensial (secara kontekstual).
  • Dinamakan parameter tipe sebagai tulisan tangan yang sepenuhnya umum untuk universal dan eksistensial. (Lebih jarang diperlukan.)

Secara naif memperlakukan impl secara teknis hanya berarti universal atau hanya makna eksistensial dan membiarkan logika bekerja dengan sendirinya tidak benar-benar berhasil. Anda akan mendapatkan ini:

@solson Bagi saya, terjemahan "naif" akan menghasilkan tepat di sebelah jenis yang dikuantifikasi. Karena itu

~ karat(tersirat MyTrait)~

hanya gula sintaksis untuk

~ karat(adaT)~

yang merupakan transformasi lokal sederhana. Jadi, terjemahan naif yang mematuhi aturan “ impl selalu merupakan eksistensial” akan menghasilkan:

~ karatfn(adaT) -> (adaR)~

Kemudian, jika Anda menarik quantifier keluar dari argumen fungsi, itu menjadi

~ karatuntukfn(T) -> (adaR)~

Jadi meskipun T selalu eksistensial relatif terhadap dirinya sendiri, ia muncul sebagai relatif universal terhadap seluruh tipe fungsi.


IMO, saya pikir impl mungkin juga menjadi kata kunci de facto untuk tipe eksistensial. Di masa depan, mungkin seseorang dapat membangun tipe eksistensial yang lebih rumit seperti:

~~ karat(tersirat(Vec, T))~ ~

dalam analogi dengan tipe universal (melalui HRTB)

~ karat(untuk<'a> FnOnce(&'a T))~

@Rufflewind Tampilan itu tidak berfungsi karena fn(T) -> (exists<R: ReturnTrait>(R)) secara logis tidak setara dengan exists<R: ReturnTrait>(fn(T) -> R) , yang sebenarnya berarti tipe pengembalian impl Trait .

(Setidaknya tidak dalam logika konstruktif yang biasanya diterapkan pada sistem tipe, di mana saksi spesifik yang dipilih untuk suatu eksistensial relevan. Yang pertama menyiratkan bahwa fungsi dapat memilih tipe yang berbeda untuk dikembalikan berdasarkan, katakanlah, argumen, sedangkan yang kedua menyiratkan ada satu tipe spesifik untuk semua pemanggilan fungsi, seperti yang terjadi pada impl Trait .)

Saya merasa bahwa kami juga menjadi agak jauh. Saya pikir impl kontekstual adalah kompromi yang baik untuk dibuat, dan saya tidak berpikir mencapai pembenaran semacam ini diperlukan atau sangat membantu (kami tentu saja tidak akan mengajarkan aturan dalam hal koneksi logis semacam ini ).

@solson Ya Anda benar: eksistensial tidak dapat melayang keluar. Yang ini tidak berlaku secara umum:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

sedangkan ini berlaku secara umum:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

Yang terakhir bertanggung jawab untuk interpretasi ulang eksistensial dalam argumen sebagai obat generik.

Edit: Ups, (∀A. g(A) → T) → (∃A. g(A)) → T tidak ditahan.

Saya telah memposting RFC dengan proposal terperinci untuk memperluas dan menstabilkan impl Trait . Ini mengacu pada banyak diskusi tentang ini dan utas sebelumnya.

Perlu dicatat bahwa https://github.com/rust-lang/rfcs/pull/1951 telah diterima.

Apa statusnya saat ini? Kami memiliki RFC yang mendarat, kami memiliki orang-orang yang menggunakan implementasi awal, tetapi saya tidak jelas tentang item apa yang harus dilakukan.

Ditemukan di #43869 bahwa fungsi -> impl Trait tidak mendukung badan divergen murni:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

Apakah ini diharapkan (karena ! tidak menyiratkan Iterator ), atau dianggap sebagai bug?

Bagaimana dengan mendefinisikan tipe yang disimpulkan, yang tidak hanya dapat digunakan sebagai nilai pengembalian, tetapi sebagai apa pun (saya kira) suatu tipe dapat digunakan untuk saat ini?
Sesuatu seperti:
type Foo: FnOnce() -> f32 = #[infer];
Atau dengan kata kunci:
infer Foo: FnOnce() -> f32;

Tipe Foo kemudian dapat digunakan sebagai tipe pengembalian, tipe parameter, atau apa pun yang dapat digunakan tipe tersebut, tetapi akan ilegal untuk menggunakannya di dua tempat berbeda yang memerlukan tipe berbeda, bahkan jika itu type mengimplementasikan FnOnce() -> f32 dalam kedua kasus. Misalnya, berikut ini tidak akan dikompilasi:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Ini tidak boleh dikompilasi karena meskipun tipe pengembalian dari return_closure dan return_closure2 keduanya FnOnce() -> f32 , tipenya sebenarnya berbeda, karena tidak ada dua penutupan yang memiliki tipe yang sama di Rust . Agar hal di atas dapat dikompilasi, Anda perlu mendefinisikan dua jenis kesimpulan yang berbeda:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Saya pikir apa yang terjadi di sini cukup jelas setelah melihat kodenya, bahkan jika Anda tidak tahu sebelumnya apa yang dilakukan kata kunci infer, dan itu sangat fleksibel.

Kata kunci infer (atau makro) pada dasarnya akan memberi tahu kompiler untuk mencari tahu apa jenisnya, berdasarkan di mana ia digunakan. Jika kompiler tidak dapat menyimpulkan jenisnya, itu akan menimbulkan kesalahan, ini bisa terjadi ketika tidak ada informasi yang cukup untuk mempersempit jenisnya (jika jenis yang disimpulkan tidak digunakan di mana pun, misalnya, meskipun mungkin lebih baik untuk membuat kasus tertentu sebagai peringatan), atau ketika tidak mungkin menemukan jenis yang cocok di mana pun digunakan (seperti pada contoh di atas).

@cramertj Ahh jadi itu sebabnya masalah ini menjadi begitu sunyi..

Jadi, @cramertj bertanya kepada saya tentang bagaimana menurut saya yang terbaik untuk menyelesaikan masalah wilayah batas akhir yang mereka temui dalam PR mereka. Pendapat saya adalah bahwa kita mungkin ingin "memperlengkapi kembali" implementasi kita sedikit untuk mencoba dan menantikan model anonymous type Foo .

Untuk konteksnya, idenya kira-kira seperti itu

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

akan (semacam) didesugasi menjadi sesuatu seperti ini

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Perhatikan bahwa dalam formulir ini, Anda dapat melihat parameter generik mana yang ditangkap karena muncul sebagai argumen untuk Foo -- khususnya, 'b tidak ditangkap, karena tidak muncul dalam referensi sifat di bagaimanapun, tetapi parameter tipe T dan U selalu demikian.

Bagaimanapun, saat ini di kompiler, ketika Anda memiliki referensi impl Debug , kami membuat def-id yang -- secara efektif -- mewakili jenis anonim ini. Kemudian kita memiliki kueri generics_of , yang menghitung parameter generiknya. Saat ini, ini mengembalikan sama dengan konteks "melampirkan" -- yaitu, fungsi foo . Inilah yang ingin kami ubah.

Di "sisi lain", yaitu, di tanda tangan foo , kami mewakili impl Foo sebagai TyAnon . Ini pada dasarnya benar -- TyAnon mewakili referensi ke Foo yang kita lihat dalam desugaring di atas. Tetapi cara kita mendapatkan "subst" untuk tipe ini adalah dengan menggunakan fungsi "identity" , yang jelas-jelas salah -- atau setidaknya tidak menggeneralisasi.

Jadi khususnya ada semacam "pelanggaran namespace" yang terjadi di sini. Saat kita menghasilkan subst "identitas" untuk suatu item, yang biasanya memberi kita substitusi yang akan kita gunakan saat memeriksa jenis item itu -- yaitu, dengan semua parameter generiknya dalam cakupan. Tetapi dalam kasus ini, kami membuat referensi ke Foo yang muncul di dalam fungsi foo() , dan jadi kami ingin memiliki parameter generik foo() muncul di Substs , bukan Foo . Ini berhasil karena saat ini keduanya adalah satu dan sama, tetapi itu tidak sepenuhnya benar .

Saya pikir apa yang harus kita lakukan adalah seperti ini:

Pertama, ketika kita menghitung parameter tipe generik dari Foo (yaitu, tipe anonim itu sendiri), kita akan mulai membuat set generik baru. Tentu itu akan mencakup jenisnya. Tetapi untuk seumur hidup, kami akan melewati batas sifat dan mengidentifikasi setiap wilayah yang muncul di dalamnya. Itu sangat mirip dengan kode yang ada yang ditulis cramertj , kecuali kami tidak ingin mengakumulasi def-id, karena tidak semua wilayah dalam cakupan memiliki def-id.

Saya pikir apa yang ingin kami lakukan adalah mengumpulkan kumpulan wilayah yang muncul dan menempatkannya dalam urutan tertentu, dan juga melacak nilai untuk wilayah tersebut dari sudut pandang foo() . Agak menjengkelkan untuk melakukan ini, karena kami tidak memiliki struktur data seragam yang mewakili wilayah logis. (Kami dulu memiliki gagasan FreeRegion , yang hampir berhasil, tetapi kami tidak lagi menggunakan FreeRegion untuk barang yang terikat awal, hanya untuk barang yang terikat akhir.)

Mungkin opsi termudah dan terbaik adalah dengan hanya menggunakan Region<'tcx> , tetapi Anda harus menggeser kedalaman indeks debruijn saat Anda pergi ke "membatalkan" semua pengikat yang diperkenalkan. Ini mungkin pilihan terbaik sekalipun.

Jadi pada dasarnya saat kita mendapatkan callback di visit_lifetime , kita akan mengubahnya menjadi Region<'tcx> diekspresikan di kedalaman awal (kita harus melacak saat melewati binder). Kami akan mengakumulasikannya menjadi vektor, menghilangkan duplikat.

Setelah selesai, kami memiliki dua hal:

  • Pertama, untuk setiap region dalam vektor, kita perlu membuat parameter region generik. Mereka semua dapat memiliki nama anonim atau apa pun, itu tidak masalah (walaupun mungkin kita membutuhkan mereka untuk memiliki def-id atau sesuatu...? Saya harus melihat struktur data RegionParameterDef ...) .
  • Kedua, daerah dalam vektor juga merupakan hal yang ingin kita gunakan untuk "pengganti".

Oke, maaf jika itu samar. Saya tidak tahu bagaimana mengatakannya dengan lebih jelas. Sesuatu yang saya tidak yakin -- saat ini, saya merasa penanganan wilayah kami cukup rumit, jadi mungkin ada cara untuk memperbaiki hal-hal agar lebih seragam? Saya berani bertaruh $10 bahwa @eddyb memiliki beberapa pemikiran di sini. ;)

@nikomatsakis Saya percaya banyak yang mirip dengan apa yang saya katakan @cramertj , tetapi lebih disempurnakan!

Saya telah memikirkan tentang impl Trait eksistensial dan saya menemukan kasus aneh di mana saya pikir kita harus melanjutkan dengan hati-hati. Pertimbangkan fungsi ini:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Karena Anda dapat memvalidasi saat bermain , kode ini dikompilasi hari ini. Namun, jika kita menggali apa yang terjadi, itu menyoroti sesuatu yang memiliki bahaya "kecocokan ke depan" yang menjadi perhatian saya.

Secara khusus, jelas bagaimana kami menyimpulkan jenis yang dikembalikan di sini ( () ). Kurang jelas bagaimana kita menyimpulkan jenis parameter impl Debug . Artinya, Anda dapat menganggap nilai pengembalian ini sebagai sesuatu seperti -> ?T mana ?T: Foo<?U> . Kita harus menyimpulkan nilai ?T dan ?U hanya berdasarkan fakta bahwa ?T = () .

Saat ini, kami melakukan ini dengan memanfaatkan fakta bahwa hanya ada satu impl. Namun, ini adalah properti yang rapuh. Jika impl baru ditambahkan, kode tidak akan dikompilasi lagi , karena sekarang kita tidak dapat secara unik menentukan ?U harus menjadi apa.

Ini dapat terjadi dalam banyak skenario di Rust -- yang cukup mengkhawatirkan, tetapi ortogonal -- tetapi ada sesuatu yang berbeda tentang kasus impl Trait . Dalam kasus impl Trait , kami tidak memiliki cara bagi pengguna untuk menambahkan anotasi jenis untuk memandu inferensi! Kami juga tidak benar-benar memiliki rencana seperti itu. Satu-satunya solusi adalah mengubah antarmuka fn menjadi impl Foo<()> atau sesuatu yang eksplisit lainnya.

Di masa depan, menggunakan abstract type , orang dapat membayangkan mengizinkan pengguna untuk secara eksplisit memberikan nilai tersembunyi (atau mungkin hanya petunjuk yang tidak lengkap, menggunakan _ ), yang kemudian dapat membantu inferensi, sambil menjaga antarmuka publik yang sama

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Namun, saya pikir akan lebih bijaksana untuk menghindari menstabilkan penggunaan "bersarang" dari Sifat impl eksistensial, kecuali untuk binding tipe terkait (misalnya, impl Iterator<Item = impl Debug> tidak mengalami ambiguitas ini).

Dalam hal Sifat impl, kami tidak memiliki cara bagi pengguna untuk menambahkan anotasi jenis untuk memandu inferensi! Kami juga tidak benar-benar memiliki rencana seperti itu.

Mungkin itu bisa terlihat seperti UFCS? misalnya <() as Foo<()>> -- tidak mengubah tipe seperti as , hanya disambiguasi. Sintaks ini saat ini tidak valid karena mengharapkan :: dan lebih banyak lagi untuk mengikuti.

Saya baru saja menemukan kasus menarik mengenai inferensi tipe dengan sifat impl untuk Fn :
Kode berikut dikompilasi dengan baik :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

Jika kami mengomentari Sub-baris, kesalahan kompilasi muncul :

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Ini sepertinya tidak terkait dengan impl Trait -- memang benar inferensi secara umum. Coba sedikit modifikasi contoh Anda ini:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

Sepertinya ini ditutup sekarang:

ICE saat berinteraksi dengan elision

Satu hal yang saya tidak melihat tercantum dalam masalah ini atau dalam diskusi adalah kemampuan untuk menyimpan penutupan dan generator - yang tidak disediakan oleh pemanggil - di bidang struct. Saat ini, ini mungkin tetapi terlihat jelek: Anda harus menambahkan parameter tipe ke struct untuk setiap bidang penutupan/generator, dan kemudian di tanda tangan fungsi konstruktor, ganti parameter tipe itu dengan impl FnMut/impl Generator . Ini contohnya , dan berhasil, yang sangat keren! Tapi itu meninggalkan banyak hal yang diinginkan. Akan jauh lebih baik jika Anda bisa menyingkirkan parameter type:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait mungkin bukan cara yang tepat untuk melakukan ini – mungkin tipe abstrak, jika saya telah membaca dan memahami RFC 2071 dengan benar. Yang kita butuhkan adalah sesuatu yang dapat kita tulis dalam definisi struct sehingga tipe sebenarnya ( [generator@src/main.rs:15:17: 21:10 _] ) dapat disimpulkan.

@mikeyhew tipe abstrak memang akan menjadi cara yang kami harapkan untuk bekerja, saya percaya. Sintaksnya kira-kira akan terlihat seperti

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

Apakah ada jalur mundur jika itu adalah impl Generator yang ingin saya masukkan ke dalam struct saya, tetapi mereka tidak membuat abstract type untuk saya gunakan?

@scottmcm Anda masih dapat mendeklarasikan abstract type :

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Tunggu, tipe abstrak sudah ada di malam hari?! Mana PRnya?

@alexreg Tidak, mereka tidak.

Sunting: Salam, pengunjung dari masa depan! Masalah di bawah ini telah diselesaikan.


Saya ingin memperhatikan kasus penggunaan edge yang funky ini yang muncul di #47348

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

Haruskah mengembalikan proyeksi pada impl Trait seperti ini bahkan diizinkan? (karena saat ini, __itu.__)

Saya tidak dapat menemukan diskusi tentang penggunaan seperti ini, saya juga tidak dapat menemukan kasus uji untuk itu.

@ExpHP Hmm. Tampaknya bermasalah, untuk alasan yang sama dengan impl Foo<impl Bar> bermasalah. Pada dasarnya, kami tidak memiliki batasan nyata pada jenis yang dimaksud -- hanya pada hal-hal yang diproyeksikan darinya.

Saya pikir kami ingin menggunakan kembali logika di sekitar "parameter tipe terbatas" dari impls. Singkatnya, menentukan tipe pengembalian harus "membatasi" impl Sub . Fungsi yang saya maksud adalah yang ini:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Sedikit triase untuk orang yang menyukai kotak centang:

  • #46464 selesai -> kotak centang
  • #48072 selesai -> kotak centang

@rfcbot menggabungkan

Saya mengusulkan agar kami menstabilkan fitur conservative_impl_trait dan universal_impl_trait , dengan satu perubahan yang tertunda (perbaikan untuk https://github.com/rust-lang/rust/issues/46541).

Tes yang mendokumentasikan semantik saat ini

Tes untuk fitur ini dapat ditemukan di direktori berikut:

run-pass/impl-sifat
ui/sifat-impl
kompilasi-gagal/impl-sifat

Pertanyaan Terselesaikan Selama Implementasi

Detail penguraian impl Trait diselesaikan di RFC 2250 dan diimplementasikan di https://github.com/rust-lang/rust/pull/45294.

impl Trait telah dicekal dari posisi tipe nested-non-associated dan posisi jalur tertentu yang memenuhi syarat untuk mencegah ambiguitas. Ini diterapkan di https://github.com/rust-lang/rust/pull/48084.

Sisa Fitur Tidak Stabil

Setelah stabilisasi ini, akan dimungkinkan untuk menggunakan impl Trait dalam posisi argumen dan posisi pengembalian fungsi non-sifat. Namun, penggunaan impl Trait di mana saja dalam sintaks Fn masih dilarang untuk memungkinkan iterasi desain di masa mendatang. Selain itu, secara manual menentukan parameter tipe fungsi yang menggunakan impl Trait dalam posisi argumen tidak diperbolehkan.

Anggota tim @cramertj telah mengusulkan untuk menggabungkan ini. Langkah selanjutnya adalah peninjauan oleh sisa tim yang ditandai:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

Tidak ada kekhawatiran saat ini terdaftar.

Setelah mayoritas pengulas menyetujui (dan tidak ada yang keberatan), ini akan memasuki periode komentar terakhirnya. Jika Anda menemukan masalah besar yang belum diangkat pada titik mana pun dalam proses ini, silakan angkat bicara!

Lihat dokumen ini untuk info tentang perintah apa yang dapat diberikan oleh anggota tim yang ditandai kepada saya.

Setelah stabilisasi ini, akan dimungkinkan untuk menggunakan Sifat impl dalam posisi argumen dan posisi kembali dari fungsi non-sifat. Namun, penggunaan Sifat impl di mana saja dalam sintaks Fn masih dilarang untuk memungkinkan iterasi desain di masa mendatang. Selain itu, secara manual menentukan parameter tipe fungsi yang menggunakan Sifat impl dalam posisi argumen tidak diperbolehkan.

Apa status menggunakan impl Trait dalam argumen posisi / kembali dalam fungsi sifat, atau sintaks Fn, dalam hal ini?

@alexreg Return-position impl Trait dalam sifat diblokir pada RFC, meskipun RFC 2071 akan memungkinkan fungsionalitas serupa setelah diterapkan. Posisi argumen impl Trait dalam sifat tidak diblokir pada fitur teknis apa pun yang saya ketahui, tetapi tidak diizinkan secara eksplisit di RFC sehingga untuk saat ini dihilangkan.

impl Trait dalam posisi argumen sintaks Fn diblokir pada HRTB tingkat tipe, karena beberapa orang berpikir bahwa T: Fn(impl Trait) harus diubah menjadi T: for<X: Trait> Fn(X) . impl Trait di posisi pengembalian sintaks Fn tidak diblokir karena alasan teknis apa pun yang saya ketahui, tetapi tidak diizinkan dalam RFC sambil menunggu pekerjaan desain lebih lanjut-- saya harapkan lihat RFC lain atau setidaknya FCP terpisah sebelum menstabilkan ini.

@cramertj Oke, terima kasih atas pembaruannya. Mudah-mudahan kita bisa melihat dua fitur ini yang tidak diblokir pada apa pun mendapatkan lampu hijau segera, setelah beberapa diskusi. Desugaring masuk akal, dalam posisi argumen, argumen foo: T mana T: Trait setara dengan foo: impl Trait , kecuali saya salah.

Kekhawatiran: https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 masih sama. Apakah mungkin untuk mengizinkan yang berikut ini?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm Tidak, itu tidak mungkin saat ini. Fungsi itu mengembalikan ! , yang tidak mengimplementasikan sifat yang Anda berikan, kami juga tidak memiliki mekanisme untuk mengubahnya menjadi tipe yang sesuai. Ini sangat disayangkan, tetapi tidak ada cara mudah untuk memperbaikinya sekarang (selain menerapkan lebih banyak sifat untuk ! ). Ini juga kompatibel ke belakang untuk diperbaiki di masa mendatang, karena membuatnya berfungsi akan memungkinkan lebih banyak kode untuk dikompilasi.

Pertanyaan tentang turbofish baru selesai setengahnya. Setidaknya kita harus memperingatkan impl Trait dalam argumen fungsi publik yang efektif, mengingat impl Trait dalam argumen menjadi tipe pribadi untuk private baru

Motivasinya adalah untuk mencegah lib merusak turbofish pengguna dengan mengubah argumen dari generik eksplisit menjadi impl Trait . Kami belum memiliki panduan referensi yang baik untuk lib untuk mengetahui apa yang merupakan dan bukan perubahan yang melanggar dan sangat tidak mungkin bahwa tes akan menangkap ini. Masalah ini tidak cukup dibahas, jika kita ingin menstabilkan sebelum memutuskan sepenuhnya, setidaknya kita harus mengarahkan senjata menjauh dari kaki penulis lib.

Motivasinya adalah untuk mencegah lib merusak turbofishes pengguna dengan mengubah argumen dari generik eksplisit menjadi impl Trait .

Saya berharap ketika ini mulai terjadi dan orang-orang mulai mengeluh lang-team orang-orang yang saat ini ragu-ragu akan diyakinkan bahwa impl Trait harus mendukung secara eksplisit menyediakan argumen tipe dengan turbofish.

@leodasvacas

Pertanyaan tentang turbofish baru selesai setengahnya. Kita setidaknya harus memperingatkan tentang Sifat impl dalam argumen fungsi publik yang efektif, mengingat Sifat impl dalam argumen menjadi tipe pribadi untuk pribadi baru dalam pemeriksaan publik.

Saya tidak setuju - ini telah diselesaikan. Kami tidak mengizinkan turbofish untuk fungsi ini sepenuhnya untuk saat ini. Mengubah tanda tangan fungsi publik untuk menggunakan impl Trait alih-alih parameter generik eksplisit adalah perubahan yang melanggar.

Jika kami mengizinkan turbofish untuk fungsi-fungsi ini di masa mendatang, kemungkinan itu hanya akan mengizinkan penentuan parameter tipe non- impl Trait .

:bell: Ini sekarang memasuki periode komentar terakhir , sesuai ulasan di atas . :lonceng:

Saya harus menambahkan bahwa saya tidak ingin menstabilkan sampai https://github.com/rust-lang/rust/pull/49041 mendarat. (Tapi mudah-mudahan itu akan segera terjadi.)

Jadi #49041 berisi perbaikan untuk #46541, tetapi perbaikan itu memiliki dampak yang lebih besar daripada yang saya perkirakan -- misalnya, kompiler tidak melakukan bootstrap sekarang -- dan ini memberi saya jeda tentang kursus yang benar di sini. Masalah di #49041 adalah bahwa kita dapat secara tidak sengaja membiarkan masa hidup bocor keluar yang seharusnya tidak kita lakukan. Berikut adalah bagaimana ini bermanifestasi dalam kompiler. Kami mungkin memiliki metode seperti ini:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

Kuncinya di sini adalah bahwa TyCtxt adalah invarian w/r/t 'tcx dan 'gcx , jadi mereka harus muncul dalam tipe pengembalian. Namun hanya 'cx dan 'tcx muncul di batas sifat impl, jadi hanya dua masa hidup yang seharusnya "ditangkap". Kompiler lama menerima ini karena 'gcx: 'cx , tetapi itu tidak sepenuhnya benar jika Anda memikirkan desugaring yang ada dalam pikiran kami. Desugaring itu akan membuat tipe abstrak seperti ini:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

namun nilai untuk tipe abstrak ini adalah Baz<'cx, 'gcx, 'tcx> -- tetapi 'gcx tidak ada dalam cakupan!

Solusinya di sini adalah kita harus memberi nama 'gcx di dalam batas. Ini agak menjengkelkan untuk dilakukan; kami tidak dapat menggunakan 'cx + 'gcx . Saya kira kita bisa membuat sifat dummy:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

dan kemudian kembalikan sesuatu seperti ini impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Sesuatu yang saya lupa perhatikan: jika tipe pengembalian yang dideklarasikan adalah dyn Iterator<Item = &'tcx Foo> + 'cx , itu tidak masalah, karena tipe dyn diharapkan menghapus masa pakai. Oleh karena itu saya tidak percaya ada ketidaksehatan yang mungkin terjadi di sini, dengan anggapan Anda tidak dapat melakukan sesuatu yang bermasalah dengan impl Trait yang tidak akan mungkin dilakukan dengan dyn Trait .

Orang bisa samar-samar membayangkan gagasan bahwa nilai tipe abstrak adalah eksistensial yang serupa: exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

Namun bagi saya tampaknya tidak masalah untuk menstabilkan subset konservatif (yang mengesampingkan fns di atas) dan meninjau kembali ini sebagai kemungkinan perluasan nanti, setelah kami memutuskan bagaimana kami ingin memikirkannya.

PEMBARUAN: Untuk memperjelas maksud saya tentang sifat dyn : Saya mengatakan bahwa mereka dapat "menyembunyikan" seumur hidup seperti 'gcx selama terikat ( 'cx , di sini) memastikan bahwa 'gcx akan tetap aktif dimanapun dyn Trait digunakan.

@nikomatsakis Itu adalah contoh yang menarik, tetapi saya tidak merasa itu mengubah kalkulus dasar di sini, yaitu bahwa kami ingin semua masa hidup yang relevan menjadi jelas dari tipe pengembalian saja.

Sifat Captures sepertinya merupakan pendekatan yang baik dan ringan untuk situasi ini. Sepertinya itu bisa masuk ke std::marker sebagai tidak stabil untuk saat ini?

@nikomatsakis Komentar tindak lanjut Anda membuat saya sadar bahwa saya belum mengumpulkan semua bagian di sini untuk memahami mengapa Anda mungkin berharap untuk menghindari 'gcx dalam kasus ini, yaitu 'gcx bukan "seumur hidup yang relevan" dari sudut pandang klien. Bagaimanapun, memulai konservatif tampaknya baik-baik saja.

Pendapat pribadi saya adalah bahwa https://github.com/rust-lang/rust/issues/46541 sebenarnya bukan bug-- itu adalah perilaku yang saya harapkan, saya tidak melihat bagaimana itu bisa dibuat tidak sehat, dan itu menyakitkan untuk bekerja di sekitar. IMO seharusnya memungkinkan untuk mengembalikan tipe yang mengimplementasikan Trait dan hidup lebih lama dari 'a sebagai impl Trait + 'a , tidak peduli apa masa pakai lainnya yang dikandungnya. Namun, saya baik-baik saja dengan menstabilkan pendekatan yang lebih konservatif untuk memulai jika itu yang disukai @rust-lang/lang.

(Satu hal lagi yang perlu diklarifikasi: satu-satunya saat Anda akan mendapatkan kesalahan dengan perbaikan di #49041 adalah ketika tipe tersembunyi invarian sehubungan dengan masa pakai yang hilang 'gcx , jadi ini mungkin relatif jarang menyerang.)

@cramertj

Pendapat pribadi saya adalah bahwa #46541 sebenarnya bukan bug-- itu adalah perilaku yang saya harapkan, saya tidak melihat bagaimana hal itu dapat dibuat tidak sehat, dan sulit untuk mengatasinya.

Saya bersimpati pada POV itu, tetapi saya enggan untuk menstabilkan sesuatu yang kita tidak mengerti bagaimana mendesugarnya (misalnya, karena tampaknya bergantung pada beberapa gagasan samar tentang masa hidup eksistensial).

@rfcbot menyangkut banyak situs pengembalian

Saya ingin mendaftarkan satu perhatian terakhir dengan sifat impl eksistensial. Sebagian besar waktu saya ingin menggunakan sifat impl, saya sebenarnya ingin mengembalikan lebih dari satu jenis. Sebagai contoh:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Tentu saja, ini tidak bekerja hari ini, dan itu pasti di luar jangkauan untuk membuatnya bekerja. Namun, cara yang impl sifat bekerja sekarang, kita secara efektif menutup pintu untuk itu untuk bekerja pernah (dengan sintaks yang). Ini karena -- saat ini -- Anda dapat mengumpulkan batasan dari beberapa situs pengembalian:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Di sini, tipe yang disimpulkan adalah (i32, bool) , di mana return membatasi bagian i32 , dan return membatasi bagian bool .

Ini menyiratkan bahwa kami tidak akan pernah dapat mendukung kasus di mana dua pernyataan return tidak bersatu (seperti dalam contoh pertama saya) -- atau jika tidak, akan sangat menjengkelkan untuk melakukannya.

Saya ingin tahu apakah kita harus memasukkan batasan yang mengharuskan setiap return (secara umum, setiap sumber batasan) harus ditentukan sepenuhnya secara independen? (Dan kami menyatukan mereka setelah fakta?)

Ini akan membuat contoh kedua saya ilegal, dan meninggalkan ruang bagi kita untuk berpotensi mendukung kasus pertama di beberapa titik di masa depan.

@rfcbot menyelesaikan beberapa situs pengembalian

Jadi saya berbicara sedikit dengan @cramertj di #rust-lang . Kami sedang mendiskusikan ide untuk membuat "pengembalian awal" tidak stabil untuk impl Trait , sehingga pada akhirnya kami dapat mengubahnya.

Mereka menyatakan bahwa akan lebih baik untuk memiliki keikutsertaan eksplisit untuk sintaks semacam ini, khususnya karena ada kasus lain (misalnya, let x: impl Trait = if { ... } else { ... } ) di mana seseorang menginginkannya, dan kami tidak dapat mengharapkannya untuk menangani semuanya secara implisit (pasti tidak).

Saya menemukan ini cukup persuasif. Sebelum ini, saya berasumsi kita akan memiliki beberapa sintaks opt-in di sini, tapi saya hanya ingin memastikan kita tidak menutup pintu sebelum waktunya. Lagi pula, menjelaskan kapan Anda perlu memasukkan "shim dinamis" agak rumit.

@nikomatsakis Hanya pendapat saya yang mungkin kurang informasi: Meskipun mengaktifkan fungsi untuk mengembalikan salah satu dari beberapa jenis yang mungkin pada saat run-time dapat bermanfaat, saya akan enggan untuk memiliki sintaks yang sama untuk kedua inferensi tipe pengembalian statis ke satu tipe, dan memungkinkan situasi di mana beberapa keputusan run-time diperlukan secara internal (apa yang baru saja Anda sebut "shim dinamis").

Contoh foo , sejauh yang saya mengerti masalahnya, dapat diselesaikan menjadi (1) kotak + terhapus tipe Iterator<Item = u32> , atau (2) jumlah tipe std::option::Iter atau std::slice::Iter , yang pada gilirannya akan menurunkan implementasi Iterator . Mencoba untuk membuatnya singkat, karena ada beberapa pembaruan pada diskusi (yaitu, saya membaca log IRC sekarang) dan semakin sulit untuk diambil: Saya pasti akan setuju dengan sintaks seperti dyn untuk shim dinamis, meskipun saya juga mengerti bahwa menyebutnya dyn mungkin tidak ideal.

Steker tak tahu malu dan catatan kecil hanya sebagai catatan: Anda bisa mendapatkan jenis dan produk jumlah "anonim" dengan mudah dengan:

@Centril Ya, hal-hal dari frunk itu sangat keren. Namun, perhatikan bahwa agar CoprodInjector::inject berfungsi, tipe yang dihasilkan harus dapat disimpulkan, yang biasanya tidak mungkin tanpa memberi nama tipe yang dihasilkan (mis. -> Coprod!(A, B, C) ). Sering kali Anda bekerja dengan tipe yang tidak dapat dinamai, jadi Anda memerlukan -> Coprod!(impl Trait, impl Trait, impl Trait) , yang akan gagal dalam inferensi karena tidak tahu varian mana yang harus berisi tipe impl Trait .

@cramertj Sangat benar (catatan tambahan: setiap "varian" mungkin tidak sepenuhnya tidak dapat disebutkan namanya, tetapi hanya sebagian, misalnya: Map<Namable, Unnameable> ).

Ide enum impl Trait telah dibahas sebelumnya di https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695

@Centril Ya, itu benar. Saya secara khusus memikirkan masa depan, di mana saya sering menulis hal-hal seperti

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj Saya tidak akan mengatakan enum anonim mirip dengan enum impl Trait , karena kami tidak dapat menyimpulkan X: Tr && Y: Tr (X|Y): Tr (contoh penghitung: Default sifat impl Future for (X|Y|Z|...) .

@kennytm Agaknya kami ingin membuat otomatis beberapa sifat impls untuk enum anonim, jadi sepertinya pada dasarnya fitur yang sama.

@cramertj Karena enum anonim dapat diberi nama (heh), jika Default impl dihasilkan untuk (i32|String) , kita akan dapat menulis <(i32|String)>::default() . OTOH <enum impl Default>::default() tidak akan dikompilasi, jadi apa pun yang kami buat secara otomatis, itu akan tetap aman karena tidak dapat dipanggil sama sekali.

Namun demikian, ada beberapa kasus di mana pembuatan otomatis masih dapat menyebabkan masalah dengan enum impl Trait . Mempertimbangkan

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

Sangat normal bahwa, jika kita memiliki mut rng: (XorShiftRng|IsaacRng) kita dapat menghitung rng.next_u32() atau rng.gen::<u64>() . Namun, rng.gen_iter::<u16>() tidak dapat dibuat karena pembuatan otomatis hanya dapat menghasilkan (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) , sedangkan yang sebenarnya kita inginkan adalah Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Mungkin kompiler dapat secara otomatis menolak panggilan delegasi-tidak aman seperti cek Sized .)

FWIW fitur ini menurut saya lebih dekat dalam semangat penutupan daripada tupel (yang, tentu saja, struct anonim enum anonim hipotetis

Untuk anonim struct s dan enum s (tupel dan "memisahkan"), "anonim" dalam arti jenis "struktural" (sebagai lawan dari "nominal") -- mereka' re built-in, sepenuhnya generik di atas jenis komponennya, dan bukan merupakan deklarasi bernama dalam file sumber apa pun. Tetapi programmer masih menulisnya dan menggunakannya seperti tipe lainnya, implementasi sifat untuk mereka ditulis secara eksplisit seperti biasa, dan mereka tidak terlalu ajaib (selain memiliki sintaks bawaan dan menjadi 'variadik', yang tipe lain belum bisa). Dalam beberapa hal, mereka memiliki nama, tetapi bukannya alfanumerik, 'nama' mereka adalah sintaks yang digunakan untuk menulisnya (tanda kurung dan koma).

Penutupan, di sisi lain, bersifat anonim dalam arti bahwa namanya dirahasiakan . Kompiler menghasilkan tipe baru dengan nama baru setiap kali Anda menulisnya, dan tidak ada cara untuk mengetahui apa nama itu atau merujuknya bahkan jika Anda menginginkannya. Kompiler mengimplementasikan satu atau dua sifat untuk tipe rahasia ini, dan satu-satunya cara Anda dapat berinteraksi dengannya adalah melalui sifat-sifat ini.

Mampu mengembalikan tipe yang berbeda dari cabang yang berbeda dari if , di belakang impl Trait , tampaknya lebih dekat dengan yang terakhir -- kompiler secara implisit menghasilkan tipe untuk menampung cabang yang berbeda, mengimplementasikan sifat yang diminta di atasnya untuk dikirim ke yang sesuai, dan programmer tidak pernah menulis atau melihat apa tipe itu, dan tidak dapat merujuknya atau tidak memiliki alasan nyata untuk menginginkannya.

(Faktanya, fitur ini terasa agak terkait dengan "literal objek" hipotetis -- yang akan menjadi ciri lain seperti sintaks penutupan yang ada untuk Fn . Artinya, alih-alih ekspresi lambda tunggal, Anda akan mengimplementasikan setiap metode dari sifat yang diberikan (dengan self menjadi implisit) menggunakan variabel dalam ruang lingkup, dan kompiler akan menghasilkan tipe anonim untuk menahan upvars, dan mengimplementasikan sifat yang diberikan untuk itu, itu akan memiliki mode move opsional dengan cara yang sama, dan seterusnya. Bagaimanapun, saya menduga cara lain untuk mengekspresikan if foo() { (some future) } else { (other future) } adalah object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (well, Anda juga perlu untuk mengangkat hasil foo() menjadi let sehingga hanya dijalankan sekali) Itu agak kurang ergonomis dan mungkin tidak boleh dianggap sebagai *alternatif yang sebenarnya dari yang lain fitur, tapi itu menunjukkan ada hubungan. Mungkin yang pertama bisa desugar ke yang terakhir, atau sesuatu.)

@glaebhoerl itu

Beberapa pemikiran dari atas kepala saya (jadi tidak terlalu matang):

  1. [bersepeda] awalan object menunjukkan bahwa ini adalah objek sifat daripada hanya eksistensial -- tetapi sebenarnya bukan.

Sintaks alternatif yang mungkin:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [makro/gula] Anda dapat memberikan beberapa gula sintaksis sepele sehingga Anda mendapatkan:
future!(if foo() { a.poll() } else { b.poll() })

Ya pertanyaan sintaksnya berantakan karena tidak jelas apakah Anda ingin menarik inspirasi dari struct literal, closure, atau blok impl :) Saya hanya mengambil satu dari atas kepala saya misalnya Demi. (Pokoknya, poin utama saya bukanlah bahwa kita harus pergi dan menambahkan objek literal [meskipun kita harus] tetapi menurut saya anonim enum s adalah ikan haring merah di sini [meskipun kita harus menambahkannya juga].)

Mampu mengembalikan tipe yang berbeda dari cabang yang berbeda dari if, di belakang Sifat impl, tampaknya lebih dekat dengan yang terakhir -- kompiler secara implisit menghasilkan tipe untuk menampung cabang yang berbeda, mengimplementasikan sifat yang diminta di atasnya untuk dikirim ke yang sesuai, dan pemrogram tidak pernah menulis atau melihat apa tipe itu, dan tidak dapat merujuknya atau tidak memiliki alasan nyata untuk menginginkannya.

Hmm. Jadi saya berasumsi bahwa daripada menghasilkan "nama baru" untuk tipe enum, kami malah akan memanfaatkan tipe | , sesuai dengan impls seperti ini:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Jelas akan ada masalah koherensi dengan ini, dalam arti bahwa banyak fungsi akan menghasilkan impls yang identik. Tetapi bahkan mengesampingkan itu, saya menyadari sekarang bahwa ide ini mungkin tidak berhasil karena alasan lain -- misalnya, jika ada beberapa jenis terkait, dalam beberapa konteks mereka mungkin harus sama, tetapi dalam konteks lain boleh berbeda. Misalnya mungkin kita kembali:

-> impl IntoIterator<Item = Y>

tapi di tempat lain kami melakukannya

-> impl IntoIterator<IntoIter = X, Item = Y>

Ini akan menjadi dua impls yang tumpang tindih, saya kira itu tidak bisa "digabungkan"; baik, mungkin dengan spesialisasi.

Bagaimanapun, gagasan "enum rahasia" tampaknya lebih bersih di sekitar saya kira.

Saya ingin mendaftarkan satu perhatian terakhir dengan sifat impl eksistensial. Sebagian besar waktu saya ingin menggunakan sifat impl, saya sebenarnya ingin mengembalikan lebih dari satu jenis.

@nikomatsakis : Apakah adil untuk mengatakan bahwa dalam hal ini apa yang dikembalikan lebih dekat ke dyn Trait daripada impl Trait , karena nilai pengembalian sintetis/anonim mengimplementasikan sesuatu yang mirip dengan pengiriman dinamis?

cc https://github.com/rust-lang/rust/issues/49288 , masalah yang sering saya hadapi akhir-akhir ini saat bekerja dengan Future s dan Future -returning trait metode.

Karena ini adalah kesempatan terakhir sebelum FCP ditutup, saya ingin membuat satu argumen terakhir terhadap ciri-ciri otomatis otomatis. Saya menyadari ini adalah menit terakhir, jadi paling-paling saya ingin membahas masalah ini secara resmi sebelum kita berkomitmen pada implementasi saat ini.

Untuk memperjelas bagi siapa saja yang belum mengikuti impl Trait , ini adalah masalah yang saya presentasikan. Sebuah tipe yang diwakili oleh tipe impl X saat ini secara otomatis mengimplementasikan ciri-ciri otomatis jika dan hanya jika tipe konkret di belakangnya mengimplementasikan ciri-ciri otomatis tersebut. Secara konkret, jika perubahan kode berikut dibuat, fungsi akan terus dikompilasi, tetapi penggunaan fungsi apa pun yang bergantung pada fakta bahwa tipe yang dikembalikan mengimplementasikan Send akan gagal.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(contoh sederhana: bekerja , perubahan internal menyebabkan kegagalan )

Masalah ini tidak jelas. Ada keputusan yang sangat disengaja untuk memiliki "kebocoran" ciri otomatis: jika tidak, kami harus meletakkan + !Send + !Sync pada setiap fungsi yang mengembalikan sesuatu yang tidak terkirim atau tidak sinkron, dan kami akan memiliki cerita yang tidak jelas dengan potensi ciri-ciri otomatis kustom lainnya yang tidak dapat diterapkan pada tipe konkret yang dikembalikan fungsinya. Ini adalah dua masalah yang akan saya bahas nanti.

Pertama, saya hanya ingin menyatakan keberatan saya terhadap masalah ini: ini memungkinkan mengubah badan fungsi untuk mengubah API yang dihadapi publik. Ini secara langsung mengurangi pemeliharaan kode.

Sepanjang pengembangan karat, keputusan telah dibuat yang salah di sisi verbositas atas kegunaan. Ketika pendatang baru melihat ini, mereka pikir itu verbositas demi verbositas, tapi ini tidak terjadi. Setiap keputusan, apakah itu untuk tidak memiliki struktur yang secara otomatis menerapkan Salin, atau memiliki semua jenis eksplisit di tanda tangan fungsi, adalah demi pemeliharaan.

Ketika saya memperkenalkan Rust kepada orang-orang, tentu saja, saya dapat menunjukkan kepada mereka kecepatan, produktivitas, keamanan memori. Tapi pergi memiliki kecepatan. Ada memiliki keamanan memori. Python memiliki produktivitas. Apa yang dimiliki Rust mengalahkan semua ini, ia memiliki rawatan. Ketika seorang penulis perpustakaan ingin mengubah algoritma menjadi lebih efisien, atau ketika mereka ingin mengulang struktur peti, mereka memiliki jaminan kuat dari kompiler bahwa itu akan memberi tahu mereka ketika mereka melakukan kesalahan. Dalam karat, saya dapat yakin bahwa kode saya akan terus berfungsi tidak hanya dalam hal keamanan memori, tetapi juga logika dan antarmuka. _Setiap antarmuka fungsi di Rust sepenuhnya dapat diwakili oleh deklarasi tipe fungsi_.

Menstabilkan impl Trait apa adanya memiliki peluang besar untuk melawan keyakinan ini. Tentu, ini sangat bagus untuk menulis kode dengan cepat, tetapi jika saya ingin membuat prototipe, saya akan menggunakan python. Rust adalah bahasa pilihan ketika seseorang membutuhkan pemeliharaan jangka panjang, bukan kode hanya-tulis jangka pendek.


Saya katakan hanya ada "kemungkinan besar" ini menjadi buruk di sini karena sekali lagi, masalahnya tidak jelas. Seluruh gagasan 'ciri-ciri otomatis' di tempat pertama adalah non-eksplisit. Kirim dan Sinkronkan diimplementasikan berdasarkan apa isi struct, bukan deklarasi publik. Karena keputusan ini berhasil untuk karat, impl Trait bertindak serupa juga dapat berhasil dengan baik.

Namun, fungsi dan struktur digunakan secara berbeda dalam basis kode, dan ini bukan masalah yang sama.

Saat memodifikasi bidang struktur, bahkan bidang pribadi, segera jelas bahwa seseorang mengubah konten sebenarnya. Struktur dengan bidang non-Kirim atau non-Sinkronisasi membuat pilihan itu, dan pengelola perpustakaan tahu untuk memeriksa ulang saat PR mengubah bidang struktur.

Saat memodifikasi internal suatu fungsi, jelas sekali bahwa fungsi tersebut dapat memengaruhi kinerja dan kebenarannya. Namun, di Rust, kami tidak perlu memeriksa apakah kami mengembalikan tipe yang benar. Deklarasi fungsi adalah kontrak keras yang harus kita junjung, dan rustc mengawasi kita. Ini adalah garis tipis antara ciri-ciri otomatis pada struct dan dalam pengembalian fungsi, tetapi mengubah internal suatu fungsi jauh lebih rutin. Setelah kita memiliki Future s bertenaga generator penuh, akan lebih rutin lagi untuk memodifikasi fungsi yang mengembalikan -> impl Future . Ini semua akan menjadi perubahan yang penulis perlukan untuk menyaring implementasi Kirim/Sinkronisasi yang dimodifikasi jika kompiler tidak menangkapnya.

Untuk mengatasi ini, kita dapat memutuskan bahwa ini adalah beban pemeliharaan yang dapat diterima, seperti yang dilakukan oleh diskusi RFC awal . Bagian dalam RFC sifat impl konservatif ini memaparkan argumen terbesar untuk membocorkan ciri-ciri otomatis ("OIBIT" adalah nama lama untuk ciri-ciri otomatis).

Saya sudah memberikan tanggapan utama saya untuk ini, tapi ini satu catatan terakhir. Mengubah tata letak struktur bukanlah hal yang umum dilakukan; itu bisa dijaga. Beban pemeliharaan dalam memastikan fungsi terus menerapkan ciri otomatis yang sama lebih besar daripada struktur hanya karena fungsi lebih banyak berubah.


Sebagai catatan terakhir, saya ingin mengatakan bahwa ciri otomatis otomatis bukanlah satu-satunya pilihan. Ini adalah opsi yang kami pilih, tetapi alternatif dari fitur auto opt-out masih merupakan alternatif.

Kami dapat meminta fungsi mengembalikan item yang tidak terkirim/tidak menyinkronkan ke status + !Send + !Sync atau untuk mengembalikan suatu sifat (alias mungkin?) yang memiliki batas tersebut. Ini bukan keputusan yang baik, tapi mungkin lebih baik daripada yang kita pilih saat ini.

Adapun kekhawatiran mengenai ciri-ciri otomatis kustom, saya berpendapat bahwa setiap ciri otomatis baru tidak boleh diterapkan hanya untuk tipe baru yang diperkenalkan setelah ciri otomatis. Ini mungkin memberikan lebih banyak masalah daripada yang dapat saya atasi sekarang, tetapi itu bukan masalah yang tidak dapat kami atasi dengan lebih banyak desain.


Ini sangat terlambat, dan sangat panjang lebar, dan saya yakin saya telah mengajukan keberatan ini sebelumnya. Saya senang bisa berkomentar untuk terakhir kalinya, dan memastikan kami sepenuhnya setuju dengan keputusan yang kami buat.

Terima kasih telah membaca, dan saya harap keputusan akhir membawa Rust ke arah yang terbaik.

Memperluas ulasan @daboross wrt. alias sifat, seseorang dapat meningkatkan ergonomi sifat otomatis yang tidak bocor seperti:

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Ini tidak terlalu buruk -- Anda harus membuat nama yang bagus (yang bukan FutureNSS ). Manfaat utamanya adalah mengurangi potongan kertas yang disebabkan oleh pengulangan batas.

Tidakkah mungkin untuk menstabilkan fitur ini dengan persyaratan untuk menyatakan ciri-ciri otomatis secara eksplisit dan kemudian mungkin menghapus persyaratan tersebut setelah kami menemukan solusi yang sesuai untuk masalah pemeliharaan itu atau setelah kami cukup yakin bahwa sebenarnya tidak ada beban pemeliharaan oleh keputusan untuk mengangkat persyaratan?

Bagaimana dengan meminta Send kecuali ditandai sebagai !Send , tetapi tidak memberikan Sync kecuali ditandai sebagai Sinkronisasi? Bukankah Kirim seharusnya lebih umum dibandingkan dengan Sinkronisasi?

Seperti ini:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Apakah ada inkonsistensi antara impl Trait dalam posisi argumen dan posisi pengembalian wrt. ciri-ciri mobil?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Ini masuk akal untuk posisi argumen karena jika Anda diizinkan untuk mengasumsikan Kirim di sini, Anda akan mendapatkan kesalahan monomorfisasi pos. Tentu saja, aturan untuk posisi kembali dan posisi argumen tidak harus bertepatan di sini, tetapi ini menimbulkan masalah dalam hal kemampuan untuk dipelajari.

Adapun kekhawatiran mengenai ciri-ciri otomatis kustom, saya berpendapat bahwa setiap ciri otomatis baru tidak boleh diterapkan hanya untuk tipe baru yang diperkenalkan setelah ciri otomatis.

Nah, ini benar untuk auto mendatang trait Unpin (hanya tidak-implmented untuk generator diri referensial), tapi ... yang tampaknya menjadi keberuntungan bodoh? Apakah ini batasan yang benar-benar bisa kita jalani? Saya tidak percaya bahwa tidak akan ada sesuatu di masa depan yang perlu dinonaktifkan misalnya &mut atau Rc ...

Saya percaya ini telah dibahas, dan ini tentu saja sangat terlambat, tetapi saya masih tidak puas dengan impl Trait dalam posisi argumen.

Kemampuan untuk keduanya a) bekerja dengan penutupan/masa depan berdasarkan nilai, dan b) memperlakukan beberapa jenis sebagai "keluaran" dan dengan demikian detail implementasi, adalah idiomatis dan telah ada sejak sebelum 1.0, karena mereka secara langsung mendukung nilai inti Rust dari kinerja, stabilitas, dan keamanan.

-> impl Trait dengan demikian hanya memenuhi janji yang dibuat oleh 1.0, atau menghapus kasus tepi, atau menggeneralisasi fitur yang ada: ia menambahkan tipe keluaran ke fungsi, mengambil mekanisme yang sama yang selalu digunakan untuk menangani tipe anonim dan menerapkannya dalam lebih banyak kasus. Mungkin lebih berprinsip untuk memulai dengan abstract type , yaitu tipe keluaran untuk modul, tetapi mengingat Rust tidak memiliki sistem modul ML, urutannya bukanlah masalah besar.

fn f(t: impl Trait) malah terasa seperti ditambahkan "hanya karena kita bisa," membuat bahasanya lebih besar dan asing tanpa memberikan imbalan yang cukup. Saya telah berjuang dan gagal menemukan beberapa kerangka kerja yang ada untuk menyesuaikannya. Saya memahami argumen seputar singkatnya fn f(f: impl Fn(...) -> ...) , dan pembenaran bahwa batas sudah dapat ada dalam klausa <T: Trait> dan where , tetapi itu terasa hampa. Mereka tidak meniadakan kerugiannya:

  • Anda sekarang harus mempelajari dua sintaks untuk batas- setidaknya <> / where berbagi satu sintaks.

    • Ini juga menciptakan jurang pembelajaran dan mengaburkan gagasan menggunakan tipe generik yang sama di banyak tempat.

    • Sintaks baru membuat lebih sulit untuk mengetahui apa fungsi generik over- Anda harus memindai seluruh daftar argumen.

  • Sekarang apa yang seharusnya menjadi detail implementasi fungsi (bagaimana ia mendeklarasikan parameter tipenya) menjadi bagian dari antarmukanya, karena Anda tidak dapat menulis tipenya!

    • Ini juga terkait dengan komplikasi sifat otomatis yang saat ini sedang dibahas - kekacauan lebih lanjut tentang apa yang merupakan antarmuka publik fungsi dan apa yang tidak.

  • Analogi dengan dyn Trait , sejujurnya, salah:

    • dyn Trait selalu berarti hal yang sama, dan tidak "menginfeksi" deklarasi di sekitarnya selain melalui mekanisme ciri otomatis yang ada.

    • dyn Trait dapat digunakan dalam struktur data, dan ini benar-benar salah satu kasus penggunaan utamanya. impl Trait dalam struktur data tidak masuk akal tanpa melihat semua kegunaan struktur data.

    • Bagian dari arti dyn Trait adalah penghapusan jenis, tetapi impl Trait tidak menyiratkan apa pun tentang implementasinya.

    • Poin sebelumnya akan lebih membingungkan jika kita memperkenalkan obat generik non-monomorfik. Faktanya, dalam situasi seperti itu, fn f(t: impl Trait) kemungkinan akan a) tidak berfungsi dengan fitur baru, dan/atau b) memerlukan lebih banyak lagi pengacara kasus tepi seperti masalah dengan ciri-ciri otomatis. Bayangkan fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :berteriak:

Jadi menurut saya impl Trait dalam posisi argumen menambahkan kasus tepi, menggunakan lebih banyak anggaran keanehan, membuat bahasa terasa lebih besar, dll. sementara impl Trait dalam posisi kembali bersatu, menyederhanakan, dan membuat bahasa itu lebih erat.

Bagaimana dengan mengharuskan Kirim kecuali ditandai sebagai !Kirim, tetapi tidak menyediakan Sinkronisasi kecuali ditandai sebagai Sinkronisasi? Bukankah Kirim seharusnya lebih umum dibandingkan dengan Sinkronisasi?

Itu terasa sangat… sewenang-wenang dan ad-hoc. Mungkin lebih sedikit mengetik, tapi lebih banyak mengingat dan lebih kacau.

Ide gudang sepeda di sini agar tidak mengalihkan perhatian dari poin saya di atas: alih-alih impl , gunakan type ? Itu kata kunci yang digunakan untuk jenis terkait, kemungkinan (salah satu) kata kunci yang digunakan untuk abstract type , masih cukup alami, dan lebih mengisyaratkan gagasan "jenis keluaran untuk fungsi":

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Terima kasih telah membaca, dan saya harap keputusan akhir membawa Rust ke arah yang terbaik.

Saya menghargai keberatan yang ditulis dengan baik. Seperti yang Anda tunjukkan, ciri-ciri otomatis selalu menjadi pilihan yang disengaja untuk "mengekspos" beberapa detail implementasi yang mungkin diharapkan tetap disembunyikan. Saya pikir -- sejauh ini -- pilihan itu benar-benar berjalan dengan baik, tetapi saya akui bahwa saya terus-menerus gugup tentang hal itu.

Tampaknya bagi saya pertanyaan pentingnya adalah sejauh mana fungsi benar - benar berbeda dari struct:

Mengubah tata letak struktur bukanlah hal yang umum dilakukan; itu bisa dijaga. Beban pemeliharaan dalam memastikan fungsi terus menerapkan ciri otomatis yang sama lebih besar daripada struktur hanya karena fungsi lebih banyak berubah.

Sangat sulit untuk mengetahui seberapa benar ini akan terjadi. Sepertinya aturan umumnya adalah memperkenalkan Rc adalah sesuatu yang harus dilakukan dengan hati-hati -- ini bukan masalah di mana Anda menyimpannya. (Sebenarnya, kasus yang benar-benar saya kerjakan bukanlah Rc melainkan memperkenalkan dyn Trait , karena itu bisa kurang jelas.)

Saya sangat curiga bahwa dalam kode yang mengembalikan futures, bekerja dengan tipe non-thread-safe dan sebagainya akan jarang terjadi. Anda akan cenderung menghindari perpustakaan semacam itu. (Juga, tentu saja, selalu ada gunanya melakukan tes yang menggunakan kode Anda dalam skenario yang realistis.)

Bagaimanapun, ini membuat frustrasi karena ini adalah hal yang sulit diketahui sebelumnya, tidak peduli berapa lama periode stabilisasi yang kita berikan.

Sebagai catatan terakhir, saya ingin mengatakan bahwa ciri otomatis otomatis bukanlah satu-satunya pilihan. Ini adalah opsi yang kami pilih, tetapi alternatif dari fitur auto opt-out masih merupakan alternatif.

Benar, meskipun saya benar-benar merasa gugup tentang gagasan untuk "memilih" ciri-ciri otomatis tertentu seperti Send . Juga perlu diingat bahwa ada kasus penggunaan lain untuk sifat impl selain masa depan. Misalnya, mengembalikan iterator atau penutupan -- dan dalam kasus tersebut, tidak jelas apakah Anda ingin mengirim atau menyinkronkan secara default. Bagaimanapun, apa yang benar - T adalah Kirim). Inilah tepatnya yang diberikan ciri-ciri otomatis kepada Anda.

@rpjohnst

Saya percaya ini telah dibahas

Memang, sudah :) sejak impl Trait RFC lo bertahun-tahun yang lalu. (Woah, 2014. Saya merasa tua.)

Saya telah berjuang dan gagal menemukan beberapa kerangka kerja yang ada untuk menyesuaikannya.

Saya merasa sebaliknya. Bagi saya, tanpa impl Trait dalam posisi argumen, impl Trait dalam posisi kembali lebih menonjol. Benang pemersatu yang saya lihat adalah:

  • impl Trait -- di mana ia muncul, ini menunjukkan bahwa akan ada "beberapa tipe monomorfik yang mengimplementasikan Trait ". (Pertanyaan tentang siapa yang menentukan jenis itu -- penelepon atau penerima panggilan -- bergantung pada di mana impl Trait muncul.)
  • dyn Trait -- di mana ia muncul, ini menunjukkan bahwa akan ada beberapa tipe yang mengimplementasikan Trait , tetapi pilihan tipe dibuat secara dinamis.

Ada juga rencana untuk memperluas kumpulan tempat di mana impl Trait dapat muncul, berdasarkan intuisi itu. Misalnya, https://github.com/rust-lang/rfcs/pull/2071 izin

let x: impl Trait = ...;

Prinsip yang sama berlaku: pilihan jenis diketahui secara statis. Demikian pula, RFC yang sama memperkenalkan abstract type (yang impl Trait dapat dipahami sebagai sejenis gula sintaksis), yang dapat muncul dalam impls sifat dan bahkan sebagai anggota dalam modul.

Ide gudang sepeda di sini agar tidak mengalihkan perhatian dari poin saya di atas: alih-alih impl , gunakan type ?

Secara pribadi, saya tidak cenderung untuk menghidupkan kembali sebuah gudang sepeda di sini. Kami menghabiskan cukup banyak waktu untuk membahas sintaks di https://github.com/rust-lang/rfcs/pull/2071 dan di tempat lain. Sepertinya tidak ada "kata kunci yang sempurna", tetapi membaca impl sebagai "beberapa jenis yang mengimplementasikan" bekerja dengan cukup baik.

Izinkan saya menambahkan sedikit lebih banyak tentang kebocoran sifat otomatis:

Pertama, pada akhirnya saya berpikir bahwa kebocoran sifat otomatis sebenarnya adalah hal yang benar untuk dilakukan di sini, justru karena konsisten dengan bahasa lainnya. Ciri-ciri otomatis -- seperti yang saya katakan sebelumnya -- selalu merupakan pertaruhan, tetapi tampaknya menjadi salah satu yang pada dasarnya membuahkan hasil. Saya hanya tidak melihat impl Trait sangat berbeda.

Tetapi juga, saya cukup gugup untuk menunda di sini. Saya setuju bahwa ada poin menarik lainnya di ruang desain dan saya tidak 100% yakin kami telah mencapai tempat yang tepat, tetapi saya tidak tahu apakah kami akan pernah yakin akan hal itu. Saya cukup khawatir jika kami menunda sekarang, kami akan mengalami kesulitan untuk mewujudkan peta jalan kami untuk tahun ini.

Akhirnya, mari kita pertimbangkan implikasinya jika saya salah: Apa yang pada dasarnya kita bicarakan di sini adalah bahwa semver menjadi lebih halus untuk dinilai. Ini adalah kekhawatiran, saya pikir, tetapi dapat dikurangi dengan berbagai cara. Misalnya, kita dapat menggunakan lint yang memperingatkan ketika tipe !Send atau !Sync diperkenalkan. Kami telah lama berbicara tentang memperkenalkan pemeriksa semver yang membantu Anda mencegah pelanggaran semver yang tidak disengaja -- ini sepertinya kasus lain yang akan membantu. Singkatnya, masalah, tapi saya tidak berpikir kritis.

Jadi -- setidaknya sampai saat ini -- saya masih merasa ingin melanjutkan jalan saat ini.

Secara pribadi, saya tidak cenderung untuk menghidupkan kembali sebuah gudang sepeda di sini.

Saya juga tidak terlalu berinvestasi di dalamnya; itu adalah renungan berdasarkan kesan saya bahwa impl Trait dalam posisi argumen tampaknya dimotivasi oleh "mengisi lubang" secara sintaksis daripada semantik , yang tampaknya benar mengingat tanggapan Anda. :)

Bagi saya, tanpa impl Trait dalam posisi argumen, impl Trait dalam posisi kembali lebih menonjol.

Mengingat analogi untuk tipe terkait, ini muncul sangat mirip dengan "tanpa type T dalam posisi argumen, tipe terkait lebih menonjol." Saya menduga bahwa keberatan tertentu tidak muncul karena sintaks yang kami pilih membuatnya terasa tidak masuk akal - sintaks yang ada di sana cukup baik sehingga tidak ada yang merasa perlu gula sintaksis seperti trait Trait<type SomeAssociatedType> .

Kita sudah memiliki sintaks untuk "beberapa tipe monomorfik yang mengimplementasikan Trait ." Dalam hal ciri, kami memiliki varian yang ditentukan "penelepon" dan "pemanggil". Dalam hal fungsi, kami hanya memiliki varian yang ditentukan oleh pemanggil, jadi kami memerlukan sintaks baru untuk varian yang ditentukan oleh pemanggil.

Memperluas sintaks baru ini ke variabel lokal mungkin dibenarkan, karena itu juga merupakan situasi yang mirip dengan tipe terkait- ini adalah cara untuk menyembunyikan+memberi nama tipe output ekspresi , dan berguna untuk meneruskan tipe output fungsi callee.

Seperti yang saya sebutkan di komentar saya sebelumnya, saya juga penggemar abstract type . Ini, sekali lagi, hanyalah perluasan dari konsep "tipe keluaran" ke modul. Dan menerapkan penggunaan inferensi -> impl Trait , let x: impl Trait , dan abstract type ' ke tipe terkait impls sifat juga bagus.

Ini secara khusus konsep menambahkan sintaks baru ini untuk argumen fungsi yang saya tidak suka. Itu tidak melakukan hal yang sama seperti fitur lain yang ditariknya. Itu tidak melakukan hal yang sama seperti sintaks sudah kita miliki, hanya dengan kasus tepi lebih dan kurang penerapan. :/

@nikomatsakis

Sangat sulit untuk mengetahui seberapa benar ini akan terjadi.

Tampaknya bagi saya bahwa kita harus berbuat salah dengan bersikap konservatif? Bisakah kita mendapatkan lebih banyak kepercayaan dalam desain dengan lebih banyak waktu (dengan membiarkan kebocoran ciri otomatis berada di bawah gerbang fitur terpisah dan hanya di malam hari sementara kita menstabilkan sisa impl Trait )? Kami selalu dapat menambahkan dukungan untuk kebocoran sifat otomatis nanti jika kami tidak bocor sekarang..

Tetapi juga, saya cukup gugup untuk menunda di sini. [..] Saya cukup khawatir jika kita menunda sekarang, kita akan kesulitan memenuhi roadmap kita untuk tahun ini.

Dapat dimengerti! Namun, dan seperti yang saya yakin Anda telah pertimbangkan, keputusan di sini akan tinggal bersama kita selama bertahun-tahun yang akan datang.

Misalnya, kita dapat menggunakan lint yang memperingatkan ketika tipe !Send atau !Sync diperkenalkan. Kami telah lama berbicara tentang memperkenalkan pemeriksa semver yang membantu Anda mencegah pelanggaran semver yang tidak disengaja -- ini sepertinya kasus lain yang akan membantu. Singkatnya, masalah, tapi saya tidak berpikir kritis.

Ini bagus untuk didengar! Dan saya pikir ini sebagian besar meredakan kekhawatiran saya.

Benar, meskipun saya benar-benar merasa gugup tentang gagasan untuk "memilih" ciri-ciri otomatis tertentu seperti Send .

Saya sangat setuju dengan sentimen ini 👍.

Bagaimanapun, apa yang benar-benar Anda inginkan, dan apa yang kami coba tunda =), adalah semacam ikatan "bersyarat" (Kirim jika T adalah Kirim). Inilah tepatnya yang diberikan ciri-ciri otomatis kepada Anda.

Saya merasa bahwa T: Send => Foo<T>: Send akan lebih dipahami jika kode secara eksplisit menyatakan ini.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Tho, seperti yang kita bahas di WG-Traits, Anda mungkin tidak mendapatkan inferensi sama sekali di sini, jadi Anda selalu harus menentukan Extra jika Anda menginginkan sesuatu selain Send , yang akan sangat mengecewakan .

@rpjohnst

Analogi dengan dyn Trait , sejujurnya, salah:

Sehubungan dengan impl Trait dalam posisi argumen itu salah, tetapi tidak demikian dengan -> impl Trait karena keduanya adalah tipe eksistensial.

  • Sekarang apa yang seharusnya menjadi detail implementasi fungsi (bagaimana ia mendeklarasikan parameter tipenya) menjadi bagian dari antarmukanya, karena Anda tidak dapat menulis tipenya!

Saya ingin mencatat bahwa urutan parameter tipe tidak pernah menjadi detail implementasi karena turbofish, dan dalam hal ini, saya pikir impl Trait dapat membantu karena memungkinkan Anda untuk membiarkan argumen tipe tertentu tidak ditentukan di turbofish .

[..] sintaks yang ada di sana cukup baik sehingga tidak ada yang merasa perlu gula sintaksis seperti sifat Trait.

Tidak pernah berkata tidak? https://github.com/rust-lang/rfcs/issues/2274

Seperti @nikomatsakis , saya sangat menghargai perhatian yang diberikan dalam komentar di menit-menit terakhir ini; Saya tahu rasanya seperti mencoba melemparkan diri Anda di depan kereta barang, terutama untuk fitur yang selama ini diinginkan seperti ini!


@daboross , saya ingin lebih

Sayangnya, ini mengalami beberapa masalah setelah Anda mulai melihat gambaran yang lebih besar:

  • Jika ciri-ciri otomatis diperlakukan sebagai penyisihan untuk impl Trait , itu juga harus untuk dyn Trait .
  • Ini tentu saja berlaku bahkan ketika konstruksi ini digunakan dalam posisi argumen.
  • Tapi kemudian, akan agak aneh jika obat generik berperilaku berbeda. Dengan kata lain, untuk fn foo<T>(t: T) , Anda dapat mengharapkan T: Send secara default.
  • Kami tentu saja memiliki mekanisme untuk ini, saat ini hanya diterapkan pada Sized ; itu adalah sifat yang diasumsikan secara default di mana-mana, dan untuk itu Anda menyisih dengan menulis ?Sized

Mekanisme ?Sized tetap menjadi salah satu aspek Rust yang paling tidak jelas dan sulit diajarkan, dan kami secara umum sangat enggan untuk mengembangkannya ke konsep lain. Menggunakannya untuk konsep sentral seperti Send tampaknya berisiko -- belum lagi, tentu saja, bahwa itu akan menjadi perubahan besar.

Terlebih lagi, meskipun: kami benar - benar mengenai apakah suatu jenis mengimplementasikan sifat otomatis, dan memiliki informasi itu saja "mengaliri". Misalnya, pertimbangkan fn f<T>(t: T) -> Option<T> . Kita dapat memasukkan T terlepas dari apakah itu Send , dan outputnya akan menjadi Send jika T adalah. Ini adalah bagian yang sangat penting dari cerita generik di Rust.

Ada juga masalah dengan dyn Trait . Khususnya, karena kompilasi terpisah, kita harus membatasi sifat "menyisih" ini hanya untuk sifat otomatis "terkenal" seperti Send dan Sync ; itu mungkin berarti tidak pernah menstabilkan auto trait untuk penggunaan eksternal.

Terakhir, perlu ditegaskan kembali bahwa desain "kebocoran" secara eksplisit dimodelkan setelah apa yang terjadi hari ini ketika Anda membuat pembungkus tipe baru untuk mengembalikan tipe buram. Pada dasarnya, saya percaya bahwa "kebocoran" adalah aspek inheren dari sifat-sifat otomatis; ini memiliki pengorbanan, tetapi itulah inti dari fitur tersebut, dan saya pikir kita harus berusaha agar fitur baru dapat berinteraksi dengannya.


@rpjohnst

Saya tidak memiliki banyak hal untuk ditambahkan pada masalah posisi argumen setelah diskusi ekstensif tentang komentar ringkasan RFC dan @nikomatsakis di atas.

Sekarang apa yang seharusnya menjadi detail implementasi fungsi (bagaimana ia mendeklarasikan parameter tipenya) menjadi bagian dari antarmukanya, karena Anda tidak dapat menulis tipenya!

Saya tidak mengerti apa yang Anda maksud dengan ini. Bisakah Anda memperluas?

Saya juga ingin mencatat bahwa frasa seperti:

fn f(t: impl Trait) malah terasa seperti ditambahkan "hanya karena kita bisa"

merusak diskusi itikad baik (saya menyebut ini karena ini adalah pola yang berulang). RFC berusaha keras untuk memotivasi fitur dan membantah beberapa argumen yang Anda buat di sini - belum lagi diskusi di utas tentu saja, dan dalam iterasi RFC sebelumnya, dll.

Ada timbal balik, memang ada kerugiannya, tapi itu tidak membantu kita mencapai kesimpulan yang masuk akal untuk karikatur "sisi lain" dari perdebatan.

Terima kasih kepada semua orang atas komentar terperinci mereka! Saya sangat bersemangat untuk akhirnya mengirimkan impl Trait di stable, jadi saya sangat bias terhadap implementasi saat ini dan keputusan desain yang mengarah ke sana. Karena itu, saya akan mencoba yang terbaik untuk merespons dengan tidak memihak dan mempertimbangkan hal-hal seolah-olah kita mulai dari nol:

auto Trait Kebocoran

Gagasan kebocoran auto Trait mengganggu saya untuk waktu yang lama-- dalam beberapa hal, hal itu dapat tampak bertentangan dengan banyak tujuan desain Rust. Dibandingkan dengan pendahulunya, seperti C++ atau keluarga ML, Rust tidak biasa karena memerlukan batas generik untuk dinyatakan secara eksplisit dalam deklarasi fungsi. Menurut pendapat saya, ini membuat fungsi generik Rust lebih mudah dibaca dan dipahami, dan membuatnya relatif jelas ketika perubahan yang tidak kompatibel ke belakang sedang dibuat. Kami telah melanjutkan pola ini dalam pendekatan kami ke const fn , yang membutuhkan fungsi untuk secara eksplisit menetapkan dirinya sebagai const daripada menyimpulkan const dari badan fungsi. Sama seperti batas sifat eksplisit, ini memudahkan untuk mengetahui fungsi mana yang dapat digunakan dengan cara apa, dan memberikan keyakinan kepada penulis perpustakaan bahwa perubahan implementasi kecil tidak akan merusak pengguna.

Yang mengatakan, saya telah menggunakan return-position impl Trait secara ekstensif dalam proyek saya sendiri, termasuk pekerjaan saya pada sistem operasi Fuchsia, dan saya percaya bahwa kebocoran sifat otomatis adalah default yang tepat di sini. Praktis, konsekuensi dari menghilangkan kebocoran adalah saya harus kembali dan menambahkan + Send pada dasarnya setiap impl Trait -menggunakan fungsi yang pernah saya tulis. Batas negatif (membutuhkan + !Send ) adalah ide yang menarik bagi saya, tetapi kemudian saya akan menulis + !Unpin pada hampir semua fungsi yang sama. Eksplisititas sangat membantu ketika menginformasikan keputusan pengguna atau membuat kode lebih mudah dipahami. Dalam hal ini, saya pikir itu tidak akan menghasilkan apa-apa.

Send dan Sync adalah "konteks" di mana pengguna memprogram: sangat jarang saya menulis aplikasi atau pustaka yang menggunakan tipe Send dan !Send (terutama ketika menulis kode async untuk dijalankan pada pelaksana pusat, yang multithreaded atau tidak). Pilihan untuk thread-safe atau tidak adalah salah satu pilihan pertama yang harus dibuat saat menulis aplikasi, dan dari sana, memilih untuk thread-safe berarti semua tipe saya harus Send . Untuk perpustakaan, hampir selalu saya lebih suka tipe Send , karena tidak menggunakannya biasanya berarti perpustakaan saya tidak dapat digunakan (atau memerlukan pembuatan utas khusus) bila digunakan dalam konteks berulir. parking_lot::Mutex tidak terbantahkan akan memiliki kinerja yang hampir sama dengan RefCell saat digunakan pada CPU modern, jadi saya tidak melihat motivasi apa pun untuk mendorong pengguna ke fungsi perpustakaan khusus untuk penggunaan !Send kasus. Untuk alasan ini, saya tidak berpikir penting untuk dapat membedakan antara tipe Send dan !Send pada tingkat fungsi-tanda tangan, dan saya tidak berpikir bahwa itu akan menjadi hal yang biasa untuk penulis perpustakaan secara tidak sengaja memperkenalkan tipe !Send ke dalam impl Trait tipe yang sebelumnya Send . Memang benar bahwa pilihan ini datang dengan biaya keterbacaan dan kejelasan, tetapi saya yakin pertukarannya sangat berharga untuk manfaat ergonomis dan kegunaan.

Posisi argumen impl Trait

Saya tidak punya terlalu banyak untuk dikatakan di sini, kecuali bahwa setiap kali saya mencapai posisi argumen impl Trait , saya telah menemukan bahwa itu sangat meningkatkan keterbacaan dan kesenangan keseluruhan dari tanda tangan fungsi saya. Memang benar bahwa itu tidak menambahkan kemampuan baru yang tidak mungkin dilakukan di Rust hari ini, tetapi ini adalah peningkatan kualitas hidup yang hebat untuk tanda tangan fungsi yang rumit, ini berpasangan dengan baik secara konseptual dengan return-position impl Trait , dan memudahkan transisi programmer OOP menjadi Rustaceans yang bahagia. Saat ini, ada banyak redundansi karena harus memperkenalkan tipe generik bernama hanya untuk memberikan ikatan (misalnya F dalam fn foo<F>(x: F) where F: FnOnce() vs. fn foo(x: impl FnOnce()) ). Perubahan ini memecahkan masalah itu dan menghasilkan tanda tangan fungsi yang lebih mudah dibaca dan ditulis, dan IMO terasa cocok bersama -> impl Trait .

TL; DR: Saya pikir keputusan awal kami adalah yang benar, meskipun tidak diragukan lagi mereka datang dengan pengorbanan.
Saya sangat menghargai semua orang yang berbicara dan mencurahkan begitu banyak waktu dan upaya untuk memastikan bahwa Rust adalah bahasa terbaik.

@Centril

Sehubungan dengan Sifat impl dalam posisi argumen itu salah, tetapi tidak demikian dengan -> Sifat impl karena keduanya adalah tipe eksistensial.

Ya, itulah yang saya maksud.

@aturon

frase seperti ... merusak diskusi itikad baik

Anda benar, mohon maaf untuk itu. Saya percaya saya membuat poin saya lebih baik di tempat lain.

Sekarang apa yang seharusnya menjadi detail implementasi fungsi (bagaimana ia mendeklarasikan parameter tipenya) menjadi bagian dari antarmukanya, karena Anda tidak dapat menulis tipenya!

Saya tidak mengerti apa yang Anda maksud dengan ini. Bisakah Anda memperluas?

Dengan dukungan untuk impl Trait dalam posisi argumen, Anda dapat menulis fungsi ini dengan dua cara:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

Pilihan formulir menentukan apakah konsumen API bahkan dapat menuliskan nama instansiasi tertentu (misalnya untuk mengambil alamatnya). Varian impl Trait tidak memungkinkan Anda melakukan itu, dan ini tidak selalu dapat diselesaikan tanpa menulis ulang tanda tangan untuk menggunakan sintaks <T> . Selanjutnya, pindah ke sintaks <T> adalah perubahan yang melanggar!

Dengan risiko karikatur lebih lanjut, motivasinya adalah lebih mudah untuk diajarkan, dipelajari, dan digunakan. Namun, karena pilihan di antara keduanya juga merupakan bagian utama dari antarmuka fungsi, seperti urutan parameter tipe, saya tidak merasa bahwa ini telah ditangani secara memadai- saya sebenarnya tidak setuju bahwa lebih mudah digunakan atau lebih mudah digunakan. menghasilkan tanda tangan fungsi yang lebih menyenangkan.

Saya tidak yakin ada perubahan "sederhana, tetapi terbatas -> kompleks, tetapi umum" kami yang lain, yang dimotivasi oleh kemampuan belajar/ergonomis, melibatkan perubahan yang merusak antarmuka dengan cara ini. Baik persamaan kompleks cara sederhana berperilaku identik dan Anda hanya perlu beralih ketika Anda sudah mengubah antarmuka atau perilaku (mis. penghapusan seumur hidup, ergonomi pencocokan, -> impl Trait ), atau perubahan itu sama umum dan dimaksudkan untuk diterapkan secara universal (mis. modul/jalur, masa pakai in-band, dyn Trait ).

Untuk lebih konkret, saya khawatir kita akan mulai menemukan masalah ini di perpustakaan, dan itu akan seperti "setiap orang harus ingat untuk menurunkan Copy / Clone ," tetapi lebih buruk karena a) itu akan menjadi perubahan besar, dan b) akan selalu ada ketegangan untuk mundur, khususnya karena untuk itulah fitur itu dirancang!

@cramertj Sejauh fungsi tanda tangan redundansi berjalan... bisakah kita menghilangkannya dengan cara lain? Seumur hidup dalam-band bisa lolos tanpa backreferences; mungkin kita bisa melakukan moral yang setara dengan "parameter tipe in-band" entah bagaimana. Atau dengan kata lain, “perubahan itu bersifat umum dan dimaksudkan untuk diterapkan secara universal”.

@rpjohnst

Selanjutnya, pindah ke sintaks <T> adalah perubahan yang melanggar!

Belum tentu, dengan https://github.com/rust-lang/rfcs/pull/2176 Anda dapat menambahkan parameter tipe tambahan T: Trait hingga akhir dan turbofish akan tetap berfungsi (kecuali jika Anda mengacu pada kerusakan oleh beberapa cara lain selain turbofish-breakage).

Varian impl Trait tidak memungkinkan Anda melakukan itu, dan ini tidak selalu dapat diselesaikan tanpa menulis ulang tanda tangan untuk menggunakan sintaks <T> . Selanjutnya, pindah ke sintaks <T> adalah perubahan yang melanggar!

Juga, saya pikir maksud Anda bahwa pindah dari sintaks <T> adalah perubahan yang melanggar (karena penelepon tidak dapat lagi menentukan nilai T secara eksplisit menggunakan turbofish).

PEMBARUAN: Perhatikan bahwa jika suatu fungsi menggunakan Sifat impl, maka saat ini kami tidak mengizinkan penggunaan turbofish sama sekali -- meskipun fungsi tersebut memiliki beberapa parameter umum yang normal.

@nikomatsakis Pindah ke sintaks eksplisit dapat menjadi perubahan yang mengganggu juga, jika tanda tangan lama memiliki campuran parameter tipe eksplisit dan implisit -- siapa pun yang menyediakan parameter tipe n sekarang perlu menyediakan n + 1 sebagai gantinya. Itu adalah salah satu kasus yang ingin dipecahkan oleh RFC

PEMBARUAN: Perhatikan bahwa jika suatu fungsi menggunakan Sifat impl, maka saat ini kami tidak mengizinkan penggunaan turbofish sama sekali -- meskipun fungsi tersebut memiliki beberapa parameter umum yang normal.

Ini secara teknis mengurangi jumlah kasus yang melanggar, tetapi di sisi lain itu meningkatkan jumlah kasus di mana Anda tidak dapat menyebutkan instantiasi tertentu. :(

@nikomatsakis

Terima kasih telah menangani masalah ini dengan tulus.

Saya masih ragu untuk mengatakan bahwa kebocoran sifat otomatis adalah solusi _yang tepat_, tetapi saya setuju bahwa kita tidak dapat benar-benar tahu apa yang terbaik sampai setelah fakta.

Saya terutama mempertimbangkan kasus penggunaan Futures, tapi itu bukan satu-satunya. Tanpa membocorkan Kirim/Sinkronisasi dari tipe lokal, sebenarnya tidak ada cerita yang bagus untuk menggunakan impl Trait dalam banyak konteks yang berbeda. Mengingat ini, dan diberikan ciri-ciri otomatis tambahan, saran saya tidak benar-benar layak.

Saya tidak ingin memilih Sync dan Send dan _only_ menganggapnya, karena itu agak sewenang-wenang dan hanya terbaik untuk kasus penggunaan _one_. Namun, alternatif untuk mengasumsikan semua sifat otomatis juga tidak baik. + !Unpin + !... pada setiap jenis tidak terdengar seperti solusi yang layak.

Jika kami memiliki lima tahun desain bahasa lagi untuk menghasilkan sistem efek dan ide-ide lain yang saya tidak tahu sekarang, kami mungkin dapat menemukan sesuatu yang lebih baik. Tetapi untuk saat ini, dan untuk Rust, sepertinya memiliki 100% sifat otomatis "otomatis" adalah jalan terbaik ke depan.

@lfairy

Pindah ke sintaks eksplisit dapat menjadi perubahan yang melanggar juga, jika tanda tangan lama memiliki campuran parameter tipe eksplisit dan implisit -- siapa pun yang menyediakan n parameter tipe sekarang perlu menyediakan n + 1 sebagai gantinya.

Itu tidak diperbolehkan saat ini. Jika Anda menggunakan impl Trait , Anda tidak mendapatkan turbofish untuk parameter apa pun (seperti yang saya catat). Ini tidak dimaksudkan sebagai solusi jangka panjang, lebih merupakan langkah konservatif untuk menghindari ketidaksepakatan tentang bagaimana melanjutkan sampai kita punya waktu untuk menghasilkan desain yang bulat. (Dan, seperti yang dicatat oleh @rpjohnst , ia memiliki kelemahannya sendiri .)

Desain yang ingin saya lihat adalah (a) ya untuk menerima RFC @centril atau sesuatu seperti itu dan (b) untuk mengatakan bahwa Anda dapat menggunakan turbofish untuk parameter eksplisit (tetapi bukan tipe impl Trait ). Namun, kami tidak melakukannya, sebagian karena kami bertanya-tanya apakah mungkin ada cerita yang memungkinkan migrasi dari parameter eksplisit ke sifat impl.

@lfairy

Itu adalah salah satu kasus yang ingin dipecahkan oleh RFC

_[Trivia]_ Kebetulan, sebenarnya @nikomatsakis yang menarik perhatian saya bahwa turbofish parsial dapat mengurangi jeda antara <T: Trait> dan impl Trait ;) Itu bukan tujuan RFC di semua dari awal, tapi itu adalah kejutan yang menyenangkan. 😄.

Mudah-mudahan, setelah kami mendapatkan kepercayaan lebih tentang inferensi, default, parameter bernama, dll. Kami juga dapat memiliki turbofish parsial, Akhirnya™.

Periode komentar terakhir sekarang selesai.

Jika ini dikirim dalam 1,26 maka https://github.com/rust-lang/rust/issues/49373 tampaknya sangat penting bagi saya, Future dan Iterator adalah dua penggunaan utama -cases dan keduanya sangat bergantung pada mengetahui jenis terkait.

Melakukan pencarian cepat di pelacak masalah, dan #47715 adalah ICE yang masih perlu diperbaiki. Bisakah kita mendapatkan ini sebelum menjadi stabil?

Sesuatu yang saya temui dengan Sifat impl hari ini:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Sepertinya impl Trait tidak kompatibel dengan unimplemented!() - apakah ini masalah umum?

ya, lihat #36375 dan #44923

Saya baru menyadari bahwa asumsi 2 RFC 1951 bertentangan dengan beberapa rencana penggunaan impl Trait dengan blok async. Khususnya jika Anda mengambil parameter AsRef atau Into generik untuk memiliki API yang lebih ergonomis, kemudian mengubahnya menjadi beberapa tipe yang dimiliki sebelum mengembalikan blok async , Anda masih mendapatkan yang dikembalikan impl Trait tipe terikat oleh masa pakai apa pun dalam parameter itu, mis

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

Dengan ini Anda akan mendapatkan error[E0597]: `url` does not live long enough karena get menyertakan masa pakai referensi sementara dalam impl Future dikembalikan. Contoh ini sedikit dibuat-buat karena Anda dapat meneruskan url berdasarkan nilai ke get , tetapi hampir pasti akan ada kasus serupa yang muncul dalam kode nyata.

Sejauh yang saya tahu perbaikan yang diharapkan untuk ini adalah tipe abstrak, khususnya

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

Dengan menambahkan lapisan tipuan, Anda harus secara eksplisit melewati tipe generik dan parameter masa pakai yang diperlukan untuk tipe abstrak.

Saya bertanya-tanya apakah ada kemungkinan cara yang lebih ringkas untuk menulis ini, atau apakah ini hanya akan berakhir dengan tipe abstrak yang digunakan untuk hampir setiap fungsi dan tidak pernah tipe pengembalian impl Trait telanjang?

Jadi, jika saya memahami komentar @cramertj tentang masalah itu, Anda akan mendapatkan kesalahan pada definisi HttpClient::get sesuatu seperti `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Karena RFC secara eksplisit menetapkan bahwa impl Trait menangkap _semua_ parameter tipe generik, dan itu adalah bug yang memungkinkan Anda untuk menangkap tipe yang mungkin berisi masa pakai yang lebih pendek daripada masa pakai yang dinyatakan secara eksplisit).

Dari sini, satu-satunya perbaikan tampaknya masih mendeklarasikan tipe abstrak nominal untuk memungkinkan secara eksplisit mendeklarasikan parameter tipe mana yang ditangkap.

Sebenarnya, itu sepertinya akan menjadi perubahan yang menghancurkan. Jadi jika kesalahan dalam hal itu akan ditambahkan, sebaiknya segera.

EDIT: Dan membaca ulang komentar, saya rasa bukan itu yang dikatakannya, jadi saya masih bingung apakah ada cara potensial untuk mengatasi ini tanpa menggunakan tipe abstrak atau tidak.

@Nemo157 Ya, memperbaiki #42940 akan memperbaiki masalah seumur hidup Anda karena Anda dapat menentukan bahwa tipe pengembalian harus hidup selama peminjaman diri, terlepas dari masa pakai Url . Ini jelas merupakan perubahan yang ingin kami buat, tetapi saya yakin ini kompatibel dengan mundur untuk melakukannya-- ini tidak memungkinkan tipe pengembalian memiliki masa pakai yang lebih pendek, ini terlalu membatasi cara tipe pengembalian dapat digunakan.

Misalnya, kesalahan berikut dengan "parameter Iter mungkin tidak cukup lama":

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Hanya memiliki Iter dalam generik untuk fungsi tersebut tidak cukup untuk memungkinkannya hadir dalam tipe kembalian, tetapi saat ini pemanggil fungsi secara keliru menganggapnya demikian. Ini jelas merupakan bug dan harus diperbaiki, tetapi saya percaya bahwa ini dapat diperbaiki dengan kompatibilitas mundur dan tidak boleh memblokir stabilisasi.

Tampaknya #46541 sudah selesai. Bisakah seseorang memperbarui OP?

Apakah ada alasan mengapa sintaks abstract type Foo = ...; dipilih daripada type Foo = impl ...; ? Saya lebih suka yang terakhir, untuk konsistensi sintaks, dan saya ingat beberapa diskusi tentang ini beberapa waktu lalu, tetapi sepertinya tidak dapat menemukannya.

Saya tidak setuju dengan type Foo = impl ...; atau type Foo: ...; , abstract tampaknya tidak perlu.

Jika saya ingat dengan benar, salah satu perhatian utama adalah bahwa orang telah belajar untuk menafsirkan type X = Y seperti substitusi tekstual ("ganti X dengan Y jika berlaku"). Ini tidak berfungsi untuk type X = impl Y .

Saya lebih suka type X = impl Y sendiri karena intuisi saya adalah bahwa type bekerja seperti let , tapi...

@alexreg Ada banyak diskusi tentang topik di RFC 2071 . TL;DR: type Foo = impl Trait; memecah kemampuan untuk mendesugar impl Trait menjadi beberapa bentuk "lebih eksplisit", dan itu merusak intuisi orang tentang alias tipe yang berfungsi sebagai substitusi sintaksis yang sedikit lebih cerdas.

Saya parsial untuk mengetik Foo = impl ...; atau ketik Foo: ...;, abstrak tampaknya aneh yang tidak perlu

Anda harus bergabung dengan kamp exists type Foo: Trait; :wink:

@cramertj Hmm. Saya baru saja menyegarkan diri tentang beberapa hal itu, dan jika saya jujur, saya tidak bisa mengatakan bahwa saya mengerti alasan @withoutboats . Tampaknya keduanya yang paling intuitif bagi saya (apakah Anda memiliki contoh tandingan?) Dan sedikit tentang desugaring yang tidak saya mengerti. Saya kira intuisi saya bekerja seperti @lnicola. Saya juga merasa sintaks ini adalah yang terbaik untuk melakukan hal-hal seperti https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 – dapatkah ini dilakukan dalam sintaks saat ini?

exists type Foo: Trait; adalah sedikit peningkatan, meskipun saya masih akan menghapus kata kunci exists . type Foo: Trait; tidak akan cukup mengganggu saya untuk mengeluh. abstract hanya berlebihan/aneh, seperti yang dikatakan @eddyb .

@alexreg

dapatkah ini dilakukan dalam sintaks saat ini?

Ya, tapi itu jauh lebih canggung. Ini adalah alasan utama saya untuk memilih sintaks = impl Trait (modulo kata kunci abstract ).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Sunting: exists type Foo: (Foo1, Foo2); atas seharusnya type Foo = (Foo1, Foo2); . Maaf bila membingungkan.

@cramertj Sintaksnya sepertinya bagus. Haruskah exists menjadi kata kunci yang tepat?

@cramertj Benar, saya pikir Anda harus melakukan sesuatu seperti itu... alasan yang bagus untuk memilih = impl Trait , saya rasa! Sejujurnya, jika orang berpikir intuisi tentang substitusi cukup rusak untuk tipe eksistensial di sini (dibandingkan dengan alias tipe sederhana), mengapa tidak kompromi berikut?

exists type Foo = (impl Bar, impl Baz);

(Sejujurnya, saya lebih suka hanya memiliki konsistensi menggunakan kata kunci type tunggal untuk semuanya.)

Saya menemukan:

exists type Foo: (Foo1, Foo2);

sangat aneh. Menggunakan Foo: (Foo1, Foo2) mana RHS tidak terikat tidak konsisten dengan bagaimana Ty: Bound digunakan di tempat lain dalam bahasa tersebut.

Bentuk-bentuk berikut tampak baik bagi saya:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

Saya juga lebih suka untuk tidak menggunakan abstract sebagai kata di sini.

Saya merasa exists type Foo: (Foo1, Foo2); sangat aneh

Itu jelas terlihat seperti kesalahan bagi saya, dan saya pikir itu seharusnya mengatakan type Foo = (Foo1, Foo2); .

Jika kita membagi abstract type vs exists type sini, saya pasti akan mendukung yang pertama. Sebagian besar karena "abstrak" berfungsi sebagai kata sifat. Saya dapat dengan mudah menyebut sesuatu sebagai "tipe abstrak" dalam percakapan, sedangkan rasanya aneh untuk mengatakan bahwa kita membuat "tipe yang ada".

Saya juga masih lebih suka : Foo + Bar : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar atau = (impl Foo, impl Bar . Penggunaan + bekerja dengan baik dengan semua batas tempat lain, dan kurangnya = benar-benar menandakan bahwa kita tidak dapat menuliskan tipe lengkapnya. Kami tidak membuat alias tipe di sini, kami membuat nama untuk sesuatu yang kami jamin memiliki batas tertentu, tetapi tidak dapat kami sebutkan secara eksplisit.


Saya juga masih menyukai saran sintaks dari https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 dari:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Meskipun ini, seperti yang disebutkan di utas itu, sedikit terlalu sedikit perbedaan dan tidak terlalu eksplisit.

Saya harus menafsirkan (impl Foo, impl Bar) sangat berbeda dari sebagian dari Anda... bagi saya, ini berarti tipenya adalah 2-Tuple dari beberapa tipe eksistensial, dan sama sekali berbeda dari impl Foo + Bar .

@alexreg Jika itu niat @cramertj , saya masih akan merasa sangat aneh dengan sintaks : :

exists type Foo: (Foo1, Foo2);

tampaknya masih sangat tidak jelas tentang apa yang dilakukannya - batas biasanya tidak menentukan Tuple jenis yang mungkin dalam hal apa pun, dan itu dapat dengan mudah dikacaukan dengan arti sintaks Foo: Foo1 + Foo2 .

= (impl Foo, impl Bar) adalah ide yang menarik - memungkinkan pembuatan tupel eksistensial dengan tipe yang tidak diketahui akan menarik. Saya tidak berpikir kita _perlu_ untuk mendukungnya, karena kita hanya dapat memperkenalkan dua tipe eksistensial untuk impl Foo dan impl Bar kemudian tipe ketiga alias untuk Tuple.

@daboross Nah, Anda membuat "tipe eksistensial" , bukan "tipe yang ada" ; itulah yang disebut dalam teori tipe. Tapi saya pikir ungkapan "ada tipe Foo yang ..." bekerja dengan baik baik dengan model mental dan dari perspektif teoretis tipe.

Saya tidak berpikir kita perlu mendukungnya, karena kita bisa saja memperkenalkan dua tipe eksistensial untuk impl Foo dan impl Bar kemudian tipe ketiga alias untuk Tuple.

Kelihatannya tidak ergonomis... temporer tidak begitu bagus.

@alexreg Catatan: Saya tidak bermaksud mengatakan bahwa impl Bar + Baz; sama dengan (impl Foo, impl Bar) , yang terakhir jelas adalah 2-Tuple.

@daboross

Jika itu niat @cramertj , saya masih akan merasa sangat aneh dengan sintaks ::

exists type Foo: (Foo1, Foo2);

tampaknya masih sangat tidak jelas tentang apa yang dilakukannya - batas biasanya tidak menentukan Tuple jenis yang mungkin dalam hal apa pun, dan itu dapat dengan mudah dibingungkan untuk arti sintaks Foo: Foo1 + Foo2.

Ini mungkin sedikit tidak jelas (tidak seeksplisit (impl Foo, impl Bar) , yang akan langsung saya pahami secara intuitif) – tetapi saya rasa saya tidak akan pernah bingung untuk Foo1 + Foo2 , secara pribadi.

= (impl Foo, impl Bar) adalah ide yang menarik - memungkinkan pembuatan tupel eksistensial dengan tipe yang tidak diketahui dengan sendirinya akan menarik. Saya tidak berpikir kita perlu mendukungnya, karena kita hanya dapat memperkenalkan dua tipe eksistensial untuk impl Foo dan impl Bar kemudian alias tipe ketiga untuk Tuple.

Ya, itu adalah proposal awal, dan saya masih sangat menyukainya. Telah dicatat bahwa ini dapat dilakukan dengan menggunakan sintaks saat ini, tetapi memerlukan 3 baris kode, yang tidak terlalu ergonomis. Saya juga berpendapat bahwa beberapa sintaks seperti ... = (impl Foo, impl Bar) adalah yang paling jelas bagi pengguna, tetapi saya tahu ada pertentangan di sini.

@Centril Saya tidak berpikir begitu pada awalnya, tetapi itu sedikit ambigu, dan kemudian @daboross sepertinya menafsirkannya seperti itu, hah. Bagaimanapun, senang kami telah menyelesaikannya.

Ups, lihat hasil edit saya di https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); seharusnya type Foo = (Foo1, Foo2); .

@cramertj Ah, itu lebih masuk akal sekarang. Lagi pula, tidakkah menurut Anda bisa melakukan hal berikut ini adalah yang paling ergonomis? Bahkan menelusuri utas lain itu, saya belum benar-benar melihat argumen yang bagus untuk menentangnya.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Ya, saya pikir itu adalah yang paling sintaks ergonomis.

Menggunakan RFC https://github.com/rust-lang/rfcs/pull/2289 , beginilah cara saya menulis ulang cuplikan @cramertj :

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

Namun, saya pikir untuk alias tipe, tidak memperkenalkan exists akan membantu mempertahankan kekuatan ekspresif sementara tidak perlu membuat sintaks bahasa lebih kompleks; jadi dari POV anggaran kompleksitas, impl Iterator tampaknya lebih baik daripada exists . Namun, alternatif terakhir tidak benar-benar memperkenalkan sintaks baru dan juga yang terpendek meskipun jelas.

Singkatnya, saya pikir kedua formulir berikut harus diizinkan (karena berfungsi di bawah impl Trait dan batas pada sintaks tipe terkait yang sudah kita miliki):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDIT: Sintaks mana yang harus digunakan? IMO, clippy jelas lebih memilih sintaks Type: Bound jika memungkinkan untuk digunakan karena paling ergonomis dan langsung.

Saya lebih suka varian type Foo: Trait varian type Foo = impl Trait . Ini cocok dengan sintaks tipe terkait, yang bagus karena ini juga merupakan "tipe output" dari modul yang memuatnya.

Sintaks impl Trait sudah digunakan untuk tipe input dan output, yang berarti berisiko memberikan kesan modul polimorfik. :(

Jika impl Trait hanya digunakan untuk tipe keluaran, maka saya mungkin lebih suka varian type Foo = impl Trait dengan alasan bahwa sintaks tipe terkait lebih untuk sifat (yang secara longgar sesuai dengan tanda tangan ML) sedangkan type Foo = .. Sintaks

@rpjohnst

Saya lebih suka varian type Foo: Trait varian type Foo = impl Trait .

Saya setuju, itu harus digunakan bila memungkinkan; tetapi bagaimana dengan kasus (impl T, impl U) mana sintaks terikat tidak dapat digunakan secara langsung? Bagi saya, memperkenalkan alias tipe sementara merusak keterbacaan.

Menggunakan type Name: Bound sepertinya akan membingungkan ketika digunakan di dalam blok impl :

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

Untuk sintaks tersebut dan rencana awalan kata kunci saat ini(?), biaya pengenalan alias tipe sementara untuk digunakan di blok impl juga jauh lebih besar, alias tipe ini sekarang perlu diekspor pada tingkat modul ( dan diberi nama yang bermakna secara semantik ...), yang memblokir pola yang relatif umum (setidaknya bagi saya) untuk mendefinisikan implementasi sifat di dalam modul pribadi.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

vs

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 Mengapa tidak mengizinkan keduanya:

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

dan:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

Saya tidak mengerti mengapa perlu ada dua sintaks untuk fitur yang sama, hanya menggunakan sintaks type Name = impl Bound; secara eksplisit memberikan nama untuk dua bagian masih dimungkinkan:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 Saya setuju tidak perlu (dan tidak boleh) menjadi dua sintaks yang berbeda. Saya tidak menemukan type (tanpa kata kunci awalan) membingungkan sama sekali, saya harus mengatakan.

@rpjohnst Apa sih modul polimorfik itu? :-) Bagaimanapun, saya tidak melihat mengapa kita harus memodelkan sintaks setelah definisi tipe terkait, yang menempatkan batas sifat pada suatu tipe. Ini tidak ada hubungannya dengan batas .

@alexreg Modul polimorfik adalah modul yang memiliki parameter tipe, sama seperti yang dilakukan fn foo(x: impl Trait) . Itu bukan sesuatu yang ada, jadi saya tidak ingin orang berpikir itu ada.

abstract type ( edit: untuk memberi nama fitur, bukan untuk menyarankan penggunaan kata kunci) semuanya berkaitan dengan batasan! Batas adalah satu-satunya hal yang Anda ketahui tentang jenisnya. Satu-satunya perbedaan antara mereka dan jenis terkait adalah bahwa mereka disimpulkan, karena mereka biasanya tidak dapat disebutkan namanya.

@Nemo157 sintaks Foo: Bar sudah lebih dikenal dalam konteks lain (batas pada tipe terkait, dan pada parameter tipe) dan lebih ergonomis dan (IMO) jelas ketika dapat digunakan tanpa memperkenalkan temporer.

Menulis:

type IterDisplay: Iterator<Item: Display>;

tampaknya jauh lebih langsung wrt. apa yang ingin saya katakan, dibandingkan dengan

type IterDisplay = impl Iterator<Item = impl Display>;

Saya pikir ini hanya secara konsisten menerapkan sintaks yang sudah kita miliki; jadi sebenarnya tidak baru.

EDIT2: Sintaks pertama juga seperti yang saya inginkan di rustdoc.

Beralih dari sifat yang membutuhkan sesuatu pada tipe terkait, ke impl juga menjadi sangat mudah:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

Sintaks impl Bar tampaknya lebih baik ketika Anda harus memperkenalkan temporer, tetapi juga menerapkan sintaks secara konsisten di seluruh.

Mampu menggunakan kedua sintaks tidak akan jauh berbeda dari dapat menggunakan impl Trait dalam posisi argumen serta memiliki parameter tipe eksplisit T: Trait yang kemudian digunakan oleh argumen.

EDIT1: Faktanya, hanya memiliki satu sintaks akan menjadi casing khusus, bukan sebaliknya.

@rpjohnst Saya mohon berbeda, meskipun saya seharusnya mengatakan itu tidak ada hubungannya secara eksplisit dengan batas.

Bagaimanapun, saya tidak menentang sintaks type Foo: Bar; , tetapi demi kebaikan, mari kita singkirkan kata kunci abstract . type dengan sendirinya cukup jelas, dalam keadaan apa pun.

Secara pribadi, saya merasa bahwa menggunakan = dan impl adalah petunjuk visual yang bagus bahwa inferensi sedang terjadi. Ini juga memudahkan untuk menemukan tempat-tempat itu saat membaca sekilas file yang lebih besar.

Juga, dengan asumsi saya melihat type Iter: Iterator<Item = Foo> Saya harus menemukan Foo dan mencari tahu apakah itu tipe atau sifat sebelum saya tahu apa yang terjadi.

Dan terakhir, saya pikir petunjuk visual dari titik inferensi juga akan membantu men-debug kesalahan inferensi dan menafsirkan pesan kesalahan inferensi.

Jadi saya pikir varian = / impl menyelesaikan lebih banyak potongan kertas.

@phaylon

Juga, dengan asumsi saya melihat tipe Iter: Iterator<Item = Foo> Saya harus menemukan Foo dan mencari tahu apakah itu tipe atau sifat sebelum saya tahu apa yang terjadi.

Ini saya tidak mengerti; Item = Foo harus selalu menjadi tipe saat ini mengingat dyn Foo stabil (dan sifat telanjang semakin dihapus...)?

@Centril

Ini saya tidak mengerti; Item = Foo harus selalu menjadi tipe saat ini mengingat dyn Foo stabil (dan sifat telanjang semakin dihapus...)?

Ya, tetapi dalam varian kurang impl diusulkan, itu bisa berupa tipe yang disimpulkan dengan terikat, atau tipe konkret. Misalnya Iterator<Item = String> vs Iterator<Item = Display> . Saya harus tahu ciri-cirinya untuk mengetahui apakah inferensi terjadi.

Sunting: Ah, tidak memperhatikan ada yang menggunakan : . Agak apa yang saya maksud dengan mudah untuk dilewatkan :) Tapi Anda benar bahwa mereka berbeda.

Sunting 2: Saya pikir masalah ini akan terjadi di luar tipe yang terkait. Mengingat type Foo: (Bar, Baz) Anda perlu mengetahui Bar dan Baz untuk mengetahui di mana inferensi terjadi.

@Centril

EDIT1: Faktanya, hanya memiliki satu sintaks akan menjadi casing khusus, bukan sebaliknya.

Saat ini hanya ada satu cara untuk mendeklarasikan tipe _existential_, -> impl Trait . Ada dua cara untuk mendeklarasikan tipe _universal_ ( T: Trait dan : impl Trait dalam daftar argumen).

Jika kami memiliki modul polimorfik yang mengambil tipe universal, saya dapat melihat beberapa argumen di sekitar itu, tetapi saya percaya penggunaan type Name = Type; di kedua modul dan definisi sifat adalah sebagai parameter tipe keluaran, yang seharusnya menjadi eksistensial Tipe.


@phaylon

Ya, tetapi dalam varian kurang impl diusulkan, itu bisa berupa tipe yang disimpulkan dengan terikat, atau tipe konkret. Misalnya Iterator<Item = String> vs Iterator<Item = Display> . Saya harus tahu ciri-cirinya untuk mengetahui apakah inferensi terjadi.

Saya percaya impl lebih sedikit varian menggunakan : Bound dalam semua kasus untuk tipe eksistensial, sehingga Anda dapat memiliki Iterator<Item = String> atau Iterator<Item: Display> sebagai batas sifat, tetapi Iterator<Item = Display> akan menjadi deklarasi yang tidak valid.

@Nemo157
Anda benar sehubungan dengan jenis kasus terkait, saya salah di sana. Tetapi (seperti yang tercantum dalam hasil edit saya) saya pikir masih ada masalah dengan type Foo: (A, B) . Karena A atau B dapat berupa tipe atau sifat.

Saya percaya ini juga merupakan alasan yang baik untuk menggunakan = . : hanya memberi tahu Anda bahwa beberapa hal disimpulkan, tetapi tidak memberi tahu Anda yang mana. type Foo = (A, impl B) tampaknya lebih jelas bagi saya.

Saya juga menganggap membaca dan memberikan cuplikan kode lebih mudah dengan impl , karena konteks tambahan tentang apa yang merupakan sifat dan apa yang tidak perlu disediakan.

Sunting: Beberapa Kredit: Argumen saya pada dasarnya sama dengan @alexreg di sini , saya hanya ingin memperluas mengapa menurut saya impl lebih disukai.

Saat ini hanya ada satu cara untuk mendeklarasikan tipe eksistensial, -> impl Trait . Ada dua cara untuk mendeklarasikan tipe universal ( T: Trait dan : impl Trait dalam daftar argumen).

Itulah yang saya katakan: PI Mengapa kuantifikasi universal harus memiliki dua cara tetapi eksistensial hanya satu (mengabaikan dyn Trait ) di tempat lain ?

Tampaknya sama-sama mungkin bagi saya bahwa seorang pengguna akan pergi dan menulis type Foo: Bound; dan type Foo = impl Bound; setelah mempelajari berbagai bagian bahasa, dan saya tidak dapat mengatakan bahwa satu sintaks jelas lebih baik dalam semua kasus; Jelas bagi saya bahwa satu sintaks lebih baik untuk beberapa hal dan yang lain untuk hal yang berbeda.

@phaylon

Saya percaya ini juga merupakan alasan yang baik untuk menggunakan =. The : hanya memberi tahu Anda bahwa beberapa hal disimpulkan, tetapi tidak memberi tahu Anda yang mana. ketik Foo = (A, impl B) tampaknya lebih jelas bagi saya.

Ya, ini mungkin alasan bagus lainnya. Ini benar-benar membutuhkan beberapa pembongkaran untuk mencari tahu apa yang secara eksistensial dikuantifikasi – melompat dari definisi ke definisi.

Hal lain adalah: apakah seseorang akan mengizinkan : dalam pengikatan tipe terkait, di bawah sintaks itu? Ini akan tampak seperti kasus khusus yang aneh bagi saya, mengingat tipe eksistensial tidak dapat dikomposisikan/digabungkan dengan cara lain apa pun dalam sintaks yang diusulkan ini. Saya akan membayangkan yang berikut ini akan menjadi pendekatan yang paling konsisten menggunakan sintaks itu:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Menggunakan sintaks yang saya (dan beberapa orang lain di sini) lebih suka, kita bisa menulis ini semua dalam satu baris, dan selanjutnya segera jelas di mana kuantifikasi terjadi!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Tidak terkait dengan hal di atas: kapan kita bermain untuk mengimplementasikan let x: impl Trait di malam hari? Saya telah kehilangan fitur ini untuk sementara waktu sekarang.

@alexreg

Hal lain adalah: apakah seseorang akan mengizinkan : dalam pengikatan tipe terkait, di bawah sintaks itu?

Ya, mengapa tidak; Ini akan menjadi efek alami dari rust-lang/rfcs#2289 + type Foo: Bound .

Anda juga dapat melakukan:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Saya pikir itu ide yang buruk untuk mengizinkan dua sintaks alternatif. Baunya seperti sindrom "kami tidak bisa memutuskan, jadi kami hanya akan mendukung keduanya". Melihat kode yang mencampur & mencocokkannya akan sangat merusak pemandangan!

@Centril Saya agak suka dengan @nikomatsakis di RFC Anda itu BTW, maaf. Lebih suka menulis impl Iterator<Item = impl Baz> . Bagus dan eksplisit.

@alexreg Ini adil;

Tapi (un)untungnya (tergantung pada POV Anda), kami sudah memulai "izinkan dua sintaks alternatif" dengan impl Trait dalam posisi argumen, sehingga kami memiliki Foo: Bar dan impl Bar bekerja dengan arti yang sama;

Ini untuk kuantifikasi universal, tetapi notasi impl Trait tidak terlalu peduli dengan sisi dualitasnya; lagi pula, kami tidak pergi dengan any Trait dan some Trait .

Mengingat bahwa kami telah membuat pilihan "kami tidak dapat memutuskan" dan "sisi dualitas tidak masalah secara sintaksis" , bagi saya tampaknya konsisten untuk menerapkan "kami tidak dapat memutuskan" di mana-mana sehingga pengguna tidak mengerti menjadi "tapi saya bisa menulisnya seperti ini di sana, mengapa tidak di sini?" ;)


PS:

Ulang. impl Iterator<Item = impl Baz> tidak berfungsi sebagai terikat dalam klausa where; jadi Anda harus mencampurnya seperti Iter: Iterator<Item = impl Baz> . Anda harus mengizinkan: Iter = impl Iterator<Item = impl Baz> agar berfungsi secara seragam (mungkin kita harus?).

Menggunakan : Bound juga bukan = impl Bound juga eksplisit, hanya lebih pendek ^,-
Saya pikir perbedaan spasi antara X = Ty dan X: Ty membuat sintaks dapat terbaca.

Setelah mengabaikan saran saya sendiri, mari kita lanjutkan percakapan ini di RFC ;)

Tapi (un)untungnya (tergantung pada POV Anda), kami sudah memulai "izinkan dua sintaks alternatif" dengan Sifat impl dalam posisi argumen, sehingga kami memiliki Foo: Bar dan impl Bar yang berfungsi untuk arti yang sama;

Kami melakukannya, tetapi saya yakin pilihan dibuat lebih dari sudut pandang simetri/konsistensi. Argumen yang diketik secara umum jauh lebih kuat daripada argumen yang diketik secara universal ( impl Trait ). Tapi kami memperkenalkan impl Trait di posisi pengembalian, masuk akal untuk memperkenalkannya pada posisi argumen.

Mengingat bahwa kami telah membuat pilihan "kami tidak dapat memutuskan" dan "sisi dualitas tidak masalah secara sintaksis", bagi saya tampaknya konsisten untuk menerapkan "kami tidak dapat memutuskan" di mana-mana sehingga pengguna tidak mengerti menjadi "tapi saya bisa menulisnya seperti ini di sana, mengapa tidak di sini?" ;)

Saya tidak yakin itu pada titik kita harus mengangkat tangan kita dan berkata "mari kita terapkan segalanya". Tidak ada argumen yang jelas di sini tentang keuntungan.

PS:

Ulang. impl Iterator<Item = impl Baz> tidak berfungsi sebagai terikat dalam klausa where; jadi Anda harus mencampurnya seperti Iter: Iterator<Item = impl Baz> . Anda harus mengizinkan: Iter = impl Iterator<Item = impl Baz> agar berfungsi secara seragam (mungkin kita harus?).

Saya akan mengatakan kami hanya mendukung where Iter: Iterator<Item = T>, T: Baz (seperti yang kami miliki sekarang) atau melanjutkan dengan Iter = impl Iterator<Item = impl Baz> (seperti yang Anda sarankan). Hanya membiarkan rumah setengah jalan tampaknya sedikit merepotkan.

Menggunakan : Bound juga bukan = impl Terikat juga eksplisit, hanya lebih pendek ^,-
Saya pikir perbedaan spasi antara X = Ty dan X: Ty membuat sintaks dapat terbaca.

Ini dapat dibaca, tetapi saya tidak berpikir itu hampir sejelas/eksplisit bahwa tipe eksistensial sedang digunakan. Ini diperburuk ketika definisi harus dibagi menjadi beberapa baris karena keterbatasan sintaks ini.

Setelah mengabaikan saran saya sendiri, mari kita lanjutkan percakapan ini di RFC ;)

Tunggu, maksudmu RFC-mu? Saya pikir itu relevan untuk itu dan yang ini, dari apa yang saya tahu. :-)

Tunggu, maksudmu RFC-mu? Saya pikir itu relevan untuk itu dan yang ini, dari apa yang saya tahu. :-)

OKE; Mari kita lanjutkan di sini;

Kami melakukannya, tetapi saya yakin pilihan dibuat lebih dari sudut pandang simetri/konsistensi. Argumen yang diketik secara umum jauh lebih kuat daripada argumen yang diketik secara universal ( impl Trait ). Tapi kami memperkenalkan impl Trait di posisi pengembalian, masuk akal untuk memperkenalkannya pada posisi argumen.

Seluruh poin saya adalah tentang konsistensi dan simetri. =P
Jika Anda diizinkan untuk menulis impl Trait baik untuk kuantifikasi eksistensial dan universal, bagi saya masuk akal bahwa Anda juga harus diizinkan untuk menggunakan Type: Trait untuk kuantifikasi universal dan eksistensial.

Mengenai kekuatan ekspresif, yang pertama lebih kuat daripada yang terakhir seperti yang Anda katakan, tetapi ini tidak harus demikian; Mereka bisa sama kuatnya jika kita ingin mereka menjadi AFAIK (saya sama sekali tidak mengatakan bahwa kita harus melakukan itu..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

Saya tidak yakin itu pada titik kita harus mengangkat tangan kita dan berkata "mari kita terapkan segalanya". Tidak ada argumen yang jelas di sini tentang keuntungan.

Argumen saya adalah bahwa pengguna yang mengejutkan dengan "Sintaks ini dapat digunakan di tempat lain dan artinya jelas di sini, tetapi Anda tidak dapat menulisnya di tempat ini" lebih mahal daripada memiliki dua cara untuk melakukannya (yang Anda berdua harus membiasakan diri dengan lagian ). Kami telah melakukan hal serupa dengan https://github.com/rust-lang/rfcs/pull/2300 (digabung), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https ://github.com/rust-lang/rfcs/pull/2175 (digabung) tempat kami mengisi lubang konsistensi meskipun sebelumnya dimungkinkan untuk menulis dengan cara lain.

Ini dapat dibaca, tetapi saya tidak berpikir itu hampir sejelas/eksplisit bahwa tipe eksistensial sedang digunakan.

Terbaca sudah cukup dalam pandangan saya; Saya tidak berpikir Rust menganggap "eksplisit di atas segalanya" dan menjadi terlalu bertele-tele (yang menurut saya sintaksnya ketika digunakan terlalu banyak) juga menghabiskan biaya dengan mengecilkan penggunaan.
(Jika Anda ingin sesuatu sering digunakan, berikan sintaks terser... cf ? sebagai suap terhadap .unwrap() ).

Ini diperburuk ketika definisi harus dibagi menjadi beberapa baris karena keterbatasan sintaks ini.

Ini saya tidak mengerti; Tampaknya bagi saya bahwa Assoc = impl Trait seharusnya menyebabkan pemisahan baris lebih dari Assoc: Trait hanya karena yang pertama lebih panjang.

Saya akan mengatakan kami hanya mendukung where Iter: Iterator<Item = T>, T: Baz (seperti yang kami miliki sekarang) atau melanjutkan dengan Iter = impl Iterator<Item = impl Baz> (seperti yang Anda sarankan).
Hanya membiarkan rumah setengah jalan tampaknya sedikit merepotkan.

Tepat!, jangan pergi setengah jalan / cop-out dan menerapkan where Iter: Iterator<Item: Baz> ;)

@Centril Oke, Anda telah memenangkan saya, terutama pada argumen simetri/konsistensi. Ergonomi memiliki kedua bentuk juga membantu. Serat tebal adalah suatu keharusan untuk fitur ini, tidak lama kemudian.

Akan mengedit ini dengan balasan lengkap saya besok.

Sunting

Seperti yang ditunjukkan oleh : Trait (terikat). misalnya

fn foo<T: Trait>(x: T) { ... }

di samping tipe universal yang "tepat" atau "terwujud", mis

fn foo(x: impl Trait) { ... }

Tentu saja, yang pertama lebih kuat daripada yang terakhir, tetapi yang terakhir lebih eksplisit (dan bisa dibilang lebih mudah dibaca) ketika hanya itu yang diperlukan. Bahkan, saya sangat percaya kita harus memiliki kompilator lint yang mendukung bentuk yang terakhir jika memungkinkan.

Sekarang, kita sudah memiliki impl Trait di posisi pengembalian fungsi juga, yang mewakili tipe eksistensial. Jenis ciri yang terkait berbentuk eksistensial, dan sudah menggunakan sintaks : Trait .

Ini, mengingat keberadaan apa yang akan saya bentuk baik dan terikat dari tipe universal di Rust saat ini, dan juga keberadaan bentuk yang tepat dan terikat untuk tipe eksistensial (yang terakhir hanya dalam sifat saat ini), saya sangat percaya kita harus memperluas dukungan untuk kedua bentuk yang tepat dan terikat dari tipe eksistensial ke luar sifat. Artinya, kita harus mendukung yang berikut ini baik secara umum maupun untuk tipe terkait .

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

Saya mendukung perilaku linting kompiler yang disarankan dalam komentar ini juga, yang seharusnya sangat mengurangi variasi ekspresi tipe eksistensial umum di alam liar.

Saya masih percaya bahwa menggabungkan kuantifikasi universal dan eksistensial di bawah satu kata kunci adalah sebuah kesalahan, sehingga argumen konsistensi tidak berhasil untuk saya. Satu-satunya alasan satu kata kunci berfungsi dalam tanda tangan fungsi adalah konteksnya selalu membatasi Anda untuk hanya menggunakan satu bentuk kuantifikasi di setiap posisi. Ada gula potensial yang bisa saya lihat sebagai hal di mana Anda tidak memiliki kendala yang sama

struct Foo {
    pub foo: impl Display,
}

Apakah ini singkatan untuk kuantifikasi eksistensial atau universal? Dari intuisi yang diperoleh dari penggunaan impl Trait dalam tanda tangan fungsi, saya tidak melihat bagaimana Anda bisa memutuskan. Jika Anda benar-benar mencoba menggunakannya sebagai keduanya, Anda akan segera menyadari bahwa kuantifikasi universal anonim dalam posisi ini tidak berguna, jadi itu pasti kuantifikasi eksistensial, tetapi itu tampaknya tidak konsisten dengan impl Trait dalam argumen fungsi.

Ini adalah dua operasi yang berbeda secara fundamental, ya keduanya menggunakan batas sifat, tetapi saya tidak melihat alasan bahwa memiliki dua cara untuk mendeklarasikan tipe eksistensial akan mengurangi kebingungan bagi pendatang baru. Jika mencoba menggunakan type Name: Trait sebenarnya merupakan hal yang mungkin dilakukan oleh pendatang baru, maka ini dapat diselesaikan melalui lint:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

Dan saya baru saja datang dengan formulasi alternatif dari argumen Anda bahwa saya akan jauh lebih setuju, itu harus menunggu sampai saya di komputer nyata untuk membaca ulang RFC dan memposting tentangnya.

Saya merasa belum memiliki cukup pengalaman dengan Rust untuk mengomentari RFC. Namun, saya tertarik melihat fitur ini digabung menjadi Rust malam dan stabil, untuk menggunakannya dengan Rust libp2p untuk membangun protokol sharding untuk Ethereum sebagai bagian dari implementasi sharding Drops of Diamond . Saya berlangganan masalah ini, namun saya tidak punya waktu untuk mengikuti semua komentar! Bagaimana saya bisa tetap up to date pada tingkat tinggi tentang masalah ini, tanpa harus membaca sekilas komentar?

Saya merasa belum memiliki cukup pengalaman dengan Rust untuk mengomentari RFC. Namun, saya tertarik melihat fitur ini digabung menjadi Rust malam dan stabil, untuk menggunakannya dengan Rust libp2p untuk membangun protokol sharding untuk Ethereum sebagai bagian dari implementasi sharding Drops of Diamond . Saya berlangganan masalah ini, namun saya tidak punya waktu untuk mengikuti semua komentar! Bagaimana saya bisa tetap up to date pada tingkat tinggi tentang masalah ini, tanpa harus membaca sekilas komentar? Saat ini sepertinya saya hanya perlu melakukan itu dengan memeriksa dari waktu ke waktu, dan tidak berlangganan masalah ini. Akan lebih baik jika saya bisa berlangganan melalui email untuk mendapatkan berita tingkat tinggi tentang ini.

Saya masih percaya bahwa menggabungkan kuantifikasi universal dan eksistensial di bawah satu kata kunci adalah kesalahan, sehingga argumen konsistensi tidak berhasil untuk saya.

Sebagai prinsip umum, terlepas dari fitur ini, saya menemukan alur penalaran ini bermasalah.

Saya percaya bahwa kita harus mendekati desain bahasa dari bagaimana bahasa itu daripada bagaimana kita berharap itu berada di bawah beberapa penyingkapan sejarah yang bergantian. Sintaks impl Trait sebagai kuantifikasi universal dalam posisi argumen distabilkan sehingga Anda tidak dapat menghapusnya. Bahkan jika Anda percaya X, Y dan Z adalah kesalahan (dan saya dapat menemukan banyak hal yang menurut saya pribadi adalah kesalahan dalam desain Rust, tetapi saya menerima dan menganggapnya ...), kita harus hidup dengan mereka sekarang, dan saya pikir tentang bagaimana kita bisa membuat semuanya cocok satu sama lain dengan adanya fitur baru (membuat semuanya konsisten).

Dalam diskusi, saya pikir seluruh korpus RFC dan bahasa apa adanya harus dianggap seolah-olah bukan aksioma, lalu argumen yang kuat.


Anda dapat membuat kasus (tetapi saya tidak akan) bahwa:

struct Foo {
    pub foo: impl Display,
}

secara semantik setara dengan:

struct Foo<T: Display> {
    pub foo: T,
}

di bawah penalaran fungsi-argumen.

Pada dasarnya, mengingat impl Trait , Anda harus berpikir "apakah ini tipe-kembali, atau seperti argumen?" , yang mungkin sulit.


Jika mencoba menggunakan type Name: Trait sebenarnya merupakan hal yang mungkin dilakukan oleh pendatang baru, maka ini dapat diselesaikan melalui lint:

Saya juga akan lint, tetapi ke arah lain; Saya pikir cara-cara berikut harus idiomatis:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Oke, formulasi alternatif yang menurut saya diisyaratkan oleh RFC 2071 dan mungkin telah dibahas dalam masalah ini, tetapi tidak pernah secara eksplisit dinyatakan:

Hanya ada _satu cara_ untuk mendeklarasikan tipe yang dikuantifikasi secara eksistensial: existential type Name: Bound; (menggunakan existential karena itu ditentukan dalam RFC, saya tidak sepenuhnya menentang menjatuhkan kata kunci di bawah formulasi ini).

Ada tambahan gula untuk secara implisit mendeklarasikan jenis yang dikuantifikasi secara eksistensial tanpa nama pada cakupan saat ini: impl Bound (mengabaikan gula kuantifikasi universal dalam argumen fungsi untuk saat ini).

Jadi, penggunaan tipe pengembalian saat ini adalah desugaring sederhana:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

memperluas ke const , static dan let juga sepele.

Satu ekstensi yang tidak disebutkan dalam RFC adalah: mendukung gula ini dalam sintaks type Alias = Concrete; , jadi ketika Anda menulis

type Foo = impl Iterator<Item = impl Display>;

ini sebenarnya gula untuk

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

yang kemudian bergantung pada sifat transparan dari alias tipe untuk memungkinkan modul saat ini melihat melalui Foo dan melihat bahwa itu merujuk pada tipe eksistensial.

Bahkan, saya sangat percaya kita harus memiliki kompilator lint yang mendukung bentuk yang terakhir jika memungkinkan.

Saya sebagian besar selaras dengan komentar @alexreg , tetapi saya memiliki beberapa kekhawatiran tentang linting ke arg: impl Trait , terutama karena risiko mendorong perubahan yang merusak perpustakaan karena impl Trait tidak berfungsi dengan turbofish (sekarang, dan Anda membutuhkan turbofish parsial untuk membuatnya bekerja dengan baik). Oleh karena itu, linting di clippy terasa kurang mudah dibandingkan dengan tipe alias (di mana tidak ada turbofish yang menyebabkan masalah).

Saya sebagian besar selaras dengan komentar @alexreg , tetapi saya memiliki beberapa kekhawatiran tentang linting terhadap arg: impl Trait, terutama karena risiko mendorong perubahan yang merusak perpustakaan karena impl Trait tidak berfungsi dengan turbofish (sekarang, dan Anda membutuhkan turbofish parsial untuk membuatnya bekerja dengan baik). Oleh karena itu, linting di clippy terasa kurang mudah dibandingkan dengan tipe alias (di mana tidak ada turbofish yang menyebabkan masalah).

@Centril baru saja

Jadi... kita sudah cukup banyak berdiskusi tentang sintaks untuk tipe-tipe eksistensial bernama sekarang. Haruskah kita mencoba mengambil kesimpulan dan menuliskannya ke dalam posting RFC / PR, sehingga seseorang dapat mulai mengerjakan implementasi yang sebenarnya? :-)

Secara pribadi, setelah kami menamai eksistensial, saya lebih suka lint (jika ada) jauh dari penggunaan impl Trait mana saja .

@rpjohnst Yah Anda tentu setuju dengan saya dan @Centril sehubungan dengan eksistensial bernama... untuk menjauh dari mereka dalam argumen fungsi, itu kemungkinan, tapi mungkin diskusi untuk tempat lain. Itu tergantung apakah seseorang ingin mendukung kesederhanaan atau umum dalam konteks ini.

Apakah RFC pada impl Trait dalam posisi argumen mutakhir? Jika demikian, apakah aman untuk menyatakan bahwa semantiknya adalah _universal_? Jika demikian: Saya ingin menangis. Dalam.

@phaazon : Catatan rilis Rust 1.26 untuk impl Trait :

Catatan tambahan untuk Anda mengetik ahli teori di luar sana: ini bukan eksistensial, masih universal. Dengan kata lain, sifat impl bersifat universal pada posisi input, tetapi eksistensial pada posisi output.

Hanya untuk mengungkapkan pemikiran saya tentang itu:

  • Kami sudah memiliki sintaks untuk variabel tipe dan sebenarnya, ada beberapa kegunaan untuk variabel tipe anynomous (yaitu sangat sering Anda ingin menggunakan variabel tipe di beberapa tempat alih-alih hanya menjatuhkannya di satu tempat).
  • Eksistensial kovarian akan membuka pintu untuk fungsi peringkat-n, sesuatu yang sulit dilakukan saat ini tanpa sifat (lihat ini ) dan merupakan fitur yang benar-benar hilang dari Rust.
  • impl Trait mudah untuk disebut sebagai "type pick by the callee", karena… karena itu adalah satu-satunya konstruksi bahasa untuk saat ini yang memungkinkan kita untuk melakukannya! Memilih jenis oleh penelepon sudah tersedia melalui beberapa konstruksi.

Saya benar-benar berpikir impl Trait dalam posisi argumen keputusan saat ini sangat disayangkan. :menangis:

Saya benar-benar berpikir Sifat impl dalam posisi argumen keputusan saat ini sangat disayangkan. 😢.

Meskipun saya agak bingung dengan ini, saya pasti berpikir waktu akan lebih baik dihabiskan untuk mengimplementasikan let x: impl Trait sekarang!

Eksistensial kovarian akan membuka pintu bagi kita untuk fungsi peringkat-n

Kami sudah memiliki sintaks untuk itu ( fn foo(f: impl for<T: Trait> Fn(T)) ), (alias "ketik HRTB"), tetapi belum diimplementasikan. fn foo(f: impl Fn(impl Trait)) menghasilkan kesalahan bahwa "bersarang impl Trait tidak diperbolehkan", dan saya berharap kita akan menginginkannya berarti versi peringkat yang lebih tinggi, ketika kita mendapatkan tipe HRTB.

Ini mirip dengan bagaimana Fn(&'_ T) berarti for<'a> Fn(&'a T) , jadi saya tidak berharap itu menjadi kontroversial.

Melihat draf saat ini, impl Trait dalam posisi argumen adalah _universal_, tetapi Anda mengatakan bahwa impl for<_> Trait mengubahnya menjadi _eksistensial_?! Seberapa gila itu?

Mengapa kami pikir kami perlu memperkenalkan _cara lain_ untuk membangun _universal_? Maksudku:

fn foo(x: impl MyTrait)

Hanya menarik karena variabel tipe anonim hanya muncul sekali dalam tipe . Jika Anda perlu mengembalikan jenis yang sama:

fn foo(x: impl Trait) -> impl Trait

Jelas tidak akan berhasil. Kami memberi tahu orang-orang untuk beralih dari idiom yang lebih umum ke tidak memiliki where dan parameter-templat di tempat.

Argh saya kira semua ini sudah diterima dan saya mengomel untuk apa-apa. Saya hanya berpikir itu sangat disayangkan. Saya cukup yakin saya bukan satu-satunya yang kecewa dengan keputusan RFC.

(Mungkin tidak ada gunanya terus memperdebatkan ini setelah fitur distabilkan, tetapi lihat di sini untuk argumen yang meyakinkan (yang saya setujui) mengapa impl Trait dalam posisi argumen memiliki semantik yang masuk akal dan koheren. Tl;dr itu untuk alasan yang sama mengapa fn foo(arg: Box<Trait>) bekerja dengan cara yang kira-kira sama dengan fn foo<T: Trait>(arg: Box<T>) , meskipun dyn Trait adalah eksistensial; sekarang gantikan dyn dengan impl .)

Melihat draf saat ini, impl Trait dalam posisi argumen adalah universal, tetapi Anda mengatakan bahwa impl for<_> Trait mengubahnya menjadi eksistensial?!

Tidak, keduanya universal. Saya mengatakan bahwa penggunaan dengan peringkat lebih tinggi akan terlihat seperti ini:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

yang pada saat yang sama dapat ditambahkan (yaitu tanpa perubahan pada impl Trait ) dapat ditulis sebagai:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

Itu universal impl Trait , hanya saja Trait adalah HRTB (mirip dengan impl for<'a> Fn(&'a T) ).
Jika kami memutuskan (yang saya harapkan mungkin) bahwa argumen impl Trait di dalam Fn(...) juga universal, Anda dapat menulis ini untuk mencapai efek yang sama:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

Inilah yang saya pikir Anda maksud dengan "peringkat lebih tinggi", jika tidak, beri tahu saya.

Keputusan bahkan lebih menarik bisa untuk menerapkan perlakuan yang sama di posisi eksistensial, yaitu memungkinkan ini (yang berarti "kembali beberapa penutupan yang mengambil setiap penutupan lainnya"):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

untuk ditulis seperti ini:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Itu akan menjadi eksistensial impl Trait mengandung universal yang impl Trait (terikat pada eksistensial, bukan fungsi melampirkan).

@eddyb Bukankah lebih masuk akal untuk memiliki dua kata kunci terpisah untuk kuantifikasi eksistensial dan universal secara umum, untuk konsistensi dan tidak membingungkan pemula?
Bukankah kata kunci untuk kuantifikasi eksistensial juga dapat digunakan kembali untuk tipe eksistensial?
Mengapa kita menggunakan impl untuk kuantifikasi eksistensial ( dan universal) tetapi existential untuk tipe eksistensial?

Saya ingin membuat tiga poin:

  • Tidak banyak gunanya membahas apakah impl Trait itu eksistensial atau universal. Sebagian besar programmer di luar sana mungkin tidak cukup membaca buku pegangan teori tipe. Pertanyaannya adalah apakah orang menyukainya atau apakah mereka merasa membingungkan. Untuk menjawab pertanyaan itu, beberapa bentuk umpan balik dapat dilihat baik di sini, di utas ini, di reddit, atau di forum . Jika ada sesuatu yang perlu dijelaskan lebih lanjut, itu gagal dalam tes litmust untuk fitur intuitif atau tidak mengejutkan. Jadi kita harus melihat berapa banyak orang dan seberapa bingungnya mereka dan apakah itu lebih banyak pertanyaan daripada dengan fitur lain. Sungguh menyedihkan umpan balik ini datang setelah stabilisasi dan sesuatu harus dilakukan tentang fenomena ini, tetapi ini untuk diskusi terpisah.
  • Secara teknis, bahkan setelah stabilisasi, akan ada cara untuk menyingkirkan fitur dalam kasus ini (mengesampingkan keputusan jika perlu). Dimungkinkan untuk menentang fungsi penulisan yang menggunakan itu dan menghapus kemampuan di edisi berikutnya (sambil mempertahankan kemampuan untuk memanggilnya jika mereka berasal dari peti edisi berbeda). Itu akan memenuhi jaminan stabilitas karat.
  • Tidak, menambahkan dua kata kunci lagi untuk menentukan tipe eksistensial dan universal tidak akan memperbaiki kebingungan, itu hanya akan memperburuk keadaan.

Sungguh menyedihkan umpan balik ini datang setelah stabilisasi dan sesuatu harus dilakukan tentang fenomena ini

Sudah ada keberatan untuk impl Trait dalam posisi argumen selama itu ide. Umpan balik seperti ini _bukan baru_, sangat diperdebatkan bahkan di utas RFC yang relevan. Ada banyak diskusi tidak hanya tentang tipe universal/eksistensial dari perspektif teori tipe, tetapi juga tentang bagaimana hal ini akan membingungkan bagi pengguna baru.

Benar, kami tidak mendapatkan perspektif pengguna baru yang sebenarnya, tetapi ini tidak muncul begitu saja.

@Boscop any dan some diusulkan sebagai pasangan kata kunci untuk melakukan pekerjaan ini tetapi diputuskan (walaupun saya tidak tahu apakah alasannya pernah ditulis di mana pun).

Benar, kami tidak bisa mendapatkan umpan balik dari orang-orang yang baru mengenal karat dan yang bukan ahli teori tipe

Dan argumen untuk penyertaan selalu bahwa hal itu akan memudahkan pendatang baru. Jadi jika kita sekarang memiliki umpan balik yang sebenarnya dari pendatang baru, bukankah itu seharusnya menjadi jenis umpan balik yang sangat relevan daripada berdebat tentang bagaimana pendatang baru harus memahaminya?

Saya kira jika ada yang punya waktu, semacam penelitian melalui forum dan tempat lain betapa bingungnya orang sebelum dan sesudah inklusi dapat dilakukan (saya tidak pandai statistik, tapi saya cukup yakin seseorang yang bisa datang dengan sesuatu yang lebih baik daripada prediksi buta).

Dan argumen untuk penyertaan selalu bahwa hal itu akan memudahkan pendatang baru. Jadi jika kita sekarang memiliki umpan balik yang sebenarnya dari pendatang baru, bukankah itu seharusnya menjadi jenis umpan balik yang sangat relevan daripada berdebat tentang bagaimana pendatang baru harus memahaminya?

Ya? Maksud saya, saya tidak sedang memperdebatkan apakah yang terjadi adalah ide yang baik atau buruk. Saya hanya bermaksud menunjukkan bahwa utas RFC memang mendapatkan umpan balik tentang ini, dan itu sudah diputuskan.

Seperti yang Anda katakan, mungkin lebih baik melakukan diskusi meta tentang umpan balik di tempat lain, meskipun saya tidak yakin di mana itu.

Tidak, menambahkan dua kata kunci lagi untuk menentukan tipe eksistensial dan universal tidak akan memperbaiki kebingungan, itu hanya akan memperburuk keadaan.

Lebih buruk? Bagaimana begitu? Saya lebih suka memiliki lebih banyak untuk diingat daripada ambiguitas / kebingungan.

Ya? Maksud saya, saya tidak sedang memperdebatkan apakah yang terjadi adalah ide yang baik atau buruk. Saya hanya bermaksud menunjukkan bahwa utas RFC memang mendapatkan umpan balik tentang ini, dan itu sudah diputuskan.

Tentu. Tetapi kedua belah pihak yang berdebat adalah programmer tua, bekas luka dan berpengalaman dengan pemahaman mendalam tentang apa yang terjadi di bawah tenda, menebak tentang kelompok yang bukan bagian dari mereka (pendatang baru) dan menebak tentang masa depan. Dari sudut pandang faktual, itu tidak jauh lebih baik daripada melempar dadu dalam hal apa yang sebenarnya terjadi dalam kenyataan. Ini bukan tentang pengalaman para ahli yang tidak memadai, tetapi tentang tidak memiliki data yang memadai untuk mendasarkan keputusan.

Sekarang diperkenalkan dan kami memiliki cara untuk mendapatkan data keras yang sebenarnya, atau data keras apa pun yang dapat diperoleh di tanah tentang berapa banyak orang yang bingung dalam skala dari 0 hingga 10.

Seperti yang Anda katakan, mungkin lebih baik melakukan diskusi meta tentang umpan balik di tempat lain

Sebagai contoh di sini, saya sudah memulai diskusi seperti itu dan ada beberapa langkah nyata yang dapat diambil, meskipun kecil: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -sebelum-menstabilkan-fitur/7522/14. Saya tidak punya waktu untuk menulis RFC, jadi jika seseorang memukul saya atau ingin membantu, saya tidak keberatan.

Lebih buruk? Bagaimana begitu?

Karena, kecuali impl Trait tidak digunakan lagi, Anda memiliki semua 3, oleh karena itu harus lebih banyak mengingat selain kebingungan. Jika impl Trait akan hilang, situasinya akan berbeda dan itu akan menjadi pembobotan pro dan kontra dari kedua pendekatan tersebut.

impl Trait seperti pada callee-picking sudah cukup. Jika Anda mencoba menggunakannya dalam posisi argumen, maka Anda menimbulkan kebingungan. HRTB akan menghilangkan kebingungan itu.

@vorner Sebelumnya saya berpendapat bahwa kita harus melakukan pengujian A/B yang sebenarnya dengan pemula Rust untuk melihat apa yang mereka anggap lebih mudah dan lebih sulit untuk dipelajari, karena sulit ditebak sebagai seseorang yang mahir dengan Rust.
FWIW, saya ingat, ketika saya belajar Rust (berasal dari C++, D, Java dll), generik tipe kuantifikasi universal (termasuk sintaksnya) mudah dimengerti (masa pakai generik sedikit lebih sulit).
Saya pikir impl Trait untuk tipe arg akan menghasilkan banyak kebingungan dari pemula dan banyak pertanyaan seperti ini .
Jika tidak ada bukti bahwa perubahan akan membuat Rust lebih mudah dipelajari, kita harus menahan diri dari membuat perubahan tersebut, dan sebaliknya membuat perubahan yang membuat/menjaga Rust lebih konsisten karena konsistensi membuatnya setidaknya mudah diingat. Pemula karat harus membaca buku beberapa kali, jadi memperkenalkan impl Trait untuk argumen untuk memungkinkan menunda obat generik dalam buku sampai nanti tidak benar-benar menghilangkan kerumitan apa pun.

@eddyb Btw, kenapa kita perlu kata kunci existential untuk tipe selain impl ? (Saya berharap kita akan menggunakan some untuk keduanya..)

FWIW, saya ingat, ketika saya belajar Rust (berasal dari C++, D, Java dll), generik tipe kuantifikasi universal (termasuk sintaksnya) mudah dimengerti (masa pakai generik sedikit lebih sulit).

Saya sendiri juga tidak menganggap itu masalah. Di perusahaan saya saat ini, saya memimpin kelas Rust untuk saat ini kami bertemu seminggu sekali dan saya mencoba mengajar dalam implementasi praktis. Orang-orangnya adalah programmer berpengalaman, sebagian besar berasal dari latar belakang Java dan Scala. Meskipun ada beberapa hambatan, obat generik (setidaknya membacanya mereka agak berhati-hati untuk benar-benar menulisnya) dalam posisi argumen tidak menjadi masalah. Ada sedikit kejutan tentang obat generik dalam posisi kembali (mis. pemanggil memilih apa yang dikembalikan oleh fungsi), terutama yang sering dapat dihilangkan, tetapi penjelasannya membutuhkan waktu 2 menit sebelum diklik. Tetapi saya bahkan takut untuk menyebutkan keberadaan Sifat impl dalam posisi argumen, karena sekarang saya harus menjawab pertanyaan mengapa itu ada dan saya tidak memiliki jawaban nyata untuk itu. Itu tidak baik untuk motivasi dan memiliki motivasi sangat penting untuk proses belajar.

Lantas, pertanyaannya, apakah masyarakat memiliki cukup suara untuk membuka kembali perdebatan dengan sejumlah data untuk mendukung argumentasi?

@eddyb Btw, mengapa kita membutuhkan kata kunci eksistensial lain untuk tipe selain impl? (Saya berharap kami akan menggunakan beberapa untuk keduanya ..)

Kenapa tidak forall … /me perlahan menyelinap pergi

@phaazon Kami memiliki forall (yaitu "universal") dan itu for , misalnya di HRTB ( for<'a> Trait<'a> ).

@eddyb Ya, lalu gunakan untuk eksistensial juga, seperti yang dilakukan Haskell dengan forall , misalnya.

Seluruh diskusi sangat berpendirian, saya agak terkejut bahwa ide argumen terlihat stabil. Saya harap ada cara untuk mendorong RFC lain nanti untuk membatalkannya (saya benar-benar bersedia untuk menulisnya karena saya benar-benar tidak suka semua kebingungan yang akan terjadi).

Aku tidak mengerti. Apa gunanya memiliki mereka dalam posisi argumen? Saya tidak banyak menulis Rust, tetapi saya sangat senang bisa melakukan -> impl Trait . Kapan saya akan menggunakannya dalam posisi argumen?

Pemahaman saya adalah bahwa itu sebagian besar untuk konsistensi. Jika saya dapat menulis tipe impl Trait dalam posisi argumen di tanda tangan fn, mengapa saya tidak dapat menulisnya di tempat lain?

Yang mengatakan, saya pribadi lebih suka memberi tahu orang-orang "cukup gunakan parameter tipe" ...

Ya, itu untuk konsistensi. Tapi saya tidak yakin itu argumen yang cukup bagus, ketika parameter tipe sangat mudah digunakan. Juga, masalah kemudian muncul yang harus ditentang/dilawan!

Juga, masalah kemudian muncul yang harus ditentang/dilawan!

Mengingat Anda tidak dapat mengekspresikan beberapa hal dengan impl Trait sama sekali, berfungsi dengan impl Trait sebagai salah satu argumen yang tidak dapat melakukan turbofish dan oleh karena itu Anda tidak dapat mengambil alamatnya (apakah saya lupa beberapa kelemahan lainnya?), Saya pikir tidak masuk akal untuk menggunakan parameter tipe, karena Anda tetap harus menggunakannya.

oleh karena itu Anda tidak dapat mengambil alamatnya

Anda bisa, dengan menyimpulkannya dari tanda tangan.

Apa gunanya memiliki mereka dalam posisi argumen?

Tidak ada karena itu sama persis dengan menggunakan sifat terikat.

fn foo(x: impl Debug)

Apakah hal yang sama persis dengan

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Juga, pertimbangkan ini:

fn foo<A>(x: A) -> A where A: Debug

impl Trait dalam posisi argumen tidak mengizinkan Anda melakukan ini karena dianonimkan . Ini adalah fitur yang sangat tidak berguna karena kita sudah memiliki semua yang kita butuhkan untuk menghadapi situasi seperti itu. Orang tidak akan mempelajari fitur baru itu dengan mudah karena hampir semua orang tahu variabel tipe/parameter template dan Rust adalah satu-satunya bahasa yang menggunakan sintaks impl Trait . Itu sebabnya banyak orang mengomel bahwa itu seharusnya tetap pada nilai pengembalian / biarkan binding, karena itu memperkenalkan semantik baru yang dibutuhkan (yaitu tipe yang dipilih oleh callee).

Singkatnya, @iopq : Anda tidak akan membutuhkan ini, dan tidak ada gunanya selain "Mari kita tambahkan konstruksi gula sintaksis lain yang sebenarnya tidak diperlukan oleh siapa pun karena ini mengatasi penggunaan yang sangat spesifik - yaitu variabel tipe anonim" .

Juga, sesuatu yang saya lupa katakan: itu membuat lebih sulit untuk melihat bagaimana fungsi Anda diparameterkan/dimonomorfisasi.

@Verner Dengan

Bagaimana konsisten ketika -> impl Trait callee memilih jenis, sedangkan di x: impl Trait callee memilih jenis?

Saya mengerti bahwa tidak ada cara lain untuk bekerja, tetapi itu tampaknya tidak "konsisten", tampaknya kebalikan dari konsisten

Saya benar-benar setuju bahwa itu segalanya tapi konsisten dan bahwa orang akan bingung, pendatang baru serta rustacean mahir tingkat lanjut.

Kami memiliki dua RFC, yang menerima total hampir 600 komentar di antara mereka mulai lebih dari 2 tahun yang lalu, untuk menyelesaikan pertanyaan yang diajukan kembali di utas ini:

  • rust-lang/rfcs#1522 ("Minimal impl Trait ")
  • rust-lang/rfcs#1951 ("Selesaikan sintaks dan pelingkupan parameter untuk impl Trait , sambil memperluasnya ke argumen")

(Jika Anda membaca diskusi ini, Anda akan melihat bahwa pada awalnya saya adalah pendukung kuat dari pendekatan dua kata kunci. Sekarang saya pikir menggunakan satu kata kunci adalah pendekatan yang tepat.)

Setelah 2 tahun dan ratusan komentar, keputusan tercapai dan fitur tersebut kini telah stabil. Ini adalah masalah pelacakan untuk fitur tersebut, yang terbuka untuk melacak kasus penggunaan yang masih tidak stabil untuk impl Trait . Menggugat aspek yang diselesaikan dari impl Trait adalah di luar topik untuk masalah pelacakan ini. Anda dipersilakan untuk terus membicarakan hal ini, tetapi tolong jangan di pelacak masalah.

Bagaimana itu distabilkan ketika impl Trait bahkan belum mendapat dukungan dalam posisi argumen untuk fns dalam sifat??

@daboross Kemudian kotak centang di pos asli perlu dicentang!

(Baru mengetahui bahwa https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable saat ini berfungsi)

Saya pikir itu aneh bahwa https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug tidak berfungsi (belum), terlebih lagi dengan pesan kesalahan ini. Apakah saya satu-satunya yang berpikir seperti ini? Mungkin perlu menambahkan kotak centang untuk impl Trait dalam posisi pengembalian dalam sifat, atau apakah itu keputusan sadar untuk hanya mengizinkan impl Trait dalam posisi argumen untuk fungsi sifat, memaksa penggunaan existential type untuk tipe pengembalian? (yang… akan terlihat tidak konsisten bagi saya, tapi mungkin saya melewatkan satu poin?)

@Ekleog

apakah itu keputusan sadar untuk hanya mengizinkan Sifat impl dalam posisi argumen untuk fungsi sifat, memaksa penggunaan tipe eksistensial untuk tipe pengembalian?

Ya-- kembalikan posisi impl Trait dalam sifat ditunda sampai kami memiliki pengalaman yang lebih praktis menggunakan tipe eksistensial dalam sifat.

@cramertj Apakah kita belum pada titik di mana kita memiliki pengalaman praktis yang cukup untuk mengimplementasikannya?

Saya ingin melihat Sifat impl dalam beberapa rilis stabil sebelum kami menambahkan lebih banyak fitur.

@mark-im Saya gagal melihat apa yang kontroversial tentang posisi kembali impl Trait untuk metode sifat, secara pribadi... mungkin saya melewatkan sesuatu.

Saya tidak berpikir itu kontroversial. Saya hanya merasa kami menambahkan fitur terlalu cepat. Akan menyenangkan untuk berhenti dan fokus pada utang teknis untuk sementara waktu dan mendapatkan pengalaman dengan set fitur saat ini terlebih dahulu.

Jadi begitu. Saya kira saya hanya menganggapnya sebagai bagian yang hilang dari fitur yang ada daripada fitur baru.

Saya pikir @alexreg benar, sangat menggoda untuk menggunakan impl Trait eksistensial pada metode sifat. Ini sebenarnya bukan fitur baru, tapi saya rasa ada beberapa hal yang harus diperhatikan sebelum mencoba mengimplementasikannya?

@phaazon Mungkin, ya... Saya tidak begitu tahu seberapa banyak detail implementasi akan berbeda dibandingkan dengan apa yang sudah kita miliki saat ini, tetapi mungkin seseorang dapat mengomentarinya. Saya ingin melihat tipe eksistensial untuk ikatan let/const juga, tetapi saya pasti dapat menerimanya sebagai fitur di luar yang ini, sehingga menunggu siklus lain atau lebih sebelum memulainya.

Saya ingin tahu apakah kita dapat menahan sifat impl universal dalam sifat...

Tapi ya, kurasa aku mengerti maksudmu.

@mark-im Tidak, kami tidak bisa, mereka sudah stabil .

Mereka dalam fungsi, tetapi bagaimana dengan deklarasi Sifat?

@mark-im seperti yang ditunjukkan cuplikan, mereka stabil dalam deklarasi impls dan sifat..

Hanya melompat untuk mengejar di mana kita menetap dengan abstract type . Secara pribadi, saya cukup paham dengan sintaks dan praktik terbaik

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Yang diterapkan pada beberapa kode saya, saya kira akan terlihat seperti:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

Tidak masuk akal bagi saya bahwa type Bar = (impl Display,); akan baik, tetapi type Bar = impl Display; akan buruk.

Jika kita memutuskan sintaks tipe eksistensial alternatif yang berbeda (semua berbeda dari rfc 2071 ?), apakah utas forum di https://users.rust-lang.org/ menjadi tempat yang baik untuk melakukan itu?

Saya tidak memiliki cukup pemahaman tentang alternatif untuk memulai utas seperti itu sekarang, tetapi karena tipe eksistensial masih belum diterapkan, saya pikir diskusi di forum dan kemudian RFC baru mungkin akan lebih baik daripada membicarakannya dalam masalah pelacakan .

Apa yang salah dengan type Foo = impl Trait ?

@daboross Mungkin forum internal saja. Saya sedang mempertimbangkan untuk menulis RFC tentang itu untuk menyelesaikan sintaks.

@daboross Sudah lebih dari cukup diskusi tentang sintaks di utas ini. Saya pikir jika @Centril dapat menulis RFC untuk itu pada saat ini, maka bagus.

Apakah ada masalah yang saya dapat berlangganan untuk diskusi tentang eksistensial dalam sifat?

Apakah ada argumen terkait makro untuk satu sintaks atau yang lain?

@tomaka dalam kasus pertama type Foo = (impl Display,) benar-benar satu-satunya sintaks yang Anda miliki. Preferensi saya untuk type Foo: Trait daripada type Foo = impl Trait hanya berasal dari fakta bahwa kami mengikat jenis yang dapat kami beri nama, seperti <TFoo: Trait> atau where TFoo: Trait , sedangkan dengan impl Trait kami tidak dapat menyebutkan jenisnya.

Untuk memperjelas, saya tidak mengatakan bahwa type Foo = impl Bar itu buruk, saya mengatakan type Foo: Bar lebih baik dalam kasus-kasus sederhana, sebagian karena motivasi @KodrAus .

Yang terakhir saya baca sebagai: "tipe Foo memenuhi Bar" dan yang pertama sebagai: "tipe Foo sama dengan beberapa tipe yang memenuhi Bar". Yang pertama, menurut saya, lebih langsung dan alami dari pandangan ekstensional ("apa yang bisa saya lakukan dengan Foo"). Untuk memahami yang terakhir, Anda perlu melibatkan pemahaman yang lebih dalam tentang kuantifikasi eksistensial jenis.

type Foo: Bar juga cukup rapi karena jika itu adalah sintaks yang digunakan sebagai terikat pada tipe terkait dalam suatu sifat, maka Anda cukup menyalin deklarasi dalam sifat tersebut ke impl dan itu akan berfungsi (jika itu semua informasi yang ingin Anda ungkapkan..).

Sintaksnya juga terser, terutama ketika batas tipe terkait terlibat dan ketika ada banyak tipe terkait. Ini dapat mengurangi kebisingan dan karenanya membantu keterbacaan.

@KodrAus

Inilah cara saya membaca definisi tipe tersebut:

  • type Foo: Trait berarti " Foo adalah tipe yang mengimplementasikan Trait "
  • type Foo = impl Trait berarti " Foo adalah alias dari beberapa jenis yang mengimplementasikan Trait "

Bagi saya, Foo: Trait cukup mendeklarasikan batasan pada Foo mengimplementasikan Trait . Di satu sisi, type Foo: Trait terasa tidak lengkap. Sepertinya kita memiliki batasan, tetapi definisi sebenarnya dari Foo tidak ada.

Di sisi lain, impl Trait menggugah "ini adalah tipe tunggal, tetapi kompiler mengetahui namanya". Oleh karena itu, type Foo = impl Trait menyiratkan bahwa kita sudah memiliki tipe konkret (yang mengimplementasikan Trait ), di mana Foo hanyalah sebuah alias.

Saya percaya type Foo = impl Trait menyampaikan arti yang benar dengan lebih jelas: Foo adalah alias dari beberapa jenis yang mengimplementasikan Trait .

@stjepang

type Foo: Trait berarti "Foo adalah tipe yang mengimplementasikan Sifat"
[..]
Di satu sisi, type Foo: Trait terasa tidak lengkap.

Ini adalah bagaimana saya membacanya juga (frasa modulo ...), dan ini adalah interpretasi yang benar secara ekstensif. Ini mengatakan semua tentang apa yang dapat Anda lakukan dengan Foo (morfisme yang diberikan oleh tipe tersebut). Oleh karena itu, ekstensional lengkap. Dari sudut pandang pembaca dan khususnya pemula, saya pikir ekstensionalitas lebih penting.

Di sisi lain, impl Trait menggugah "ini adalah tipe tunggal, tetapi kompiler mengisi celah". Oleh karena itu, type Foo = impl Trait menyiratkan bahwa kita sudah memiliki tipe konkret (yang mengimplementasikan Trait ), di mana Foo adalah alias, tetapi kompilator akan mencari tahu tipe mana yang sebenarnya.

Ini adalah interpretasi yang lebih rinci dan intensional berkaitan dengan representasi yang berlebihan dari sudut pandang ekstensional. Tapi ini lebih lengkap dalam arti intensional.

@Centril

Dari sudut pandang pembaca dan khususnya pemula, saya pikir ekstensionalitas lebih penting.

Ini adalah interpretasi yang lebih rinci dan intensif yang berkaitan dengan representasi yang berlebihan dari sudut pandang ekstensional

Dikotomi ekstensional vs intensional menarik - Saya tidak pernah memikirkan impl Trait seperti ini sebelumnya.

Namun, saya mohon berbeda pada kesimpulannya. FWIW, saya tidak pernah berhasil grok tipe eksistensial di Haskell dan Scala, jadi anggap saya sebagai pemula. :) impl Trait di Rust terasa sangat intuitif sejak hari pertama, yang mungkin disebabkan oleh fakta bahwa saya menganggapnya sebagai alias terbatas daripada apa yang dapat dilakukan dengan tipe tersebut. Jadi antara mengetahui apa Foo adalah dan apa yang bisa dilakukan dengan itu, saya memilih mantan.

Hanya 2c saya. Orang lain mungkin memiliki model mental yang berbeda dari impl Trait .

Saya setuju sepenuhnya dengan komentar ini : type Foo: Trait terasa tidak lengkap. Dan type Foo = impl Trait terasa lebih analog dengan penggunaan impl Trait tempat lain, yang membantu bahasa tersebut terasa lebih konsisten dan mudah diingat.

@joshtriplett Lihat https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 untuk memulai diskusi konsistensi; Saya percaya mengizinkan formulir formulir sebenarnya adalah hal yang konsisten untuk dilakukan. Dan hanya mengizinkan salah satu formulir (yang mana saja ...) tidak konsisten. Mengizinkan type Foo: Trait juga sangat cocok dengan https://github.com/rust-lang/rfcs/pull/2289 yang dengannya Anda dapat menyatakan: type Foo: Iterator<Item: Display>; yang membuat semuanya seragam dengan rapi.

@stjepang Perspektif ekstensional dari type Foo: Bar; tidak mengharuskan Anda untuk memahami kuantifikasi eksistensial dalam teori tipe. Yang benar-benar perlu Anda pahami adalah bahwa Foo memungkinkan Anda untuk melakukan semua operasi yang diberikan oleh Bar , itu saja. Dari sudut pandang pengguna Foo , itu juga yang menarik.

@Centril

Saya yakin saya mengerti sekarang dari mana Anda berasal dan daya tarik untuk mendorong sintaks Type: Trait ke sebanyak mungkin tempat.

Ada konotasi kuat di sekitar : digunakan untuk batasan tipe-implements-trait dan = digunakan untuk definisi tipe dan batasan tipe-sama dengan-tipe lain.

Saya pikir ini juga terlihat di RFC Anda. Misalnya, ambil dua jenis batas ini:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

Kedua batas ini pada akhirnya memiliki efek yang sama, tetapi (menurut saya) agak berbeda. Yang pertama mengatakan " Item harus mengimplementasikan sifat Bar ", sedangkan yang kedua mengatakan " Item harus sama dengan beberapa tipe yang mengimplementasikan Bar ".

Biarkan saya mencoba mengilustrasikan ide ini menggunakan contoh lain:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

Bagaimana seharusnya kita mendefinisikan tipe eksistensial yang mengimplementasikan Person ?

  • type Someone: Person , yang terlihat seperti tipe terikat.
  • type Someone = impl Person , yang terlihat seperti definisi tipe.

@stjepang Terlihat seperti tipe terikat bukanlah hal yang buruk :) Kita dapat mengimplementasikan Person for Alice seperti:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Lihat bu! Hal-hal di dalam { .. } untuk sifat dan impl adalah identik, itu berarti Anda dapat menyalin teks dari sifat yang tidak tersentuh sejauh menyangkut Name .

Karena tipe terkait adalah fungsi level tipe (di mana argumen pertama adalah Self ), kita dapat melihat alias tipe sebagai tipe terkait 0-arity, jadi tidak ada hal aneh yang terjadi.

Kedua batas ini pada akhirnya memiliki efek yang sama, tetapi (menurut saya) agak berbeda. Yang pertama mengatakan "Item harus menerapkan Bar sifat", sedangkan yang kedua mengatakan "Item harus sama dengan beberapa jenis yang menerapkan Bar".

Ya; Saya menemukan ungkapan pertama lebih to the point dan alami. :)

@Centril hehe . Apakah itu berarti type Thing; saja sudah cukup untuk memperkenalkan tipe abstrak?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm saya pikir secara teknis mungkin; tetapi Anda dapat menanyakan apakah itu diinginkan atau tidak tergantung pada pemikiran seseorang tentang implisit/eksplisit. Dalam kasus khusus itu saya pikir secara teknis cukup untuk menulis:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

dan kompiler hanya dapat menyimpulkan type Output: Sized; untuk Anda (yang merupakan ikatan yang sangat tidak menarik yang tidak memberi Anda informasi). Ini adalah sesuatu yang perlu dipertimbangkan untuk batasan yang lebih menarik, tetapi itu tidak akan ada dalam proposal awal saya karena saya pikir itu mungkin mendorong API dengan harga rendah, bahkan ketika tipe konkretnya sangat sederhana, karena kemalasan programmer :) Tidak juga type Output; awalnya untuk alasan yang sama.

Saya pikir setelah membaca ini semua, saya cenderung lebih setuju dengan @Centril. Ketika saya melihat type Foo = impl Bar Saya cenderung berpikir Foo adalah tipe tertentu, seperti dengan alias lainnya. Tapi tidak. Pertimbangkan contoh ini:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

IMHO agak aneh melihat = dalam deklarasi Displayable tetapi kemudian tidak memiliki tipe pengembalian foo dan bar yang sama (yaitu ini = tidak transitif, tidak seperti di tempat lain). Masalahnya adalah Foo adalah _bukan_ alias untuk tipe tertentu yang kebetulan menyiratkan beberapa Sifat. Dengan kata lain, ini adalah jenis tunggal dalam konteks apa pun yang digunakan tetapi jenis itu mungkin berbeda untuk penggunaan yang berbeda, seperti dalam contoh.

Beberapa orang menyebutkan bahwa type Foo: Bar terasa "tidak lengkap". Bagi saya ini adalah hal yang baik. Dalam beberapa hal Foo tidak lengkap; kita tidak tahu apa itu, tapi kita tahu itu memenuhi Bar .

@mark-im

Masalahnya adalah Foo bukan alias untuk tipe tertentu yang kebetulan menyiratkan beberapa Sifat. Dengan kata lain, ini adalah jenis tunggal dalam konteks apa pun yang digunakan tetapi jenis itu mungkin berbeda untuk penggunaan yang berbeda, seperti dalam contoh.

Wah, benarkah itu? Itu pasti akan sangat membingungkan saya.

Apakah ada alasan mengapa Displayable akan menjadi singkatan untuk impl Display daripada satu jenis beton? Apakah perilaku seperti itu berguna mengingat alias sifat (masalah pelacakan: https://github.com/rust-lang/rust/issues/41517) dapat digunakan dengan cara yang sama? Contoh:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Itu bukan contoh yang valid. Dari bagian referensi tentang tipe eksistensial di RFC 2071 :

existential type Foo = impl Debug;

Foo dapat digunakan sebagai i32 di banyak tempat di seluruh modul. Namun, setiap fungsi yang menggunakan Foo sebagai i32 harus secara independen menempatkan batasan pada Foo sedemikian rupa sehingga harus i32

Setiap deklarasi tipe eksistensial harus dibatasi oleh setidaknya satu badan fungsi atau penginisialisasi const/statis. Badan atau penginisialisasi harus sepenuhnya membatasi atau tidak menempatkan batasan pada tipe eksistensial yang diberikan.

Tidak disebutkan secara langsung, tetapi diperlukan agar RFC lainnya berfungsi, adalah bahwa dua fungsi dalam cakupan tipe eksistensial tidak dapat menentukan tipe konkret yang berbeda untuk eksistensial itu. Itu akan menjadi beberapa bentuk kesalahan tipe yang saling bertentangan.

Saya kira contoh Anda akan memberikan sesuatu seperti expected type `&'static str` but found type `i32` pada pengembalian bar , karena foo sudah menetapkan tipe konkret Displayable ke &'static str .

EDIT: Kecuali jika Anda memahami ini dari intuisi bahwa

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

setara dengan

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

daripada harapan saya

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Saya kira yang mana dari dua interpretasi itu yang benar mungkin tergantung pada RFC yang mungkin ditulis oleh

Masalahnya adalah Foo bukan alias untuk tipe tertentu yang kebetulan menyiratkan beberapa Sifat.

Saya kira yang mana dari dua interpretasi itu yang benar mungkin tergantung pada RFC yang mungkin ditulis oleh

Alasan mengapa type Displayable = impl Display; ada adalah karena itu adalah alias untuk tipe tertentu.
Lihat https://github.com/rust-lang/rfcs/issues/1738 , yang merupakan masalah yang dipecahkan oleh fitur ini.

@Nemo157 Harapan Anda benar. :)

Pengikut:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

akan dianggap sebagai:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

di mana _0 , _1 dan _2 semuanya adalah tipe nominal yang berbeda karenanya Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (dan simetris) semuanya tidak berpenghuni, di mana Id didefinisikan dalam refl .

Penafian: Saya (dengan sukarela) tidak membaca RFC (tetapi tahu tentang apa itu), sehingga saya dapat mengomentari apa yang terasa "intuitif" dengan sintaksis.

Untuk sintaks type Foo: Trait , saya benar-benar mengharapkan sesuatu seperti ini menjadi mungkin:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

Dengan cara yang sama seperti where Foo: Display, Foo: Debug saat ini dimungkinkan.

Jika tidak diperbolehkan sintaks, saya pikir itu masalah dengan sintaks.

Oh, dan saya pikir semakin banyak sintaks yang dimiliki Rust, semakin sulit untuk mempelajarinya. Bahkan jika satu sintaks "lebih mudah dipelajari", selama dua sintaks itu diperlukan, pemula pada akhirnya harus mempelajari keduanya, dan kemungkinan lebih cepat daripada nanti jika mereka masuk untuk proyek yang sudah ada.

@Ekleog

Untuk sintaks type Foo: Trait , saya benar-benar mengharapkan sesuatu seperti ini menjadi mungkin:

Hal ini mungkin. "Tipe alias" tersebut mendeklarasikan tipe terkait (alias tipe dapat diartikan sebagai fungsi level tipe 0-ary sementara tipe terkait adalah fungsi level tipe 1+-ary). Tentu saja Anda tidak dapat memiliki beberapa tipe terkait dengan nama yang sama dalam satu sifat, itu seperti mencoba mendefinisikan dua alias tipe dengan nama yang sama dalam sebuah modul. Dalam impl , type Foo: Bar juga sesuai dengan kuantifikasi eksistensial.

Oh, dan saya pikir semakin banyak sintaks yang dimiliki Rust, semakin sulit untuk mempelajarinya.

Kedua sintaks sudah digunakan. type Foo: Bar; sudah legal dalam sifat, dan juga untuk kuantifikasi universal sebagai Foo: Bar mana Foo adalah variabel tipe. impl Trait digunakan untuk kuantifikasi eksistensial di posisi balik dan untuk kuantifikasi universal di posisi argumen. Mengizinkan kedua colokan kesenjangan konsistensi dalam bahasa. Mereka juga optimal untuk skenario yang berbeda, sehingga memiliki keduanya memberi Anda optimal global.

Selanjutnya, pemula tidak mungkin membutuhkan type Foo = (impl Bar, impl Baz); . Sebagian besar penggunaan kemungkinan adalah type Foo: Bar; .

Permintaan tarik asli untuk RFC 2071 menyebutkan kata kunci typeof yang tampaknya telah diabaikan sepenuhnya dalam diskusi ini. Saya menemukan sintaks yang diusulkan saat ini agak implisit, memiliki kompiler dan manusia mana pun yang membaca pencarian kode untuk tipe konkret.

Saya lebih suka jika ini dibuat eksplisit. Jadi alih-alih

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

kita akan menulis

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(dengan nama return_type_of yang akan di-bikeshed), atau bahkan

fn foo_func() -> impl SomeTrait as Foo { ... }

yang bahkan tidak memerlukan kata kunci baru dan mudah dimengerti oleh siapa saja yang mengetahui sintaks Sifat impl. Sintaks yang terakhir ringkas dan memiliki semua informasi di satu tempat. Untuk ciri-cirinya bisa terlihat seperti ini:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

atau bahkan

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Saya minta maaf jika ini telah dibahas dan diberhentikan, tetapi saya tidak dapat menemukannya.

@Centril

Hal ini mungkin. "Tipe alias" tersebut mendeklarasikan tipe terkait (alias tipe dapat diartikan sebagai fungsi level tipe 0-ary sementara tipe terkait adalah fungsi level tipe 1+-ary). Tentu saja Anda tidak dapat memiliki beberapa tipe terkait dengan nama yang sama dalam satu sifat, itu seperti mencoba mendefinisikan dua alias tipe dengan nama yang sama dalam sebuah modul. Dalam impl, ketik Foo: Bar juga sesuai dengan kuantifikasi eksistensial.

(maaf, saya bermaksud memasukkannya ke dalam impl Trait for Struct , bukan dalam trait Trait )

Maaf, saya tidak yakin saya mengerti. Apa yang saya coba katakan adalah, bagi saya kode seperti

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(tautan taman bermain untuk versi lengkap)
rasanya harus berhasil.

Karena itu hanya menempatkan dua batas pada Type , dengan cara yang sama seperti where Type: Debug, Type: Display work .

Jika ini tidak diizinkan (apa yang tampaknya saya pahami dengan "Tentu saja Anda tidak dapat memiliki beberapa tipe terkait dengan nama yang sama dalam satu sifat"? tetapi mengingat kesalahan saya dalam menulis trait Trait alih-alih impl Trait for Struct Saya tidak yakin), maka saya pikir itu masalah dengan sintaks type Type: Trait .

Kemudian, di dalam deklarasi trait , sintaksnya sudah type Type: Trait , dan tidak mengizinkan banyak definisi. Jadi saya rasa mungkin kapal ini sudah berlayar sejak lama…

Namun, seperti yang ditunjukkan oleh @stjepang dan @joshtriplett di atas , type Type: Trait terasa tidak lengkap. Dan meskipun mungkin masuk akal dalam deklarasi trait (sebenarnya ini dirancang untuk tidak lengkap, meskipun aneh ia tidak mengizinkan banyak definisi), itu tidak masuk akal dalam blok impl Trait , di mana jenisnya seharusnya diketahui dengan pasti (dan saat ini hanya dapat ditulis sebagai type Type = RealType )

impl Trait digunakan untuk kuantifikasi eksistensial di posisi balik dan untuk kuantifikasi universal di posisi argumen.

Ya, saya juga memikirkan impl Trait dalam posisi argumen saat menulis ini, dan bertanya-tanya apakah saya harus mengatakan bahwa saya akan mendukung argumen yang sama untuk impl Trait dalam posisi argumen seandainya saya tahu itu sedang mengalami stabilisasi . Yang mengatakan saya pikir akan lebih baik untuk tidak menyalakan kembali perdebatan ini :)

Mengizinkan kedua colokan kesenjangan konsistensi dalam bahasa. Mereka juga optimal untuk skenario yang berbeda, sehingga memiliki keduanya memberi Anda optimal global.

Optimal dan kesederhanaan

Yah, saya pikir terkadang kehilangan yang optimal demi kesederhanaan adalah hal yang baik. Seperti, C dan ML lahir pada waktu yang hampir bersamaan. C membuat konsesi besar ke optimal demi kesederhanaan, ML jauh lebih dekat ke optimal tetapi jauh lebih kompleks. Bahkan menghitung turunan dari bahasa-bahasa ini, saya rasa jumlah pengembang C dan pengembang ML tidak sebanding.

impl Trait dan :

Saat ini, di sekitar sintaks impl Trait dan : , saya merasa ada kecenderungan untuk membuat kedua sintaks alternatif untuk set fitur yang sama. Namun, saya tidak berpikir itu hal yang baik, karena memiliki dua sintaks untuk fitur yang sama hanya dapat membingungkan pengguna, terutama ketika mereka akan selalu berbeda secara halus dalam semantik persisnya.

Bayangkan seorang pemula yang selalu melihat type Type: Trait datang pada type Type = impl Trait . Mereka mungkin bisa menebak apa yang terjadi, tapi saya cukup yakin akan ada momen “WTF kan? Saya telah menggunakan Rust selama bertahun-tahun dan masih ada sintaks yang belum pernah saya lihat?”. Yang kurang lebih merupakan jebakan di mana C++ jatuh.

Fitur kembung

Apa yang saya pikirkan adalah, pada dasarnya, semakin banyak fitur, semakin sulit bahasa untuk dipelajari. Dan saya tidak melihat keuntungan besar menggunakan type Type: Trait dibandingkan type Type = impl Trait : seperti, 6 karakter disimpan?

Memiliki rustc menampilkan kesalahan saat melihat type Type: Trait yang mengatakan orang yang menulisnya untuk menggunakan type Type = impl Trait akan lebih masuk akal bagi saya: setidaknya ada satu cara untuk menulis sesuatu , masuk akal untuk semua ( impl Trait sudah jelas dikenali sebagai eksistensial di posisi pengembalian), dan mencakup semua kasus penggunaan. Dan jika orang mencoba menggunakan apa yang mereka anggap intuitif (walaupun saya tidak setuju dengan itu, bagi saya = impl Trait lebih intuitif, dibandingkan dengan = i32 ), mereka berhak diarahkan ke cara penulisan yang benar secara konvensional.

Permintaan tarik asli untuk RFC 2071 menyebutkan jenis kata kunci yang tampaknya telah diabaikan sepenuhnya dalam diskusi ini. Saya menemukan sintaks yang diusulkan saat ini agak implisit, memiliki kompiler dan manusia mana pun yang membaca pencarian kode untuk tipe konkret.

typeof dibahas secara singkat dalam masalah yang saya buka 1,5 tahun yang lalu: https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

Berbicara sebagai pemula, saya menemukan sintaks type Foo: Bar membingungkan. Ini adalah sintaks tipe yang terkait, tetapi itu seharusnya dalam sifat, bukan struct. Jika Anda melihat impl Trait sekali, Anda dapat mengetahui apa itu, atau Anda dapat mencarinya. Lebih sulit melakukannya dengan sintaks lain, dan saya tidak yakin apa manfaatnya.

Sepertinya beberapa orang di tim bahasa sangat menentang penggunaan impl Trait untuk menyebut tipe eksistensial, jadi mereka lebih suka menggunakan yang lain. Bahkan komentar di sini tidak masuk akal bagi saya.

Tapi bagaimanapun, saya pikir kuda ini dipukuli sampai mati. Mungkin ada ratusan komentar tentang sintaks, dan hanya sedikit saran (saya sadar saya hanya memperburuk keadaan). Jelas bahwa tidak ada sintaks yang tidak akan membuat semua orang senang, dan ada argumen yang mendukung dan menentang semuanya. Mungkin kita harus memilih satu dan menaatinya.

Wah, saya tidak mengerti sama sekali. Terima kasih @Nemo157 karena telah meluruskan saya!

Dalam hal ini, saya memang lebih suka sintaks =.

@Ekleog

maka saya pikir itu masalah dengan sintaks type Type: Trait .

Itu bisa diizinkan dan akan didefinisikan dengan sangat baik, tetapi Anda biasanya menulis where Type: Foo + Bar alih-alih where Type: Foo, Type: Bar , jadi itu sepertinya bukan ide yang bagus. Anda juga dapat dengan mudah memecat pesan kesalahan yang bagus untuk kasus ini yang menyarankan agar Anda menulis Foo + Bar alih-alih dalam kasus jenis yang terkait.

type Foo = impl Bar; juga memiliki masalah yang dapat dimengerti karena Anda melihat = impl Bar dan menyimpulkan bahwa Anda dapat menggantinya di setiap kemunculan di mana ia digunakan sebagai -> impl Bar ; tapi itu tidak akan berhasil. @mark-im membuat interpretasi ini, yang sepertinya merupakan kesalahan yang jauh lebih mungkin dilakukan. Oleh karena itu, saya menyimpulkan bahwa type Foo: Bar; adalah pilihan yang lebih baik untuk kemampuan belajar.

Namun, seperti yang ditunjukkan oleh @stjepang dan @joshtriplett di atas , ketik Type: Trait terasa tidak lengkap.

Ini tidak lengkap dari POV ekstensional. Anda mendapatkan informasi yang sama persis dari type Foo: Bar; seperti yang Anda dapatkan dari type Foo = impl Bar; . Jadi dari perspektif apa yang dapat Anda lakukan dengan type Foo: Bar; , selesai. Faktanya, yang terakhir ini dianggap sebagai type _0: Bar; type Foo = _0; .

EDIT: yang saya maksud adalah bahwa meskipun mungkin terasa tidak lengkap bagi sebagian orang, itu bukan dari sudut pandang teknis.

Yang mengatakan saya pikir akan lebih baik untuk tidak menyalakan kembali perdebatan ini :)

Itu ide yang bagus. Kita harus mempertimbangkan bahasa apa adanya saat mendesain, bukan seperti yang kita inginkan.

Yah, saya pikir terkadang kehilangan yang optimal demi kesederhanaan adalah hal yang baik.

Jika kita harus pergi untuk kesederhanaan, saya malah akan menjatuhkan type Foo = impl Bar; sebagai gantinya.
Perlu dicatat bahwa kesederhanaan C yang seharusnya (seharusnya, karena Haskell Core dan hal-hal serupa mungkin lebih sederhana sementara masih terdengar..) datang dengan harga yang mahal dalam hal ekspresivitas dan kesehatan. C bukan bintang utara saya dalam desain bahasa; jauh dari itu.

Saat ini, di sekitar sintaks impl Trait dan : , saya merasa ada kecenderungan untuk membuat kedua sintaks alternatif untuk set fitur yang sama. Namun, saya tidak berpikir itu hal yang baik, karena memiliki dua sintaks untuk fitur yang sama hanya dapat membingungkan pengguna, terutama ketika mereka akan selalu berbeda secara halus dalam semantik persisnya.

Tetapi mereka tidak akan berbeda sama sekali dalam semantik mereka. Satu degulars ke yang lain.
Saya pikir kebingungan mencoba menulis type Foo: Bar; atau type Foo = impl Bar hanya untuk salah satunya tidak berfungsi meskipun keduanya memiliki semantik yang terdefinisi dengan baik hanya di jalan pengguna. Jika pengguna mencoba menulis type Foo = impl Bar; , maka lint akan diaktifkan dan mengusulkan type Foo: Bar; . Lint mengajarkan pengguna tentang sintaks lainnya.
Bagi saya, penting bahwa bahasa itu seragam dan konsisten; Jika kita telah memutuskan untuk menggunakan kedua sintaks di suatu tempat, kita harus menerapkan keputusan itu secara konsisten.

Bayangkan seorang pemula yang selalu melihat type Type: Trait datang pada type Type = impl Trait .

Dalam kasus khusus ini, lint akan diaktifkan, dan merekomendasikan sintaks sebelumnya. Dalam hal type Foo = (impl Bar, impl Baz); , pemula harus belajar -> impl Trait dalam hal apa pun, jadi mereka harus dapat menyimpulkan artinya dari itu.

Yang kurang lebih merupakan jebakan di mana C++ jatuh.

Masalah C++ terutama karena cukup lama, memiliki bagasi C, dan banyak fitur yang mendukung terlalu banyak paradigma. Ini bukan fitur yang berbeda, hanya sintaks yang berbeda.

Apa yang saya pikirkan adalah, pada dasarnya, semakin banyak fitur, semakin sulit bahasa untuk dipelajari.

Saya pikir belajar bahasa baru terutama tentang mempelajari perpustakaan yang penting. Di situlah sebagian besar waktu akan dihabiskan. Fitur yang tepat dapat membuat perpustakaan jauh lebih dapat disusun dan berfungsi dalam lebih banyak kasus. Saya lebih suka bahasa yang memberikan kekuatan abstraksi yang baik daripada bahasa yang memaksa Anda untuk berpikir tingkat rendah dan yang menyebabkan duplikasi. Dalam hal ini, kami tidak menambahkan lebih banyak kekuatan abstraksi atau bahkan tidak benar-benar fitur, hanya ergonomi yang lebih baik.

Dan saya tidak melihat keuntungan besar menggunakan type Type: Trait dibandingkan type Type = impl Trait: , seperti, 6 karakter disimpan?

Ya, hanya 6 karakter yang disimpan. Tetapi jika kita mempertimbangkan type Foo: Iterator<Item: Iterator<Item: Display>>; , maka kita akan mendapatkan: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; yang memiliki lebih banyak noise. type Foo: Bar; juga lebih langsung dibandingkan dengan yang terakhir, kurang rentan terhadap salah tafsir (substitusi ulang..), dan bekerja lebih baik untuk jenis terkait (salin jenis dari sifat..).
Selanjutnya, type Foo: Bar dapat secara alami diperluas ke type Foo: Bar = ConcreteType; yang akan mengekspos tipe beton tetapi juga memastikan bahwa itu memenuhi Bar . Tidak ada hal seperti itu yang dapat dilakukan untuk type Foo = impl Trait; .

Memiliki rustc menghasilkan kesalahan ketika melihat type Type: Trait yang mengatakan orang yang menulisnya untuk menggunakan type Type = impl Trait akan jauh lebih masuk akal bagi saya: setidaknya ada satu cara untuk menulis sesuatu,

mereka berhak diarahkan ke cara penulisan yang benar secara konvensional.

Saya mengusulkan bahwa ada satu cara konvensional untuk menulis sesuatu; type Foo: Bar; .

@lnicola

Berbicara sebagai pemula, saya menemukan sintaks type Foo: Bar membingungkan. Ini adalah sintaks tipe yang terkait, tetapi itu seharusnya dalam sifat, bukan struct.

Saya akan mengulangi bahwa alias tipe benar-benar dapat dilihat sebagai tipe terkait. Anda akan dapat mengatakan:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Kami melihat bahwa ini bekerja sama persis dalam tipe terkait dan alias tipe.

Ya, hanya 6 karakter yang disimpan. Tetapi jika kita mempertimbangkan type Foo: Iterator<Item: Iterator<Item: Display>>; , maka kita akan mendapatkan: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; yang memiliki lebih banyak suara.

Ini tampaknya ortogonal dengan sintaks untuk mendeklarasikan eksistensial bernama. Empat sintaks yang saya ingat sedang diusulkan semuanya berpotensi memungkinkan ini sebagai

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Mampu menggunakan sintaks Trait<AssociatedType: Bound> Anda usulkan alih-alih sintaks Trait<AssociatedType = impl Bound> untuk mendeklarasikan tipe eksistensial anonim untuk tipe terkait dari tipe eksistensial (baik bernama atau anonim) adalah fitur independen (tetapi mungkin relevan di hal menjaga seluruh rangkaian fitur tipe eksistensial tetap konsisten).

@Nemo157 Mereka adalah fitur yang berbeda, ya; tapi saya pikir wajar untuk mempertimbangkannya bersama demi konsistensi.

@Centril

Maaf, tapi mereka salah. Ini tidak lengkap dari POV ekstensional.

Saya tidak pernah menyarankan bahwa sintaks yang Anda usulkan tidak memiliki informasi; Saya menyarankan bahwa rasanya tidak lengkap; itu terlihat salah bagi saya, dan bagi orang lain. Saya mengerti bahwa Anda tidak setuju dengan itu, dari sudut pandang Anda berasal, tetapi itu tidak membuat perasaan itu salah.

Perhatikan juga bahwa di utas ini, orang-orang menunjukkan masalah interpretasi dengan perbedaan sintaksis yang tepat ini. type Foo = impl Trait terasa membuatnya lebih jelas bahwa Foo adalah tipe beton yang spesifik tetapi tidak disebutkan namanya tidak peduli berapa kali Anda menggunakannya, daripada alias untuk sifat yang dapat mengambil tipe beton yang berbeda setiap kali Anda menggunakannya.

Saya pikir itu membantu untuk memberi tahu orang-orang bahwa mereka dapat mengambil semua hal yang mereka ketahui tentang -> impl Trait dan menerapkannya ke type Foo = impl Trait ; ada konsep umum impl Trait yang dapat mereka lihat digunakan sebagai blok bangunan di kedua tempat. Sintaks seperti type Foo: Trait menyembunyikan blok penyusun umum tersebut.

@joshtriplett

Saya menyarankan bahwa rasanya tidak lengkap; itu terlihat salah bagi saya, dan bagi orang lain.

Baiklah; Saya mengusulkan agar kami menggunakan istilah yang berbeda dari incomplete sini karena bagi saya, ini menunjukkan kurangnya informasi.

Perhatikan juga bahwa di utas ini, orang-orang menunjukkan masalah interpretasi dengan perbedaan sintaksis yang tepat ini.

Apa yang saya amati adalah kesalahan interpretasi, yang dibuat di utas, tentang apa arti type Foo = impl Bar; . Satu orang menafsirkan penggunaan Foo sebagai tidak secara nominal jenis yang sama, melainkan jenis yang berbeda. Yaitu, tepatnya: "sebuah alias untuk suatu sifat yang dapat mengambil tipe konkret yang berbeda setiap kali Anda menggunakannya" .

Beberapa telah menyatakan bahwa type Foo: Bar; membingungkan, tetapi saya tidak yakin apa interpretasi alternatif dari type Foo: Bar; yang berbeda dari arti yang dimaksudkan. Saya akan tertarik untuk mendengar tentang interpretasi alternatif.

@Centril

Saya akan mengulangi bahwa alias tipe benar-benar dapat dilihat sebagai tipe terkait.

Mereka bisa, tapi sekarang tipe terkait terkait dengan sifat. impl Trait bekerja di mana saja, atau hampir. Jika Anda ingin menampilkan impl Trait sebagai jenis tipe terkait, Anda harus memperkenalkan dua konsep sekaligus. Artinya, Anda melihat impl Trait sebagai tipe pengembalian fungsi, menebak atau membaca tentang apa itu, kemudian ketika Anda melihat impl Trait dalam alias tipe, Anda dapat menggunakan kembali pengetahuan itu.

Bandingkan dengan melihat tipe terkait dalam definisi sifat. Dalam hal ini Anda menganggap itu adalah sesuatu yang harus didefinisikan atau diterapkan oleh struct lain. Tetapi jika Anda menemukan type Foo: Debug luar suatu sifat, Anda tidak akan tahu apa itu. Tidak ada yang mengimplementasikannya, jadi apakah ini semacam deklarasi maju? Apakah itu ada hubungannya dengan warisan, seperti di C++? Apakah ini seperti modul ML di mana orang lain memilih jenisnya? Dan jika Anda pernah melihat impl Trait sebelumnya, tidak ada yang bisa menghubungkan mereka. Kami menulis fn foo() -> impl ToString , bukan fn foo(): ToString .

ketik Foo = impl Bar; juga memiliki masalah yang dapat dimengerti karena Anda melihat = impl Bar dan menyimpulkan bahwa Anda bisa menggantinya di setiap kemunculan di mana ia digunakan sebagai -> impl Bar

Saya sudah mengatakannya di sini sebelumnya, tetapi itu seperti berpikir bahwa let x = foo(); berarti Anda dapat menggunakan x alih-alih foo() . Bagaimanapun, ini adalah detail yang dapat dicari seseorang dengan cepat saat dibutuhkan, tetapi tidak mengubah konsep secara mendasar.

Artinya, mudah untuk mengetahui tentang apa ini (tipe yang disimpulkan seperti di -> impl Trait ), bahkan jika Anda tidak tahu persis cara kerjanya (apa yang terjadi ketika Anda memiliki definisi yang bertentangan untuk itu). Dengan sintaks lain, sulit untuk menyadari apa itu.

@Centril

Baiklah; Saya mengusulkan agar kita menggunakan istilah yang berbeda dari tidak lengkap di sini karena bagi saya, ini menunjukkan kurangnya informasi.

"tidak lengkap" tidak harus berarti kurangnya informasi , itu bisa berarti bahwa sesuatu terlihat seperti seharusnya memiliki sesuatu yang lain dan tidak.

type Foo: Trait; tidak terlihat seperti deklarasi yang lengkap. Sepertinya ada yang hilang. Dan tampaknya sangat berbeda dari type Foo = SomeType<X, Y, Z>; .

Mungkin kita mencapai titik bahwa one-liner kita sendiri tidak dapat benar-benar menjembatani kesenjangan konsensus ini antara type Inferred: Trait dan type Inferred = impl Trait .

Menurut Anda, apakah layak untuk menyusun implementasi eksperimental fitur ini dengan sintaks apa pun (bahkan yang ditentukan dalam RFC) sehingga kami dapat mulai memainkannya di program yang lebih besar untuk melihat kesesuaiannya dalam konteks?

@lnicola

[..] impl Trait bekerja di mana saja, atau hampir

Yah, Foo: Bound juga berfungsi hampir di mana-mana ;)

Tetapi jika Anda menemukan type Foo: Debug luar suatu sifat, Anda tidak akan tahu apa itu.

Saya pikir perkembangan menggunakannya di: sifat -> impl -> jenis alias bantu dalam belajar.
Selanjutnya, saya pikir kesimpulan bahwa "tipe Foo mengimplementasikan Debug" kemungkinan berasal dari
melihat type Foo: Debug dalam sifat dan dari batas umum dan itu juga benar.

Apakah itu ada hubungannya dengan warisan, seperti di C++?

Saya pikir kurangnya pewarisan di Rust perlu dipelajari pada tahap yang jauh lebih awal daripada saat mempelajari fitur yang sedang kita diskusikan karena ini sangat mendasar bagi Rust.

Apakah ini seperti modul ML di mana orang lain memilih jenisnya?

Inferensi itu juga dapat dibuat untuk type Foo = impl Bar; karena arg: impl Bar mana pemanggil (pengguna) memilih jenisnya. Bagi saya, kesimpulan bahwa pengguna memilih jenis tampaknya lebih kecil kemungkinannya untuk type Foo: Bar; .

Saya sudah mengatakannya di sini sebelumnya, tetapi itu seperti berpikir bahwa let x = foo(); berarti Anda dapat menggunakan x alih-alih foo() .

Jika bahasanya transparan secara referensi, Anda dapat mengganti x untuk foo() . Sampai kita menambahkan type Foo = impl Foo; ke dalam sistem, ketik alias afaik transparan secara referensial. Sebaliknya, jika sudah ada ikatan x = foo() tersedia, maka foo() in lainnya dapat diganti dengan x .

@joshtriplett

"tidak lengkap" tidak harus berarti kurangnya informasi, itu bisa berarti bahwa sesuatu terlihat seperti seharusnya memiliki sesuatu yang lain dan tidak.

Cukup adil; tapi apa yang seharusnya memiliki yang tidak?

type Foo: Trait; tidak terlihat seperti deklarasi yang lengkap.

Tampak lengkap bagi saya. Sepertinya penilaian bahwa Foo memenuhi Trait yang merupakan makna yang dimaksudkan.

@Centril bagi saya "sesuatu yang hilang" adalah tipe sebenarnya yang merupakan alias untuk. Itu sedikit terkait dengan kebingungan saya sebelumnya. Bukannya tidak ada tipe seperti itu, hanya saja tipe itu anonim... Menggunakan = secara halus menyiratkan bahwa ada tipe dan selalu tipe yang sama tetapi kita tidak bisa menyebutkannya.

Saya pikir kami agak melelahkan argumen ini. Akan sangat bagus untuk mengimplementasikan kedua sintaks secara eksperimental dan melihat apa yang paling berhasil.

@mark-im

@Centril bagi saya "sesuatu yang hilang" adalah tipe sebenarnya yang merupakan alias untuk. Itu sedikit terkait dengan kebingungan saya sebelumnya. Bukannya tidak ada tipe seperti itu, hanya saja tipe itu anonim... Menggunakan = secara halus menyiratkan bahwa ada tipe dan selalu tipe yang sama tetapi kita tidak bisa menyebutkannya.

Persis seperti itulah rasanya bagi saya, juga.

Adakah peluang untuk menangani dua item yang ditangguhkan segera, ditambah masalah tentang penghapusan seumur hidup? Saya akan melakukannya sendiri, tetapi tidak tahu caranya!

Masih ada banyak kebingungan tentang apa sebenarnya arti impl Trait , dan itu sama sekali tidak jelas. Saya pikir item yang ditangguhkan pasti harus menunggu sampai kami memiliki gagasan yang jelas tentang semantik yang tepat dari impl Trait (yang akan segera hadir).

@varkor Semantik apa yang tidak jelas? AFAIK tidak ada apa pun tentang semantik impl Trait berubah sejak RFC 1951, dan diperluas pada 2071.

@alexreg Saya tidak punya rencana untuk itu, tapi inilah garis besarnya: Setelah parsing ditambahkan, Anda perlu menurunkan jenis static s dan const s di dalam impl eksistensial konteks sifat, seperti yang dilakukan di sini untuk jenis fungsi yang dikembalikan. . Namun, Anda ingin membuat DefId di ImplTraitContext::Existential opsional, karena Anda tidak ingin impl Trait mengambil generik dari definisi fungsi induk. Itu seharusnya memberi Anda sedikit jalan yang layak. Anda mungkin memiliki waktu yang lebih mudah jika Anda mendasarkan pada PR tipe eksistensial @oli-obk.

@cramertj : semantik impl Trait dalam bahasa sepenuhnya terbatas pada penggunaannya dalam tanda tangan fungsi dan tidak benar bahwa memperluasnya ke posisi lain memiliki arti yang jelas. Saya akan mengatakan sesuatu yang lebih rinci tentang ini segera, di mana sebagian besar percakapan tampaknya terjadi.

@varkor

semantik sifat impl dalam bahasa sepenuhnya terbatas pada penggunaannya dalam tanda tangan fungsi dan tidak benar bahwa memperluasnya ke posisi lain memiliki arti yang jelas.

Artinya ditentukan dalam RFC 2071 .

@cramertj : makna dalam RFC 2071 ambigu dan memungkinkan banyak interpretasi tentang apa arti frasa "tipe eksistensial" di sana.

TL;DR — Saya telah mencoba menetapkan arti yang tepat untuk impl Trait , yang menurut saya memperjelas detail yang, setidaknya secara intuitif, tidak jelas; bersama dengan proposal untuk sintaks alias tipe baru.

Tipe eksistensial di Rust (posting)


Ada banyak diskusi yang terjadi di Discord rust-lang chat tentang semantik (yaitu formal, teoretis) yang tepat dari impl Trait dalam beberapa hari terakhir. Saya pikir sangat membantu untuk mengklarifikasi banyak detail tentang fitur dan apa itu dan apa yang tidak. Ini juga menjelaskan beberapa sintaks yang masuk akal untuk alias tipe.

Saya menulis sedikit ringkasan dari beberapa kesimpulan kami. Ini memberikan interpretasi impl Trait yang menurut saya cukup bersih, dan secara tepat menggambarkan perbedaan antara posisi argumen impl Trait dan posisi kembali impl Trait (yang bukan "secara universal- dikuantifikasi" vs "dikuantifikasi secara eksistensial"). Ada juga beberapa kesimpulan praktis.

Di dalamnya, saya mengusulkan sintaks baru yang memenuhi persyaratan yang dinyatakan secara umum dari "alias tipe eksistensial":
type Foo: Bar = _;

Karena topik ini sangat kompleks, ada cukup banyak yang perlu diklarifikasi terlebih dahulu, jadi saya telah menulisnya sebagai posting terpisah. Umpan balik sangat dihargai!

Tipe eksistensial di Rust (posting)

@varkor

RFC 2071 bersifat ambigu dan memungkinkan interpretasi ganda tentang apa arti frasa "tipe eksistensial" di sana.

Bagaimana ambigu? Saya telah membaca posting Anda-- Saya masih hanya menyadari satu arti eksistensial non-dinamis dalam statika dan konstanta. Ini berperilaku dengan cara yang sama seperti mengembalikan posisi impl Trait , dengan memperkenalkan definisi tipe eksistensial baru per item.

type Foo: Bar = _;

Kami membahas sintaks ini selama RFC 2071. Seperti yang saya katakan di sana, saya suka itu menunjukkan dengan jelas bahwa Foo adalah tipe yang disimpulkan tunggal dan meninggalkan ruang untuk tipe yang tidak disimpulkan yang dibiarkan eksis di luar modul saat ini ( misalnya type Foo: Bar = u32; ). Saya tidak menyukai dua aspeknya: (1) tidak memiliki kata kunci, dan karena itu lebih sulit untuk dicari dan (b) memiliki masalah verbositas yang sama dibandingkan dengan type Foo = impl Trait yang abstract type Foo: Bar; sintaks memiliki: type Foo = impl Iterator<Item = impl Display>; menjadi type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . Saya tidak berpikir salah satu dari ini adalah pemecah kesepakatan, tetapi ini bukan kemenangan yang jelas dengan satu atau lain cara.

@cramertj Ambiguitas muncul di sini:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Jika Foo benar-benar merupakan alias tipe untuk tipe eksistensial, maka f dan g akan mendukung tipe pengembalian beton yang berbeda . Beberapa orang secara naluriah membaca sintaks itu dengan cara ini, dan faktanya beberapa peserta dalam diskusi sintaks RFC 2071 baru menyadari bahwa itu bukan cara kerja proposal sebagai bagian dari diskusi Discord baru-baru ini.

Masalahnya adalah, terutama dalam menghadapi posisi argumen impl Trait , sama sekali tidak jelas ke mana quantifier eksistensial dimaksudkan untuk pergi. Untuk argumen, cakupannya ketat; untuk posisi pengembalian tampaknya cakupannya ketat tetapi ternyata lebih lebar dari itu; untuk type Foo = impl Bar kedua posisi masuk akal. Sintaks berbasis _ mendorong ke arah interpretasi yang bahkan tidak melibatkan "eksistensial", dengan rapi menghindari masalah ini.

Jika Foo benar-benar alias tipe untuk tipe eksistensial

(penekanan saya). Saya membaca bahwa 'an' sebagai 'spesifik' yang berarti f dan g _tidak_ mendukung tipe pengembalian beton yang berbeda, karena mereka merujuk pada tipe eksistensial yang sama. Saya selalu melihat type Foo = impl Bar; menggunakan arti yang sama dengan let foo: impl Bar; , yaitu memperkenalkan tipe eksistensial anonim baru; membuat contoh Anda setara dengan

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

yang saya harapkan relatif tidak ambigu.


Satu masalah adalah bahwa arti dari " impl Trait dalam jenis alias" tidak pernah ditentukan dalam RFC. Ini disebutkan secara singkat di bagian "Alternatif" RFC 2071 , tetapi secara eksplisit diabaikan karena ambiguitas pengajaran yang melekat ini.

Saya juga merasa seperti melihat beberapa yang menyebutkan bahwa alias tipe sudah tidak transparan secara referensi. Saya pikir itu ada di url.o, tetapi saya belum dapat menemukan diskusi setelah beberapa pencarian.

@cramertj
Untuk melanjutkan dari impl Trait , yang semuanya konsisten dengan penggunaan tanda tangan saat ini, tetapi memiliki konsekuensi yang berbeda ketika memperluas impl Trait ke yang lain lokasi (saya tahu 2 selain yang dijelaskan dalam posting, tetapi tidak cukup siap untuk diskusi). Dan saya rasa tidak benar bahwa interpretasi dalam posting tersebut tentu yang paling jelas (saya pribadi tidak melihat penjelasan serupa tentang APIT dan RTIP dari perspektif itu).

Mengenai type Foo: Bar = _; , saya pikir mungkin perlu didiskusikan lagi — tidak ada salahnya meninjau kembali ide-ide lama dengan pandangan baru. Mengenai masalah Anda dengannya:
(1) Tidak memiliki kata kunci, tetapi sintaksnya sama dengan inferensi tipe di mana saja. Mencari dokumentasi untuk "garis bawah" / "tipe garis bawah" / dll. dapat dengan mudah menyediakan halaman tentang inferensi tipe.
(2) Ya, itu benar. Kami telah memikirkan solusi untuk ini, yang menurut saya cocok dengan notasi garis bawah, yang diharapkan akan segera siap untuk disarankan.

Seperti @cramertj saya tidak benar-benar melihat argumen di sini.

Saya hanya tidak melihat ambiguitas mendasar yang dijelaskan oleh posting @varkor . Saya pikir kami selalu menafsirkan "tipe eksistensial" di Rust sebagai "ada tipe _unique_ yang..." dan bukan "ada setidaknya satu tipe itu..." karena (seperti yang dikatakan oleh postingan impl Trait selalu tidak jelas dan perlu dipikirkan kembali.

Ambiguitas pelingkupan yang dijelaskan oleh @rpjohnst adalah masalah serius, tetapi setiap sintaks yang diusulkan berpotensi membingungkan dengan jenis alias atau jenis terkait. Manakah dari kebingungan itu yang "lebih buruk" atau "lebih mungkin" yang justru merupakan persekongkolan tanpa akhir yang telah gagal kami atasi setelah beberapa ratus komentar. Saya suka itu type Foo: Bar = _; tampaknya memperbaiki masalah type Foo: Bar; membutuhkan ledakan beberapa pernyataan untuk mendeklarasikan eksistensial yang sedikit non-sepele, tetapi saya rasa itu tidak cukup untuk benar-benar mengubah situasi "tidak pernah berhenti bersepeda".

Apa yang saya yakini adalah bahwa sintaks apa pun yang kita dapatkan harus memiliki kata kunci selain type , karena semua sintaks "hanya type " terlalu menyesatkan. Faktanya, mungkin tidak menggunakan type dalam sintaks _at all_ jadi tidak mungkin seseorang dapat berasumsi bahwa mereka sedang melihat "alias tipe, tetapi entah bagaimana lebih eksistensial".

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Tidak jelas bagi saya bahwa salah satu dari ini sepenuhnya _mencegah_ salah tafsir bahwa f dan g dapat mengembalikan dua jenis berbeda yang mengimplementasikan Trait , tetapi saya menduga ini sedekat mungkin dengan pencegahan kita mungkin bisa mendapatkan.

@Ixrec
Ungkapan "tipe eksistensial" bermasalah secara khusus _karena_ ambiguitas pelingkupan. Saya belum pernah melihat orang lain menunjukkan bahwa pelingkupan sama sekali berbeda untuk APIT dan RPIT. Ini berarti bahwa sintaks seperti type Foo = impl Bar , di mana impl Bar adalah "tipe eksistensial" secara inheren ambigu.

Ya, istilah teori tipe telah banyak disalahgunakan. Tapi itu telah disalahgunakan (atau setidaknya tidak dijelaskan) di RFC — jadi ada ambiguitas yang berasal dari RFC itu sendiri.

Ambiguitas pelingkupan yang dijelaskan oleh @rpjohnst adalah masalah serius, tetapi setiap sintaks yang diusulkan berpotensi membingungkan dengan jenis alias atau jenis terkait. Manakah dari kebingungan itu yang "lebih buruk" atau "lebih mungkin" yang justru merupakan persekongkolan tanpa akhir yang telah gagal kami atasi setelah beberapa ratus komentar.

Tidak, saya rasa ini tidak benar. Dimungkinkan untuk menghasilkan sintaks yang konsisten yang tidak memiliki kebingungan ini. Saya berani melakukan penumpahan sepeda karena dua proposal saat ini buruk, jadi mereka tidak benar-benar memuaskan siapa pun.

Apa yang saya yakini adalah bahwa sintaks apa pun yang kita dapatkan harus memiliki kata kunci selain type

Saya rasa ini juga tidak perlu. Dalam contoh Anda, Anda telah menemukan notasi yang sama sekali baru, yang merupakan sesuatu yang ingin Anda hindari dalam desain bahasa sedapat mungkin — jika tidak, Anda membuat bahasa besar yang penuh dengan sintaks yang tidak konsisten. Anda harus menjelajahi sintaks yang benar-benar baru hanya jika tidak ada opsi yang lebih baik. Dan saya berpendapat bahwa ada _is_ pilihan yang lebih baik.

Selain: di samping catatan, saya pikir mungkin untuk menjauh dari "tipe eksistensial" sepenuhnya, membuat seluruh situasi menjadi lebih jelas, yang akan segera saya atau orang lain tindak lanjuti.

Saya mendapati diri saya berpikir bahwa sintaks selain type akan membantu juga, justru karena banyak orang menafsirkan type sebagai alias yang dapat diganti sederhana, yang akan menyiratkan interpretasi "tipe yang berpotensi berbeda setiap waktu".

Saya belum pernah melihat orang lain menunjukkan bahwa pelingkupan sama sekali berbeda untuk APIT dan RPIT.

Saya pikir pelingkupan selalu merupakan bagian eksplisit dari proposal Sifat impl, jadi tidak perlu "menunjukkan". Semua yang Anda katakan tentang pelingkupan sepertinya hanya mengulangi apa yang telah kami terima di RFC sebelumnya. Saya mengerti bahwa itu tidak jelas bagi semua orang dari sintaks dan itu masalah, tetapi tidak seperti tidak ada yang mengerti ini sebelumnya. Faktanya, saya pikir sebagian besar diskusi di RFC 2701 adalah tentang apa yang seharusnya menjadi pelingkupan type Foo = impl Trait; , dalam arti jenis inferensi apa yang boleh dan tidak boleh dilihat.

Dimungkinkan untuk menghasilkan sintaks yang konsisten yang tidak memiliki kebingungan ini.

Apakah Anda mencoba mengatakan type Foo: Bar = _; adalah sintaksis itu, atau menurut Anda kami belum menemukannya?

Saya tidak berpikir itu mungkin untuk menghasilkan sintaks yang tidak memiliki kebingungan serupa, bukan karena kami kurang kreatif, tetapi karena sebagian besar programmer bukan ahli teori tipe. Kami mungkin dapat menemukan sintaks yang mengurangi kebingungan ke tingkat yang dapat ditoleransi, dan tentu saja ada banyak sintaks yang tidak ambigu untuk mengetik veteran teori, tetapi kami tidak akan pernah menghilangkan kebingungan sepenuhnya.

Anda telah menemukan notasi yang sama sekali baru

Saya pikir saya baru saja mengganti satu kata kunci dengan kata kunci lain. Apakah Anda melihat beberapa perubahan tambahan yang tidak saya maksudkan?

Kalau dipikir-pikir, karena kita telah menyalahgunakan "eksistensial" selama ini, itu berarti existential Foo: Trait / = impl Trait mungkin bukan sintaks yang sah lagi.

Jadi kita membutuhkan kata kunci baru untuk diletakkan di depan nama yang merujuk pada beberapa jenis kode yang tidak diketahui-ke-eksternal... alias , secret , internal , dll semuanya tampak sangat buruk, dan tidak mungkin memiliki "kebingungan keunikan" yang lebih sedikit daripada type .

Kalau dipikir-pikir, karena kita telah menyalahgunakan "eksistensial" selama ini, itu berarti existential Foo: Trait / = impl Trait mungkin bukan sintaks yang sah lagi.

Ya, saya sepenuhnya setuju — saya pikir kita perlu menjauh dari istilah "eksistensial" sepenuhnya* (ada beberapa ide tentatif tentang bagaimana melakukan ini sambil tetap menjelaskan impl Trait baik).

*(mungkin memesan istilah hanya untuk dyn Trait )

@joshtriplett , @Ixrec : Saya setuju bahwa notasi _ berarti Anda tidak dapat lagi mengganti ke tingkat yang sama seperti sebelumnya, dan jika itu prioritas untuk dipertahankan, kami memerlukan sintaks yang berbeda.

Ingatlah bahwa _ sudah menjadi kasus khusus sehubungan dengan substitusi — bukan hanya jenis alias yang terpengaruh: di mana pun Anda saat ini dapat menggunakan _ , Anda mencegah transparansi referensial penuh.

Ingatlah bahwa _ sudah merupakan kasus khusus sehubungan dengan substitusi — bukan hanya jenis alias yang terpengaruh: di mana pun Anda saat ini dapat menggunakan _, Anda mencegah transparansi referensial penuh.

Bisakah Anda memandu kami melalui apa artinya ini sebenarnya? Saya tidak mengetahui gagasan "transparansi referensial" yang dipengaruhi oleh _ .

Saya setuju bahwa notasi _ berarti Anda tidak dapat lagi mengganti ke tingkat yang sama seperti sebelumnya, dan jika itu adalah prioritas untuk dipertahankan, kita akan membutuhkan sintaks yang berbeda.

Saya tidak yakin itu _prioritas_. Bagi saya itu hanya satu-satunya argumen objektif-ish yang pernah kami temukan yang tampaknya lebih memilih satu sintaks daripada yang lain. Tapi itu semua kemungkinan akan berubah berdasarkan kata kunci apa yang bisa kita temukan untuk menggantikan type .

Bisakah Anda memandu kami melalui apa artinya ini sebenarnya? Saya tidak mengetahui gagasan "transparansi referensial" yang dipengaruhi oleh _ .

Ya, maaf, saya melontarkan kata-kata tanpa menjelaskannya. Biarkan saya mengumpulkan pemikiran saya, dan saya akan merumuskan penjelasan yang lebih kohesif. Ini cocok dengan cara alternatif (dan berpotensi lebih bermanfaat) untuk melihat impl Trait .

Dengan transparansi referensial , berarti dimungkinkan untuk mengganti referensi untuk definisinya dan sebaliknya tanpa perubahan semantik. Di Rust, ini jelas tidak berlaku pada level istilah untuk fn . Sebagai contoh:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

jika kita mengganti setiap kemunculan bar untuk foo() (definisi dari bar ), maka kita jelas mendapatkan output yang berbeda.

Namun, untuk jenis alias, saat ini masih mengacu pada transparansi referensial (AFAIK). Jika Anda memiliki alias:

type Foo = Definition;

Kemudian Anda dapat melakukan (tangkap menghindari) substitusi kemunculan Foo untuk Definition dan substitusi kemunculan Definition untuk Foo tanpa mengubah semantik program Anda , atau ketepatan jenisnya.

Memperkenalkan:

type Foo = impl Bar;

artinya setiap kemunculan Foo adalah tipe yang sama artinya jika anda menulis:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

anda tidak dapat mengganti kemunculan Foo untuk impl Bar dan sebaliknya. Artinya, jika Anda menulis:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

jenis pengembalian tidak akan menyatu dengan Foo . Jadi transparansi referensial rusak untuk alias tipe dengan memperkenalkan impl Trait dengan semantik RFC 2071 di dalamnya.

Tentang transparansi referensial dan type Foo = _; , bersambung... (oleh @varkor)

Saya mendapati diri saya berpikir bahwa sintaks selain tipe akan membantu juga, justru karena banyak orang menafsirkan tipe sebagai alias sederhana yang dapat diganti, yang akan menyiratkan interpretasi "tipe yang berpotensi berbeda setiap saat".

Poin bagus. Tetapi bukankah bit penugasan = _ menyiratkan bahwa itu hanya satu jenis?

Aku pernah menulis ini sebelumnya, tapi...

Transparansi referensial: Saya pikir lebih berguna untuk melihat type sebagai pengikatan (seperti let ) daripada substitusi seperti praprosesor C. Setelah Anda melihatnya seperti itu, type Foo = impl Trait berarti persis seperti yang terlihat.

Saya membayangkan pemula akan cenderung tidak menganggap impl Trait sebagai tipe eksistensial vs. universal, tetapi sebagai "sesuatu yang impl Trait . If they want to know more, they can read the impl dokumentasi Trait`. Setelah Anda mengubah sintaks, Anda kehilangan koneksi antara itu dan fitur yang ada dengan tidak banyak manfaat. _Anda hanya mengganti satu sintaks yang berpotensi menyesatkan dengan yang lain._

Kembali type Foo = _ , itu membebani _ dengan arti yang sama sekali tidak terkait. Mungkin juga sulit ditemukan di dokumentasi dan/atau Google.

@lnicola Anda juga bisa menggunakan binding const alih-alih binding let , di mana yang pertama transparan secara referensial. Memilih let (yang kebetulan tidak transparan secara referensial di dalam fn ) adalah pilihan arbitrer yang menurut saya tidak terlalu intuitif. Saya pikir pandangan intuitif dari alias tipe adalah bahwa mereka transparan secara referensi (bahkan jika kata itu tidak digunakan) karena mereka aliases .

Saya juga tidak melihat type sebagai substitusi preprosesor C karena harus ditangkap menghindari dan menghormati obat generik (tidak ada SFINAE). Alih-alih, saya memikirkan type persis seperti yang saya lakukan pada pengikatan dalam bahasa seperti Idris atau Agda di mana semua pengikatan murni.

Saya membayangkan pemula akan cenderung tidak menganggap impl Trait sebagai tipe eksistensial vs. universal, tetapi sebagai "sesuatu yang menyiratkan Sifat

Itu tampak seperti perbedaan tanpa perbedaan bagi saya. Jargon "eksistensial" tidak digunakan, tetapi saya yakin pengguna secara intuitif menautkannya ke konsep yang sama dengan tipe eksistensial (yang tidak lebih dari "beberapa tipe Foo yang menyiratkan Bar" dalam konteks Rust).

Re type Foo = _ , itu membebani _ dengan arti yang sama sekali tidak terkait.

Bagaimana? type Foo = _; sini sejalan dengan penggunaan _ dalam konteks lain di mana sebuah tipe diharapkan.
Artinya "simpulkan tipe sebenarnya", sama seperti ketika Anda menulis .collect::<Vec<_>>() .

Mungkin juga sulit ditemukan di dokumentasi dan/atau Google.

Bukankah seharusnya begitu sulit? "ketik alias garis bawah" semoga memunculkan hasil yang diinginkan...?
Tampaknya tidak ada bedanya dengan mencari "ketik alias sifat impl".

Google tidak mengindeks karakter khusus. Jika pertanyaan StackOverflow saya memiliki garis bawah di dalamnya, Google tidak akan secara otomatis mengindeksnya untuk kueri yang berisi kata garis bawah

@Centril

Bagaimana? type Foo = _; sini sejalan dengan penggunaan _ dalam konteks lain di mana tipe diharapkan.
Artinya "simpulkan tipe sebenarnya", sama seperti ketika Anda menulis .collect::>().

Tetapi fitur ini tidak menyimpulkan tipenya dan memberi Anda alias tipe untuknya, fitur ini menciptakan tipe eksistensial yang (di luar beberapa cakupan terbatas seperti modul atau peti) tidak menyatu dengan "tipe sebenarnya".

Google tidak mengindeks karakter khusus.

Ini tidak lagi benar (meskipun mungkin bergantung pada spasi..?).

Tetapi fitur ini tidak menyimpulkan tipenya dan memberi Anda alias tipe untuknya, fitur ini menciptakan tipe eksistensial yang (di luar beberapa cakupan terbatas seperti modul atau peti) tidak menyatu dengan "tipe sebenarnya".

Semantik yang disarankan dari type Foo = _; adalah sebagai alternatif untuk memiliki alias tipe eksistensial, yang sepenuhnya didasarkan pada inferensi. Jika itu tidak sepenuhnya jelas, saya akan segera menindaklanjuti dengan sesuatu yang harus menjelaskan niatnya sedikit lebih baik.

@iopq Selain catatan @varkor tentang perubahan terkini, saya juga ingin menambahkan bahwa untuk mesin telusur lain, dokumentasi resmi dan semacamnya selalu mungkin menggunakan kata literal "garis bawah" dalam hubungannya dengan type sedemikian rupa sehingga dapat dicari.

Anda masih tidak akan mendapatkan hasil yang baik dengan _ dalam kueri Anda, untuk alasan apa pun. Jika Anda mencari garis bawah, Anda mendapatkan hal-hal dengan kata garis bawah di dalamnya. Jika Anda mencari _ Anda mendapatkan semua yang memiliki garis bawah, jadi saya bahkan tidak tahu apakah itu relevan

@Centril

Memilih let (yang kebetulan tidak transparan secara referensial di dalam fn) adalah pilihan sewenang-wenang yang menurut saya tidak terlalu intuitif. Saya pikir pandangan intuitif dari alias tipe adalah bahwa mereka transparan secara referensi (bahkan jika kata itu tidak digunakan) karena mereka adalah alias.

Maaf, saya masih tidak bisa membungkus kepala saya di sekitar ini karena intuisi saya benar-benar mundur.

Misalnya, jika kita memiliki type Foo = Bar , intuisi saya mengatakan:
"Kami mendeklarasikan Foo , yang menjadi tipe yang sama dengan Bar ."

Kemudian, jika kita menulis type Foo = impl Bar , intuisi saya mengatakan:
"Kami mendeklarasikan Foo , yang menjadi tipe yang mengimplementasikan Bar ."

Jika Foo hanyalah alias tekstual untuk impl Bar , maka itu sangat tidak intuitif bagi saya. Saya suka memikirkan ini sebagai alias tekstual vs semantik .

Jadi jika Foo dapat diganti dengan impl Bar mana pun ia muncul, itu adalah alias tekstual , bagi saya yang paling mengingatkan pada makro dan metaprogramming. Tetapi jika Foo diberi makna pada titik deklarasi dan dapat digunakan di banyak tempat dengan makna aslinya (bukan makna kontekstual!), itu adalah alias semantik .

Juga, saya gagal memahami motivasi di balik tipe-tipe eksistensial kontekstual. Apakah mereka akan berguna, mengingat alias sifat dapat mencapai hal yang sama persis?

Mungkin saya menemukan transparansi referensial tidak intuitif karena latar belakang non-Haskell saya, siapa tahu... :) Tapi bagaimanapun, itu jelas bukan jenis perilaku yang saya harapkan di Rust.

@Nemo157 @stjepang

Jika Foo benar-benar alias tipe untuk tipe eksistensial

(penekanan saya). Saya membaca bahwa 'an' sebagai 'spesifik' yang berarti f dan g tidak akan mendukung tipe pengembalian beton yang berbeda, karena mereka merujuk pada tipe eksistensial yang sama.

Ini adalah penyalahgunaan istilah "tipe eksistensial", atau setidaknya cara yang bertentangan dengan posting @varkor . type Foo = impl Bar dapat muncul untuk membuat Foo alias untuk tipe ∃ T. T: Trait - dan jika Anda mengganti ∃ T. T: Trait mana pun Anda menggunakan Foo , bahkan non -tekstual , Anda bisa mendapatkan tipe beton yang berbeda di setiap posisi.

Cakupan dari quantifier ∃ T (dinyatakan dalam contoh Anda sebagai existential type _0 ) adalah hal yang dipertanyakan. Ini ketat seperti ini di APIT- pemanggil dapat memberikan nilai apa pun yang memenuhi ∃ T. T: Trait . Tapi itu tidak dalam RPIT, dan tidak dalam deklarasi existential type RFC 2071, dan tidak dalam contoh desugaring Anda- di sana, quantifier lebih jauh, pada tingkat seluruh-fungsi atau seluruh-modul, dan Anda berurusan dengan sama T mana-mana.

Jadi ambiguitas- kita sudah memiliki impl Trait menempatkan quantifier di tempat yang berbeda tergantung pada posisinya, jadi mana yang harus kita harapkan untuk type T = impl Trait ? Beberapa jajak pendapat informal, serta beberapa realisasi setelah fakta oleh peserta dalam rangkaian RFC 2071, membuktikan bahwa itu tidak jelas dalam satu atau lain cara.

Inilah sebabnya mengapa kami ingin menjauh dari interpretasi impl Trait sebagai segala sesuatu yang berkaitan dengan eksistensial, dan sebagai gantinya menggambarkan semantiknya dalam hal inferensi tipe. type T = _ tidak memiliki jenis ambiguitas yang sama- masih ada tingkat permukaan "tidak dapat menyalin-menempel _ sebagai ganti T ," tetapi tidak ada lagi " satu jenis yang T adalah alias dapat berarti beberapa jenis konkret." (Perilaku buram/tidak menyatukan adalah hal yang dibicarakan @varkor untuk ditindaklanjuti .)

transparansi referensial

Hanya karena jenis alias yang saat ini kompatibel dengan transparansi referensial, tidak berarti orang mengharapkan fitur tersebut mengikutinya.

Sebagai contoh, item const transparan referensial (disebutkan dalam https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), dan itu sebenarnya menyebabkan kebingungan pada yang baru dan yang lama pengguna (rust-lang-nursery/rust-clippy#1560).

Jadi saya pikir transparansi referensial programmer Rust bukanlah hal pertama yang mereka pikirkan.

@stjepang @kennytm Saya tidak mengatakan bahwa semua orang akan mengharapkan alias tipe dengan type Foo = impl Trait; akan bertindak secara referensial transparan. Tapi saya pikir jumlah pengguna yang tidak sepele akan, sebagaimana dibuktikan oleh kebingungan di utas ini dan di tempat lain (apa yang dimaksud @rpjohnst ...). Ini adalah masalah, tetapi mungkin bukan masalah yang tidak dapat diatasi. Ini adalah sesuatu yang perlu diingat tho saat kita bergerak maju.

Pemikiran saya saat ini tentang apa yang harus dilakukan dalam masalah ini telah bergerak sejalan dengan @varkor dan @rpjohnst.

re: transparansi referensial

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

artinya, bahkan menghasilkan tipe baru di setiap instance tidak transparan secara referensial dalam konteks alias tipe generik.

@centril Saya mengangkat tangan ketika mengharapkan transparansi referensial untuk Foo di type Foo = impl Bar; . Namun, dengan type Foo: Bar = _; , saya tidak mengharapkan transparansi referensial.

Mungkin juga kita dapat memperluas return-position impl Trait untuk mendukung banyak tipe, tanpa mekanisme seperti enum impl Trait -seperti, dengan membuat monomorf (bagian dari) pemanggil . Ini memperkuat interpretasi " impl Trait selalu eksistensial", membuatnya lebih dekat dengan dyn Trait , dan menyarankan sintaks abstract type yang tidak menggunakan impl Trait sama sekali.

Saya menulis ini di internal di sini: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Sekedar catatan ketika kami menstabilkan tipe eksistensial baru - "eksistensial" selalu dimaksudkan sebagai kata kunci sementara (menurut RFC) dan (IMO) mengerikan. Kami harus menemukan sesuatu yang lebih baik sebelum menstabilkan.

Pembicaraan tentang tipe-tipe "eksistensial" tampaknya tidak menjelaskan banyak hal. Saya akan mengatakan bahwa impl Trait adalah singkatan dari sifat implementasi tipe tertentu yang disimpulkan. Digambarkan seperti itu, type Foo = impl Bar jelas merupakan tipe yang spesifik, selalu sama—dan itu juga satu-satunya interpretasi yang benar-benar berguna: sehingga dapat digunakan dalam konteks lain selain konteks asalnya, seperti di struct.

Dalam pengertian ini, masuk akal juga untuk menulis impl Trait sebagai _ : Trait .

@rpjohnst ,

Mungkin juga kami dapat memperluas posisi pengembalian impl Trait untuk mendukung beberapa jenis

Itu akan membuatnya menjadi IMO yang kurang berguna. Inti dari alias untuk tipe impl adalah bahwa suatu fungsi dapat didefinisikan sebagai mengembalikan impl Foo , tetapi tipe spesifik masih disebarkan melalui program di struct dan lainnya. Itu akan berfungsi jika kompiler secara implisit menghasilkan enum , tetapi tidak dengan monomorfisasi.

@jan-hudec Ide-ide itu telah muncul dalam diskusi tentang Discord, dan ada beberapa masalah, terutama didasarkan pada fakta bahwa interpretasi posisi-kembali dan posisi-argumen impl Trait ini tidak konsisten.

Membuat impl Trait berdiri untuk tipe kesimpulan tertentu adalah pilihan yang baik, tetapi untuk memperbaiki ketidakkonsistenan itu, itu harus menjadi jenis inferensi tipe yang impl Trait . Ini mungkin cara yang paling mudah untuk dilakukan, tetapi tidak sesederhana yang Anda katakan.

Misalnya, setelah impl Trait berarti "gunakan tipe inferensi baru ini untuk menemukan tipe polimorfik yang memungkinkan yang mengimplementasikan Trait ," type Foo = impl Bar mulai menyiratkan hal-hal tentang modul. Aturan RFC 2071 tentang cara menyimpulkan abstract type mengatakan bahwa semua penggunaan harus secara independen menyimpulkan jenis yang sama, tetapi inferensi polimorfik ini setidaknya menyiratkan bahwa lebih banyak kemungkinan. Dan jika kita pernah mendapatkan modul parametrized (bahkan hanya selama masa hidup, ide yang jauh lebih masuk akal), akan ada pertanyaan seputar interaksi itu.

Ada juga fakta bahwa beberapa orang akan selalu menafsirkan sintaks type Foo = impl Bar sebagai alias untuk eksistensial, terlepas dari apakah mereka memahami kata "eksistensial" dan terlepas dari bagaimana kita mengajarkannya. Jadi memilih sintaks alternatif, bahkan jika itu berhasil dengan interpretasi berbasis inferensi, mungkin masih merupakan ide yang bagus.

Lebih jauh, meskipun sintaks _: Trait sebenarnya yang mengilhami diskusi seputar interpretasi berbasis inferensi, ia tidak melakukan apa yang kita inginkan. Pertama, inferensi yang disiratkan oleh _ bukan polimorfik, jadi itu analogi yang buruk untuk bahasa lainnya. Kedua, _ menyiratkan bahwa tipe aktual terlihat di tempat lain, sedangkan impl Trait dirancang khusus untuk menyembunyikan tipe aktual.

Akhirnya, alasan saya menulis proposal monomorfisasi adalah dari sudut menemukan cara lain untuk menyatukan makna argumen dan posisi kembali impl Trait . Dan meskipun ya, itu berarti bahwa -> impl Trait tidak lagi menjamin satu jenis beton, kami saat ini tidak memiliki cara untuk memanfaatkannya. Dan solusi yang diusulkan adalah solusi yang mengganggu - trik boilerplate ekstra abstract type , typeof , dll. Memaksa semua orang yang ingin mengandalkan perilaku tipe tunggal untuk juga memberi nama tipe tunggal itu melalui abstract type Sintaks

Ide-ide itu telah muncul dalam diskusi di Discord, dan ada beberapa masalah, terutama didasarkan pada fakta bahwa interpretasi posisi-kembali dan posisi-argumen impl Trait ini tidak konsisten.

Secara pribadi, saya tidak menemukan inkonsistensi ini menjadi masalah dalam praktiknya. Cakupan di mana tipe konkret ditentukan untuk posisi argumen vs posisi kembali vs posisi tipe tampaknya bekerja dengan cukup intuitif.

Saya memiliki fungsi di mana penelepon memutuskan jenis pengembaliannya. Tentu saja, saya tidak bisa menggunakan Sifat impl di sana. Ini tidak seintuitif yang Anda maksudkan sampai Anda memahami perbedaannya.

Secara pribadi, saya tidak menemukan inkonsistensi ini menjadi masalah dalam praktiknya.

Memang. Apa yang disarankan kepada saya bukanlah bahwa kita harus mengabaikan inkonsistensi, tetapi kita harus menjelaskan kembali desain sehingga konsisten (misalnya, dengan menjelaskannya sebagai inferensi tipe polimorfik). Dengan cara ini, ekstensi di masa mendatang (RFC 2071, dll.) dapat diperiksa dengan interpretasi baru yang konsisten untuk mencegah hal-hal menjadi membingungkan.

@rpjohnst

Memaksa semua orang yang ingin mengandalkan perilaku tipe tunggal untuk juga memberi nama tipe tunggal itu melalui sintaks tipe abstrak (apa pun itu) bisa dibilang merupakan manfaat secara keseluruhan.

Untuk beberapa kasus saya setuju dengan sentimen itu, tetapi itu tidak bekerja dengan penutupan atau generator, dan tidak ergonomis untuk banyak kasus di mana Anda tidak peduli apa jenisnya dan yang Anda pedulikan adalah ia mengimplementasikan sifat tertentu , misalnya dengan kombinator iterator.

@mikeyhew Anda salah paham - ini berfungsi dengan baik untuk penutupan atau jenis lain yang tidak dapat menemukan nama melalui sintaks RFC 2071 abstract type . Anda harus menemukan nama terlepas dari apakah Anda akan menggunakan satu jenis di tempat lain.

@rpjohnst oh begitu, terima kasih telah mengklarifikasi

Menunggu let x: impl Trait dengan cemas.

Sebagai suara lain untuk let x: impl Trait itu akan menyederhanakan beberapa contoh futures , berikut ini contoh contoh , saat ini menggunakan fungsi hanya untuk mendapatkan kemampuan menggunakan impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

alih-alih ini dapat ditulis sebagai pengikatan let normal:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Saya dapat membimbing seseorang melalui penerapan let x: impl Trait jika diinginkan. Ini tidak mustahil untuk dilakukan, tapi jelas juga tidak mudah. Sebuah titik masuk:

Sama halnya dengan cara kita mengunjungi Return type impl Trait di https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 kita perlu mengunjungi jenis locals di https ://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 dan pastikan item eksistensial yang baru dibuat dikembalikan bersama dengan lokal.

Kemudian, saat mengunjungi tipe penduduk setempat, pastikan untuk menyetel ExistentialContext ke Return untuk benar-benar mengaktifkannya.

Ini seharusnya sudah membuat kita sangat jauh. Tidak yakin apakah sepanjang jalan, itu tidak 100% seperti mengembalikan sifat impl posisi, tetapi sebagian besar harus berperilaku seperti itu.

@rpjohnst ,

Ide-ide itu telah muncul dalam diskusi tentang Discord, dan ada beberapa masalah, terutama didasarkan pada fakta bahwa interpretasi posisi-kembali dan posisi-argumen impl Sifat saat ini tidak konsisten.

Membawa kita kembali ke ruang lingkup yang Anda bicarakan di artikel Anda. Dan saya pikir mereka benar-benar sesuai dengan "kurung" terlampir: untuk posisi argumen itu adalah daftar argumen, untuk posisi kembali itu adalah fungsi—dan untuk alias itu akan menjadi lingkup di mana alias didefinisikan.

Saya telah membuka RFC yang mengusulkan resolusi ke sintaks konkret existential type , berdasarkan diskusi di utas ini, RFC asli, dan diskusi sinkron: https://github.com/rust-lang/rfcs/pull /2515.

Implementasi tipe eksistensial saat ini tidak dapat digunakan untuk mewakili semua posisi pengembalian saat ini definisi impl Trait , karena impl Trait menangkap setiap argumen tipe generik bahkan jika tidak digunakan, seharusnya dimungkinkan untuk melakukan hal yang sama dengan existential type , tetapi Anda mendapatkan peringatan parameter tipe yang tidak digunakan: (taman bermain)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Ini bisa menjadi masalah karena parameter tipe dapat memiliki masa pakai internal yang membatasi masa pakai impl Trait dikembalikan meskipun nilainya sendiri tidak digunakan, hapus <T> dari Bar di taman bermain di atas untuk melihat bahwa panggilan ke foo gagal tetapi bar berhasil.

Implementasi tipe eksistensial saat ini tidak dapat digunakan untuk mewakili semua posisi pengembalian saat ini menyiratkan Definisi sifat

bisa, hanya sangat merepotkan. Anda dapat mengembalikan tipe baru dengan bidang PhantomData + bidang data aktual dan menerapkan sifat sebagai penerusan ke bidang data aktual

@oli-obk Terima kasih atas saran tambahannya. Dengan saran Anda sebelumnya dan beberapa dari @cramertj , saya mungkin bisa segera mencobanya.

@fasihrana @Nemo157 Lihat di atas. Mungkin dalam beberapa minggu! :-)

Dapatkah seseorang mengklarifikasi bahwa perilaku existential type tidak menangkap parameter tipe secara implisit (yang disebutkan @Nemo157 ) disengaja dan akan tetap seperti itu? Saya menyukainya karena ini memecahkan #42940

Saya sangat sengaja menerapkannya dengan cara ini

@Arnavion Ya, ini disengaja, dan cocok dengan cara deklarasi item lain (misalnya fungsi bersarang) bekerja di Rust.

Apakah interaksi antara existential_type dan never_type sudah dibahas?

Mungkin ! harus dapat mengisi jenis eksistensial apa pun terlepas dari sifat yang terlibat.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Atau akankah ada beberapa tipe khusus yang tidak dapat disentuh yang berfungsi sebagai level tipe unimplemented!() yang dapat secara otomatis memenuhi tipe eksistensial apa pun?

@vi Saya pikir itu akan termasuk dalam "tipe tidak pernah harus mengimplementasikan semua sifat tanpa metode non-self non-default atau tipe terkait". Saya tidak tahu di mana itu akan dilacak.

Apakah ada rencana untuk memperluas dukungan ke tipe pengembalian metode sifat segera?

existential type sudah berfungsi untuk metode sifat. Wrt impl Trait , apakah itu bahkan dicakup oleh RFC?

@alexreg Saya percaya itu membutuhkan GAT untuk dapat melakukan desugar ke jenis terkait anonim ketika Anda memiliki sesuatu seperti fn foo<T>(..) -> impl Bar<T> (menjadi kira-kira -> Self::AnonBar0<T> ).

@Centril apakah maksud Anda melakukan <T> pada impl Bar sana? Perilaku penangkapan tipe implisit dari impl Trait berarti Anda mendapatkan kebutuhan yang sama untuk GAT bahkan dengan sesuatu seperti fn foo<T>(self, t: T) -> impl Bar; .

@Nemo157 tidak maaf saya tidak melakukannya. Tetapi contoh Anda menggambarkan masalahnya dengan lebih baik. Terima kasih :)

@alexreg Saya percaya itu membutuhkan GAT untuk dapat melakukan desugar ke jenis terkait anonim ketika Anda memiliki sesuatu seperti fn foo(..) -> impl Bar(menjadi kira-kira -> Self::AnonBar0).

Aku mengerti. Sejujurnya, itu tidak terdengar sangat diperlukan, tapi itu pasti salah satu cara untuk mengimplementasikannya. Kurangnya pergerakan pada GATs agak mengkhawatirkan saya meskipun ... belum mendengar apa-apa dalam waktu yang lama.

Triage: https://github.com/rust-lang/rust/pull/53542 telah digabungkan, jadi kotak centang untuk {let,const,static} foo: impl Trait dapat dicentang, saya pikir.

Akankah saya bisa menulis:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Mungkin tidak. Tapi ada rencana berkelanjutan untuk mempersiapkan semuanya sehingga kita bisa mendapatkannya

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Anda dapat bereksperimen dengan fitur ini di malam hari dalam bentuk

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Awal yang baik untuk mendapatkan info lebih lanjut tentang ini adalah https://github.com/rust-lang/rfcs/pull/2071 (dan semua yang ditautkan darinya)

@oli-obk pada rustc 1.32.0-nightly (00e03ee57 2018-11-22) , saya juga perlu memberikan batas sifat untuk existential type untuk bekerja di blok impl seperti itu. Apakah itu yang diharapkan?

@jonhoo dapat menentukan sifat-sifat itu berguna karena Anda dapat memberikan lebih dari sekadar sifat-sifat yang diperlukan

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

Sifat yang diperlukan dapat ditambahkan secara implisit ke tipe terkait eksistensial, jadi Anda hanya perlu batasan di sana saat memperluasnya, tetapi secara pribadi saya lebih suka dokumentasi lokal karena harus memasukkannya ke dalam implementasi.

@Nemo157 Ah, maaf, yang saya maksud adalah bahwa saat ini Anda _harus_ memiliki batasan di sana. Artinya, ini tidak akan dikompilasi:

impl A for B {
    existential type Assoc;
    // ...
}

sedangkan ini akan:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Oh, jadi bahkan dalam kasus di mana suatu sifat tidak memerlukan batas dari jenis yang terkait, Anda masih harus memberikan terikat pada jenis eksistensial (yang mungkin kosong) ( playground ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Ini tampak seperti kasus tepi bagi saya, memiliki tipe eksistensial tanpa batas berarti menyediakan operasi _no_ kepada pengguna (selain sifat-otomatis), jadi untuk apa itu bisa digunakan?

Juga perhatikan adalah tidak ada cara untuk melakukan hal yang sama dengan -> impl Trait , -> impl () adalah kesalahan sintaks dan -> impl sendiri memberikan error: at least one trait must be specified ; jika sintaks tipe eksistensial menjadi type Assoc = impl Debug; atau serupa maka sepertinya tidak akan ada cara untuk menentukan tipe terkait tanpa setidaknya satu sifat terikat.

@Nemo157 ya, saya baru menyadarinya karena saya benar-benar mencoba kode yang Anda sarankan di atas, dan tidak berhasil :p Saya berasumsi bahwa itu akan menyimpulkan batas dari sifat tersebut. Sebagai contoh:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Tampaknya masuk akal untuk tidak harus menentukan Future<Output = u32> untuk kedua kalinya, tetapi itu tidak berhasil. Saya berasumsi bahwa existential type Assoc: ; (yang juga tampak seperti sintaks yang sangat aneh) juga tidak akan melakukan kesimpulan itu?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Ini tampak seperti kasus tepi bagi saya, memiliki tipe eksistensial tanpa batas berarti menyediakan operasi _no_ kepada pengguna (selain sifat-otomatis), jadi untuk apa itu bisa digunakan?

Tidak bisakah ini digunakan untuk konsumsi dalam implementasi sifat yang sama? Sesuatu seperti ini:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

Ini agak dibuat-buat, tetapi bisa berguna - saya bisa membayangkan situasi di mana beberapa bagian awal perlu dibangun sebelum struct nyata untuk alasan seumur hidup. Atau bisa jadi seperti:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

Dalam kedua kasus ini existential type Assoc; akan berguna.

Apa cara yang tepat untuk mendefinisikan tipe terkait untuk Sifat impl?

Misalnya, jika saya memiliki sifat Action dan saya ingin memastikan penerapan tipe terkait sifat tersebut dapat dikirim, dapatkah saya melakukan sesuatu seperti ini:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

Apakah sesuatu yang saat ini tidak mungkin?

@acycliczebra Saya pikir sintaks untuk itu adalah -> impl Action<Result = impl Send> - ini adalah sintaks yang sama, misalnya, -> impl Iterator<Item = u32> hanya menggunakan jenis impl Trait anonim lainnya.

Apakah ada diskusi tentang memperluas sintaks impl Trait ke hal-hal seperti bidang struct? Misalnya, jika saya menerapkan pembungkus di sekitar tipe iterator tertentu untuk antarmuka publik saya:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

Ini akan berguna dalam situasi di mana saya tidak terlalu peduli dengan tipe sebenarnya selama itu memenuhi batas sifat tertentu. Contoh ini sederhana, tetapi saya pernah mengalami situasi di masa lalu di mana saya menulis tipe yang sangat panjang dengan sekelompok parameter tipe bersarang, dan itu benar-benar tidak perlu karena saya benar-benar tidak peduli tentang apa pun kecuali bahwa ini adalah ExactSizeIterator .

Namun IIRC, saya tidak berpikir ada cara untuk menentukan beberapa batas dengan impl Trait saat ini, jadi saya akan kehilangan beberapa hal berguna seperti Clone .

@AGausmann Diskusi terbaru tentang masalah ini ada di https://github.com/rust-lang/rfcs/pull/2515. Ini akan memungkinkan Anda untuk mengatakan type Foo = impl Bar; struct Baz { field: Foo } ... . Saya pikir kita mungkin ingin mempertimbangkan field: impl Trait sebagai gula untuk itu setelah menstabilkan type Foo = impl Bar; . Itu memang terasa seperti ekstensi kenyamanan ramah makro yang masuk akal.

@Sentral ,

Saya pikir kita mungkin ingin menganggap field: impl Trait sebagai gula

Saya tidak berpikir ini akan masuk akal. Bidang struct masih harus memiliki tipe konkret, jadi Anda harus memberi tahu kompiler tentang kembalinya fungsi yang terkait. Itu bisa menyimpulkannya, tetapi jika Anda memiliki banyak fungsi daripada itu tidak akan mudah untuk menemukan yang mana itu — dan kebijakan Rust yang biasa adalah eksplisit dalam kasus seperti itu.

Itu bisa menyimpulkannya, tetapi jika Anda memiliki banyak fungsi maka tidak akan mudah untuk menemukan yang mana itu

Anda akan memunculkan persyaratan untuk mendefinisikan penggunaan ke tipe induk. Kemudian semua fungsi dalam modul yang sama yang mengembalikan tipe induk. Sepertinya tidak terlalu sulit untuk saya temukan. Namun saya pikir kami ingin menyelesaikan cerita di type Foo = impl Bar; sebelum melanjutkan dengan ekstensi.

Saya rasa saya menemukan bug dalam implementasi existential type .


Kode

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Kesalahan

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tempat bermain

Anda dapat menemukan ini di stackoverflow juga.

Saya tidak 100% yakin kami dapat mendukung kasus ini di luar kotak, tetapi yang dapat Anda lakukan adalah menulis ulang fungsi untuk memiliki dua parameter umum:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Terima kasih!
Yup, inilah yang saya lakukan seperti yang diposting di posting stackoverflow:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Apakah ada rencana untuk impl Trait tersedia dalam konteks sifat?
Tidak hanya sebagai tipe terkait, tetapi juga sebagai nilai balik dalam metode.

sifat impl dalam sifat adalah fitur terpisah dari yang dilacak di sini, dan saat ini tidak memiliki RFC. Ada sejarah desain yang cukup panjang di ruang ini, dan iterasi lebih lanjut ditunda hingga implementasi 2071 (tipe eksistensial) stabil, yang diblokir pada masalah implementasi serta sintaks yang belum terselesaikan (yang memiliki RFC terpisah).

@cramertj Sintaksnya hampir terselesaikan. Saya percaya pemblokir utama adalah GAT sekarang.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 masih menunggu di @withoutboats.

@varkor Ya, saya hanya optimis mereka akan segera melihat cahaya dengan RFC itu. ;-)

Akankah sesuatu seperti berikut ini mungkin?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Anda dapat melakukan ini sekarang, meskipun hanya dengan fungsi hint untuk menentukan tipe konkret Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

Bagaimana Anda akan menulisnya jika panggilan balik dapat memilih jenis argumennya? Sebenarnya nvm, saya kira Anda bisa menyelesaikannya melalui generik normal.

@CryZe Apa yang Anda cari tidak terkait dengan impl Trait . Lihat https://github.com/rust-lang/rfcs/issues/2413 untuk semua yang saya ketahui tentangnya.

Ini berpotensi terlihat seperti itu:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, menarik. Terima kasih!

Apakah ini seharusnya berhasil?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Apakah kita harus menyimpan kata kunci existential dalam bahasa untuk fitur existential_type ?

@jethrogb Ya. Fakta bahwa saat ini tidak adalah bug.

@cramertj Oke. Haruskah saya mengajukan masalah terpisah untuk itu atau apakah posting saya cukup di sini?

Mengajukan masalah akan sangat bagus, terima kasih! :)

Apakah kita harus menyimpan kata kunci existential dalam bahasa untuk fitur existential_type ?

Saya pikir tujuannya adalah untuk segera menghentikan ini ketika fitur type-alias-impl-trait diimplementasikan (yaitu, dimasukkan ke dalam lint) dan akhirnya menghapusnya dari sintaks.

Seseorang mungkin bisa mengklarifikasi sekalipun.

Menutup ini demi masalah meta yang melacak impl Trait lebih umum: https://github.com/rust-lang/rust/issues/63066

tidak ada satu pun contoh bagus di mana pun tentang cara menggunakan Sifat impl,, sangat menyedihkan

Apakah halaman ini membantu?
0 / 5 - 0 peringkat