Rust: Masalah pelacakan untuk `ops::Try` (fitur`try_trait`)

Dibuat pada 31 Mei 2017  ·  99Komentar  ·  Sumber: rust-lang/rust

Sifat Try dari https://github.com/rust-lang/rfcs/pull/1859; diimplementasikan dalam PR https://github.com/rust-lang/rust/pull/42275.

Pisahkan dari https://github.com/rust-lang/rust/issues/31436 untuk kejelasan (per https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [ ] Menstabilkan ini akan memungkinkan orang untuk mengimplementasikan Iterator::try_fold

    • [ ] Sebagai bagian dari stabilisasi, buka kembali PR #62606 untuk mendokumentasikan penerapan try_fold untuk iterator

    • [ ] Pastikan bahwa implementasi default dari hal-hal lain memiliki DAG jangka panjang yang diinginkan, karena mengubahnya pada dasarnya tidak mungkin nanti. (Secara khusus, akan lebih baik jika fold diimplementasikan dalam try_fold , sehingga keduanya tidak perlu diganti.)

A-error-handling B-RFC-implemented B-unstable C-tracking-issue Libs-Tracked T-lang T-libs

Komentar yang paling membantu

Apa status fitur ini saat ini?

Semua 99 komentar

Beberapa potong bikeshedding:

  • Apakah kita memiliki motivasi tertentu untuk memanggil tipe terkait Error alih-alih Err ? Menyebutnya Err akan membuatnya sejajar dengan Result : yang lain sudah disebut Ok .

  • Apakah kita memiliki motivasi khusus untuk memiliki metode from_error dan from_ok yang terpisah, alih-alih satu from_result yang akan lebih simetris dengan into_result yang merupakan metode lain setengah dari sifat?

( versi terbaru dari tautan taman bermain dari RFC )

