Rust: Masalah pelacakan untuk async/menunggu (RFC 2394)

Dibuat pada 8 Mei 2018  ·  308Komentar  ·  Sumber: rust-lang/rust

Ini adalah masalah pelacakan untuk RFC 2394 (rust-lang/rfcs#2394), yang menambahkan sintaks async dan menunggu ke bahasa.

Saya akan mempelopori pekerjaan implementasi RFC ini, tetapi akan menghargai bimbingan karena saya memiliki pengalaman yang relatif sedikit bekerja di rustc.

MELAKUKAN:

Pertanyaan yang belum terselesaikan:

A-async-await A-generators AsyncAwait-Triaged B-RFC-approved C-tracking-issue T-lang

Komentar yang paling membantu

Tentang sintaks: Saya sangat ingin memiliki await sebagai kata kunci sederhana. Sebagai contoh, mari kita lihat sebuah keprihatinan dari blog:

Kami tidak yakin persis sintaks apa yang kami inginkan untuk kata kunci menunggu. Jika sesuatu adalah masa depan Hasil - seperti kemungkinan masa depan IO - Anda ingin dapat menunggunya dan kemudian menerapkan operator ? ke dalamnya. Tetapi urutan prioritas untuk mengaktifkan ini mungkin tampak mengejutkan - await io_future? akan await pertama dan ? kedua, meskipun ? secara leksikal terikat lebih erat daripada menunggu.

Saya setuju di sini, tetapi kawat gigi itu jahat. Saya pikir lebih mudah untuk mengingat bahwa ? memiliki prioritas lebih rendah daripada await dan diakhiri dengan itu:

let foo = await future?

Lebih mudah dibaca, lebih mudah untuk refactor. Saya percaya itu pendekatan yang lebih baik.

let foo = await!(future)?

Memungkinkan untuk lebih memahami urutan operasi yang dijalankan, tetapi kurang dapat dibaca.

Saya percaya bahwa setelah Anda mendapatkan await foo? mengeksekusi await terlebih dahulu maka Anda tidak memiliki masalah dengan itu. Ini mungkin secara leksikal lebih terikat, tetapi await ada di sisi kiri dan ? di sisi kanan. Jadi masih cukup logis untuk await terlebih dahulu dan menangani Result setelahnya.


Jika ada ketidaksepakatan, silakan ungkapkan sehingga kita bisa berdiskusi. Saya tidak mengerti apa yang dimaksud dengan silent downvote. Kita semua berharap yang baik untuk Rust.

Semua 308 komentar

Diskusi di sini tampaknya telah mereda, jadi tautkan di sini sebagai bagian dari pertanyaan sintaks await : https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

Implementasi diblokir pada #50307.

Tentang sintaks: Saya sangat ingin memiliki await sebagai kata kunci sederhana. Sebagai contoh, mari kita lihat sebuah keprihatinan dari blog:

Kami tidak yakin persis sintaks apa yang kami inginkan untuk kata kunci menunggu. Jika sesuatu adalah masa depan Hasil - seperti kemungkinan masa depan IO - Anda ingin dapat menunggunya dan kemudian menerapkan operator ? ke dalamnya. Tetapi urutan prioritas untuk mengaktifkan ini mungkin tampak mengejutkan - await io_future? akan await pertama dan ? kedua, meskipun ? secara leksikal terikat lebih erat daripada menunggu.

Saya setuju di sini, tetapi kawat gigi itu jahat. Saya pikir lebih mudah untuk mengingat bahwa ? memiliki prioritas lebih rendah daripada await dan diakhiri dengan itu:

let foo = await future?

Lebih mudah dibaca, lebih mudah untuk refactor. Saya percaya itu pendekatan yang lebih baik.

let foo = await!(future)?

Memungkinkan untuk lebih memahami urutan operasi yang dijalankan, tetapi kurang dapat dibaca.

Saya percaya bahwa setelah Anda mendapatkan await foo? mengeksekusi await terlebih dahulu maka Anda tidak memiliki masalah dengan itu. Ini mungkin secara leksikal lebih terikat, tetapi await ada di sisi kiri dan ? di sisi kanan. Jadi masih cukup logis untuk await terlebih dahulu dan menangani Result setelahnya.


Jika ada ketidaksepakatan, silakan ungkapkan sehingga kita bisa berdiskusi. Saya tidak mengerti apa yang dimaksud dengan silent downvote. Kita semua berharap yang baik untuk Rust.

Saya memiliki pandangan yang beragam tentang await menjadi kata kunci, @Pzixel. Meskipun memiliki daya tarik estetis, dan mungkin lebih konsisten, mengingat async adalah kata kunci, "kata kunci mengasapi" dalam bahasa apa pun benar-benar menjadi perhatian. Yang mengatakan, apakah memiliki async tanpa await bahkan masuk akal, dari segi fitur? Jika ya, mungkin kita bisa membiarkannya apa adanya. Jika tidak, saya akan cenderung menjadikan await sebagai kata kunci.

Saya pikir lebih mudah untuk mengingat bahwa ? memiliki prioritas lebih rendah daripada await dan diakhiri dengan itu

Mungkin saja untuk mempelajarinya dan menginternalisasikannya, tetapi ada intuisi yang kuat bahwa hal-hal yang bersentuhan lebih terikat erat daripada hal-hal yang dipisahkan oleh spasi, jadi saya pikir itu akan selalu salah membaca pada pandangan pertama dalam praktik.

Itu juga tidak membantu dalam semua kasus, misalnya fungsi yang mengembalikan Result<impl Future, _> :

let foo = await (foo()?)?;

Kekhawatiran di sini bukan hanya "dapatkah Anda memahami prioritas dari satu waiting+ ? ," tetapi juga "seperti apa rasanya merangkai beberapa menunggu." Jadi bahkan jika kita hanya memilih prioritas, kita masih akan memiliki masalah await (await (await first()?).second()?).third()? .

Ringkasan opsi untuk sintaks await , beberapa dari RFC dan sisanya dari utas RFC:

  • Memerlukan semacam pembatas: await { future }? atau await(future)? (ini berisik).
  • Cukup pilih prioritas, sehingga await future? atau (await future)? melakukan apa yang diharapkan (keduanya terasa mengejutkan).
  • Gabungkan kedua operator menjadi sesuatu seperti await? future (ini tidak biasa).
  • Buat await postfix entah bagaimana, seperti pada future await? atau future.await? (ini belum pernah terjadi sebelumnya).
  • Gunakan sigil baru seperti yang dilakukan ? , seperti pada future@? (ini adalah "line noise").
  • Tidak menggunakan sintaks sama sekali, membuat menunggu implisit (ini membuat poin suspensi lebih sulit untuk dilihat). Agar ini berhasil, tindakan membangun masa depan juga harus dibuat eksplisit. Ini adalah subjek dari utas internal yang saya tautkan di atas .

Yang mengatakan, apakah memiliki async tanpa await bahkan masuk akal, dari segi fitur?

@alexreg Memang. Kotlin bekerja dengan cara ini, misalnya. Ini adalah opsi "menunggu implisit".

@rpjohnst Menarik. Yah, saya biasanya meninggalkan async dan await sebagai fitur eksplisit bahasa, karena saya pikir itu lebih dalam semangat Rust, tapi kemudian saya bukan ahli dalam pemrograman asinkron. ..

@alexreg async/await adalah fitur yang sangat bagus, karena saya bekerja dengannya setiap hari di C# (yang merupakan bahasa utama saya). @rpjohnst mengklasifikasikan semua kemungkinan dengan sangat baik. Saya lebih suka opsi kedua, saya setuju dengan pertimbangan orang lain (berisik/tidak biasa/...). Saya telah bekerja dengan kode async/menunggu selama 5 tahun terakhir atau sesuatu, sangat penting untuk memiliki kata kunci bendera seperti itu.

@rpjohnst

Jadi bahkan jika kita hanya memilih prioritas, kita masih akan memiliki masalah menunggu (menunggu (menunggu pertama()?).kedua()?).ketiga()?.

Dalam praktik saya, Anda tidak pernah menulis dua await dalam satu baris. Dalam kasus yang sangat jarang ketika Anda membutuhkannya, Anda cukup menulis ulang sebagai then dan tidak menggunakan menunggu sama sekali. Anda dapat melihat diri Anda sendiri bahwa membaca itu jauh lebih sulit daripada

let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;

Jadi saya pikir tidak apa-apa jika bahasa melarang untuk menulis kode sedemikian rupa untuk membuat kasus utama lebih sederhana dan lebih baik.

hero away future await? terlihat menarik meskipun tidak familiar, tapi saya tidak melihat adanya argumentasi yang logis untuk menentangnya.

Dalam praktik saya, Anda tidak pernah menulis dua await dalam satu baris.

Tetapi apakah ini karena ini adalah ide yang buruk terlepas dari sintaksnya, atau hanya karena sintaks C# await membuatnya jelek? Orang-orang membuat argumen serupa seputar try!() (pendahulu dari ? ).

Versi postfix dan implisit jauh lebih jelek:

first().await?.second().await?.third().await?
first()?.second()?.third()?

Tetapi apakah ini karena ini adalah ide yang buruk terlepas dari sintaksnya, atau hanya karena sintaks C# yang ada membuatnya jelek?

Saya pikir itu ide yang buruk terlepas dari sintaks karena memiliki satu baris per async operasi sudah cukup rumit untuk dipahami dan sulit untuk di-debug. Membuat mereka dirantai dalam satu pernyataan tampaknya lebih buruk.

Misalnya mari kita lihat kode nyata (saya telah mengambil satu bagian dari proyek saya):

[Fact]
public async Task Should_UpdateTrackableStatus()
{
    var web3 = TestHelper.GetWeb3();
    var factory = await SeasonFactory.DeployAsync(web3);
    var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
    var request = await season.GetOrCreateRequestAsync("123");

    var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
    var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");

    await request.UpdateStatusAsync(trackableStatus);
    await request.UpdateStatusAsync(nonTrackableStatus);

    var statuses = await request.GetStatusesAsync();

    Assert.Single(statuses);
    Assert.Equal(trackableStatus, statuses.Single());
}

Ini menunjukkan bahwa dalam praktiknya tidak layak untuk merantai await s bahkan jika sintaks mengizinkannya, karena itu akan menjadi benar-benar tidak dapat dibaca await hanya membuat oneliner lebih sulit untuk ditulis dan dibaca, tetapi saya melakukannya percaya itu bukan satu-satunya alasan mengapa itu buruk.

Versi postfix dan implisit jauh lebih jelek

Kemungkinan untuk membedakan tugas mulai dan tugas menunggu sangat penting. Misalnya, saya sering menulis kode seperti itu (sekali lagi, cuplikan dari proyek):

public async Task<StatusUpdate[]> GetStatusesAsync()
{
    int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
    var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
    var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
    {
        var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
        return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
    });

    return await Task.WhenAll(tasks);
}

Di sini kita membuat N permintaan async dan kemudian menunggunya. Kami tidak menunggu pada setiap iterasi loop, tetapi pertama-tama kami membuat larik permintaan asinkron dan kemudian menunggu semuanya sekaligus.

Saya tidak tahu Kotlin, jadi mungkin mereka menyelesaikan ini entah bagaimana. Tetapi saya tidak melihat bagaimana Anda dapat mengungkapkannya jika "berjalan" dan "menunggu" tugasnya sama.


Jadi saya pikir versi implisit tidak mungkin dilakukan dalam bahasa yang lebih implisit seperti C#.
Di Rust dengan aturannya yang bahkan tidak mengizinkan Anda untuk secara implisit mengonversi u8 menjadi i32 itu akan jauh lebih membingungkan.

@Pzixel Ya, opsi kedua terdengar seperti salah satu yang lebih disukai. Saya telah menggunakan async/await di C# juga, tetapi tidak terlalu banyak, karena saya belum memprogram terutama dalam C# selama beberapa tahun sekarang. Adapun prioritas, await (future?) lebih alami bagi saya.

@rpjohnst Saya agak menyukai ide operator postfix, tetapi saya juga khawatir tentang keterbacaan dan asumsi yang akan dibuat orang – ini dapat dengan mudah membingungkan anggota struct bernama await .

Kemungkinan untuk membedakan tugas mulai dan tugas menunggu sangat penting.

Untuk apa nilainya, versi implisit memang melakukan ini. Itu dibahas sampai mati baik di utas RFC dan di utas internal, jadi saya tidak akan membahas banyak detail di sini, tetapi ide dasarnya hanya memindahkan kejelasan dari tugas yang menunggu ke konstruksi tugas - tidak 't memperkenalkan implisit baru .

Contoh Anda akan terlihat seperti ini:

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Inilah yang saya maksud dengan "agar ini berhasil, tindakan membangun masa depan juga harus dibuat eksplisit." Ini sangat mirip dengan bekerja dengan utas dalam kode sinkronisasi - memanggil fungsi selalu menunggu sampai selesai sebelum melanjutkan pemanggil, dan ada alat terpisah untuk memperkenalkan konkurensi. Misalnya, penutupan dan thread::spawn / join sesuai dengan blok asinkron dan join_all / select /etc.

Untuk apa nilainya, versi implisit memang melakukan ini. Itu dibahas sampai mati baik di utas RFC dan di utas internal, jadi saya tidak akan membahas banyak detail di sini, tetapi ide dasarnya hanya memindahkan kejelasan dari tugas yang menunggu ke konstruksi tugas - tidak 't memperkenalkan implisit baru.

Saya percaya itu tidak. Saya tidak bisa melihat di sini aliran apa yang akan ada dalam fungsi ini, di mana titik-titik di mana eksekusi berhenti sampai menunggu selesai. Saya hanya melihat blok async yang mengatakan "halo, di suatu tempat di sini ada fungsi async, coba cari tahu yang mana, Anda akan terkejut!".

Poin lain: Rust cenderung menjadi bahasa di mana Anda dapat mengekspresikan segalanya, dekat dengan bare metal dan sebagainya. Saya ingin memberikan beberapa kode yang cukup artifisial, tetapi saya pikir itu menggambarkan idenya:

var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
   var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
   if (firstFinishedTask != b) // our task is timeouted
      throw new Exception(); // doing something
   // more logic here
}
else
{
   // more logic here
}

Karat selalu cenderung memberikan kontrol penuh atas apa yang terjadi. await memungkinkan Anda untuk menentukan titik di mana proses lanjutan. Ini juga memungkinkan Anda untuk unwrap nilai di masa depan. Jika Anda mengizinkan konversi implisit di sisi penggunaan, ini memiliki beberapa implikasi:

  1. Pertama-tama, Anda harus menulis beberapa kode kotor untuk meniru perilaku ini.
  2. Sekarang RLS dan IDE seharusnya mengharapkan bahwa nilai kita adalah Future<T> atau menunggu T itu sendiri. Ini bukan masalah dengan kata kunci - itu ada, maka hasilnya adalah T , jika tidak Future<T>
  3. Itu membuat kode lebih sulit untuk dipahami. Dalam contoh Anda, saya tidak melihat mengapa itu mengganggu eksekusi pada baris get_status_updates , tetapi tidak pada get_status_update . Mereka sangat mirip satu sama lain. Jadi itu tidak berfungsi seperti kode aslinya atau sangat rumit sehingga saya tidak dapat melihatnya bahkan ketika saya cukup akrab dengan subjeknya. Kedua alternatif tidak membuat pilihan ini menguntungkan.

Saya tidak bisa melihat di sini aliran apa yang akan ada dalam fungsi ini, di mana titik-titik di mana eksekusi berhenti sampai menunggu selesai.

Ya, ini yang saya maksud dengan "ini membuat titik suspensi lebih sulit dilihat." Jika Anda membaca utas internal yang ditautkan, saya membuat argumen mengapa ini bukan masalah besar. Anda tidak perlu menulis kode baru, Anda cukup meletakkan anotasi di tempat yang berbeda ( blok async alih-alih ekspresi await ed). IDE tidak memiliki masalah untuk menentukan tipenya (selalu T untuk panggilan fungsi dan Future<Output=T> untuk blok async ).

Saya juga akan mencatat bahwa pemahaman Anda mungkin salah terlepas dari sintaksnya. Fungsi async Rust tidak menjalankan kode apa pun sampai mereka ditunggu dalam beberapa cara, jadi cek b.Status != TaskStatus.RanToCompletion akan selalu lolos. Ini juga dibahas sampai mati di utas RFC, jika Anda tertarik mengapa ini bekerja dengan cara ini.

Dalam contoh Anda, saya tidak melihat mengapa itu mengganggu eksekusi pada baris get_status_updates , tetapi tidak pada get_status_update . Mereka sangat mirip satu sama lain.

Itu tidak eksekusi interupsi di kedua tempat. Kuncinya adalah blok async tidak berjalan sampai mereka ditunggu, karena ini berlaku untuk semua futures di Rust, seperti yang saya jelaskan di atas. Dalam contoh saya, get_statuses memanggil (dan dengan demikian menunggu) get_status_updates , kemudian dalam loop ia membangun (tetapi tidak menunggu) count futures, lalu ia memanggil (dan dengan demikian menunggu ) join_all , pada saat itu futures tersebut secara bersamaan memanggil (dan dengan demikian menunggu) get_status_update .

Satu-satunya perbedaan dengan contoh Anda adalah kapan tepatnya masa depan mulai berjalan- di Anda, itu selama loop; di tambang, itu selama join_all . Tapi ini adalah bagian mendasar dari cara kerja Rust futures, tidak ada hubungannya dengan sintaks implisit atau bahkan dengan async / await sama sekali.

Saya juga akan mencatat bahwa pemahaman Anda mungkin salah terlepas dari sintaksnya. Fungsi asinkron Rust tidak menjalankan kode apa pun sampai mereka ditunggu, jadi pemeriksaan b.Status != TaskStatus.RanToCompletion Anda akan selalu lolos.

Ya, tugas C# dijalankan secara serempak hingga titik penangguhan pertama. Terima kasih telah menunjukkan hal itu.
Namun, itu tidak terlalu penting karena saya masih harus dapat menjalankan beberapa tugas di latar belakang saat menjalankan sisa metode dan kemudian memeriksa apakah tugas latar belakang sudah selesai. Misalnya bisa

var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the same

Saya mendapatkan ide Anda tentang blok async dan seperti yang saya lihat mereka adalah binatang yang sama, tetapi dengan lebih banyak kerugian. Dalam proposal asli, setiap tugas asinkron dipasangkan dengan await . Dengan blok async setiap tugas akan dipasangkan dengan blok async pada titik konstruksi, jadi kami berada dalam situasi yang hampir sama seperti sebelumnya (hubungan 1:1), tetapi bahkan sedikit lebih buruk, karena rasanya lebih tidak wajar, dan lebih sulit untuk dipahami, karena perilaku callsite menjadi tergantung konteks. Dengan menunggu saya dapat melihat let a = foo() atau let b = await foo() dan saya akan tahu bahwa tugas ini baru saja dibangun atau dibangun dan menunggu. Jika saya melihat let a = foo() dengan async blok saya harus mencari apakah ada beberapa async atas, jika saya benar, karena dalam kasus ini

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Kami sedang menunggu untuk semua tugas sekaligus sementara di sini

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Isn't "just a construction" anymore
        task.push({
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }
    tasks 
}

Kami mengeksekusi mereka satu menjadi satu.

Jadi saya tidak bisa mengatakan apa perilaku sebenarnya dari bagian ini:

let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))

Tanpa memiliki lebih banyak konteks.

Dan hal-hal menjadi lebih aneh dengan blok bersarang. Belum lagi pertanyaan tentang perkakas dll.

perilaku callsite menjadi tergantung konteks

Ini sudah benar dengan kode sinkronisasi normal dan penutupan. Sebagai contoh:

// Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
    let data = do_something_synchronous();
    StatusUpdate { data }
});

vs

// Execute a block, immediately running `do_something_synchronous()`:
task.push({
    let data = do_something_synchronous();
    StatusUpdate { data }
});

Satu hal lain yang harus Anda perhatikan dari proposal menunggu implisit penuh adalah bahwa Anda tidak dapat memanggil async fn s dari konteks non- async . Ini berarti sintaks pemanggilan fungsi some_function(arg1, arg2, etc) selalu menjalankan some_function 's body sampai selesai sebelum pemanggil melanjutkan, terlepas dari apakah some_function adalah async . Jadi masuk ke dalam konteks async selalu ditandai secara eksplisit, dan sintaks pemanggilan fungsi sebenarnya lebih konsisten.

Mengenai sintaks menunggu: Bagaimana dengan makro dengan sintaks metode? Saya tidak dapat menemukan RFC aktual untuk mengizinkan ini, tetapi saya telah menemukan beberapa diskusi ( 1 , 2 ) di reddit sehingga idenya tidak pernah terjadi sebelumnya. Ini akan memungkinkan await untuk bekerja di posisi postfix tanpa menjadikannya kata kunci / memperkenalkan sintaks baru hanya untuk fitur ini.

// Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()?

Ada perpustakaan dari dunia Scala yang menyederhanakan komposisi monad: http://monadless.io

Mungkin beberapa ide menarik untuk Rust.

kutipan dari dokumen:

Sebagian besar bahasa utama memiliki dukungan untuk pemrograman asinkron menggunakan idiom async/await atau menerapkannya (misalnya F#, C#/VB, Javascript, Python, Swift). Meskipun berguna, async/await biasanya terkait dengan monad tertentu yang mewakili komputasi asinkron (Tugas, Masa Depan, dll.).

Pustaka ini mengimplementasikan solusi yang mirip dengan async/menunggu tetapi digeneralisasi untuk semua jenis monad. Generalisasi ini merupakan faktor utama mengingat beberapa basis kode menggunakan monad lain seperti Task selain Future untuk komputasi asinkron.

Diberikan monad M , generalisasi menggunakan konsep mengangkat nilai reguler ke monad ( T => M[T] ) dan melepas nilai dari instance monad ( M[T] => T ). > Contoh penggunaan:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Perhatikan bahwa pengangkatan sesuai dengan asinkron dan pelepasan untuk menunggu.

Ini sudah benar dengan kode sinkronisasi normal dan penutupan. Sebagai contoh:

Saya melihat beberapa perbedaan di sini:

  1. Konteks Lambda tidak dapat dihindari, tetapi bukan untuk await . Dengan await kita tidak memiliki konteks, dengan async kita harus memilikinya. Yang pertama menang, karena menyediakan fitur yang sama, tetapi membutuhkan lebih sedikit pengetahuan tentang kodenya.
  2. Lambdas cenderung pendek, paling banyak beberapa baris sehingga kita melihat seluruh tubuh sekaligus, dan sederhana. Fungsi async mungkin cukup besar (sebesar fungsi biasa) dan rumit.
  3. Lambdas jarang bersarang (kecuali untuk panggilan then , tetapi await diusulkan), blok async sering bersarang.

Satu hal lain yang harus Anda perhatikan dari proposal menunggu implisit penuh adalah bahwa Anda tidak dapat memanggil async fns dari konteks non-async.

Hmm, aku tidak menyadarinya. Kedengarannya tidak bagus, karena dalam praktik saya, Anda sering ingin menjalankan async dari konteks non-async. Di C# async hanyalah kata kunci yang memungkinkan kompiler untuk menulis ulang badan fungsi, itu tidak memengaruhi antarmuka fungsi dengan cara apa pun sehingga async Task<Foo> dan Task<Foo> benar-benar dapat dipertukarkan, dan itu memisahkan implementasi dan API.

Terkadang Anda mungkin ingin memblokir tugas async , misalnya ketika Anda ingin memanggil beberapa API jaringan dari main . Anda harus memblokir (jika tidak, Anda kembali ke OS dan program berakhir) tetapi Anda harus menjalankan permintaan HTTP asinkron. Saya tidak yakin solusi apa yang bisa ada di sini kecuali meretas main untuk memungkinkannya menjadi async serta yang kami lakukan dengan Result tipe pengembalian utama, jika Anda tidak dapat memanggilnya dari non-async main .

Pertimbangan lain yang mendukung await ini adalah cara kerjanya dalam bahasa populer lainnya (seperti yang dicatat oleh @fdietze ). Itu membuatnya lebih mudah untuk bermigrasi dari bahasa lain seperti C#/TypeScript/JS/Python dan dengan demikian merupakan pendekatan yang lebih baik dalam hal menarik orang baru.

Saya melihat beberapa perbedaan di sini

Anda juga harus menyadari bahwa RFC utama sudah memiliki blok async , dengan semantik yang sama dengan versi implisit.

Kedengarannya tidak bagus, karena dalam praktik saya, Anda sering ingin menjalankan async dari konteks non-async.

Ini bukan masalah. Anda masih bisa menggunakan async blok di non async konteks (yang baik-baik saja karena mereka hanya mengevaluasi ke F: Future seperti biasa), dan Anda masih bisa bertelur atau blok pada futures menggunakan API yang sama persis seperti sebelumnya.

Anda tidak dapat memanggil async fn s, tetapi alihkan panggilan ke mereka dalam blok async seperti yang Anda lakukan terlepas dari konteks Anda, jika Anda menginginkan F: Future keluar dari itu.

async hanyalah kata kunci yang memungkinkan kompiler untuk menulis ulang badan fungsi, itu tidak memengaruhi antarmuka fungsi dengan cara apa pun

Ya, ini adalah perbedaan yang sah antara proposal. Itu juga tercakup dalam utas internal. Diperdebatkan, memiliki antarmuka yang berbeda untuk keduanya berguna karena ini menunjukkan kepada Anda bahwa versi async fn tidak akan menjalankan kode apa pun sebagai bagian dari konstruksi, sedangkan versi -> impl Future mungkin misalnya memulai permintaan sebelum memberi Anda sebuah F: Future . Itu juga membuat async fn s lebih konsisten dengan normal fn s, dalam memanggil sesuatu yang dideklarasikan sebagai -> T akan selalu memberi Anda T , terlepas dari apakah itu async .

(Anda juga harus mencatat bahwa di Rust masih cukup lompatan antara async fn dan Future versi -returning, seperti yang dijelaskan dalam RFC. The async fn versi tidak menyebutkan Future di mana saja dalam tanda tangannya; dan versi manual memerlukan impl Trait , yang membawa serta beberapa masalah yang berkaitan dengan masa pakai. Sebenarnya, ini adalah bagian dari motivasi untuk async fn untuk memulai.)

Itu membuatnya lebih mudah untuk bermigrasi dari bahasa lain seperti C#/TypeScript/JS/Python

Ini adalah keuntungan hanya untuk sintaks literal await future , yang cukup bermasalah sendiri di Rust. Hal lain yang mungkin kita temukan juga memiliki ketidakcocokan dengan bahasa tersebut, sementara menunggu implisit setidaknya memiliki a) kesamaan dengan Kotlin dan b) kesamaan dengan sinkron, kode berbasis utas.

