Признак Try
из https://github.com/rust-lang/rfcs/pull/1859; реализовано в PR https://github.com/rust-lang/rust/pull/42275.
Отделите от https://github.com/rust-lang/rust/issues/31436 для ясности (согласно https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)
Iterator::try_fold
fold
было реализовано в терминах try_fold
, чтобы не нужно было переопределять оба значения.)Пара штук велосипедных навесов:
Есть ли у нас особая мотивация называть связанный тип Error
вместо Err
? Назвав его Err
будет совпадать с Result
: другой уже называется Ok
.
Есть ли у нас особая мотивация иметь отдельные методы from_error
и from_ok
вместо одного from_result
который был бы более симметричным с into_result
который является другим? половина черты?
( обновленная версия ссылки на игровую площадку из RFC )
@glaebhoerl
Error
vs Err
обсуждалось относительно TryFrom
в https://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 и https: // github.com/rust-lang/rust/pull/40281; Я предполагаю, что это имя было выбрано здесь по аналогичным причинам.Result
который они пытаются превратить в T:Try
. Я предпочитаю Try::from_ok
и Try::from_error
всегда вызывать Try::from_result(Ok(
и Try::from_result(Err(
, и я счастлив просто использовать эти два метода вместо записи совпадения. Возможно, это потому, что я думаю о into_result
не как о Into<Result>
, а как о «пройдено это или нет?», А конкретный тип - Result
как неважная деталь реализации. (Я не хочу предлагать или повторно открывать «должен быть новый тип для производственной ценности и раннего возврата».) А что касается документации, мне нравится, что from_error
говорит о ?
(или, в конечном итоге, throw
), а from_ok
говорит об успешной упаковке (# 41414), а не об их обоих в одном методе.Я не уверен, что это правильный форум для этого комментария, пожалуйста, перенаправьте меня, если это не так: smiley :. Возможно, это должно было быть на https://github.com/rust-lang/rfcs/pull/1859, и я пропустил период комментариев; ой!
Мне было интересно, можно ли разделить черту Try
или удалить метод into_result
; для меня в настоящее время Try
чем-то напоминает сумму признаков FromResult
(содержащих from_error
и from_ok
) и IntoResult
(содержащих into_result
).
FromResult
обеспечивает очень эргономичный ранний выход с помощью оператора ?
, что, на мой взгляд, является убийственным вариантом использования этой функции. Я думаю, что IntoResult
уже можно аккуратно реализовать с помощью методов для каждого варианта использования или как Into<Result>
; мне не хватает полезных примеров?
Следуя RFC для Try
trait , expr?
можно десахарировать как:
match expr { // Removed `Try::into_result()` here.
Ok(v) => v,
Err(e) => return Try::from_error(From::from(e)),
}
Я рассмотрел мотивирующие примеры Future
и Option
.
Мы можем реализовать FromResult
естественно для struct FutureResult: Future
как:
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)
}
}
Предполагая разумную реализацию для ?
, которая вернется из impl Future
оцениваемой функции при применении к Result::Err
тогда я могу написать:
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> {}
Это именно то, что я пытался реализовать ранее сегодня, и Try
прибивает! Но если мы попытаемся реализовать текущую Try
для Future
возможно, канонического выбора для into_result
; может быть полезно паниковать, блокировать или опрашивать один раз, но ни один из них не кажется универсальным. Если на Try
не было into_result
я могу реализовать ранний выход, как указано выше, и если мне нужно преобразовать Future
в Result
(а затем в любой Try
) Я могу преобразовать его подходящим методом (вызвать метод wait
для блокировки, вызвать какой-нибудь poll_once() -> Result<T,E>
и т. д.).
Option
- аналогичный случай. Мы реализуем from_ok
, from_err
естественным образом, как в RFC Try
trait , и можем преобразовать Option<T>
в Result<T, Missing>
просто с помощью o.ok_or(Missing)
или удобный метод ok_or_missing
.
Надеюсь, это поможет!
Возможно, я очень опаздываю на все это, но в эти выходные мне пришло в голову, что ?
имеет довольно естественную семантику в тех случаях, когда вы хотите замкнуть _success_.
fn fun() -> SearchResult<Socks> {
search_drawer()?;
search_wardrobe()
}
Но в данном случае не подходит именование методов трейтов Try
.
Однако это расширило бы смысл оператора ?
.
При создании прототипа try_fold
для вискозы я обнаружил, что мне нужно что-то вроде Try::is_error(&self)
для методов Consumer::full()
. (Это есть в Rayon, потому что потребители в основном являются итераторами push-стиля, но мы все еще хотим устранять ошибки короткого замыкания.) Вместо этого мне пришлось сохранить промежуточные значения как Result<T::Ok, T::Err>
чтобы я мог вызвать Result::is_err()
.
Оттуда я также хотел, чтобы Try::from_result
вернулся к T
в конце, но match
сопоставление с T::from_ok
и T::from_error
не слишком плохо.
Черта Try
может предоставить эргономичный метод from_result
если требуются from_ok
и from_error
. Или наоборот. Из приведенных примеров я вижу возможность предложить и то, и другое.
Поскольку PR # 42275 был объединен, означает ли это, что проблема решена?
@skade Обратите внимание, что тип может определять то, что является короткозамкнутым, как LoopState
для некоторых внутренних компонентов Iterator
. Возможно, это было бы более естественно, если бы в Try
говорилось о «продолжении или прерывании», а не об успехе или неудаче.
@cuviper Версия, которая мне в итоге понадобилась, была либо типа Ok
либо типа Error
-in-original. Я надеюсь, что деструктуризация и перестройка - это достаточно общая вещь, которая будет хорошо оптимизирована, и куча специальных методов для этого трейта не понадобится.
@ErichDonGubler. Это проблема отслеживания, поэтому она не решается, пока соответствующий код не станет стабильным.
Отчет об опыте работы:
Я был немного разочарован, пытаясь применить эту черту на практике. Несколько раз я испытывал искушение определить свой собственный вариант для Result
по какой-то причине, но каждый раз я заканчивал просто использованием Result
в конце, в основном из-за реализации Try
слишком раздражает. Я не совсем уверен, хорошо это или нет!
Пример:
В новом решателе для Chalk VM я хотел иметь перечисление, которое указывает результат решения «цепочки». У этого было четыре возможности:
enum StrandFail<T> {
Success(T),
NoSolution,
QuantumExceeded,
Cycle(Strand, Minimums),
}
Я хотел, чтобы ?
применительно к этому перечислению разворачивало "успех", но распространял все остальные неудачи вверх. Однако, чтобы реализовать черту Try
, мне пришлось бы определить своего рода «остаточный» тип, который инкапсулирует только случаи ошибок:
enum StrandFail {
NoSolution,
QuantumExceeded,
Cycle(Strand, Minimums),
}
Но как только у меня появится этот тип, я могу сделать StrandResult<T>
псевдонимом:
type StrandResult<T> = Result<T, StrandFail>;
и это то, что я сделал.
Это не обязательно кажется хуже, чем наличие одного перечисления, но это действительно кажется немного странным. Обычно, когда я пишу документацию для функции, например, я не «группирую» результаты по «ок» и «ошибка», а говорю о различных возможностях как о «равных». Например:
/// 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.
Обратите внимание, что я не сказал «мы возвращаем Err(StrandFail::NoSolution)
. Это потому, что Err
просто ощущается как раздражающий артефакт, который я должен добавить.
(С другой стороны, текущее определение поможет читателям узнать, как ведет себя ?
, не обращаясь к Try
impl.)
Думаю, такой результат не так уж и удивителен: текущий Try
impl заставляет вас использовать ?
на вещах, изоморфных Result
. Это единообразие не случайно, но, как следствие, раздражает использование ?
с вещами, которые не являются "просто Result
". (В этом отношении, по сути, это та же проблема, которая порождает NoneError
- необходимость искусственно определять тип, представляющий «отказ» Option
.)
Я также отмечу, что в случае StrandFail
я не особо хочу преобразование From::from
которое получают обычные результаты, хотя мне это не мешает.
Является ли связанный тип Try::Error
когда-либо доступным / использованным пользователем напрямую? Или это просто необходимо как часть обессахаривания ?
? В последнем случае я не вижу реальной проблемы в том, чтобы просто определить это «структурно» - type Error = Option<Option<(Strand, Minimums)>>
или что-то в этом роде. (Необходимость вычислить структурный эквивалент «половины отказа» определения enum
невелика, но кажется менее раздражающей, чем необходимость переделывать весь общедоступный API.)
Я тоже не слежу. Я успешно реализовал Try для типа с представлением внутренней ошибки, и мне казалось естественным, что Ok
и Error
относятся к одному и тому же типу. Вы можете увидеть реализацию здесь: https://github.com/kevincox/ecl/blob/8ca7ad2bc4775c5cfc8eb9f4309b2666e5163e02/src/lib.rs#L298 -L308
На самом деле кажется, что существует довольно простой шаблон для выполнения этой работы.
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) }
}
}
Если вы хотите развить успех, должно сработать что-то вроде этого:
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),
}
}
}
Определение структурного типа возможно, но это довольно неприятно. Я согласен, что могу использовать Self
. Мне кажется немного странным, что вы можете затем использовать ?
для преобразования из StrandResult
в Result<_, StrandResult>
и т. Д.
Отчет об отличном опыте, @nikomatsakis! Еще я был недоволен Try
(с другой стороны).
TL / DR : Я думаю, что FoldWhile
понял это правильно, и мы должны удвоить внимание на интерпретацию Break
-vs- Continue
?
вместо того, чтобы говорить о это с точки зрения ошибок.
Дольше:
Я продолжаю использовать Err
для чего-то более близкого к "успеху", чем "ошибка", потому что ?
очень удобно.
При использовании try_fold
для реализации position
(и множества других методов итератора) я так запутался, что было так же, как при использовании Result
(моему мозгу не нравилась find
обнаружил, что это Err
), вместо этого я сделал собственное перечисление с вариантами Break
и Continue
.
При написании tree_fold1
в itertools я получил странную match
что внутренняя функция всегда возвращает Err
. Есть странное несоответствие: None
- это "ошибка" от Iterator::next()
, но в то же время "успех" с fold
, так как вам нужно дойти до конца. должно быть сделано. И очень удобно использовать ?
(ну, try!
в этом коде, поскольку он должен компилироваться на Rust 1.12), чтобы иметь дело с распространением конечного условия.
Итак, я считаю, что описание, которое я написал для Try
, неверно и не должно говорить о «дихотомии успеха / неудачи», а скорее абстрагироваться от «ошибок».
Еще я подумал о том, что мы должны рассмотреть несколько безумных имплсов для Try
. Например, Ordering: Try<Ok = (), Error = GreaterOrLess>
сочетании с "пробными функциями" может позволить cmp
для struct Foo<T, U> { a: T, b: U }
просто быть
fn cmp(&self, other: &self) -> Ordering try {
self.a.cmp(&other.a)?;
self.b.cmp(&other.b)?;
}
Я еще не знаю, хорошее это безумие или плохое: смеется: В этом определенно есть некоторая элегантность, хотя, раз уж вещи стали другими, вы знаете, что они разные. И попытки приписать «успех» или «ошибку» какой-либо стороне этого не подходят.
Я также отмечаю, что это еще один случай, когда «преобразование ошибок» бесполезно. Я также никогда не использовал его в приведенных выше примерах «Я хочу? Но это не ошибка». И, конечно, известно, что это вызывает грусть вывода, поэтому мне интересно, должно ли это быть ограничено только Результатом (или потенциально удалено в пользу .map_err(Into::into)
, но это, вероятно, невозможно).
Изменить: О, и все, что заставляет меня задаться вопросом, возможно, ответ на «Я продолжаю использовать Результат для своих ошибок вместо реализации Try
для своего собственного типа» - это «хорошо, это ожидалось».
Изменить 2: это мало чем отличается от https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 выше
Изменить 3: похоже, что вариант Continue
также был предложен в https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250
Мои два цента:
Мне нравится предложение @fluffysquirrels разделить черту на две черты. Один для преобразования в результат, а другой для преобразования из результата. Но я действительно думаю, что мы должны оставить into_result
или эквивалент как часть обессахаривания. И я думаю, что на данный момент мы должны это сделать, поскольку использование Option
в качестве Try
стабилизировалось.
Мне также нравится идея @scottmcm об использовании имен, которые предполагают break / continue, а не error / ok.
Чтобы вставить здесь конкретный код, мне нравится, как это читается:
Это, конечно, не так хорошо, как прямой цикл, но с «методами попытки» он был бы близок:
self.try_for_each(move |x| try {
if predicate(&x) { return LoopState::Break(x) }
}).break_value()
Для сравнения, я считаю, что версия "словаря ошибок" действительно вводит в заблуждение:
self.try_for_each(move |x| {
if predicate(&x) { Err(x) }
else { Ok(()) }
}).err()
Можем ли мы реализовать Display for NoneError? Это позволит ящику с ошибкой автоматически получить From<NoneError> for failure::Error
. См. Https://github.com/rust-lang-nursery/failure/issues/61
Это должно быть изменение на 3 строки, но я не уверен в процессе RFC и тому подобном.
@ cowang4 Я бы хотел попытаться избежать включения какого-либо смешивания Result-and-Option прямо сейчас, так как этот тип нестабилен в основном для того, чтобы наши варианты оставались открытыми. Не удивлюсь, если в итоге мы изменим дизайн Try
и десахара на что-то, что не требует NoneError
...
@scottmcm Хорошо. Я понимаю вашу точку зрения. В конце концов, я бы хотел получить чистый способ приукрасить шаблон возврата Err, когда библиотечная функция возвращает None. Может быть, вы знаете кого-то другого, кроме Try
? Пример:
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(_)
}
Как только я нашел эту экспериментальную функцию и ящик failure
, я, естественно, обратился к:
use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
Ok({
title: pb.file_stem?.to_str()?.to_string()
})
}
Что _почти_ работает, за исключением отсутствия impl Display for NoneError
как я упоминал ранее.
Но, если это не тот синтаксис, который мы хотели бы использовать, тогда, возможно, могла бы быть другая функция / макрос, упрощающий шаблон:
if option.is_none() {
return Err(_);
}
@ cowang4 Я считаю, что это сработает, если вы реализовали From<NoneError>
для failure::Error
, который использовал ваш собственный тип, реализующий Display
.
Однако, вероятно, лучше использовать opt.ok_or(_)?
чтобы вы могли явно указать, какой должна быть ошибка, если для параметра Option установлено значение None. В вашем примере, например, вам может потребоваться другая ошибка, если pb.file_stem
равно None, чем если to_str()
возвращает None.
@tmccombs Я попытался создать свой собственный тип ошибки, но, должно быть, сделал это неправильно. Это было так:
#[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(),
})
}
А потом я попытался использовать свой тип ошибки ...
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`
Хорошо, я только что понял, что он хочет знать, как преобразовать из других типов ошибок в мою ошибку, которую я могу написать в общем виде:
impl<E: failure::Fail> From<E> for SiteError {
fn from(_err: E) -> Self {
SiteError
}
}
Неа...
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;
Хорошо, а как насчет std::error::Error
?
impl<E: std::error::Error> From<E> for SiteError {
fn from(_err: E) -> Self {
SiteError
}
}
Это тоже не работает. Отчасти потому, что он конфликтует с моим 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
Это странно, потому что я думал, что NoneError
не реализует std::error::Error
. Когда я комментирую свои необщие impl From<NoneError>
я получаю:
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`
Надо ли писать все From
вручную. Я думал, что их должен был доставить ящик отказа?
Может мне стоит придерживаться option.ok_or()
Надо ли писать все From вручную. Я думал, что их должен был доставить ящик отказа?
Я не думаю, что это делает ящик отказа. Но я мог ошибаться.
Хорошо, поэтому я повторно проверил ящик с ошибкой, и, если я правильно читаю документацию и разные версии, он предназначен для всегда использования failure::Error
в качестве типа ошибки в вашем Result
s, см. здесь . И он реализует общую черту impl Fail
для большинства типов ошибок:
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
И затем impl From
чтобы он мог Try
/ ?
другие ошибки (например, из std) в общий тип failure::Error
.
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
Но дело в том, что поскольку ошибка, относящаяся к этой проблеме ржавчины, NoneError
, является экспериментальной, она еще не может быть преобразована автоматически, потому что она не реализует черту Display
. И мы этого не хотим, потому что это стирает грань между Option
s и еще Result
s. В конечном итоге все это, вероятно, будет переработано и разобрано, но пока я буду придерживаться техник без сахара, которым научился.
Спасибо всем за помощь. Я потихоньку изучаю Rust! :улыбка:
Я буду придерживаться техник без сахара, которым научился.
: +1: Честно говоря, я думаю, что .ok_or(...)?
и останется (или, конечно, .ok_or_else(|| ...)?
). Даже если NoneError
_had_ a Display
impl, что он скажет? «Чего-то там не было»? Это не большая ошибка ...
Попытка приблизиться к конкретному предложению ...
Мне начинает нравиться альтернатива TrySuccess
. Что интересно, я думаю, что _nobody_, в том числе и я, изначально понравился этот вариант - его даже нет в окончательной версии RFC. Но, к счастью, он живет в истории: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md# using -an-associated-type-for-the-success -ценить
Мне кажется, что самым большим возражением против этого была обоснованная жалоба на то, что целая основная черта только для связанного типа ( trait TrySuccess { type Success; }
) была излишней. Но с возвращением блоков catch
try
(https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777) для выполнения оклейки (как в RFC) , внезапно у него есть прямое и важное применение: это черта, которая контролирует эту упаковку. В сочетании с целью « ?
всегда должны приводить к одному и тому же типу», что, как мне кажется, было желательным, эта черта, кажется, лучше удерживает свой вес. Строуман:
trait TryContinue {
type Continue;
fn from_continue(_: Self::Continue) -> Self;
}
Этот связанный тип, который будет возвращен оператором ?
, также явно должен существовать. Это означает, что он не попадает в раздражение «должен был определить своего рода« остаточный »тип», который сформулировал Нико . И быть ()
разумно, даже часто, поэтому избегайте искажений, подобных NoneError
.
Изменить: для велоспорта «возврат» может быть хорошим словом, так как это то, что происходит, когда вы return
из метода try
(также известного как ok-wrapping). return
- это также имя оператора монады для этого, iiuc ...
Изменить 2: Мои мысли о других чертах / методах еще не стабилизировались, @tmccombs.
@scottmcm для ясности, или вы предлагаете следующие две черты?
trait TryContinue {
type Continue;
fn from_continue(_: Self::Continue) -> Self;
}
trait Try<E>: TryContinue {
fn try(self) -> Result<Self::Continue, E>
}
И удаление x?
будет выглядеть примерно так:
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
}
и try { ...; expr}
десахарирует до чего-то вроде:
{
...
TryContinue::from_continue(expr);
}
@scottmcm Я тоже считаю этот вариант более привлекательным, если учесть ок-обертку =)
Переходя к следующему признаку, я думаю, что для ?
будет следующим (по модулю массового байкшеддинга):
trait Try<Other: TryContinue = Self>: TryContinue + Sized {
fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}
enum ControlFlow<C, B> {
Continue(C),
Break(B),
}
Разное обоснование:
TryContinue
в качестве типа аргумента, чтобы тип выражения x?
всегда был одинаковым.Self
для простых случаев, таких как try_fold
которые проверяют и возвращают один и тот же тип, поэтому все в порядке с T: Try
.TryContinue
потому что, если тип, используемый в качестве возвращаемого типа, допускает ?
в своем теле, он также должен поддерживать ok-wrapping.?
является универсальным типом, так что, как и TryContinue
, он "производит Self
из чего-то"Полная демонстрация концепции, включая макросы для try{}
/ ?
и имплицитные для Option
, Result
и Ordering
: https : //play.rust-lang.org/? gist = 18663b73b6f35870d20fd172643a4f96 & version = stable (спасибо @nikomatsakis за создание оригинала год назад 🙂)
Минусы:
Result
все еще есть параметры фантомного типа, которые мне не очень нравятсяLessOrGreater
в Ordering
impl.From
StrandResult
любом случае не заботится, для чего-то вроде SearchResult
это было бы странно, поскольку путь Break - это путь успеха, и это может в конечном итоге помочь в выводе в в любом случае вложенные кейсы ?
.throw
?
как Ok(x) => x, Err(r) => throw e.into()
которое мне действительно понравилосьthrow;
быть способом получения None
(через что-то вроде impl<T> Throw<()> for Option<T>
), что намного лучше, чем throw NoneError;
throw LessOrGreater::Less
было бы _действительно_ глупо.Изменить: Ping @glaebhoerl , чье мнение я хотел бы по этому
Изменить 2: Также cc @ colin-kiegel для этого утверждения из https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652:
Интересно, может ли эссенциалистский подход перенять некоторую элегантность редукционистов, не жертвуя вышеуказанными целями.
Мне очень нравится это предложение.
Не так очевидно, какой будет синтаксис throw
Игнорируя багаж ключевого слова throw из других языков, слово «throw» имеет смысл, поскольку вы «перебрасываете» значение в более высокую область видимости. Python и scala также используют исключения для потока управления, кроме случаев ошибок (хотя в случае scala вы обычно не используете try / catch напрямую), поэтому есть некоторый прецедент использования throw для успешных путей.
@tmccombs
Игнорируя багаж ключевого слова throw из других языков, слово «throw» имеет смысл, поскольку вы «перебрасываете» значение в более высокую область видимости.
Хотя в нем такой же багаж, я подозреваю, что "поднять" лучше:
throw (v): положить или заставить пойти или войти в какое-то место, положение, состояние и т. д., как будто бросая:
Raise (v): перейти на более высокую позицию; поднимать; поднять
Может быть способ объединить «рейз» с логикой ?
, учитывая, что это повышение может также означать «собирать». Что-то вроде: Ok(v) => v, Err(e) => raise From::from(e)
, где raise
имитирует совпавший шаблон (например, для шаблона Err(e)
это синтаксическая магия для ControlFlow::Break(Err(...))
).
Я подозреваю, что "рейз" больше подходит
хорошая точка зрения
@scottmcm
Не так очевидно, какой будет синтаксис throw
Есть ли причина, по которой у нас не может быть from_err(value: Other)
?
ОБНОВЛЕНИЕ: Возможно, я запутался в роли Other
самом деле. Мне нужно еще немного изучить эту черту. знак равно
@nikomatsakis
Есть ли причина, по которой у нас не может быть
from_err(value: Other)
?
Ну, Other
- это целый тип ?
-able (например, Result
), поэтому я бы не хотел, чтобы throw Ok(4)
работал. (И это должно быть полное разделение, чтобы избежать принудительного введения искусственного типа ошибки.) Например, я думаю, что наше текущее взаимодействие Option-Result будет эквивалентно этому (и обратному):
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))),
}
}
}
Моя текущая склонность к throw
была бы такой:
trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }
с участием
throw $expr ==> break 'closest_catch TryBreak::from_break($expr)
TryContinue
так что ok-wrapping также доступен для возвращаемого типа метода, в котором он используется.TryBreak<TryFromIntError>
и TryBreak<io::Error>
для типа.impl<T> TryBreak<()> for Option<T>
чтобы включить только throw;
кажется правдоподобным, в отличие от связанного типа ошибки ()
.impl<T> TryBreak<!> for T
тоже было бы неплохо, но, вероятно, бессвязно.(В сторону: эти черты и имена методов ужасны; пожалуйста, помогите !)
Мои мысли о других проблемах, поднятых здесь, еще не превратились в легко формулируемую форму, но в отношении проблем вывода типа вокруг десугарирования From::from()
(я не могу вспомнить, обсуждалось ли это также где-то в другом месте в последнее время , или только здесь?):
Инстинктивное ощущение (из опыта Haskell), что «такая неоднозначность вывода типа лжи» было одной из (хотя и не главной) причин, по которым я не хотел иметь преобразование From
как часть исходного RFC. Теперь, когда он запекся в пирог, мне интересно, не могли бы мы попробовать получить этот пирог и съесть его, просто «просто» добавив для него специальное правило по умолчанию в процесс определения типа?
То есть, я думаю: всякий раз, когда мы видим From::from()
[необязательно: тот, который был вставлен с помощью удаления сахара ?
, а не написан вручную], и мы точно знаем один из двух типов (ввод vs. output), в то время как другой неоднозначен, мы по умолчанию используем другое значение, аналогичное первому. Другими словами, я думаю, когда неясно, какой impl From
использовать, мы по умолчанию используем impl<T> From<T> for T
. Думаю, это всегда то, чего вы на самом деле хотите? Это может быть немного спонтанно, но если это действительно сработает, преимущества, похоже, окупаются ИМХО.
(Я также думал, что From
уже является элементом языка, именно из-за удаления сахара ?
, но похоже, что это не так? В любом случае, это уже в некотором роде особенное по этой причине .)
в предложении @scottmcm From::from()
_не_ часть процесса обессахаривания, скорее это реализация Try
для Result
.
@tmccombs Я не предлагал поправку к его предложению.
В try{}
RFC (https://github.com/rust-lang/rfcs/pull/2388) обсуждаются блоки try
где результат не важен. Эта альтернатива, кажется, справляется с этим прилично, поскольку она может полностью игнорировать типы ошибок, если того пожелает, позволяя
let IgnoreErrors = try {
error()?;
none()?;
};
Доказательная реализация с использованием тех же характеристик, что и раньше: https://play.rust-lang.org/?gist=e0f6677632e0a9941ed1a67ca9ae9c98&version=stable
Я думаю, что здесь есть некоторые интересные возможности, особенно потому, что пользовательская реализация может, скажем, принимать только результаты и связывать E: Debug
поэтому она автоматически регистрирует любую возникающую ошибку. Или версия может быть специально предназначена как возвращаемый тип для main
в сочетании с Termination
который "просто работает", чтобы позволить вам использовать ?
без сигнатуры сложного типа (https : //github.com/rust-lang/rfcs/issues/2367).
У меня были проблемы, аналогичные тем, о которых свидетельствует @nikomatsakis, использующий существующую версию Try
. По конкретным вопросам см. Https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108.
Определения признаков, предложенные @scottmcm, решают эти проблемы. Однако они кажутся более сложными, чем необходимо. Я попытался реализовать их заново и придумал следующее:
#[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>;
}
Основное изменение заключается в том, что связанный с Continue
тип относится к типу Try
а не к FromTry
(ранее TryContinue
). Помимо упрощения определений, это имеет то преимущество, что Try
может быть реализовано независимо от FromTry
, которое я считаю более распространенным, и что реализация FromTry
упрощается после того, как Try
Реализовано Try
и FromTry
были реализованы одновременно, мы можем просто переместить метод from_try
в Try
)
Посмотрите полную игровую площадку с реализациями для Result
и Option
а также Outcome
Rocket на этой игровой площадке .
Спасибо за отчет, @SergioBenitez! Эта реализация соответствует альтернативной версии "перевернуть параметры типа" исходного предложения по признаку в RFC 1859: https://github.com/rust-lang/rfcs/blob/f89568b1fe5db4d01c4668e0d334d4a5abb023d8/text/0000-try-trait.md#unresolved -вопросов
Больше всего теряется то свойство, что typeof(x?)
зависит только от typeof(x)
. Отсутствие этого свойства было одной из проблем оригинала («Я немного беспокоюсь о читабельности кода в этих строках» https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967) и преимуществом окончательного редукционистского предложения («Для любого данного типа T? может дать ровно один вид значения ok / error» https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310). Конечно, были также аргументы в пользу того, что указанное свойство является ненужным или слишком ограничивающим, но в окончательном резюме до FCP оно все еще присутствовало в качестве преимущества (https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466).
Помимо упрощения определений, это имеет то преимущество, что
Try
может быть реализовано независимо отFromTry
, которое я считаю более распространенным.
Конечно, сегодня from_ok
встречается реже, поскольку Try
и do catch
нестабильны. И даже если бы do catch
были стабильными, я согласен, что он будет использоваться менее ?
целом (поскольку большинство таких блоков содержат несколько ?
s).
Однако с точки зрения трейта и его операций, я думаю, что «обернуть значение« продолжать работу »в тип носителя» является абсолютно важной частью ?
. Сегодня редко используют эту черту - просто вместо этого используют Ok(...)
или Some(...)
- но это критично для общего использования. Например, try_fold
:
Если функция собирается возвращать тип носителя, очень важно, чтобы был способ поместить интересующее значение в этот тип носителя. И, что немаловажно, я не думаю, что это будет трудностью. У него очень естественное определение результата, варианта, результата и т. Д.
@Centril также ранее указывал, что "обернуть скаляр в носитель" также теоретически является более простой конструкцией. Например, Haskell называет это классом типов Pointed
, хотя я не думаю, что мы хотим обобщать его _that_ далеко в Rust: разрешение try { 4 }
↦ vec![4]
кажется мне излишним. .
Я также представляю себе будущее, в котором, подобно функциям async
которые предлагаются для переноса значения блока в будущее, у нас могут быть функции try
которые превращают значение блока в ошибочный тип. Опять же, TryContinue
является критической частью - такая функция может даже не использовать ?
, если у нас есть конструкция throw
.
Так что все это, к сожалению, скорее философское, чем конкретное. Дайте мне знать, имеет ли это какой-то смысл или вы думаете об обратном в любой из частей: слегка_smiling_face:
Изменить : извиняюсь, если вы получили электронное письмо с ранней версией этого; Я нажал "комментарий" слишком рано ...
Больше всего теряется свойство, согласно которому typeof (x?) Зависит только от typeof (x).
Ах да, конечно. Спасибо что подметил это.
Конечно, были также аргументы в пользу того, что указанное свойство является ненужным или слишком ограничивающим, но в окончательном резюме до FCP оно все еще присутствовало в качестве преимущества (rust-lang / rfcs # 1859 (комментарий)).
Есть ли какие-то конкретные примеры, когда это может быть слишком ограничительным?
Если функция собирается возвращать тип носителя, очень важно, чтобы был способ поместить интересующее значение в этот тип носителя. И, что немаловажно, я не думаю, что это будет трудностью.
Я считаю, что это справедливый анализ. Я согласен.
@SergioBenitez Из https://github.com/rust-lang/rfcs/pull/1859#issuecomment -279187967
Итак, вопрос в том, достаточно ли предложенных ограничений? Есть ли хорошее использование контекста случая ошибки при определении типа успеха? Возможны ли нарушения?
По своему опыту работы с фьючерсами я могу сказать, что здесь могут быть некоторые полезные случаи. В частности, тип опроса, о котором вы говорите, можно обработать двумя способами. Иногда мы хотим отказаться от варианта NotReady и остаться с по существу значением Result. Иногда нас интересует только готовый вариант, и мы хотим перейти к любому из других вариантов (как в вашем скетче). Если мы позволим типу успеха частично определяться типом ошибки, более правдоподобно поддержать оба этих случая и в основном использовать контекст для определения желаемой декомпозиции.
OTOH, меня немного беспокоит читабельность кода в этих строках. Кажется, что это качественно отличается от простого преобразования компонента ошибки - это означает, что основное совпадение? будет зависеть от контекстной информации.
Таким образом, можно представить себе черту, которая перемещает _ оба_ типа в параметры типа, например
trait Try<T,E> {
fn question(self) -> Either<T, E>;
}
И используйте это, чтобы включить все
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
Но я думаю, что это определенно плохая идея, поскольку это означает, что они не работают.
println!("{}", z?);
z?.method();
Поскольку нет контекста типа, чтобы сказать, что производить.
Другая версия должна была бы включать такие вещи:
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()?;
}
Мой инстинкт подсказывает, что вывод "вытекает из?" там слишком удивительно, а значит, это в «слишком умном» ведре.
Крайне важно, я не думаю, что его отсутствие является слишком ограничительным. my_result?
в функции -> Poll
не нуждается, так как тип успеха такой же, как обычно (важно, чтобы синхронный код работал одинаково в контекстах async), и вариант ошибки также преобразуется нормально . Использование ?
на Poll
в методе, возвращающем Result
, в любом случае кажется анти-шаблоном, а не чем-то, что должно быть обычным, поэтому использование (гипотетических) выделенных методов, таких как .wait(): T
(чтобы заблокировать результат) или .ready(): Option<T>
(чтобы проверить, выполнено ли), чтобы выбрать режим, в любом случае, вероятно, лучше.
Это "интересно" https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug
Мне не нравятся эти блоки try (do catch), они не кажутся очень удобными для новичков.
Я пытаюсь собрать текущее состояние предложения, которое, кажется, разбросано по нескольким комментариям. Есть ли единый обзор предлагаемого в настоящее время набора признаков (который отбрасывает связанный тип Error
)?
В начале этой ветки я вижу комментарий о разделении Try
на отдельные черты ошибок и от них - есть ли какие-либо планы по реализации этого разделения?
Было бы полезно иметь прозрачное преобразование из Result<T, E>
в любой тип Other<T2, E>
под вопросительным знаком - это позволило бы существующим функциям ввода-вывода вызываться с красивым синтаксисом из функции с более специализированной (например, ленивый) возвращаемый тип.
pub fn async_handler() -> AsyncResult<()> {
let mut file = File::create("foo.txt")?;
AsyncResult::lazy(move || {
file.write_all(b"Hello, world!")?;
AsyncResult::Ok(())
})
}
Семантически это похоже на From::from(E) -> Other<T2, E>
, но использование From
в настоящее время ограничено существующей Result
-эквивалентной реализацией Try
.
Я действительно думаю, что у NoneError
должна быть отдельная проблема с отслеживанием. Даже если признак Try
никогда не стабилизируется, NoneError
должен стабилизироваться, потому что это делает использование ?
на Option
гораздо более эргономичным. Учитывайте это в случае ошибок типа struct MyCustomSemanthicalError;
или ошибок реализации Default
. None
можно легко преобразовать в MyCustomSeemanthicalError
помощью From<NoneError>
.
Работая над https://github.com/rust-analyzer/rust-analyzer/ , я встретил несколько отличающийся от недостатков в операторе ?
, вырезанный из бумаги, особенно когда возвращаемый тип - Result<Option<T>, E>
.
Для этого типа имеет смысл для ?
эффективно десахарировать:
match value? {
None => return Ok(None),
Some(it)=>it,
}
где value
имеет тип Result<Option<V>, E>
, или:
value?
где value
- это Result<V, E>
. Я считаю, что это возможно, если стандартные библиотеки реализуют Try
следующим образом для Option<T>
, хотя я не тестировал это явно и думаю, что могут быть проблемы со специализацией.
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)
}
}
На вопрос @matklad, почему он не может создать настраиваемое перечисление, реализующее Try
, которое в этом случае будет называться Cancellable
, он указал, что std::ops::Try
нестабильно, поэтому в любом случае нельзя использовать, учитывая, что rust-analyzer
(в настоящее время) нацелен на стабильную ржавчину.
Сделайте репост с https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288, потому что я хотел прокомментировать это, но я думаю, что это была неправильная проблема:
По сути, у меня есть ситуация обратного вызова в среде графического интерфейса - вместо того, чтобы возвращать Option
или Result
, им нужно вернуть UpdateScreen
, чтобы сообщить платформе, если экран нужно обновлять или нет. Часто мне вообще не требуется протоколирование (просто нецелесообразно регистрировать каждую незначительную ошибку) и просто возвращать UpdateScreen::DontRedraw
при возникновении ошибки. Однако с текущим оператором ?
я должен писать это все время:
let thing = match fs::read(path) {
Ok(o) => o,
Err(_) => return UpdateScreen::DontRedraw,
};
Поскольку я не могу преобразовать Result::Err
в UpdateScreen::DontRedraw
помощью оператора Try, это становится очень утомительным - часто у меня есть простые поиски в хэш-картах, которые могут завершиться неудачно (что не является ошибкой. ) - так часто в одном обратном вызове у меня 5-10 использований оператора ?
. Поскольку приведенное выше очень многословно для написания, мое текущее решение - это impl From<Result<T>> for UpdateScreen
как это , а затем использовать внутреннюю функцию в обратном вызове следующим образом:
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()
}
Поскольку обратный вызов используется как указатель на функцию, я не могу использовать -> impl Into<UpdateScreen>
(по какой-то причине возврат impl
в настоящее время не разрешен для указателей на функции). Так что единственный способ для меня вообще использовать оператор Try
- это проделать трюк с внутренней функцией. Было бы неплохо, если бы я мог просто сделать что-то вроде этого:
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
}
Я не уверен, возможно ли это с текущим предложением, и просто хотел добавить свой вариант использования для рассмотрения. Было бы здорово, если бы пользовательский оператор Try мог поддерживать что-то подобное.
@joshtriplett Извините, что https://github.com/rust-lang/rust/compare/master...scottmcm : try-trait-v2, чтобы быть конкретным. Я надеюсь попробовать еще кое-что с ним.
@scottmcm У вас есть более
@scottmcm FWIW Я тоже пробовал ваши изменения в районе:
https://github.com/rayon-rs/rayon/compare/master...cuviper : try-trait-v2
(по-прежнему используются частные копии, а не нестабильные элементы)
Так как же можно преобразовать Option NoneError? Похоже, что из-за того, что он реализует Try Trait, он не будет компилироваться, если вы не включите использование этой конкретной (нестабильной?) Функции.
В общем, вы не можете использовать? оператор с Option, насколько мне известно?
@omarabid , оператор стабилен для использования с Option
или Result
, но вы не можете использовать Try
в качестве общего ограничения, пока оно не станет стабильным. Совершенно нормально использовать ?
для Option
в функции, возвращающей Option
, поскольку вам вообще не нужно задействовать NoneError
. Вы также можете вернуть Result
если удалите типы:
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)?)
}
( детская площадка )
@scottmcm , ваш прототип try-trait-v2
не справляется с этим примером!
Если мы не хотим, чтобы мой пример ломался, try-trait-v2
понадобится что-то вроде:
#[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))),
}
}
}
Каков текущий статус этой функции?
PR # 62606 для документирования реализации try_fold
для итераторов должен быть открыт повторно, как только это станет стабильным.
Изменить: обновлена операция с элементом отслеживания для этого ~ scottmcm
Есть ли планы заменить черту Try
на любую из альтернатив, предложенных в этой ветке? Версия, предложенная @scottmcm, кажется хорошей. Я хочу продолжать использовать оператор ?
с Option
, и я думаю, что признак Try
следует изменить, чтобы не принудительно использовать семантику Result
для Option
.
Использование альтернативы @scottmcm позволит нам использовать ?
с Option
и избавиться от NoneError
. Я согласен с @nikomatsakis ( комментарий ), что черта Try
не должна способствовать необходимости «искусственно определять тип, представляющий« отказ » 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>;
Новичок здесь, я был немного упрям в желании получить? чтобы автоматически ввести стирание всех ошибок и опций.
Потратив слишком много времени на то, чтобы понять, почему другие вероятные решения не могут быть реализованы, я обнаружил, что @cuviper - самое близкое к тому, что я могу получить.
Некоторые объяснения помогли бы, но, по крайней мере, я должен был близко познакомиться с некоторыми ограничениями метапрограммирования Rust.
Поэтому я попытался понять это и объяснить конкретными словами.
Эта ветка кажется наиболее вероятным перекрестком, где я, надеюсь, смогу помочь любому, как я, наткнувшемуся на это, не стесняйтесь исправлять:
From<T: StdError> for Error
и специальный From<NoneError> for Error
конфликтуютtype Error = Box<Debug>
связывает Debug, что приводит к конфликту From<T:Debug> for Error
с From<T> for T
(рефлексивно From для идемпотентности)Итак, поскольку Error не может реализовать Debug, вам может понадобиться помощник для развертывания в Result с транзитивной Debug:
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) } }
Это не может быть ни impl From<Result<T>> for StdResult<T>
ни Into<StdResult> for Result<T>
(не может подразумевать свойство восходящего потока для восходящего типа)
Например, вы можете использовать его, чтобы получить возврат отладки для завершения:
fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }
Злая идея: возможно, оператор ?
каким-то образом может представлять монадическое связывание, поэтому
let x: i32 = something?;
rest of function body
становится
Monad::bind(something, fn(x) {
rest of function body
})
Ужасная идея, но внутреннему выродку она нравится во мне.
@derekdreery Я не думаю, что это будет хорошо работать с потоком императивного управления, например return
и continue
Имейте в виду, что семантика оператора ?
уже стабильна. Только фактическая черта Try
является нестабильной, и любые изменения должны сохранять тот же стабильный эффект для Option
и Result
.
@KrishnaSannasi
@derekdreery Я не думаю, что это будет хорошо работать с императивным потоком управления, таким как возврат и продолжение
Я согласен с этим утверждением.
@cuviper
Имейте в виду, что семантика оператора? Оператор уже стабилен. Нестабильным является только фактический признак Try, и любые изменения должны сохранять тот же стабильный эффект для Option и Result.
Верно ли это и в разные эпохи?
В более общем плане, я не думаю, что возможно объединить такие концепции, как .await
, ?/
Early return, Option :: map, Result :: map, Iterator :: map в ржавчине, но понимание того, что все они имеют общую структуру, определенно помогает мне стать лучшим программистом.
Извиняюсь за то, что был ОТ.
Я не уверен, что эпохе / редакции будет разрешено изменять ?
desugaring, но это, безусловно, усложнит множество перекрестных проблем, таких как макросы.
Моя интерпретация RFC о гарантиях стабильности и эпохах заключается в том, что поведение ?
(сахарное или иное) может быть изменено, но его текущее поведение для типов Option
/ Result
должно оставаться неизменным. то же самое, потому что взлом, который вызовет гораздо больший отток, чем мы когда-либо могли надеяться оправдать.
Что, если Option
каким-то образом был псевдонимом типа для Result
? Т.е. type Option<T> = Result<T, NoValue>
, Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x)
, Option::<T>::None = Result::<T, NoValue>::Err(NoValue)
? Для реализации этого потребуется некоторая магия, и это нереально в текущей языковой среде, но, возможно, это стоит изучить.
Мы не можем внести это изменение, потому что есть подразумеваемые признаки, которые полагаются на то, что Option
и Result
являются отдельными типами.
Если мы проигнорируем это, мы могли бы пойти еще дальше и даже сделать bool
псевдонимом для Result<True, False>
, где True
и False
- это типы единиц.
Рассматривалось ли добавление Try
impl для единицы ()
? Для функций, которые ничего не возвращают, ранний возврат может быть полезен в случае ошибки в некритической функции, такой как обратный вызов журналирования. Или модуль был исключен, потому что лучше никогда не игнорировать ошибки молча в любой ситуации?
Или модуль был исключен, потому что лучше никогда не игнорировать ошибки молча в любой ситуации?
Этот. Если вы хотите игнорировать ошибки в некритическом контексте, вы должны использовать unwrap
или один из его вариантов.
возможность использовать ?
с чем-то вроде foo() -> ()
была бы весьма полезна в контексте условной компиляции и должна быть строго рассмотрена. Я думаю, что это отличается от вашего вопроса.
вы должны использовать развёртку или один из её вариантов.
unwrap()
вызывает панику, поэтому я бы не рекомендовал использовать его в некритических контекстах. Это было бы неуместно в ситуациях, когда требуется простой возврат. Можно привести аргумент, что ?
не является полностью "тихим" из-за явного, видимого использования ?
в исходном коде. Таким образом, ?
и unwrap()
аналогичны для единичных функций, только с другой семантикой возврата. Единственное различие, которое я вижу, заключается в том, что unwrap()
будет иметь видимые побочные эффекты (прервать программу / распечатать трассировку стека), а ?
нет.
Прямо сейчас эргономика раннего возврата в модульных функциях значительно хуже, чем в тех, которые возвращают Result
или Option
. Возможно, такое положение дел желательно, потому что в этих случаях пользователи должны использовать возвращаемый тип Result
или Option
, и это дает им стимул изменить свой API. В любом случае, было бы неплохо включить обсуждение возвращаемого типа единицы в PR или документацию.
возможность использовать
?
с чем-то вродеfoo() -> ()
была бы весьма полезна в контексте условной компиляции и должна быть строго рассмотрена. Я думаю, что это отличается от вашего вопроса.
Как это будет работать? Просто всегда оценивать как ОК (()) и игнорировать?
Кроме того, не могли бы вы привести пример того, где вы бы хотели это использовать?
Да, идея заключалась в том, что что-то вроде MyCollection :: push может, в зависимости от конфигурации времени компиляции, иметь либо возвращаемое значение Result <(), AllocError>, либо возвращаемое значение (), если коллекция настроена так, чтобы просто паниковать при ошибке. Промежуточному коду, использующему коллекцию, не нужно думать об этом, поэтому, если бы он мог просто _всегда_ использовать ?
даже если тип возврата ()
это было бы удобно.
Спустя почти 3 года, приблизился ли этот вопрос к решению?
@Lokathor, который будет работать только в том случае, если тип возврата Result<Result<X,Y>,Z>
или аналогичный невозможен. Но это. Таким образом, невозможно.
Я не понимаю, почему многослойный результат вызывает проблемы. Не могли бы вы уточнить?
Для целей перекрестных ссылок @dureuill предложил альтернативную формулировку внутренних
@Lokathor
Хорошо, я подумал об этом более глубоко и думаю, что, возможно, нашел довольно хорошее объяснение.
Проблема в том, что интерпретация возвращаемого типа или странные аннотации загромождают код. Это было бы возможно, но это затруднило бы чтение кода. (Предварительное условие: #[debug_result]
действительно применяет желаемое поведение и изменяет функцию для паники в режиме выпуска вместо возврата Result::Err(...)
и разворачивает Result::Ok
, но эта часть сложна)
#[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)
}
Таким образом, это затруднило бы чтение кода.
Мы не можем просто изменить поведение ?
на бездействие,
особенно, если возвращаемое значение #[debug_result]
сохраняется во временной переменной и позже "пробует распространяться" с помощью оператора ?
. Это сделало бы семантику ?
настоящему запутанной, потому что она будет зависеть от многих «переменных», которые не обязательно известны во «время написания» или их трудно угадать, просто прочитав исходный код. #[debug_result]
нужно будет испортить переменные, которым присвоены значения функций, но это не сработает, если одна функция помечена #[debug_result]
а другая нет, например, следующее будет ошибкой типа.
// 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()?
};
«Более чистым» решением может быть тип DebugResult<T, E>
который просто паникует, когда установлен определенный флаг компиляции и создается из-за ошибки, но в противном случае был бы эквивалентен Result<T, E>
противном случае. Но я думаю, что это было бы возможно и с нынешним предложением.
Ответ на последний пост @zserik : Макрос, который вы описали, бесполезен - тип возврата функции, основанный на конфигурации сборки, нарушит каждое совпадение результата функции во всех возможных конфигурациях сборки, кроме единственной, независимо от того, как это было сделано.
@ tema3210 Я знаю. Я только хотел отметить, что идея @Lokathor в целом не сработает на практике. Единственное, что может частично сработать, - это следующее, но только с множеством ограничений, которые, я думаю, не стоит того в долгосрочной перспективе:
// 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 Возможно ли, что он действительно принимает такую форму?
#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
x_innner()
}
хорошо, хорошая идея.
Я не совсем уверен, нужно ли это учитывать до стабилизации, но я заинтересован в реализации трассировки возврата ошибок, и я думаю, что это связано с изменениями в трейте Try
или, по крайней мере, в его предоставленном impl за Result
чтобы заставить его работать. В конце концов я планирую превратить это в RFC, но я хочу убедиться, что типаж try не стабилизирован таким образом, чтобы его невозможно было добавить позже, если мне потребуется время, чтобы написать упомянутый RFC. Основная идея такова.
У вас есть черта, которую вы используете для передачи информации отслеживания, в которой используется специализация и имплицит по умолчанию для T, чтобы поддерживать обратную совместимость.
pub trait Track {
fn track(&mut self, location: &'static Location<'static>);
}
default impl<T> Track for T {
fn track(&mut self, _: &'static Location<'static>) {}
}
А затем вы изменяете реализацию Try
для Result
чтобы использовать track_caller
и передавать эту информацию во внутренний тип,
#[track_caller]
fn from_error(mut v: Self::Error) -> Self {
v.track(Location::caller());
Self::Err(v)
}
А затем для типов, для которых вы хотите собирать обратные следы, реализуйте 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);
}
}
Использование в конечном итоге выглядит так
#![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())?
}
И результат очень дерьмовой версии обратной трассировки выглядит так
ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }
И вот доказательство его концепции в действии https://github.com/yaahc/error-return-traces
Я подумал, что только один тип ошибки, который мы можем преобразовать, чтобы попробовать реализовать трейт, может оказаться недостаточным, поэтому мы могли бы предоставить такой интерфейс:
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;
}
Обратите внимание, что здесь компилятор может мономорфизировать from_error
, избегая вызова From::from
, и можно вручную предоставить метод impl для разных типов ошибок, что приводит к способности оператора ?
"разворачивать" разные типы ошибок.
fn from_error<T>(val: T)->Self;
Как написано, разработчик должен без ограничений принять T
любого_ размера. Если вы хотели разрешить это настраиваемое ограничение, это должен быть параметр признака, например Try<T>
. Это похоже на комбинацию TryBlock
/ Bubble<Other>
которая была у @scottmcm в https://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 .
Как написано, разработчик должен без ограничений принять
T
любого_ размера. Если вы хотели разрешить это настраиваемое ограничение, это должен быть параметр признака, напримерTry<T>
. Это похоже на комбинациюTryBlock
/Bubble<Other>
которая была у @scottmcm в # 42327 (комментарий) .
Я имел в виду, что использование должно выглядеть так:
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}
}
}
вам нужно разделить Try и TryFromError. Мне это нравится больше, чем первоначальное предложение, но я думаю, что для этого потребуется новый RFC.
(и я все еще думаю, что его следовало назвать «распространять», а не «пытаться», но я отвлекся)
@ tema3210 Думаю, я понимаю ваши намерения, но это не верный Rust.
@ SoniEx2
вам нужно разделить Try и TryFromError.
Верно, поэтому я упомянул дизайн TryBlock
/ Bubble<Other>
. Мы можем обсудить этот выбор наименования, но идея заключалась в том, что раннее возвращение не всегда связано с ошибками как таковыми. Например, многие внутренние методы Iterator
используют пользовательский тип LoopState
. Для чего-то вроде find
поиск того, что вы ищете, не является ошибкой, но мы хотим остановить итерацию на этом этапе.
Мы можем обсудить этот выбор наименования, но идея заключалась в том, что раннее возвращение не всегда связано с ошибками как таковыми.
именно поэтому мне не нравится имя «попытка» и я предпочел бы имя «распространять», потому что оно «распространяет» «ранний» возврат: p
(не уверен, имеет ли это смысл? В прошлый раз, когда я поднял эту тему, «распространение» имело смысл только для меня по какой-то причине, и я никогда не мог объяснить это другим.)
Поможет ли эта черта при попытке перезаписать поведение по умолчанию ?
fe, чтобы добавить перехватчик журнала fe для регистрации отладочной информации (например, номера файла / строки)?
В настоящее время поддерживается перезапись макросов stdlib, но кажется, что оператор ?
не преобразуется явно в макрос try!
. Это прискорбно.
@stevenroose Чтобы добавить поддержку этого только в Try
, потребуется модификация трейта Try
чтобы включить информацию о местоположении файла о том месте, где "произошло" ?
.
@stevenroose Чтобы добавить поддержку этого только в
Try
, потребуется модификация трейтаTry
чтобы включить информацию о местоположении файла о том месте, где "произошло"?
.
Это неправда, # [track_caller] можно использовать с имплицитными признаками, даже если определение признака не включает аннотацию.
@stevenroose, чтобы ответить на ваш вопрос, да, хотя, если вы хотите распечатать все ?
местоположения, через которые распространяется ошибка, вы должны проверить комментарий трассировки возврата ошибки сверху
https://github.com/rust-lang/rust/issues/42327#issuecomment -619218371
Я не уверен, что кто-то уже упоминал об этом, может быть, impl Try for bool
, может быть, Try<Ok=(), Error=FalseError>
?
Чтобы мы могли сделать что-то подобное
fn check_key(v: Vec<A>, key: usize) -> bool {
let x = v.get_mut(key)?; // Option
x.is_valid()?; // bool
x.transform()?; // Result
true
}
Теперь мне приходится использовать возвращаемый тип Option<()>
в большинстве случаев, когда я думаю, что ?
может сделать код более понятным.
Я не уверен, что кто-то уже упоминал об этом, может быть,
impl Try for bool
, может быть,Try<Ok=(), Error=FalseError>
?
Это заставит Try
вести себя как оператор &&
на bool
s. Как уже отмечалось выше, существуют также варианты использования короткого замыкания в случае успеха, и в этом случае Try
должно вести себя как ||
. Поскольку операторы &&
и ||
не намного длиннее для ввода, я также не вижу особых преимуществ в этой реализации.
@calebsander спасибо за любезный ответ.
Это верно для некоторых простых случаев, но я не думаю, что это часто бывает, особенно у нас никогда не могло бы быть статуса присваивания, такого как let x = v.get_mut(key)?
в выражении.
Если бы &&
и ||
всегда работали, мы могли бы поиграть с .unwrap_or_else()
, .and_then()
для Option
и Error
кейсов.
Не могли бы вы выразить текущий код в &&
и ||
?
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
}
Для некоторых условий, при которых true
означает сбой, а false
означает успех, !expr?
может сбивать с толку, но мы могли бы использовать expr.not()?
для выполнения трюка (примечание: для ops::Try
, у нас всегда false
на Error
)
Это верно для некоторых простых случаев, но я не думаю, что это часто бывает, особенно у нас никогда не могло бы быть статуса присваивания, такого как
let x = v.get_mut(key)?
в выражении.
Что ж, если я неправильно понимаю ваше предложение, простая реализация Try<Ok = (), Error = FalseError>
для bool
не позволит этого. Вам также потребуется impl From<NoneError> for FalseError
чтобы оператор ?
мог преобразовать None
в false
. (И аналогично, если вы хотите применить ?
к Result<T, E>
внутри функции, которая возвращает bool
, вам потребуется impl From<E> for FalseError
. Я думаю, что такой реализация blanket будет проблематичной.) Вы также можете просто использовать some_option().ok_or(false)?
и some_result().map_err(|_| false)?
если вас устраивает возвращаемое значение Result<bool, bool>
(которое можно свернуть с помощью .unwrap_or_else(|err| err)
).
Оставляя в стороне проблемы преобразования других ошибок Try
в bool
, реализация Try
вы предлагаете для bool
представляет собой просто оператор &&
. Например, это эквивалентные
fn using_try() -> bool {
some_bool()?;
some_bool()?;
some_bool()
}
а также
fn using_and() -> bool {
some_bool() &&
some_bool() &&
some_bool()
}
(По общему признанию, такие потоки управления, как if
и loop
которые возвращают не ()
, переводить сложнее.)
Для некоторых условий, при которых
true
означает сбой, аfalse
означает успех,!expr?
может сбивать с толку, но мы могли бы использоватьexpr.not()?
чтобы сделать трюк (примечание: дляops::Try
, у нас всегдаfalse
наError
)
Мне не ясно, что false
должно представлять случай Error
. (Например, Iterator.all()
потребует короткого замыкания false
, а Iterator::any()
true
вместо этого потребует ?
(а также инвертирования возвращаемого значения функции). Но я не думаю, что это приводит к очень удобочитаемому коду. Возможно, имеет смысл иметь отдельные struct
s And(bool)
и Or(bool)
которые реализуют два разных поведения.
А также, если вы хотите подать заявку? к результату
внутри функции, возвращающей bool, вам нужно будет имплантировать From для FalseError. Я думаю, что такая бланкетная реализация будет проблематичной.
Нет, я не хочу impl From<T> for FalseError
, может быть, мы могли бы сделать result.ok()?
Мне не ясно, что false должно представлять случай ошибки.
Я думаю, что это как-то естественно, когда у нас есть bool::then
которые сопоставляют true
с Some
.
Простите, если пропустил - почему нет NoneError
impl std::error::Error
? Это делает его бесполезным для любых функций, возвращающих Box<dyn Error>
или аналогичные.
Простите, если пропустил - почему нет
NoneError
implstd::error::Error
? Это делает его бесполезным для любых функций, возвращающихBox<dyn Error>
или аналогичные.
Здесь я не эксперт, но я вижу значительные проблемы с попыткой выдать полезное сообщение об ошибке «что-то было None
а вы ожидали, что это будет Some
» (что в основном то, что вы Набираю сюда - какое-то диагностическое сообщение). По моему опыту, всегда имело смысл использовать комбинатор Option::ok_or_else
чтобы вместо этого использовать другой тип ошибки, потому что в качестве вызывающего кода у меня в любом случае гораздо лучший контекст.
Я согласен с @ErichDonGubler. Очень неприятно, что нельзя использовать ?
с Option
, но на это есть веская причина, и, как пользователь языка, я думаю, что правильная обработка ошибок в общих интересах. Выполнение этой дополнительной работы по привязке контекста ошибки к None
вместо простого выполнения ?
действительно очень раздражает, но заставляет правильно сообщать об ошибках, что того стоит.
Единственный раз, когда я позволю интерпретировать None
как ошибку, - это очень грубое прототипирование. Я подумал, что если мы добавим что-то вроде вызова Option::todo_err()
, это вернет Ok
внутреннего значения, но вызовет панику при None
. Это очень противоречит интуиции, пока вы не проанализируете потребности режима «грубого прототипирования» разработки кода. Он очень похож на Ok(my_option.unwrap())
, но не требует упаковки Ok(...)
. Кроме того, он явно определяет характер «задачи», тем самым сообщая о намерении удалить этот код из производственной логики, заменив его правильной привязкой ошибки.
но паникует ни на кого
Твердо чувствую, что мы должны просто оставить это unwrap
. Если мы добавили todo_err, он должен вернуть фактический тип ошибки, что вызывает вопрос о том, какой тип ошибки возвращать.
Также я подозреваю, что у fn todo_err(self) -> Result<Self, !>
будет очевидная проблема, связанная с требованием !
для стабилизации, что, ну, когда-нибудь.
Мой вариант использования - это действительно грубое прототипирование, когда меня не слишком заботят ошибки. Разве весь NoneError
страдает от перечисленных вами проблем? Если решено, что он должен существовать (что, по моему мнению, хорошо, по крайней мере, для прототипирования), я считаю, что он должен подразумевать Error
поскольку он назван так.
Из-за того, что у этого типа нет этого impl, я прибегнул к тому, чтобы просто нанести на него .ok_or("error msg")
, что тоже работает, но менее удобно.
Самый полезный комментарий
Каков текущий статус этой функции?