@glaebhoerl

  • Error vs Err dibahas terkait dengan TryFrom di https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 dan https:// github.com/rust-lang/rust/pull/40281; Saya berasumsi nama itu dipilih di sini untuk alasan yang sama.
  • Saya percaya mereka terpisah karena mereka memiliki kegunaan yang berbeda, dan saya berharap jarang ada seseorang yang benar-benar memiliki Result yang mereka coba ubah menjadi T:Try . Saya lebih suka Try::from_ok dan Try::from_error untuk selalu memanggil Try::from_result(Ok( dan Try::from_result(Err( , dan saya senang hanya menerapkan dua metode untuk menulis kecocokan. Mungkin itu karena saya menganggap into_result bukan sebagai Into<Result> , tetapi sebagai "apakah itu lulus atau gagal?", dengan tipe spesifik menjadi Result sebagai detail implementasi yang tidak penting. (Saya tidak ingin menyarankan atau membuka kembali "seharusnya ada tipe baru untuk nilai produksi vs pengembalian awal.) Dan untuk dokumentasi, saya suka bahwa from_error berbicara tentang ? (atau akhirnya throw ), sementara from_ok berbicara tentang success-wrapping (#41414), daripada memiliki keduanya dalam metode yang sama.

Saya tidak yakin apakah ini forum yang benar untuk komentar ini, mohon arahkan saya jika tidak : smiley:. Mungkin ini seharusnya ada di https://github.com/rust-lang/rfcs/pull/1859 dan saya melewatkan periode komentar; ups!


Saya bertanya-tanya apakah ada kasus untuk memisahkan sifat Try atau menghapus metode into_result ; bagi saya saat ini Try agak seperti jumlah sifat FromResult (berisi from_error dan from_ok ) dan IntoResult (berisi into_result ).

FromResult memungkinkan exit awal yang sangat ergonomis dengan operator ? , yang menurut saya merupakan kasus penggunaan yang mematikan untuk fitur tersebut. Saya pikir IntoResult sudah dapat diimplementasikan dengan rapi dengan metode per kasus penggunaan atau sebagai Into<Result> ; apakah saya melewatkan beberapa contoh yang berguna?

Mengikuti sifat Try RFC , expr? dapat diubah sebagai:

match expr { // Removed `Try::into_result()` here.
    Ok(v) => v,
    Err(e) => return Try::from_error(From::from(e)),
}

Contoh motivasi yang saya pertimbangkan adalah Future dan Option .

Masa depan

Kita dapat mengimplementasikan FromResult secara alami untuk struct FutureResult: Future sebagai:

impl<T, E> FromResult for FutureResult {
    type Ok = T;
    type Error = E;
    fn from_error(v: Self::Error) -> Self {
        future::err(v)
    }
    fn from_ok(v: Self::Ok) -> Self {
        future::ok(v)
    }
}

Dengan asumsi implementasi yang masuk akal untuk ? yang akan kembali dari fungsi impl Future ketika diterapkan ke Result::Err maka saya dapat menulis:

fn async_stuff() -> impl Future<V, E> {
    let t = fetch_t();
    t.and_then(|t_val| {
        let u: Result<U, E> = calc(t_val);
        async_2(u?)
    })
}
fn fetch_t() -> impl Future<T, E> {}
fn calc(t: T) -> Result<U,E> {}

Itulah tepatnya yang saya coba terapkan sebelumnya hari ini dan Try berhasil! Tetapi jika kami mencoba mengimplementasikan Try untuk Future mungkin tidak ada pilihan kanonik untuk into_result ; mungkin berguna untuk panik, memblokir, atau melakukan polling sekali, tetapi tidak satu pun dari ini yang tampaknya berguna secara universal. Jika tidak ada into_result pada Try Saya dapat menerapkan exit awal seperti di atas, dan jika saya perlu mengonversi Future menjadi Result (dan kemudian ke any Try ) Saya dapat mengonversinya dengan metode yang sesuai (panggil metode wait untuk memblokir, panggil beberapa poll_once() -> Result<T,E> , dll.).

Pilihan

Option adalah kasus serupa. Kami menerapkan from_ok , from_err secara alami seperti pada Try sifat RFC , dan dapat mengonversi Option<T> menjadi Result<T, Missing> hanya dengan o.ok_or(Missing) atau metode praktis ok_or_missing .


Semoga membantu!

Saya mungkin sangat terlambat untuk semua ini, tetapi saya baru sadar akhir pekan ini bahwa ? memiliki semantik yang agak alami jika Anda ingin melakukan hubungan pendek pada _success_.

fn fun() -> SearchResult<Socks> {
    search_drawer()?;
    search_wardrobe()
}

Namun dalam kasus ini, penamaan metode sifat Try tidak cocok.

Namun, itu akan memperluas arti dari operator ? .

Dalam membuat prototipe try_fold untuk rayon , saya mendapati diri saya menginginkan sesuatu seperti Try::is_error(&self) untuk metode Consumer::full() . (Rayon memiliki ini karena konsumen pada dasarnya adalah iterator gaya push, tetapi kami masih ingin membuat kesalahan hubung singkat.) Saya malah harus menyimpan nilai antara sebagai Result<T::Ok, T::Err> sehingga saya dapat memanggil Result::is_err() .

Dari sana saya juga berharap Try::from_result untuk kembali ke T di akhir, tetapi match memetakan ke T::from_ok dan T::from_error tidak _terlalu_ buruk.

Sifat Try dapat menyediakan metode from_result untuk ergonomi jika from_ok dan from_error diperlukan. Atau sebaliknya. Dari contoh yang diberikan saya melihat kasus untuk menawarkan keduanya.

Karena PR #42275 telah digabungkan, apakah itu berarti masalah ini telah teratasi?

@skade Perhatikan bahwa suatu tipe dapat menentukan yang mana saja sebagai hubungan arus pendek, seperti yang dilakukan LoopState untuk beberapa internal Iterator . Mungkin akan lebih wajar jika Try berbicara tentang "melanjutkan atau menghancurkan" daripada sukses atau gagal.

@cuviper Versi yang akhirnya saya butuhkan adalah tipe Ok atau Error - tipe asli. Saya berharap bahwa destructure-and-rebuild adalah hal yang cukup umum sehingga akan dioptimalkan dengan baik dan banyak metode khusus pada sifat tersebut tidak akan diperlukan.

@ErichDonGubler Ini adalah masalah pelacakan, jadi tidak diselesaikan sampai kode yang sesuai stabil.

Laporan pengalaman:

Saya sedikit frustrasi mencoba menerapkan sifat ini. Beberapa kali sekarang saya tergoda untuk mendefinisikan varian saya sendiri pada Result untuk alasan apa pun, tetapi setiap kali saya akhirnya hanya menggunakan Result pada akhirnya, sebagian besar karena menerapkan Try terlalu mengganggu. Saya tidak sepenuhnya yakin apakah ini hal yang baik atau tidak!

Contoh:

Di pemecah baru untuk VM Kapur, saya ingin memiliki enum yang menunjukkan hasil pemecahan "untai". Ini memiliki empat kemungkinan:

enum StrandFail<T> {
    Success(T),
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Saya ingin ? , ketika diterapkan ke enum ini, untuk membuka "sukses" tetapi menyebarkan semua kegagalan lainnya ke atas. Namun, untuk menerapkan sifat Try , saya harus mendefinisikan jenis "sisa" yang merangkum hanya kasus kesalahan:

enum StrandFail {
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

Tapi begitu saya mendapatkan tipe ini, maka saya mungkin juga membuat StrandResult<T> sebagai alias:

type StrandResult<T> = Result<T, StrandFail>;

dan inilah yang saya lakukan.

Sekarang, ini tidak selalu tampak lebih buruk daripada memiliki satu enum -- tetapi rasanya agak aneh. Biasanya ketika saya menulis dokumentasi untuk suatu fungsi, misalnya, saya tidak "mengelompokkan" hasil dengan "ok" dan "kesalahan", melainkan berbicara tentang berbagai kemungkinan sebagai "sama dengan". Sebagai contoh:

    /// Invoked when a strand represents an **answer**. This means
    /// that the strand has no subgoals left. There are two possibilities:
    ///
    /// - the strand may represent an answer we have already found; in
    ///   that case, we can return `StrandFail::NoSolution`, as this
    ///   strand led nowhere of interest.
    /// - the strand may represent a new answer, in which case it is
    ///   added to the table and `Ok` is returned.

Perhatikan bahwa saya tidak mengatakan "kami mengembalikan Err(StrandFail::NoSolution) . Ini karena Err hanya terasa seperti artefak yang mengganggu ini yang harus saya tambahkan.

(Di sisi lain, definisi saat ini akan membantu pembaca untuk mengetahui apa perilaku ? tanpa berkonsultasi dengan impl Try .)

Saya kira hasil ini tidak terlalu mengejutkan: impl Try memaksa Anda untuk menggunakan ? pada hal-hal yang isomorfik dengan Result . Keseragaman ini bukan kebetulan, tetapi akibatnya, penggunaan ? menjadi menjengkelkan untuk hal-hal yang pada dasarnya tidak "hanya Result ". (Dalam hal ini, pada dasarnya masalah yang sama yang memunculkan NoneError -- kebutuhan untuk mendefinisikan tipe secara artifisial untuk mewakili "kegagalan" dari Option .)

Saya juga mencatat bahwa, dalam kasus StrandFail , saya tidak terlalu menginginkan konversi From::from yang didapat hasil biasa, meskipun itu tidak merugikan saya.

Apakah tipe Try::Error pernah terpapar / digunakan oleh pengguna secara langsung? Atau hanya diperlukan sebagai bagian dari desugaring ? ? Jika yang terakhir, saya tidak melihat masalah nyata dengan hanya mendefinisikannya "secara struktural" - type Error = Option<Option<(Strand, Minimums)>> atau apa pun. (Harus mencari tahu padanan struktural dari "kegagalan setengah" dari definisi enum tidak bagus, tetapi tampaknya tidak terlalu mengganggu daripada harus mengubah seluruh API yang menghadap publik.)

Saya juga tidak mengikuti. Saya telah berhasil menerapkan Coba untuk tipe yang memiliki representasi kesalahan internal dan rasanya wajar memiliki Ok dan Error menjadi tipe yang sama. Anda dapat melihat implementasinya di sini: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308

Ternyata ada pola yang cukup sederhana untuk membuat karya ini.

impl std::ops::Try for Value {
    type Ok = Self;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { v }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.is_ok() { Ok(val) } else { Err(val) }
    }
}

Jika Anda ingin membuka bungkus kesuksesan, sesuatu seperti ini akan berfungsi:

impl std::ops::Try for StrandFail<T> {
    type Ok = T;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            StrandFail::Success(v) => Ok(v),
            other => Err(other),
        }
    }
}

Mendefinisikan tipe struktural dimungkinkan tetapi terasa sangat menjengkelkan. Saya setuju saya bisa menggunakan Self . Rasanya agak aneh bagi saya bahwa Anda kemudian dapat menggunakan ? untuk mengonversi dari StrandResult ke Result<_, StrandResult> dll.

Laporan pengalaman yang luar biasa, @nikomatsakis! Saya juga tidak puas dengan Try (dari arah lain).

TL/DR : Saya pikir FoldWhile benar, dan kita harus menggandakan interpretasi Break -vs- Continue dari ? alih-alih berbicara tentang itu dalam hal kesalahan.

Lebih lama:

Saya terus menggunakan Err untuk sesuatu yang lebih dekat dengan "sukses" daripada "kesalahan" karena ? sangat nyaman.

  • Saat menggunakan try_fold untuk mengimplementasikan position (dan banyak metode iterator lainnya), saya menjadi sangat bingung saat menggunakan Result (otak saya tidak suka find menemukan hal yang menjadi Err ) yang saya buat sendiri enum dengan Break dan Continue varian sebagai gantinya.

  • Saat menulis tree_fold1 di itertools , saya berakhir dengan match yang aneh None adalah "kesalahan" dari Iterator::next() , tetapi pada saat yang sama "sukses" dari fold , karena Anda harus sampai akhir harus dilakukan. Dan sangat nyaman untuk menggunakan ? (baik, try! dalam kode itu karena perlu dikompilasi pada Rust 1.12) untuk menangani penyebaran kondisi akhir.

Jadi jika tidak ada yang lain, saya pikir deskripsi yang saya tulis untuk Try salah, dan tidak boleh berbicara tentang "dikotomi keberhasilan/kegagalan", melainkan abstrak dari "kesalahan".

Hal lain yang saya pikirkan adalah bahwa kita harus mempertimbangkan beberapa impls yang sedikit gila untuk Try . Misalnya, Ordering: Try<Ok = (), Error = GreaterOrLess> , dikombinasikan dengan "coba fungsi", dapat memungkinkan cmp untuk struct Foo<T, U> { a: T, b: U } menjadi

fn cmp(&self, other: &self) -> Ordering try {
    self.a.cmp(&other.a)?;
    self.b.cmp(&other.b)?;
}

Saya belum tahu apakah itu jenis gila yang baik atau jenis yang buruk :laughing: Ini pasti ada keanggunannya, karena begitu hal-hal berbeda Anda tahu mereka berbeda. Dan mencoba untuk menetapkan "sukses" atau "kesalahan" di kedua sisi itu sepertinya tidak cocok.

Saya juga mencatat bahwa itu adalah contoh lain di mana "konversi kesalahan" tidak membantu. Saya juga tidak pernah menggunakannya pada contoh "I want ? but it's not a error" di atas. Dan itu pasti diketahui menyebabkan kesedihan kesimpulan, jadi saya ingin tahu apakah itu hal yang seharusnya dibatasi hanya Result (atau berpotensi dihapus demi .map_err(Into::into) , tapi itu mungkin tidak layak).

Sunting: Oh, dan semua itu membuat saya bertanya-tanya apakah mungkin jawaban untuk "Saya tetap menggunakan Hasil untuk kesalahan saya alih-alih menerapkan Try untuk jenis saya sendiri" adalah "bagus, itu yang diharapkan".

Sunting 2: Ini tidak seperti https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 di atas

Sunting 3: Sepertinya varian Continue juga diusulkan di https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250

Dua sen saya:

Saya suka saran @fluffysquirrels untuk membagi sifat menjadi dua sifat. Satu untuk mengonversi ke hasil, dan satu lagi untuk mengonversi dari hasil. Tapi saya pikir kita harus menyimpan into_result atau yang setara sebagai bagian dari desugaring. Dan saya pikir pada titik ini kita harus sejak menggunakan Option sebagai Try telah stabil.

Saya juga menyukai ide @scottmcm untuk menggunakan nama yang menyarankan break/continue daripada error/ok.

Untuk memasukkan kode spesifik di sini, saya suka bagaimana ini berbunyi:

https://github.com/rust-lang/rust/blob/ab8b961677ac5c74762dcea955aa0ff4d7fe4915/src/libcore/iter/iterator.rs#L1738 -L1746

Ini tentu saja tidak sebagus loop lurus, tetapi dengan "coba metode" itu akan menjadi dekat:

self.try_for_each(move |x| try { 
    if predicate(&x) { return LoopState::Break(x) } 
}).break_value() 

Sebagai perbandingan, saya menemukan versi "kesalahan kosakata" benar-benar menyesatkan:

self.try_for_each(move |x| { 
    if predicate(&x) { Err(x) } 
    else { Ok(()) } 
}).err() 

Bisakah kita mengimplementasikan Tampilan untuk NoneError? Itu akan memungkinkan peti kegagalan untuk secara otomatis mendapatkan From<NoneError> for failure::Error . Lihat https://github.com/rust-lang-nursery/failure/issues/61
Seharusnya perubahan 3 baris, tetapi saya tidak yakin tentang proses RFC dan semacamnya.

@cowang4 Saya ingin mencoba menghindari memungkinkan pencampuran lagi Hasil-dan-Pilihan sekarang, karena jenisnya sebagian besar tidak stabil untuk menjaga opsi kami tetap terbuka di sana. Saya tidak akan terkejut jika kami akhirnya mengubah desain Try dan desugar menjadi sesuatu yang tidak membutuhkan NoneError ...

@scottmcm Oke. Saya mengerti maksud Anda. Saya akhirnya ingin cara yang bersih untuk mengatur pola pengembalian Err ketika fungsi perpustakaan mengembalikan None. Mungkin Anda tahu satu selain Try ? Contoh:

fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    if let Some(filestem) = pb.file_stem() {
        if let Some(filestr) = filestem.to_str() {
            return Ok(MyStruct {
                filename: filestr.to_string()
            });
        }
     }
    Err(_)
}

Setelah saya menemukan fitur eksperimental ini dan peti failure , saya secara alami tertarik pada:

use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    Ok({
        title: pb.file_stem?.to_str()?.to_string()
    })
}

_almost_ mana yang berfungsi, kecuali kurangnya impl Display for NoneError seperti yang saya sebutkan sebelumnya.
Tetapi, jika ini bukan sintaks yang ingin kita gunakan, mungkin ada fungsi/makro lain yang menyederhanakan polanya:

if option.is_none() {
    return Err(_);
}

@cowang4 Saya percaya itu akan berhasil jika Anda menerapkan From<NoneError> untuk failure::Error , yang menggunakan tipe Anda sendiri yang mengimplementasikan Display .

Namun, mungkin praktik yang lebih baik untuk menggunakan opt.ok_or(_)? sehingga Anda dapat secara eksplisit mengatakan apa kesalahannya jika Opsinya Tidak Ada. Dalam contoh Anda, misalnya, Anda mungkin menginginkan kesalahan yang berbeda jika pb.file_stem adalah Tidak Ada daripada jika to_str() mengembalikan Tidak Ada.

@tmccombs Saya mencoba membuat jenis kesalahan saya sendiri, tetapi saya pasti salah melakukannya. Itu seperti ini:

#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;

impl From<std::option::NoneError> for SiteError {
    fn from(_err: std::option::NoneError) -> Self {
        SiteError
    }
}

fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
    let title: String = piece
        .file_stem()?
        .to_str()?
        .to_string();
    Ok(Piece {
        title: title,
        url: piece
            .strip_prefix(cur_dir)?
            .to_str()
            .ok_or(err_msg("tostr"))?
            .to_string(),
    })
}

Dan kemudian saya mencoba menggunakan jenis kesalahan saya ...

error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
    | |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
197 | |             .to_str()
198 | |             .ok_or(err_msg("tostr"))?
    | |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

Oke, saya baru menyadari bahwa ia ingin tahu cara mengonversi dari jenis kesalahan lain ke kesalahan saya, yang dapat saya tulis secara umum:

impl<E: failure::Fail> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Tidak...

error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
   --> src/main.rs:183:1
    |
183 | impl<E: failure::Fail> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> std::convert::From<T> for T;

Oke, bagaimana dengan std::error::Error ?

impl<E: std::error::Error> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

Itu juga tidak berhasil. Sebagian karena bertentangan dengan From<NoneError>

error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
   --> src/main.rs:181:1
    |
175 | impl From<std::option::NoneError> for SiteError {
    | ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
    |
    = note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions

Yang aneh karena saya pikir NoneError tidak mengimplementasikan std::error::Error . Ketika saya mengomentari impl From<NoneError> non-generik saya, saya mendapatkan:

error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
   --> src/main.rs:189:25
    |
189 |       let title: String = piece
    |  _________________________^
190 | |         .file_stem()?
    | |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
    |
    = note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
    = note: required by `std::convert::From::from`

Apakah saya harus menulis semua From s secara manual. Saya berpikir bahwa peti kegagalan seharusnya menurunkannya?

Mungkin saya harus tetap dengan option.ok_or()

Apakah saya harus menulis semua Froms secara manual. Saya berpikir bahwa peti kegagalan seharusnya menurunkannya?

Saya tidak berpikir peti kegagalan melakukan itu. Tapi aku bisa saja salah.

Oke, jadi saya memeriksa kembali peti kegagalan, dan jika saya membaca dokumentasi dan versi yang berbeda dengan benar, itu dirancang untuk selalu menggunakan failure::Error sebagai jenis Kesalahan di Result , lihat di sini . Dan, itu menerapkan sifat selimut impl Fail untuk sebagian besar jenis Kesalahan:

impl<E: StdError + Send + Sync + 'static> Fail for E {}

https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218

Dan kemudian impl From sehingga dapat Try / ? kesalahan lain (seperti yang dari std) ke dalam tipe failure::Error menyeluruh.
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

Tapi, hanya saja karena kesalahan yang relevan dengan masalah karat ini, NoneError , bersifat eksperimental, belum dapat dikonversi secara otomatis, karena tidak menerapkan sifat Display . Dan, kami tidak menginginkannya, karena itu mengaburkan batas antara Option s dan Result s lainnya. Semuanya mungkin akan dikerjakan kembali dan diselesaikan pada akhirnya, tetapi untuk saat ini, saya akan tetap berpegang pada teknik penghilangan gula yang telah saya pelajari.

Terima kasih semua orang untuk membantu. Saya perlahan-lahan belajar Rust! :senyum:

Saya akan tetap berpegang pada teknik penghilangan gula yang telah saya pelajari.

:+1: Sejujurnya, saya pikir .ok_or(...)? akan tetap jalan (atau .ok_or_else(|| ...)? , tentu saja). Bahkan jika NoneError _had_ a Display impl, apa yang akan dikatakannya? "Sesuatu tidak ada di sana"? Itu bukan kesalahan besar...

Mencoba untuk bergerak lebih dekat ke proposal konkret...

Saya mulai menyukai alternatif TrySuccess . Menariknya, saya pikir _nobody_, termasuk saya sendiri, menyukai yang satu ini -- bahkan tidak ada di versi final RFC. Tapi untungnya itu hidup dalam sejarah: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#using -an-associated-type-for-the-success -nilai

Bagi saya, keberatan terbesar untuk itu adalah keluhan yang masuk akal bahwa seluruh sifat inti hanya untuk tipe terkait ( trait TrySuccess { type Success; } ) berlebihan. Tetapi dengan catch try memblokir kembali (https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) untuk melakukan ok-wrapping (seperti dalam RFC) , tiba-tiba memiliki kegunaan langsung dan penting: ini adalah sifat yang mengontrol pembungkus itu. Digabungkan dengan tujuan " ? harus selalu menghasilkan tipe yang sama" yang menurut saya diinginkan secara umum, sifat tersebut tampaknya lebih menahan bobotnya. Kaki tangan:

trait TryContinue {
    type Continue;
    fn from_continue(_: Self::Continue) -> Self;
}

Jenis terkait itu, sebagai salah satu yang akan dikembalikan dari operator ? , juga merupakan salah satu yang jelas harus ada. Itu berarti tidak mengenai gangguan "harus mendefinisikan jenis 'sisa'" yang diartikulasikan Niko . Dan itu menjadi () masuk akal, bahkan umum, jadi hindari NoneError -seperti liuk.

Sunting: Untuk gudang sepeda, "kembali" mungkin kata yang bagus, karena inilah yang terjadi ketika Anda return dari metode try (alias ok-wrapping). return juga merupakan nama operator monad untuk ini, iiuc...

Sunting 2: Pikiran saya tentang sifat/metode lain belum stabil, @tmccombs.

@scottmcm hanya untuk memperjelas, atau Anda menyarankan dua sifat berikut?

trait TryContinue {
  type Continue;
  fn from_continue(_: Self::Continue) -> Self;
}

trait Try<E>: TryContinue {
  fn try(self) -> Result<Self::Continue, E>
}

Dan desuguring x? akan terlihat seperti:

x.try() match {
    Ok(c) => c,
   Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}

dan try { ...; expr} akan berubah menjadi sesuatu seperti:

{ 
    ...
    TryContinue::from_continue(expr);
}

@scottmcm Saya juga menemukan varian itu jauh lebih menarik ketika mempertimbangkan ok-wrapping =)

Melanjutkan ke sifat berikutnya, saya pikir satu untuk ? menjadi ini (penurunan sepeda besar-besaran modulo):

trait Try<Other: TryContinue = Self>: TryContinue + Sized {
    fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}

enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

Berbagai macam justifikasi:

  • Ini membutuhkan TryContinue untuk tipe argumennya sehingga tipe ekspresi x? selalu sama
  • Ini default parameter type ke Self untuk kasus sederhana seperti try_fold yang memeriksa dan mengembalikan jenis yang sama, jadi baik-baik saja hanya dengan T: Try .
  • Sifat ini meluas TryContinue karena jika tipe yang digunakan sebagai tipe pengembalian memungkinkan ? di tubuhnya, maka itu juga harus mendukung ok-wrapping.
  • Enum baru adalah hasil isomorfik, tetapi menghindari pembicaraan dalam istilah "kesalahan" seperti yang dibahas di atas, dan sebagai hasilnya saya pikir itu memberikan arti yang sangat jelas untuk masing-masing varian.
  • Argumen ke ? adalah tipe generik sehingga, seperti TryContinue , ia "menghasilkan Self dari sesuatu"

Demo bukti konsep lengkap, termasuk makro untuk try{} / ? dan impls untuk Option , Result , dan Ordering : https ://play.rust-lang.org/?gist=18663b73b6f35870d20fd172643a4f96&version=stable (terima kasih @nikomatsakis karena membuat yang asli setahun yang lalu )

Kelemahan:

  • Masih memiliki parameter phantom type di Result impl yang saya tidak terlalu suka

    • Tapi mungkin itu tradeoff yang baik karena tidak memerlukan tambahan LessOrGreater ketik Ordering impl.

    • Dan parameter tipe phantom di impls kurang menjijikkan daripada di tipe

  • Tidak memberikan transformasi From konsisten

    • Tapi itu mungkin bagus, karena StrandResult tidak peduli, untuk sesuatu seperti SearchResult itu akan aneh karena jalur Break adalah jalur sukses, dan mungkin akhirnya membantu inferensi kasus bersarang- ? .

  • Tidak terlalu jelas seperti apa sintaks throw itu

    • Yang kehilangan penjelasan ? sebagai Ok(x) => x, Err(r) => throw e.into() yang sangat saya sukai

    • Tetapi juga dapat membiarkan throw; menjadi cara untuk menghasilkan None (melalui sesuatu seperti impl<T> Throw<()> for Option<T> ), yang jauh lebih baik daripada throw NoneError;

    • Dan memisahkannya mungkin bagus, karena throw LessOrGreater::Less akan _benar-benar_ konyol.

Sunting: Ping @glaebhoerl , yang pendapatnya saya suka ini sebagai peserta besar di RFC 1859.

Sunting 2: Juga cc @colin-kiegel, untuk pernyataan ini dari https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652:

Saya bertanya-tanya apakah pendekatan esensialis dapat mengadopsi beberapa keanggunan reduksionis tanpa mengorbankan tujuan di atas.

Saya sangat suka usulan itu.

Tidak begitu jelas seperti apa sintaks lemparan itu

Mengabaikan bagasi dari kata kunci throw dari bahasa lain, "throw" memang masuk akal, karena Anda "melempar" nilainya ke cakupan yang lebih tinggi. Python dan scala juga menggunakan pengecualian untuk aliran kontrol selain kasus kesalahan (meskipun, dalam kasus scala Anda biasanya tidak akan menggunakan try/catch secara langsung), jadi ada beberapa preseden untuk menggunakan throw untuk jalur yang berhasil.

@tmcombs

Mengabaikan bagasi dari kata kunci throw dari bahasa lain, "throw" memang masuk akal, karena Anda "melempar" nilainya ke cakupan yang lebih tinggi.

Meskipun dilengkapi dengan bagasi yang serupa, saya menduga "naikkan" lebih cocok:

lempar (v): untuk menempatkan atau menyebabkan pergi atau datang ke suatu tempat, posisi, kondisi, dll, seolah-olah dengan melemparkan:

menaikkan (v): untuk pindah ke posisi yang lebih tinggi; mengangkat; mengangkat

Mungkin ada cara untuk menggabungkan "kenaikan" dengan logika ? , mengingat kenaikan juga bisa berarti "mengumpulkan". Sesuatu seperti: Ok(v) => v, Err(e) => raise From::from(e) , di mana raise meniru pola yang cocok (misalnya diberi pola Err(e) itu adalah sihir sintaksis untuk ControlFlow::Break(Err(...)) ).

Saya menduga "menaikkan" lebih cocok

poin bagus

@scottmcm

Tidak begitu jelas seperti apa sintaks lemparan itu

Apakah ada alasan kami tidak dapat memiliki from_err(value: Other) ?

UPDATE: Mungkin saya bingung tentang peran Other , sebenarnya. Saya harus mempelajari sifat ini sedikit lebih banyak. =)