Ya, ini adalah perbedaan yang sah antara proposal. Itu juga tercakup dalam utas internal. Diperdebatkan, memiliki antarmuka yang berbeda untuk keduanya berguna

Saya akan mengatakan _memiliki antarmuka yang berbeda untuk keduanya memiliki beberapa keuntungan_, karena memiliki API yang bergantung pada detail implementasi tidak terdengar bagus bagi saya. Misalnya, Anda menulis kontrak yang hanya mendelegasikan panggilan ke masa depan internal

fn foo(&self) -> Future<T> {
   self.myService.foo()
}

Dan kemudian Anda hanya ingin menambahkan beberapa logging

async fn foo(&self) -> T {
   let result = await self.myService.foo();
   self.logger.log("foo executed with result {}.", result);
   result
}

Dan itu menjadi perubahan yang menghancurkan. Siapa?

Ini adalah keuntungan hanya untuk sintaks menunggu literal di masa depan, yang cukup bermasalah sendiri di Rust. Hal lain yang mungkin kita temukan juga memiliki ketidakcocokan dengan bahasa tersebut, sementara menunggu implisit setidaknya memiliki a) kesamaan dengan Kotlin dan b) kesamaan dengan sinkron, kode berbasis utas.

Ini adalah keuntungan untuk semua sintaks await , await foo / foo await / foo@ / foo.await /... setelah Anda mendapatkannya hal yang sama, satu-satunya perbedaan adalah Anda menempatkannya sebelum/sesudah atau memiliki sigil alih-alih kata kunci.

Anda juga harus mencatat bahwa di Rust masih ada lompatan besar antara async fn dan versi Future-return, seperti yang dijelaskan dalam RFC

Saya tahu itu dan itu sangat mengganggu saya.

Dan itu menjadi perubahan yang menghancurkan.

Anda dapat menyiasatinya dengan mengembalikan blok async . Di bawah proposal menunggu implisit, contoh Anda terlihat seperti ini:

fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
    async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}

Dan dengan masuk:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        result
    }
}

Masalah yang lebih besar dengan memiliki perbedaan ini muncul selama transisi ekosistem dari implementasi dan kombinator manual di masa mendatang (satu-satunya cara hari ini) ke async/menunggu. Tetapi meskipun demikian, proposal tersebut memungkinkan Anda untuk mempertahankan antarmuka lama dan menyediakan antarmuka asinkron baru di sampingnya. C# penuh dengan pola itu, misalnya.

Yah, itu terdengar masuk akal.

Namun, saya percaya implisit seperti itu (kami tidak melihat apakah foo() sini adalah fungsi asinkron atau sinkronisasi) mengarah ke masalah yang sama yang muncul dalam protokol seperti COM+ dan merupakan alasan penerapan WCF seperti sebelumnya . Orang-orang mengalami masalah ketika permintaan jarak jauh async tampak seperti panggilan metode sederhana.

Kode ini terlihat baik-baik saja kecuali saya tidak dapat melihat apakah ada permintaan apakah async atau sync. Saya percaya bahwa itu informasi penting. Sebagai contoh:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        let bars: Vec<Bar> = Vec::new();
        for i in 0..100 {
           bars.push(self.my_other_service.bar(i, result));
        }
        result
    }
}

Sangat penting untuk mengetahui apakah bar adalah fungsi sinkron atau asinkron. Saya sering melihat await dalam loop sebagai penanda bahwa kode ini harus diubah untuk mencapai yang lebih baik di seluruh beban dan kinerja. Ini adalah kode yang saya ulas kemarin (kode tidak optimal, tetapi ini adalah salah satu iterasi ulasan):

image

Seperti yang Anda lihat, saya dengan mudah melihat bahwa kami memiliki perulangan menunggu di sini dan saya meminta untuk mengubahnya. Ketika perubahan dilakukan, kami mendapat kecepatan memuat halaman 3x. Tanpa await saya dapat dengan mudah mengabaikan perilaku buruk ini.

Saya akui saya belum pernah menggunakan Kotlin, tetapi terakhir kali saya melihat bahasa itu, tampaknya sebagian besar merupakan varian Java dengan sintaks yang lebih sedikit, sampai pada titik di mana mudah untuk menerjemahkan satu sama lain secara mekanis. Saya juga dapat membayangkan mengapa itu akan disukai di dunia Java (yang cenderung sedikit berat sintaksis), dan saya sadar itu baru-baru ini mendapat peningkatan popularitas khususnya karena bukan Java (situasi Oracle vs. Google ).

Namun, jika kita memutuskan untuk mempertimbangkan popularitas dan keakraban, kita mungkin ingin melihat apa yang dilakukan JavaScript, yang juga secara eksplisit await .

Yang mengatakan, await diperkenalkan ke bahasa utama oleh C#, yang mungkin merupakan salah satu bahasa di mana kegunaan dianggap paling penting . Dalam C#, panggilan asinkron tidak hanya ditunjukkan oleh kata kunci await , tetapi juga oleh akhiran Async dari panggilan metode. Fitur bahasa lain yang paling banyak berbagi dengan await , yield return juga terlihat jelas dalam kode.

Mengapa demikian? Pendapat saya adalah bahwa generator dan panggilan asinkron adalah konstruksi yang terlalu kuat untuk membiarkannya lewat tanpa diketahui dalam kode. Ada hierarki operator aliran kontrol:

  • eksekusi berurutan dari pernyataan (implisit)
  • panggilan fungsi/metode (cukup jelas, bandingkan dengan misalnya Pascal mana tidak ada perbedaan di situs panggilan antara fungsi nullary dan variabel)
  • goto (baiklah, ini bukan hierarki yang ketat)
  • generator ( yield return cenderung menonjol)
  • await + Async akhiran

Perhatikan bagaimana mereka juga berubah dari kurang menjadi lebih bertele-tele, sesuai dengan ekspresi atau kekuatannya.

Tentu saja, bahasa lain mengambil pendekatan yang berbeda. Kelanjutan skema (seperti di call/cc , yang tidak terlalu berbeda dari await ) atau makro tidak memiliki sintaks untuk menunjukkan apa yang Anda panggil. Untuk makro, Rust mengambil pendekatan untuk membuatnya mudah dilihat.

Jadi saya berpendapat bahwa memiliki lebih sedikit sintaks tidak diinginkan dalam dirinya sendiri (ada bahasa seperti APL atau Perl untuk itu), dan sintaks itu tidak harus hanya boilerplate, dan memiliki peran penting dalam keterbacaan.

Ada juga argumen paralel (maaf, saya tidak ingat sumbernya, tetapi mungkin berasal dari seseorang di tim bahasa) bahwa orang lebih nyaman dengan sintaks yang bising untuk fitur baru ketika mereka baru, tetapi kemudian baik-baik saja dengan kurang verbose setelah mereka akhirnya menjadi umum digunakan.


Adapun pertanyaan await!(foo)? vs. await foo? , saya berada di kubu sebelumnya. Anda dapat menginternalisasi hampir semua sintaks, namun kami terlalu terbiasa mengambil isyarat dari spasi dan kedekatan. Dengan await foo? ada kemungkinan seseorang akan menebak-nebak sendiri prioritas kedua operator, sementara kurung kurawal memperjelas apa yang terjadi. Menyimpan tiga karakter tidak sepadan. Dan untuk praktik chaining await! s, meskipun mungkin merupakan idiom populer di beberapa bahasa, saya merasa ini memiliki terlalu banyak kelemahan seperti keterbacaan yang buruk dan interaksi dengan debugger yang layak untuk dioptimalkan.

Menyimpan tiga karakter tidak sepadan.

Dalam pengalaman anekdot saya, karakter tambahan (misalnya nama yang lebih panjang) tidak terlalu menjadi masalah, tetapi token tambahan bisa sangat mengganggu. Dalam hal analogi CPU, nama panjang adalah kode garis lurus dengan lokalitas yang baik - saya hanya bisa mengetiknya dari memori otot - sementara jumlah karakter yang sama ketika melibatkan banyak token (misalnya tanda baca) bercabang dan penuh dengan kesalahan cache.

(Saya sepenuhnya setuju bahwa await foo? akan sangat tidak jelas dan kita harus menghindarinya, dan bahwa mengetik lebih banyak token akan jauh lebih disukai; pengamatan saya hanya bahwa tidak semua karakter dibuat sama.)


@rpjohnst Saya pikir proposal alternatif Anda mungkin memiliki penerimaan yang sedikit lebih baik jika disajikan sebagai "async eksplisit" daripada "menunggu implisit" :-)

Sangat penting untuk mengetahui apakah bar adalah fungsi sinkron atau asinkron.

Saya tidak yakin ini benar-benar berbeda dari mengetahui apakah beberapa fungsi murah atau mahal, atau apakah itu IO atau tidak, atau apakah itu menyentuh beberapa keadaan global atau tidak. (Ini juga berlaku untuk hierarki @lnicola - jika panggilan async berjalan hingga selesai seperti panggilan sinkronisasi, maka mereka benar-benar tidak berbeda dalam hal kekuatan!)

Misalnya, fakta bahwa panggilan itu dalam satu lingkaran sama pentingnya, jika tidak lebih, daripada fakta bahwa panggilan itu tidak sinkron. Dan di Rust, di mana paralelisasi jauh lebih mudah dilakukan, Anda bisa saja menyarankan agar loop sinkron yang tampak mahal dialihkan ke iterator Rayon!

Jadi menurut saya, membutuhkan await sebenarnya tidak terlalu penting untuk mendapatkan optimasi ini. Loop sudah selalu menjadi tempat yang baik untuk mencari pengoptimalan, dan async fn s sudah menjadi indikator bagus bahwa Anda bisa mendapatkan konkurensi IO yang murah. Jika Anda kehilangan kesempatan itu, Anda bahkan dapat menulis Clippy lint untuk "panggilan asinkron dalam satu lingkaran" yang kadang-kadang Anda jalankan. Akan sangat bagus untuk memiliki lint yang serupa untuk kode sinkron juga!

Motivasi untuk "async eksplisit" bukan hanya "sintaks yang lebih sedikit," seperti yang disiratkan oleh @lnicola . Ini untuk membuat perilaku sintaks pemanggilan fungsi lebih konsisten, sehingga foo() selalu menjalankan tubuh foo sampai selesai. Di bawah proposal ini, mengabaikan anotasi hanya memberi Anda kode yang tidak bersamaan, yang sebenarnya sudah berlaku pada semua kode. Di bawah "penantian eksplisit", meninggalkan anotasi menyebabkan konkurensi yang tidak disengaja, atau setidaknya interleaving yang tidak disengaja,

Saya pikir proposal alternatif Anda mungkin memiliki penerimaan yang sedikit lebih baik jika disajikan sebagai "async eksplisit" daripada "menunggu implisit" :-)

Utas diberi nama "konstruksi masa depan eksplisit, penantian implisit," tetapi tampaknya nama terakhir telah macet. :P

Saya tidak yakin ini benar-benar berbeda dari mengetahui apakah beberapa fungsi murah atau mahal, atau apakah itu IO atau tidak, atau apakah itu menyentuh beberapa keadaan global atau tidak. (Ini juga berlaku untuk hierarki @lnicola - jika panggilan async berjalan hingga selesai seperti panggilan sinkronisasi, maka mereka benar-benar tidak berbeda dalam hal kekuatan!)

Saya pikir ini sama pentingnya dengan mengetahui bahwa fungsi mengubah beberapa keadaan, dan kami sudah memiliki kata kunci mut di kedua sisi panggilan dan sisi pemanggil.

Motivasi untuk "async eksplisit" bukan hanya "sintaks yang lebih sedikit," seperti yang disiratkan oleh @lnicola . Ini untuk membuat perilaku sintaks pemanggilan fungsi lebih konsisten, sehingga foo() selalu menjalankan body foo hingga selesai.

Satu satu sisi itu pertimbangan yang baik. Di sisi lain, Anda dapat dengan mudah memisahkan pembuatan di masa mendatang dan proses di masa mendatang. Maksud saya jika foo mengembalikan Anda beberapa abstraksi yang memungkinkan Anda untuk memanggil run dan mendapatkan beberapa hasil, itu tidak membuat foo sampah tidak berguna yang tidak melakukan apa-apa, itu sangat hal yang berguna: itu membangun beberapa objek yang dapat Anda panggil metode nanti. Itu tidak membuatnya berbeda. Metode foo yang kami panggil hanyalah kotak hitam dan kami melihat tanda tangannya Future<Output=T> dan itu benar-benar mengembalikan masa depan. Jadi kita secara eksplisit await ketika kita ingin melakukannya.

Utas diberi nama "konstruksi masa depan eksplisit, penantian implisit," tetapi tampaknya nama terakhir telah macet. :P

Saya pribadi berpendapat bahwa alternatif yang lebih baik adalah "menunggu eksplisit async eksplisit" :)


PS

Saya juga terkena pemikiran malam ini: apakah Anda mencoba berkomunikasi dengan C# LDM? Misalnya, orang-orang seperti @HaloFour , @gafter atau @CyrusNajmabadi . Mungkin ide yang sangat bagus untuk bertanya kepada mereka mengapa mereka mengambil sintaks yang mereka ambil. Saya akan mengusulkan untuk bertanya kepada orang-orang dari bahasa lain juga, tetapi saya hanya tidak mengenal mereka :) Saya yakin mereka memiliki banyak perdebatan tentang sintaks yang ada dan mereka sudah dapat banyak mendiskusikannya dan mereka mungkin memiliki beberapa ide yang berguna.

Itu tidak berarti Rust harus memiliki sintaks ini karena C# melakukannya, tetapi itu hanya memungkinkan untuk membuat keputusan yang lebih berbobot.

Saya pribadi berpendapat bahwa alternatif yang lebih baik adalah "menunggu eksplisit async eksplisit" :)

Proposal utama bukanlah "async eksplisit", itulah sebabnya saya memilih nama itu. Ini " asinkron implisit ," karena Anda tidak dapat mengetahui secara sekilas di mana asinkron diperkenalkan. Setiap panggilan fungsi yang tidak diberi notasi mungkin sedang membangun masa depan tanpa menunggunya, meskipun Future tidak muncul di tanda tangannya.

Untuk apa nilainya, utas internal memang menyertakan alternatif "eksplisit async eksplisit menunggu", karena itu kompatibel di masa depan dengan salah satu alternatif utama. (Lihat bagian terakhir dari posting pertama.)

apakah Anda mencoba untuk berkomunikasi dengan C# LDM?

Penulis RFC utama melakukannya. Poin utama yang keluar darinya, sejauh yang saya ingat, adalah keputusan untuk tidak menyertakan Future dalam tanda tangan async fn s. Di C#, Anda dapat mengganti Task dengan tipe lain untuk memiliki kendali atas bagaimana fungsi dijalankan. Tetapi di Rust, kami tidak (dan tidak akan) memiliki mekanisme seperti itu- semua masa depan akan melalui satu sifat, jadi tidak perlu menuliskan sifat itu setiap saat.

Kami juga berkomunikasi dengan desainer bahasa Dart, dan itu adalah sebagian besar motivasi saya untuk menulis proposal "eksplisit async". Dart 1 mengalami masalah karena fungsi tidak berjalan ke penantian pertama mereka saat dipanggil (tidak persis sama dengan cara kerja Rust, tetapi serupa), dan itu menyebabkan kebingungan besar sehingga di Dart 2 mereka berubah sehingga fungsi berjalan ke yang pertama menunggu saat dipanggil. Rust tidak dapat melakukannya karena alasan lain, tetapi dapat menjalankan seluruh fungsi saat dipanggil, yang juga akan menghindari kebingungan itu.

Kami juga berkomunikasi dengan desainer bahasa Dart, dan itu adalah sebagian besar motivasi saya untuk menulis proposal "eksplisit async". Dart 1 mengalami masalah karena fungsi tidak berjalan ke penantian pertama mereka saat dipanggil (tidak persis sama dengan cara kerja Rust, tetapi serupa), dan itu menyebabkan kebingungan besar sehingga di Dart 2 mereka berubah sehingga fungsi berjalan ke yang pertama menunggu saat dipanggil. Rust tidak dapat melakukannya karena alasan lain, tetapi dapat menjalankan seluruh fungsi saat dipanggil, yang juga akan menghindari kebingungan itu.

Pengalaman hebat, saya tidak menyadarinya. Senang mendengar Anda telah melakukan pekerjaan yang begitu besar. Bagus 👍

Saya juga terkena pemikiran malam ini: apakah Anda mencoba berkomunikasi dengan C# LDM? Misalnya, orang-orang seperti @HaloFour , @gafter atau @CyrusNajmabadi . Mungkin ide yang sangat bagus untuk bertanya kepada mereka mengapa mereka mengambil sintaks yang mereka ambil.

Saya senang memberikan info apa pun yang Anda minati. Namun, dan saya hanya membaca sekilas. Apakah mungkin untuk menyingkat pertanyaan spesifik yang Anda miliki saat ini?

Mengenai sintaks await (ini mungkin benar-benar bodoh, jangan ragu untuk meneriaki saya; Saya seorang pemula pemrograman async dan saya tidak tahu apa yang saya bicarakan):

Alih-alih menggunakan kata "menunggu", tidak bisakah kita memperkenalkan simbol/operator, mirip dengan ? . Misalnya, itu bisa berupa # atau @ atau sesuatu lainnya yang saat ini tidak digunakan.

Misalnya, jika itu adalah operator postfix:

let stuff = func()#?;
let chain = blah1()?.blah2()#.blah3()#?;

Ini sangat ringkas dan membaca secara alami dari kiri ke kanan: menunggu terlebih dahulu ( # ), kemudian menangani kesalahan ( ? ). Itu tidak memiliki masalah yang dimiliki kata kunci postfix menunggu, di mana .await terlihat seperti anggota struct. # jelas merupakan operator.

Saya tidak yakin apakah postfix adalah tempat yang tepat untuk itu, tetapi rasanya seperti itu karena didahulukan. Sebagai awalan:

let stuff = #func()?;

Atau bahkan:

let stuff = func#()?; // :-D :-D

Apakah ini pernah dibahas?

(Saya menyadari ini agak mulai mendekati sintaks "keyboard mash simbol acak" yang Perl terkenal ... :-D )

@rayvector https://github.com/rust-lang/rust/issues/50547#issuecomment -388108875 , alternatif ke-5.

@CyrusNajmabadi terima kasih sudah datang. Pertanyaan utamanya adalah opsi apa dari yang terdaftar yang menurut Anda lebih cocok dengan bahasa Rust saat ini, atau mungkin ada alternatif lain? Topik ini tidak terlalu panjang sehingga Anda dapat dengan mudah menggulirnya dari atas ke bawah dengan cepat. Pertanyaan utama: haruskah Rust mengikuti cara C#/TS/... await saat ini atau mungkin ia harus mengimplementasikannya sendiri. Apakah sintaks saat ini semacam "warisan" yang ingin Anda ubah dalam beberapa cara atau paling cocok dengan C# dan itu juga merupakan opsi terbaik untuk bahasa baru?

Pertimbangan utama terhadap sintaks C# adalah prioritas operator await foo? harus menunggu terlebih dahulu dan kemudian mengevaluasi operator ? serta perbedaan bahwa tidak seperti eksekusi C# tidak berjalan di utas pemanggil sampai await , tetapi tidak memulai sama sekali, dengan cara yang sama cuplikan kode saat ini tidak menjalankan pemeriksaan negatif hingga GetEnumerator dipanggil pertama kali:

IEnumerable<int> GetInts(int n)
{
   if (n < 0)
      throw new InvalidArgumentException(nameof(n));
   for (int i = 0; i <= n; i++)
      yield return i;
}

Lebih rinci dalam komentar pertama saya dan diskusi kemudian.

@Pzixel Oh, saya kira saya melewatkan yang itu ketika saya membaca sepintas utas ini sebelumnya ...

Bagaimanapun, saya belum melihat banyak diskusi tentang ini, selain penyebutan singkat itu.

Apakah ada argumen yang bagus untuk/menentang?

@rayvector Saya berdebat sedikit di sini demi sintaks yang lebih verbose. Salah satu alasannya adalah yang Anda sebutkan:

sintaks "keyboard mash simbol acak" yang membuat Perl terkenal

Untuk memperjelas, saya tidak berpikir await!(f)? benar-benar sedang berjalan untuk sintaks akhir, itu dipilih secara khusus karena ini adalah cara yang solid untuk tidak berkomitmen pada pilihan tertentu. Berikut adalah sintaks (termasuk operator ? ) yang menurut saya masih "dalam proses":

  • await f?
  • await? f
  • await { f }?
  • await(f)?
  • (await f)?
  • f.await?

Atau mungkin beberapa kombinasi dari ini. Intinya adalah bahwa beberapa dari mereka memang mengandung kurung kurawal agar lebih jelas tentang prioritas & ada banyak opsi di sini - tetapi tujuannya adalah await akan menjadi operator kata kunci, bukan makro, di versi final ( kecuali beberapa perubahan besar seperti yang diusulkan rpjohnst).

Saya memilih operator menunggu postfix sederhana (misalnya ~ ) atau kata kunci tanpa parens dan prioritas tertinggi.

Saya telah membaca utas ini, dan saya ingin mengusulkan yang berikut:

  • await f? mengevaluasi operator ? terlebih dahulu, dan kemudian menunggu hasil di masa mendatang.
  • (await f)? menunggu masa depan terlebih dahulu, dan kemudian mengevaluasi operator ? terhadap hasilnya (karena prioritas operator Rust biasa)
  • await? f tersedia sebagai gula sintaksis untuk `(menunggu f)?. Saya percaya "masa depan mengembalikan hasil" akan menjadi kasus yang sangat umum, jadi sintaks khusus sangat masuk akal.

Saya setuju dengan komentator lain bahwa await harus eksplisit. Cukup mudah melakukan ini dalam JavaScript, dan saya sangat menghargai keeksplisitan dan keterbacaan kode Rust, dan saya merasa membuat async implisit akan merusak ini untuk kode async.

Terpikir oleh saya bahwa "blok async implisit" harus dapat diimplementasikan sebagai proc_macro, yang hanya memasukkan kata kunci await sebelum masa depan.

Pertanyaan utamanya adalah opsi apa dari yang terdaftar yang menurut Anda lebih cocok dengan bahasa Rust saat ini,

Bertanya kepada desainer C # apa yang paling cocok dengan bahasa rust itu ... menarik :)

Saya tidak merasa memenuhi syarat untuk membuat keputusan seperti itu. Saya suka karat dan mencoba-cobanya. Tapi itu bukan bahasa yang saya gunakan hari demi hari. Saya juga tidak mendarah daging dalam jiwa saya. Karena itu, saya rasa saya tidak memenuhi syarat untuk membuat klaim tentang pilihan apa yang tepat untuk bahasa ini di sini. Ingin bertanya tentang Go/TypeScript/C#/VB/C++. Tentu, saya akan merasa jauh lebih nyaman. Tetapi karat terlalu banyak di luar bidang keahlian saya untuk merasa nyaman dengan pemikiran seperti itu.

Pertimbangan utama terhadap sintaks C# adalah prioritas operator await foo?

Ini adalah sesuatu yang saya rasa bisa saya komentari. Kami banyak berpikir tentang prioritas dengan 'menunggu' dan kami mencoba banyak bentuk sebelum menetapkan pada bentuk yang kami inginkan. Salah satu hal inti yang kami temukan adalah bahwa bagi kami, dan pelanggan (internal dan eksternal) yang ingin menggunakan fitur ini, jarang ada orang yang benar-benar ingin 'merantai' apa pun melewati panggilan asinkron mereka. Dengan kata lain, orang tampaknya sangat tertarik pada 'menunggu' menjadi bagian terpenting dari ekspresi penuh apa pun, dan dengan demikian membuatnya berada di dekat bagian atas. Catatan: dengan 'ekspresi penuh' yang saya maksud adalah hal-hal seperti ekspresi yang Anda dapatkan di bagian atas pernyataan ekspresi, atau ekspresi di sebelah kanan penetapan tingkat atas, atau ekspresi yang Anda berikan sebagai 'argumen' untuk sesuatu.

Kecenderungan orang untuk ingin 'melanjutkan' dengan 'menunggu' di dalam expr jarang terjadi. Kami kadang-kadang melihat hal-hal seperti (await expr).M() , tetapi itu tampaknya kurang umum dan kurang diinginkan daripada jumlah orang yang melakukan await expr.M() .

Ini juga mengapa kami tidak menggunakan formulir 'implisit' untuk 'menunggu'. Dalam praktiknya, itu adalah sesuatu yang ingin dipikirkan orang dengan sangat jelas, dan yang mereka inginkan di depan dan di tengah kode mereka sehingga mereka dapat memperhatikannya. Yang cukup menarik, bahkan bertahun-tahun kemudian, kecenderungan ini tetap ada. yaitu kadang-kadang kita menyesal bertahun-tahun kemudian bahwa ada sesuatu yang terlalu bertele-tele. Beberapa fitur bagus dengan cara itu sejak awal, tetapi begitu orang merasa nyaman dengannya, lebih cocok dengan sesuatu yang lebih seru. Itu tidak terjadi dengan 'menunggu'. Orang-orang tampaknya masih sangat menyukai sifat kata kunci yang berat dan prioritas yang kami pilih.

Sejauh ini, kami sangat senang dengan pilihan prioritas untuk audiens kami . Kami mungkin, di masa depan, membuat beberapa perubahan di sini. Tapi secara keseluruhan tidak ada tekanan kuat untuk melakukannya.

--

serta perbedaan bahwa tidak seperti eksekusi C# tidak berjalan di utas pemanggil sampai menunggu pertama kali, tetapi tidak memulai sama sekali, dengan cara yang sama cuplikan kode saat ini tidak menjalankan pemeriksaan negatif hingga GetEnumerator dipanggil pertama kali:

IMO, cara kami melakukan enumerator agak salah dan telah menyebabkan banyak kebingungan selama bertahun-tahun. Ini sangat buruk karena kecenderungan banyak kode harus ditulis seperti ini:

```c#
void SomeEnumerator(X args)
{
// Validasi Args, lakukan pekerjaan sinkron.
kembalikan SomeEnumeratorImpl(args);
}

void SomeEnumeratorImpl(X args)
{
// ...
menghasilkan
// ...
}

People have to write this *all the time* because of the unexpected behavior that the iterator pattern has.  I think we were worried about expensive work happening initially.  However, in practice, that doesn't seem to happen, and people def think about the work as happening when the call happens, and the yields themselves happening when you actually finally start streaming the elements.

Linq (which is the poster child for this feature) needs to do this *everywhere*, this highly diminishing this choice.

For ```await``` i think things are *much* better.  We use 'async/await' a ton ourselves, and i don't think i've ever once said "man... i wish that it wasn't running the code synchronously up to the first 'await'".  It simply makes sense given what the feature is.  The feature is literally "run the code up to await points, then 'yield', then resume once the work you're yielding on completes".  it would be super weird to not have these semantics to me since it is precisely the 'awaits' that are dictating flow, so why would anything be different prior to hitting the first await.

Also... how do things then work if you have something like this:

```c#
async Task FooAsync()
{
    if (cond)
    {
        // only await in method
        await ...
    }
} 

Anda benar-benar dapat memanggil metode ini dan tidak pernah menunggu. jika "eksekusi tidak berjalan di utas pemanggil sampai menunggu pertama" apa yang sebenarnya terjadi di sini?

menunggu? f tersedia sebagai gula sintaksis untuk `(menunggu f)?. Saya percaya "masa depan mengembalikan hasil" akan menjadi kasus yang sangat umum, jadi sintaks khusus sangat masuk akal.

Ini paling beresonansi dengan saya. Ini memungkinkan 'menunggu' menjadi konsep paling atas, tetapi juga memungkinkan penanganan tipe Hasil yang sederhana.

Satu hal yang kita ketahui dari C# adalah bahwa intuisi orang tentang prioritas terikat dengan spasi putih. Jadi jika Anda memiliki "menunggu x?" kemudian langsung terasa seperti await memiliki prioritas yang lebih rendah daripada ? karena ? berbatasan dengan ekspresi. Jika di atas benar-benar diuraikan sebagai (await x)? itu akan mengejutkan audiens kami.

Mengurainya sebagai await (x?) akan terasa paling alami hanya dari sintaks, dan akan sesuai dengan kebutuhan untuk mendapatkan 'Hasil' dari masa depan/tugas kembali, dan ingin 'menunggu' jika Anda benar-benar menerima nilai . Jika itu kemudian mengembalikan Hasil kembali dengan sendirinya, rasanya tepat untuk menggabungkannya dengan 'menunggu' untuk menandakan bahwa itu terjadi setelahnya. jadi await? x? each ? mengikat erat ke bagian kode yang paling terkait secara alami. Yang pertama ? berhubungan dengan await (dan khususnya hasilnya), dan yang kedua berhubungan dengan x .

jika "eksekusi tidak berjalan di utas pemanggil sampai menunggu pertama" apa yang sebenarnya terjadi di sini?

Tidak ada yang terjadi sampai pemanggil menanti kembalinya nilai FooAsync , di mana titik FooAsync 's tubuh berjalan sampai baik sebuah await atau mengembalikan.

Ini bekerja dengan cara ini karena Rust Future s didorong oleh polling, alokasi stack, dan tidak dapat dipindahkan setelah panggilan pertama ke poll . Penelepon harus memiliki kesempatan untuk memindahkannya ke tempatnya--di heap untuk Future s tingkat atas, atau berdasarkan nilai di dalam induk Future , sering kali di "stack frame" dari panggilan async fn --sebelum kode apa pun dieksekusi.

Ini berarti kita terjebak dengan a) semantik mirip generator C#, di mana tidak ada kode yang berjalan saat pemanggilan, atau b) semantik mirip coroutine Kotlin, di mana pemanggilan fungsi juga segera dan secara implisit menunggunya (dengan penutupan seperti async { .. } blok ketika Anda memang membutuhkan eksekusi bersamaan).

