Rust: Проблема с отслеживанием для RFC 1937: `?` в `main`

Созданный на 18 июл. 2017  ·  183Комментарии  ·  Источник: rust-lang/rust

Это проблема отслеживания для RFC " ? in main " (rust-lang/rfcs#1937).

Шаги:

Стабилизации:

  • [x] Стабилизировать main с возвращаемыми типами, отличными от() (https://github.com/rust-lang/rust/issues/48453) Объединено в https://github.com/rust-lang/ ржавчина/тянуть/49162
  • [x] Стабилизировать модульные тесты с возвращаемыми типами, отличными от() (https://github.com/rust-lang/rust/issues/48854)

Связанные вопросы:

  • [x] Сообщение об ошибке для модульных тестов не очень хорошее (https://github.com/rust-lang/rust/issues/50291)

Нерешенные вопросы:

B-RFC-approved C-tracking-issue E-mentor T-compiler T-lang WG-compiler-middle

Самый полезный комментарий

Извините, что вмешиваюсь в момент, когда, вероятно, уже слишком поздно что-либо делать по этому поводу, но я хотел оставить свой отзыв здесь на случай, если он будет. Я прочитал большую часть этой ветки, поэтому я говорю с учетом этого контекста. Тем не менее, эта ветка длинная, поэтому, если кажется, что я что-то упустил из виду, то, вероятно, так и было, и я был бы признателен, если бы мне на это указали. :-)

TL;DR — я думаю, что показ сообщения об ошибке Debug был ошибкой, и что лучшим выбором было бы использовать сообщение об ошибке Display .

В основе моего убеждения лежит то, что, как человек, который _регулярно создает CLI-программы на Rust_, я не могу припомнить, чтобы меня сильно заботило, что такое сообщение Debug сообщения Error . А именно, Debug ошибки предназначены для разработчиков, а не для конечных пользователей. Когда вы создаете программу CLI, ее интерфейс в основном предназначен для чтения конечными пользователями, поэтому сообщение Debug здесь имеет очень мало пользы. То есть, если бы какая-нибудь CLI-программа, которую я отправлял конечным пользователям, показывала бы отладочное представление значения Rust при нормальной работе, я бы считал, что это ошибка, которую нужно исправить. Обычно я думаю, что это должно быть верно для каждой CLI-программы, написанной на Rust, хотя я понимаю, что это может быть пунктом, с которым разумные люди могут не согласиться. С учетом сказанного, по моему мнению, несколько поразительным следствием является то, что мы эффективно стабилизировали функцию, в которой ее режим работы по умолчанию запускает вас с ошибкой (опять же, IMO), которую следует исправить.

Показывая Debug представление ошибки по умолчанию, я также думаю, что мы поощряем плохую практику. В частности, в ходе написания программы CLI на Rust очень часто можно заметить, что даже Display импликация ошибки недостаточно хороша для того, чтобы ее могли использовать конечные пользователи, и что необходимо проделать реальную работу, чтобы почини это. Конкретным примером этого является io::Error . Отображение io::Error без соответствующего пути к файлу (при условии, что это произошло в результате чтения/записи/открытия/создания файла) в основном является ошибкой, потому что конечному пользователю сложно что-либо сделать с этим. Выбрав отображение ошибки в виде Debug по умолчанию, мы усложнили обнаружение такого рода ошибок людьми, создающими CLI-программы. (Кроме того, Debug в io::Error гораздо менее полезен, чем его Display , но, по моему опыту, это само по себе не является большой проблемой. )

Наконец, чтобы завершить мой аргумент, я также с трудом представляю себе обстоятельства, при которых я бы использовал ?-in-main даже в примерах. А именно, я пытался написать примеры, максимально соответствующие реальным программам, и это обычно включало в себя написание таких вещей:

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

На первый взгляд, было бы _красиво_ заменить это на ?-in-main , но я не могу, потому что это не покажет Display ошибки. То есть при написании реальной CLI-программы я фактически буду использовать описанный выше подход, поэтому, если я хочу, чтобы мои примеры отражали действительность, то я считаю, что должен показывать, что я делаю в реальных программах, а не сокращать пути (в разумных пределах). ). Я на самом деле думаю, что такие вещи действительно важны, и исторически сложилось, что одним из побочных эффектов этого было то, что они показали людям, как писать идиоматический код на Rust, не разбрасывая повсюду unwrap . Но если я вернусь к использованию ?-in-main в своих примерах, то я только что отказался от своей цели: теперь я настраиваю людей, которые, возможно, ничего не знают, просто писать программы, которые по умолчанию испускают очень много бесполезные сообщения об ошибках.

Шаблон «выдать сообщение об ошибке и выйти с соответствующим кодом ошибки» фактически используется в отточенных программах. Например, если ?-in-main использовала Display , то сегодня я мог бы объединить функции main и run в ripgrep:

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

Конечно, я мог бы использовать ?-in-main в будущем, предоставив свой собственный импл для трейта Termination , как только он стабилизируется, но зачем мне это делать, если я могу просто выписать main Функция impl в примеры, чтобы они соответствовали действительности, и в этот момент я мог бы также придерживаться тех примеров, которые у меня есть сегодня (используя main и try_main ).

Судя по всему, исправление этого было бы серьезным изменением. То есть этот код сегодня компилируется на стабильной версии Rust:

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

Я думаю, что переход на Display сломает этот код. Но я точно не знаю! Если это действительно проблема «корабль-уплыл», то я понимаю, и нет особого смысла в обсуждении этого вопроса, но я чувствую себя достаточно уверенно, чтобы хотя бы что-то сказать и посмотреть, не смогу ли я убедить других и увидеть если можно что-то исправить. (Также вполне возможно, что я слишком остро реагирую, но до сих пор у меня было несколько человек, которые спрашивали меня: «Почему вы не используете ?-in-main ?» в моих примерах CSV, и мой ответ в основном был, «Я не понимаю, как это когда-либо будет возможно сделать.» Возможно, это не была проблема, которую предполагалось решить с помощью ?-in-main , но у некоторых людей определенно сложилось такое впечатление. , я мог видеть, что это полезно в тестах документов и модульных тестах, но мне трудно думать о других ситуациях, в которых я мог бы его использовать.)

Все 183 Комментарий

Как будут обрабатываться статусы выхода?

Этот комментарий @Screwtapello , похоже, был сделан слишком близко к концу FCP, чтобы в ответ на него можно было внести какие-либо изменения в RFC.

Вкратце: RFC предлагает вернуть 2 в случае неудачи на основаниях, которые, хотя и вполне обоснованы, неясны и приводят к несколько необычному результату; наименее удивительным является возврат 1, когда программа не указывает, что ей нужны какие-либо подробности, кроме просто успеха или неудачи. Является ли это достаточно крутым, чтобы его можно было обсуждать, не чувствуя, что мы искажаем процесс RFC, или мы теперь зациклены на этой конкретной детали реализации?

Это не детали реализации, не так ли?

Некоторые скрипты используют коды выхода как способ получить информацию от подпроцесса.

Речь идет именно о том случае, когда подпроцесс (реализованный на Rust) не имеет никакой информации для предоставления, кроме бинарного "все в порядке"/"что-то пошло не так".

Некоторые скрипты используют коды выхода как способ получить информацию от подпроцесса.

Это поведение всегда чрезвычайно зависит от программы, вызываемой _кроме_ в том, что ненулевое значение означает сбой. Учитывая, что std::process::exit с оболочкой основной функции и таблицей поиска останется лучшим вариантом для тех, кто хочет получить более четкий статус выхода независимо от того, что делается, это кажется в основном незначительной деталью.

Однако я не думаю, что у SemVer есть исключение «в основном незначительные детали».

Я думаю, что код выхода должен быть добавлен в список нерешенных вопросов. @zackw также открыл связанную внутреннюю ветку.

Многие согласны с тем, что в случае ошибки код выхода должен быть 1 (вместо 2 ):
https://www.reddit.com/r/rust/comments/6nxg6t/the_rfc_using_in_main_just_got_merged/

@ arielb1 , ты собираешься реализовать этот RFC?

@bkchr

Нет, просто для наставничества. Я назначил, так что я не забуду написать заметки наставничества.

Ааа, здорово, мне было бы интересно это сделать :)
Но я понятия не имею, с чего начать :D

@bkchr

Вот почему я здесь :-). Я должен написать инструкции по наставничеству достаточно скоро.

Хорошо, тогда я жду ваших указаний.

Инструкции по наставничеству

Это проблема [WG-compiler-middle]. Если вы хотите обратиться за помощью, вы можете присоединиться к #rustc на irc.mozilla.org (я — arielby) или https://gitter.im/rust-impl-period/WG-compiler-middle (я — @arielb1). там).

В #44505 есть файл readme компилятора WIP - он описывает некоторые вещи в компиляторе.

План работы по этому RFC:

  • [ ] - добавить элемент языка Termination в libcore
  • [ ] - разрешить использование Termination в main
  • [ ] - разрешить использование Termination в доктестах
  • [ ] - разрешить использование Termination в #[test]

добавить элемент языка Termination в libcore

Во-первых, вам нужно добавить черту Termination в libcore/ops/termination.rs вместе с некоторой документацией. Вам также нужно будет пометить его как нестабильный с помощью атрибута #[unstable(feature = "termination_trait", issue = "0")] — это не позволит людям использовать его до того, как он будет стабилизирован.

Затем вам нужно пометить его как элемент языка в src/librustc/middle/lang_items.rs . Это означает, что компилятор может обнаружить это при проверке типов main (например, см. 0c3ac648f85cca1e8dd89dfff727a422bc1897a6).
Это значит:

  1. добавление его в список языковых элементов (в librustc/middle/lang_items.rs )
  2. добавление #[cfg_attr(not(stage0), lang = "termination")] к признаку Termination . Причина, по которой вы не можете просто добавить атрибут #[lang = "termination"] , заключается в том, что компилятор «stage0» (во время начальной загрузки) не будет знать, что termination существует, поэтому он не сможет скомпилировать libstd. Мы вручную удалим cfg_attr при обновлении компилятора stage0.
    Подробности смотрите в документации по начальной загрузке на XXX.

разрешить использование Termination в main

Это интересная часть, с которой я знаю, как справиться. Это означает создание main , которое возвращает () без проверки типа (в настоящее время вы получаете ошибку main function has wrong type ) и работает.

Чтобы проверить тип, вам сначала нужно удалить существующую ошибку в:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/lib.rs#L171 -L218

Затем вам нужно добавить проверку того, что возвращаемый тип реализует трейт Termination (вы добавляете обязательство трейта, используя register_predicate_obligation — ищите варианты его использования). Это можно сделать здесь:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/check/mod.rs#L1100 -L1108