@nikomatsakis

Apakah ada alasan kami tidak dapat memiliki from_err(value: Other) ?

Yah, Other adalah seluruh tipe ? -able (seperti Result ), jadi saya tidak ingin throw Ok(4) berfungsi. (Dan itu perlu disjungsi keseluruhan untuk menghindari pemaksaan pengenalan jenis kesalahan buatan.) Sebagai contoh, saya pikir interop Option-Result kita saat ini akan setara dengan ini (dan kebalikannya):

impl<T, F, U> Try<Option<U>> for Result<T, F>
   where F: From<NoneError>
{
    fn check(x: Option<U>) -> ControlFlow<U, Self> {
        match x {
            Some(x) => ControlFlow::Continue(x),
            None => ControlFlow::Break(Err(From::from(NoneError))),
        }
    }
}

Kecenderungan saya saat ini untuk throw adalah untuk ini:

trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }

dengan

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • Memperluas TryContinue sehingga ok-wrapping juga tersedia untuk tipe pengembalian metode yang digunakan
  • Bukan jenis yang terkait sehingga Anda dapat menyiratkan keduanya, katakanlah, TryBreak<TryFromIntError> dan TryBreak<io::Error> untuk suatu jenis jika Anda mau.

    • dan impl<T> TryBreak<()> for Option<T> untuk mengaktifkan hanya throw; terasa masuk akal dengan cara yang tidak dilakukan oleh jenis kesalahan terkait () .

    • impl<T> TryBreak<!> for T juga bagus, tapi mungkin tidak koheren.