Saya lebih suka yang terakhir, karena menghindari masalah yang Anda sebutkan dengan generator C#, dan juga menghindari pertanyaan prioritas operator sepenuhnya.

@CyrusNajmabadi Di Rust, Future biasanya tidak berfungsi sampai muncul sebagai Task (jauh lebih mirip dengan F# Async ):

let bar = foo();

Dalam hal ini foo() mengembalikan Future , tetapi mungkin tidak benar - benar Async ):

tokio::run(bar);

Ketika muncul, ia akan menjalankan Future . Karena ini adalah perilaku default dari Future , akan lebih konsisten untuk async/await di Rust untuk tidak menjalankan kode apa pun hingga muncul.

Jelas situasinya berbeda di C#, karena di C# ketika Anda memanggil foo() itu segera mulai menjalankan Task , jadi masuk akal dalam C# untuk menjalankan kode hingga await .

Juga... bagaimana cara kerjanya jika Anda memiliki sesuatu seperti ini [...] Anda benar-benar dapat memanggil metode ini dan tidak pernah menunggu. jika "eksekusi tidak berjalan di utas pemanggil sampai menunggu pertama" apa yang sebenarnya terjadi di sini?

Jika Anda memanggil FooAsync() maka tidak melakukan apa-apa, tidak ada kode yang dijalankan. Kemudian ketika Anda menelurkannya, itu akan menjalankan kode secara serempak, await tidak akan pernah berjalan, dan dengan demikian ia segera mengembalikan () (yang merupakan versi Rust dari void )

Dengan kata lain, ini bukan "eksekusi tidak berjalan di utas pemanggil sampai pertama kali menunggu", ini "eksekusi tidak berjalan sampai secara eksplisit muncul (seperti dengan tokio::run )"

Tidak ada yang terjadi sampai pemanggil menunggu nilai kembalian FooAsync, di mana tubuh FooAsync berjalan sampai menunggu atau kembali.

Ick. Itu tampaknya disayangkan. Ada banyak waktu saya mungkin tidak pernah menunggu sesuatu (seringkali karena pembatalan dan komposisi dengan tugas). Sebagai seorang dev saya masih menghargai mendapatkan kesalahan awal itu (yang merupakan salah satu alasan paling umum orang ingin eksekusi dijalankan hingga menunggu).

Ini berarti kita terjebak dengan a) semantik seperti generator C#, di mana tidak ada kode yang berjalan saat pemanggilan, atau b) semantik seperti coroutine Kotlin, di mana pemanggilan fungsi juga segera dan secara implisit menunggunya (dengan penutupan seperti async { . .} blok ketika Anda memang membutuhkan eksekusi bersamaan).

Mengingat ini, saya jauh lebih suka yang pertama daripada yang terakhir. Hanya preferensi pribadi saya. Jika pendekatan kotlin terasa lebih alami untuk domain Anda, lakukanlah!

@CyrusNajmabadi Ick. Itu tampaknya disayangkan. Ada banyak waktu saya mungkin tidak pernah menunggu sesuatu (seringkali karena pembatalan dan komposisi dengan tugas). Sebagai seorang dev saya masih menghargai mendapatkan kesalahan awal itu (yang merupakan salah satu alasan paling umum orang ingin eksekusi dijalankan hingga menunggu).

Saya merasakan kebalikannya. Dalam pengalaman saya dengan JavaScript, sangat umum untuk lupa menggunakan await . Dalam hal ini Promise masih akan berjalan, tetapi kesalahan akan tertelan (atau hal aneh lainnya terjadi).

Dengan gaya Rust/Haskell/F#, Future berjalan (dengan penanganan kesalahan yang benar), atau tidak berjalan sama sekali. Kemudian Anda melihat bahwa itu tidak berjalan, jadi Anda menyelidiki dan memperbaikinya. Saya percaya ini menghasilkan kode yang lebih kuat.

@Pauan @rpjohnst Terima kasih atas penjelasannya. Itu adalah pendekatan yang kami pertimbangkan juga. Tapi ternyata tidak benar-benar diinginkan dalam praktiknya.

Dalam kasus di mana Anda tidak ingin "benar-benar melakukan apa pun. Anda harus menelurkannya secara manual", kami merasa lebih bersih untuk memodelkannya sebagai mengembalikan sesuatu yang menghasilkan tugas sesuai permintaan. yaitu sesuatu yang sederhana seperti Func<Task> .

Saya merasakan kebalikannya. Dalam pengalaman saya dengan JavaScript, sangat umum untuk lupa menggunakan menunggu.

C# berfungsi untuk mencoba memastikan bahwa Anda menunggu, atau menggunakan tugas dengan bijaksana.

tapi kesalahan akan ditelan

Itu kebalikan dari apa yang saya katakan. Saya mengatakan saya ingin kode dieksekusi dengan penuh semangat sehingga kesalahan adalah hal yang segera saya tekan, bahkan jika saya tidak pernah berhasil mengeksekusi kode dalam tugas. Ini sama dengan iterator. Saya lebih suka tahu saya membuatnya salah pada titik waktu ketika saya memanggil fungsi versus berpotensi lebih jauh ke depan jika/ketika iterator dialirkan.

Kemudian Anda melihat bahwa itu tidak berjalan, jadi Anda menyelidiki dan memperbaikinya.

Dalam skenario yang saya bicarakan, "tidak berjalan" sepenuhnya masuk akal. Lagi pula, aplikasi saya dapat memutuskan kapan saja bahwa itu tidak perlu benar-benar menjalankan tugas. Itu bukan bug yang saya jelaskan. Bug yang saya jelaskan adalah bahwa saya tidak lulus validasi, dan saya ingin mencari tahu tentang itu sedekat mungkin dengan titik di mana saya secara logis membuat pekerjaan yang bertentangan dengan titik ketika pekerjaan benar-benar perlu dijalankan. Mengingat bahwa ini adalah model untuk menggambarkan pemrosesan asinkron, seringkali dianggap sebagai kasus bahwa ini berjauhan satu sama lain. Jadi memiliki informasi tentang masalah terjadi sedini mungkin sangat berharga.

Seperti yang disebutkan, ini juga bukan hipotetis. Hal serupa terjadi dengan stream/iterator. Orang sering membuatnya, tetapi kemudian tidak menyadarinya sampai nanti. Merupakan beban tambahan bagi orang untuk melacak hal-hal ini kembali ke sumbernya. Inilah sebabnya mengapa begitu banyak API (termasuk hte BCL) sekarang harus melakukan pemisahan antara pekerjaan sinkron/awal, dan pekerjaan tertunda/malas yang sebenarnya.

Itu kebalikan dari apa yang saya katakan. Saya mengatakan saya ingin kode dieksekusi dengan penuh semangat sehingga kesalahan adalah hal yang segera saya tekan, bahkan jika saya tidak pernah berhasil mengeksekusi kode dalam tugas.

Saya dapat memahami keinginan untuk kesalahan awal, tetapi saya bingung: dalam situasi apa Anda akan "akhirnya tidak berhasil menelurkan Future "?

Cara Future s bekerja di Rust adalah Anda menyusun Future s bersama-sama dalam berbagai cara (termasuk async/menunggu, termasuk kombinator paralel, dll.), dan dengan melakukan ini ia membangun single fused Future yang berisi semua sub- Future s. Dan kemudian di tingkat atas program Anda ( main ) Anda kemudian menggunakan tokio::run (atau serupa) untuk menelurkannya.

Selain itu tunggal tokio::run panggilan di main , Anda biasanya tidak akan pemijahan Future s manual, bukan Anda hanya menulis mereka. Dan komposisi secara alami menangani pemijahan/penanganan kesalahan/pembatalan/dll. benar.

saya juga ingin membuat sesuatu yang jelas. Ketika saya mengatakan sesuatu seperti:

Tapi ternyata tidak benar-benar diinginkan dalam praktiknya.

Saya berbicara sangat spesifik tentang hal-hal dengan bahasa/platform kami. Saya hanya dapat memberikan wawasan tentang keputusan yang masuk akal untuk C#/.Net/CoreFx dll. Mungkin sepenuhnya situasi Anda berbeda dan apa yang ingin Anda optimalkan dan jenis pendekatan yang harus Anda ambil sepenuhnya arah yang berbeda.

Saya dapat memahami keinginan untuk kesalahan awal, tetapi saya bingung: dalam situasi apa Anda akan "akhirnya tidak berhasil menelurkan Masa Depan"?

Sepanjang waktu :)

Pertimbangkan bagaimana Roslyn (kompiler C#/VB/basis kode IDE) itu sendiri ditulis. Ini sangat asinkron dan interaktif . yaitu kasus penggunaan utama untuk itu akan digunakan dalam mode bersama dengan banyak klien mengaksesnya. Layanan paling sering berinteraksi dengan pengguna melalui banyak fitur, banyak di antaranya memutuskan bahwa mereka tidak perlu lagi melakukan pekerjaan yang awalnya mereka anggap penting, karena pengguna melakukan sejumlah tindakan. Misalnya, saat pengguna mengetik, kami melakukan banyak komposisi dan manipulasi tugas, dan kami mungkin akhirnya memutuskan untuk tidak menjalankannya karena acara lain datang beberapa ms kemudian.

Misalnya, saat pengguna mengetik, kami melakukan banyak komposisi dan manipulasi tugas, dan kami mungkin akhirnya memutuskan untuk tidak menjalankannya karena acara lain datang beberapa ms kemudian.

Bukankah itu hanya ditangani oleh pembatalan?

Dan komposisi secara alami menangani pemijahan/penanganan kesalahan/pembatalan/dll. benar.

Kedengarannya seperti kita memiliki dua model yang sangat berbeda untuk mewakili sesuatu. Tidak apa-apa :) Penjelasan saya dimaksudkan untuk diambil dalam konteks model yang kita pilih. Mereka mungkin tidak masuk akal untuk model yang Anda pilih.

Kedengarannya seperti kita memiliki dua model yang sangat berbeda untuk mewakili sesuatu. Tidak apa-apa :) Penjelasan saya dimaksudkan untuk diambil dalam konteks model yang kita pilih. Mereka mungkin tidak masuk akal untuk model yang Anda pilih.

Tentu saja, saya hanya mencoba memahami perspektif Anda, dan juga menjelaskan perspektif kami. Terima kasih telah meluangkan waktu untuk menjelaskan banyak hal.

Bukankah itu hanya ditangani oleh pembatalan?

Pembatalan adalah konsep ortogonal untuk asinkron (bagi kami). Mereka biasanya digunakan bersama. Tetapi tidak ada yang membutuhkan yang lain.

Anda dapat memiliki sistem sepenuhnya tanpa pembatalan, dan mungkin saja Anda tidak pernah menjalankan kode yang 'menunggu' tugas yang telah Anda buat. yaitu untuk alasan logis kode Anda mungkin hanya pergi "saya tidak perlu menunggu 't', saya hanya akan melakukan sesuatu yang lain". Tidak ada tentang tugas (di dunia kita) yang mendikte atau mengharuskan bahwa tugas itu harus ditunggu. Dalam sistem seperti itu, saya ingin mendapatkan validasi awal.

Catatan: ini mirip dengan masalah iterator. Anda dapat menghubungi seseorang untuk mendapatkan hasil yang ingin Anda gunakan nanti dalam kode Anda. Namun, untuk sejumlah alasan, Anda mungkin tidak benar-benar harus menggunakan hasilnya. Keinginan pribadi saya adalah tetap mendapatkan hasil validasi lebih awal, meskipun secara teknis saya tidak bisa mendapatkannya dan program saya berhasil.

Saya pikir ada argumen yang masuk akal untuk kedua arah. Tetapi pendapat saya adalah bahwa pendekatan sinkron memiliki lebih banyak pro daripada kontra. Tentu saja, jika pendekatan sinkron secara harfiah tidak cocok karena bagaimana impl Anda yang sebenarnya ingin bekerja maka itu tampaknya menjawab pertanyaan tentang apa yang perlu Anda lakukan: D

Dengan kata lain, saya tidak berpikir pendekatan Anda buruk di sini. Dan jika itu memiliki manfaat yang kuat di sekitar model ini yang menurut Anda tepat untuk Rust, maka lakukanlah :)

Anda dapat memiliki sistem sepenuhnya tanpa pembatalan, dan mungkin saja Anda tidak pernah menjalankan kode yang 'menunggu' tugas yang telah Anda buat. yaitu untuk alasan logis kode Anda mungkin hanya pergi "saya tidak perlu menunggu 't', saya hanya akan melakukan sesuatu yang lain".

Secara pribadi, saya pikir itu paling baik ditangani dengan logika if/then/else :

async fn foo() {
    if some_condition {
        await!(bar());
    }
}

Tetapi seperti yang Anda katakan, itu hanya perspektif yang sangat berbeda dari C#.

Secara pribadi, saya pikir itu paling baik ditangani dengan logika if/then/else yang biasa:

Ya. itu akan baik-baik saja jika pemeriksaan kondisi dapat dilakukan pada titik yang sama dengan tugas dibuat (dan banyak kasus seperti ini). Tetapi di dunia kita biasanya tidak terjadi hal-hal yang terhubung dengan baik seperti itu. Lagi pula, kami ingin melakukan pekerjaan async dengan penuh semangat dalam menanggapi pengguna (agar hasilnya siap saat dibutuhkan), tetapi kami mungkin nanti memutuskan bahwa kami tidak peduli lagi.

Di domain kami, 'menunggu' terjadi pada titik orang "membutuhkan nilai", yang merupakan penentuan/komponen/dll yang berbeda. dari keputusan tentang "haruskah saya mulai mengerjakan nilai?"

Dalam arti, ini sangat terpisah, dan itu dipandang sebagai suatu kebajikan. Produsen dan konsumen dapat memiliki kebijakan yang sama sekali berbeda, tetapi dapat berkomunikasi secara efektif tentang pekerjaan asinkron yang dilakukan melalui abstraksi yang bagus dari 'Tugas'.

Lagi pula, saya akan mundur dari pendapat sinkron/asinkron. Jelas ada model yang sangat berbeda yang berperan di sini. :)

Dalam hal prioritas saya telah memberikan beberapa informasi tentang bagaimana C# berpikir tentang berbagai hal. Saya harap ini membantu. Beri tahu saya jika Anda ingin informasi lebih lanjut di sana.

@CyrusNajmabadi Ya, wawasan Anda cukup membantu. Secara pribadi saya setuju dengan Anda bahwa await? foo adalah jalan yang harus ditempuh (walaupun saya juga menyukai proposal "eksplisit async ").

BTW, jika Anda menginginkan salah satu pendapat ahli terbaik tentang semua seluk-beluk model .net seputar pemodelan kerja async/sync, dan semua pro/kontra sistem itu, maka @stephentoub akan menjadi orang yang bisa diajak bicara. Dia akan menjadi sekitar 100x lebih baik dari saya dalam menjelaskan hal-hal, mengklarifikasi pro/kontra, dan kemungkinan mampu menyelam jauh ke dalam model di kedua sisi. Dia sangat akrab dengan pendekatan .net di sini (termasuk pilihan yang dibuat dan pilihan yang ditolak), dan bagaimana perkembangannya sejak awal. Dia juga sangat menyadari biaya kinerja dari pendekatan .net telah diambil (yang merupakan salah satu alasan ValueTask sekarang ada), yang saya bayangkan akan menjadi sesuatu yang kalian pikirkan pertama-dan-terutama dengan keinginan Anda untuk nol/rendah - abstraksi biaya.

Dari ingatan saya, pemikiran serupa tentang perpecahan ini dimasukkan ke dalam pendekatan .net di hari-hari awal, dan saya pikir dia dapat berbicara dengan sangat baik tentang keputusan akhir yang dibuat dan seberapa tepat keputusan itu.

Saya masih akan memilih mendukung await? future meskipun terlihat agak asing. Apakah ada kerugian nyata dalam menyusun itu?

Berikut analisis menyeluruh lainnya tentang pro dan kontra asinkron dingin (F#) vs panas (C#,JS): http://tomasp.net/blog/async-csharp-differences.aspx

Sekarang ada RFC baru untuk makro postfix yang memungkinkan eksperimen dengan postfix await tanpa perubahan sintaks khusus: https://github.com/rust-lang/rfcs/pull/2442

await {} adalah favorit saya di sini, mengingatkan pada unsafe {} plus itu menunjukkan prioritas.

let value = await { future }?;

@seunlanlege
ya, itu mengingatkan, jadi orang memiliki asumsi yang salah bahwa mereka dapat menulis kode seperti ini

let value = await {
   let val1 = future1;
   future2(val1)
}

Tapi mereka tidak bisa.

@Pzixel
jika saya memahami Anda dengan benar, Anda berasumsi orang akan berasumsi bahwa masa depan secara implisit ditunggu di dalam blok await {} ? Saya tidak setuju dengan itu. await {} hanya akan menunggu pada ekspresi yang dievaluasi oleh blok.

let value = await {
    let future = create_future();
    future
};

Dan itu harus menjadi pola yang tidak dianjurkan

disederhanakan

let value = await { create_future() };

Anda mengusulkan pernyataan di mana lebih dari satu ekspresi "harus dikecilkan". Apakah Anda tidak melihat ada yang salah dengan itu?

Apakah menguntungkan untuk membuat await menjadi pola (selain ref dll)?
Sesuatu seperti:

let await n = bar();

Saya lebih suka menyebutnya pola async daripada await , meskipun saya tidak melihat banyak keuntungan dari membuatnya menjadi sintaks pola. Sintaks pola umumnya bekerja secara ganda sehubungan dengan rekan ekspresinya.

Menurut halaman https://doc.rust-lang.org/nightly/std/task/index.html saat ini , mod tugas terdiri dari ekspor ulang dari libcore dan ekspor ulang untuk liballoc, yang membuat hasilnya sedikit ... kurang optimal. Semoga ini diatasi entah bagaimana sebelum menjadi stabil.

Saya melihat kodenya. Dan saya punya beberapa saran:

  • [x] Sifat UnsafePoll dan Poll enum memiliki nama yang sangat mirip, tetapi tidak terkait. Saya sarankan untuk mengganti nama UnsafePoll , misalnya menjadi UnsafeTask .
  • [x] Di peti berjangka, kode itu dipecah menjadi submodul yang berbeda. Sekarang, sebagian besar kode digabungkan menjadi task.rs yang membuatnya lebih sulit untuk dinavigasi. Saya sarankan membaginya lagi.
  • [x] TaskObj#from_poll_task() memiliki nama yang aneh. Saya sarankan untuk menamainya new() sebagai gantinya
  • [x] TaskObj#poll_task bisa saja poll() . Bidang yang disebut poll dapat disebut poll_fn yang juga menyarankan bahwa itu adalah penunjuk fungsi
  • Waker mungkin dapat menggunakan strategi yang sama seperti TaskObj dan meletakkan vtable di tumpukan. Hanya sebuah ide, saya tidak tahu apakah kita menginginkan ini. Apakah akan lebih cepat karena sedikit tipuan?
  • [ ] dyn sekarang stabil dalam versi beta. Kode mungkin harus menggunakan dyn tempat yang berlaku

Saya dapat memberikan PR untuk hal ini juga. @cramertj @aturon jangan ragu untuk menghubungi saya melalui Discord untuk mendiskusikan detailnya.

bagaimana kalau menambahkan metode await() untuk semua Future ?

    /// just like and_then method
    let x = f.and_then(....);
    let x = f.await();

    await f?     =>   f()?.await()
    await? f     =>   f().await()?

/// with chain invoke.
let x = first().await().second().await()?.third().await()?
let x = first().await()?.second().await()?.third().await()?
let x = first()?.await()?.second().await()?.third().await()?

@zengsai Masalahnya adalah await tidak berfungsi seperti metode biasa. Faktanya, pertimbangkan apa yang akan dilakukan metode await jika tidak berada dalam blok/fungsi async . Metode tidak tahu dalam konteks apa mereka dieksekusi, sehingga tidak dapat menyebabkan kesalahan kompilasi.

@xfix ini tidak benar secara umum. Kompiler dapat melakukan apa pun yang diinginkannya dan dapat menangani pemanggilan metode secara khusus dalam kasus ini. Panggilan gaya metode memecahkan masalah preferensi tetapi tidak terduga (menunggu tidak berfungsi seperti ini dalam bahasa lain) dan mungkin akan menjadi peretasan yang buruk di kompiler.

@elszben Bahwa kompiler dapat melakukan apa pun yang diinginkannya tidak berarti ia harus melakukan apa pun yang diinginkannya.

future.await() terdengar seperti panggilan fungsi biasa, padahal tidak. Jika Anda ingin pergi ke sini, sintaks future.await!() diusulkan di suatu tempat di atas akan memungkinkan semantik yang sama, dan dengan jelas menandai dengan makro "Sesuatu yang aneh sedang terjadi di sini, saya tahu."

Sunting: Postingan dihapus

Saya memindahkan posting ini ke RFC berjangka. Tautan

Adakah yang melihat interaksi antara async fn dan #[must_use] ?

Jika Anda memiliki async fn , memanggilnya secara langsung tidak menjalankan kode dan mengembalikan Future ; sepertinya semua async fn harus memiliki melekat #[must_use] pada "luar" impl Future jenis, sehingga Anda tidak dapat memanggil mereka tanpa melakukan sesuatu dengan Future .

Selain itu, jika Anda melampirkan sendiri #[must_use] ke async fn , sepertinya itu harus diterapkan pada pengembalian fungsi dalam . Jadi, jika Anda menulis #[must_use] async fn foo() -> T { ... } , maka Anda tidak dapat menulis await!(foo()) tanpa melakukan sesuatu dengan hasil menunggu.

Adakah yang melihat interaksi antara async fn dan #[must_use]?

Untuk orang lain yang tertarik dengan diskusi ini, lihat https://github.com/rust-lang/rust/issues/51560.

Saya sedang berpikir tentang bagaimana fungsi asinkron diimplementasikan dan menyadari bahwa fungsi ini tidak mendukung rekursi, atau rekursi timbal balik.

untuk sintaks menunggu, saya pribadi menuju makro pasca-perbaikan, tidak ada pendekatan menunggu implisit, untuk rantai yang mudah, dan itu juga dapat digunakan seperti pemanggilan metode

@ warlord500 Anda benar-benar mengabaikan seluruh pengalaman jutaan pengembang yang dijelaskan di atas. Anda tidak ingin merantai await .

@Pzixel tolong jangan menganggap saya belum membaca utas atau apa yang saya inginkan.
Saya tahu bahwa beberapa kontributor mungkin tidak ingin membiarkan rantai menunggu tetapi ada beberapa dari kita
pengembang yang melakukannya. Saya tidak yakin dari mana Anda mendapatkan gagasan bahwa saya mengabaikannya
pendapat pengembang, komentar saya hanya menentukan pendapat anggota komunitas dan alasan saya memegang pendapat itu.

EDIT : jika Anda memiliki perbedaan pendapat, silakan bagikan! Saya ingin tahu mengapa Anda mengatakan
kita seharusnya tidak mengizinkan chaining menunggu melalui metode seperti sintaks?

@warlord500 karena tim MS berbagi pengalamannya dengan ribuan pelanggan dan jutaan pengembang. Saya mengetahuinya sendiri karena saya menulis kode async/menunggu setiap hari, dan Anda tidak pernah ingin mengikatnya. Berikut kutipan yang tepat, jika Anda mau:

Kami banyak berpikir tentang prioritas dengan 'menunggu' dan kami mencoba banyak bentuk sebelum menetapkan pada bentuk yang kami inginkan. Salah satu hal inti yang kami temukan adalah bahwa bagi kami, dan pelanggan (internal dan eksternal) yang ingin menggunakan fitur ini, jarang ada orang yang benar-benar ingin 'merantai' apa pun melewati panggilan asinkron mereka. Dengan kata lain, orang tampaknya sangat tertarik pada 'menunggu' menjadi bagian terpenting dari ekspresi penuh apa pun, dan dengan demikian membuatnya berada di dekat bagian atas. Catatan: dengan 'ekspresi penuh' yang saya maksud adalah hal-hal seperti ekspresi yang Anda dapatkan di bagian atas pernyataan ekspresi, atau ekspresi di sebelah kanan penetapan tingkat atas, atau ekspresi yang Anda berikan sebagai 'argumen' untuk sesuatu.

Kecenderungan orang untuk ingin 'melanjutkan' dengan 'menunggu' di dalam expr jarang terjadi. Kami kadang-kadang melihat hal-hal seperti (await expr).M() , tetapi itu tampaknya kurang umum dan kurang diinginkan daripada jumlah orang yang melakukan await expr.M() .

Saya sekarang cukup bingung, Jika saya memahami Anda dengan benar, kami seharusnya tidak mendukung
menunggu gaya rantai mudah pasca-perbaikan karena tidak umum digunakan? Anda lihat menunggu sebagai bagian terpenting dari sebuah ekspresi.
Saya hanya berasumsi dalam hal ini untuk memastikan saya memahami Anda dengan benar.
Jika saya salah jangan ragu untuk mengoreksi saya.

juga, Anda dapat memposting tautan ke tempat Anda mendapatkan kutipan,
Terima kasih.

penghitung saya untuk dua poin di atas hanya karena Anda tidak menggunakan sesuatu secara umum, tidak berarti mendukungnya akan berbahaya untuk kasus di mana itu membuat kode lebih bersih.

terkadang menunggu bagian terpenting dari sebuah ekspresi, jika ekspresi pembangkit masa depan adalah
bagian terpenting dan Anda ingin meletakkannya di atas, Anda masih dapat melakukannya jika kami mengizinkan gaya makro postfix selain gaya makro normal

juga, Anda dapat memposting tautan ke tempat Anda mendapatkan kutipan,
Terima kasih.

Tapi ... tapi Anda mengatakan bahwa Anda telah membaca seluruh utasnya ...

Tapi saya tidak punya masalah dengan membagikannya: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886 . Saya menyarankan Anda untuk membaca semua posting Cyrus, ini benar-benar pengalaman seluruh ekosistem C#/.Net, ini adalah pengalaman tak ternilai yang dapat digunakan kembali oleh Rust.

terkadang menunggu bagian terpenting dari sebuah ekspresi

Kutipan itu jelas mengatakan sebaliknya Dan Anda tahu, saya sendiri memiliki perasaan yang sama, menulis async/menunggu setiap hari.

Apakah Anda memiliki pengalaman dengan async/menunggu? Bisakah Anda membagikannya, tolong?

Wow, saya tidak percaya saya melewatkan itu. Terima kasih telah meluangkan waktu dari hari Anda untuk menautkan itu.
Saya tidak punya pengalaman jadi, saya kira dalam skema besar, pendapat saya tidak terlalu penting

@Pzixel Saya menghargai Anda berbagi informasi tentang pengalaman Anda dan orang lain menggunakan async / await , tapi tolong hormati kontributor lain. Anda tidak perlu mengkritik tingkat pengalaman orang lain untuk membuat poin teknis Anda didengar.

Catatan moderator: @Pzixel Serangan pribadi terhadap anggota komunitas tidak diperbolehkan. Saya telah mengeditnya dari komentar Anda. Jangan lakukan itu lagi. Jika Anda memiliki pertanyaan tentang kebijakan moderasi kami, silakan hubungi kami di [email protected].

@crabtw saya tidak mengkritik siapa pun. Saya mohon maaf atas ketidaknyamanan yang mungkin terjadi di sini.

Saya bertanya tentang pengalaman sekali ketika saya ingin memahami apakah seseorang memiliki kebutuhan aktual dalam rantai 'menunggu' atau itu ekstrapolasi fitur hari ini. Saya tidak ingin mengajukan banding ke otoritas, itu hanya sekelompok informasi yang berguna di mana saya dapat mengatakan "Anda perlu mencobanya sendiri dan menyadari kebenaran ini sendiri". Tidak ada yang menyinggung di sini.

Serangan pribadi terhadap anggota komunitas tidak diperbolehkan. Saya telah mengeditnya dari komentar Anda.

Tidak ada serangan pribadi. Seperti yang saya lihat, Anda mengomentari referensi saya tentang downvotes. Yah, itu hanya reaktor saya di posting saya, tidak ada yang istimewa. Karena telah dihapus, masuk akal juga untuk menghapus referensi itu (bahkan mungkin membingungkan bagi pembaca selanjutnya), jadi terima kasih telah menghapusnya.

Terima kasih untuk referensinya. Saya ingin menyebutkan bahwa Anda tidak boleh mengambil apa pun yang saya katakan sebagai 'injil' :) Rust dan C# adalah bahasa yang berbeda dengan komunitas, paradigma, dan idiom yang berbeda. Anda harus membuat pilihan terbaik untuk bahasa Anda. Semoga kata-kata saya bermanfaat dan dapat memberikan wawasan. Tapi selalu terbuka untuk cara yang berbeda untuk melakukan sesuatu.