Другая часть заставляет это работать. Это должно быть довольно легко. Как говорится в RFC, вы хотите сделать lang_start универсальным по типу возвращаемого значения.

lang_start в настоящее время определяется здесь:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/libstd/rt.rs#L32

Поэтому вам нужно изменить его, чтобы он был универсальным и соответствовал RFC:

#[lang = "start"]
fn lang_start<T: Termination>
    (main: fn() -> T, argc: isize, argv: *const *const u8) -> !
{
    use panic;
    use sys;
    use sys_common;
    use sys_common::thread_info;
    use thread::Thread;

    sys::init();

    sys::process::exit(unsafe {
        let main_guard = sys::thread::guard::init();
        sys::stack_overflow::init();

        // Next, set up the current Thread with the guard information we just
        // created. Note that this isn't necessary in general for new threads,
        // but we just do this to name the main thread and to give it correct
        // info about the stack bounds.
        let thread = Thread::new(Some("main".to_owned()));
        thread_info::set(main_guard, thread);

        // Store our args if necessary in a squirreled away location
        sys::args::init(argc, argv);

        // Let's run some code!
        let exitcode = panic::catch_unwind(|| main().report())
            .unwrap_or(101);

        sys_common::cleanup();
        exitcode
    });
}

И тогда вам нужно будет вызвать его из create_entry_fn . В настоящее время он создает мономорфный экземпляр lang_start с использованием Instance::mono , и вам нужно будет изменить его, чтобы использовать monomorphize::resolve с правильными заменителями.

https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_trans/base.rs#L697

разрешить использование Termination в doctests

Я не совсем понимаю, как работают доктесты. Может быть, спросить @alexcrichton (я бы так и сделал)?

разрешить использование Termination в #[test]

Я не совсем понимаю, как работает libtest. Может быть, спросить @alexcrichton (я бы так и сделал)? Модульные тесты в основном генерируются макросом, поэтому вам нужно изменить этот макрос или его вызывающую программу, чтобы обрабатывать возвращаемые типы, отличные от () .

@bkchr

Вы можете хотя бы присоединиться к IRC/gitter?

@bkchr только что проверил -- я видел, как вы и @arielb1 некоторое время назад разговаривали о хламе, есть прогресс? Пососать где-нибудь?

Нет извините, никакого прогресса до сих пор. В настоящее время у меня много дел, но я надеюсь, что на этой неделе найду время, чтобы заняться этим.

@bkchr Если вам нужна помощь, дайте мне знать!

В настоящее время я немного застрял, я хочу создать Обязательство. Для создания обязательства мне нужен TraifRef, для TraitRef мне нужен DefId. Может ли кто-нибудь указать мне какой-нибудь код о том, как создать DefId из черты завершения?

@bkchr Черта должна быть добавлена ​​в список элементов lang, например: https://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/librustc/middle/lang_items.rs#L312
и получить пометку #[termination_trait] , например: https://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/libcore/fmt/mod.rs#L525 -L526

Да проблема не в этом, я это уже сделал. Мне нужно проверить черту завершения в функции check_fn. Я хочу использовать register_predicate_obligation, и для этого мне нужен defid признака завершения.

О, тогда все, что вам нужно, это tcx.require_lang_item(TerminationTraitLangItem) .

@bkchr как дела? Просто проверяю снова. =) Не беспокойтесь, если вы заняты, просто хотите убедиться, что получаете всю необходимую помощь.

Извините, сейчас занят :/ До сих пор я получил всю необходимую помощь :)

Это код для проверки TerminationTrait: https://github.com/bkchr/rust/blob/f185e355d8970c3350269ddbc6dfe3b8f678dc44/src/librustc_typeck/check/mod.rs#L1108

Я думаю, что я не проверяю возвращаемый тип функции? Я получаю следующую ошибку:

error[E0277]: the trait bound `Self: std::ops::Termination` is not satisfied
  --> src/rustc/rustc.rs:15:11
   |