(Selain: nama sifat dan metode ini mengerikan; tolong bantu !)

Pikiran saya tentang masalah lain yang diangkat di sini belum menjadi bentuk yang mudah diartikulasikan, tetapi sehubungan dengan masalah inferensi tipe di sekitar From::from() desugaring (Saya tidak ingat apakah ini juga dibahas di tempat lain baru-baru ini , atau hanya di sini?):

Perasaan instingtual (dari pengalaman Haskell) bahwa "dengan cara itu ambiguitas inferensi tipe kebohongan" adalah salah satu (meskipun bukan yang utama) alasan mengapa saya tidak ingin memiliki konversi From sebagai bagian dari RFC asli. Sekarang setelah dipanggang ke dalam kue, saya bertanya-tanya apakah kita tidak dapat mencoba memiliki kue itu dan memakannya juga dengan 'hanya' menambahkan aturan default khusus untuk itu ke proses inferensi tipe?

Artinya, saya pikir: setiap kali kita melihat From::from() [opsional: yang dimasukkan oleh ? desugaring, daripada ditulis secara manual], dan kita tahu persis salah satu dari dua jenis (input vs. output), sementara yang lain ambigu, kami defaultkan yang lain sama dengan yang satu. Dengan kata lain, saya pikir, ketika tidak jelas impl From akan digunakan, kita default ke impl<T> From<T> for T . Ini, saya pikir, pada dasarnya selalu apa yang sebenarnya Anda inginkan? Ini mungkin sedikit ad-hoc, tetapi jika berhasil, manfaatnya tampaknya sepadan dengan biayanya IMHO.

(Saya juga berpikir From sudah menjadi item lang, tepatnya karena desugaring ? , tetapi tampaknya tidak? Bagaimanapun, itu sudah dalam beberapa hal khusus untuk alasan itu .)

dalam proposal @scottmcm , From::from() _bukan_ bagian dari desugaring, melainkan dalam implementasi Try untuk Result .

@tmccombs Saya tidak mengusulkan amandemen proposalnya.

try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) telah membahas blok try mana orang tidak peduli dengan hasilnya. Alternatif ini tampaknya menanganinya dengan baik, karena dapat memilih untuk mengabaikan jenis kesalahan sepenuhnya jika diinginkan, memungkinkan

let IgnoreErrors = try {
    error()?;
    none()?;
};

Implementasi proof-of-concept menggunakan ciri yang sama seperti sebelumnya: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable

Saya pikir ada beberapa kemungkinan menarik di sana, terutama karena implementasi khusus dapat, katakanlah, hanya mengambil hasil dan mengikat E: Debug sehingga secara otomatis mencatat kesalahan yang terjadi. Atau sebuah versi dapat dibuat dimaksudkan secara khusus sebagai tipe pengembalian untuk main dalam hubungannya dengan Termination yang "hanya berfungsi" untuk memungkinkan Anda menggunakan ? tanpa tanda tangan tipe yang rumit (https ://github.com/rust-lang/rfcs/issues/2367).

Saya memiliki masalah serupa seperti yang dibuktikan oleh @nikomatsakis menggunakan versi yang ada dari sifat Try . Untuk masalah spesifik, lihat https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.

Definisi sifat yang diusulkan oleh @scottmcm menyelesaikan masalah ini. Mereka tampaknya lebih rumit dari yang diperlukan, namun. Saya mengambil celah untuk mengimplementasikannya kembali dan menghasilkan yang berikut:

#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

// Used by `try { }` expansions.
trait FromTry: Try {
    fn from_try(value: Self::Continue) -> Self;
}

// Used by `?`.
trait Try<T = Self>: Sized {
    type Continue;
    fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}

Perubahan utama adalah bahwa Continue tipe terkait ada pada sifat Try sebagai lawan dari FromTry (sebelumnya TryContinue ). Selain menyederhanakan definisi, ini memiliki keuntungan bahwa Try dapat diimplementasikan secara independen dari FromTry , yang menurut saya lebih umum, dan implementasi FromTry disederhanakan setelah Try diimplementasikan. (Catatan: jika diinginkan Try dan FromTry diimplementasikan secara bersamaan, kita cukup memindahkan metode from_try ke Try )

Lihat taman bermain lengkap dengan implementasi untuk Result dan Option serta Outcome Rocket di taman bermain ini .

Terima kasih atas laporannya, @SergioBenitez! Implementasi itu cocok dengan versi alternatif "balik parameter tipe" dari proposal sifat asli di RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -pertanyaan

Kerugian terbesar adalah properti yang typeof(x?) hanya bergantung pada typeof(x) . Kurangnya properti itu adalah salah satu masalah dengan aslinya ("Saya sedikit khawatir tentang keterbacaan dalam kode di sepanjang baris ini" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) dan keuntungan dari proposal reduksionis terakhir ("Untuk tipe T apa pun, ? dapat menghasilkan tepat satu jenis nilai ok/kesalahan" https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Tentu saja, ada juga argumen yang mengatakan bahwa properti tidak perlu atau terlalu membatasi, tetapi dalam ringkasan akhir sebelum FCP, properti itu masih ada sebagai keuntungan (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).

Selain menyederhanakan definisi, ini memiliki keuntungan bahwa Try dapat diimplementasikan secara independen dari FromTry , yang menurut saya lebih umum

Tentu saja hari ini from_ok kurang umum, karena Try dan do catch tidak stabil. Dan bahkan jika do catch stabil, saya setuju bahwa itu akan digunakan kurang dari ? secara keseluruhan (karena sebagian besar blok tersebut berisi beberapa ? s).

Namun, dari perspektif sifat dan operasinya, saya pikir "bungkus nilai 'teruskan' dalam jenis pembawa" adalah bagian yang sangat penting dari ? . Seseorang jarang melewati sifat untuk itu hari ini -- hanya menggunakan Ok(...) atau Some(...) sebagai gantinya -- tetapi ini penting untuk penggunaan umum. Misalnya, try_fold :

https://github.com/rust-lang/rust/blob/8728c7a726f3e8854f5a80b474d1a8bacab10304/src/libcore/iter/iterator.rs#L1478 -L1482

Jika suatu fungsi akan mengembalikan jenis pembawa, sangat penting bahwa ada cara untuk memasukkan nilai yang diinginkan ke dalam jenis pembawa itu. Dan, yang penting, saya tidak berpikir menyediakan ini adalah kesulitan. Ini memiliki definisi yang sangat alami untuk Hasil, untuk Opsi, untuk Hasil, dll.

@Centril juga telah menunjukkan sebelumnya bahwa "bungkus skalar dalam pembawa" juga merupakan konstruksi yang lebih sederhana secara teoritis. Sebagai contoh, Haskell menyebutnya Pointed typeclass , meskipun saya tidak berpikir kita ingin menggeneralisasinya _that_ jauh di Rust: allow try { 4 } vec![4] sepertinya berlebihan bagi saya .

Saya juga membayangkan masa depan di mana, seperti fungsi async diusulkan untuk membungkus nilai blok ke masa depan, kita mungkin memiliki fungsi try yang membungkus nilai blok menjadi tipe yang bisa salah. Di sana, sekali lagi, TryContinue adalah bagian penting -- fungsi seperti itu mungkin bahkan tidak menggunakan ? , jika kita mendapatkan throw konstruksi.

Jadi semua itu, sayangnya, lebih filosofis daripada konkret. Beri tahu saya jika itu masuk akal, atau jika Anda berpikir sebaliknya di salah satu bagian :slightly_smiling_face:

Sunting : Maaf jika Anda mendapat email dengan versi awal ini; Saya menekan "komentar" terlalu dini ...

Hal terbesar yang hilang adalah properti yang typeof(x?) hanya bergantung pada typeof(x).

Ah ya, tentu saja. Terima kasih telah menunjukkan hal itu.

Tentu saja, ada juga argumen yang mengatakan bahwa properti tidak perlu atau terlalu membatasi, tetapi dalam ringkasan akhir sebelum FCP, itu masih ada sebagai keuntungan (rust-lang/rfcs#1859 (komentar)).

Apakah ada contoh spesifik di mana itu mungkin terlalu membatasi?

Jika suatu fungsi akan mengembalikan jenis pembawa, sangat penting bahwa ada cara untuk memasukkan nilai yang diinginkan ke dalam jenis pembawa itu. Dan, yang penting, saya tidak berpikir menyediakan ini adalah kesulitan.

Saya pikir itu analisis yang adil. Saya setuju.

@SergioBenitez Dari https://github.com/rust-lang/rfcs/pull/1859#issuecomment -279187967

Jadi pertanyaannya, apakah pembatasan yang diajukan sudah cukup? Apakah ada penggunaan konteks kasus kesalahan yang baik dalam menentukan jenis keberhasilan? Apakah ada kemungkinan pelanggaran?

Saya dapat mengatakan dari pengalaman saya di masa depan bahwa mungkin ada beberapa kasus yang berguna di sini. Secara khusus, jenis Poll yang Anda bicarakan dapat diproses dalam beberapa cara. Terkadang, kami ingin melompat keluar pada varian NotReady dan dibiarkan dengan nilai Result pada dasarnya. Terkadang, kami hanya tertarik pada varian Ready dan ingin melompat ke salah satu varian lainnya (seperti pada sketsa Anda). Jika kita mengizinkan jenis keberhasilan untuk ditentukan sebagian oleh jenis kesalahan, lebih masuk akal untuk mendukung kedua kasus ini, dan pada dasarnya menggunakan konteks untuk menentukan jenis dekomposisi yang diinginkan.

OTOH, saya sedikit khawatir tentang keterbacaan dalam kode di sepanjang baris ini. Ini terasa secara kualitatif berbeda dari sekadar mengubah komponen kesalahan -- ini berarti bahwa dasarnya cocok dengan itu ? akan tampil tergantung pada informasi kontekstual.

Jadi orang bisa membayangkan sifat yang memindahkan _keduanya_ tipe ke parameter tipe, seperti

trait Try<T,E> {
    fn question(self) -> Either<T, E>;
}

Dan gunakan itu untuk mengaktifkan semua

let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors

Tapi saya pikir itu pasti ide yang buruk, karena itu berarti ini tidak berhasil

println!("{}", z?);
z?.method();

Karena tidak ada konteks tipe untuk mengatakan apa yang harus diproduksi.

Versi lainnya adalah mengaktifkan hal-hal seperti ini:

fn foo() -> Result<(), Error> {
    // `x` is an Async<i32> because NotReady doesn't fit in Result
    let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
    // `x` is just i32 because we're in a Poll-returning method
    let x = something_that_returns_poll()?;
}

Insting saya adalah bahwa kesimpulan "mengalir keluar dari ?" ada terlalu mengejutkan, dan dengan demikian ini dalam ember "terlalu pintar".

Secara kritis, saya tidak berpikir bahwa tidak memilikinya terlalu membatasi. my_result? dalam fungsi -> Poll tidak memerlukannya, karena jenis keberhasilannya sama seperti biasanya (penting untuk menjaga agar kode sinkron tetap berfungsi sama dalam konteks asinkron) dan varian kesalahan juga mengonversi dengan baik . Menggunakan ? pada Poll dalam metode yang mengembalikan Result tampaknya seperti anti-pola, bukan sesuatu yang harus umum, jadi menggunakan metode khusus (hipotetis) seperti .wait(): T (untuk memblokir hasil) atau .ready(): Option<T> (untuk memeriksa apakah sudah selesai) untuk memilih mode mungkin lebih baik.

Ini "menarik" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

Saya tidak suka blok try (do catch) ini, sepertinya tidak terlalu ramah bagi pendatang baru.

Saya mencoba menyusun status proposal saat ini, yang tampaknya tersebar di banyak komentar. Apakah ada ringkasan tunggal dari kumpulan sifat yang diusulkan saat ini (yang menghilangkan tipe terkait Error )?

Di awal utas ini saya melihat komentar tentang pemisahan Try menjadi sifat kesalahan yang terpisah ke/dari -- apakah ada rencana untuk mengimplementasikan pemisahan itu?

Akan berguna untuk memiliki konversi transparan dari Result<T, E> ke tipe apa pun Other<T2, E> pada tanda tanya -- ini akan memungkinkan fungsi IO yang ada dipanggil dengan sintaks Nice dari dalam fungsi dengan fungsi yang lebih khusus (mis. malas) tipe pengembalian.

pub fn async_handler() -> AsyncResult<()> {
    let mut file = File::create("foo.txt")?;
    AsyncResult::lazy(move || {
        file.write_all(b"Hello, world!")?;
        AsyncResult::Ok(())
    })
}

Secara semantik ini terasa seperti From::from(E) -> Other<T2, E> , tetapi penggunaan From saat ini terbatas pada implementasi Result -equivalent Try .

Saya benar-benar berpikir NoneError harus memiliki masalah pelacakan terpisah. Bahkan jika sifat Try tidak pernah stabil, NoneError harus distabilkan karena membuat penggunaan ? pada Option jauh lebih ergonomis. Pertimbangkan ini untuk kesalahan seperti struct MyCustomSemanthicalError; atau kesalahan dalam mengimplementasikan Default . None dapat dengan mudah dikonversi menjadi MyCustomSeemanthicalError melalui From<NoneError> .

Dalam mengerjakan https://github.com/rust-analyzer/rust-analyzer/ , saya menemukan potongan kertas yang sedikit berbeda dari ketidakcukupan di operator ? , terutama ketika tipe pengembaliannya adalah Result<Option<T>, E> .

Untuk tipe ini, masuk akal jika ? secara efektif desugar ke:

match value? {
    None => return Ok(None),
    Some(it)=>it,
}

di mana value bertipe Result<Option<V>, E> , atau:

value?

di mana value adalah Result<V, E> . Saya percaya bahwa ini mungkin jika pustaka standar mengimplementasikan Try dengan cara berikut untuk Option<T> , meskipun saya belum secara eksplisit menguji ini dan saya pikir mungkin ada masalah spesialisasi.

enum NoneError<E> {
    None,
    Err(E),
}

impl From<T> for NoneError<T> {
    fn from(v: T) -> NoneError<T> {
        NoneError:Err(v)
    }
}

impl<T, E> std::Ops::Try for Result<Option<T>, E> {
    type OK = T;
    type Error = NoneError<E>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            Ok(option) => {
                if let Some(inner) = option {
                    Ok(inner)
                } else {
                    Err(NoneError::None)
                }
            }
            Err(error) => {
                Err(NoneError::Err(error));
            }
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::Err(error) => Err(error),
            None => Some(None),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(Some(v))
    }
}

impl<T> std::Ops::Try for Option<T> {
    type OK = T;
    type Error = NoneError<!>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            None => Err(NoneError::None),
            Some(v) => Ok(v),
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::None => Some(None),
            _ => unreachable!("Value of type ! obtained"),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(v)
    }
}

Ketika bertanya kepada @matklad mengapa dia tidak bisa membuat enum khusus yang mengimplementasikan Try , yang akan disebut Cancellable dalam kasus ini, dia menunjukkan bahwa std::ops::Try tidak stabil, jadi tidak dapat digunakan karena rust-analyzer (saat ini) menargetkan karat yang stabil.

Repost dari https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 karena saya ingin mengomentari ini, tapi saya pikir itu masalah yang salah:

Pada dasarnya, situasi yang saya miliki adalah panggilan balik dalam kerangka kerja GUI - alih-alih mengembalikan Option atau Result , mereka harus mengembalikan UpdateScreen , untuk memberi tahu kerangka kerja jika layar perlu diperbarui atau tidak. Seringkali saya tidak perlu masuk sama sekali (tidak praktis untuk masuk setiap kesalahan kecil) dan hanya mengembalikan UpdateScreen::DontRedraw ketika kesalahan telah terjadi. Namun, dengan operator ? , saya harus menulis ini setiap saat:

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

Karena saya tidak dapat mengonversi dari Result::Err menjadi UpdateScreen::DontRedraw melalui operator Coba, ini menjadi sangat membosankan - sering kali saya memiliki pencarian sederhana di peta hash yang dapat gagal (yang bukan merupakan kesalahan ) - begitu sering dalam satu panggilan balik saya memiliki 5 - 10 penggunaan operator ? . Karena di atas sangat bertele-tele untuk ditulis, solusi saya saat ini adalah impl From<Result<T>> for UpdateScreen seperti ini , dan kemudian gunakan fungsi dalam dalam panggilan balik seperti ini:

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

Karena panggilan balik digunakan sebagai penunjuk fungsi, saya tidak dapat menggunakan -> impl Into<UpdateScreen> (untuk beberapa alasan, mengembalikan impl saat ini tidak diperbolehkan untuk penunjuk fungsi). Jadi satu-satunya cara bagi saya untuk menggunakan operator Try adalah dengan melakukan trik fungsi dalam. Akan lebih baik jika saya bisa melakukan sesuatu seperti ini:

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

Saya tidak yakin apakah ini mungkin dengan proposal saat ini dan hanya ingin menambahkan kasus penggunaan saya untuk dipertimbangkan. Akan sangat bagus jika operator Try khusus dapat mendukung sesuatu seperti ini.

@joshtriplett Maaf telah mengambil beberapa saat untuk kembali ke ini. Saya telah mengumpulkan prototipe proposal yang berfungsi di https://github.com/rust-lang/rust/compare/master...scottmcm :try-trait-v2 menjadi konkret. Saya berharap untuk mencoba beberapa hal lagi dengannya.

@scottmcm Apakah Anda memiliki penjelasan tingkat tinggi tentang perubahan?

@scottmcm FWIW Saya mencoba perubahan Anda di rayon juga:
https://github.com/rayon-rs/rayon/compare/master...cuviper :try-trait-v2
(masih menggunakan salinan pribadi daripada item yang tidak stabil)

Jadi apa solusi untuk mengonversi Opsi NoneError? Tampaknya, karena, ia mengimplementasikan Sifat Coba, ia tidak akan dikompilasi kecuali Anda mengaktifkannya menggunakan fitur tertentu (tidak stabil?).

Jadi pada dasarnya, Anda tidak dapat menggunakan ? operator dengan Option sejauh yang saya ketahui?

@omarabid , operator stabil untuk digunakan dengan Option atau Result , tetapi Anda tidak dapat menggunakan Try sebagai batasan umum hingga stabil. Tidak apa-apa untuk menggunakan ? pada Option dalam fungsi yang mengembalikan Option , karena Anda tidak perlu melibatkan NoneError sama sekali. Anda juga dapat mengembalikan Result jika Anda menghapus jenis:

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

impl<T: Debug + 'static> From<T> for Error {
    fn from(error: T) -> Self {
        Error(Box::new(error))
    }
}

pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
    Ok(slice.get(index)?)
}

( taman bermain )

@scottmcm , prototipe Anda try-trait-v2 gagal dalam contoh ini!

Jika kita tidak ingin contoh saya rusak, try-trait-v2 akan membutuhkan sesuatu seperti:

#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
    fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
        match self {
            Some(x) => ops::ControlFlow::Continue(x),
            None => ops::ControlFlow::Break(Err(E::from(NoneError))),
        }
    }
}

Apa status fitur ini saat ini?

PR #62606 untuk mendokumentasikan implementasi try_fold untuk iterator harus dibuka kembali setelah ini menjadi stabil.

Sunting: Memperbarui operasi dengan item pelacakan untuk itu ~ scottmcm

Apakah ada rencana untuk mengganti sifat Try dengan salah satu alternatif yang disarankan di utas ini? Versi yang disarankan oleh @scottmcm tampaknya baik-baik saja. Saya ingin terus menggunakan operator ? dengan Option , dan saya pikir sifat Try harus diubah untuk tidak memaksa Result semantik pada Option .

Menggunakan alternatif @scottmcm akan memungkinkan kita untuk menggunakan ? dengan Option dan menyingkirkan NoneError . Saya setuju dengan @nikomatsakis ( komentar ) bahwa sifat Try tidak boleh mempromosikan kebutuhan untuk "secara artifisial menentukan tipe untuk mewakili 'kegagalan' dari Option ".

pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;

Pemula di sini, saya agak keras kepala dalam ingin mendapatkan? untuk secara otomatis mengetik menghapus Kesalahan dan Opsi apa pun.
Setelah menghabiskan terlalu banyak waktu untuk mencoba memahami mengapa solusi lain yang mungkin tidak dapat diterapkan, saya menemukan @cuviper adalah yang paling dekat dengan apa yang bisa saya dapatkan.
Beberapa penjelasan akan membantu, tetapi setidaknya saya harus mengenal beberapa batasan metaprogramming Rust.
Jadi saya mencoba untuk mencari tahu dan menjelaskan secara konkret.
Utas ini tampaknya persimpangan yang paling mungkin di mana saya semoga dapat membantu siapa pun seperti saya yang tersandung pada ini, jangan ragu untuk mengoreksi:

  • Menggunakan Debug (umum untuk StdError dan NoneError) alih-alih StdError :
    Konflik From<T: StdError> for Error generik dan From<NoneError> for Error
    Karena akan ambigu jika NoneError mengimplementasikan StdError (tampaknya bahkan default tidak mengizinkan penggantian dan memaksakan eksklusivitas)
    Ini dapat diselesaikan dengan "batas negatif" yang tidak didukung, mungkin karena itu akan menjadi satu-satunya use case? (lebih dari spesialisasi)
    impl StdError untuk NoneError hanya dapat didefinisikan bersama StdError atau NoneError itu sendiri, sehingga konsisten di semua downstream.
  • Kesalahan tidak boleh alias:
    type Error = Box<Debug> mengikat Debug yang membuat From<T:Debug> for Error bertentangan dengan From<T> for T (Refleksif Dari untuk idempotence)

Jadi karena Kesalahan tidak dapat mengimplementasikan Debug, Anda mungkin ingin memiliki pembantu untuk membukanya menjadi Hasil dengan Debug transitif :

fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }

Tidak boleh impl From<Result<T>> for StdResult<T> atau Into<StdResult> for Result<T> (tidak dapat menyiratkan sifat upstream untuk tipe upstream)

Misalnya, Anda dapat menggunakannya untuk mendapatkan pengembalian Debug untuk Termination :

fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }

Ide jahat : mungkin operator ? entah bagaimana bisa mewakili ikatan monadik, jadi

let x: i32 = something?;
rest of function body

menjadi

Monad::bind(something, fn(x) {
    rest of function body
})

Ide yang buruk, tapi itu menyenangkan jiwa geek dalam diriku.

@derekdreery Saya tidak berpikir itu akan bekerja dengan baik dengan aliran kontrol imperatif seperti return dan continue

Perlu diingat bahwa semantik dari operator ? sudah stabil. Hanya sifat Try aktual yang tidak stabil, dan setiap perubahan harus mempertahankan efek stabil yang sama untuk Option dan Result .

@KrishnaSannasi

@derekdreery Saya tidak berpikir itu akan bekerja dengan baik dengan aliran kontrol imperatif seperti kembali dan lanjutkan

Saya setuju dengan pernyataan ini.

@cuviper

Perlu diingat bahwa semantik dari ? operator sudah stabil. Hanya sifat Coba yang sebenarnya yang tidak stabil, dan setiap perubahan harus mempertahankan efek stabil yang sama untuk Opsi dan Hasil.

Apakah ini benar di seluruh zaman juga?

Pada catatan yang lebih umum, saya rasa tidak mungkin untuk menyatukan konsep seperti .await , ?/ pengembalian awal, Option::map, Result::map, Iterator::map in rust, tetapi memahami bahwa semua ini berbagi beberapa struktur pasti membantu saya menjadi programmer yang lebih baik.

Maaf karena menjadi OOT.

Saya tidak yakin apakah Epoch/edisi akan diizinkan untuk mengubah ? desugaring, tetapi itu pasti akan memperumit banyak masalah lintas-peti seperti makro.

Interpretasi saya tentang jaminan stabilitas dan zaman RFC adalah bahwa perilaku ? (gula atau lainnya) dapat diubah, tetapi perilakunya saat ini pada tipe Option / Result harus tetap sama karena melanggar itu akan menciptakan lebih banyak churn daripada yang bisa kita harapkan untuk dibenarkan.

Bagaimana jika Option entah bagaimana adalah alias tipe untuk Result ? Yaitu type Option<T> = Result<T, NoValue> , Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x) , Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? Itu akan membutuhkan keajaiban untuk diterapkan dan tidak realistis dalam lingkungan bahasa saat ini, tapi mungkin itu perlu ditelusuri.

Kami tidak dapat melakukan perubahan ini karena ada impls sifat yang bergantung pada Option dan Result sebagai tipe yang berbeda.

Jika kita mengabaikannya, maka kita dapat mengambil satu langkah lebih jauh dan bahkan membuat bool sebuah alias untuk Result<True, False> , di mana True dan False adalah tipe unit.

Apakah sudah dipertimbangkan untuk menambahkan impl Try untuk unit, () ? Untuk fungsi yang tidak mengembalikan apa pun, pengembalian awal mungkin masih berguna jika terjadi kesalahan dalam fungsi non-kritis, seperti panggilan balik logging. Atau, apakah unit dikecualikan karena lebih disukai untuk tidak pernah mengabaikan kesalahan secara diam-diam dalam situasi apa pun?

Atau, apakah unit dikecualikan karena lebih disukai untuk tidak pernah mengabaikan kesalahan secara diam-diam dalam situasi apa pun?

Ini. Jika Anda ingin mengabaikan kesalahan dalam konteks non-kritis, Anda harus menggunakan unwrap atau salah satu variannya.

dapat menggunakan ? pada sesuatu seperti foo() -> () akan sangat berguna dalam konteks kompilasi bersyarat dan harus sangat dipertimbangkan. Saya pikir itu berbeda dari pertanyaan Anda.

anda harus menggunakan unwrap atau salah satu variannya.

unwrap() menyebabkan kepanikan, jadi saya tidak akan merekomendasikan menggunakannya dalam konteks yang tidak penting. Itu tidak akan sesuai dalam situasi di mana pengembalian sederhana diinginkan. Seseorang dapat membuat argumen bahwa ? tidak sepenuhnya "diam" karena penggunaan ? yang eksplisit dan terlihat dalam kode sumber. Dengan cara ini ? dan unwrap() analog untuk fungsi unit, hanya dengan semantik pengembalian yang berbeda. Satu-satunya perbedaan yang saya lihat adalah unwrap() akan membuat efek samping yang terlihat (membatalkan program / mencetak stacktrace) dan ? tidak.

Saat ini, ergonomi pengembalian awal dalam fungsi unit jauh lebih buruk daripada pengembalian Result atau Option . Mungkin keadaan ini diinginkan karena pengguna harus menggunakan tipe pengembalian Result atau Option dalam kasus ini, dan ini memberikan insentif bagi mereka untuk mengubah API mereka. Either way, mungkin baik untuk memasukkan diskusi tentang jenis pengembalian unit dalam PR atau dokumentasi.

dapat menggunakan ? pada sesuatu seperti foo() -> () akan sangat berguna dalam konteks kompilasi bersyarat dan harus sangat dipertimbangkan. Saya pikir itu berbeda dari pertanyaan Anda.

Bagaimana ini akan berhasil? Selalu evaluasi ke Ok(()) dan diabaikan?

Juga, Bisakah Anda memberikan contoh di mana Anda ingin menggunakan ini?

Ya, idenya adalah bahwa sesuatu seperti MyCollection::push mungkin, tergantung pada konfigurasi waktu kompilasi, memiliki nilai pengembalian Result<(), AllocError> atau () nilai pengembalian jika koleksi dikonfigurasi untuk hanya panik pada kesalahan. Kode perantara yang menggunakan koleksi seharusnya tidak perlu memikirkannya, jadi jika kode tersebut dapat _selalu_ menggunakan ? bahkan ketika tipe pengembaliannya adalah () itu akan berguna.

Setelah hampir 3 tahun, apakah ini semakin dekat untuk diselesaikan?