Harapan saya adalah Anda datang dengan sesuatu yang luar biasa untuk Rust. Kemudian kami dapat melihat apa yang Anda lakukan dan mencuri dengan anggun mengadopsinya untuk C# :)

Sejauh yang saya tahu, argumen tertaut terutama berbicara tentang prioritas await , dan khususnya berpendapat bahwa masuk akal untuk menguraikan await x.y() sebagai await (x.y()) daripada (await x).y() karena pengguna akan lebih sering menginginkan dan mengharapkan interpretasi sebelumnya (dan spasi juga menunjukkan interpretasi tersebut). Dan saya cenderung setuju, meskipun saya juga mengamati bahwa sintaks seperti await!(x.y()) menghilangkan ambiguitas.

Namun, saya tidak berpikir itu menyarankan jawaban tertentu mengenai nilai rantai seperti x.y().await!().z() .

Komentar yang dikutip menarik sebagian karena ada perbedaan besar dalam Rust, yang telah menjadi salah satu faktor besar dalam menunda kami mencari tahu sintaks menunggu terakhir: C# tidak memiliki operator ? , jadi mereka tidak memiliki kode yang perlu ditulis (await expr)? . Mereka menggambarkan (await expr).M() sebagai sangat tidak biasa, dan saya cenderung berpikir itu juga benar di Rust, tetapi satu-satunya pengecualian untuk itu, dari sudut pandang saya, adalah ? , yang akan sangat umum karena banyak masa depan akan mengevaluasi hasil ( semua yang ada saat ini, misalnya).

@tanpa perahu ya, benar. Saya ingin mengutip bagian ini sekali lagi:

satu-satunya pengecualian untuk itu, dari sudut pandang saya, adalah ?

Jika hanya ada pengecualian maka tampaknya masuk akal untuk membuat await? foo sebagai jalan pintas untuk (await foo)? dan memiliki yang terbaik dari kedua dunia.

Saat ini, setidaknya, sintaks yang diusulkan dari await!() akan memungkinkan penggunaan ? tidak ambigu. Kita dapat mengkhawatirkan beberapa sintaks yang lebih pendek untuk kombinasi await dan ? jika dan ketika kita memutuskan untuk mengubah sintaks dasar untuk await . (Dan tergantung pada apa yang kami ubah menjadi , kami mungkin tidak memiliki masalah sama sekali.)

@joshtriplett kawat gigi tambahan ini menghilangkan ambiguitas, tetapi mereka sangat berat. Misalnya, cari di seluruh proyek saya saat ini:

Matching lines: 139 Matching files: 10 Total files searched: 77

Saya memiliki 139 menunggu di 2743 sloc. Mungkin ini bukan masalah besar, tapi saya pikir kita harus mempertimbangkan alternatif tanpa kawat gigi sebagai yang lebih bersih dan lebih baik. Dikatakan, ? adalah satu-satunya pengecualian, jadi kita dapat dengan mudah menggunakan await foo tanpa kawat gigi, dan memperkenalkan sintaks khusus hanya untuk kasus khusus ini. Ini bukan masalah besar, tetapi bisa menghemat beberapa kawat gigi untuk proyek LISP.

Saya telah membuat posting blog tentang mengapa saya pikir fungsi async harus menggunakan pendekatan tipe pengembalian luar untuk tanda tangannya. Selamat membaca!

https://github.com/MajorBreakfast/rust-blog/blob/master/posts/2018-06-19-outer-return-type-approach.md

Saya belum mengikuti semua diskusi, jadi jangan ragu untuk mengarahkan saya ke tempat ini akan dibahas jika saya melewatkannya.

Berikut adalah perhatian tambahan tentang pendekatan tipe pengembalian dalam: bagaimana sintaks untuk Stream s terlihat seperti, ketika akan ditentukan? Saya akan berpikir async fn foo() -> impl Stream<Item = T> akan terlihat bagus dan konsisten dengan async fn foo() -> impl Future<Output = T> , tetapi itu tidak akan berfungsi dengan pendekatan tipe pengembalian dalam. Dan saya rasa kami tidak ingin memperkenalkan kata kunci async_stream .

@Ekleog Stream perlu menggunakan kata kunci yang berbeda. Itu tidak dapat menggunakan async karena impl Trait bekerja sebaliknya. Itu hanya dapat memastikan bahwa sifat-sifat tertentu diimplementasikan, tetapi sifat-sifat itu sendiri harus sudah diterapkan pada jenis beton yang mendasarinya.

Namun, pendekatan tipe pengembalian luar akan berguna jika suatu hari kita ingin menambahkan fungsi generator async:

async_gen fn foo() -> impl AsyncGenerator<Yield = i32, Return = ()> { yield 1; ... }

Stream dapat diimplementasikan untuk semua generator async dengan Return = () . Hal ini memungkinkan:

async_gen fn foo() -> impl Stream<Item = i32> { yield 1;  ... }

Catatan: Generator sudah ada di malam hari, tetapi mereka tidak menggunakan sintaks ini. Saat ini juga tidak pinning-aware tidak seperti Stream di futures 0.3.

Sunting: Kode ini sebelumnya menggunakan Generator . Saya melewatkan perbedaan antara Stream dan Generator . Aliran tidak sinkron. Ini berarti bahwa mereka mungkin tetapi tidak harus menghasilkan nilai. Mereka dapat merespons dengan Poll::Ready atau Poll::Pending . A Generator di sisi lain harus selalu menghasilkan atau menyelesaikan secara sinkron. Saya sekarang telah mengubahnya menjadi AsyncGenerator untuk mencerminkan hal ini.

Sunting2 : yield di dalam tubuh. Ini berarti Anda benar jika mengatakan bahwa async dapat digunakan kembali. Apakah pendekatan itu masuk akal adalah pertanyaan lain. Tapi saya rasa itu untuk topik lain ^^'

Memang, saya berpikir bahwa async dapat digunakan kembali, apakah itu hanya karena async akan, sesuai RFC ini, hanya diizinkan dengan Future s, dan dengan demikian dapat mendeteksi itu menghasilkan Stream dengan melihat tipe pengembalian (yang harus berupa Future atau Stream ).

Alasan mengapa saya mengangkat ini sekarang adalah karena jika kita ingin memiliki kata kunci async untuk menghasilkan Future s dan Stream s, maka saya pikir hasil luarnya jenis pendekatan akan jauh lebih bersih, karena akan eksplisit, dan saya tidak berpikir siapa pun akan berharap bahwa async fn foo() -> i32 akan menghasilkan aliran i32 (yang akan mungkin jika tubuh berisi yield dan pendekatan tipe pengembalian dalam dipilih).

Kita dapat memiliki kata kunci kedua untuk generator (misalnya gen fn ), dan kemudian membuat aliran hanya dengan menerapkan keduanya (misalnya async gen fn ). Tipe pengembalian luar tidak perlu membahas ini sama sekali.

@rpjohnst Saya mengangkatnya karena pendekatan tipe pengembalian luar memungkinkan untuk dengan mudah mengatur dua tipe terkait.

Kami tidak ingin menetapkan dua jenis terkait. Aliran masih hanya satu jenis, bukan impl Iterator<Item=impl Future>> atau semacamnya.

@rpjohnst maksud saya tipe terkait Yield dan Return dari generator (async)

gen fn foo() -> impl Generator<Yield = i32, Return = ()> { ... }

Ini adalah sketsa asli saya, tetapi saya pikir berbicara tentang generator terlalu jauh di depan kita, setidaknya untuk masalah pelacakan:

// generator
fn foo() -> T yields Y

// generator that implements Iterator
fn foo() yields Y

// async generator
async fn foo() -> T yields Y

// async generator that implements Stream
async fn foo() yields Y

Secara lebih umum, saya pikir kita harus memiliki lebih banyak pengalaman dengan implementasi sebelum kita meninjau kembali keputusan yang dibuat di RFC. Kami mengitari argumen yang sama yang telah kami buat, kami membutuhkan pengalaman dengan fitur seperti yang diusulkan oleh RFC untuk melihat apakah pembobotan ulang diperlukan.

Saya ingin sepenuhnya setuju dengan Anda, tetapi hanya ingin tahu: jika saya membaca komentar Anda dengan benar, stabilisasi sintaks async/menunggu akan menunggu sintaks dan implementasi yang layak untuk aliran async, dan untuk mendapatkan pengalaman dengan keduanya? (karena tidak mungkin untuk mengubah antara tipe pengembalian luar dan tipe pengembalian dalam setelah stabil)

Saya pikir async/menunggu diharapkan untuk Rust 2018 dan tidak berharap generator async siap saat itu, tapi…?

(Juga, komentar saya dimaksudkan hanya sebagai argumen tambahan untuk posting blog @MajorBreakfast , namun tampaknya telah benar-benar menghapus diskusi tentang topik ini… itu sama sekali bukan tujuan saya, dan saya kira perdebatan harus dipusatkan kembali pada postingan blog ini?)

Kasus penggunaan yang sempit dari kata kunci menunggu masih membingungkan saya. (Terutama Masa Depan vs Aliran vs Generator)

Bukankah kata kunci hasil cukup untuk semua kasus penggunaan? Seperti dalam

{ let a = yield future; println(a) } -> Future

Yang membuat tipe pengembalian tetap eksplisit dan oleh karena itu hanya satu kata kunci yang diperlukan untuk semua semantik berbasis "kelanjutan" tanpa menggabungkan kata kunci dan pustaka bersama-sama terlalu erat.

(Kami melakukan ini dalam bahasa tanah liat btw)

@aep await tidak menghasilkan masa depan dari generator-- ia menjeda eksekusi Future dan mengembalikan kontrol ke pemanggil.

@cramertj baik itu bisa melakukan hal itu (mengembalikan masa depan yang berisi kelanjutan setelah kata kunci hasil), yang merupakan kasus penggunaan yang jauh lebih luas.
tapi saya kira saya agak terlambat ke pesta untuk diskusi itu? :)

@aep Alasan untuk await - kata kunci khusus adalah untuk penyusunan dengan kata kunci yield khusus generator di masa mendatang. Kami ingin mendukung generator async dan itu berarti dua "cakupan" lanjutan yang independen.

Juga, itu tidak dapat mengembalikan masa depan yang berisi kelanjutan, karena masa depan Rust berbasis polling bukan berbasis panggilan balik, setidaknya sebagian untuk alasan manajemen memori. Jauh lebih mudah bagi poll untuk mengubah satu objek daripada yield untuk membuang referensi ke objek tersebut.

Saya pikir async/menunggu seharusnya tidak menjadi kata kunci penyebab mencemari bahasa itu sendiri, karena async hanya fitur bukan internal bahasa.

@sackery Ini adalah bagian dari bahasa internal, dan tidak dapat diimplementasikan murni sebagai perpustakaan.

jadi jadikan saja sebagai kata kunci seperti halnya nim, c#!

Pertanyaan: apa yang seharusnya menjadi tanda tangan dari async penutupan non-move yang menangkap nilai dengan referensi yang dapat diubah? Saat ini mereka hanya dilarang langsung. Sepertinya kami menginginkan semacam pendekatan GAT yang memungkinkan peminjaman penutupan berlangsung hingga masa depan mati, misalnya:

trait AsyncFnMut {
    type Output<'a>: Future;
    fn call(&'a mut self, args: ...) -> Self::Output<'a>;
}

@cramertj ada masalah umum di sini dengan mengembalikan referensi yang bisa berubah ke lingkungan penutupan yang ditangkap. Mungkin solusinya tidak perlu diikat ke async fn?

@withoutboats benar, itu hanya akan jauh lebih umum dalam situasi async daripada di tempat lain.

Bagaimana dengan fn async bukannya async fn ?
Saya suka let mut lebih baik daripada mut let .

fn foo1() {
}
fn async foo2() {
}
pub fn foo3() {
}
pub fn async foo4() {
}

Setelah Anda mencari pub fn , Anda masih dapat menemukan semua fungsi publik dalam kode sumber.tetapi saat ini sintaks tidak.

fn foo1() {
}
async fn foo2() {
}
pub fn foo3() {
}
pub async fn foo4() {
}

Usulan ini tidak terlalu penting, Ini masalah selera pribadi.
Jadi saya menghargai pendapat Anda semua :)

Saya percaya semua pengubah harus pergi sebelum fn. Jelas dan bagaimana hal itu dilakukan dalam bahasa lain. Ini hanya akal sehat.

@Pzixel Saya tahu bahwa pengubah akses harus sebelum fn karena ini penting.
tapi saya pikir async mungkin tidak.

@xmeta Saya belum pernah melihat ide ini diajukan sebelumnya. Kita mungkin ingin meletakkan async di depan fn agar konsisten, tetapi saya pikir penting untuk mempertimbangkan semua opsi. Terima kasih telah memposting!

// Status quo:
pub unsafe async fn foo() {} // #![feature(async_await, futures_api)]
pub const unsafe fn foo2() {} // #![feature(const_fn)]

@MayorBreakfast Terima kasih atas balasan Anda, saya pikir seperti ini.

{ Public, Private } ⊇ Function  → put `pub` in front of `fn`
{ Public, Private } ⊇ Struct    → put `pub` in front of `struct`
{ Public, Private } ⊇ Trait     → put `pub` in front of `trait`
{ Public, Private } ⊇ Enum      → put `pub` in front of `enum`
Function ⊇ {Async, Sync}        → put `async` in back of `fn`
Variable ⊇ {Mutable, Imutable}  → put `mut` in back of `let`

@xmeta @MajorBreakfast

async fn tidak dapat dibagi, Ini mewakili fungsi asinkron。

async fn adalah keseluruhan.

Anda mencari pub fn itu berarti Anda sedang mencari fungsi sinkronisasi publik.
Dengan cara yang sama, Anda mencari pub async fn itu berarti Anda sedang mencari fungsi asinkron publik.

@ZhangHanDong

  • async fn mendefinisikan fungsi normal yang mengembalikan masa depan. Semua fungsi yang mengembalikan masa depan dianggap "asinkron". Pointer fungsi async fn s dan fungsi lain yang mengembalikan masa depan adalah sama°. Berikut contoh taman bermain . Pencarian untuk "async fn" hanya dapat menemukan fungsi yang menggunakan notasi, tidak akan menemukan semua fungsi asinkron.
  • Pencarian untuk pub fn tidak akan menemukan fungsi unsafe atau const .

° Jenis konkret yang dikembalikan oleh async fn tentu saja anonim. Maksud saya, keduanya mengembalikan tipe yang mengimplementasikan Future

@xmeta perhatikan bahwa mut tidak "mengejar let", atau lebih tepatnya, bahwa mut tidak mengubah let . let mengambil pola, yaitu

let PATTERN = EXPRESSION;

mut adalah bagian dari PATTERN , bukan dari let itu sendiri. Sebagai contoh:

// one is mutable one is not
let (mut a, b) = (1, 2);

@steveklabnik saya mengerti. Saya hanya ingin menunjukkan hubungan antara struktur hierarkis dan urutan kata. Terima kasih