15 | fn main() { rustc_driver::main() }
   |           ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::ops::Termination` is not implemented for `Self`
   |
   = help: consider adding a `where Self: std::ops::Termination` bound

Что мне нужно изменить, чтобы проверить возвращаемый тип функции?

@bkchr Я бы порекомендовал присоединиться к Gitter рабочей группы компилятора по адресу https://gitter.im/rust-impl-period/WG-compiler-middle для обратной связи, а также попробовать IRC-канал #rust-internals по адресу https ://chat.mibbit.com/?server=irc.mozilla.org%3A%2B6697&channel=%23rust-internals. :)

@bstrie да, спасибо, я уже участвую в чате и могу решить свою проблему. :)

@bkchr ваша проблема в этой строке . Ссылка на трейт, которую вы хотите построить, выглядит примерно так: R: Termination , где R — это возвращаемый тип функции. Это определяется путем создания соответствующих «подставок», которые представляют собой набор значений для замены параметров типа типажа (в данном случае Self ).

Однако вы вызываете метод Substs::identity_for_item для признака. Это вернет вам замены, которые можно было бы использовать внутри самого определения черты . т. е. в этом случае вы сопоставляете параметр Self , объявленный в Termination , с Self . Это было бы уместно, если бы вы проверяли определение какой-то функции внутри трейта Terminator , но не здесь.

Вместо этого вы хотите получить возвращаемый тип функции входа. Это всего лишь одна из переменных ret_ty или actual_ret_ty . Любой из них хорош, но я думаю, что ret_ty лучше — это соответствует типу возвращаемого значения, объявленному пользователем (тогда как actual_ret_ty — это тип, возвращаемый фактическим кодом).

Вы можете создать нужные вам субстанции, просто вызвав метод mk_substs() из файла tcx. В этом случае есть только один параметр, тип, так что, я думаю, подойдет что-то вроде let substs = fcx.tcx.mk_substs(&[ret_ty]); .

Я считаю, что нужно использовать tcx.mk_substs_trait(ret_ty, &[]) .

@bkchr только что зарегистрировался — у вас была возможность воспользоваться этим советом? (Кроме того, для более быстрых ответов может быть целесообразно спросить на gitter .)

Да, я мог бы решить проблему с гиттером :)

@bkchr как дела? Просто проверяю.

Все в порядке, возможно, на этой неделе у меня будет немного времени, чтобы изучить код.

Есть ли место для еще одного человека, чтобы помочь с этим? Я хотел бы начать вносить свой вклад в сообщество Rust до конца года и хотел бы помочь с этой функцией. Надеюсь, это не будет слишком запутанно, если над этим будут работать два человека.

@U007D

Это небольшая функция, и @bkchr почти закончил с ней.

А, хорошо, это приятно знать, спасибо. Я буду следить за чем-то еще, с чем я могу помочь.

@U007D Вы видели https://www.rustaceans.org/findwork ?

@lnicola Да, есть! Я пытаюсь найти что-то на пересечении того, над чем я уверен, что смогу работать (т.е. быть чистым позитивом) и чем я увлечен. Честно говоря, хотя я изучаю Rust уже около года, все еще немного пугающе выступать добровольцем в чем-то. FWIW, это ни в коем случае не вина сообщества Rust — сообщество Rust из кожи вон лезет, чтобы сделать эту культуру открытой, гостеприимной и инклюзивной — лучшее, что мне довелось испытать. (Я подозреваю, что это больше связано со старыми боевыми шрамами от многолетнего опыта работы в технологической отрасли, где команды склонны к соперничеству, а не к сотрудничеству.)

В любом случае, моя цель — выбрать что-то в этом году и, по крайней мере, начать вносить положительный вклад. Пора и мне вмешаться! :)

Спасибо за предложение, @lnicola. Это хороший ресурс.

@bkchr какие-нибудь обновления?

Я на нем (https://github.com/rust-lang/rust/pull/46479). Сейчас у меня каникулы и время поработать в комментариях в пулреквесте. Извините за все задержки :/

О, извините, не заметил, что у вас есть запрос на включение. Скрестил его.

Привет, ммм. Так что я подумал, что начну свою потенциальную карьеру участника Rust с байкшеринга, как это принято по традиции. Конкретно об этом:

  • [ ] Название внедряемой черты

Как насчет Exit ? Это кратко и по существу, соответствует существующему словарю Rust. Выход-как-существительное является естественным аналогом выхода-как-глагола, который для большинства является знакомым словом для контролируемого завершения процесса «изнутри».

В частности, для программиста на C++ «завершение» напоминает std::terminate , которое по умолчанию является ненормальным завершением (вызов abort ) и в основном является C++ эквивалентом паники (но в отличие от паники, никогда не раскручивает куча).

Подождите, проигнорируйте этот комментарий, похоже, RFC оставил это открытым для обсуждения.

Мне нравится Exit как название черты.

Я полагаю, что функция будет стабилизирована задолго до того, как стабилизируется черта, как это произошло с Carrier .

FWIW, это еще один случай, когда я очень рад, что временное имя было изменено до стабилизации :D

Как автор RFC, я не возражаю против изменения названия трейта на Exit или что-то еще. Я не особенно хорошо умею называть вещи и рад, что у кого-то есть идея получше.

https://github.com/rust-lang/rust/blob/5f7aeaf6e2b90e247a2d194d7bc0b642b287fc16/src/libstd/lib.rs#L507

Должна ли быть черта

  1. помещается в libstd вместо libcore, и
  2. только что позвонил std::Termination , а не std::ops::Termination ?

Черта не может быть помещена в libcore , потому что реализация для Result требует вывода в stderr , а это невозможно сделать в libcore .

@bkchr То, что impl находится в libstd, не означает, что черта также должна быть в libstd.

@kennytm Я знаю, но Result также определен в libcore, поэтому завершение не может быть реализовано для Result в libstd.

@zackw Еще +1 голос за Exit в качестве названия черты.

@ U007D : Не могли бы вы использовать кнопку реакции (например, 👍) вместо публикации такого сообщения? Это позволит вам избежать раздражающих подписчиков проблем, пингуя их без необходимости.

Могу ли я зарегистрировать libtest / libsyntax , если language_feature активирован (в ящике)? @arielb1 @nikomatsakis @alexcrichton

@bkchr в libsyntax вам, возможно, придется передать его, но теоретически это возможно, но в самом libtest во время выполнения я не верю, что вы можете проверить.

@bkchr как дела?

Я все еще работаю над этим, но в настоящее время у меня нет больше вопросов :)

Я думаю, что этот импл слишком строгий:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Error> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                print_error(err);
                exit::FAILURE
            }
        }
    }
}


#[unstable(feature = "termination_trait", issue = "43301")]
fn print_error<E: Error>(err: E) {
    eprintln!("Error: {}", err.description());

    if let Some(ref err) = err.cause() {
        eprintln!("Caused by: {}", err.description());
    }
}

Есть несколько часто используемых ошибок, которые не реализуют Error , наиболее важные из которых Box<::std::error::Error> и failure::Error . Я также считаю ошибкой использовать метод description вместо отображения этой ошибки.

Я бы предложил заменить этот импл на этот более широкий импл:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                eprintln!("Error: {}", err)
                exit::FAILURE
            }
        }
    }
}

Это теряет цепочку причин, что является обломом.

Тем не менее, использование отображения, а не описания, определенно лучше.

Цепочка причин представляет собой интересную проблему. В частности, эта реализация выводит только первые два члена цепочки причин.

сбой должен был иметь дело с тем, как обрабатывать цепочку причин, и по умолчанию остановился на этом поведении (например, если вы просто создаете ошибки, используя .context :

  • {} выводит только эту ошибку
  • {:?} выводит эту ошибку, а также ее причину (рекурсивно)

Мы могли бы решить использовать здесь :? и привязать его к Debug вместо Display. Не уверен.

Да, я уже знаю, что мне нужно улучшить реализацию для поддержки. Я открыт для того, что мы могли бы сделать. Привязка к Debug может быть хорошей идеей.

Хм, это непростой вопрос. Я думаю, это зависит от того, думаем ли мы, что «отшлифованная» программа будет использовать эту черту реализации. Я склонен думать, что можно сказать, что «нет, они не будут» — в основном, отточенная программа либо (а) перехватит вывод и обработает его каким-то другим способом, либо (б) использует какой-то новый тип или что-то, что реализует отладку. правильный путь. Это означало бы, что мы можем оптимизировать реализацию для вывода полезной информации, но обязательно в самой красивой форме (что похоже на роль Debug ).

Возможно, было бы правильным сделать это очень четко нацеленным на прототипирование с помощью Debug , поскольку я не думаю, что мы когда-либо сможем автоматически обрабатывать ошибки таким образом, который подходит для большинства случаев использования в производственной среде.

@без лодок Я согласен.

@nikomatsakis Я полагаю, вы имели в виду « не обязательно в самой красивой форме»? Если так, то да, согласен.

Обновление: поработав над этим пару дней, я переключился на это. Смотри ниже.

:+1: на Debug , здесь; Мне нравится «своего рода аналог необработанного исключения» @nikomatsakis из https://github.com/rust-lang/rfcs/pull/1937#issuecomment -284509933. Комментарий от Diggsey также предлагает Debug : https://github.com/rust-lang/rfcs/pull/1937#issuecomment -289248751

К вашему сведению, я перешел к проблеме «более полной» и «более удобной для пользователя» по умолчанию (т. Е. Привязка черты Debug против Display ).

TL; DR: теперь я считаю, что мы должны установить границу на Display (согласно исходному сообщению @withoutboats ), чтобы обеспечить более чистый итоговый вывод в случае «ничего не делать».

Вот мое обоснование:

В RFC-проблеме черты termination @zackw делает убедительный вывод о том, что в Rust есть двойная система panic / Result , потому что panic предназначены для ошибок, а Result для ошибок. Исходя из этого, я думаю, можно сделать убедительный аргумент в пользу оценки представления ошибок по умолчанию независимо от представления паники по умолчанию.

Конечно, не существует одного значения по умолчанию, которое удовлетворило бы всех, поэтому, применяя принцип наименьшего удивления, я спрашиваю себя, какое значение по умолчанию более подходит?

  • Ошибка часто не обрабатывается дизайном , поскольку она предназначена для сообщения пользователю о том, что что-то (возможно, исправимое) пошло не так (файл не найден и т. д.). Таким образом, вариант использования существует и может быть обоснованно признан обычным, если пользователь является целевой аудиторией.

  • Как отметил @nikomatsakis , независимо от выбранного нами значения по умолчанию, любой разработчик, желающий изменить поведение, может либо использовать шаблон newtype, либо разработать собственную реализацию в main().

И, наконец, с более субъективной точки зрения, работая с этой функцией в течение последних нескольких дней, я обнаружил, что вывод Debug оставляет у меня ощущение, что мое «приложение Rust» кажется более неотшлифованным :

$ foo
Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }
$

против

$ foo
Error: returned Box<Error> from main()
$

Черта Dispay кажется гораздо более цивилизованной по умолчанию для ошибки (в отличие от ошибки), не так ли?

@ U007D подождите, какой из этих двух выходов вы предпочитаете?

(а) Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }

или

(б) Error: returned Box<Error> from main()

Он предпочитает вариант (б).

@nikomatsakis Первоначально я был в порядке с а) Debug как с концепцией в моей голове, но после того, как я поработал с ней пару дней, фактически увидев результат, теперь я предпочитаю б) Display как по умолчанию. Я думаю, что мое предпочтение b) стало бы еще более выраженным, если бы я моделировал цепную ошибку.

Я не думаю, что целью этого является «полированный» или «цивилизованный», поскольку я понял, что поток уже принял это как в основном для примеров, и люди полностью ожидали добавления пользовательской обработки по мере развития программы.

В таких случаях для меня «наименее неожиданным» является вывод, ориентированный на разработчиков, например, unwrap .

Стоит ли обсуждать {:#?} здесь, если есть опасения по поводу длинной ошибки?

Отчеты об ошибках для конечных пользователей будут разными для каждого инструмента и каждого варианта использования, но отчеты об ошибках для разработчиков должны напоминать то, что делает Rust в других ситуациях, таких как .unwrap() . Поскольку может быть только одно значение по умолчанию, а отшлифованное программное обеспечение все равно должно будет переопределить вывод, я голосую за значение по умолчанию, ориентированное на разработчиков, с помощью Debug .

Я думаю, что суть этой дискуссии на самом деле заключается в том, «кто является целевой аудиторией для сообщения по умолчанию?»

Скажем на мгновение, что мы все согласны с тем, что целевой аудиторией по умолчанию являются разработчики. Я думаю, что ограничение по умолчанию Debug было бы прямым выбором.

Теперь давайте на мгновение скажем, что мы согласились, что целевой аудиторией по умолчанию был пользователь, тогда здесь, соответственно, я не согласен с некоторыми другими и чувствую, что субъективные качества, такие как «полированный» и «цивилизованный» вывод, действительно играют важную роль в играть. Для некоторых «полировка» презентации для конечного пользователя может быть лучшей причиной для избегания Display . (Я не разделяю эту точку зрения, но понимаю и уважаю ее.)

Так что да, я определенно вижу разумные аргументы в пользу любой группы в качестве цели по умолчанию. Я думаю, что если возникнет прочный консенсус относительно того, какая аудитория должна быть целевой по умолчанию, тогда выбор для привязки черты будет ясен (э-э)... :)

(я не полностью разбираюсь во всей этой теме, но) не мыслимо ли, что могут быть небольшие утилиты, для которых вывод ошибки по умолчанию с Termination был бы вполне адекватным, при условии , что он в каком-то удобном для пользователя формате как Display ? В этом случае единственная причина, по которой авторам придется обращаться к «индивидуальной обработке», заключается в том, что мы их делаем.

Может ли кто-нибудь привести примеры того, как выглядит вывод в каждом случае (я предполагаю, что это также зависит от конкретного используемого типа E ?), и какие шаги на самом деле нужно предпринять авторам, если они хотят «индивидуальную обработку» вместо? Я просто иду на гипотетических выше.

(Вывод буквально похож на то, что @U007D вставил выше? Почему он печатает «returned Box\Коробка<Ошибка>?)

Как часто даже Display сообщения об ошибке достаточно удобны для пользователя? Например, следующая программа:

fn main() {
    if let Err(e) = std::fs::File::open("foo") {
        println!("{}", e)
    }
}

выдает следующее сообщение:

No such file or directory (os error 2)

Что, я бы сказал, не лучший UX, особенно с неупомянутым именем файла. По крайней мере, если программа буквально не принимает одно имя файла в качестве входных данных. С другой стороны, это также не очень хороший опыт разработчика , поскольку отсутствует исходный файл/номер строки/трассировка стека. Вывод Debug , очевидно, еще хуже для пользователя и не добавляет никакой полезной информации для деселопера.

Итак, я думаю, что я пытаюсь сказать, что без улучшения информационного содержания самих библиотечных ошибок ни Debug , ни Display не замечательны.

Вывод буквально похож на то, что @U007D вставил выше? Почему он печатает «возвращенный ящикfrom main()" вместо... фактического содержимого этой коробки?

@glaebhoerl Вы правы - в этом случае «фактическое содержимое этого Box<Error> » было настраиваемым полем message , которое я создал для проверки termination_trait , отображаемого дословно . Вместо этого я мог бы написать «foo bar baz» или что-то еще (но это могло быть не так полезно для пользователя, запускающего тесты компилятора).

Используя пример @jdahlstrom , вот фактический вывод для стандартной библиотеки Box ed «файл не найден» Error (обратите внимание, как вы правильно заметили, нигде не упоминается бокс):
Debug :

$ foo
Error { repr: Os { code: 2, message: "No such file or directory" } }
$

и Display :

$ foo
No such file or directory (os error 2)
$

@jdahlstrom Думаю, ты прав. Я согласен с тем, что, хотя оба сообщения могут не удовлетворить свою целевую аудиторию, я хочу подчеркнуть, что предоставление неправильного сообщения еще хуже (как я думаю, вы намекали):

Предоставление Display разработчику имеет все недостатки Debug плюс упускает из виду то, какой тип ошибки даже отображается.

Предоставление пользователю Debug имеет все недостатки Display , а также добавляет дополнительную техническую информацию, которая пользователю не нужна и может быть непонятна.

Так что да, я согласен, что сообщения часто недостаточно нацелены на любую аудиторию. Я думаю, что это подчеркивает еще одну важную причину, по которой мы должны четко понимать, на кого мы ориентируемся, чтобы предоставить наилучший опыт, который мы можем (несмотря на недостатки) для этой группы.

Мне нужна помощь для реализации поддержки ? в #[test] . Мою текущую реализацию можно найти здесь: https://github.com/rust-lang/rust/compare/master...bkchr:termination_trait_in_tests

Компиляция теста с моими изменениями приводит к следующей ошибке:

error: use of unstable library feature 'test' (see issue #27812)
  |
  = help: add #![feature(test)] to the crate attributes to enable

@eddyb сказал, что мне больше не следует использовать quote_item!/expr! , потому что они устарели.
Что мне теперь делать, переходить на новый макрос quote! или переделывать все на ручную сборку аста?

Я ценю любую помощь :)

Я думаю, что генерация вызова макроса для некоторого макроса, определенного в libtest , может работать очень хорошо.

@eddyb Я не уверен, что понимаю ваше предложение:

Я думаю, что генерация вызова макроса для некоторого макроса, определенного в libtest, может работать очень хорошо.

О, я думаю, может быть, я знаю. Вы говорите -- определите макрос в libtest, а затем сгенерируйте код, который его вызывает? Интересная идея. Не будет ли это имя макроса «просачиваться» наружу? (т.е. он становится частью общедоступного интерфейса libtest?)


@bkchr

Компиляция теста с моими изменениями приводит к следующей ошибке:

У вас есть идеи, почему возникает эта ошибка? Просто по читал diff, у меня нет, но могу попробовать собрать локально и разобраться.

Я не должен больше использовать quote_item!/expr! , потому что они устарели.

У меня нет твердого мнения здесь. Я согласен с @eddyb , что они унаследованы, но я не уверен, являются ли они унаследованными, когда добавление еще нескольких применений затруднит их удаление - т.е., как только мы получим недавнюю замену, будет ли это легко @eddyb перейти от одного к другому?

Ручное построение AST, безусловно, утомительно, хотя, я думаю, у нас есть несколько помощников для этого.

В основном нам просто нужно сделать крошечное редактирование, верно? т.е. перейти от вызова функции к проверке результата report() ?

PS, мы, вероятно, хотим сгенерировать что-то вроде Termination::report(...) , а не использовать нотацию .report() , чтобы не полагаться на трейт Termination , находящийся в области видимости?

Нет, я понятия не имею, откуда эта ошибка берется :(

В RFC предлагается создать функцию-оболочку, которая вызывает исходную тестовую функцию. Это также мой текущий способ сделать это.
Я думаю, что мы могли бы также отказаться от функции-оболочки, но тогда нам потребуется упаковать указатель функции, так как каждая тестовая функция может возвращать другой тип.

Хм, поскольку тест уже импортирует другие вещи, не так сложно также импортировать черту Termination.

@alexcrichton , может быть, у вас есть идея, откуда эта ошибка?

@nikomatsakis libtest нестабилен, и можем ли мы также пометить макросы как нестабильные, даже если это не так?

@eddyb о, хороший момент.

В RFC предлагается создать функцию-оболочку, которая вызывает исходную тестовую функцию. Это также мой текущий способ сделать это.

Функция-оболочка мне кажется прекрасной.

@eddyb с макросом вы имеете в виду что-то вроде create_test , которое определено в libtest ? Но чего я не понимаю, так это «пометить макрос как нестабильный». Что вы хотите этим сказать? Не могли бы вы привести пример?

@bkchr Добавление атрибута #[unstable(...)] в определение макроса, например: https://github.com/rust-lang/rust/blob/3a39b2aa5a68dd07aacab2106db3927f666a485a/src/libstd/thread/local.rs#L159 -L165

Итак, должен ли этот первый флажок...

Реализовать RFC

...проверить теперь, когда связанный PR был объединен?

@ErichDonGubler Готово :)

Хм, в настоящее время только половина rfc реализована с объединенным pr ^^

Разделено на 3 флажка :)

Я пытался использовать эту функцию в main , и это меня очень разочаровало. Существующие реализации признака завершения просто не позволяют мне удобно «накапливать» несколько типов ошибок — например, я не могу использовать failure::Fail , потому что он не реализует Error ; Я не могу использовать Box<Error> той же причине. Я думаю, что мы должны уделить первоочередное внимание переходу на Debug . знак равно

Привет, @nikomatsakis ,

Я почувствовал точно такое же разочарование, как и вы, когда попытался использовать termination_trait .

Это, в сочетании с вашими сообщениями о взломе компилятора, вдохновило меня на решение этой проблемы в начале этого месяца. Я разместил реализацию для Display (и для Debug в предыдущем коммите) вместе с тестами здесь: https://github.com/rust-lang/rust/pull/47544. (Это очень незначительно, но все же мой первый пиар компилятора Rust! :tada:) :)

Насколько я понимаю, команда lang сделает запрос о том, какую черту использовать, но в любом случае реализация готова к работе.

Вопрос, который меня все еще интересует: предположим, вы не хотите полагаться на вывод сообщения об ошибке по умолчанию (будь то Debug или Display ) и вместо этого хотите использовать свой собственный, как вы это делаете? это? (Извините, если это уже было где-то записано, и я пропустил это.) Вам не придется полностью прекратить использование ? -in- main , не так ли? Это что-то вроде написания собственного результата и/или типа ошибки и/или impl ? (Мне кажется прискорбным, если ? -in- main были всего лишь игрушкой, и как только вы захотели «стать серьезным», вам пришлось бы вернуться к менее эргономичным способам.)

@glaebhoerl Это очень просто:

  1. Создайте новый тип.
  2. Реализуйте From свой старый тип ошибки.
  3. Реализуйте Debug (или Display ).
  4. Замените тип в подписи на main .

Спасибо!

Мне кажется немного странным писать собственные реализации Debug , которые не ориентированы на отладку, но я думаю, что это не конец света.

@glaebhoerl Вот почему импл Result должен использовать Display , а не Debug IMO.

Может ли черта Termination иметь дополнительный метод с именем message или error_message или что-то в этом роде, который имеет реализацию по умолчанию, использующую Debug / Display , чтобы показать сообщение? Тогда вам просто нужно реализовать один метод, а не создавать новый тип.

@glaebhoerl @Thomasdezeeuw Что-то вроде метода error_message было в исходном проекте RFC, но от него отказались из-за отсутствия консенсуса. В то время я чувствовал, что было бы лучше получить базовую функцию (не обязательно стабилизированную), а затем итерацию.

@zackw Согласен, я лично был бы в порядке без обмена сообщениями, только число, скажем, 0 и 1 для кодов ошибок. Но если мы хотим получить сообщения в первой итерации, я думаю, что я бы больше предпочел Termination::message , чем что-либо на Debug или Display .

Будет ли этот метод message возвращать String ? Разве это не будет несовместимо с тем, что Termination находится в libcore?

@SimonSapin Termination в настоящее время определен в libstd .

Хм, но я не думаю, что добавление метода message к трейту было бы лучшей реализацией. Что вернет метод для таких типов, как i32 ? Как решить, когда напечатать это сообщение? В настоящее время реализация Termination для Result выводит ошибку в функции report . Это работает, потому что Result знает, что это Err . Мы могли бы интегрировать где-нибудь чек report() != 0 , а затем распечатать, но это кажется неправильным.
Следующая проблема заключается в том, что мы хотим предоставить стандартную реализацию для Result , но что, вероятно, должен реализовать тип Error для печати? Это вернет нас к текущему вопросу Debug или Display .

Еще одна головная боль, которая возникает, если вы хотите использовать ? в main в «серьезной» программе, заключается в том, что при некоторых обстоятельствах программы командной строки хотят выйти с ненулевым статусом, но без вывода _anything_ (рассмотрите grep -q ). Итак, теперь вам нужен импл Termination для чего-то, что _не_ является Error , что ничего не печатает, что позволяет вам контролировать статус выхода... и вам нужно решить, будете ли вы re возвращает эту вещь _после_ разбора аргументов командной строки.

Вот что я думаю:

Возврат Result<T, E> должен использовать реализацию отладки для E. Это наиболее удобная черта — она широко реализована и удовлетворяет вариант использования «быстрый и грязный вывод», а также вариант использования модульного тестирования. Я бы предпочел не использовать Display как потому, что он менее широко реализован, так и потому, что создается впечатление, что это полированный вывод, что я думаю крайне маловероятно.

Но также должен быть способ получить профессионально выглядящий результат. К счастью, таких способов уже два. Во-первых, как сказал @withoutboats , люди могут создать мост «отображение из отладки», если они хотят использовать E: Display или иным образом выводить результат в профессиональном стиле. Но если это кажется слишком хакерским, вы также можете просто определить свой собственный тип, чтобы заменить результат. например, вы можете сделать:

fn main() -> ProfessionalLookingResult {
    ...
}

а затем реализовать Try для ProfessionalLookingResult . Затем вы также можете реализовать Terminate , что угодно:

impl Terminate for ProfessionalLookingResult {
    fn report(self) -> i32 {
        ...
        eprintln!("Something very professional here.");
        return 1;
        ...
    }
}

Я согласен с @nikomatsakis , что здесь следует использовать Debug .

Я также думаю, что для полированного вывода написание некоторого кода в main , вероятно, лучше, чем создание нового типа для реализации Try и Terminate . Мне кажется, что точка Terminate заключается в том, что библиотеки могут легко сделать хорошее значение по умолчанию, что никогда не бывает в ситуации, когда завершение программы важно для конечных пользователей (например, профессиональные интерфейсы командной строки) .

Конечно, у других людей может быть другое мнение, и есть несколько способов использовать трейты, задействованные для внедрения кода, вместо того, чтобы писать его непосредственно в main . Что здорово, так это то, что у нас есть несколько вариантов, и нам не нужно всегда придумывать единственный благословенный способ обработки ошибок.

Позвольте мне просто записать некоторые мысли, хотя с ними есть несколько проблем.

хотелось бы увидеть следующее

fn main() -> i32 {
    1
}

Что можно записать в более общем виде:

fn main() -> impl Display {
    1
}

Обе эти основные функции должны возвращать код выхода 0 и println! Display из 1.

Это должно быть так же просто, как следующее (я думаю).

impl<T> Termination for T where T: Display {
    fn report(self) -> i32 {
        println!("{}", self);
        EXIT_SUCCESS
    }
}

Тогда для ошибок мы можем иметь:

impl<T: Termination, E: Debug> Termination for Result<T, E> { ... }

где реализация такая же, как в RFC, только с использованием "{:?}"
формат Debug .

Как упоминалось ранее, люди, которым требуется больший контроль над выводом, могут просто
записывать:

fn main() -> Result<i32, MyError> { ... }
impl Termination for Result<i32, MyError> { ... }

Хотя это было бы неразрешимо с нашим текущим компилятором, я думаю, поскольку он
увидели бы конфликтующие реализации... Итак, мы делаем то, что предлагает @nikomatsakis
и написать:

fn main() -> MyResult { ... }
impl Termination for MyResult { ... }
or, if you want something more general.
impl<T, E> Termination for MyResult<T, E> { ... }

Я знаю, что это частично переформулирует то, что было сказано, но я решил представить свое видение в целом, показав более общее решение для отображения возвращаемых значений, а не только результатов. Похоже, что многие из этих комментариев спорят о том, какие реализации Termination мы поставляем по умолчанию. Также этот комментарий не соответствует реализации, такой как impl Termination for bool , как описано в RFC. Я лично считаю, что ненулевые коды выхода должны обрабатываться исключительно Results или пользовательскими типами, реализующими Termination .

Это интересный вопрос о том, как затем обрабатывать ? на типах Option в main, поскольку у них нет реализации для Display .

TL;DR: я в порядке с Debug .

Более подробное объяснение:
Я потратил некоторое время на размышления об этом вчера и сегодня с целью выявить неявные предположения, которые у меня есть или которые я делаю, чтобы помочь прийти к наилучшему возможному ответу.

Ключевое предположение: из всех различных типов приложений, которые можно написать на Rust, я думаю, что консольное приложение больше всего выиграет/на него больше всего повлияет это решение. Я думаю, что при написании библиотеки, названия игры ААА, IDE или проприетарной системы управления можно, вероятно, не ожидать, что основная черта завершения по умолчанию удовлетворит ваши потребности из коробки (на самом деле, у человека может даже не быть основной). Таким образом, мой уклон в сторону Display по умолчанию исходит из того, что мы ожидаем увидеть при использовании небольшого приложения командной строки, например:

$ cd foo
bash: cd: foo: No such file or directory

Большинство из нас не ожидает каких-либо средств отладки, а просто краткое указание того, что пошло не так. Я просто выступаю за это как за позицию по умолчанию.

Когда я думаю о том, чтобы написать импл Terminate для получения простого вывода, подобного этому, я понимаю, что функция ? from main не так уж сильно отличается от сегодняшней стабильной ржавчины (с точки зрения количества написанного кода). ), где Result -aware "inner_main()" часто создается для обработки E .

Помня об этом предположении, в качестве мыслительного упражнения я попытался определить, сильно ли я считаю, что преобладание существующих сегодня реализаций в стиле " inner_main() " имеет более непринужденный вкус Display (по сравнению с более техническим вкусом Debug ). Я думал, что это будет показателем того, как функция, вероятно, будет использоваться на самом деле.

Я не мог убедить себя, что это так. (Это означает, что я не думаю, что в настоящее время существует сильный уклон в сторону Display в существующих реализациях). Действительно, просматривая свои собственные репозитории, которые я написал за последние 16 месяцев, я также нашел достаточное количество обоих случаев, и я не могу сказать, что реализация Display по умолчанию составила бы чистую экономию.

Придерживаясь предположения, что «основным бенефициаром является приложение cli», существует большое количество консольных приложений, которые предоставляют справку и информацию об использовании. Например:

$ git foo
git: 'foo' is not a git command. See 'git --help'.

The most similar command is
    log

Так что даже для консольных приложений мне трудно определить «пострадавшую группу», выбрав Debug .

И, наконец, я был бы более счастлив с импл Debug , чем с тем, чтобы эта функция задерживалась еще на 6 месяцев, так что, эгоистично, вот что :).

Итак, мой мыслительный процесс выложен на всеобщее обозрение. Подводя итог, я думаю, что Debug будет не лучше и не хуже, чем Display , и поэтому должно подойти в качестве реализации по умолчанию.

Я уверен, что, как и многие из вас, я бы хотел, чтобы была реализация, которой я был бы более взволнован — например, «ДА, ЭТО ТО!!!», TBH. Но, может быть, это просто мои ожидания нереалистичны... Может быть, когда у нас будет решение, которое работает с failure и сокращает шаблоны в моих проектах, оно мне понравится. :)

Примечание. Я открыл PR для поддержки признака терминации в тестах: #48143 (на основе работы @bkchr ).

Я позволил себе расширить трейт Termination методом обработки результата теста. Это упростило реализацию, но также имеет смысл, поскольку при сбоях тестов может потребоваться более подробный вывод, чем при сбоях исполняемых файлов.

Termination следует переименовать в Terminate соответствии с нашим общим предпочтением глаголов для признаков в libstd.

@withoutboats Я думаю, что в какой-то момент обсуждалось, что черты глагола - это в основном те, у которых есть один метод с тем же именем, что и черта. В любом случае, могу я снова предложить свое собственное предложение для велопарковки, Exit ?

Беспричинный отказ от велосипедов: это черта одного метода. Если мы хотим дать им одно и то же имя, может быть, ToExitCode / to_exit_code ?

Стабилизация возврата Result может выполняться независимо от стабилизации черты, верно?

Для меня это очень похоже на ? , где большая часть ценности исходит от языковой функции, и мы можем отложить определение черты. Обсуждение RFC даже заставило меня задуматься о том, нужно ли стабилизировать трейт _ever_, поскольку поместить код в inner_main казалось проще, чем реализовать трейт для этого...

Да, нам не нужно стабилизировать трейт, хотя он полезен в стабильной версии для таких вещей, как фреймворки, которые не обязательно могут так сильно полагаться на inner_main .

@SimonSapin Я думаю, что To относится к преобразованию типов, а это не так. Но мы могли бы назвать метод terminate (к тому же я не думаю, что это ограничение на то, когда называть глаголы свойств, действует. Try — очевидный контрпример).

Я предложил стабилизировать fn main() -> T , где T не является единицей. Это оставляет многие детали — в частности, имя/местоположение/детали черты — нестабилизированными, но некоторые вещи исправляются. Подробности здесь:

https://github.com/rust-lang/rust/issues/48453

Пожалуйста, дайте свой отзыв!

terminate кажется более описательным, чем report . Мы всегда terminate , но можем опустить report ing.

Но в отличие, например, от std::process::exit этот метод ничего не завершает. Он только преобразует возвращаемое значение main() в код выхода (после необязательной печати Result::Err в stderr).

Еще один голос за Exit . Мне нравится, что он краткий, достаточно описательный и соответствует традиционной концепции кодов выхода/статуса выхода.

Я бы определенно предпочел Exit Terminate , поскольку мы изящно выходим, возвращаясь из main, а не внезапно завершаем работу там, где мы находимся, потому что что-то пошло не так.

Я добавил https://github.com/rust-lang/rust/issues/48854 , чтобы предложить стабилизирующие модульные тесты, которые возвращают результаты.

О, эй, я нашел подходящее место, чтобы поговорить об этом.

Использование ? в doctests

В настоящее время doctests работает примерно так:

  • rustdoc сканирует doctest на предмет объявления fn main

    • (в настоящее время он просто выполняет построчный текстовый поиск «fn main», который не следует после // )

  • Если fn main найдено, это не затронет то, что уже есть
  • Если fn main не найдено, большая часть* doctest будет заключена в базовый fn main() { }

    • *Полная версия: он извлечет объявления #![inner_attributes] и extern crate и поместит их за пределы сгенерированной основной функции, но все остальное останется внутри.

  • Если doctest не включает никаких операторов extern crate (и документируемый крейт не называется std ), то rustdoc также вставит оператор extern crate my_crate; прямо перед сгенерированной основной функцией.
  • Затем rustdoc компилирует и запускает окончательный результат как отдельный двоичный файл как часть тестовой программы.

(Я упустил пару деталей, но для удобства сделал полное описание здесь.)

Таким образом, чтобы без проблем использовать ? в doctest, необходимо изменить часть, в которой добавляется fn main() { your_code_here(); } Объявление собственных fn main() -> Result<(), Error> будет работать, как только вы сможете это сделать. что в обычном коде - rustdoc там даже не нужно модифицировать. Однако, чтобы заставить его работать без объявления main вручную, потребуется небольшая настройка. Не следя внимательно за этой функцией, я не уверен, что существует универсальное решение. Возможно ли fn main() -> impl Termination ?

Возможно ли завершение fn main() -> impl?

В поверхностном смысле да: https://play.rust-lang.org/?gist=8e353379f77a546d152c9113414a88f7&version=nightly

К сожалению, я думаю, что -> impl Trait принципиально проблематичен с ? из-за встроенного преобразования ошибок, которому требуется контекст вывода, чтобы указать, какой тип использовать: https://play.rust- lang.org/?gist=23410fa4fa684710bc75e16f0714ec4b&version=nightly

Лично я представлял, как ? -in-doctests работают через что-то вроде https://github.com/rust-lang/rfcs/pull/2107 как fn main() -> Result<(), Box<Debug>> catch { your_code_here(); } (используя синтаксис из https:// github.com/rust-lang/rust/issues/41414#issuecomment-373985777).

Тем не менее, версия impl Trait крутая и могла бы работать, если бы существовала какая-то поддержка «предпочитать тот же тип, если не ограничен», которую rustc мог бы использовать внутри в десахаре ? , но, насколько я помню, что идея такой функции, как правило, заставляет людей, которые понимают, как все работает, отшатываться в ужасе :sweat_smile: Но, возможно, это внутренняя вещь, которая применима только в том случае, если вывод impl Trait был бы возможен...

О, это очень настоящие опасения. Я забыл о том, как это может испортить вывод типа. Если блок catch начал оборачиваться в порядке, как эта связанная проблема, то это кажется гораздо более простым (и намного более быстрым с точки зрения стабилизации) путем вперед.

Единственное, что мне интересно, это то, как на это повлияет переход редакции. Разве синтаксис catch не меняется в эпоху 2018 года? Rustdoc, скорее всего, захочет компилировать доктесты в той же редакции, что и библиотека, в которой он работает, поэтому ему нужно будет различать синтаксисы на основе переданного ему флага эпохи.

Я обеспокоен тем, что теперь это стабилизировано, но простые случаи, по-видимому, все еще ICE: https://github.com/rust-lang/rust/issues/48890#issuecomment -375952342

fn main() -> Result<(), &'static str> {
    Err("An error message for you")
}
assertion failed: !substs.has_erasable_regions(), librustc_trans_utils/symbol_names.rs:169:9

https://play.rust-lang.org/?gist=fe6ae28c67e7d3195a3731839d4aac84&version=nightly

В какой момент мы говорим: «это слишком глючно, и мы должны отменить стабилизацию»? Похоже, если бы это попало в стабильный канал в его нынешнем виде, это вызвало бы много путаницы.

@frewsxcv Я думаю, что проблемы теперь исправлены, верно?

@nikomatsakis проблема, которую я поднял в https://github.com/rust-lang/rust/issues/48389 , решена в бета-версии 1.26, так что да, с моей точки зрения.

Да, ICE, о котором я беспокоился, теперь исправлен!

Извините, что вмешиваюсь в момент, когда, вероятно, уже слишком поздно что-либо делать по этому поводу, но я хотел оставить свой отзыв здесь на случай, если он будет. Я прочитал большую часть этой ветки, поэтому я говорю с учетом этого контекста. Тем не менее, эта ветка длинная, поэтому, если кажется, что я что-то упустил из виду, то, вероятно, так и было, и я был бы признателен, если бы мне на это указали. :-)

TL;DR — я думаю, что показ сообщения об ошибке Debug был ошибкой, и что лучшим выбором было бы использовать сообщение об ошибке Display .

В основе моего убеждения лежит то, что, как человек, который _регулярно создает CLI-программы на Rust_, я не могу припомнить, чтобы меня сильно заботило, что такое сообщение Debug сообщения Error . А именно, Debug ошибки предназначены для разработчиков, а не для конечных пользователей. Когда вы создаете программу CLI, ее интерфейс в основном предназначен для чтения конечными пользователями, поэтому сообщение Debug здесь имеет очень мало пользы. То есть, если бы какая-нибудь CLI-программа, которую я отправлял конечным пользователям, показывала бы отладочное представление значения Rust при нормальной работе, я бы считал, что это ошибка, которую нужно исправить. Обычно я думаю, что это должно быть верно для каждой CLI-программы, написанной на Rust, хотя я понимаю, что это может быть пунктом, с которым разумные люди могут не согласиться. С учетом сказанного, по моему мнению, несколько поразительным следствием является то, что мы эффективно стабилизировали функцию, в которой ее режим работы по умолчанию запускает вас с ошибкой (опять же, IMO), которую следует исправить.

Показывая Debug представление ошибки по умолчанию, я также думаю, что мы поощряем плохую практику. В частности, в ходе написания программы CLI на Rust очень часто можно заметить, что даже Display импликация ошибки недостаточно хороша для того, чтобы ее могли использовать конечные пользователи, и что необходимо проделать реальную работу, чтобы почини это. Конкретным примером этого является io::Error . Отображение io::Error без соответствующего пути к файлу (при условии, что это произошло в результате чтения/записи/открытия/создания файла) в основном является ошибкой, потому что конечному пользователю сложно что-либо сделать с этим. Выбрав отображение ошибки в виде Debug по умолчанию, мы усложнили обнаружение такого рода ошибок людьми, создающими CLI-программы. (Кроме того, Debug в io::Error гораздо менее полезен, чем его Display , но, по моему опыту, это само по себе не является большой проблемой. )

Наконец, чтобы завершить мой аргумент, я также с трудом представляю себе обстоятельства, при которых я бы использовал ?-in-main даже в примерах. А именно, я пытался написать примеры, максимально соответствующие реальным программам, и это обычно включало в себя написание таких вещей:

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

На первый взгляд, было бы _красиво_ заменить это на ?-in-main , но я не могу, потому что это не покажет Display ошибки. То есть при написании реальной CLI-программы я фактически буду использовать описанный выше подход, поэтому, если я хочу, чтобы мои примеры отражали действительность, то я считаю, что должен показывать, что я делаю в реальных программах, а не сокращать пути (в разумных пределах). ). Я на самом деле думаю, что такие вещи действительно важны, и исторически сложилось, что одним из побочных эффектов этого было то, что они показали людям, как писать идиоматический код на Rust, не разбрасывая повсюду unwrap . Но если я вернусь к использованию ?-in-main в своих примерах, то я только что отказался от своей цели: теперь я настраиваю людей, которые, возможно, ничего не знают, просто писать программы, которые по умолчанию испускают очень много бесполезные сообщения об ошибках.

Шаблон «выдать сообщение об ошибке и выйти с соответствующим кодом ошибки» фактически используется в отточенных программах. Например, если ?-in-main использовала Display , то сегодня я мог бы объединить функции main и run в ripgrep:

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

Конечно, я мог бы использовать ?-in-main в будущем, предоставив свой собственный импл для трейта Termination , как только он стабилизируется, но зачем мне это делать, если я могу просто выписать main Функция impl в примеры, чтобы они соответствовали действительности, и в этот момент я мог бы также придерживаться тех примеров, которые у меня есть сегодня (используя main и try_main ).

Судя по всему, исправление этого было бы серьезным изменением. То есть этот код сегодня компилируется на стабильной версии Rust:

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

Я думаю, что переход на Display сломает этот код. Но я точно не знаю! Если это действительно проблема «корабль-уплыл», то я понимаю, и нет особого смысла в обсуждении этого вопроса, но я чувствую себя достаточно уверенно, чтобы хотя бы что-то сказать и посмотреть, не смогу ли я убедить других и увидеть если можно что-то исправить. (Также вполне возможно, что я слишком остро реагирую, но до сих пор у меня было несколько человек, которые спрашивали меня: «Почему вы не используете ?-in-main ?» в моих примерах CSV, и мой ответ в основном был, «Я не понимаю, как это когда-либо будет возможно сделать.» Возможно, это не была проблема, которую предполагалось решить с помощью ?-in-main , но у некоторых людей определенно сложилось такое впечатление. , я мог видеть, что это полезно в тестах документов и модульных тестах, но мне трудно думать о других ситуациях, в которых я мог бы его использовать.)

Я согласен с тем, что отображение Display над Debug лучше для приложений CLI. Я не согласен с тем, что это должно быть Display вместо Debug , потому что это резко ограничивает количество ошибок, которые на самом деле могут быть ? -ed, и противоречит цели ?-in-main . Насколько я могу судить (может быть совершенно неправильно, так как у меня нет времени компилировать stdlib, и я не знаю, охватывает ли это специализация), нет никаких причин, по которым мы не можем добавить следующий импл в Termination. Это обеспечило бы непрерывный способ использования Display , когда это доступно, и возврата к Debug , когда это не так.

#[unstable(feature = "termination_trait_lib", issue = "43301")]
impl<E: fmt::Display> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {}", err);
        ExitCode::FAILURE.report()
    }
}

Наконец, чтобы завершить мой аргумент, я также с трудом представляю себе обстоятельства, при которых я бы использовал ?-in-main даже в примерах.

Я должен согласиться с @BurntSushi в целом для реальных программ CLI, но для случайных скриптов и внутренних инструментов, которые только я планирую использовать, это действительно удобно. Кроме того, это очень удобно для прототипов и игрушек. Мы всегда можем отговорить его от использования в рабочем коде, верно?

Часть цели с ?-in-main состоит в том, чтобы избежать однообразного кода в примерах кода, а именно разворачиваний. Но если ?-in-main само по себе становится унидиоматичным, то это подрывает всю функцию. Я очень хотел бы избежать любого результата, который заставит нас отказаться от его использования в производственном коде или иным образом.

Я пытался использовать эту функцию в основном, и я нашел ее довольно разочаровывающей. Существующие реализации признака завершения просто не позволяют мне удобно "накапливать" несколько типов ошибок - например, я не могу использовать failure::Fail, потому что он не реализует Error; Я не могу использовать бокс, той же причине. Я думаю, что мы должны уделить первоочередное внимание переходу на Debug. знак равно

Если мы используем Display , мы можем ввести быстрый и грязный тип, такой как MainError , для накопления нескольких типов ошибок:

pub struct MainError {
    s: String,
}

impl std::fmt::Display for MainError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.s.fmt(f)
    }
}

impl<T> From<T> for MainError where T: std::error::Error {
    fn from(t: T) -> Self {
        MainError {
            s: t.to_string(),
        }
    }
}

Это позволит что-то вроде ниже Box<Error> :

fn main() -> Result<(), MainError> {
    let _ = std::fs::File::open("foo")?;
    Ok(())
}

Предыдущее обсуждение Display vs Debug находится здесь в скрытой части, начиная с https://github.com/rust-lang/rust/issues/43301#issuecomment -362020946.

@BuntSushi Я согласен с тобой. Если это невозможно исправить, может быть обходной путь:

use std::fmt;
struct DisplayAsDebug<T: fmt::Display>(pub T);

impl<T: fmt::Display> Debug for DisplayAsDebug {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl<T: fmt::Display> From<T> for DisplayAsDebug {
    fn from(val: T) -> Self {
        DisplayAsDebug(val)
    }
}

Если бы это было в std::fmt , мы могли бы использовать что-то вроде этого:

use std::{fmt, io};

fn main() -> Result<(), fmt::DisplayAsDebug<io::Error>> {
    let mut file = File::open("/some/file")?;
    // do something with file
}

У меня все еще нет времени внести свой вклад в это в обозримом будущем, но я также согласен с тем, что ? в main следует использовать в серьезных программах CLI, и в первоначальном проекте RFC, которые должны были облегчить это. От них отказались во имя инкрементализма, но, возможно, их следует пересмотреть.

Я думаю, что можно исправить это обратно несовместимым способом, если это будет сделано в ближайшее время, до того, как много кода будет использовать его в стабильной версии.

Как человек, который пишет много программ CLI на Rust и отвечает за стиль Rust на работе, я в значительной степени согласен с @burntsushi здесь. Я бы с удовольствием использовал версию этой функции, указанную в RFC.

Но я считаю, что показ реализации Debug пользователю является ошибкой, и не намного лучше, чем просто вызывать unwrap везде в программе CLI. Поэтому даже в примере кода я не могу представить, чтобы когда-либо использовалась эта версия функции. Это очень плохо, потому что удаление шаблона из main упростило бы обучение для новых разработчиков Rust на работе, и нам больше не нужно было бы объяснять quick_main! или его эквивалент.

Практически единственные обстоятельства, при которых я могу представить себе использование этого, - это исключение unwrap из doctests. Но я не знаю, поддерживается ли это еще.

Если мы используем Display , мы можем ввести быстрый и грязный тип, такой как MainError , для накопления нескольких видов ошибок:

Лично такой обходной путь добавил бы достаточной сложности, чтобы было проще полностью избежать ? -in- main .

@Aaronepower :

Насколько я могу судить (может быть совершенно неправильно, так как у меня нет времени компилировать stdlib, и я не знаю, охватывает ли это специализация), нет никаких причин, по которым мы не можем добавить следующий импл в Termination. Это обеспечило бы непрерывный способ использования Display, когда он доступен, и возврата к Debug, когда он не доступен.

Если бы это позволило мне написать fn main() -> Result<(), failure::Error> и получить красивую, удобочитаемую ошибку, используя Display , это определенно удовлетворило бы мои основные опасения. Тем не менее, я полагаю, что это по-прежнему оставит вопрос о необязательном отображении обратной трассировки в failure::Error , когда установлено значение RUST_BACKTRACE=1 , но в любом случае это может выйти за рамки или, по крайней мере, проблема, которую следует решить. быть поднятым с failure .

Это было стабилизировано некоторое время назад , и я не думаю, что мы действительно можем изменить поведение Result . Я лично остаюсь вполне доволен текущим поведением для вариантов использования, к которым я склонен — быстрые и грязные сценарии и тому подобное, — но я согласен, что более сложные сценарии этого не захотят. Однако во время обсуждения мы также обсудили ряд способов, которыми вы могли бы контролировать это поведение (сейчас я не могу найти эти комментарии, потому что github скрывает от меня некоторые вещи).

Например, вы можете определить свою собственную «оболочку» для типа ошибки и реализовать для нее From :

struct PrettyPrintedError { ... }
impl<E: Display> From<E> for PrettyPrintedError { }

impl Debug { /* .. invoke Display .. */ }

Теперь вы можете написать что-то вроде этого, что означает, что вы можете использовать ? в main :

fn main() -> Result<(), PrettyPrintedError> { ... }

Возможно, такой тип должен быть частью quick-cli или что-то в этом роде?

@nikomatsakis Да, я полностью понимаю этот обходной путь, но я действительно чувствую, что это противоречит цели краткости использования ?-in-main . Я чувствую, что «можно использовать ?-in-main , используя этот обходной путь», к сожалению, подрывает саму ?-in-main . Например, я не собираюсь описывать ваш обходной путь в кратких примерах, и я также не собираюсь навязывать зависимость от quicli для каждого написанного мной примера. Я, конечно, не собираюсь использовать его и для быстрых программ, потому что мне нужен вывод Display ошибки практически в каждой CLI-программе, которую я пишу. Мое мнение таково, что отладочный вывод ошибки — это ошибка в CLI-программах, которую вы ставите перед пользователями.

Альтернативой ?-in-main является дополнительная функция ~4 строки. Так что, если обходной путь для исправления ?-in-main для использования Display намного больше, чем это, то я лично не вижу особых причин использовать его вообще (за исключением тестов документации или модульных тестов, поскольку упомянутый выше).

Это что-то, что можно было бы изменить в издании?

@BurntSushi Как бы вы отнеслись к тому, что обходной путь находится в std , поэтому вам нужно всего лишь написать немного более длинное объявление возвращаемого типа для main() ?

@Kixunil Мое первоначальное ощущение, что это может быть приемлемо. Хотя особо об этом не думал.

@BuntSushi

Альтернативой ?-in-main является дополнительная функция ~4 строки.

В конечном счете, поведение стабильно, и я не думаю, что мы можем реально изменить его на данный момент. (Я имею в виду, что это не нарушение целостности или что-то в этом роде.)

Тем не менее, я по-прежнему нахожу текущую настройку довольно приятной и предпочтительной, Display , но я полагаю, что это вопрос того, что вы хотите, чтобы было «по умолчанию», то есть любую настройку можно заставить работать как другие через различные новые типы. Таким образом, либо мы по умолчанию отдаем предпочтение «быстрым и грязным» сценариям, которые требуют Debug , либо отшлифованным конечным продуктам. Я склонен думать, что безупречный конечный продукт — это когда я могу достаточно легко позволить себе дополнительный импорт, а также когда я хотел бы выбирать из множества различных возможных форматов (например, мне нужна только ошибка или я хочу включить argv [0] и т. д.).

Чтобы быть более конкретным, я представляю, что это будет выглядеть так:

use failure::format::JustError;

fn main() -> Result<(), JustError> { .. }

или, чтобы получить другой формат, возможно, я делаю это:

use failure::format::ProgramNameAndError;

fn main() -> Result<(), ProgramNameAndError> { .. }

Оба они выглядят довольно хорошо по сравнению с 4-строчной функцией, которую вам приходилось писать раньше:

use std::sys;

fn main() {
  match inner_main() {
    Ok(()) => { }
    Err(error) => {
      println!("{}", error);
      sys::exit(1);
    }
}

fn inner_main() -> Result<(), Error> {
  ...
}

Я подхожу к этой теме, так как для качества производства добавление того, что по сути, к пользовательскому типу ошибки для вывода, кажется хорошим направлением, чтобы подтолкнуть людей в любом случае. Кажется, это похоже на то, как panic! , например, по умолчанию выдает сообщения о качестве отладки.

@nikomatsakis Это, возможно, лучшая долгосрочная перспектива, и я нахожу ваш конечный результат аппетитным. Было бы неплохо, если бы эти типы ошибок когда-нибудь оказались в std , чтобы их можно было использовать в примерах с минимальными накладными расходами. :-)

Примечание процесса: я попытался сформулировать свой первоначальный отзыв здесь так: «можно ли что-нибудь сделать», а не «давайте изменим то, что уже стабилизировалось», чтобы сосредоточиться на цели высокого уровня, а не просить о чем-то, что мы конечно не может сделать. :-)

Что ж , это не заняло много времени: https://crates.io/crates/exitfailure 😆

Мне интересно, сколько людей удивлены использованием признака Debug против признака Display . И первоначальный проект обсуждения, и окончательный RFC используют Display . Это похоже на инцидент с универсальной чертой импликации, когда люди думали, что получают одно, но узнают, что получают другое, только после того, как оно стабилизируется. Кроме того, учитывая, что многие люди удивлены тем, что не будут использовать эту функцию, я чувствую, что не будет большой обратной косой черты, если мы изменим ее в следующем выпуске.

@WiSaGaN Детали RFC могут и будут время от времени меняться. Это ожидаемо и здорово. RFC — это проектные документы, а не идеальное представление того, что есть и всегда будет. Поиск информации затруднен, и это моя вина, что я не уделяю этому больше внимания. Я надеюсь, что мы сможем избежать пересмотра стабильности этой функции и вместо этого сосредоточимся на целях более высокого уровня, которые мы можем реализовать.

@nikomatsakis Основная проблема, которую я вижу в пользу быстрых и грязных дел, заключается в том, что уже есть способ их решить: .unwrap() . Таким образом, с этой точки зрения, ? — это просто еще один способ писать быстрые и грязные вещи, но не существует такого же простого способа писать отточенные вещи.

Конечно, корабль уплыл, так что мы в основном облажались. Я бы хотел, чтобы это изменилось в следующем издании, если это возможно.

@Kixunil , unwrap на самом деле не лучший способ решить проблему, поскольку в заголовке этой темы говорится, что мы хотели бы иметь возможность использовать синтаксический сахар ? в main . Я могу понять, откуда вы пришли (на самом деле это было частью моих первоначальных колебаний с использованием ? для обработки опционов), но с включением ? в язык эта проблема становится серьезной. довольно приятно выиграть юзабилити ИМХО. Если вам нужен более приятный результат, то для вас есть варианты. Вы не выпускаете что-то для пользователей без предварительного тестирования, и открытие того, что вам нужен пользовательский тип для вывода main , будет довольно быстрой реализацией.

Что касается «почему» мы хотели бы ? в main , подумайте, как это странно сейчас. Вы можете использовать его практически везде (поскольку у вас есть контроль над типом возвращаемого значения). Затем функция main начинает казаться чем-то особенным, чего не должно быть.

.unwrap() — это быстрое и грязное решение, которое не компонуется, поэтому вы не можете учитывать код в main() , пока вы набрасываете программу.

Для сравнения, ?это быстрое и грязное решение, которое компонуется, и сделать его небыстрым и грязным — это вопрос установки правильного возвращаемого типа в main() , а не модификации самого кода.

@Screwtapello ах, теперь это имеет смысл для меня. Спасибо!

Я просто хочу выразить, что я думал, что целью этого было принести ? в main для всех приложений, не прибегая к дополнительным оболочкам... Если это просто для тестирования, я не вижу в этом много пользы и, к сожалению, буду продолжать придерживаться .unwrap() .

@oblitum Это не только для тестирования. Просто (по умолчанию) не будет использоваться формат Display . Это может быть или не быть выходом, который вы ищете, но он почти наверняка будет лучше, чем вывод .unwrap . С учетом сказанного, использование ? может сделать ваш код «красивее». В любом случае по-прежнему можно настроить вывод ошибок ? в main , как подробно описал @nikomatsakis выше.

Можно ли ввести новый тип в stdlib для витрины? Что-то типа:

#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use]
struct DisplayResult<E: fmt::Display>(Result<(), E>)

impl<E> Termination for DisplayResult<E> {
    // ... same as Result, but use "{}" instead of "{:?}"
}

impl<E> From<Result<(), E>> for DisplayResult<E> {}
impl<E> Deref<Result<(), E>> for DisplayResult<E> {}
impl<E> Try for DisplayResult<E> {}
// ...

Это должно позволить пользователям писать:

fn main() -> DisplayResult<MyError> {
    // Ordinary code; conversions happen automatically via From, Try, etc.
}

Основными мотивами здесь являются:

  • Дисплей используется, когда возвращается ошибка
  • Пользователям не нужно дополнительно оборачивать свои ошибки или типы результатов; они могут просто использовать ? в своей основной функции.

Дополнение: мне приходит в голову, что семантика ? и From не позволяет этому работать так неявно, как хотелось бы. Я знаю, что ? будет конвертировать между типами Error через From , но я не знаю, делает ли это то же самое для разных реализаторов Try . То есть он будет конвертировать из Result<?, io::Error> в Result<?, FromIoError> , но не обязательно из Result<?, Err> в DisplayResult<Result<?, Err>> . В принципе, я ищу что-то вроде этого для работы:

fn main() -> DisplayResult<io::Error> {
    let f = io::File::open("path_to_file")?;
    let result = String::new();
    f.read_to_string(result)?
}

Предполагая, что это не работает, возможно, вместо этого можно было бы сделать простой флаг условной компиляции в Cargo.toml?

@Lucretiel : вы, кажется, предполагаете, что программисты в основном просто хотят вывести сообщение о выходе на консоль. Это не относится к нетехническим настольным приложениям, демонам с ведением журналов и т. д. Я предпочитаю, чтобы мы не добавляли «обычно полезные» вещи в стандартную библиотеку без убедительных доказательств (например, цифр), что они должны быть там.

Я предполагаю, что программисты, которые возвращают Result<(), E> where E: Error из main , заинтересованы в выводе сообщения об ошибке на консоль, да. Даже текущее поведение, которое печатается через Debug , - это "вывести выходное сообщение на консоль".

Кажется, в этой ветке существует широкое согласие в том, что что-то должно быть напечатано в stderr; необходимо принять ключевое решение: «Должен ли он печатать с "{}" или "{:?}" или что-то еще, и насколько он должен быть настраиваемым, если вообще должен быть».

@Lucretiel , для меня ценность возврата Result заключается в том, чтобы хорошо контролировать статус выхода. Будет ли печатать что-то не лучше обработчику паники? Вопрос не только в том, печатать ли что-либо в stderr, но и в том, что печатать (след, сообщение об ошибке и т. д.?). См. https://github.com/rust-lang/rust/issues/43301#issuecomment -389099936, особенно. последняя часть.

Я использовал это недавно и хотел, чтобы он назывался Display . Почти уверен, что мы сделали неправильный звонок здесь.

Сбой процесса, который я заметил, заключается в том, что решение было принято командой lang (в основном Нико и я), а не командой libs, но рассматриваемый код полностью находится в std. Возможно, libs приняли бы лучшее решение.

@без лодок уже поздно что-то менять? Я заметил, что эта функция по-прежнему помечена как ночная/экспериментальная в документации , но, возможно, я не понимаю некоторых деталей, лежащих в основе процесса стабилизации.

Я тоже хотел бы увидеть это изменение и сожалею, что не стал более активным сторонником этого, когда команда попросила меня изменить реализацию с Display на Debug .

Черта @Lucretiel Termination работает по ночам, но сама функция (возвращающая реализации Termination из main /тестов) уже стабильна.

Есть ли билет или сроки для стабилизации названия черты?

@xfix Это означает, что реализацию можно изменить, верно? Поведение не меняется ( Termination возвращается из main ), но сам признак изменится с:

impl<E: Debug> Termination for Result<(), E> ...

к:

impl<E: Display> Termination for Result<(), E> ...

можно ли изменить реализацию, верно?

Не без нарушения обратной совместимости. Этот код компилируется в сегодняшней стабильной версии Rust:

#[derive(Debug)]
struct X;

fn main() -> Result<(), X> {
    Ok(())
}

С предложенным изменением, требующим Display , компиляция прекратится.

Как уже упоминалось , возможно, что-то вроде специализации могло бы помочь, но мое текущее понимание специализации таково, что она будет полезна только для тех типов, которые реализуют как Display , так и Debug (т.е. такие, что есть одна реализация, которая более особенная).

trait ResultTerm {
    fn which(&self);
}
impl<T: Debug> ResultTerm for T {
    default fn which(&self) {
        println!("{:?}", self)
    }
}
impl<T: Debug + Display> ResultTerm for T {
    fn which(&self) {
        println!("{}", self)
    }
}

enum MyResult<T, E> {
    Ok(T),
    Err(E),
}

impl<T, E> Termination for MyResult<T, E>
where
    E: ResultTerm,
{
    fn report(self) -> i32 {
        match self {
            MyResult::Err(e) => {
                e.which();
                1
            }
            _ => 0,
        }
    }
}

игровая площадка

Этого может быть достаточно, чтобы охватить все распространенные случаи, но это публично раскрывает специализацию.

А, я понимаю, что вы имеете в виду под устойчивой ржавчиной. По какой-то причине я не знал, что это доступно в стабильной версии.

Это то, что можно переключить во время компиляции с помощью флага Cargo? Точно так же переключаются другие вещи, например panic = "abort" .

Может быть? Я видел, что можно было бы иметь другое поведение для ? в main в другой версии Rust. Однако, наверное, не 2018 год, может быть, 2021 год?

Разные ящики в одной и той же программе могут быть в разных редакциях, но использовать одни и те же std , поэтому трейты, доступные в std , и их реализация должны быть одинаковыми в разных редакциях. То, что мы могли бы сделать в теории (я не защищаю это), — это иметь два трейта, Termination и Termination2 , и требовать тип возвращаемого значения main() для реализации одного или другие в зависимости от редакции ящика, который определяет main() .

Я не думаю, что нам нужно что-то осуждать. Я поддержу мнение @Aaronepower о том, что ограничение только типами Display может привести к недовольству пользователей быстрых сценариев (получение Debug тривиально, реализация Display — нет) точно так же, как текущая ситуация неудовлетворяет производственных пользователей. Даже если бы мы могли, я не думаю, что в конечном итоге это принесло бы пользу. Я думаю, что подход к специализации, изложенный @shepmaster , многообещающий, поскольку он создаст следующее поведение:

  1. Тип, у которого есть Debug, но не Display, будет иметь вывод Debug по умолчанию.
  2. Тип, у которого есть и Debug, и Display, по умолчанию будет печатать вывод Display.
  3. Тип, имеющий Display, но не Debug, приводит к ошибке компилятора.

Кто-то может возразить против пункта 2 в случае, когда у вас есть тип с Display, но вы хотите , чтобы он по какой-то причине печатал Debug; Я думаю, что в этом случае Нико предложил бы различные предварительно разработанные типы ошибок формата вывода (которые, вероятно, стоит изучить, даже если мы пойдем по пути специализации).

Что касается пункта 3, то он немного дерганый (может быть, нам стоило сделать trait Display: Debug еще в версии 1.0?), но если кто-то взял на себя труд написать реализацию отображения, то не так уж и сложно просить, чтобы они его прихлопнули. один производный (который, я подозреваю, у них, вероятно, в любом случае ... есть ли кто-нибудь, кто принципиально возражает против наличия Debug в их типе Display?).

ограничение только типами отображения может служить только для неудовлетворения пользователей быстрых сценариев (получение отладки тривиально, реализация отображения - нет)

Случай, когда Display предпочтительнее Debug в быстрых сценариях, использует &'static str или String в качестве типа ошибки. Если у вас вообще есть пользовательский тип ошибки, на который нужно наложить #[derive(Debug)] , вы уже тратите некоторую нетривиальную энергию на обработку ошибок.

@SimonSapin Я бы не сказал, что это предпочтительнее в среде быстрого написания сценариев, или, скорее, недостаточно предпочтительно, чтобы я был в положении, когда я хотел, чтобы строка печаталась в формате Display , не желая прилагать усилий чтобы иметь правильно отформатированные ошибки, для меня Debug предпочтительнее, потому что формат ошибки, когда это String, неправильно сформирован, поэтому наличие Err("…") вокруг него позволяет мне легко визуально выделить ошибку из любого другого вывод, который мог быть испущен.

FWIW форматируется не значение Result<_, E> , а только значение E внутри. Таким образом, вы не увидите Err( на выходе. Однако текущий код добавляет префикс Error: , который, по-видимому, останется, если используется Display . В дополнение к тому, что кавычки не печатаются, Display также не будет экранировать содержимое строки обратной косой чертой, что желательно, IMO, когда дело доходит до отображения сообщения (ошибки) пользователям.

https://github.com/rust-lang/rust/blob/cb6eeddd4dcefa4b71bb4b6bb087d05ad8e82145/src/libstd/process.rs#L1527 -L1533

Я думаю, что в данной ситуации лучшее, что мы можем сделать, — это специализация типов, реализующих Display + Debug , для вывода реализации Display . К сожалению, типы, которые не реализуют Debug , не могут использоваться как ошибки (пока мы не поддерживаем пересечение импликаций), но это лучшее, что мы могли сделать.

Главный вопрос заключается в том, подпадает ли этот импл под нашу текущую политику в отношении специализированных имплов в стандартном исполнении. Обычно у нас есть правило, что мы используем специализацию только в std, где мы не предоставляем стабильную гарантию того, что поведение не изменится позже (потому что специализация сама по себе является нестабильной функцией). Удобны ли мы, предоставляя этот импл сейчас, понимая, что, возможно, когда-нибудь эти типы вернутся к выводу своих Debug вывода, если мы в конечном итоге удалим специализацию как функцию?

На самом деле, я думаю, мы не можем сейчас допустить эту специализацию, потому что она может быть использована для внесения несостоятельности. Этот импл на самом деле является именно той специализацией, которая доставляет нам столько проблем!

use std::cell::Cell;
use std::fmt::*;

struct Foo<'a, 'b> {
     a: &'a Cell<&'a i32>,
     b: &'b i32,
}

impl<'a, 'b> Debug for Foo<'a, 'b> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        Ok(())
    }
}

impl<'a> Display for Foo<'a, 'a> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        self.a.set(self.b);
        Ok(())
    }
}

Из-за того, как в настоящее время работает специализация, я мог бы вызвать report для Foo<'a, 'b> , где 'b не переживет 'a , и в настоящее время вполне возможно, что компилятор выберет реализацию, которая вызывает экземпляр Display , даже если требования к сроку службы не соблюдены, что позволяет пользователю недопустимо продлить время жизни ссылки.

Не знаю, как вы, ребята, но когда я пишу быстрые «сценарии», я просто unwrap() все на херню. Это в 100 раз лучше, потому что у него есть обратные следы, которые предоставляют мне контекстную информацию. Без него, если у меня есть let ... = File::open() более одного раза в моем коде, я уже не могу определить, какой из них не сработал.

Не знаю, как вы, ребята, но когда я пишу быстрые «сценарии», я просто unwrap() все на херню. Это в 100 раз лучше, потому что у него есть обратные следы, которые предоставляют мне контекстную информацию. Без него, если у меня есть let ... = File::open() более одного раза в моем коде, я уже не могу определить, какой из них не сработал.

Именно поэтому я так сильно отношусь к Display , так как обычно в этом случае я делаю map_err , который прикрепляет имя файла и другую соответствующую контекстуальную информацию. По моему опыту, как правило, вывод отладки менее полезен для отслеживания того, что на самом деле пошло не так.

@Kixunil : В качестве альтернативы я бы предпочел более сложную стратегию обработки ошибок и/или более обширное ведение журнала и/или отладку.

Я думаю, что «Реализовать RFC» можно поставить галочку, верно? По крайней мере, по этому ! :улыбка:

Простите меня, если это совершенно невежественно, но не могли бы вы использовать специализацию, чтобы сообщить о цепочке ошибок, когда это возможно:

use std::error::Error;

impl<E: fmt::Debug> Termination for Result<!, E> {
    default fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);
        ExitCode::FAILURE.report()
    }
}

impl<E: fmt::Debug + Error> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);

        for cause in Error::chain(&err).skip(1) {
            eprintln!("Caused by: {:?}", cause);
        }
        ExitCode::FAILURE.report()
    }
}

https://github.com/rust-lang/rfcs/blob/f4b8b61a414298ba0f76d9b786d58ccdc34a44bb/text/1937-ques-in-main.md#L260 -L270

impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(ref err) => {
                print_diagnostics_for_error(err);
                EXIT_FAILURE
            }
        }
    }
}

Есть ли причина, по которой эта часть RFC не была реализована?

Мы внесли некоторые изменения в точные наборы трейтов и импликаций, которые будут использоваться, хотя я не помню подробностей на данный момент — и я полагаю, что осталось несколько деталей, которые нужно стабилизировать. Вы можете проверить отчеты о стабилизации, связанные с главным выпуском, для получения более подробной информации.

Каков статус этой функции? Есть предложение по стабилизации?

@GrayJack Эту проблему следует закрыть, так как она возникла довольно давно.

Правда, и я немного запутался, я попал сюда по ссылке в черте Termination и спрашивал об этом, есть ли правильный вопрос для termination_trait_lib ?

Этот вопрос должен быть закрыт

Этот вопрос все еще открыт для отслеживания стабилизации Termination .

Была ли эта страница полезной?
0 / 5 - 0 рейтинги