@Lokathor yang hanya akan berfungsi jika tipe pengembalian Result<Result<X,Y>,Z> atau yang serupa tidak memungkinkan. Tapi itu. Dengan demikian tidak mungkin.

Saya tidak mengerti mengapa Hasil berlapis menyebabkan masalah. Bisa tolong jelaskan?

Untuk tujuan referensi silang, formulasi alternatif telah diusulkan di internal oleh @dureuill :

https://internals.rust-lang.org/t/a-slightly-more-general-easier-to-implement-alternative-to-the-try-trait/12034

@Lokathor
Ok, saya memikirkannya lebih dalam dan berpikir saya mungkin telah menemukan penjelasan yang cukup bagus.

Menggunakan anotasi

Masalahnya adalah interpretasi dari tipe pengembalian atau anotasi aneh akan mengacaukan kode. Itu mungkin, tetapi itu akan membuat kode lebih sulit dibaca. (Prasyarat: #[debug_result] menerapkan perilaku yang Anda inginkan, dan memodifikasi fungsi menjadi panik dalam mode rilis alih-alih mengembalikan Result::Err(...) , dan membuka Result::Ok , tetapi bagian itu rumit)

#[debug_result]
fn f() -> Result<X, Y>;

#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;

#[debug_result]
fn g() -> Result<X, Y> {
    // #[debug_result_spoiled]
    let w = f();
    // w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
    // the following line would currently only work half of the time
    // we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
    // and `w` was marked as `#[debug_result]`-spoiled
    let x = w?;

    // but it gets worse; what's with the following:
    let y = f2();
    let z = y?;
    // it has completely different sematics based on a #[cfg(debug_...)],
    // but would (currently?) print no warnings or errors at all,
    // and the type of z would be differently.

    Ok(z)
}

Dengan demikian, itu akan membuat kode lebih sulit dibaca.
Kami tidak bisa begitu saja mengubah perilaku ? hanya menjadi no-op,
terutama jika nilai kembalian dari #[debug_result] disimpan dalam variabel sementara dan kemudian "dipropagasi" dengan operator ? . Itu akan membuat semantik ? benar-benar membingungkan, karena itu akan bergantung pada banyak "variabel", yang belum tentu diketahui pada "waktu penulisan" atau mungkin sulit ditebak hanya dengan membaca kode sumber. #[debug_result] perlu merusak variabel yang diberi nilai fungsi, tetapi itu tidak akan berfungsi jika satu fungsi ditandai dengan #[debug_result] dan satu tidak, misalnya berikut ini akan menjadi kesalahan ketik.

// f3 is not annotated
fn f3() -> Result<X, Y>;

// later inside of a function:
   // z2 needs to be both marked spoiled and non-spoiled -> type error
   let z2 = if a() {
       f3()
   } else {
       f()
   };
   // althrough the following would be possible:
   let z2_ = if a() {
       f3()?
   } else {
       f()?
   };

Alternatif: menggunakan tipe baru

Solusi "cleaner" mungkin merupakan tipe DebugResult<T, E> yang hanya panik ketika flag kompilasi tertentu disetel dan dibuat dari kesalahan, tetapi sebaliknya akan setara dengan Result<T, E> jika tidak. Tapi itu akan mungkin dengan proposal saat ini juga, saya pikir.

Balas ke posting terakhir @zserik : Makro yang Anda jelaskan tidak ada gunanya - pengisian jenis pengembalian fungsi berdasarkan konfigurasi build akan merusak setiap kecocokan pada hasil fungsi di setiap konfigurasi build yang mungkin kecuali satu-satunya, terlepas dari cara itu dilakukan.

@tema3210 saya tahu. Saya hanya ingin menunjukkan bahwa ide @Lokathor umumnya tidak akan berhasil dalam praktik. Satu-satunya hal yang mungkin berhasil sebagian adalah sebagai berikut, tetapi hanya dengan banyak batasan, yang menurut saya tidak layak untuk jangka panjang:

// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
  /* ..{xbody}.. */
}

// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
  /* ..{xbody}.. */
}

#[cfg(panic_on_err)]
fn x() {
  let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

//..end eval

fn y() -> Result<(), YError> {
  /* ... */
  // #[debug_result] results can't be matched on and can't be assigned to a
  // variable, they only can be used together with `?`, which would create
  // an asymetry in the type system, but could otherwise work, althrough
  // usage would be extremely restricted.
  x()?;
  // e.g. the following would fail to compile because capturing the result
  // is not allowed
  if let Err(z) = x() {
    // ...
  }
}

@zserik Mungkinkah itu benar-benar berbentuk seperti ini?

#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
  let _: () = x_inner().unwrap();
}

#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

oke, ide bagus.

Saya tidak begitu yakin apakah ini adalah sesuatu yang perlu diperhitungkan sebelum stabilisasi tetapi saya tertarik untuk menerapkan jejak pengembalian kesalahan dan saya pikir ini melibatkan perubahan pada sifat Try atau setidaknya implikasi yang disediakan untuk Result untuk membuatnya bekerja. Akhirnya saya berencana untuk mengubah ini menjadi RFC tetapi saya ingin memastikan bahwa sifat coba tidak distabilkan dengan cara yang membuat ini tidak mungkin untuk ditambahkan nanti jika perlu beberapa saat untuk menulis kata RFC. Ide dasarnya adalah ini.

Anda memiliki sifat yang Anda gunakan untuk meneruskan info pelacakan yang menggunakan spesialisasi dan impl default untuk T untuk mempertahankan kompatibilitas mundur

pub trait Track {
    fn track(&mut self, location: &'static Location<'static>);
}

default impl<T> Track for T {
    fn track(&mut self, _: &'static Location<'static>) {}
}

Dan kemudian Anda memodifikasi implementasi Try untuk Result untuk menggunakan track_caller dan meneruskan informasi ini ke tipe dalam,

    #[track_caller]
    fn from_error(mut v: Self::Error) -> Self {
        v.track(Location::caller());
        Self::Err(v)
    }

Dan kemudian untuk tipe yang ingin Anda kumpulkan jejak baliknya untuk Anda terapkan Track

#[derive(Debug, Default)]
pub struct ReturnTrace {
    frames: Vec<&'static Location<'static>>,
}

impl Track for ReturnTrace {
    fn track(&mut self, location: &'static Location<'static>) {
        self.frames.push(location);
    }
}

Penggunaan akhirnya terlihat seperti ini

#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};

fn main() {
    let trace = match one() {
        MyResult::Ok(()) => unreachable!(),
        MyResult::Err(trace) => trace,
    };

    println!("{:?}", trace);
}

fn one() -> MyResult<(), ReturnTrace> {
    try { two()? }
}

fn two() -> MyResult<(), ReturnTrace> {
    MyResult::Err(ReturnTrace::default())?
}

Dan output dari versi backtrace yang sangat menyebalkan terlihat seperti ini

ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }

Dan inilah bukti konsepnya dalam tindakan https://github.com/yaahc/error-return-traces

Saya pikir hanya satu jenis kesalahan yang dapat kami konversi untuk mencoba pelaksana sifat mungkin tidak cukup, jadi, kami dapat menyediakan antarmuka seperti ini:

trait Try{
     type Error;
     type Ok;
     fn into_result(self)->Result<Self::Ok,Self::Error>;
     fn from_ok(val: Self::Ok)->Self;
     fn from_error<T>(val: T)->Self;
}

Catatan, di sana kompiler dapat memonomorfisasi from_error , menghindari panggilan From::from , dan seseorang dapat memberikan metode impl untuk berbagai jenis kesalahan secara manual, menghasilkan kemampuan operator ? untuk "membuka" berbeda jenis kesalahan.

 fn from_error<T>(val: T)->Self;

Seperti yang tertulis, pelaksana harus menerima _any_ sized T , tidak dibatasi. Jika Anda bermaksud membiarkan ini dibatasi khusus, itu harus menjadi parameter sifat, seperti Try<T> . Ini mirip dengan kombinasi TryBlock / Bubble<Other> yang dimiliki @scottmcm di https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 .

Seperti yang tertulis, pelaksana harus menerima _any_ berukuran T , tidak dibatasi. Jika Anda bermaksud membiarkan ini dibatasi khusus, itu harus menjadi parameter sifat, seperti Try<T> . Ini mirip dengan kombinasi TryBlock / Bubble<Other> yang dimiliki @scottmcm di #42327 (komentar) .

Maksud saya penggunaan akan terlihat seperti ini:

trait Try{
     //same from above
}
struct Dummy {
    a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
    type Ok=();
    type Error=();

    fn into_result(self)->Result<Self::Ok,Self::Error>{
        std::result::Result::Ok(())
    }
    fn from_ok(val: Self::Ok)->Self{
        Self{a: 0u8}
    }
    fn from_error<T>(val: Err1)->Self where T == Err1{
        Self{a: 1u8}
    }
    fn from_error<T>(val: Err2)->Self where T == Err2{
        Self{a: 2u8}
    }
}

anda harus membagi Try dan TryFromError. Saya suka itu lebih dari proposal asli, tetapi saya pikir itu akan membutuhkan RFC baru.

(dan saya masih berpikir itu seharusnya disebut "menyebarkan" alih-alih "mencoba", tapi saya ngelantur)

@tema3210 Saya pikir saya mengerti maksud Anda, tapi itu tidak valid Rust.

@SoniEx2

anda harus membagi Try dan TryFromError.

Benar, itu sebabnya saya menyebutkan desain TryBlock / Bubble<Other> . Kita dapat memperdebatkan pilihan penamaan itu, tetapi idenya adalah bahwa pengembalian awal tidak selalu tentang _errors_, per se. Misalnya, banyak metode internal Iterator menggunakan tipe LoopState kustom. Untuk sesuatu seperti find , menemukan apa yang Anda cari bukanlah kesalahan, tetapi kami ingin menghentikan iterasi di sana.

https://github.com/rust-lang/rust/blob/3360cc3a0ea33c84d0b0b1163107b1c1acbf2a69/src/libcore/iter/mod.rs#L375 -L378