Apa pendapat orang tentang perilaku yang diinginkan dari return dan break di dalam blok async ? Saat ini return kembali dari blok async-- jika kita mengizinkan return sama sekali, ini adalah satu-satunya pilihan yang mungkin. Kita bisa langsung melarang return dan menggunakan sesuatu seperti 'label: async { .... break 'label x; } untuk kembali dari blok async. Ini juga terkait dengan percakapan seputar apakah akan menggunakan kata kunci break atau return untuk fitur break-to-blocks (https://github.com/rust-lang/rust/issues/ 48594).

Saya mengizinkan return . Perhatian utama untuk melarangnya adalah itu bisa membingungkan karena tidak kembali dari fungsi saat ini, tetapi dari blok async. Saya, bagaimanapun, ragu bahwa itu akan membingungkan. Penutupan sudah memungkinkan return dan saya tidak pernah menganggapnya membingungkan. Mempelajari bahwa return berlaku untuk blok asinkron adalah IMO yang mudah dan mengizinkannya adalah IMO yang cukup berharga.

@cramertj return harus selalu keluar dari fungsi yang mengandung, tidak pernah blok dalam; jika tidak masuk akal untuk itu berfungsi, yang sepertinya tidak, maka return seharusnya tidak berfungsi sama sekali.

Menggunakan break untuk ini tampaknya tidak menguntungkan, tetapi karena sayangnya kami memiliki label-break-value, maka setidaknya konsisten dengan itu.

Apakah pemindahan dan penutupan asinkron masih direncanakan? Berikut ini dari RFC:

// closure which is evaluated immediately
async move {
     // asynchronous portion of the function
}

dan lebih jauh ke bawah halaman

async { /* body */ }

// is equivalent to

(async || { /* body */ })()

yang membuat return selaras dengan penutupan, dan tampaknya cukup mudah untuk diambil dan dijelaskan.

Apakah rencana rfc break-to-block memungkinkan melompat keluar dari penutupan bagian dalam dengan label? Jika tidak (dan saya tidak menyarankan itu harus mengizinkannya), akan sangat disayangkan untuk melarang perilaku konsisten returns , kemudian gunakan alternatif yang juga tidak konsisten dengan rfc.

@memoryruins async || { ... return x; ... } harus benar-benar berfungsi. Saya mengatakan bahwa async { ... return x; ... } tidak boleh, justru karena async bukanlah penutupan. return memiliki arti yang sangat spesifik: "kembali dari fungsi yang mengandung". Penutupan adalah fungsi. blok async tidak.

@memoryruins Keduanya sudah diimplementasikan.

@joshtriplett

blok async tidak.

Saya kira saya masih menganggapnya sebagai fungsi dalam arti bahwa mereka adalah badan dengan konteks eksekusi yang ditentukan secara terpisah dari blok yang berisi mereka, jadi masuk akal bagi saya bahwa return internal ke async blok. Kebingungan di sini tampaknya sebagian besar sintaksis, di blok itu biasanya hanya pembungkus untuk ekspresi daripada hal-hal yang membawa kode ke dalam konteks eksekusi baru seperti || dan async lakukan.

@cramertj "sintaksis" itu penting.

Pikirkan seperti ini. Jika Anda memiliki sesuatu yang tidak terlihat seperti fungsi (atau seperti penutupan, dan Anda terbiasa mengenali penutupan sebagai fungsi), dan Anda melihat return , menurut pengurai mental Anda, kemana perginya?

Apa pun yang membajak return membuatnya lebih membingungkan untuk membaca kode orang lain. Orang-orang setidaknya terbiasa dengan gagasan bahwa break kembali ke beberapa blok induk dan mereka harus membaca konteksnya untuk mengetahui blok mana . return selalu menjadi palu yang lebih besar yang kembali dari seluruh fungsi.

Jika mereka tidak diperlakukan sama dengan penutupan yang segera dievaluasi, saya setuju bahwa pengembalian akan menjadi tidak konsisten, terutama secara sintaksis. Jika ? 's di blok async telah diputuskan (RFC masih mengatakan itu belum diputuskan), maka saya membayangkan itu akan selaras dengan itu.

@joshtriplett rasanya sewenang-wenang bagi saya untuk mengatakan bahwa Anda dapat mengenali fungsi dan penutupan (yang secara sintaksis sangat berbeda) sebagai "cakupan pengembalian" tetapi blok async tidak dapat dikenali di sepanjang baris yang sama. Mengapa dua bentuk sintaksis yang berbeda dapat diterima, tetapi tidak tiga?

Ada beberapa diskusi sebelumnya tentang topik ini di RFC . Seperti yang saya katakan di sana, saya mendukung blok async menggunakan break _tanpa_ harus memberikan label (tidak ada cara untuk keluar dari blok async ke loop luar sehingga Anda tidak kehilangan ekspresivitas apa pun).

@withoutboats Penutupan hanyalah jenis fungsi lain; setelah Anda mempelajari "penutupan adalah fungsi" maka Anda dapat menerapkan semua yang Anda ketahui tentang fungsi ke penutupan, termasuk " return selalu kembali dari fungsi yang berisi".

@Nemo157 Bahkan jika Anda tidak berlabel break menargetkan blok async , Anda harus menyediakan mekanisme (seperti 'label: async ) untuk kembali lebih awal dari loop di dalam blok async .

@joshtriplett

Penutupan hanyalah jenis fungsi lain; setelah Anda mempelajari "penutupan adalah fungsi" maka Anda dapat menerapkan semua yang Anda ketahui tentang fungsi ke penutupan, termasuk "pengembalian selalu kembali dari fungsi yang mengandung".

Saya pikir blok async juga semacam "fungsi"-- yang tanpa argumen yang dapat dijalankan hingga selesai secara asinkron. Itu adalah kasus khusus penutupan async yang tidak memiliki argumen dan telah diterapkan sebelumnya.

@cramertj ya, saya berasumsi bahwa setiap break point implisit juga dapat diberi label jika perlu (seperti yang saya yakini saat ini semuanya bisa).

Apa pun yang membuat aliran kontrol lebih sulit untuk diikuti, dan khususnya mengubah arti return , memberikan banyak tekanan pada kemampuan untuk membaca kode dengan lancar.

Sejalan dengan itu, panduan standar dalam C adalah "jangan menulis makro yang kembali dari tengah makro". Atau, sebagai kasus yang kurang umum tetapi masih bermasalah: jika Anda menulis makro yang terlihat seperti loop, break dan continue akan bekerja dari dalamnya. Saya telah melihat orang menulis makro loop-ish yang sebenarnya menyematkan dua loop, jadi break tidak berfungsi seperti yang diharapkan, dan itu sangat membingungkan.

Saya pikir blok async juga semacam "fungsi"

Saya pikir itu perspektif berdasarkan mengetahui internal implementasi.

Saya tidak melihatnya sebagai fungsi sama sekali.

Saya tidak melihatnya sebagai fungsi sama sekali.

@joshtriplett

Kecurigaan saya adalah bahwa Anda akan membuat argumen yang sama datang ke bahasa dengan penutupan untuk pertama kalinya-- bahwa return tidak boleh bekerja dalam penutupan, tetapi dalam fungsi yang menentukan. Dan memang, ada bahasa yang mengambil interpretasi ini, seperti Scala.

@cramertj saya tidak akan, tidak; untuk lambda dan/atau fungsi yang didefinisikan dalam suatu fungsi, rasanya benar-benar alami bahwa mereka adalah sebuah fungsi. (Paparan pertama saya untuk itu adalah di Python, FWIW, di mana lambdas tidak dapat menggunakan return dan dalam fungsi bersarang return kembali dari fungsi yang berisi return .)

Saya pikir begitu seseorang mengetahui apa yang dilakukan blok async, secara intuitif jelas bagaimana return harus berperilaku. Setelah Anda mengetahui bahwa ini mewakili eksekusi yang tertunda, jelas bahwa return tidak dapat diterapkan ke fungsi tersebut. Jelas bahwa fungsi akan kembali pada saat blok berjalan. Belajar IMO ini seharusnya tidak terlalu sulit. Setidaknya kita harus mencobanya dan melihat.

RFC ini tidak mengusulkan bagaimana konstruksi ? -operator dan aliran kontrol seperti return , break dan continue harus bekerja di dalam blok async.

Apakah lebih baik untuk melarang operator aliran kontrol atau menunda blok sampai RFC khusus ditulis? Ada fitur lain yang diinginkan yang akan dibahas nanti. Sementara itu, kami akan memiliki fungsi async, penutupan, dan await! :)

Saya setuju dengan @memoryruins di sini, saya pikir ada baiknya membuat RFC lain untuk membahas hal tersebut secara lebih rinci.

Apa pendapat Anda tentang fungsi yang memungkinkan kita mengakses konteks dari dalam async fn, mungkin disebut core::task::context() ? Itu hanya akan panik jika dipanggil dari luar async fn. Saya pikir itu akan sangat berguna, misalnya untuk mengakses eksekutor untuk menelurkan sesuatu.

@MajorBreakfast fungsi itu disebut lazy

async fn foo() -> i32 {
    await!(lazy(|ctx| {
        // do something with ctx
        42
    }))
}

Untuk hal yang lebih spesifik seperti pemijahan kemungkinan akan ada fungsi pembantu yang membuatnya lebih ergonomis

async fn foo() -> i32 {
    let some_task = lazy(|_| 5);
    let spawned_task = await!(spawn_with_handle(some_task));
    await!(spawned_task)
}

@Nemo157 Sebenarnya spawn_with_handle adalah tempat saya ingin menggunakan ini. Saat mengonversi kode menjadi 0,3 saya perhatikan bahwa spawn_with_handle sebenarnya hanya masa depan karena memerlukan akses ke konteks ( lihat kode ). Yang ingin saya lakukan adalah menambahkan metode spawn_with_handle ke ContextExt dan menjadikan spawn_with_handle fungsi gratis yang hanya berfungsi di dalam fungsi async:

fn poll(self: PinMut<Self>, cx: &mut Context) -> Poll<Self::Output> {
     let join_handle = ctx.spawn_with_handle(future);
     ...
}
async fn foo() {
   let join_handle = spawn_with_handle(future); // This would use this function internally
   await!(join_handle);
}

Ini akan menghapus semua omong kosong menunggu ganda yang kita miliki saat ini.

Kalau dipikir-pikir, metode ini perlu dipanggil core::task::with_current_context() dan bekerja sedikit berbeda karena tidak mungkin menyimpan referensi.

Sunting: Fungsi ini sudah ada dengan nama get_task_cx . Saat ini ada di libstd karena alasan teknis. Saya mengusulkan untuk menjadikannya API publik setelah dapat dimasukkan ke libcore.

Saya ragu akan mungkin untuk memiliki fungsi yang dapat dipanggil dari fungsi non- async yang dapat memberi Anda konteks dari beberapa fungsi induk async setelah dipindahkan dari TLS. Pada saat itu konteks kemungkinan akan diperlakukan seperti variabel lokal tersembunyi di dalam fungsi async , sehingga Anda dapat memiliki makro yang mengakses konteks secara langsung dalam fungsi itu, tetapi tidak akan ada cara untuk memiliki spawn_with_handle secara ajaib menarik konteks dari pemanggilnya.

Jadi, berpotensi sesuatu seperti

fn spawn_with_handle(executor: &mut Executor, future: impl Future) { ... }

async fn foo() {
    let join_handle = spawn_with_handle(async_context!().executor(), future);
    await!(join_handle);
}

@Nemo157 Saya pikir Anda benar: Fungsi seperti yang saya usulkan kemungkinan tidak dapat berfungsi jika tidak dipanggil langsung dari dalam async fn. Mungkin cara terbaik adalah membuat spawn_with_handle makro yang menggunakan await internal (seperti select! dan join! ):

async fn foo() {
    let join_handle = spawn_with_handle!(future);
    await!(join_handle);
}

Ini terlihat bagus dan dapat diimplementasikan dengan mudah melalui await!(lazy(|ctx| { ... })) di dalam makro.

async_context!() bermasalah karena tidak dapat mencegah saya menyimpan referensi konteks di seluruh titik menunggu.

async_context!() bermasalah karena tidak dapat mencegah saya menyimpan referensi konteks di seluruh titik menunggu.

Tergantung implementasinya, bisa. Jika argumen generator lengkap dibangkitkan, mereka harus dibatasi sehingga Anda tidak dapat menyimpan referensi di seluruh titik hasil, nilai di balik argumen akan memiliki masa hidup yang hanya berjalan hingga titik hasil. Async/menunggu hanya akan mewarisi batasan itu.

@Nemo157 Maksud Anda seperti ini?

let my_arg = yield; // my_arg lives until next yield

@Pzixel Maaf untuk membangunkan _mungkin_ diskusi lama, tapi saya ingin menambahkan pemikiran saya.

Ya, saya suka sintaks await!() menghilangkan ambiguitas saat menggabungkannya dengan hal-hal seperti ? , namun saya juga setuju bahwa sintaks ini menjengkelkan untuk mengetik seribu kali dalam satu proyek. Saya juga percaya itu berisik, dan kode yang bersih itu penting.

Itu sebabnya saya bertanya-tanya apa argumen sebenarnya terhadap simbol sufiks (yang telah disebutkan beberapa kali sebelumnya), seperti something_async()@ dibandingkan dengan sesuatu dengan await , mungkin karena await adalah kata kunci terkenal dari bahasa lain? @ bisa jadi lucu karena menyerupai a dari menunggu, tetapi mungkin simbol apa pun yang cocok.

Saya berpendapat bahwa pilihan sintaksis seperti itu akan logis karena sesuatu yang serupa terjadi dengan try!() yang pada dasarnya menjadi sufiks ? (saya tahu ini tidak persis sama). Ringkas, mudah diingat dan mudah diketik.

Hal lain yang mengagumkan tentang sintaks tersebut adalah bahwa perilaku segera jelas ketika dikombinasikan dengan simbol ? (setidaknya saya percaya itu akan). Simak berikut ini:

// Await, then unwrap a Result from the future
awaiting_a_result()@?;

// Unwrap a future from a result, then await
result_with_future()?@;

// The real crazy can make it as funky as they want
magic()?@@?@??@; 
// - I'm joking, of course

Ini tidak memiliki masalah karena await future? tidak jelas pada pandangan pertama apa yang akan terjadi kecuali Anda tahu tentang situasi seperti itu. Namun implementasinya konsisten dengan ? .

Sekarang, hanya ada beberapa _minor_ hal yang dapat saya pikirkan yang akan melawan ide ini:

  • mungkin _terlalu_ ringkas dan kurang terlihat/verbose tidak seperti sesuatu dengan await , yang membuatnya _sulit_ untuk menemukan titik penangguhan dalam suatu fungsi.
  • mungkin asimetris dengan kata kunci async , di mana yang satu adalah kata kunci dan yang lainnya sebagai simbol. Meskipun, await!() mengalami masalah yang sama yaitu keywore versus makro.
  • memilih simbol menambahkan elemen sintaksis lain, dan sesuatu untuk dipelajari. Tetapi, dengan asumsi bahwa ini mungkin menjadi sesuatu yang umum digunakan, saya rasa ini bukan masalah.

@phaux juga menyebutkan penggunaan simbol ~ . Namun saya percaya karakter ini funky untuk mengetik pada beberapa tata letak keyboard karena itu saya akan merekomendasikan untuk membuang ide itu.

Apa pendapat kalian guys? Apakah Anda setuju itu mirip dengan bagaimana try!() _menjadi_ ? ? Apakah Anda lebih suka await atau simbol, dan mengapa? Apakah saya gila karena membahas ini, atau mungkin saya melewatkan sesuatu?

Maaf untuk istilah yang salah yang mungkin saya gunakan.

Kekhawatiran terbesar yang saya miliki dengan sintaks berbasis sigil adalah ia dapat dengan mudah berubah menjadi sup mesin terbang, seperti yang Anda tunjukkan dengan baik. Siapa pun yang akrab dengan Perl (pra-6) akan mengerti ke mana saya akan pergi dengan ini. Menghindari kebisingan garis adalah cara terbaik untuk bergerak maju.

Yang mengatakan, mungkin cara terbaik untuk pergi sebenarnya persis seperti dengan try! ? Artinya, mulailah dengan makro async!(foo) eksplisit, dan jika perlu, tambahkan beberapa sigil yang akan menjadi gula untuk itu. Tentu, itu menunda masalah untuk nanti, tetapi async!(foo) cukup untuk iterasi pertama async/menunggu, dengan keuntungan yang relatif tidak kontroversial. (dan memiliki preseden try! / ? harus muncul dari sigil)

@withoutboats Saya belum membaca seluruh utas ini, tetapi adakah yang membantu implementasinya? Di mana cabang pengembangan Anda?

Dan, mengenai sisa pertimbangan yang belum terselesaikan, apakah ada yang meminta bantuan ahli di luar komunitas Rust? Joe Duffy tahu dan sangat peduli tentang konkurensi dan memahami detail fiddly dengan cukup baik , dan dia diberikan keynote di RustConf , jadi saya curiga dia mungkin menerima permintaan panduan, jika permintaan seperti itu dibenarkan.

@BatmanAoD implementasi awal mendarat di https://github.com/rust-lang/rust/pull/51580

Utas RFC asli mendapat komentar dari sejumlah ahli di ruang PLT, bahkan di luar Rust :)

Saya ingin menyarankan simbol '$' untuk menunggu Futures, karena waktu adalah uang, dan saya ingin mengingatkan penyusun ini.

Hanya bercanda. Saya tidak berpikir memiliki simbol untuk menunggu adalah ide yang baik. Rust adalah tentang menjadi eksplisit, dan memungkinkan orang untuk menulis kode tingkat rendah dalam bahasa yang kuat yang tidak membiarkan Anda menembak diri sendiri. Simbol jauh lebih kabur daripada makro await! , dan memungkinkan orang untuk menembak diri mereka sendiri dengan cara yang berbeda dengan menulis kode yang sulit dibaca. Saya sudah berpendapat bahwa ? adalah langkah yang terlalu jauh.

Saya juga tidak setuju dengan adanya kata kunci async akan digunakan dalam bentuk async fn . Ini menyiratkan semacam "bias" terhadap async. Mengapa async layak mendapatkan kata kunci? Kode asinkron hanyalah abstraksi lain bagi kami yang tidak selalu diperlukan. Saya pikir atribut async bertindak lebih seperti "ekstensi" dari dialek Rust dasar yang memungkinkan kita untuk menulis kode yang lebih kuat.

Saya bukan arsitek bahasa, tetapi saya memiliki sedikit pengalaman menulis kode async dengan Promises dalam JavaScript, dan saya pikir cara melakukannya di sana membuat kode async menyenangkan untuk ditulis.

@steveklabnik Ah, oke, terima kasih. Bisakah kami ( / haruskah saya) memperbarui deskripsi masalah? Mungkin butir butir "implementasi awal" harus dipecah menjadi "implementasi tanpa dukungan move " dan "implementasi penuh"?

Apakah iterasi implementasi berikutnya sedang dikerjakan di beberapa cabang/cabang publik? Atau bisakah itu tidak dilanjutkan sampai RFC 2418 diterima?

Mengapa masalah sintaks async/menunggu dibahas di sini daripada di RFC?

@c-edw Saya rasa pertanyaan tentang kata kunci async dijawab dengan Apa Warna Fungsi Anda?

@parasyte Telah disarankan kepada saya bahwa posting itu sebenarnya adalah argumen yang menentang seluruh gagasan fungsi async tanpa konkurensi gaya green-thread yang dikelola secara otomatis.

Saya tidak setuju dengan posisi ini, karena utas hijau tidak dapat diimplementasikan secara transparan tanpa runtime (terkelola), dan ada alasan bagus bagi Rust untuk mendukung kode asinkron tanpa memerlukan itu.

Tetapi sepertinya Anda membaca posting ini bahwa semantik async / await baik-baik saja, tetapi ada kesimpulan yang bisa ditarik tentang kata kunci? Maukah Anda memperluas itu?

Saya setuju dengan sudut pandang Anda di sana, juga. Saya berkomentar bahwa kata kunci async diperlukan, dan artikel tersebut menjelaskan alasan di baliknya. Kesimpulan yang diambil oleh penulis adalah hal yang berbeda.

@parasyte Ah, oke. Senang saya bertanya - karena keengganan penulis untuk dikotomi merah/biru, saya pikir Anda mengatakan yang sebaliknya!

Saya ingin mengklarifikasi lebih lanjut, karena saya merasa saya tidak melakukannya dengan adil.

Dikotomi tak terhindarkan . Beberapa proyek telah mencoba menghapusnya dengan membuat setiap panggilan fungsi menjadi asinkron, sehingga panggilan fungsi sinkronisasi tidak ada. Midori adalah contoh nyata. Dan proyek lain telah mencoba menghapus dikotomi dengan menyembunyikan fungsi asinkron di balik fasad fungsi sinkronisasi. gevent adalah contoh dari jenis ini.

Keduanya memiliki masalah yang sama; mereka masih membutuhkan dikotomi untuk membedakan antara menunggu tugas asinkron untuk diselesaikan dan memulai tugas secara asinkron !

  • Midori tidak hanya memperkenalkan kata kunci await , tetapi juga kata kunci async pada situs panggilan fungsi .
  • gevent menyediakan gevent.spawn selain penantian implisit dari panggilan fungsi yang tampak normal.

Itulah alasan utama saya memunculkan artikel fungsi warna, karena ini menjawab pertanyaan, "Mengapa async pantas mendapatkan kata kunci?"

Yah, bahkan kode sinkron berbasis utas dapat membedakan "menunggu tugas selesai" (bergabung) dan "memulai tugas" (spawn). Anda dapat membayangkan bahasa di mana semuanya async (bijaksana implementasi), tetapi tidak ada anotasi pada await (karena itu adalah perilaku default), dan async Midori sebagai gantinya merupakan penutupan yang diteruskan ke spawn API. Ini menempatkan all-async pada pijakan sintaksis/fungsi-warna yang sama persis dengan all-sync.

Jadi sementara saya setuju async layak mendapatkan kata kunci, menurut saya itu lebih karena Rust peduli dengan mekanisme implementasi pada level ini, dan perlu memberikan kedua warna untuk alasan itu.

@rpjohnst Ya, saya sudah membaca proposal Anda. Secara konseptual sama dengan menyembunyikan warna a la gevent. Yang saya kritik di forum rust di utas yang sama; setiap panggilan fungsi terlihat sinkron yang merupakan bahaya tertentu ketika suatu fungsi sinkron dan memblokir dalam pipa asinkron. Bug semacam ini tidak dapat diprediksi dan merupakan bencana nyata untuk dipecahkan.

Saya tidak berbicara tentang proposal saya secara khusus, saya berbicara tentang bahasa di mana semuanya tidak sinkron. Anda dapat menghindari dikotomi seperti itu - proposal saya tidak mencoba itu.

IIUC itulah yang dicoba Midori. Pada saat itu, kata kunci vs penutupan hanya memperdebatkan semantik.

Pada Kam, 12 Juli 2018 pukul 15:01, Russell Johnston [email protected]
menulis:

Anda menggunakan keberadaan kata kunci sebagai argumen Anda mengapa dikotomi
masih ada di Midori. Jika Anda menghapusnya, di mana dikotominya? Itu
sintaks identik dengan kode semua-sinkronisasi, tetapi dengan kemampuan async
kode.

Karena ketika Anda memanggil fungsi async tanpa menunggu hasilnya, itu
secara sinkron mengembalikan janji. Yang bisa ditunggu nanti. 😐

_Wow, ada yang tahu sesuatu tentang Midori? Saya selalu berpikir bahwa ini adalah proyek tertutup dengan hampir tidak ada makhluk hidup yang mengerjakannya. Akan menarik jika seseorang dari kalian telah menulis tentang hal itu secara lebih rinci._

/keluar topik

@Pzixel Tidak ada makhluk hidup yang masih mengerjakannya, karena proyeknya ditutup. Tapi blog Joe Duffy memiliki banyak detail menarik. Lihat tautan saya di atas.

Kami telah keluar jalur di sini, dan saya merasa seperti mengulangi diri saya sendiri, tetapi itu adalah bagian dari "kehadiran kata kunci"- kata kunci await . Jika Anda mengganti kata kunci dengan API seperti spawn dan join , Anda dapat sepenuhnya asinkron (seperti Midori) tetapi tanpa dikotomi (tidak seperti Midori).

Atau dengan kata lain, seperti yang saya katakan sebelumnya, itu tidak mendasar- kita hanya memilikinya karena kita menginginkan pilihan.

@CyrusNajmabadi maaf telah berikut adalah beberapa informasi tambahan tentang pengambilan keputusan.

Jika Anda tidak ingin disebutkan lagi, tolong beri tahu saya. Saya hanya berpikir Anda mungkin tertarik.

Dari saluran perselisihan #wg-net :

@cramertj
bahan untuk dipikirkan: saya sering menulis Ok::<(), MyErrorType>(()) di akhir blok async { ... } . mungkin ada sesuatu yang bisa kita lakukan untuk membuat pembatasan jenis kesalahan lebih mudah?

@tanpa perahu
[...] mungkin kita ingin konsisten dengan [ try ]?

(Saya ingat beberapa diskusi yang relatif baru tentang bagaimana blok try dapat mendeklarasikan tipe pengembaliannya, tetapi saya tidak dapat menemukannya sekarang ...)

warna yang disebutkan:

async -> io::Result<()> {
    ...
}

async: io::Result<()> {
    ...
}

async as io::Result<()> {
    ...
}

Satu hal yang dapat dilakukan try yang kurang ergonomis dengan async adalah menggunakan penjilidan variabel atau mengetikkan ascription, mis.

let _: io::Result<()> = try { ... };
let _: impl Future<Output = io::Result<()>> = async { ... };

Saya sebelumnya telah melemparkan ide untuk mengizinkan sintaks seperti fn untuk sifat Future , misalnya Future -> io::Result<()> . Itu akan membuat opsi penyediaan tipe manual terlihat sedikit lebih baik, meskipun masih banyak karakter:

let _: impl Future -> io::Result<()> = async {
}
async -> impl Future<Output = io::Result<()>> {
    ...
}

akan menjadi pilihan saya.

Ini mirip dengan sintaks penutupan yang ada:

|x: i32| -> i32 { x + 1 };

Sunting: Dan akhirnya ketika memungkinkan TryFuture untuk mengimplementasikan Future :

async -> impl TryFuture<Ok = i32, Error = ()> {
    ...
}

Sunting2: Tepatnya di atas akan bekerja dengan definisi sifat hari ini. Hanya saja tipe TryFuture tidak berguna hari ini karena saat ini tidak mengimplementasikan Future

@MajorBreakfast Mengapa -> impl Future<Output = io::Result<()>> daripada -> io::Result<()> ? Kami sudah melakukan return-type-desugaring untuk async fn foo() -> io::Result<()> , jadi IMO jika kami menggunakan sintaks berbasis -> tampaknya jelas bahwa kami menginginkan gula yang sama di sini.

@cramertj Ya itu harus konsisten. Posting saya di atas agak berasumsi bahwa saya dapat meyakinkan Anda semua bahwa pendekatan tipe pengembalian luar lebih unggul 😁

Jika kita menggunakan async -> R { .. } maka mungkin kita juga harus menggunakan try -> R { .. } serta menggunakan expr -> TheType secara umum untuk jenis ascription. Dengan kata lain, sintaks jenis ascription yang kita gunakan harus diterapkan secara seragam di mana-mana.

@Centril saya setuju. Itu harus dapat digunakan di mana-mana. Saya tidak yakin lagi apakah -> benar-benar pilihan yang tepat. Saya mengaitkan -> dengan dapat dipanggil. Dan blok async tidak.

@MajorBreakfast pada dasarnya saya setuju; Saya pikir kita harus menggunakan : untuk jenis ascription, jadi async : Type { .. } , try : Type { .. } , dan expr : Type . Kami telah membahas potensi ambiguitas pada Discord dan saya pikir kami menemukan jalan ke depan dengan : yang masuk akal...

Pertanyaan lain adalah tentang Either enum. Kita sudah memiliki Either di futures . Ini juga membingungkan karena terlihat seperti Either dari peti either padahal tidak.

Karena Futures tampaknya digabungkan dalam std (setidaknya bagian yang sangat mendasar darinya) dapatkah kita juga menyertakan Either sana? Sangat penting untuk memilikinya agar dapat mengembalikan impl Future dari fungsi.

Sebagai contoh, saya sering menulis kode seperti berikut:

fn handler() -> impl Future<Item = (), Error = Bar> + Send {
    someFuture()
        .and_then(|x| {
            if condition(&x) {
                Either::A(anotherFuture(x))
            } else {
                Either::B(future::ok(()))
            }
        })
}

Saya ingin bisa menulisnya seperti:

async fn handler() -> Result<(), Bar> {
    let x = await someFuture();
    if condition(&x) {
        await anotherFuture(x);
    }
}

Tetapi seperti yang saya pahami ketika async diekspan, itu membutuhkan Either untuk dimasukkan di sini, karena kita masuk ke kondisi atau tidak.

_Anda dapat menemukan kode aktual di sini jika Anda mau.

@Pzixel Anda tidak perlu Either di dalam fungsi async , selama Anda await futures maka transformasi kode yang dilakukan async akan menyembunyikan keduanya mengetik secara internal dan menyajikan satu tipe pengembalian ke kompiler.

@Pzixel Juga, saya (secara pribadi) berharap Either tidak akan diperkenalkan dengan RFC ini, karena itu akan memperkenalkan versi terbatas https://github.com/rust-lang/rfcs/issues /2414 (yang hanya berfungsi dengan 2 jenis dan hanya dengan Future s), sehingga kemungkinan menambahkan API cruft jika solusi umum pernah digabungkan -- dan seperti yang disebutkan @Nemo157 tampaknya tidak darurat untuk punya Either sekarang :)

@Ekleog yakin, saya baru saja dikejutkan oleh gagasan ini bahwa saya sebenarnya memiliki banyak either dalam kode async saya yang ada dan saya benar-benar ingin menyingkirkannya. Kemudian saya mengingat kebingungan saya ketika saya menghabiskan ~ setengah jam sampai saya menyadari bahwa itu tidak dapat dikompilasi karena saya memiliki dependensi either peti (kesalahan di masa depan cukup sulit untuk dipahami sehingga butuh waktu cukup lama). Jadi inilah mengapa saya menulis komentar, hanya untuk memastikan masalah ini diatasi.

Tentu saja, ini tidak terkait dengan async/await saja, ini lebih umum, sehingga layak mendapatkan RFC-nya sendiri. Saya hanya ingin menekankan bahwa futures harus tahu tentang either atau sebaliknya (untuk mengimplementasikan IntoFuture dengan benar).

@Pzixel Either diekspor oleh peti berjangka adalah ekspor ulang dari peti either . futures peti 0.3 tidak dapat mengimplementasikan Future untuk Either karena aturan yatim piatu. Kemungkinan besar kami juga akan menghapus Stream dan Sink impls untuk Either untuk konsistensi dan menawarkan alternatif sebagai gantinya (dibahas di sini ). Selain itu, peti either kemudian dapat mengimplementasikan Future , Stream dan Sink itu sendiri, kemungkinan di bawah tanda fitur.

Yang mengatakan, seperti yang telah disebutkan @Nemo157 , ketika bekerja dengan futures, lebih baik menggunakan fungsi async daripada Either .

Barang async : Type { .. } sekarang diusulkan di https://github.com/rust-lang/rfcs/pull/2522.

Apakah fungsi async/await secara otomatis mengimplementasikan Send sudah diimplementasikan?

Sepertinya fungsi async berikut ini belum (belum?) Send :

pub async fn __receive() -> ()
{
    let mut chan: futures::channel::mpsc::Receiver<Box<Send + 'static>> = None.unwrap();

    await!(chan.next());
}

Tautan ke reproduksi lengkap (yang tidak dapat dikompilasi di taman bermain karena kurangnya futures-0.3 , saya kira) ada di sini .

Juga, ketika menyelidiki masalah ini saya menemukan https://github.com/rust-lang/rust/issues/53249 , yang saya kira harus ditambahkan ke daftar pelacakan posting paling atas :)

Berikut adalah taman bermain yang menunjukkan bahwa fungsi async/await yang mengimplementasikan Send _should_ berfungsi. Membatalkan komentar pada versi Rc dengan benar mendeteksi fungsi itu sebagai non- Send . Saya dapat melihat contoh spesifik Anda sedikit (tidak ada kompiler Rust pada mesin ini :slightly_frowning_face :) untuk mencoba dan mencari tahu mengapa itu tidak berfungsi.

@Ekleog std::mpsc::Receiver bukan Sync , dan async fn Anda tulis menyimpan referensi untuk itu. Referensi ke item !Sync adalah !Send .

@cramertj Hmm… tapi, apakah saya tidak memegang mpsc::Receiver , yang seharusnya Send jika tipe generiknya adalah Send ? (juga, ini bukan std::mpsc::Receiver tetapi futures::channel::mpsc::Receiver , yaitu Sync juga jika tipenya Send , maaf karena tidak memperhatikan mpsc::Receiver alias ambigu!)

@Nemo157 Terima kasih! Saya telah membuka https://github.com/rust-lang/rust/issues/53259 untuk menghindari terlalu banyak kebisingan pada masalah ini :)

Pertanyaan apakah dan bagaimana blok async mengizinkan ? dan aliran kontrol lainnya mungkin memerlukan beberapa interaksi dengan blok try (misalnya try async { .. } untuk mengizinkan ? tanpa kebingungan serupa dengan return ?).

Ini berarti mekanisme untuk menentukan tipe blok async mungkin perlu berinteraksi dengan mekanisme untuk menentukan tipe blok try . Saya meninggalkan komentar pada sintaks ascription RFC: https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Tekan saja apa yang pada awalnya saya pikir adalah masalah futures-rs , tetapi ternyata itu sebenarnya masalah async/menunggu, jadi ini dia: https://github.com/rust-lang-nursery/ futures-rs/issues/1199#issuecomment -413089012

Seperti yang dibahas beberapa hari yang lalu di discord, await belum dicadangkan sebagai kata kunci. Sangat penting untuk memasukkan reservasi ini (dan ditambahkan ke kata kunci edisi 2018) sebelum rilis 2018. Ini reservasi yang sedikit rumit karena kami ingin terus menggunakan sintaks makro untuk saat ini.

Akankah Future/Task API memiliki cara untuk menelurkan masa depan lokal?
Saya melihat ada SpawnLocalObjError , tetapi tampaknya tidak digunakan.

@panicbit Di kelompok kerja kami sedang mendiskusikan apakah masuk akal untuk memasukkan fungsi spawning dalam konteks sama sekali. https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError tidak sepenuhnya tidak terpakai: LocalPool dari peti berjangka menggunakannya. Anda benar, bagaimanapun, tidak ada di libcore yang menggunakannya)

@withoutboats Saya perhatikan beberapa tautan dalam deskripsi masalah sudah kedaluwarsa. Secara khusus, https://github.com/rust-lang/rfcs/pull/2418 ditutup dan https://github.com/rust-lang-nursery/futures-rs/issues/1199 telah dipindahkan ke https:/ /github.com/rust-lang/rust/issues/53548

catatan Nama masalah pelacakan ini adalah async/menunggu tetapi juga ditetapkan ke API tugas! API tugas saat ini memiliki RFC stabilisasi yang tertunda: https://github.com/rust-lang/rfcs/pull/2592

Adakah peluang untuk membuat kata kunci dapat digunakan kembali untuk implementasi async alternatif? saat ini ia menciptakan Masa Depan, tetapi ini semacam peluang yang terlewatkan untuk membuat async berbasis push lebih bermanfaat.

@aep Dimungkinkan untuk dengan mudah mengonversi dari sistem berbasis push ke sistem Future berbasis pull dengan menggunakan oneshot::channel .

Sebagai contoh, Janji JavaScript berbasis push, jadi stdweb menggunakan oneshot::channel untuk mengubah Janji JavaScript menjadi Rust Futures . Itu juga menggunakan oneshot::channel untuk beberapa API panggilan balik berbasis push lainnya, seperti setTimeout .

Karena model memori Rust, Futures berbasis push memiliki biaya kinerja ekstra dibandingkan dengan pull . Jadi lebih baik membayar biaya kinerja itu hanya saat dibutuhkan (misalnya dengan menggunakan oneshot::channel ), daripada membuat seluruh sistem Future berbasis push.

Karena itu, saya bukan bagian dari tim inti atau tim lang, jadi tidak ada yang saya katakan memiliki otoritas. Itu hanya pendapat pribadi saya.

sebenarnya sebaliknya dalam kode sumber daya terbatas. model pull memiliki penalti karena Anda memerlukan sumber daya di dalam benda yang sedang ditarik daripada memberi nilai siap berikutnya melalui setumpuk fungsi menunggu. Desain futures.rs terlalu mahal untuk apa pun yang dekat dengan perangkat keras, seperti sakelar jaringan (kasus penggunaan saya) atau penyaji game.

Namun, dalam hal ini yang kita perlukan di sini adalah membuat async memancarkan sesuatu seperti yang dilakukan Generator. Seperti yang saya katakan sebelumnya, saya pikir async dan generator sebenarnya adalah hal yang sama jika Anda cukup mengabstraksikannya alih-alih mengikat dua kata kunci ke satu perpustakaan.

Namun, dalam hal ini yang kita perlukan di sini adalah membuat async memancarkan sesuatu seperti yang dilakukan Generator.

async pada titik ini secara harfiah merupakan pembungkus minimal di sekitar literal generator. Saya mengalami kesulitan melihat bagaimana generator membantu dengan async IO berbasis push, bukankah Anda lebih membutuhkan transformasi CPS untuk itu?

Bisakah Anda lebih spesifik tentang apa yang Anda maksud dengan "Anda membutuhkan sumber daya di dalam benda yang sedang ditarik?" Saya tidak yakin mengapa Anda membutuhkannya, atau bagaimana "memberi makan nilai siap berikutnya melalui setumpuk fungsi menunggu" berbeda dari poll() .

Saya mendapat kesan bahwa futures berbasis push lebih mahal (dan karenanya lebih sulit untuk digunakan di lingkungan yang terbatas). Mengizinkan panggilan balik sewenang-wenang untuk dilampirkan ke masa depan memerlukan beberapa bentuk tipuan, biasanya alokasi tumpukan, jadi alih-alih mengalokasikan sekali dengan masa depan root, Anda mengalokasikan di setiap kombinator. Dan pembatalan juga menjadi lebih kompleks karena masalah keamanan utas, jadi Anda tidak mendukungnya atau Anda memerlukan semua penyelesaian panggilan balik untuk menggunakan operasi atom untuk menghindari balapan. Semua itu menambah kerangka kerja yang jauh lebih sulit untuk dioptimalkan, sejauh yang saya tahu.

bukankah Anda lebih membutuhkan transformasi CPS untuk itu?

ya, sintaks generator saat ini tidak berfungsi untuk itu karena tidak memiliki argumen untuk kelanjutannya, itulah sebabnya saya berharap async akan membawa cara untuk melakukannya.

Anda membutuhkan sumber daya di dalam benda yang sedang ditarik?

itulah cara buruk saya untuk mengatakan bahwa membalikkan urutan async berfungsi dua kali memiliki biaya. Yaitu sekali dari perangkat keras ke masa depan dan kembali lagi menggunakan saluran. Anda perlu membawa banyak barang yang tidak memiliki manfaat dalam kode perangkat keras.

Contoh umum adalah Anda tidak bisa hanya memanggil tumpukan masa depan ketika Anda tahu deskriptor file soket sudah siap, tetapi sebaliknya harus menerapkan semua logika eksekusi untuk memetakan peristiwa dunia nyata ke masa depan, yang memiliki biaya eksternal seperti penguncian, ukuran kode dan yang paling penting kompleksitas kode.

Mengizinkan panggilan balik sewenang-wenang untuk dilampirkan ke masa depan membutuhkan beberapa bentuk tipuan

ya saya mengerti bahwa panggilan balik mahal di beberapa lingkungan (bukan di lingkungan saya, di mana kecepatan eksekusi tidak relevan, tetapi saya memiliki total memori 1MB, jadi futures.rs bahkan tidak muat di flash), namun, Anda tidak memerlukan pengiriman dinamis sama sekali ketika Anda memiliki sesuatu seperti kelanjutan (yang setengah menerapkan konsep generator saat ini).

Dan pembatalan juga menjadi lebih kompleks karena keamanan utas

saya pikir kita membingungkan hal-hal di sini. Saya tidak menganjurkan untuk panggilan balik. Kelanjutan dapat berupa tumpukan statis dengan baik. Misalnya apa yang kami terapkan dalam bahasa tanah liat hanyalah pola generator yang dapat Anda gunakan untuk mendorong atau menarik. yaitu:

async fn add (a: u32) -> u32 {
    let b = await
    a + b
}

add(3).continue(2) == 5

Saya kira saya bisa terus melakukan itu dengan makro, tetapi saya merasa ini adalah kesempatan yang terlewatkan di sini membuang kata kunci bahasa pada satu konsep tertentu.

bukan milik saya, di mana kecepatan eksekusi tidak relevan, tetapi saya memiliki total memori 1MB, jadi futures.rs bahkan tidak muat di flash

Saya cukup yakin masa depan saat ini dimaksudkan untuk berjalan di lingkungan yang dibatasi memori. Apa sebenarnya yang menghabiskan begitu banyak ruang?

Sunting: program ini membutuhkan ruang disk 295KB saat dikompilasi --release di macbook saya (halo dunia dasar membutuhkan 273KB):

use futures::{executor::LocalPool, future};

fn main() {
    let mut pool = LocalPool::new();
    let hello = pool.run_until(future::ready("Hello, world!"));
    println!("{}", hello);
}

bukan milik saya, di mana kecepatan eksekusi tidak relevan, tetapi saya memiliki total memori 1MB, jadi futures.rs bahkan tidak muat di flash

Saya cukup yakin masa depan saat ini dimaksudkan untuk berjalan di lingkungan yang dibatasi memori. Apa sebenarnya yang menghabiskan begitu banyak ruang?

Juga apa yang Anda maksud dengan memori? Saya telah menjalankan kode berbasis async/menunggu saat ini pada perangkat dengan 128 kB flash/16 kB ram. Pasti ada masalah penggunaan memori dengan async/menunggu saat ini, tetapi itu sebagian besar adalah masalah implementasi dan dapat ditingkatkan dengan menambahkan beberapa pengoptimalan tambahan (mis. https://github.com/rust-lang/rust/issues/52924).

Contoh umum adalah Anda tidak bisa hanya memanggil tumpukan masa depan ketika Anda tahu deskriptor file soket sudah siap, tetapi sebaliknya harus menerapkan semua logika eksekusi untuk memetakan peristiwa dunia nyata ke masa depan, yang memiliki biaya eksternal seperti penguncian, ukuran kode dan yang paling penting kompleksitas kode.

Mengapa? Ini masih tidak tampak seperti apa pun yang memaksa Anda masuk ke masa depan. Anda dapat dengan mudah memanggil poll seperti halnya mekanisme berbasis push.

Juga apa yang Anda maksud dengan memori?

Saya rasa ini tidak relevan. Seluruh diskusi ini telah merinci menjadi membatalkan poin yang bahkan tidak ingin saya sampaikan. Saya di sini bukan untuk mengkritik masa depan selain mengatakan bahwa menggabungkan desainnya ke dalam bahasa inti adalah sebuah kesalahan.

Maksud saya adalah kata kunci async dapat dijadikan bukti di masa mendatang jika dilakukan dengan benar. Kelanjutan adalah apa yang saya inginkan, tapi mungkin orang lain datang dengan ide yang lebih baik.

Anda dapat dengan mudah memanggil polling seperti halnya mekanisme berbasis push.

Ya itu masuk akal jika Future:poll memiliki call args. Itu tidak dapat memilikinya karena jajak pendapat harus abstrak. Alih-alih, saya mengusulkan untuk memancarkan kelanjutan dari kata kunci async, dan menyiratkan Masa Depan untuk Kelanjutan apa pun dengan argumen nol.

Ini adalah perubahan sederhana, upaya rendah yang tidak menambahkan biaya untuk masa depan tetapi memungkinkan penggunaan kembali kata kunci yang saat ini khusus untuk satu perpustakaan.

Namun kelanjutan tentu saja dapat diimplementasikan dengan praprosesor juga, yang akan kami lakukan. Sayangnya desugar hanya bisa menjadi penutupan, yang lebih mahal daripada kelanjutan yang tepat.

@aep Bagaimana kami memungkinkan untuk menggunakan kembali kata kunci ( async dan await )?

@Centril perbaikan cepat naif saya adalah menurunkan async ke Generator bukan ke Masa Depan. Itu akan memberikan waktu untuk membuat generator berguna untuk kelanjutan yang tepat daripada menjadi backend eksklusif untuk masa depan.

Ini seperti PR 10 baris mungkin. Tapi saya tidak punya kesabaran untuk melawan sarang lebah di atasnya, jadi saya hanya akan membangun sebuah preproc untuk desugar kata kunci yang berbeda.

Saya belum mengikuti hal-hal async jadi mohon maaf jika ini telah dibahas sebelumnya / di tempat lain tetapi apa rencana (implementasi) untuk mendukung async / menunggu di no_std ?

AFAICT implementasi saat ini menggunakan TLS untuk menyebarkan Waker tetapi tidak ada dukungan TLS (atau utas) di no_std / core . Saya mendengar dari @alexcrichton bahwa mungkin untuk menyingkirkan TLS jika / ketika Generator.resume mendapatkan dukungan untuk argumen.

Apakah rencana untuk memblokir stabilisasi async / menunggu pada dukungan no_std sedang dilaksanakan? Atau apakah kami yakin bahwa dukungan no_std dapat ditambahkan tanpa mengubah bagian mana pun yang akan distabilkan untuk dikirimkan std async / menunggu di stable?

@japaric poll sekarang mengambil konteks secara eksplisit. AFAIK, TLS seharusnya tidak lagi diperlukan.

https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll

Sunting: tidak relevan untuk async/menunggu, hanya untuk masa depan.

[...] yakin bahwa dukungan no_std dapat ditambahkan tanpa mengubah bagian apa pun yang akan distabilkan untuk dikirimkan std async / menunggu di stable?

Saya percaya begitu. Bagian yang relevan adalah fungsi dalam std::future , ini semua tersembunyi di balik fitur tambahan gen_future tidak stabil yang tidak akan pernah distabilkan. Transformasi async menggunakan set_task_waker untuk menyimpan waker ke TLS kemudian await! menggunakan poll_with_tls_waker untuk mengaksesnya. Jika generator mendapatkan dukungan argumen resume, maka transformasi async dapat meneruskan waker sebagai argumen resume dan await! dapat membacanya di luar argumen.

EDIT: Bahkan tanpa argumen generator, saya yakin ini juga bisa dilakukan dengan beberapa kode yang sedikit lebih rumit dalam transformasi async. Saya pribadi ingin melihat argumen generator ditambahkan untuk kasus penggunaan lain, tetapi saya cukup yakin bahwa menghapus persyaratan TLS dengan/tanpa mereka akan dimungkinkan.

@japaric Perahu yang sama. Bahkan jika seseorang membuat masa depan bekerja pada tertanam, itu sangat berisiko karena semuanya Tier3.

Saya menemukan peretasan jelek yang membutuhkan jauh lebih sedikit pekerjaan daripada memperbaiki async: menenun di Arcmelalui tumpukan Generator.

  1. lihat argumen "Poll" https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 ini adalah Arc
  2. mendaftarkan sesuatu dalam polling di Line 87
  3. hasil untuk menghasilkan titik kelanjutan pada baris 92
  4. panggil generator dari generator untuk membuat tumpukan level yang lebih tinggi di jalur 207
  5. akhirnya mengeksekusi seluruh tumpukan dengan melewatkan runtime di baris 215

Idealnya mereka hanya menurunkan async ke tumpukan penutupan "murni" daripada Future sehingga Anda tidak memerlukan asumsi runtime dan Anda kemudian dapat mendorong lingkungan yang tidak murni sebagai argumen di root.

Saya setengah jalan dalam menerapkan itu

https://twitter.com/arvidep/status/1067383652206690307

tapi agak sia-sia untuk pergi jauh-jauh jika saya satu-satunya yang menginginkannya.

Dan saya tidak bisa berhenti memikirkan apakah async/await tanpa TLS tanpa argumen generator dimungkinkan, jadi saya menerapkan no_std proc-macro berbasis async_block! / await! pasangan makro hanya menggunakan variabel lokal.

Ini pasti membutuhkan jaminan keamanan yang jauh lebih halus daripada solusi berbasis TLS saat ini atau solusi berbasis argumen generator (setidaknya ketika Anda hanya menganggap argumen generator yang mendasarinya adalah suara), tapi saya cukup yakin itu suara (selama tidak ada seorang pun menggunakan lubang kebersihan yang agak besar yang saya tidak dapat menemukan jalan keluarnya, ini tidak akan menjadi masalah untuk implementasi dalam kompiler karena dapat menggunakan idents gensym yang tidak dapat disebutkan namanya untuk berkomunikasi antara transformasi async dan menunggu makro).

Saya baru menyadari bahwa tidak disebutkan untuk memindahkan await! dari std ke core di OP, mungkin #56767 dapat ditambahkan ke daftar masalah yang harus diselesaikan sebelum stabilisasi untuk dilacak ini.

@Nemo157 Karena await! tidak diharapkan untuk distabilkan, itu bukan pemblokir.

@Centril Saya tidak tahu siapa yang memberi tahu Anda await! tidak diharapkan untuk distabilkan... :wink:

@cramertj Maksudnya versi makro bukan versi kata kunci yang saya percaya ...

@ crlf0710 bagaimana dengan versi async-block implisit/eksplisit ?

@crlf0710 saya juga melakukannya :)

@cramertj Tidakkah kami ingin menghapus makro karena saat ini ada peretasan buruk di kompiler yang memungkinkan keberadaan await dan await! ? Jika kita menstabilkan makro, kita tidak akan pernah bisa menghapusnya.

@stjepang Saya benar-benar tidak terlalu peduli ke segala arah tentang sintaks await! , selain dari preferensi umum untuk notasi postfix dan ketidaksukaan ambiguitas dan simbol yang tidak dapat diucapkan/tidak dapat Google. Sejauh yang saya ketahui, saran saat ini (dengan ? untuk memperjelas prioritas) adalah:

  • await!(x)? (apa yang kita miliki hari ini)
  • await x? ( await mengikat lebih ketat dari ? , masih notasi awalan, membutuhkan parens untuk metode rantai)
  • await {x}? (sama seperti di atas, tetapi sementara membutuhkan {} untuk disambiguasi)
  • await? x ( await mengikat kurang erat, masih notasi awalan, membutuhkan parens untuk metode rantai)
  • x.await? (tampak seperti akses lapangan)
  • x# / x~ /dll. (beberapa simbol)
  • x.await!()? (postfix-macro-style, @withoutboats dan saya pikir mungkin yang lain bukan penggemar postfix-makro karena mereka mengharapkan . mengizinkan pengiriman berbasis tipe, yang tidak untuk makro postfix )

Saya pikir rute terbaik untuk pengiriman adalah mendarat await!(x) , un-keyword-ify await , dan akhirnya suatu hari nanti menjual orang-orang pada kebaikan makro postfix, memungkinkan kita untuk menambahkan x.await!() . Pendapat orang lain berbeda ;)

Saya mengikuti masalah ini dengan sangat longgar, tetapi inilah pendapat saya:

Secara pribadi saya suka makro await! apa adanya dan seperti yang dijelaskan di sini: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Ini bukan sihir atau sintaks baru, hanya makro biasa. Lebih sedikit lebih banyak.

Kemudian lagi, saya juga lebih suka try! , karena Try masih belum stabil. Namun, await!(x)? adalah kompromi yang layak antara gula dan tindakan bernama yang jelas, dan saya pikir itu berfungsi dengan baik. Selain itu, ini berpotensi digantikan oleh beberapa makro lain di perpustakaan pihak ketiga untuk menangani fungsionalitas tambahan, seperti pelacakan debug.

Sementara itu async / yield adalah gula sintaksis "hanya" untuk generator. Itu mengingatkan saya pada hari-hari di mana JavaScript mendapatkan dukungan async/menunggu dan Anda memiliki proyek seperti Babel dan Regenerator yang mentranspilasikan kode async untuk menggunakan generator dan Promises/Futures untuk operasi async, pada dasarnya seperti yang kami lakukan.

Ingatlah bahwa pada akhirnya kita ingin async dan generator menjadi fitur yang berbeda, bahkan berpotensi dapat dikomposisi satu sama lain (menghasilkan Stream ). Membiarkan await! sebagai makro yang baru saja diturunkan ke yield bukanlah solusi permanen.

Meninggalkan menunggu! sebagai makro yang hanya menurunkan hasil bukanlah solusi permanen.

Itu tidak dapat dilihat pengguna secara permanen yang diturunkan ke yield , tetapi tentu saja dapat terus diimplementasikan seperti itu. Bahkan ketika Anda memiliki async + generators = Stream Anda masih dapat menggunakan misalnya yield Poll::Pending; vs. yield Poll::Ready(next_value) .

Ingatlah bahwa pada akhirnya kami ingin async dan generator menjadi fitur yang berbeda

Apakah async dan generator bukan fitur yang berbeda? Terkait, tentu saja, tetapi membandingkan ini lagi dengan bagaimana JavaScript melakukannya, saya selalu berpikir async akan dibangun di atas generator; bahwa satu-satunya perbedaan adalah fungsi async akan kembali dan menghasilkan Future s sebagai lawan dari nilai reguler apa pun. Seorang pelaksana akan diminta untuk mengevaluasi dan menunggu fungsi async untuk dieksekusi. Ditambah beberapa hal ekstra seumur hidup, saya tidak yakin.

Faktanya, saya pernah menulis perpustakaan tentang hal yang tepat ini, mengevaluasi secara rekursif fungsi async dan fungsi generator yang mengembalikan Janji/Masa Depan.

@cramertj Tidak dapat diimplementasikan seperti itu jika keduanya adalah "efek" yang berbeda. Ada beberapa diskusi seputar ini di sini: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202 . Kami tidak ingin yield Poll::Ready(next_value) , kami ingin yield next_value , dan memiliki await s di tempat lain dalam fungsi yang sama.

@rpjohnst

Kami tidak ingin menghasilkan Poll::Ready(next_value), kami ingin menghasilkan next_value, dan telah menunggu di tempat lain dalam fungsi yang sama.

Ya, tentu saja seperti itu bagi pengguna, tetapi dalam hal desugaring Anda hanya perlu membungkus yield s dalam Poll::Ready dan menambahkan Poll::Pending ke yield dihasilkan dari await! . Secara sintaksis untuk pengguna akhir mereka muncul sebagai fitur terpisah, tetapi mereka masih dapat berbagi implementasi di kompiler.

@cramertj Juga yang ini:

  • await? x

@novacrazy Ya, mereka adalah fitur yang berbeda, tetapi mereka harus dapat dikomposisi bersama.

Dan memang dalam JavaScript mereka dapat dikomposisi:

https://thenewstack.io/whats-coming-up-in-javascript-2018-async-generators-better-regex/

“Async generators dan iterator adalah apa yang Anda dapatkan ketika Anda menggabungkan fungsi async dan iterator sehingga seperti generator async yang dapat Anda tunggu atau fungsi async yang dapat Anda hasilkan,” jelasnya. Sebelumnya, ECMAScript memungkinkan Anda untuk menulis fungsi yang dapat Anda hasilkan atau tunggu, tetapi tidak keduanya. “Ini benar-benar nyaman untuk mengonsumsi aliran yang semakin menjadi bagian dari platform web, terutama dengan objek Ambil yang mengekspos aliran.”

Iterator async mirip dengan pola Observable, tetapi lebih fleksibel. “An Observable adalah model push; setelah Anda berlangganan, Anda mendapatkan ledakan dengan acara dan pemberitahuan dengan kecepatan penuh apakah Anda siap atau tidak, jadi Anda harus menerapkan strategi buffering atau pengambilan sampel untuk menangani cerewet, ”terlson menjelaskan. Iterator async adalah model push-pull — Anda meminta nilai dan itu dikirimkan kepada Anda — yang berfungsi lebih baik untuk hal-hal seperti jaringan IO primitif.

@Centril ok, dibuka #56974, apakah itu cukup benar untuk ditambahkan sebagai pertanyaan yang belum terselesaikan ke OP?


Saya benar-benar tidak ingin masuk ke bikeshed sintaks await lagi, tetapi saya harus menanggapi setidaknya satu poin:

Secara pribadi saya suka makro await! apa adanya dan seperti yang dijelaskan di sini: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Perhatikan bahwa saya juga mengatakan bahwa saya tidak percaya bahwa makro dapat tetap menjadi makro yang diimplementasikan perpustakaan (mengabaikan apakah itu akan terus muncul sebagai makro kepada pengguna atau tidak), untuk memperluas alasannya:

  1. Menyembunyikan implementasi yang mendasarinya, seperti yang dikatakan oleh salah satu masalah yang belum terselesaikan saat ini Anda dapat membuat generator dengan menggunakan || await!() .
  2. Mendukung generator async, seperti yang disebutkan @cramertj, ini memerlukan perbedaan antara yield s yang ditambahkan oleh await dan yield yang ditulis oleh pengguna. Ini _dapat_ dilakukan sebagai tahap pra-perluasan makro, _jika_ pengguna tidak pernah ingin yield di dalam makro, tetapi ada yield -konstruk dalam makro yang sangat berguna seperti yield_from! . Dengan batasan bahwa yield s dalam makro harus didukung, ini membutuhkan await! untuk menjadi makro bawaan setidaknya (jika bukan sintaks sebenarnya).
  3. Mendukung async fn pada no_std . Saya tahu dua cara untuk mengimplementasikan ini, kedua cara memerlukan async fn -created- Future dan await untuk membagikan pengidentifikasi tempat waker disimpan. Satu-satunya cara saya dapat melihat untuk memiliki pengidentifikasi yang aman secara higienis yang dibagikan di antara dua tempat ini jika keduanya diimplementasikan dalam kompiler.

Saya pikir ada sedikit kebingungan di sini-- tidak pernah ada niat bahwa await! secara publik dapat diperluas untuk membungkus panggilan ke yield . Masa depan apa pun untuk sintaks seperti makro await! akan bergantung pada implementasi yang tidak berbeda dengan implementasi yang didukung oleh kompiler saat ini compile_error! , assert! , format_args! dll. dan akan dapat mendesugar ke kode yang berbeda tergantung pada konteksnya.

Satu-satunya hal penting untuk dipahami di sini adalah bahwa tidak ada perbedaan semantik yang signifikan antara salah satu sintaks yang diusulkan-- mereka hanya sintaks permukaan.

Saya akan menulis alternatif untuk menyelesaikan sintaks await .

Pertama-tama saya menyukai ide yang menempatkan await sebagai operator postfix. Tetapi expression.await terlalu mirip dengan bidang, seperti yang telah ditunjukkan.

Jadi proposal saya adalah expression awaited . Kerugiannya di sini adalah awaited belum diawetkan sebagai kata kunci, tetapi lebih alami dalam bahasa Inggris dan belum ada ekspresi seperti itu (maksud saya, bentuk tata bahasa seperti expression [token] ) valid di Rust sekarang, jadi ini bisa dibenarkan.

Kemudian kita dapat menulis expression? awaited untuk menunggu Result<Future,_> , dan expression awaited? untuk menunggu Future<Item=Result<_,_>> .

@earthengine

Meskipun saya tidak menjual kata kunci awaited , saya pikir Anda menyukai sesuatu.

Wawasan utama di sini adalah: yield dan await seperti return dan ? .

return x mengembalikan nilai x , sedangkan x? membuka hasil x , kembali lebih awal jika Err .
yield x menghasilkan nilai x , sementara x awaited menunggu masa depan x , kembali lebih awal jika Pending .

Ada simetri yang bagus untuk itu. Mungkin await benar-benar harus menjadi operator postfix.

let x = x.do_something() await.do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Saya bukan penggemar sintaks yang ditunggu postfix karena alasan yang tepat @cramertj baru saja ditampilkan. Ini mengurangi keterbacaan keseluruhan, terutama untuk ekspresi panjang atau ekspresi berantai. Itu tidak memberikan rasa bersarang seperti await! / await . Itu tidak memiliki kesederhanaan ? , dan kami kehabisan simbol untuk digunakan untuk operator postfix...

Secara pribadi, saya masih mendukung await! untuk alasan yang saya jelaskan sebelumnya. Rasanya Rust-y dan tanpa basa-basi.

Ini mengurangi keterbacaan keseluruhan, terutama untuk ekspresi panjang atau ekspresi berantai.

Dalam standar Rustfmt, contohnya harus ditulis

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;

Saya hampir tidak bisa melihat bagaimana ini memengaruhi keterbacaan.

Saya suka postfix menunggu juga. Saya pikir menggunakan spasi akan menjadi tidak biasa dan cenderung merusak pengelompokan mental. Namun, saya berpikir bahwa .await!() akan memasangkan baik, dengan ? pas baik sebelum atau setelah, dan ! akan memungkinkan untuk interaksi kontrol aliran.

(Itu tidak memerlukan mekanisme makro postfix yang sepenuhnya umum; kompiler dapat menggunakan huruf khusus .await!() .)

Saya awalnya sangat tidak menyukai postfix await (tanpa . atau () ) karena terlihat sangat aneh-- orang yang berasal dari bahasa lain akan tertawa terbahak-bahak di biaya pasti. Itu biaya yang harus kita anggap serius. Namun, x await jelas bukan panggilan fungsi atau akses lapangan ( x.await / x.await() / await(x) semua memiliki masalah ini) dan ada lebih sedikit funky masalah prioritas. Sintaks ini akan dengan jelas menyelesaikan ? dan prioritas akses metode, misalnya foo await? dan foo? await keduanya memiliki prioritas yang jelas untuk memesan kepada saya, seperti halnya foo await?.x dan foo await?.y (tidak menyangkal bahwa mereka terlihat aneh, hanya berargumen bahwa prioritasnya jelas).

Saya juga berpikir bahwa

stream.for_each(async |item| {
    ...
}) await;

membaca lebih baik daripada

await!(stream.for_each(async |item| {
    ...
});

Secara keseluruhan, saya akan mendukung ini.

@joshtriplett RE .await!() kita harus berbicara secara terpisah-- Saya awalnya mendukung ini juga, tapi saya tidak berpikir kita harus mendaratkan ini jika kita juga tidak bisa mendapatkan makro postfix secara umum, dan saya pikir ada banyak penentangan terhadap mereka (dengan alasan yang cukup bagus, meskipun tidak menguntungkan), dan saya sangat ingin hal itu tidak menghalangi stabilisasi await .

Mengapa tidak keduanya?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Saya melihat daya tarik postfix lebih banyak sekarang, dan berbatasan dengan lebih menyukainya dalam beberapa skenario. Apalagi dengan cheat di atas yang sangat sederhana bahkan tidak perlu disediakan oleh Rust sendiri.

Jadi, +1 untuk postfix.

Saya pikir kita juga harus memiliki fungsi awalan, selain versi postfix.

Adapun spesifikasi sintaks postfix, saya tidak mencoba untuk mengatakan bahwa .await!() adalah satu-satunya sintaks postfix yang layak; Saya bukan penggemar postfix await dengan spasi utama.

Kelihatannya lumayan (meskipun masih tidak biasa) ketika Anda memformatnya dengan satu pernyataan per baris, tetapi jauh lebih tidak masuk akal jika Anda memformat pernyataan sederhana dalam satu baris.

Bagi mereka yang tidak menyukai operator kata kunci postfix, kita dapat mendefinisikan operator simbolik yang tepat untuk await .

Saat ini, kami kehabisan operator dalam karakter ASCII sederhana untuk operator postfix. Namun bagaimana?

let x = do_something()⌛.do_somthing_else()⌛;

Jika kita benar-benar membutuhkan ASCII polos, saya datang dengan (terinspirasi oleh bentuk di atas)

let x = do_something()><.do_somthing_else()><;

atau (bentuk serupa dalam posisi horizontal)

let x = do_something()>=<.do_somthing_else()>=<;

Ide lain, adalah membuat struct await menjadi braket.

let x = >do_something()<.>do_something_else()<;

Semua solusi ASCII tersebut, berbagi masalah yang sama yang <..> sudah terlalu sering digunakan dan kami memiliki masalah penguraian dengan < dan > . Namun, >< atau >=< mungkin lebih baik untuk ini karena tidak memerlukan ruang di dalam operator, dan tidak ada < s terbuka di posisi saat ini.


Bagi mereka yang tidak menyukai spasi di antaranya tetapi OK untuk operator kata kunci postfix, bagaimana kalau menggunakan tanda hubung:

let x = do_something()-await.do_something_else()-await;

Tentang memiliki banyak cara berbeda untuk menulis kode yang sama, saya pribadi tidak menyukainya. Alasan utama jauh lebih sulit bagi orang-orang yang baru untuk memiliki pemahaman yang tepat apa itu cara yang benar atau titik memilikinya. Alasan kedua adalah bahwa kita akan memiliki banyak proyek berbeda yang menggunakan sintaks yang berbeda dan akan lebih sulit untuk melompat di antara dan membacanya (khusus untuk pendatang baru di karat). Saya pikir sintaks yang berbeda harus diterapkan hanya jika benar-benar ada perbedaan dan memberikan beberapa keuntungan. Banyak kode gula hanya membuat lebih sulit untuk belajar dan bekerja dengan bahasa.

@goffrie Ya saya setuju bahwa kita seharusnya tidak memiliki banyak cara berbeda untuk melakukan hal yang sama. Namun saya hanya mengusulkan alternatif yang berbeda, masyarakat hanya perlu memilih satu. Oleh karena itu hal ini tidak terlalu menjadi perhatian.

Selanjutnya, dalam hal await! makro tidak ada cara untuk menghentikan pengguna menciptakan makro mereka sendiri untuk melakukannya secara berbeda, dan Rust dimaksudkan untuk mengaktifkan ini. Oleh karena itu "memiliki banyak cara berbeda untuk melakukan hal yang sama" tidak dapat dihindari.

Saya pikir makro bodoh sederhana yang saya tunjukkan menunjukkan bahwa apa pun yang kami lakukan, pengguna akan tetap melakukan apa pun yang mereka inginkan. Sebuah kata kunci, baik itu prefiks atau postfix, dapat dibuat menjadi makro prefiks seperti fungsi, atau mungkin menjadi makro seperti metode postfix, kapan pun itu ada. Bahkan jika kita memilih makro seperti fungsi atau metode untuk await , mereka dapat dibalikkan dengan makro lain . Ini benar-benar tidak masalah.

Oleh karena itu, kita harus fokus pada fleksibilitas dan format. Berikan solusi yang paling mudah mengisi semua kemungkinan itu.

Lebih jauh lagi, meskipun dalam waktu singkat ini saya telah melekat pada sintaks kata kunci postfix, await harus mencerminkan apa pun yang diputuskan untuk yield dengan generator, yang mungkin merupakan kata kunci awalan. Untuk pengguna yang menginginkan solusi postfix, makro seperti metode mungkin akan ada pada akhirnya.

Kesimpulan saya adalah bahwa kata kunci awalan await adalah sintaks default terbaik untuk saat ini, mungkin dengan peti biasa yang memberi pengguna makro await! seperti fungsi, dan di masa depan metode seperti metode postfix .await!() makro.

@novacrazy

Lebih jauh lagi, meskipun dalam waktu singkat ini saya telah melekat pada sintaks kata kunci postfix, await harus mencerminkan apa pun yang diputuskan untuk yield dengan generator, yang mungkin merupakan kata kunci awalan.

Ekspresi yield 42 berada pada tipe ! , sedangkan foo.await berada pada tipe T dimana foo: impl Future<Output = T> . @stjepang membuat analogi yang tepat dengan ? dan return sini. await tidak seperti yield .

Mengapa tidak keduanya?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Anda harus memberi nama makro dengan nama lain karena await harus tetap menjadi kata kunci yang sebenarnya.


Untuk berbagai alasan, saya menentang awalan await dan terlebih lagi bentuk blok await { ... } .

Pertama ada masalah prioritas dengan await expr? mana prioritas yang konsisten adalah await (expr?) tetapi Anda ingin (await expr)? . Sebagai solusi untuk masalah prioritas, beberapa orang menyarankan await? expr selain await expr . Ini memerlukan await? sebagai unit dan casing khusus; yang tampaknya tidak beralasan, pemborosan anggaran kompleksitas kami, dan indikasi bahwa await expr memiliki masalah serius.

Lebih penting lagi, kode Rust, dan khususnya pustaka standar sangat berpusat di sekitar kekuatan sintaks pemanggilan titik dan metode. Ketika await adalah awalan, ini mendorong pengguna untuk menemukan ikatan let sementara alih-alih hanya metode rantai. Inilah alasan mengapa ? adalah postfix, dan untuk alasan yang sama, await juga harus postfix.

Lebih buruk lagi adalah await { ... } . Sintaks ini, jika diformat secara konsisten menurut rustfmt , berubah menjadi:

    let x = await { // by analogy with `loop`
        foo.bar.baz.other_thing()
    };

Ini tidak akan ergonomis dan secara signifikan akan membengkakkan panjang vertikal fungsi.


Sebaliknya, saya pikir menunggu, seperti ? , harus postfix karena itu cocok dengan ekosistem Rust yang berpusat di sekitar metode chaining. Sejumlah sintaks postfix telah disebutkan; Saya akan membahas beberapa di antaranya:

  1. foo.await!() -- Ini adalah solusi makro postfix . Meskipun saya sangat mendukung makro postfix, saya setuju dengan @cramertj di https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040 bahwa kita tidak boleh melakukan ini kecuali kita juga berkomitmen pada postfix makro pada umumnya. Saya juga berpikir bahwa menggunakan makro postfix dengan cara ini memberikan perasaan yang agak non-kelas; kita harus menghindari membuat konstruksi bahasa menggunakan sintaks makro.

  2. foo await -- Ini tidak terlalu buruk, ini benar-benar berfungsi seperti operator postfix ( expr op ) tetapi saya merasa ada sesuatu yang hilang dengan pemformatan ini (yaitu rasanya "kosong"); Sebaliknya, expr? melampirkan ? langsung ke expr ; tidak ada ruang di sini. Ini membuat ? terlihat menarik secara visual.

  3. foo.await -- Ini telah dikritik karena terlihat seperti akses lapangan; dan itu benar. Namun kita harus ingat bahwa await adalah kata kunci dan dengan demikian sintaks akan disorot seperti itu. Jika Anda membaca kode Rust di IDE Anda atau yang setara di GitHub, await akan memiliki warna atau ketebalan yang berbeda dari foo . Dengan menggunakan kata kunci yang berbeda, kami dapat menunjukkan ini:

    let x = foo.match?;
    

    Biasanya, field juga merupakan kata benda sedangkan await adalah kata kerja.

    Meskipun ada faktor ejekan awal tentang foo.await , saya pikir itu harus diberikan pertimbangan serius sebagai sintaks yang menarik secara visual dan juga dapat dibaca.

    Sebagai bonus, menggunakan .await memberi Anda kekuatan titik dan pelengkapan otomatis titik yang biasanya ada di IDE (lihat halaman 56). Misalnya, Anda dapat menulis foo. dan jika foo kebetulan merupakan masa depan, await akan ditampilkan sebagai pilihan pertama. Ini memfasilitasi ergonomi dan produktivitas pengembang karena meraih titik adalah hal yang telah dilatih oleh banyak pengembang ke dalam memori otot.

    Dalam semua kemungkinan sintaks postfix, terlepas dari kritik tentang terlihat seperti akses lapangan, ini tetap menjadi sintaks favorit saya.

  4. foo# -- Ini menggunakan sigil # untuk menunggu di foo . Saya pikir mempertimbangkan sigil adalah ide yang bagus mengingat ? juga merupakan sigil dan karena itu membuat penantian menjadi ringan. Dikombinasikan dengan ? akan terlihat seperti foo#? -- yang terlihat OK. Namun, # tidak memiliki alasan khusus. Sebaliknya, itu hanya sigil yang masih tersedia.

  5. foo@ -- Sigil lainnya adalah @ . Ketika digabungkan dengan ? , kita mendapatkan foo@? . Satu pembenaran untuk sigil khusus ini adalah tampilannya a -ish ( @wait ).

  6. foo! -- Akhirnya, ada ! . Ketika digabungkan dengan ? , kita mendapatkan foo!? . Sayangnya, ini memiliki perasaan WTF tertentu. Namun, ! sepertinya memaksakan nilai, yang cocok dengan "menunggu". Ada satu kelemahan bahwa foo!() sudah merupakan pemanggilan makro yang sah, jadi menunggu dan memanggil fungsi perlu ditulis (foo)!() . Menggunakan foo! sebagai sintaks juga akan merampas kesempatan kita untuk memiliki makro kata kunci (misalnya foo! expr ).

Sigil tunggal lainnya adalah foo~ . Gelombang dapat dipahami sebagai "gema" atau "membutuhkan waktu". Namun itu tidak digunakan di mana pun dalam bahasa Rust.

Tilde ~ digunakan di masa lalu untuk jenis alokasi tumpukan: https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

Bisakah ? digunakan kembali? Atau apakah itu terlalu banyak sihir? Seperti apa tampilan impl Try for T: Future ?

@parasyte Ya saya ingat. Tapi tetap saja itu sudah lama berlalu.

@jethrogb tidak mungkin saya bisa melihat impl Try langsung bekerja, ? secara eksplisit return s hasil Try dari fungsi saat ini sementara await perlu yield .

Mungkin ? dapat dibuat khusus untuk melakukan sesuatu yang lain dalam konteks generator sehingga dapat yield atau return tergantung pada jenis ekspresi yang diterapkannya , tapi saya tidak yakin seberapa bisa dimengerti. Juga bagaimana itu berinteraksi dengan Future<Output=Result<...>> , apakah Anda harus let foo = bar()??; untuk keduanya melakukan "menunggu" dan kemudian mendapatkan Ok varian dari Result ( atau akankah ? dalam generator didasarkan pada sifat tristate yang dapat yield , return atau menyelesaikan ke nilai dengan satu aplikasi)?

Komentar tanda kurung terakhir itu benar-benar membuat saya berpikir itu bisa diterapkan, klik untuk melihat sketsa cepat
enum GenOp<T, U, E> { Break(T), Yield(U), Error(E) }

trait TryGen {
    type Ok;
    type Yield;
    type Error;

    fn into_result(self) -> GenOp<Self::Ok, Self::Yield, Self::Error>;
}
dengan `foo?` di dalam generator yang berkembang menjadi sesuatu seperti (walaupun ini memiliki masalah kepemilikan, dan perlu juga menumpuk-pin hasil `foo`)
loop {
    match TryGen::into_result(foo) {
        GenOp::Break(val) => break val,
        GenOp::Yield(val) => yield val,
        GenOp::Return(val) => return Try::from_error(val.into()),
    }
}

Sayangnya saya tidak melihat bagaimana menangani variabel konteks waker dalam skema seperti ini, mungkin jika ? dibuat khusus untuk async alih-alih generator, tetapi jika itu akan menjadi khusus -cased di sini alangkah baiknya jika dapat digunakan untuk kasus penggunaan generator lain.

Saya memiliki pemikiran yang sama tentang penggunaan kembali ? sebagai @jethrogb.

@Nemo157

tidak mungkin saya bisa melihat impl Try langsung bekerja, ? secara eksplisit return s hasil Try dari fungsi saat ini sementara menunggu perlu yield .

Mungkin saya melewatkan beberapa detail tentang ? dan sifat Try , tetapi di mana/mengapa itu eksplisit? Dan bukankah return dalam penutupan async pada dasarnya sama dengan yield , hanya transisi status yang berbeda?

Mungkin ? dapat dibuat khusus untuk melakukan sesuatu yang lain dalam konteks generator sehingga dapat yield atau return tergantung pada jenis ekspresi yang diterapkannya , tapi saya tidak yakin seberapa bisa dimengerti.

Saya tidak mengerti mengapa itu harus membingungkan. Jika Anda menganggap ? sebagai "lanjutkan atau menyimpang", maka tampaknya wajar, IMHO. Memang, mengubah sifat Try untuk menggunakan nama yang berbeda untuk jenis pengembalian terkait akan membantu.

Juga bagaimana itu berinteraksi dengan Future<Output=Result<...>> , apakah Anda harus let foo = bar()?? ;

Jika Anda ingin menunggu hasilnya dan juga keluar lebih awal pada hasil Kesalahan, maka itu akan menjadi ekspresi logis, ya. Saya tidak berpikir tri-negara bagian khusus TryGen akan diperlukan sama sekali.

Sayangnya saya tidak melihat bagaimana menangani variabel konteks waker dalam skema seperti ini, mungkin jika ? adalah casing khusus untuk async alih-alih generator, tetapi jika akan menjadi casing khusus di sini, alangkah baiknya jika dapat digunakan untuk kasus penggunaan generator lain.

Saya tidak mengerti bagian ini. Bisakah Anda menguraikannya?

@jethrogb @rolandsteiner Sebuah struct dapat mengimplementasikan Try dan Future . Dalam hal ini, yang mana yang harus dibuka ? ?

@jethrogb @rolandsteiner Sebuah struct dapat mengimplementasikan Try dan Future. Dalam hal ini, mana yang harus? membuka?

Tidak, tidak bisa karena selimut impl Coba untuk T: Masa Depan.

Mengapa tidak ada yang berbicara tentang konstruksi eksplisit dan implisit menunggu proposal? Ini sama dengan sinkronisasi io, hanya saja itu memblokir tugas alih-alih utas. saya bahkan akan mengatakan bahwa memblokir utas lebih invasif daripada memblokir tugas, jadi mengapa tidak ada sintaks "menunggu" khusus untuk pemblokiran utas-io?

tapi itu semua hanya bayangan sepeda, saya pikir kita harus puas dengan sintaks makro sederhana await!(my_future) setidaknya untuk saat ini

tapi itu semua hanya bayangan sepeda, saya pikir kita harus puas dengan sintaks makro sederhana await!(my_future) setidaknya untuk saat ini

Tidak, ini bukan "hanya" penumpahan sepeda seolah-olah itu adalah sesuatu yang biasa dan tidak penting. Apakah await ditulis awalan atau postfix pada dasarnya memengaruhi cara kode async ditulis wrt. metode chaining dan bagaimana rasanya dikomposisi. Stabilisasi pada await!(future) juga berarti bahwa await sebagai kata kunci dilepaskan yang membuat penggunaan await sebagai kata kunci di masa mendatang menjadi tidak mungkin. "Setidaknya untuk saat ini" menunjukkan bahwa kita dapat menemukan sintaks yang lebih baik nanti dan mengabaikan hutang teknis yang menyertainya. Saya menentang dengan sengaja memperkenalkan hutang untuk sintaks yang dimaksudkan untuk diganti nanti.

Menstabilkan pada menunggu! (masa depan) juga memerlukan menunggu sebagai kata kunci dilepaskan yang membuat penggunaan masa depan menunggu sebagai kata kunci tidak mungkin.

kita bisa menjadikannya kata kunci di zaman berikutnya, membutuhkan sintaks ident mentah untuk makro, seperti yang kita lakukan dengan try .

@rolandsteiner

Dan bukankah return dalam penutupan async pada dasarnya sama dengan yield , hanya transisi status yang berbeda?

yield tidak ada dalam penutupan asinkron, ini adalah operasi yang diperkenalkan selama penurunan dari sintaks async / await ke generators/ yield . Dalam sintaks generator saat ini yield sangat berbeda dengan return , jika ekspansi ? dilakukan sebelum generator berubah maka saya tidak tahu bagaimana ia akan tahu kapan harus memasukkan a return atau yield .

Jika Anda ingin menunggu hasilnya dan nalso keluar lebih awal pada hasil Error, maka itu akan menjadi ekspresi logis, ya.

Ini mungkin logis, tetapi bagi saya tampaknya merupakan kelemahan bahwa banyak (kebanyakan?) kasus di mana Anda menulis fungsi async akan diisi dengan ?? ganda untuk menangani kesalahan IO.

Sayangnya saya tidak melihat bagaimana menangani variabel konteks waker ...

Saya tidak mengerti bagian ini. Bisakah Anda menguraikannya?

Transformasi async mengambil variabel waker dalam fungsi Future::poll dihasilkan, ini kemudian perlu diteruskan ke operasi menunggu yang diubah. Saat ini ini ditangani dengan variabel TLS yang disediakan oleh std yang keduanya mengubah referensi, jika ? malah ditangani sebagai titik hasil ulang _di level generator_ maka transformasi async akan hilang untuk memasukkan referensi variabel ini.

Saya menulis posting blog tentang sintaks await menguraikan preferensi saya sejak dua bulan lalu. Namun, itu pada dasarnya mengasumsikan sintaks awalan, dan hanya mempertimbangkan masalah prioritas dari perspektif itu. Berikut adalah beberapa pemikiran tambahan sekarang:

  • Pendapat umum saya adalah bahwa Rust telah benar-benar meregangkan anggaran ketidaktahuannya. Akan ideal untuk sintaks async/await tingkat permukaan untuk menjadi akrab bagi seseorang yang berasal dari JavaScript atau Python atau C# mungkin. Akan ideal dari perspektif ini untuk menyimpang hanya dalam cara-cara kecil dari norma. Sintaks postfix bervariasi pada seberapa jauh divergensinya (misalnya foo await kurang divergensi daripada beberapa sigil seperti foo@ ), tetapi semuanya lebih divergen daripada prefiks yang menunggu.
  • Saya juga lebih suka menstabilkan sintaks yang tidak menggunakan ! . Setiap pengguna yang berurusan dengan async/menunggu akan bertanya-tanya mengapa menunggu adalah makro alih-alih konstruksi aliran kontrol normal, dan saya percaya cerita di sini pada dasarnya akan "baik kami tidak dapat menemukan sintaks yang baik jadi kami baru saja menyelesaikannya membuatnya terlihat seperti makro." Ini bukan jawaban yang meyakinkan. Saya tidak berpikir hubungan antara ! dan aliran kontrol benar-benar cukup untuk membenarkan sintaks ini: Saya percaya ! secara khusus berarti pengeluaran makro, yang sebenarnya tidak.
  • Saya agak meragukan manfaat dari postfix menunggu secara umum (tidak sepenuhnya, hanya semacam ). Saya merasa keseimbangannya sedikit berbeda dari ? , karena menunggu adalah operasi yang lebih mahal (Anda menghasilkan dalam satu lingkaran sampai siap, daripada hanya bercabang dan kembali sekali). Saya agak curiga dengan kode yang akan menunggu dua atau tiga kali dalam satu ekspresi; tampaknya baik bagi saya untuk mengatakan ini harus ditarik ke dalam ikatan let mereka sendiri. Jadi trade off try! vs ? tidak terlalu menarik bagi saya di sini. Tetapi juga, saya akan terbuka untuk sampel kode yang menurut orang benar-benar tidak boleh ditarik ke dalam let dan lebih jelas sebagai rantai metode.

Yang mengatakan, foo await adalah sintaks postfix paling layak yang pernah saya lihat sejauh ini:

  • Ini relatif akrab untuk sintaks postfix. Yang harus Anda pelajari adalah menunggu mengikuti ekspresi alih-alih sebelumnya di Rust, daripada sintaks yang berbeda secara signifikan.
  • Ini jelas menyelesaikan masalah prioritas bahwa semua ini telah tentang.
  • Fakta bahwa itu tidak bekerja dengan baik dengan metode chaining sepertinya hampir merupakan keuntungan bagi saya, daripada kerugiannya, untuk alasan yang saya singgung sebelumnya. Saya mungkin lebih terdorong jika kami memiliki beberapa aturan tata bahasa yang mencegah foo await.method() hanya karena saya benar-benar merasa metode ini (tidak masuk akal) diterapkan ke await , bukan foo (sedangkan yang menarik Saya tidak merasakannya dengan foo await? ).

Saya masih condong ke sintaks awalan, tapi saya pikir await adalah sintaks postfix pertama yang terasa seperti tembakan nyata bagi saya.

Sidenote: selalu memungkinkan untuk menggunakan parens untuk membuat prioritas lebih jelas:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Ini tidak benar-benar ideal, tetapi mengingat itu mencoba menjejalkan banyak hal ke dalam satu baris, saya pikir itu masuk akal.

Dan seperti yang disebutkan @earthengine sebelumnya, versi multi-baris sangat masuk akal (tidak ada parens tambahan):

let x = x.do_something() await
         .do_another_thing() await;

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Akan ideal untuk sintaks async/await tingkat permukaan untuk menjadi akrab bagi seseorang yang berasal dari JavaScript atau Python atau C# mungkin.

Dalam kasus try { .. } , kami memperhitungkan keakraban dengan bahasa lain. Namun, itu juga merupakan desain yang tepat dari POV konsistensi internal dengan Rust. Jadi dengan segala hormat untuk bahasa-bahasa lain, konsistensi internal di Rust tampaknya lebih penting dan saya tidak berpikir sintaks awalan cocok dengan Rust baik dalam hal prioritas atau bagaimana API terstruktur.

  • Saya juga lebih suka menstabilkan sintaks yang tidak menggunakan ! . Setiap pengguna yang berurusan dengan async/menunggu akan bertanya-tanya mengapa menunggu adalah makro alih-alih konstruksi aliran kontrol normal, dan saya percaya cerita di sini pada dasarnya akan "baik kami tidak dapat menemukan sintaks yang baik jadi kami baru saja menyelesaikannya membuatnya terlihat seperti makro." Ini bukan jawaban yang meyakinkan.

Saya setuju dengan sentimen ini, .await!() tidak akan terlihat cukup berkelas.

  • Saya agak meragukan manfaat dari menunggu postfix secara umum (tidak sepenuhnya, hanya _sort of_). Saya merasa keseimbangannya sedikit berbeda dari ? , karena menunggu adalah operasi yang lebih mahal (Anda menghasilkan dalam satu lingkaran sampai siap, daripada hanya bercabang dan kembali sekali).

Saya tidak melihat apa yang harus dilakukan kemahalan dengan mengekstraksi sesuatu ke dalam let binding. Rantai metode bisa dan terkadang mahal juga. Manfaat dari let binding adalah untuk a) memberikan potongan yang cukup besar nama yang masuk akal untuk meningkatkan keterbacaan, b) dapat merujuk ke nilai yang dihitung yang sama lebih dari sekali (misalnya dengan &x atau ketika suatu jenis dapat disalin).

Saya agak curiga dengan kode yang akan menunggu dua atau tiga kali dalam satu ekspresi; tampaknya baik bagi saya untuk mengatakan ini harus ditarik ke dalam ikatan let mereka sendiri.

Jika Anda merasa bahwa mereka harus ditarik ke dalam ikatan let mereka sendiri, Anda masih dapat membuat pilihan itu dengan postfix await :

let temporary = some_computation() await?;

Bagi mereka yang tidak setuju dan lebih memilih metode chaining, postfix await memberikan kemampuan untuk memilih. Saya juga berpikir bahwa postfix lebih baik mengikuti pembacaan kiri-ke-kanan dan urutan aliran data di sini, jadi meskipun Anda mengekstrak ke let binding, saya masih lebih suka postfix.

Saya juga tidak berpikir Anda perlu menunggu dua atau tiga kali agar postfix await berguna. Perhatikan misalnya (ini adalah hasil dari rustfmt ):

    let foo = alpha()
        .beta
        .some_other_stuff()
        .await?
        .even_more_stuff()
        .stuff_and_stuff();

Tetapi juga, saya akan terbuka untuk sampel kode yang menurut orang benar-benar tidak boleh ditarik ke dalam let dan lebih jelas sebagai rantai metode.

Sebagian besar kode fuchsia yang saya baca terasa tidak wajar ketika diekstraksi ke let binding dan dengan let binding = await!(...)?; .

  • Ini relatif akrab untuk sintaks postfix. Yang harus Anda pelajari adalah menunggu mengikuti ekspresi alih-alih sebelumnya di Rust, daripada sintaks yang berbeda secara signifikan.

Preferensi saya untuk foo.await sini terutama karena Anda mendapatkan pelengkapan otomatis yang bagus dan kekuatan titik. Rasanya juga tidak terlalu berbeda. Menulis foo.await.method() juga memperjelas bahwa .method() diterapkan ke foo.await . Jadi itu menyelesaikan kekhawatiran itu.

  • Ini jelas menyelesaikan masalah prioritas bahwa semua ini telah tentang.

Tidak, ini bukan hanya tentang prioritas. Rantai metode sama pentingnya.

  • Fakta bahwa itu tidak bekerja dengan baik dengan metode chaining sepertinya hampir merupakan keuntungan bagi saya, daripada kerugiannya, untuk alasan yang saya singgung sebelumnya.

Saya tidak yakin mengapa itu tidak bekerja dengan baik dengan metode chaining.

Saya mungkin lebih terdorong jika kami memiliki beberapa aturan tata bahasa yang mencegah foo await.method() hanya karena saya benar-benar merasa metode ini (secara tidak masuk akal) diterapkan ke await , bukan foo (sedangkan yang menarik Saya tidak merasakannya dengan foo await? ).

Sedangkan saya tidak akan dipaksa untuk pergi dengan foo await jika kita memperkenalkan potongan kertas desain yang disengaja dan mencegah metode chaining dengan sintaks await postfix.

Mengakui bahwa setiap opsi memiliki sisi negatifnya, dan salah satunya harus tetap dipilih... satu hal yang mengganggu saya tentang foo.await adalah, bahkan jika kita berasumsi bahwa itu tidak akan benar-benar disalahartikan bidang struct, masih terlihat seperti mengakses lapangan struct. Konotasi dari akses lapangan adalah bahwa tidak ada yang berdampak besar terjadi -- ini adalah salah satu operasi yang paling tidak efektif di Rust. Sementara menunggu sangat berdampak, salah satu operasi dengan efek samping yang paling (keduanya melakukan operasi I/O yang dibangun di Masa Depan dan memiliki efek aliran kontrol). Jadi ketika saya membaca foo.await.method() , otak saya menyuruh saya untuk melompati .await karena itu relatif tidak menarik, dan saya harus menggunakan perhatian dan upaya untuk mengesampingkan naluri itu secara manual.

masih _terlihat seperti_ mengakses bidang struct.

@glaebhoerl Anda membuat poin bagus; namun, apakah penyorotan sintaks tidak berdampak/tidak cukup pada tampilannya dan cara otak Anda memproses sesuatu? Setidaknya bagi saya warna dan keberanian sangat penting ketika membaca kode jadi saya tidak akan melewatkan .await yang memiliki warna berbeda dari yang lainnya.

Konotasi dari akses lapangan adalah bahwa tidak ada yang berdampak besar terjadi -- ini adalah salah satu operasi yang paling tidak efektif di Rust. Sementara menunggu sangat berdampak, salah satu operasi dengan efek samping yang paling (keduanya melakukan operasi I/O yang dibangun di Masa Depan dan memiliki efek aliran kontrol).

Saya sangat setuju dengan ini. await adalah operasi aliran kontrol seperti break atau return , dan harus eksplisit. Notasi postfix yang diusulkan terasa tidak wajar, seperti Python if : compare if c { e1 } else { e2 } to e1 if c else e2 . Melihat operator di akhir membuat Anda melakukan pengambilan ganda, terlepas dari penyorotan sintaks apa pun.

Saya juga tidak melihat bagaimana e.await lebih konsisten dengan sintaks Rust daripada await!(e) atau await e . Tidak ada kata kunci postfix lain, dan karena salah satu idenya adalah membuat huruf besar-kecil di parser, saya rasa itu bukan bukti konsistensi.

Ada juga masalah keakraban @withoutboats yang disebutkan. Kita dapat memilih sintaks yang aneh dan indah jika memiliki beberapa manfaat yang luar biasa. Apakah postfix await memilikinya?

apakah penyorotan sintaks tidak berdampak/tidak cukup pada tampilannya dan cara otak Anda memproses sesuatu?

(Pertanyaan yang bagus, saya yakin itu akan memiliki beberapa dampak, tapi sulit untuk menebak berapa banyak tanpa benar-benar mencoba (dan mengganti kata kunci yang berbeda hanya mendapat sejauh ini). Sementara kita pada subjek ... lama lalu saya menyebutkan bahwa saya pikir penyorotan sintaks harus menyoroti semua operator dengan efek aliran kontrol ( return , break , continue , ? ... dan sekarang await ) dalam beberapa warna ekstra-khas khusus, tetapi saya tidak bertanggung jawab atas penyorotan sintaks untuk apa pun dan tidak tahu apakah ada yang benar-benar melakukan ini.)

Saya sangat setuju dengan ini. await adalah operasi aliran kontrol seperti break atau return , dan harus eksplisit.

Kami setuju. Notasi foo.await , foo await , foo# , ... eksplisit . Tidak ada penantian implisit yang dilakukan.

Saya juga tidak melihat bagaimana e.await lebih konsisten dengan sintaks Rust daripada await!(e) atau await e .

Sintaks e.await per se tidak konsisten dengan sintaks Rust tetapi postfix umumnya lebih cocok dengan ? dan bagaimana Rust API terstruktur (metode lebih disukai daripada fungsi gratis).

Sintaks await e? , jika dikaitkan sebagai (await e)? sama sekali tidak konsisten dengan cara break dan return dikaitkan. await!(e) juga tidak konsisten karena kami tidak memiliki makro untuk aliran kontrol dan juga memiliki masalah yang sama seperti metode awalan lainnya.

Tidak ada kata kunci postfix lain, dan karena salah satu idenya adalah membuat huruf besar-kecil di parser, saya rasa itu bukan bukti konsistensi.

Saya tidak berpikir Anda benar-benar perlu mengubah libsyntax sama sekali untuk .await karena seharusnya sudah ditangani sebagai operasi lapangan. Logikanya lebih suka ditangani dalam penyelesaian atau HIR di mana Anda menerjemahkannya ke konstruksi khusus.

Kita dapat memilih sintaks yang aneh dan indah jika memiliki beberapa manfaat yang luar biasa. Apakah postfix await memilikinya?

Seperti yang disebutkan di atas, saya berpendapat itu karena metode chaining dan preferensi Rust untuk panggilan metode.

Saya tidak berpikir Anda benar-benar perlu mengubah libsyntax sama sekali untuk .await karena seharusnya sudah ditangani sebagai operasi lapangan.

Ini menyenangkan.
Jadi idenya adalah untuk menggunakan kembali pendekatan self / super ..., tetapi untuk bidang daripada untuk segmen jalur.

Ini secara efektif menjadikan await kata kunci segmen jalur (karena melewati resolusi), jadi Anda mungkin ingin melarang pengidentifikasi mentah untuk itu.

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

Tidak ada penantian implisit yang dilakukan.

Idenya muncul beberapa kali di utas ini (proposal "menunggu implisit").

kami tidak memiliki makro untuk aliran kontrol

Ada try! (yang memenuhi tujuannya dengan cukup baik) dan bisa dibilang select! sudah usang. Perhatikan bahwa await "lebih kuat" dari return , jadi tidak masuk akal untuk mengharapkannya lebih terlihat dalam kode daripada ? 's return .

Saya berpendapat itu karena metode chaining dan preferensi Rust untuk panggilan metode.

Ini juga memiliki preferensi (lebih terlihat) untuk operator aliran kontrol awalan.

Menunggu e? sintaks, jika dikaitkan sebagai (menunggu e)? benar-benar tidak konsisten dengan bagaimana istirahat dan kembali asosiasi.

Saya lebih suka await!(e)? , await { e }? atau bahkan mungkin { await e }? -- Saya rasa saya tidak pernah melihat yang terakhir dibahas, dan saya tidak yakin apakah itu berhasil.


Saya akui mungkin memiliki bias kiri-ke-kanan. _Catatan_

Pendapat saya tentang ini tampaknya berubah setiap kali saya melihat masalah ini, seolah-olah memainkan advokat Iblis untuk diri saya sendiri. Sebagian karena saya sudah terbiasa menulis masa depan dan mesin negara saya sendiri. Masa depan khusus dengan poll benar-benar normal.

Mungkin ini harus dipikirkan dengan cara lain.

Bagi saya, abstraksi tanpa biaya di Rust mengacu pada dua hal: tanpa biaya saat runtime, dan yang lebih penting secara mental tanpa biaya.

Saya dapat dengan mudah bernalar tentang sebagian besar abstraksi di Rust, termasuk futures, karena mereka hanyalah mesin negara.

Untuk tujuan ini, solusi sederhana harus ada yang memperkenalkan sedikit keajaiban kepada pengguna. Sigils terutama adalah ide yang buruk, karena mereka merasa magis yang tidak perlu. Ini termasuk bidang ajaib .await .

Mungkin solusi terbaik adalah yang termudah, makro await! .

Jadi dengan segala hormat untuk bahasa-bahasa lain, konsistensi internal di Rust tampaknya lebih penting dan saya tidak berpikir sintaks awalan cocok dengan Rust baik dalam hal prioritas atau bagaimana API terstruktur.

Saya tidak melihat bagaimana ...? await(foo)? / await { foo }? tampaknya baik-baik saja dalam hal prioritas operator dan bagaimana API terstruktur di Rust- kelemahannya adalah kata-kata dari parens dan (tergantung pada perspektif Anda) chaining, tidak melanggar preseden atau sedang membingungkan.

Ada try! (yang memenuhi tujuannya dengan cukup baik) dan bisa dibilang select! sudah usang.

Saya pikir kata operasi di sini sudah usang . Menggunakan try!(...) adalah kesalahan yang sulit pada Rust 2018. Ini adalah kesalahan yang sulit sekarang karena kami memperkenalkan sintaks yang lebih baik, kelas satu, dan postfix.

Perhatikan bahwa await "lebih kuat" daripada return , jadi tidak masuk akal untuk mengharapkannya lebih terlihat dalam kode daripada ? 's return .

Operator ? juga dapat menjadi efek samping (melalui implementasi lain selain Result ) dan melakukan aliran kontrol sehingga cukup "kuat" juga. Ketika dibahas, ? dituduh "menyembunyikan pengembalian" dan mudah diabaikan. Saya pikir prediksi itu benar-benar gagal menjadi kenyataan. Situasi kembali. await tampaknya sangat mirip dengan saya.

Ini juga memiliki preferensi (lebih terlihat) untuk operator aliran kontrol awalan.

Operator aliran kontrol awalan tersebut diketik pada tipe ! . Sementara itu, operator aliran kontrol lain ? yang mengambil konteks impl Try<Ok = T, ...> dan memberi Anda T adalah postfix.

Saya tidak melihat bagaimana ...? await(foo)? / await { foo }? tampaknya baik-baik saja dalam hal prioritas operator dan bagaimana API terstruktur di Rust-

Sintaks await(foo) tidak sama dengan await foo jika tanda kurung diperlukan untuk yang pertama dan bukan untuk yang terakhir. Yang pertama belum pernah terjadi sebelumnya, yang terakhir memiliki masalah prioritas wrt. ? seperti yang telah kita bahas di sini, di posting blog boat, dan di Discord. Sintaks await { foo } bermasalah karena alasan lain (lihat https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

kelemahannya adalah kata-kata dari parens dan (tergantung pada perspektif Anda) chaining, tidak melanggar preseden atau membingungkan.

Inilah yang saya maksud dengan "API terstruktur". Saya pikir metode dan metode chaining umum dan idiomatis di Rust. Sintaks awalan dan blok ditulis dengan buruk dengan itu dan dengan ? .

Saya mungkin minoritas dengan pendapat ini, dan jika demikian, abaikan saya:

Apakah adil untuk memindahkan diskusi awalan-vs-postfix ke utas Internal, dan kemudian kembali ke sini dengan hasilnya? Dengan begitu kita bisa menjaga masalah pelacakan untuk

@seanmonstar Ya, saya sangat bersimpati dengan keinginan untuk membatasi diskusi tentang masalah pelacakan dan memiliki masalah yang sebenarnya hanya pembaruan status. Ini adalah salah satu masalah yang saya harap dapat kita atasi dengan beberapa revisi tentang bagaimana kita mengelola proses dan masalah RFC secara umum. Untuk saat ini, saya telah membuka masalah baru di sini untuk kita gunakan untuk diskusi.

PENTING UNTUK SEMUA: diskusi sintaks await lanjut harus dilakukan di sini .

Mengunci sementara selama satu hari untuk memastikan bahwa diskusi mendatang tentang sintaks await terjadi pada masalah yang sesuai.

Pada Selasa, 15 Januari 2019 pukul 07:10:32-0800, Pauan menulis:

Sidenote: selalu memungkinkan untuk menggunakan parens untuk membuat prioritas lebih jelas:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Itu mengalahkan manfaat utama dari postfix menunggu: "tetap saja
menulis/membaca". Postfix menunggu, seperti postfix ? , memungkinkan aliran kontrol
untuk terus bergerak dari kiri ke kanan:

foo().await!()?.bar().await!()

Jika await! adalah awalan, atau kembali ketika try! adalah awalan, atau jika Anda memiliki
untuk tanda kurung, maka Anda harus melompat kembali ke sisi kiri
ekspresi saat menulis atau membacanya.

EDIT: Saya membaca komentar dari awal hingga akhir melalui email, dan tidak melihat komentar "pindahkan percakapan ke masalah lain" sampai setelah mengirim email ini.

Laporan status tunggu asinkron:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Saya ingin memposting pembaruan cepat tentang status async-wait
upaya. Versi singkatnya adalah kami berada di kandang untuk
semacam stabilisasi, tetapi tetap ada beberapa yang signifikan
pertanyaan untuk diatasi.

Mengumumkan kelompok kerja implementasi

Sebagai bagian dari dorongan ini, dengan senang hati saya umumkan bahwa kami telah membentuk
kelompok kerja implementasi async-menunggu . Kelompok kerja ini
adalah bagian dari seluruh upaya async-menunggu, tetapi berfokus pada
implementasi, dan merupakan bagian dari tim penyusun. Jika Anda ingin
membantu mendapatkan async-menunggu di garis finish, kami punya daftar masalah
di mana kami pasti membutuhkan bantuan (baca terus).

Jika Anda tertarik untuk mengambil bagian, kami memiliki "jam kerja"dijadwalkan untuk hari Selasa (lihat [kalender tim penyusun]) -- jika Anda
dapat muncul kemudian di [Zulip], itu akan ideal! (Tetapi jika tidak, masukkan saja
waktu.)

...

Kapan std::future::Future akan stabil? Apakah harus menunggu async menunggu? Saya pikir ini adalah desain yang sangat bagus dan ingin memulai porting kode ke sana. (Apakah ada shim untuk menggunakannya di kandang?)

@ry lihat masalah pelacakan baru untuknya: https://github.com/rust-lang/rust/issues/59113

Masalah kompiler lain untuk async/await: https://github.com/rust-lang/rust/issues/59245

Perhatikan juga bahwa https://github.com/rust-lang-nursery/futures-rs/issues/1199 di pos teratas dapat dicentang, karena sekarang sudah diperbaiki.

Sepertinya ada masalah dengan HRLB dan penutupan asinkron: https://github.com/rust-lang/rust/issues/59337. (Meskipun, membaca ulang RFC, itu tidak benar-benar menentukan bahwa penutupan async tunduk pada penangkapan argumen seumur hidup yang sama dengan yang dimiliki fungsi async).

Ya, penutupan async memiliki banyak masalah dan tidak boleh dimasukkan dalam putaran awal stabilisasi. Perilaku saat ini dapat ditiru dengan penutupan + blok async, dan di masa depan saya ingin melihat versi yang memungkinkan referensi upvar penutupan dari masa depan yang kembali.

Saya baru menyadari bahwa saat ini await!(fut) mengharuskan fut menjadi Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

Apakah itu yang diharapkan? Sepertinya tidak ada di RFC.

@Ekleog itu bukan await! memberikan kesalahan, await! secara konseptual menumpuk pin masa depan yang lewat untuk memungkinkan !Unpin futures digunakan ( contoh taman bermain cepat ). Kesalahan berasal dari batasan pada impl Future for Box<impl Future + Unpin> , yang mengharuskan masa depan menjadi Unpin untuk menghentikan Anda melakukan sesuatu seperti:

// where Foo: Future + !Unpin
let mut foo: Box<Foo> = ...;
Pin::new(&mut foo).poll(cx);
let mut foo = Box::new(*foo);
Pin::new(&mut foo).poll(cx);

Karena Box adalah Unpin dan memungkinkan pemindahan nilai darinya, Anda dapat melakukan polling masa depan sekali di satu lokasi heap, lalu memindahkan masa depan dari kotak dan memasukkannya ke lokasi heap baru dan polling itu lagi.

menunggu mungkin harus diberi casing khusus untuk mengizinkan Box<dyn Future> karena menghabiskan masa depan

Mungkin sifat IntoFuture harus dibangkitkan untuk await! ? Box<dyn Future> dapat mengimplementasikannya dengan mengonversi ke Pin<Box<dyn Future>> .

Ini dia bug saya berikutnya dengan async/await: sepertinya menggunakan tipe terkait ke parameter tipe dalam tipe pengembalian inferensi istirahat async fn : https://github.com/rust-lang/rust/ masalah/60414

Selain berpotensi menambahkan #60414 ke daftar posting teratas (tidak tahu apakah itu masih digunakan - mungkin lebih baik menunjuk ke label github?), Saya pikir "Resolusi rust-lang/ rfcs#2418” dapat dicentang, karena IIRC sifat Future baru-baru ini telah distabilkan.

Saya baru saja datang dari posting Reddit dan saya harus mengatakan bahwa saya tidak suka sintaks postfix sama sekali. Dan sepertinya mayoritas Reddit juga tidak menyukainya.

saya lebih suka menulis

let x = (await future)?

daripada menerima sintaks aneh itu.

Untuk chaining, saya dapat memfaktorkan ulang kode saya untuk menghindari lebih dari 1 await .

Juga, JavaScript di masa depan dapat melakukan ini ( proposal pipa pintar ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Jika prefix await diimplementasikan, bukan berarti await tidak dapat di-chain.

@KSXGitHub ini benar-benar bukan tempat untuk diskusi ini tetapi logika diuraikan di sini dan ada alasan yang sangat bagus untuk itu yang telah dipikirkan selama berbulan-bulan oleh banyak orang https://boats.gitlab.io/blog/post /menunggu-keputusan/

@KSXGitHub Sementara saya juga tidak menyukai sintaks final, telah dibahas secara luas di #57640, https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https://internals.rust- lang.org/t/a-final-proposal-for-await-syntax/ , dan di berbagai tempat lainnya. Banyak orang telah menyatakan preferensi mereka di sana, dan Anda tidak membawa argumen baru ke subjek ini.

Tolong jangan diskusikan keputusan desain di sini, ada utas untuk tujuan eksplisit ini

Jika Anda berencana untuk berkomentar di sana, harap diingat bahwa diskusi telah berlangsung cukup lama: pastikan Anda memiliki sesuatu yang substansial untuk dikatakan, dan pastikan itu belum pernah dikatakan sebelumnya di utas.

@withoutboats untuk pemahaman saya sintaks akhir sudah disepakati, mungkin sudah waktunya untuk menandainya sebagai Selesai? :merah:

Apakah niat untuk menstabilkan waktu untuk pemotongan beta berikutnya pada 4 Juli, atau akankah pemblokiran bug memerlukan siklus lain untuk diselesaikan? Ada banyak masalah terbuka di bawah tag A-async-await, meskipun saya tidak yakin berapa banyak di antaranya yang kritis.

Aha, abaikan itu, saya baru saja menemukan label AsyncAwait-Blocking .

Halo yang disana! Kapan kita harus mengharapkan rilis stabil dari fitur ini? Dan bagaimana saya bisa menggunakannya di build malam?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 berisi informasi tentang target tanggal rilis dan banyak lagi

Apakah ada rencana untuk mengimplementasikan Unpin untuk futures yang dihasilkan oleh async fn ?

Secara khusus saya bertanya-tanya apakah Unpin tidak tersedia secara otomatis karena kode Future yang dihasilkan itu sendiri, atau apakah karena kita dapat menggunakan referensi sebagai argumen

@DoumanAsh Saya kira jika async fn tidak pernah memiliki referensi diri aktif pada titik hasil maka Masa Depan yang dihasilkan dapat mengimplementasikan Unpin, mungkin?

Saya pikir itu perlu disertai dengan beberapa pesan kesalahan yang cukup membantu yang mengatakan "bukan Unpin karena _ini_ meminjam" + petunjuk "sebagai alternatif, Anda dapat mengemas masa depan ini"

PR stabilisasi di #63209 mencatat bahwa "Semua pemblokir sekarang ditutup." dan mendarat ke nightly pada 20 Agustus, oleh karena itu menuju pemotongan beta akhir pekan ini. Tampaknya perlu dicatat bahwa sejak 20 Agustus beberapa masalah pemblokiran baru telah diajukan (seperti yang dilacak oleh tag AsyncAwait-Blocking). Dua di antaranya (#63710, #64130) tampaknya bagus untuk dimiliki yang sebenarnya tidak akan menghambat stabilisasi, namun ada tiga masalah lain (#64391, #64433, #64477) yang tampaknya layak untuk didiskusikan. Tiga masalah terakhir ini terkait, semuanya muncul karena PR #64292, yang dengan sendirinya ditujukan untuk mengatasi masalah AsyncAwait-Blocking #63832. Sebuah PR, #64584, telah mendarat dalam upaya untuk mengatasi sebagian besar masalah, tetapi tiga masalah tetap terbuka untuk saat ini.

Sisi baiknya adalah bahwa tiga pemblokir terbuka yang serius tampaknya menyangkut kode yang harus dikompilasi, tetapi saat ini tidak dikompilasi. Dalam hal itu, ini akan kompatibel ke belakang untuk perbaikan tanah nanti tanpa menghalangi beta-isasi dan akhirnya stabilisasi async/menunggu. Namun, saya bertanya-tanya apakah ada orang dari tim lang yang berpikir bahwa apa pun di sini cukup mengkhawatirkan untuk menyarankan bahwa async/menunggu harus dipanggang setiap malam untuk siklus lain (yang, kedengarannya tidak menyenangkan, adalah inti dari jadwal rilis cepat Lagipula).

@bstrie Kami hanya menggunakan kembali "AsyncAwait-Blocking" karena tidak ada label yang lebih baik untuk mencatatnya sebagai "prioritas tinggi", mereka sebenarnya tidak memblokir. Sistem pelabelan harus segera kita perbaiki agar tidak membingungkan, cc @nikomatsakis.

... Tidak bagus ... kami melewatkan async-menunggu di 1,38 yang diharapkan. Harus menunggu 1,39, hanya karena beberapa "masalah" yang tidak masuk hitungan...

@earthengine Saya tidak berpikir itu penilaian situasi yang adil. Isu-isu yang muncul semuanya layak untuk ditanggapi dengan serius. Tidaklah baik untuk mendaratkan async hanya menunggu orang untuk kemudian mengalami masalah ini mencoba menggunakannya dalam praktik :)

Apakah halaman ini membantu?
0 / 5 - 0 peringkat