Kita dapat memperdebatkan pilihan penamaan itu, tetapi idenya adalah bahwa pengembalian awal tidak selalu tentang kesalahan, itu sendiri.

tepatnya mengapa saya tidak suka nama "coba" dan lebih suka nama "menyebarkan", karena "menyebarkan" pengembalian "awal" :p

(tidak yakin apakah ini masuk akal? Terakhir kali saya membahas ini, hal "menyebarkan" hanya masuk akal bagi saya untuk beberapa alasan dan saya tidak pernah bisa menjelaskannya kepada orang lain.)

Apakah sifat ini akan membantu ketika mencoba untuk menimpa perilaku default ? fe untuk menambahkan log hook fe untuk mencatat informasi debug (seperti file/nomor baris)?

Saat ini didukung untuk menimpa makro stdlib, tetapi tampaknya operator ? tidak dikonversi ke makro try! secara eksplisit. Itu sangat disayangkan.

@stevenroose Untuk menambahkan dukungan untuk itu semata-mata ke sifat Try , itu akan membutuhkan modifikasi dari sifat Try untuk memasukkan informasi lokasi file tentang lokasi di mana ? "terjadi" .

@stevenroose Untuk menambahkan dukungan untuk itu semata-mata ke sifat Try , itu akan membutuhkan modifikasi dari sifat Try untuk memasukkan informasi lokasi file tentang lokasi di mana ? "terjadi" .

Ini tidak benar, #[track_caller] dapat digunakan pada impls sifat meskipun definisi sifat tidak menyertakan anotasi

@stevenroose untuk menjawab pertanyaan Anda, ya, meskipun jika Anda tertarik untuk mencetak semua ? lokasi kesalahan menyebar melalui Anda harus memeriksa kesalahan kembali jejak komentar dari atas

https://github.com/rust-lang/rust/issues/42327#issuecomment -619218371

Saya tidak yakin apakah ada yang sudah menyebutkan ini, akankah kita impl Try for bool , mungkin Try<Ok=(), Error=FalseError> ?
Sehingga kita bisa melakukan hal seperti ini

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.is_valid()?; // bool
  x.transform()?; // Result
  true
}

Sekarang saya harus menggunakan tipe pengembalian Option<()> dalam banyak kasus di mana saya pikir ? dapat membuat kode lebih jelas.

Saya tidak yakin apakah ada yang sudah menyebutkan ini, akankah kita impl Try for bool , mungkin Try<Ok=(), Error=FalseError> ?

Ini akan membuat Try berperilaku sebagai operator && pada bool s. Seperti yang telah ditunjukkan orang lain di atas, ada juga kasus penggunaan untuk hubungan arus pendek pada kesuksesan, dalam hal ini Try harus berperilaku sebagai || . Karena operator && dan || tidak lagi mengetik, saya juga tidak melihat banyak keuntungan dari implementasi ini.

@calebsander terima kasih atas balasannya.
Itu benar untuk beberapa kasus sederhana, tetapi saya tidak berpikir itu sering terjadi, terutama kita tidak akan pernah memiliki pernyataan penugasan seperti let x = v.get_mut(key)? dalam ekspresi.
Jika && dan || selalu berhasil, kita juga bisa bermain dengan .unwrap_or_else() , .and_then() untuk Option dan Error kasus.
Bisakah Anda mengungkapkan kode yang mengalir dalam && dan || ?

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.not_valid().not()?; // bool
  for i in x.iter() {
    if i == 1 { return true }
    if i == 2 { return false }
    debug!("get {}" i);
  }
  let y = x.transform()?; // Result
  y == 1
}

Untuk beberapa kondisi true berarti gagal sedangkan false berarti sukses, !expr? mungkin membingungkan, tetapi kita dapat menggunakan expr.not()? untuk melakukan trik (catatan: untuk ops::Try , kami selalu memiliki false untuk Error )

Itu benar untuk beberapa kasus sederhana, tetapi saya tidak berpikir itu sering terjadi, terutama kita tidak akan pernah memiliki pernyataan penugasan seperti let x = v.get_mut(key)? dalam ekspresi.

Yah, kecuali saya salah memahami proposal Anda, hanya menerapkan Try<Ok = (), Error = FalseError> untuk bool tidak akan mengizinkan ini. Anda juga perlu impl From<NoneError> for FalseError agar ? operator dapat mengonversi None menjadi false . (Dan juga, jika Anda ingin menerapkan ? ke Result<T, E> di dalam fungsi yang mengembalikan bool , Anda perlu impl From<E> for FalseError . Saya pikir seperti itu implementasi selimut akan menjadi masalah.) Anda juga dapat menggunakan some_option().ok_or(false)? dan some_result().map_err(|_| false)? jika Anda setuju dengan nilai kembalian Result<bool, bool> (yang dapat Anda tutup dengan .unwrap_or_else(|err| err) ).

Mengesampingkan masalah konversi Try kesalahan lain menjadi bool , implementasi Try Anda usulkan untuk bool hanyalah operator && . Misalnya, ini setara

fn using_try() -> bool {
    some_bool()?;
    some_bool()?;
    some_bool()
}

dan

fn using_and() -> bool {
    some_bool() &&
    some_bool() &&
    some_bool()
}

(Diakui, aliran kontrol seperti if dan loop yang mengembalikan non- () kurang mudah untuk diterjemahkan.)

Untuk beberapa kondisi true berarti gagal sedangkan false berarti sukses, !expr? mungkin membingungkan, tetapi kita dapat menggunakan expr.not()? untuk melakukan trik (catatan: untuk ops::Try , kami selalu memiliki false untuk Error )

Tidak jelas bagi saya bahwa false harus mewakili kasus Error . (Misalnya, Iterator.all() ingin false melakukan hubungan pendek, tetapi Iterator::any() menginginkan true sebagai gantinya.) Seperti yang Anda tunjukkan, Anda bisa mendapatkan berlawanan dengan perilaku hubungan arus pendek dengan membalikkan nilai yang diteruskan ke ? (dan juga membalikkan nilai pengembalian fungsi). Tapi saya tidak berpikir itu menghasilkan kode yang sangat mudah dibaca. Mungkin lebih masuk akal untuk memisahkan struct s And(bool) dan Or(bool) yang mengimplementasikan dua perilaku yang berbeda.

Demikian pula, jika Anda ingin melamar? untuk Hasildi dalam fungsi yang mengembalikan bool, Anda perlu mengimplikasikan Dariuntuk FalseError. Saya pikir implementasi selimut seperti itu akan bermasalah.

Tidak, saya tidak ingin impl From<T> for FalseError , mungkin kita bisa melakukan result.ok()?

Tidak jelas bagi saya bahwa false harus mewakili kasus Kesalahan.

Saya pikir itu wajar ketika kita memiliki bool::then yang memetakan true ke Some .

Maafkan saya jika saya melewatkannya - mengapa NoneError impl std::error::Error ? Ini membuatnya tidak berguna untuk fungsi apa pun yang mengembalikan Box<dyn Error> atau serupa.

Maafkan saya jika saya melewatkannya - mengapa NoneError impl std::error::Error ? Ini membuatnya tidak berguna untuk fungsi apa pun yang mengembalikan Box<dyn Error> atau serupa.

Bukan ahli di sini, tetapi saya dapat melihat masalah yang signifikan dengan mencoba membuat pesan kesalahan yang berguna untuk "sesuatu adalah None dan Anda mengharapkannya menjadi Some " (yang pada dasarnya adalah apa yang Anda akan diperoleh di sini -- beberapa pesan diagnostik). Dalam pengalaman saya, selalu masuk akal untuk menggunakan kombinator Option::ok_or_else untuk menggunakan jenis kesalahan lain sebagai gantinya, karena sebagai kode panggilan saya biasanya memiliki konteks yang jauh lebih baik untuk diberikan.

Saya setuju dengan @ErichDonGubler. Sangat menjengkelkan tidak dapat menggunakan ? dengan Option , namun itu untuk alasan yang bagus, dan, sebagai pengguna bahasa, saya pikir adalah kepentingan terbaik semua orang untuk menangani kesalahan dengan benar. Melakukan pekerjaan ekstra untuk mengikat konteks kesalahan ke None alih-alih hanya melakukan ? benar-benar menjengkelkan, tetapi memaksa kesalahan komunikasi yang benar, yang sangat berharga.

Satu-satunya waktu saya mengizinkan None ditafsirkan sebagai kesalahan adalah pada pembuatan prototipe yang sangat kasar. Saya pikir, jika kita menambahkan sesuatu seperti panggilan Option::todo_err() , itu akan mengembalikan Ok dari nilai internal, tetapi akan panik pada None . Ini sangat kontra-intuitif sampai Anda menganalisis kebutuhan mode pembuatan kode "prototipe kasar". Ini sangat mirip dengan Ok(my_option.unwrap()) , tetapi tidak memerlukan pembungkus Ok(...) . Selain itu, secara eksplisit menentukan sifat "todo", sehingga mengkomunikasikan maksud untuk menghapus kode ini dari logika produksi, menggantinya dengan pengikatan kesalahan yang tepat.

tapi akan panik pada None

Sangat merasa bahwa kita harus membiarkan ini unwrap . Jika kita menambahkan todo_err, itu akan mengembalikan jenis kesalahan yang sebenarnya, yang menimbulkan pertanyaan tentang jenis kesalahan apa yang akan dikembalikan.

Saya juga menduga fn todo_err(self) -> Result<Self, !> akan memiliki masalah yang jelas membutuhkan ! untuk menstabilkan, yang eh, well, Suatu hari nanti.

Usecase saya memang prototipe kasar di mana saya tidak terlalu peduli dengan kesalahannya. Tidakkah seluruh NoneError mengalami masalah yang Anda daftarkan ini? Jika diputuskan bahwa itu harus ada (yang menurut saya adalah hal yang baik, setidaknya untuk pembuatan prototipe), saya percaya itu harus menyiratkan Error seperti yang dinamai menjadi satu.

Karena tipe yang tidak memiliki impl ini saya terpaksa hanya menampar .ok_or("error msg") di atasnya, yang bekerja juga tetapi kurang nyaman.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat