Rust: Разрешить синтаксис `await`

Созданный на 15 янв. 2019  ·  512Комментарии  ·  Источник: rust-lang/rust

Прежде чем комментировать эту тему, проверьте https://github.com/rust-lang/rust/issues/50547 и попробуйте убедиться, что вы не дублируете аргументы, которые уже были там.


Записки пастухов:

Если вы новичок в этой теме, начните с https://github.com/rust-lang/rust/issues/57640#issuecomment -456617889, за которым последовали три замечательных сводных комментария, последним из которых был https: //github.com/rust-lang/rust/issues/57640#issuecomment -457101180. (Спасибо, @traviscross!)

A-async-await AsyncAwait-Focus C-tracking-issue T-lang

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

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


Котлин

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (сопрограммы TR)

auto result = co_await task;

Взломать

$result = await task;

Дротик

var result = await task;

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

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

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


Котлин

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (сопрограммы TR)

auto result = co_await task;

Взломать

$result = await task;

Дротик

var result = await task;

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

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

Я бы сказал, что у языков, поддерживающих методы расширения, они, как правило, есть. К ним относятся Rust, Kotlin, C # (например, метод-синтаксис LINQ и различные компоновщики) и F #, хотя последний активно использует оператор канала для того же эффекта.

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

Я хотел бы видеть, что эта проблема упоминалась в верхнем сообщении № 50547 (рядом с флажком «Последний синтаксис для ожидания»).

Котлин

val result = task.await()

Синтаксис Котлина:

val result = doTask()

await - это просто suspendable function , а не первоклассная вещь.

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

@cramertj Поскольку в https://github.com/rust-lang/rust/issues/50547 есть 276 комментариев, не могли бы вы подвести итог приведенным там аргументам, чтобы упростить их здесь повторение? (Может быть, добавить их в ОП здесь?)

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

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

Также что с другими языками, использующими неявные ожидания, например go-lang?

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

Но хотя неявный стиль сопрограммы обсуждался, а я сторонник явности, может ли вызов сопрограммы быть недостаточно явным? Это, вероятно, работает лучше всего, когда сопрограмма не используется напрямую там, где она построена (или может работать с потоками). По сути, в отличие от обычного вызова, мы ожидаем, что сопрограмма займет больше времени, чем необходимо, в более спокойном порядке оценки. А .await!() - это более или менее попытка различить обычные вызовы и вызовы сопрограмм.

Итак, после того, как мы надеемся представить несколько новый взгляд на то, почему можно предпочесть пост-исправление, скромное предложение по синтаксису:

  • future(?)
  • или future(await) который, конечно, имеет свои собственные компромиссы, но кажется менее запутанным, см. нижнюю часть сообщения.

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

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.log("beginning service call")(?);
   let output = service(?); // Actually wait for its result
   self.logger.log("foo executed with result {}.", output)(?);
   output
}

И с альтернативой:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.log("beginning service call")(await);
   let output = service(await);
   self.logger.log("foo executed with result {}.", output)(await);
   output
}

Во избежание нечитаемого кода и для облегчения анализа разрешайте пробелы только после вопросительного знака, а не между ним и открытым параном. Итак, future(? ) - это хорошо, а future( ?) нет. Эта проблема не возникает в случае future(await) где все текущие токены могут использоваться, как и раньше.

Взаимодействие с другими операторами пост-исправления (такими как текущая попытка ? ) также аналогична вызовам функций:

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(?);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(?)?;
    logger.timestamp()(?);
    Ok(length)
}

Или же

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(await);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(await)?;
    logger.timestamp()(await);
    Ok(length)
}

Несколько причин полюбить это:

  • По сравнению с .await!() он не указывает на член, который мог бы иметь другое использование.
  • Он следует естественному приоритету вызовов, например, цепочке и использованию ? . Это снижает количество классов старшинства и помогает в обучении. И вызовы функций всегда были чем-то особенным в этом языке (даже несмотря на то, что у них есть черта), так что нет никаких ожиданий, что пользовательский код сможет определить свой собственный my_await!() который имеет очень похожие синтаксис и эффект.
  • Это может быть распространено на генераторы и потоки, а также на генераторы, которые ожидают, что при возобновлении будет предоставлено больше аргументов. По сути, это ведет себя как FnOnce тогда как Streams будет вести себя как FnMut . Дополнительные аргументы также можно легко найти.
  • Для тех, кто раньше использовал текущие Futures , это показывает, как ? с Poll должны были работать все время (неудачная последовательность стабилизации здесь). В качестве шага обучения это также согласуется с ожиданием, что оператор на основе ? отклонит поток управления. (await) с другой стороны, этого не удовлетворит, но после этого функция всегда будет ожидать возобновления в точке расхождения.
  • Он действительно использует функциональный синтаксис, хотя этот аргумент хорош, только если вы со мной согласны: smile:

И причины не любить это:

  • ? выглядит аргументом, но даже не применяется к выражению. Я считаю, что это можно решить с помощью обучения, поскольку токен, к которому, похоже, применяется, это сам вызов функции, что является в некоторой степени правильным понятием. Это также положительно означает, что синтаксис однозначный, я надеюсь.
  • Более (и разное) сочетание парантезиса и ? может быть трудно разобрать. Особенно, когда одно будущее возвращает результат другого будущего: construct_future()(?)?(?)? . Но вы могли бы привести тот же аргумент в пользу возможности получить результат объекта fn , что приведет к разрешению такого выражения: foobar()?()?()? . Поскольку, тем не менее, я никогда не видел, чтобы это использовалось или жалоба, разделение на отдельные утверждения в таких случаях, кажется, требуется достаточно редко. Эта проблема также не существует для construct_future()(await)?(await)? -
  • future(?) - мой лучший способ получить лаконичный и все же несколько лаконичный синтаксис. Тем не менее, его рассуждения основаны на деталях реализации в сопрограммах (временное возвращение и отправка при возобновлении), что может сделать его непригодным для абстракции. future(await) было бы альтернативой, которую все еще можно было бы объяснить после того, как await было усвоено как ключевое слово, но позицию аргумента мне немного трудно проглотить. Это могло бы быть хорошо, и это, безусловно, более читабельно, когда сопрограмма возвращает результат.
  • Вмешательство в другие предложения вызова функций?
  • Твой собственный? Вам это не обязательно должно нравиться, просто не предлагать этот краткий синтаксис после исправления было пустой тратой.

future(?)

В Result нет ничего особенного: Futures может возвращать любой тип Rust. Так уж случилось, что некоторые фьючерсы возвращают Result

Итак, как это будет работать для фьючерсов, которые не возвращают Result ?

Кажется, было непонятно, что я имел в виду. future(?) - это то, что ранее обсуждалось как future.await!() или подобное. Ветвление в будущем, которое также возвращает результат, будет future(?)? (два разных способа преждевременного отказа от потока управления). Это делает будущий опрос (?) и результат тестирования ? ортогональными. Изменить: для этого добавлен дополнительный пример.

Ветвление в будущем, которое также возвращает результат, будет future(?)?

Спасибо за разъяснения. В таком случае я определенно не фанат этого.

Это означает, что вызов функции, возвращающей Future<Output = Result<_, _>> , будет записан как foo()(?)?

Он очень сложен синтаксисом и использует ? для двух совершенно разных целей.

Если это конкретно намек на оператор ? который является тяжелым, его, конечно, можно заменить новым зарезервированным ключевым словом. Я только сначала подумал, что это слишком похоже на реальный аргумент непонятного типа, но компромисс может сработать с точки зрения помощи в мысленном анализе утверждения. Таким образом, тот же оператор для impl Future<Output = Result<_,_>> будет выглядеть следующим образом:

  • foo()(await)?

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

Это очень тяжелый синтаксис

я думал, в этом весь смысл явных ожиданий?

оно использует ? для двух совершенно разных целей.

да, так что foo()(await) -синтаксис был бы намного лучше.

этот синтаксис подобен вызову функции, которая возвращает закрытие, а затем вызывает это закрытие в JS.

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

Более (и разное) сочетание парантезиса и ? может быть трудно разобрать. Особенно, когда у вас есть одно будущее, возвращающее результат другого будущего: construct_future()(?)?(?)?

Но вы могли бы привести тот же аргумент в пользу возможности получить результат объекта fn , что приведет к разрешению такого выражения: foobar()?()?()? . Поскольку, тем не менее, я никогда не видел, чтобы это использовалось или жалоба, разделение на отдельные утверждения в таких случаях, кажется, требуется достаточно редко.

Я думаю, что опровержение здесь таково: сколько раз вы видели -> impl Fn в дикой природе (не говоря уже о -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _> )? Сколько раз вы ожидаете увидеть -> impl Future<Output = Result<_, _>> в базе асинхронного кода? Необходимость называть редкое возвращаемое значение impl Fn для облегчения чтения кода сильно отличается от необходимости указывать значительную часть временных возвращаемых значений impl Future .

Необходимость называть редкое возвращаемое значение impl Fn для облегчения чтения кода сильно отличается от необходимости именовать значительную часть временных возвращаемых значений impl Future.

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

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

Сколько раз вы ожидаете увидеть -> Im Future> в базе асинхронного кода?

Я ожидаю видеть эквивалент этого типа (async fn, который возвращает Result) все время , вероятно, даже большинство всех async fn, поскольку, если то, что вы ожидаете, является даже IO, вы почти наверняка бросите Ошибки ввода-вывода вверх.


Ссылка на мой предыдущий пост о проблеме с отслеживанием и добавление еще нескольких мыслей.

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

  • Префикс await с обязательными разделителями. Здесь также решается, какие разделители (фигурные скобки или скобки, или принятие того и другого; все они имеют свои плюсы и минусы). То есть await(future) или await { future } . Это полностью решает проблемы с приоритетом, но является синтаксически сложным, и оба параметра разделителя представляют возможные источники путаницы.
  • Префикс await с "полезным" приоритетом относительно ? . (То есть, что await связывает крепче?). Это может удивить некоторых пользователей, читающих код, но я считаю, что функции, возвращающие фьючерсы результатов, будут в подавляющем большинстве более распространены, чем функции, возвращающие результаты фьючерсов.
  • Префикс await с «очевидным» приоритетом относительно ? . (То есть это? Связывает сильнее, чем ожидание). Дополнительный синтаксический сахар await? для комбинированного await и? оператор. Я думаю, что этот синтаксический сахар необходим для того, чтобы вообще сделать этот порядок приоритета жизнеспособным, иначе все будут все время писать (await future)? , что является худшим вариантом первого варианта, который я перечислил.
  • Postfix await с синтаксическим пространством await. Это решает проблему приоритета за счет четкого визуального упорядочивания между двумя операторами. Во многих отношениях меня беспокоит это решение.

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

Для обсуждения я дам этим четырем параметрам следующие имена:

Имя | Будущее | Будущее результата | Результат будущего
--- | --- | --- | ---
Обязательные разделители | await(future) или await { future } | await(future)? или await { future }? | await(future?) или await { future? }
Полезный приоритет | await future | await future? | await (future?)
Очевидное преимущество с сахаром | await future | await? future или (await future)? | await future?
Ключевое слово Postfix | future await | future await? | future? await

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

Одним из недостатков «благословения» await future? в полезном приоритете, но также и других, которые не работают в качестве пост-исправления, может быть то, что обычные шаблоны ручного преобразования выражений с помощью ? могут больше не применяться, или потребовать, чтобы Future явно копировал Result -методы совместимым образом. Я нахожу это удивительным. Если они будут воспроизведены, то неожиданно становится непонятно, какие комбинаторы работают над возвращенным будущим, а какие стремятся. Другими словами, было бы так же сложно решить, что на самом деле делают комбинаторы, как в случае неявного ожидания. (Изменить: на самом деле, см. Два комментария ниже, где у меня есть более техническая перспектива, что я имею в виду с неожиданной заменой ? )

Пример, в котором мы можем исправить ошибку:

async fn previously() -> Result<_, lib::Error> {
    let _ = await get_result()?;
}

async fn with_recovery() -> Result<_, lib::Error> {
    // Does `or_recover` return a future or not? Suddenly very important but not visible.
    let _ = await get_result().unwrap_or_else(or_recover);
    // If `or_recover` is sync, this should still work as a pattern of replacing `?` imho.
    // But we also want `or_recover` returning a future to work, as a combinator for futures?

    // Resolving sync like this just feel like wrong precedence in a number of ways
    // Also, conflicts with `Result of future` depending on choice.
    let _ = await get_result()?.unwrap_or_else(or_recover);
}

Эта проблема не возникает для фактических операторов после исправления:

async fn with_recovery() -> Result<_, lib::Error> {
    // Also possible in 'space' delimited post-fix await route, but slightly less clear
    let _ = get_result()(await)
        // Ah, this is sync
        .unwrap_or_else(or_recover);
    // This would be future combinator.
    // let _ = get_result().unwrap_or_else(or_recover)(await);
}
// Obvious precedence syntax
let _ = await get_result().unwrap_or_else(or_recover);
// Post-fix function argument-like syntax
let _ = get_result()(await).unwrap_or_else(or_recover);

Это разные выражения, оператор точки имеет более высокий приоритет, чем оператор «очевидного приоритета» await , поэтому эквивалент:

let _ = get_result().unwrap_or_else(or_recover)(await);

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

Это имеет ту же двусмысленность, является ли or_recover асинхронным или нет.

Не совсем то же самое. unwrap_or_else должен создать сопрограмму, потому что она ожидается, поэтому неоднозначно, является ли get_result сопрограммой (так создается комбинатор) или Result<impl Future, _>Ok уже содержит сопрограмму, а Err создает ее). У обоих из них нет одинаковых проблем, связанных с возможностью с первого взгляда определить повышение эффективности за счет перемещения точки последовательности await на join , что является одной из основных проблем неявное ожидание. Причина в том, что в любом случае это промежуточное вычисление должно быть синхронизированным и должно быть применено к типу до await и должно было привести к ожидаемой сопрограмме. Здесь есть еще одно, более серьезное беспокойство:

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

Это часть путаницы, замена ? операцией восстановления коренным образом изменила положение await . В контексте синтаксиса ? , учитывая частичное выражение expr типа T , я ожидаю следующую семантику от преобразования (при условии, что T::unwrap_or_else существует) :

  • expr? -> expr.unwrap_or_else(or_recover)
  • <T as Try>::into_result(expr)? -> T::unwrap_or_else(expr, or_recover)

Однако в разделе «Полезный приоритет» и await expr? ( await expr дает T ) вместо этого мы получаем

  • await expr? -> await expr.unwrap_or_else(or_recover)
  • <T as Try>::into-result(await expr) -> await Future::unwrap_or_else(expr, or_recover)

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

А как насчет еще более интересного случая, когда вы ждете в двух разных точках комбинаторной последовательности? Я думаю, что при любом синтаксисе префикса это требует скобок. Остальная часть языка Rust пытается избежать этого, чтобы заставить работать «выражения, вычисляемые слева направо», одним из примеров этого является магия auto-ref.

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

// Chain such that we
// 1. Create a future computing some partial result
// 2. wait for a result 
// 3. then recover to a new future in case of error, 
// 4. then try its awaited result. 
async fn await_chain() -> Result<usize, Error> {
    // Mandatory delimiters
    let _ = await(await(partial_computation()).unwrap_or_else(or_recover))?
    // Useful precedence requires paranthesis nesting afterall
    let _ = await { await partial_computation() }.unwrap_or_else(or_recover)?;
    // Obivious precendence may do slightly better, but I think confusing left-right-jumps after all.
    let _ = await? (await partial_computation()).unwrap_or_else(or_recover);
    // Post-fix
    let _ = partial_computation()(await).unwrap_or_else(or_recover)(await)?;
}

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

Запись в таблице в стиле @withoutboats :

| Имя | Будущее | Будущее результата | Результат будущего |
| - | - | - | - |
| Обязательные разделители | await(future) | await(future)? | await(future?) |
| Полезный приоритет | await future | await future? | await (future?) |
| Очевидный приоритет | await future | await? future | await future? |
| Постфиксный вызов | future(await) | future(await)? | future?(await) |

| Имя | Прикованный |
| - | - |
| Обязательные разделители | await(await(foo())?.bar())? |
| Полезный приоритет | await(await foo()?).bar()? |
| Очевидный приоритет | await? (await? foo()).bar() |
| Постфиксный вызов | foo()(await)?.bar()(await) |

Я решительно поддерживаю postfix await по разным причинам, но мне не нравится вариант, показанный @withoutboats , в первую очередь, кажется, по тем же причинам. Например. foo await.method() сбивает с толку.

Сначала давайте посмотрим на аналогичную таблицу, но добавим еще пару вариантов постфикса:

| Имя | Будущее | Будущее результата | Результат будущего |
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| Обязательные разделители | await { future } | await { future }? | await { future? } |
| Полезный приоритет | await future | await future? | await (future?) |
| Очевидный приоритет | await future | await? future | await future? |
| Ключевое слово Postfix | future await | future await? | future? await |
| Поле Postfix | future.await | future.await? | future?.await |
| Постфиксный метод | future.await() | future.await()? | future?.await() |

Теперь давайте посмотрим на связанное будущее выражение:

| Имя | Связанные фьючерсы результатов |
| ---------------------- | -------------------------- ----------- |
| Обязательные разделители | await { await { foo() }?.bar() }? |
| Полезный приоритет | await (await foo()?).bar()? |
| Очевидный приоритет | await? (await? foo()).bar() |
| Ключевое слово Postfix | foo() await?.bar() await? |
| Поле Postfix | foo().await?.bar().await? |
| Постфиксный метод | foo().await()?.bar().await()? |

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

let res: MyResponse = client.get("https://my_api").send().await?.json().await?;

На самом деле я думаю, что каждый разделитель отлично подходит для постфиксного синтаксиса, например:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
Но у меня нет твердого мнения о том, какой из них использовать.

Может ли постфиксный макрос (например, future.await!() ) по-прежнему быть вариантом? Понятно, лаконично и недвусмысленно:

| Будущее | Будущее результата | Результат будущего |
| --- | --- | --- |
| future.await! () | future.await! ()? | будущее? .await! () |

Также постфиксный макрос требует меньше усилий для реализации, его легко понять и использовать.

Также постфиксный макрос требует меньше усилий для реализации, его легко понять и использовать.

Кроме того, он просто использует функцию общего языка (или, по крайней мере, это будет выглядеть как обычный макрос postfix).

Макрос постфикса был бы хорош, поскольку он сочетает в себе лаконичность и цепочность постфикса с немагическими свойствами и очевидным присутствием макросов и хорошо сочетается со сторонними пользовательскими макросами, такими как некоторые .await_debug!() , .await_log!(WARN) или .await_trace!()

Постфиксный макрос был бы хорош, поскольку он сочетает в себе [...] немагические [...] свойства макросов.

@novacrazy проблема с этим аргументом в том, что любой макрос await! _would_ быть волшебным, он выполняет операцию, которая невозможна в написанном пользователем коде (в настоящее время реализация на основе базового генератора в некоторой степени раскрыта, но я понимаю, что до стабилизации он будет полностью скрыт (и для реального взаимодействия с ним на данный момент требуется использование некоторых rustc -внутренних ночных функций)).

@ Nemo157 Хм. Я не знал, что он должен быть таким непрозрачным.

Не слишком ли поздно пересматривать использование процедурного макроса типа #[async] для преобразования из "асинхронной" функции в функцию генератора, а не волшебного ключевого слова? Это три дополнительных символа, которые нужно ввести, и они могут быть отмечены в документации так же, как #[must_use] или #[repr(C)] .

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

Я твердо верю, что язык Rust (а не std / core ) должен предоставлять абстракции и синтаксис, только если они невозможны (или крайне непрактичны) для пользователей или std . В этом отношении вся эта асинхронная штука вышла из-под контроля. Неужели нам действительно нужно что-то большее, чем API контактов и генераторы в rustc ?

@novacrazy Я в целом согласен с мнением, но не с заключением.

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

В чем причина наличия for -циклов в языке, когда они также могут быть макросом, который превращается в loop с перерывами. В чем причина || closure когда это могут быть выделенные конструкторы признаков и объектов. Почему мы ввели ? когда у нас уже было try!() . Причина, по которой я не согласен с этими вопросами и вашими выводами, заключается в последовательности. Смысл этих абстракций не только в том, какое поведение они инкапсулируют, но и в его доступности. for -replacement отличается изменчивостью, основным путем кода и удобочитаемостью. || -replacement отличается подробностью объявления - аналогично Futures настоящее время. try!() разбивается в ожидаемом порядке выражений и возможности компоновки.

Учтите, что async - это не только декоратор функции, но и другие мысли о предоставлении дополнительных шаблонов с помощью aync-blocks и async || . Поскольку это применимо к элементам на разных языках, удобство использования макроса кажется неоптимальным. Даже не думать о реализации, если тогда она должна быть видна пользователю.

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

Я не думаю, что этот аргумент применим, потому что реализация сопрограмм, полностью использующих std api, скорее всего, будет сильно зависеть от unsafe . И тогда возникает обратный аргумент , потому что в то время как это выполнимо, и вы не остановить , даже если есть синтаксический и семантический способ на языке , чтобы сделать это, любое изменение будет в значительной степени подвержено риску ломающихся предположений , сделанных в unsafe -код. Я утверждаю, что Rust не должен делать вид, будто пытается предложить стандартный интерфейс для реализации битов, которые он не намерен в ближайшее время стабилизировать, включая внутреннее устройство сопрограмм. Аналогом этого может быть extern "rust-call" который служит текущей магией, чтобы прояснить, что вызовы функций не имеют такой гарантии. Возможно, нам никогда не понадобится return , даже если судьба стековых сопрограмм еще не решена. Мы могли бы захотеть глубже встроить оптимизацию в компилятор.

Кроме того: кстати говоря, о какой теоретически не столь серьезной идее можно было бы обозначить сопрограмму await как гипотетический extern "await-call" fn () -> T ? Если так, это позволит в прелюдии

trait std::ops::Co<T> {
    extern "rust-await" fn await(self) -> T;
}

impl<T> Co<T> for Future<Output=T> { }

ака. future.await() в документированных элементах пользовательского пространства. Или, если на то пошло, также возможен другой синтаксис оператора.

@HeroicKatora

Почему мы ввели ? когда у нас уже было try!()

Честно говоря, я тоже был против, хотя на мне это прижилось. Было бы более приемлемо, если бы Try когда-либо стабилизировалось, но это уже другая тема.

Проблема с приведенными вами примерами «сахара» в том, что они очень и очень жидкие. Даже impl MyStruct - это более или менее сахар для impl <anonymous trait> for MyStruct . Это сахара качества жизни, которые вообще не добавляют никаких накладных расходов.

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

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

В идеале, было бы замечательно , чтобы поддержать async ключевое слово и #[async] атрибут / процедурный макросъемки, с бывшим разрешение доступа низкого уровня к сгенерированной (не каламбур) генератора. Между тем, yield следует запретить в блоках или функциях, использующих async в качестве ключевого слова. Я уверен, что они могли бы даже поделиться кодом реализации.

Что касается await , если оба из вышеперечисленных возможны, мы могли бы сделать что-то подобное и ограничить ключевое слово await до async функции / блоки ключевого слова и использовать какие-то await!() макрос в #[async] функциях.

Псевдокод:

// imaginary generator syntax stolen from JavaScript
fn* my_generator() -> T {
    yield some_value;

    // explicit return statements are only included to 
    // make it clear the generator/async functions are finished.
    return another_value;
}

// `await` keyword would not be allowed here, but the `yield` keyword is
#[async]
fn* my_async_generator() -> Result<T, E> {
    let item = some_op().await!()?; // uses the `.await!()` macro
    // which would really just use `yield` internally, but with the pinning API

    yield future::ok(item.clone());

    return Ok(item);
}

// `yield` would not be allowed here, but the `await` keyword is.
async fn regular_async() -> Result<T, E> {
   let some_op = async || { /*...*/ };

   let item = some_op() await?;

   return Ok(item);
}

Лучшее из обоих миров.

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

Помните, что эта проблема предназначена специально для обсуждения синтаксиса await . Другие разговоры о том, как реализованы функции и блоки async выходят за рамки, за исключением того, чтобы напомнить людям, что await! - это не то, что вы можете или когда-либо сможете написать в Rust. поверхностный язык.

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

Для сравнения: syntax , example (reqwest из @mehcode выглядит как реальный тест, который можно использовать в этом отношении), затем таблица ( concerns и необязательная resolution , например, если договорились, что все сводится к обучению). Не стесняйтесь добавлять синтаксис и / или проблемы, я отредактирую их в этом коллективном сообщении. Насколько я понимаю, любой синтаксис, который не включает await , скорее всего, будет казаться чуждым как новичкам, так и опытным пользователям, но все перечисленные в настоящее время включают его.

Пример в одном синтаксисе префикса только для справки, не используйте эту часть, пожалуйста:

let sent = (await client.get("https://my_api").send())?;
let res: MyResponse = (await sent.json())?;
  • Ключевое слово Postfix foo() await?

    • Пример: client.get("https://my_api").send() await?.json() await?

    • | Обеспокоенность | Разрешение |

      | - | - |

      | Цепочка без ? может сбивать с толку или быть запрещенной | |

  • Поле постфикса foo().await?

    • Пример: client.get("https://my_api").send().await?.json().await?

    • | Обеспокоенность | Разрешение |

      | - | - |

      | Похоже на поле | |

  • Постфиксный метод foo().await()?

    • Пример: client.get("https://my_api").send().await()?.json().await()?

    • | Обеспокоенность | Разрешение |

      | - | - |

      | Похоже на метод или черту | Это может быть задокументировано как ops:: trait? |

      | Не вызов функции | |

  • Постфиксный вызов foo()(await)?

    • Пример: client.get("https://my_api").send()(await)?.json()(await)?

    • | Обеспокоенность | Разрешение |

      | - | - |

      | Можно спутать с реальным аргументом | ключевое слово + выделение + неперекрытие |

  • Макрос постфикса foo().await!()?

    • Пример: client.get("https://my_api").send().await!()?.json().await!()?

    • | Обеспокоенность | Разрешение |

      | - | - |

      | Фактически не будет макросом… | |

      | … Или await больше не является ключевым словом | |

Дополнительная мысль о пост-исправлении и префиксе с точки зрения возможного включения генераторов: учитывая значения, yield и await занимают два противоположных типа операторов. Первый передает значение вашей функции извне, второй принимает значение.

Примечание: у Python есть интерактивные генераторы, где yield может возвращать значение. Симметрично, вызовы такого генератора или потока требуют дополнительных аргументов в настройке строгого типа. Не будем обобщать слишком далеко, и мы увидим, что в любом случае аргумент, вероятно, передается.

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

Поскольку неявное кажется неподходящим.

Из-за использования async / await на других языках и просмотра вариантов здесь я никогда не считал синтаксически приятным связывание фьючерсов.

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

// TODO: Better variable names.
await response = client.get("https://my_api").send();
await response = response?.json();
await response = response?;

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

Проблема с созданием привязки await заключается в том, что история ошибки не очень приятна.

// Error comes _after_ future is awaited
let await res = client.get("http://my_api").send()?;

// Ok
let await res = client.get("http://my_api").send();
let res = res?;

Мы должны помнить, что почти все фьючерсы, доступные в сообществе для ожидания, подвержены ошибкам и должны сочетаться с ? .

Если нам действительно нужен синтаксический сахар:

await? response = client.get("https://my_api").send();
await? response = response.json();

И await и await? необходимо добавить в качестве ключевых слов, или мы также расширим это до let , то есть let? result = 1.divide(0);

Учитывая, как часто цепочки используются в коде Rust, я полностью согласен с тем, что цепные ожидания должны быть как можно более понятными для читателя. В случае постфиксного варианта await:

client.get("https://my_api").send().await()?.json().await()?;

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

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

client.get("https://my_api").send().await!()?.json().await!()?;

С учетом сказанного, стоит учесть, что у нас уже есть try!(expr) в языке, и это был наш предшественник для ? . Добавление макроса await!(expr) сейчас будет полностью соответствовать тому, как try!(expr) и ? были введены в язык.

С версией await await!(expr) у нас есть возможность либо перейти на постфиксный макрос позже , либо добавить новый оператор в стиле ? , чтобы упростить цепочку. Пример похож на ? но для ожидания:

// Not proposing this syntax at the moment. Just an example.
let a = perform()^;

client.get("https://my_api").send()^?.json()^?;

Я думаю, что сейчас нам следует использовать await!(expr) или await!{expr} поскольку это и очень разумно, и прагматично. Затем мы можем запланировать переход на постфиксную версию await (например, .await! или .await!() ) позже, если / когда-то станут актуальными постфиксные макросы. (Или в конечном итоге пойти по пути добавления дополнительного оператора стиля ? ... после долгих размышлений по этому поводу: P)

К вашему сведению, синтаксис Scala - это не Await.result поскольку это блокирующий вызов. Фьючерсы Scala - это монады, поэтому используют обычные вызовы методов или понимание монады for :

for {
  result <- future.map(further_computation)
  a = result * 2
  _ <- future_fn2(result)
} yield 123

В результате этой ужасной записи была создана библиотека под названием scala-async с синтаксисом, который я больше всего поддерживаю, а именно:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}

val future = async {
  val f1 = async { ...; true }
  val f2 = async { ...; 42 }
  if (await(f1)) await(f2) else 0
}

Это сильно отражает то, как я хотел бы, чтобы код ржавчины выглядел, с использованием обязательных разделителей, и, как таковой, я хотел бы согласиться с другими в том, чтобы оставаться с текущим синтаксисом await!() . Ранний Rust был тяжелым символом, и я полагаю, что от него отказались по уважительной причине. Использование синтаксического сахара в форме постфиксного оператора (или того, что у вас есть), как всегда, обратно совместимо, и ясность await!(future) однозначна. Это также отражает прогресс, который у нас был с try! , как упоминалось ранее.

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

Мои два цента (не регулярный участник, fwiw) Я очень неравнодушен к копированию модели try! . Раньше это делалось один раз, работало хорошо, а после того, как он стал очень популярным, появилось достаточно пользователей, чтобы рассмотреть постфиксный оператор.

Итак, мой голос был бы таков: стабилизируйте с помощью await!(...) и нажмите на постфиксный оператор для хорошей цепочки на основе опроса разработчиков Rust. Await - ключевое слово, но ! указывает на то, что для меня это что-то «волшебное», и скобки делают его недвусмысленным.

Также сравнение:

| Постфикс | Выражение |
| --- | --- |
| .await | client.get("https://my_api").send().await?.json().await? |
| .await! | client.get("https://my_api").send().await!?.json().await!? |
| .await() | client.get("https://my_api").send().await()?.json().await()? |
| ^ | client.get("https://my_api").send()^?.json()^? |
| # | client.get("https://my_api").send()#?.json()#? |
| @ | client.get("https://my_api").send()@?.json()@? |
| $ | client.get("https://my_api").send()$?.json()$? |

Мой третий цент - это то, что мне нравятся @ (для "ожидания") и # (для представления многопоточности / параллелизма).

Мне тоже нравится постфикс @ ! Я думаю, что на самом деле это неплохой вариант, хотя, кажется, есть некоторое мнение, что это нежизнеспособно.

  • _ @ for await_ - красивая и легко запоминающаяся мнемоника
  • ? и @ будут очень похожи, поэтому изучение @ после изучения ? не должно быть таким скачком
  • Это помогает сканировать цепочку выражений слева направо, без необходимости сканировать вперед, чтобы найти закрывающий разделитель, чтобы понять выражение.

Я очень поддерживаю синтаксис await? foo , и я думаю, что он похож на некоторый синтаксис, встречающийся в математике, где, например, sin² x может означать (sin x) ². Сначала это выглядит немного неудобно, но я думаю, что к этому очень легко привыкнуть.

Как было сказано выше, я одобряю добавление await!() в качестве макроса, точно так же, как try!() , на данный момент и в конечном итоге решаю, как его постфиксировать. Если мы можем иметь в виду поддержку rustfix, который автоматически преобразует вызовы await!() в postfix await, это еще лучше.

Для меня вариант ключевого слова postfix - явный победитель.

  • Нет проблем с приоритетом / порядком, но порядок все же можно указать в скобках. Но в большинстве случаев нет необходимости в чрезмерном вложении (аналогичный аргумент в пользу постфикса «?» Вместо «try ()!»).

  • Это хорошо выглядит с многострочным объединением (см. Предыдущий комментарий @earthengine), и снова нет путаницы в отношении порядка или того, что ожидается. И никаких вложенных / дополнительных круглых скобок для выражений с многократным использованием await:

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;
  • Он поддается простому макросу await! () (См. Предыдущий комментарий @novacrazy):
macro_rules! await {
    ($e:expr) => {{$e await}}
}
  • Даже однострочная, голая (без '?') Цепочка ключевых слов postfix await меня не беспокоит, потому что она читается слева направо, и мы ожидаем возврата значения, с которым затем работает последующий метод (хотя я бы просто предпочитаю многострочный код rustfmt'ed). Пробел разделяет строку и является достаточным визуальным индикатором / сигналом ожидания:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Чтобы предложить другого кандидата, которого я еще не видел (может быть, потому, что он не поддается синтаксическому анализу), как насчет забавного постфиксного оператора с двумя точками '..'? Напоминает, что мы чего-то ждем (результата!) ...

client.get("https://my_api").send()..?.json()..?

Мне тоже нравится постфикс @ ! Я думаю, что на самом деле это неплохой вариант, хотя, кажется, есть некоторое мнение, что это нежизнеспособно.

  • _ @ for await_ - красивая и легко запоминающаяся мнемоника
  • ? и @ будут очень похожи, поэтому изучение @ после изучения ? не должно быть таким скачком
  • Это помогает сканировать цепочку выражений слева направо, без необходимости сканировать вперед, чтобы найти закрывающий разделитель, чтобы понять выражение.

Я не поклонник использования @ для ожидания. Неловко печатать на клавиатуре с раскладкой fin / swe, так как мне нужно нажать alt-gr большим пальцем правой руки, а затем нажать клавишу 2 в числовом ряду. Кроме того, @ имеет хорошо известное значение (at), поэтому я не понимаю, почему мы должны объединять его значение.

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

Вот моя собственная, очень субъективная оценка. Я также добавил future@await , что мне кажется интересным.

| синтаксис | заметки |
| --- | --- |
| await { f } | сильный:

  • очень просто
  • параллели for , loop , async и т. д.
слабый:
  • очень многословный (5 букв, 2 фигурные скобки, 3 необязательных, но, вероятно, линзованных пробела)
  • объединение результатов во множество вложенных фигурных скобок ( await { await { foo() }?.bar() }? )
|
| await f | сильный:
  • параллели синтаксису await из Python, JS, C # и Dart
  • простой, короткий
  • оба полезного приоритета и очевидного приоритета хорошо себя ведут с ? ( await fut? vs. await? fut )
слабый:
  • неоднозначный: необходимо изучить полезный и очевидный приоритет
  • цепочка также очень громоздка ( await (await foo()?).bar()? против await? (await? foo()).bar() )
|
| fut.await
fut.await()
fut.await!() | сильный:
  • позволяет очень легко связать
  • короткая
  • хорошее завершение кода
слабый:
  • обманывает пользователей, заставляя думать, что это поле / функция / макрос, определенный где-то. Изменить: я согласен с @jplatte, что await!() кажется наименее волшебным
|
| fut(await) | сильный:
  • позволяет очень легко связать
  • короткая
слабый:
  • вводит пользователей в заблуждение, заставляя думать, что где-то определена переменная await и что фьючерсы можно вызывать как функцию
|
| f await | сильный:
  • позволяет очень легко связать
  • короткая
слабый:
  • не имеет ничего общего с синтаксисом Rust, не очевидно
  • мой мозг группирует client.get("https://my_api").send() await.unwrap().json() await.unwrap() в client.get("https://my_api").send() , await.unwrap().json() и await.unwrap() (сначала сгруппированные по , затем . ) что не правильно
  • для Haskellers: похоже на каррирование, но не
|
| f@ | сильный:
  • позволяет очень легко связать
  • очень короткий
слабый:
  • выглядит немного неуклюже (по крайней мере, сначала)
  • потребляет @ что может быть лучше подходит для чего-то другого
  • может быть легко не заметить, особенно в больших выражениях
  • использует @ иначе, чем все другие языки
|
| f@await | сильный:
  • позволяет очень легко связать
  • короткая
  • хорошее завершение кода
  • await не обязательно должно быть ключевым словом
  • прямая совместимость: позволяет добавлять новые постфиксные операторы в форме @operator . Например, ? можно было сделать как @try .
  • мой мозг группирует client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() в правильные группы (сначала сгруппированные по . , затем @ )
слабый:
  • использует @ иначе, чем все другие языки
  • может побудить добавить слишком много ненужных постфиксных операторов
|

Мои оценки:

  • осведомленность (fam): насколько этот синтаксис близок к известным синтаксисам (Rust и другие, такие как Python, JS, C #)
  • очевидность (obv): если бы вы впервые прочитали это в чужом коде, смогли бы вы угадать значение, приоритет и т. д.?
  • многословие (vrb): сколько символов требуется для написания
  • видимость (vis): насколько легко заметить (а не не заметить) в коде
  • chaining (cha): насколько легко связать его с . и другими await s
  • grouping (grp): группирует ли мой мозг код на правильные фрагменты
  • прямая совместимость (fwd): позволяет ли это настраивать позже без нарушения

| синтаксис | fam | obv | vrb | вис | ча | grp | fwd |
| --------------------- | ----- | ----- | ----- | ----- | --- - | ----- | ----- |
| await!(fut) | ++ | + | - | ++ | - | 0 | ++ |
| await { fut } | ++ | ++ | - | ++ | - | 0 | + |
| await fut | ++ | - | + | ++ | - | 0 | - |
| fut.await | 0 | - | + | ++ | ++ | + | - |
| fut.await() | 0 | - | - | ++ | ++ | + | - |
| fut.await!() | 0 | 0 | - | ++ | ++ | + | - |
| fut(await) | - | - | 0 | ++ | ++ | + | - |
| fut await | - | - | + | ++ | ++ | - | - |
| fut@ | - | - | ++ | - | ++ | ++ | - |
| fut@await | - | 0 | + | ++ | ++ | ++ | 0 |

Мне кажется, что мы должны отразить синтаксис try!() в первом фрагменте и получить реальное использование от использования await!(expr) прежде чем вводить какой-либо другой синтаксис.

Однако, если / когда мы создадим альтернативный синтаксис ..

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

Префикс async без скобок приводит к неочевидному приоритету или окружающим скобкам при использовании с ? (что будет часто).

Постфикс .await!() цепляет красиво, сразу же кажется очевидным по своему значению, включает ! чтобы сообщить мне, что он собирается творить чудеса, и синтаксически менее нов, поэтому "следующий разрез" подходит мне. лично предпочел бы это. Тем не менее, мне еще предстоит увидеть, насколько это улучшит реальный код по сравнению с первым сокращением await! (expr) .

Я предпочитаю префиксный оператор для простых случаев:
let result = await task;
Это кажется более естественным, учитывая, что мы не записываем тип результата, поэтому await помогает мысленно при чтении слева направо понять, что результатом является задача с ожиданием.
Представьте себе это так:
let result = somehowkindoflongtask await;
пока вы не дойдете до конца задачи, вы не поймете, что тип, который она возвращает, нужно ждать. Также имейте в виду (хотя это может быть изменено и напрямую не связано с будущим языка), что IDE как Intellij встраивают тип (без какой-либо персонализации, если это даже возможно) между именем и равными.
Представьте это так:
6voler6ykj

Это не значит, что я на сто процентов отношусь к приставке. Я очень предпочитаю постфиксную версию будущего, когда речь идет о результатах, поскольку это кажется более естественным. Без всякого контекста я могу легко сказать, что из этого что означает:
future await?
future? await
Вместо этого посмотрите на этот, какой из этих двух истинный, с точки зрения новичка:
await future? === await (future?)
await future? === (await future)?

Я за ключевое слово-префикс: await future .

Это тот, который используется большинством языков программирования с async / await, и поэтому он сразу знаком людям, которые знают один из них.

Что касается приоритета await future? , каков общий случай?

  • Функция, возвращающая ожидаемый Result<Future> .
  • Ожидаемое будущее, которое возвращает Result : Future<Result> .

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

await future? <=> (await future)?

В менее распространенном первом случае допустимы круглые скобки: await (future?) . Это могло бы даже быть хорошим применением для макроса try! если бы он не был объявлен устаревшим: await try!(future) . Таким образом, оператор await и вопросительный знак не находятся по разные стороны будущего.

Почему бы не использовать await качестве первого параметра функции async ?

async fn await_chain() -> Result<usize, Error> {
    let _ = partial_computation(await)
        .unwrap_or_else(or_recover)
        .run(await)?;
}

client.get("https://my_api")
    .send(await)?
    .json(await)?

let output = future
    .run(await);

Здесь future.run(await) - альтернатива await future .
Это может быть обычная функция async которая принимает future и просто запускает на нем макрос await!() .

C ++ (параллелизм TR)

auto result = co_await task;

Это в Coroutines TS, а не в параллелизме.

Другой вариант - использовать ключевое слово become вместо await :

async fn become_chain() -> Result<usize, Error> {
    let _ = partial_computation_future(become)
        .unwrap_or_else(or_recover_future)
        .start(become)?;
}

client.get("https://my_api")
    .send_future(become)?
    .json_future(become)?

let output = future.start(become);

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

Спасибо @EyeOfPython за [обзор]. Это особенно полезно для людей, которые только что присоединяются к навесу для велосипедов.

Лично я надеюсь, что мы держимся подальше от f await просто потому, что это очень нестандартный синтаксис и делает его особенным и волшебным. Это станет одной из тех вещей, в которых новички в Rust будут во многом запутываться, и я считаю, что даже ветеранам Rust это не добавляет такой ясности, чтобы того стоить.

@novacrazy проблема с этим аргументом в том, что любой макрос await! _would_ быть волшебным, он выполняет операцию, которая невозможна в написанном пользователем коде

@ Nemo157 Я согласен, что макрос await! был бы волшебным, но я бы сказал, что это не проблема. В std уже есть несколько таких макросов, например compile_error! и я не видел, чтобы кто-то на них жаловался. Я считаю нормальным использовать макрос, только понимая, что он делает, а не как он это делает.

Я согласен с предыдущими комментаторами, что postfix был бы наиболее эргономичным, но я бы предпочел начать с prefix-macro await!(expr) и, возможно, перейти на postfix-macro, как только это будет, вместо того, чтобы иметь expr.await (волшебное поле, встроенное в компилятор) или expr.await() (магический метод, встроенный в компилятор). Оба они представят совершенно новый синтаксис исключительно для этой функции, и IMO, который просто заставляет язык чувствовать себя непоследовательным.

@EyeOfPython Не хотите добавлять future(await) в свои списки и таблицу? Все положительные моменты вашей оценки future.await() кажется, передаются без слабости

Поскольку некоторые утверждали, что, несмотря на выделение синтаксиса, foo.await слишком похоже на доступ к полю, мы могли бы изменить токен . на # и вместо этого написать foo#await . Например:

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

Чтобы проиллюстрировать, как GitHub отображает это с выделением синтаксиса, давайте заменим await на match поскольку они имеют одинаковую длину:

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

Это кажется одновременно понятным и эргономичным.

Обоснование для # а не для какого-то другого токена, не является конкретным, но токен хорошо виден, что помогает.

Итак, другая концепция: если будущее было ссылкой :

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   *self.logger.log("beginning service call");
   let output = *service.exec(); // Actually wait for its result
   *self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = *acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = (*logger.log_into(message))?;
    *logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    *(*partial_computation()).unwrap_or_else(or_recover);
}

(*(*client.get("https://my_api").send())?.json())?

let output = *future;

Это было бы действительно некрасиво и непоследовательно. Позвольте хотя бы разобраться в этом синтаксисе:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   $self.logger.log("beginning service call");
   let output = $service.exec(); // Actually wait for its result
   $self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = ($logger.log_into(message))?;
    $logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    $($partial_computation()).unwrap_or_else(or_recover);
}

($($client.get("https://my_api").send())?.json())?

let output = $future;

Лучше, но все равно некрасиво (и еще хуже, это делает подсветку синтаксиса github). Однако, чтобы разобраться с этим, давайте введем возможность отложить префиксный оператор :

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.$log("beginning service call");
   let output = service.$exec(); // Actually wait for its result
   self.logger.$log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.$log_into(message)?;
    logger.$timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    ($partial_computation()).$unwrap_or_else(or_recover);
}

client.get("https://my_api").$send()?.$json()?

let output = $future;

Это именно то, что я хочу! Не только для ожидания ( $ ), но и для deref ( * ) и отрицания ( ! ).

Некоторые предостережения в этом синтаксисе:

  1. Приоритет операторов: для меня это очевидно, а для других пользователей?
  2. Взаимодействие макросов: вызовет ли здесь символ $ ?
  3. Несогласованность выражений: оператор ведущего префикса применяется ко всему выражению, а оператор отложенного префикса применяется только к выражению с разделителями . ( await_chain fn демонстрирует это), это сбивает с толку?
  4. Разбор и реализация: верен ли вообще этот синтаксис?

@Centril
IMO # слишком близок к синтаксису необработанного литерала r#"A String with "quotes""#

IMO # слишком близок к синтаксису необработанного литерала r#"A String with "quotes""#

Из контекста кажется вполне понятным, в чем разница в данном случае.

@Centril
Это так, но синтаксис также действительно чужд стилю, который Rust использует ИМХО. Он не похож ни на один существующий синтаксис с аналогичной функцией.

@Laaas Ни ? когда он был представлен. Я был бы счастлив использовать .await ; но другие кажутся недовольными этим, поэтому я пытаюсь найти что-то еще, что работает (т.е. четкое, эргономичное, достаточно простое для ввода, с возможностью цепочки, имеет хороший приоритет), и foo#await похоже, удовлетворяет всем этим.

? есть и другие положительные моменты, тогда как #await кажется довольно произвольным. В любом случае, если вам нужно что-то вроде .await , почему бы не .await! ?

@Centril Это было основным обоснованием future(await) , чтобы избежать доступа к полю, не добавляя никаких дополнительных операторов или внешнего синтаксиса.

в то время как #await кажется довольно произвольным.

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

почему не .await! ?

Это столь же произвольно, но я не возражаю против этого.

@Centril Это было основным обоснованием future(await) , чтобы избежать доступа к полю, не добавляя никаких дополнительных операторов или внешнего синтаксиса.

Вместо этого это выглядит как приложение-функция, в котором вы передаете await в future ; это кажется мне более запутанным, чем "доступ в поле".

Вместо этого это выглядит как приложение-функция, в котором вы передаете ожидание в будущее; это кажется мне более запутанным, чем "доступ в поле".

Для меня это было частью лаконичности. Await во многих аспектах похож на вызов функции (вызываемый выполняется между вами и вашим результатом), и передача ключевого слова этой функции дала понять, что это другой вид вызова. Но я понимаю, как может сбивать с толку то, что ключевое слово похоже на любой другой аргумент. (Минус подсветка синтаксиса и сообщения об ошибках компилятора, пытающиеся усилить это сообщение. Фьючерсы не могут быть вызваны иначе, как ожидание и наоборот).

@Centril

Будучи условность не отрицательная вещь, но имеющие сходство с существующим синтаксисом является положительным моментом. .await! не столь произвольный, поскольку это не совсем новый синтаксис; в конце концов, это всего лишь макрос после исправления с именем await .

Чтобы прояснить / добавить к вопросу о новом синтаксисе специально для await , я имею в виду введение нового синтаксиса, который отличается от существующего синтаксиса. Существует множество префиксных ключевых слов, некоторые из которых были добавлены после Rust 1.0 (например, union ), но, AFAIK не одно ключевое слово postfix (независимо от того, разделены ли они пробелом, точкой или чем-то еще) . Я также не могу вспомнить ни одного другого языка, в котором есть ключевые слова postfix.

ИМХО, единственный постфиксный синтаксис, который не сильно увеличивает странность Rust, - это макрос postfix, потому что он отражает вызовы методов, но может быть четко идентифицирован как макрос любым, кто раньше видел макрос Rust.

Еще мне понравился стандартный макрос await!() . Все просто и понятно.
Я предпочитаю, чтобы доступ в виде поля (или любой другой постфикс) сосуществовал как awaited .

  • Await чувствует, что вы будете ждать того, что будет дальше / правильно. Ждали, к чему пришли / уехали.
  • Согласован с методом cloned() на итераторах.

И еще мне понравился @? как ? -подобный сахар.
Это личное, но на самом деле я предпочитаю комбинацию &? , потому что @ имеет тенденцию быть слишком высоким и выходит за пределы линии, что очень отвлекает. & тоже высокий (хорошо), но не недотягивает (тоже хорошо). К сожалению, & уже имеет значение, хотя за ним всегда будет следовать ? .. Полагаю?

Например.

Lorem@?
  .ipsum@?
  .dolor()@?
  .sit()@?
  .amet@?
Lorem@?.ipsum@?.dolor()@?.sit()@?.amet@?

Lorem&?
  .ipsum&?
  .dolor()&?
  .sit()&?
  .amet&?
Lorem&?.ipsum&?.dolor()&?.sit()&?.amet&?

Для меня @ кажется раздутым персонажем, отвлекающим от чтения. С другой стороны, &? кажется приятным, не отвлекает (мое) чтение и имеет хорошее пространство между & и ? .

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

@Laaas Как бы мне ни хотелось, но постфиксных макросов в Rust пока нет, так что это новый синтаксис. Также обратите внимание, что foo.await!.bar и foo.await!().bar не совпадают с синтаксисом поверхности. В последнем случае будет фактический постфикс и встроенный макрос (что влечет за собой отказ от await в качестве ключевого слова).

@jplatte

Существует множество префиксных ключевых слов, некоторые из которых были добавлены после Rust 1.0 (например, union ),

union не является оператором унарного выражения, поэтому в данном сравнении он не имеет значения. В Rust есть ровно 3 стабильных унарных префиксных оператора ( return , break и continue ), и все они набираются как ! .

Хм ... С @ мое предыдущее предложение выглядит немного лучше:

client.get("https://my_api").@send()?.@json()?

let output = @future;

let foo = (@alpha())?
    .<strong i="8">@beta</strong>
    .@some_other_stuff()?
    .@even_more_stuff()
    .stuff_and_stuff();

@Centril Это моя точка зрения, поскольку у нас еще нет макросов для пост-исправлений, это должно быть просто await!() . Также я имел в виду .await!() FYI. Я не думаю, что await должно быть ключевым словом, хотя вы можете зарезервировать его, если сочтете это проблематичным.

union не является оператором унарного выражения, поэтому в данном сравнении он не имеет значения. В Rust есть ровно 3 стабильных унарных префиксных оператора ( return , break и continue ), и все они набираются как ! .

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

Что касается макроса префикса и возможности перехода к пост-исправлению: await должно оставаться ключевым словом, чтобы это работало. Тогда макрос будет каким-то специальным элементом lang, где разрешено использовать ключевое слово в качестве имени без лишнего r# , а r#await!() может, но, вероятно, не должен вызывать тот же макрос. В остальном это кажется наиболее прагматичным решением сделать его доступным.

То есть оставьте await в качестве ключевого слова, но сделайте await! разрешением макроса lang-item.

@HeroicKatora, почему он должен оставаться ключевым словом, чтобы оно работало?

@Laaas Потому что, если мы хотим сохранить возможность перехода, мы должны оставаться открытыми для будущего синтаксиса после исправления, который использует его в качестве ключевого слова. Следовательно, нам нужно сохранить резервирование ключевых слов, чтобы нам не потребовался перерыв в выпуске для перехода.

@Centril

Также обратите внимание, что foo.await! .Bar и foo.await! (). Bar - это не один и тот же поверхностный синтаксис. В последнем случае будет фактический постфикс и встроенный макрос (что влечет за собой отказ от ключевого слова await).

Не может ли это быть решено путем преобразования ключевого слова await сочетании с ! во внутренний макрос (который не может быть определен обычными средствами)? Тогда он остается ключевым словом, но преобразуется в макрос в синтаксисе макроса.

@HeroicKatora Почему await в x.await!() должно быть зарезервированным ключевым словом?

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

Это еще один раз, когда мы сталкиваемся с чем-то, что намного лучше работает как постфиксный оператор. Ярким примером этого является try! которому мы в конечном итоге присвоили собственный символ ? . Однако я думаю, что это не последний раз, когда постфиксный оператор более оптимален, и мы не можем дать всему свой особый характер. Поэтому я думаю, что нам не следует начинать с @ . Было бы намного лучше, если бы у нас был способ делать такие вещи. Вот почему я поддерживаю стиль постфиксных макросов .await!() .

let x = foo().try!();
let y = bar().await!();

Но для того, чтобы это имело смысл, необходимо было ввести сами постфиксные макросы. Поэтому я думаю, что было бы лучше начать с обычного синтаксиса макросов await!(foo) . Позже мы могли бы расширить это значение до foo.await!() или даже до foo@ если мы действительно чувствуем, что это достаточно важно, чтобы гарантировать наличие собственного символа.
То, что для этого макроса потребуется немного магии, не новость для std, и для меня это не большая проблема.
Как сказал @jplatte :

@ Nemo157 Я согласен с тем, что макрос await! был бы волшебным, но я бы сказал, что это не проблема. В std уже есть несколько таких макросов, например compile_error! и я не видел, чтобы кто-нибудь на них жаловался. Я считаю нормальным использовать макрос, только понимая, что он делает, а не как он это делает.

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

Если мы не хотим использовать await для цепочки и использовать await только для
присваивания, у нас могло бы получиться что-то вроде замены let на await:
await foo = future

Затем для связывания мы могли бы представить себе какую-нибудь операцию, например await res = fut1 -> fut2 или await res = fut1 >>= fut2 .

Отсутствует только один случай: ожидание и возврат результата, какой-то ярлык
за await res = fut; res .
Это можно легко сделать с помощью простого await fut

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

@HeroicKatora Я добавил fut(await) в список и оценил его согласно моему мнению.

Если кому-то кажется, что мои очки не выставлены, скажите, пожалуйста!

С точки зрения синтаксиса, я думаю, что .await!() чист и позволяет создавать цепочки, не добавляя никаких странностей в синтаксис.

Однако, если мы когда-нибудь получим «настоящие» постфиксные макросы, это будет немного странно, потому что предположительно .r#await!() может быть затенено, а .await!() нет.

Я категорически против варианта "ключевого слова постфикса" (разделенного пробелом вроде: foo() await?.bar() await? ), потому что я считаю, что await присоединяется к следующей части выражения, а не ту часть, на которую он действует. Я бы предпочел здесь практически любой символ, кроме пробелов, и даже предпочел бы синтаксис префикса этому, несмотря на его недостатки с длинными цепочками.

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

С точки зрения синтаксиса, я думаю, что .await!() чист и позволяет создавать цепочки, не добавляя никаких странностей в синтаксис.

Однако, если мы когда-нибудь получим «настоящие» постфиксные макросы, это будет немного странно, потому что предположительно .r#await!() может быть затенено, а .await!() нет.

Если мы получаем постфиксные макросы и используем .await!() тогда мы можем снять await в качестве ключевого слова и просто сделать его постфиксным макросом. Реализация этого макроса по-прежнему потребует немного магии курса, но это будет настоящий макрос, такой же, как compiler_error! сегодня.

@EyeOfPython Не могли бы вы подробно объяснить, какие изменения вы учитываете в forward-compatibility ? Я не уверен, каким образом fut@await будет оцениваться выше, чем fut.await!() а await { future } ниже, чем await!(future) . Столбец verbosity также кажется немного странным, некоторые выражения короткие, но имеют более низкий рейтинг, учитывает ли он связанные утверждения и т.д.

После прочтения этого обсуждения кажется, что многие люди хотят добавить нормальный макрос await!() и выяснить версию постфикса позже. Это предполагает, что нам действительно нужна версия постфикса, которую я буду считать верной для остальной части этого комментария.

Поэтому я просто хотел бы узнать мнение всех присутствующих: должны ли мы все согласиться с синтаксисом await!(future) СЕЙЧАС? Плюсы в том, что в этом синтаксисе нет двусмысленности при синтаксическом анализе, и это также макрос, поэтому не нужно изменять синтаксис языка для поддержки этого изменения. Минусы в том, что это будет выглядеть некрасиво для цепочки, но это не имеет значения, поскольку этот синтаксис можно легко заменить постфиксной версией автоматически.

Как только мы это уладим и внедрим в язык, мы сможем продолжить обсуждение синтаксиса postfix await с, надеюсь, более зрелым опытом, идеями и, возможно, другими функциями компилятора.

@HeroicKatora Я оценил его выше, так как он может освободить await для использования в качестве обычного идентификатора, естественно, и позволит добавлять другие постфиксные операторы, тогда как я думаю, что fut.await!() лучше, если await зарезервировано. Однако я не уверен, разумно ли это, также кажется определенно верным, что fut.await!() может быть выше.

Для await { future } vs await!(future) последний сохраняет возможность изменения почти любого из других вариантов, тогда как первый действительно допускает только любой из вариантов await future ( как указано в @withoutboats ). Так что я думаю, что это определенно должно быть fwd(await { future }) < fwd(await!(future)) .

Что касается многословности, то вы правы, после разделения дел на подгруппы я не переоценивал многословность, которая должна быть наиболее объективной из всех.

Отредактирую, чтобы учесть ваши комментарии, спасибо!

Стабилизация await!(future) - это наихудший вариант, который я могу себе представить:

  1. Это означает, что мы должны снять резерв await что также означает, что дизайн будущего языка станет сложнее.
  2. Он сознательно идет по тому же пути, что и try!(result) который мы устарели (и который требует написания r#try!(result) в Rust 2018).

    • Если мы знаем, что синтаксис await!(future) плохой, мы в конечном итоге намерены отказаться от него, это умышленно создает технический долг.

    • Более того, try!(..) определено в Rust, тогда как await!(future) не может быть и вместо этого будет магией компилятора.

    • Прекращение поддержки try!(..) было непростым делом и сказалось на обществе. Повторное испытание не кажется привлекательным.

  3. Это будет использовать синтаксис макросов для одной центральной и важной части языка; это кажется явно не первоклассным.
  4. await!(future) шумит ; в отличие от await future вам нужно написать !( ... ) .
  5. API-интерфейсы Rust, и особенно стандартная библиотека, сосредоточены вокруг синтаксиса вызова методов. Например, при работе с Iterator s обычным делом является объединение методов в цепочку. Подобно await { future } и await future , синтаксис await!(future) затруднит создание цепочки методов и вызовет временные привязки let . Это плохо для эргономики и, на мой взгляд, для читабельности.

@Centril Я хотел бы согласиться, но есть несколько открытых вопросов. Вы уверены, что насчет 1. ? Если мы сделаем его «волшебным», не могли бы мы сделать его еще более волшебным, позволив this ссылаться на макрос, не отбрасывая его как ключевое слово?

Я присоединился к Rust слишком поздно, чтобы оценить социальную перспективу 2. . Повторяя ошибку не звучит привлекательным , но в какой - то код еще не был преобразован из try! это должно быть очевидно , что это решение. Возникает вопрос, намерены ли мы иметь async/await в качестве стимула для перехода на редакцию 2018 или, скорее, проявить терпение и не повторять это.

Две другие (imho) центральные функции, очень похожие на 3. : vec![] и format! / println! . Первое в значительной степени связано с отсутствием стабильной коробочной конструкции afaik, второе - из-за конструкции строки формата и отсутствия зависимо типизированных выражений. Я думаю, что эти сравнения также частично переводят 4. в другую перспективу.

Я против любого синтаксиса, который не читается как английский. IE, "await x" читается как-то по-английски. «x # !! @! &» - нет. «x.await» соблазнительно читается как английский, но не будет, когда x - нетривиальная строка, такая как вызов функции-члена с длинными именами, или связка связанных методов итератора и т. д.

В частности, я поддерживаю ключевое слово x, где ключевое слово, вероятно, await . Я использую как сопрограммы C ++ TS, так и сопрограммы Unity C #, которые используют синтаксис, очень похожий на этот. И после многих лет использования их в производственном коде я считаю, что знание того, где находятся ваши точки доходности, с первого взгляда, является абсолютно важным . Когда вы просматриваете строку отступа в своей функции, вы можете выделить каждый co_await / yield return в 200-строчной функции за считанные секунды, без когнитивной нагрузки.

То же самое не относится к оператору точки с ожиданием после или некоторому другому синтаксису постфиксной "груды символов".

Я считаю, что await - это фундаментальная операция потока управления. К нему следует относиться с таким же уважением, как к 'if , while , match и return . Представьте, что если бы это были постфиксные операторы, чтение кода на Rust было бы кошмаром. Как и в случае с моим аргументом в пользу await, вы можете просмотреть строку отступа любой функции Rust и сразу же выделить весь поток управления. Есть исключения, но это исключения, и это не то, к чему мы должны стремиться.

Я согласен с @ejmahler. Не стоит забывать и о другой стороне разработки - ревью кода. Файл с исходным кодом гораздо чаще читается, чем записывается, поэтому я считаю, что его будет легче читать и понимать, чем писать. Поиск точек уступки действительно важен при проверке кода. И я лично проголосовал бы за Useful precedence .
Я считаю, что это:

...
let response = await client.get("https://my_api").send()?;
let body: MyResponse = await response.into_json()?;

легче понять, чем это:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

@HeroicKatora

@Centril Я хотел бы согласиться, но есть несколько открытых вопросов. Вы уверены, что насчет 1. ? Если мы сделаем его «волшебным», не могли бы мы сделать его еще более волшебным, позволив this ссылаться на макрос, не отбрасывая его как ключевое слово?

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

Возникает вопрос, намерены ли мы иметь async/await в качестве стимула для перехода на редакцию 2018 или, скорее, проявить терпение и не повторять это.

Я не знаю, говорили ли мы когда-нибудь, что намерены создать новую модульную систему, async / await и try { .. } в качестве стимулов; но независимо от наших намерений они таковы, и я думаю, что это хорошо. Мы хотим, чтобы люди в конечном итоге начали использовать новые языковые функции для написания лучших и более идиоматических библиотек.

Две другие (imho) основные функции, очень похожие на 3. : vec![] и format! / println! . Первые очень нравятся, потому что на аффайк нет стабильной коробчатой ​​конструкции,

Первый существует и записывается как vec![1, 2, 3, ..] , чтобы имитировать буквальные выражения массива, например, [1, 2, 3, ..] .

@ejmahler

«x.await» соблазнительно читается как английский, но не будет, когда x - нетривиальная строка, такая как вызов функции-члена с длинными именами, или связка связанных методов итератора и т. д.

Что не так с кучей связанных методов итератора? Это явный идиоматический Rust.
Инструмент rustfmt также отформатирует цепочки методов в разных строках, чтобы вы получили (снова используя match чтобы показать подсветку синтаксиса):

let foo = alpha().match?  // or `alpha() match?`, `alpha()#match?`, `alpha().match!()?`
    .beta
    .some_other_stuff().match?
    .even_more_stuff().match
    .stuff_and_stuff();

Если вы прочитаете .await как «затем ждите», оно читается отлично, по крайней мере, для меня.

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

Я не вижу, как постфикс await отрицает это, особенно в форматировании rustfmt выше. Кроме того, вы можете написать:

let foo = alpha().match?;
let bar = foo.beta.some_other_stuff().match?;
let baz = bar..even_more_stuff().match;
let quux = baz.stuff_and_stuff();

если вам это нравится.

в функции из 200 строк за считанные секунды без когнитивной нагрузки.

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

Я считаю, что await - это фундаментальная операция потока управления.

Так что ? . Фактически, await и ? являются эффективными операциями потока управления, которые говорят «извлекать значение из контекста». Другими словами, в локальном контексте вы можете представить эти операторы, имеющие тип await : impl Future<Output = T> -> T и ? : impl Try<Ok = T> -> T .

Есть исключения, но это исключения, и это не то, к чему мы должны стремиться.

И здесь исключение составляет ? ?

@andreytkachenko

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

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

легче понять, чем это:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

Это не то, как это будет отформатировано; пропустив его через rustfmt вы получите:

let body: MyResponse = client
    .get("https://my_api")
    .send()
    .match?
    .into_json()
    .match?;

@ejmahler @andreytkachenko Я согласен с @Centril здесь, самое большое изменение (некоторые могут сказать, что улучшение, я бы не стал), которое вы получаете от синтаксиса префикса, заключается в том, что у пользователей есть стимул разбивать свои утверждения на несколько строк, потому что все остальное не читается. Это не Rust-y, и обычные правила форматирования компенсируют это в синтаксисе post-fix. Я также считаю, что точка выхода более неясна в синтаксисе префикса, потому что await самом деле не помещается в кодовую точку, в которой вы уступаете, а не в противоположность этому.

Если вы пойдете по этому пути, давайте поэкспериментируем, чтобы прояснить это, лучше с await в качестве замены let в духе идеи @Keruspe , чтобы действительно обеспечить это. Без других расширений синтаксиса, потому что они кажутся натянутыми.

await? response = client.get("https://my_api").send();
await? body: MyResponse = response.into_json();

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

Хм ... желательно ли иметь как префиксную, так и суффиксную форму ожидания? Или просто суффиксная форма?

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

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

В сб, 19 января 2019 г., в 11:59 Маздак Фаррохзад [email protected]
написал:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril Я бы согласился, но есть
некоторые открытые вопросы. Вы уверены в 1.? Если мы сделаем это «волшебство»,
мы не делаем его еще более волшебным, позволяя ему ссылаться на макрос без
отбрасывая его как ключевое слово?

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

Это поднимает вопрос, собираемся ли мы использовать async / await как
стимул перейти на версию 2018 или, скорее, набраться терпения, а не
повтори это.

Я не знаю, говорили ли мы когда-нибудь, что планируем новую модульную систему async /
ждите и старайтесь {..} быть стимулами; но независимо от наших намерений
они есть, и я думаю, что это хорошо. Мы хотим, чтобы люди со временем
начать использовать новые языковые функции, чтобы писать лучше и идиоматичнее
библиотеки.

Две другие (imho) центральные функции, очень похожие на 3: vec! [] И format! /
println !. Первые очень нравятся, потому что нет стабильной коробочной
строительство афаик,

Первый существует и записывается как vec! [1, 2, 3, ..], чтобы имитировать массив
буквальные выражения, например [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" дразняще читается как английский, но это не так, когда x - это
нетривиальная строка, такая как вызов функции-члена с длинными именами или связка
связанных методов итератора и т. д.

Что не так с кучей связанных методов итератора? Это отчетливо
идиоматический Rust.
Инструмент rustfmt также форматирует цепочки методов в разных строках, чтобы вы
get (снова используя match, чтобы показать подсветку синтаксиса):

пусть foo = alpha (). соответствует? // или alpha() match? , alpha()#match? , alpha().match!()?
.бета
.some_other_stuff (). совпадение?
.even_more_stuff (). совпадение
.stuff_and_stuff ();

Если вы прочитаете .await как «тогда ждите», он читается отлично, по крайней мере, для меня.

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

Я не понимаю, как postfix await отрицает это, особенно в rustfmt
форматирование выше. Кроме того, вы можете написать:

let foo = alpha (). match?; let bar = foo.beta.some_other_stuff (). match?; let baz = bar..even_more_stuff (). match; let quux = baz.stuff_and_stuff ();

если вам это нравится.

в функции из 200 строк за считанные секунды без когнитивной нагрузки.

Не зная слишком много о конкретной функции, мне кажется
что 200 LOC, вероятно, нарушают принцип единой ответственности и
слишком. Решение состоит в том, чтобы сделать меньше и разделить. Фактически, я
думаю, что это самое главное для удобства сопровождения и удобочитаемости.

Я считаю, что ожидание - это фундаментальная операция потока управления.

Так что? На самом деле ждать и? обе эффективные операции потока управления
которые говорят «извлечь ценность из контекста». Другими словами, в местном
контекст, вы можете представить, что эти операторы имеют тип await: impl
Будущее-> Т и? : impl Попробуйте-> Т.

Есть исключения, но это исключения, и мы не должны
стремиться к.

И исключение здесь есть? ?

@andreytkachenko https://github.com/andreytkachenko

Я согласен с @ejmahler https://github.com/ejmahler . Мы не должны
Забудьте о другой стороне разработки - ревью кода. Файл с исходным кодом
гораздо чаще читают, чем пишут, поэтому я думаю, что будет легче
читать и понимать потом писать.

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

легче понять, чем это:

... let body: MyResponse = client.get ("https: // my_api") .send (). await? .into_json (). await ?;

Это не то, как это будет отформатировано; идиоматическое форматирование rustfmt
является:

let body: MyResponse = client
.get ("https: // my_api")
.Отправить()
.соответствие?
.into_json ()
.соответствие?;

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@ejmahler Мы не согласны. "прячется"; те же аргументы были сделаны и в отношении.

Оператор ? очень короткий, и в прошлом его критиковали за «скрытие» возврата. В большинстве случаев код читается, а не записывается. Если переименовать его в ?! станет в два раза длиннее, и поэтому его будет труднее не заметить.

Тем не менее, мы в конечном итоге стабилизировали ? и с тех пор я думаю, что пророчество не сбылось.
Для меня постфикс await следует естественному порядку чтения (по крайней мере, для говорящих слева направо). В частности, он следует порядку потока данных.

Не говоря уже о подсветке синтаксиса: все, что связано с ожиданием, можно выделить ярким цветом, чтобы их можно было сразу найти. Таким образом, даже если бы у нас был символ вместо фактического слова await , он все равно был бы очень удобочитаем и доступен для поиска в выделенном синтаксисе коде. С учетом сказанного, я по-прежнему предпочитаю использовать слово await только из соображений grepping - проще найти код для всего, что ожидается, если мы используем только слово await вместо символа например @ или #, значение которых зависит от грамматики.

вы все это не ракетостроение

let body: MyResponse = client.get("https://my_api").send()...?.into_json()...?;

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

(извинения перед @solson)

@ ben0x539 Означает ли это, что я могу получить доступ к члену моего результата, например future()....start ? Или дождаться результата диапазона, например range()..... ? И как именно вы имеете в виду no precedence/macro shenanigans necessary and поскольку в настоящее время многоточие .. требует наличия бинарного оператора или парантезы справа, и это очень близко с первого взгляда.

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

В сб, 19 января 2019 г., в 13:51 Бенджамин Херр [email protected]
написал:

вы все это не ракетостроение

let body: MyResponse = client.get ("https: // my_api") .send () ...?. into_json () ...?;

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

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora Это выглядит немного искусственно, но да, конечно. Я имел в виду, что, поскольку это постфиксная операция, как и другие предложенные постфиксные решения, она позволяет избежать противоречащего интуиции приоритета для await x? , и это не макрос.

@ejmahler

Да? Оператор существует. Я уже признал, что были исключения. Но это исключение.

Есть две формы выражения, которые подходят keyword expr , а именно return expr и break expr . Первое встречается чаще, чем второе. Форма continue 'label самом деле не считается, поскольку, хотя это выражение, оно не имеет формы keyword expr . Итак, теперь у вас есть 2 формы унарного выражения с целыми префиксными ключевыми словами и 1 форма унарного выражения постфиксного типа. Прежде чем мы даже примем во внимание, что ? и await более похожи, чем await и return , я бы вряд ли назвал return/break expr правило для ? должно быть исключением.

Подавляющее большинство потока управления в любой программе на Rust происходит через префиксные ключевые слова.

Как упоминалось выше, break expr не так уж часто встречается ( break; более типичен, а return; более типичен, и они не являются унарными формами выражений). Остается только ранний return expr; s, и мне совсем не ясно, что это гораздо чаще, чем match , ? , просто вложенные if let s и else s, а также for циклы. Как только try { .. } стабилизируется, я ожидаю, что ? будет использоваться еще больше.

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

Мне очень нравится идея нововведения с постфиксным синтаксисом. Это имеет больше смысла с потоком, и я помню, насколько улучшился код, когда мы перешли от префикса try! к постфиксу ? . Я думаю, что в сообществе Rust есть много людей, которые на собственном опыте убедились, сколько улучшений было внесено в код.

Если нам не нравится идея .await я уверен, что можно проявить немного творчества, чтобы найти настоящий постфиксный оператор. Одним из примеров может быть просто использование ++ или @ для ожидания.

:( Я просто не хочу больше ждать.

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

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

Обычный синтаксис макросов похож на яблочный пирог, это второй любимый вариант для всех, но в результате - любимый вариант для всей семьи [0]. Главное, хотелось бы попробовать! мы всегда можем изменить это позже. Но самое главное, чем раньше мы все согласимся, тем скорее мы все начнем использовать его и работать продуктивно!

[0] (упоминается в первую минуту) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

совпадение, if, if let, while, while let и for - все это всеобъемлющий поток управления
которые используют префиксы. Притворяться, что перерыв и продолжение - единственный поток управления
ключевые слова вводят в заблуждение.

В сб, 19 января 2019 г., в 15:37 Язад Дарувала [email protected]
написал:

:( Я просто не хочу больше ждать.

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

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

Обычный синтаксис макросов похож на яблочный пирог, он всем по душе
любимый вариант, но в результате - любимый вариант семьи [0].
Главное, хотелось бы попробовать! мы всегда можем изменить это позже. Но большинство
важно то, что чем раньше мы все договоримся, тем скорее мы все начнем
фактически используя его и будьте продуктивны!

[0] (упоминается в первую минуту)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mituhiko Согласен! Postfix кажется более простоватым из-за цепочки. Я думаю, что предложенный мной синтаксис fut@await - еще один интересный вариант, у которого, похоже, не так много недостатков, как у других предложений. Я не уверен, что это слишком далеко и более практичная версия будет предпочтительнее.

@ejmahler

match, if, if let, while, while let и for - все это всеобъемлющий поток управления, использующий префиксы. Представление, что break и continue - единственные ключевые слова потока управления, разочаровывающе вводит в заблуждение.

Это совсем не вводит в заблуждение. Соответствующая грамматика для этих конструкций примерно такая:

Expr = kind:ExprKind;
ExprKind =
  | If:{ "if" cond:Cond then:Block { "else" else_expr:ElseExpr }? };
  | Match:{ "match" expr:Expr "{" arms:MatchArm* "}" }
  | While:{ { label:LIFETIME ":" }? "while" cond:Cond body:Block }
  | For:{ { label:LIFETIME ":" }? "for" pat:Pat "in" expr:Expr body:Block }
  ;

Cond =
  | Bool:Expr
  | Let:{ "let" pat:Pat "=" expr:Expr }
  ;

ElseExpr =
  | Block:Block
  | If:If
  ;

MatchArm = pats:Pat+ % "|" { "if" guard:Expr }? "=>" body:Expr ","?;

Здесь формы: if/while expr block , for pat in expr block и match expr { pat0 => expr0, .., patn => exprn } . Во всех этих формах есть ключевое слово, которое предшествует следующему. Я думаю, это то, что вы имеете в виду под «использует префиксы». Однако это все блочные формы, а не унарные префиксные операторы. Таким образом, сравнение с await expr вводит в заблуждение, поскольку нет согласованности или правила, о котором можно было бы говорить. Если вы стремитесь к согласованности с блочными формами, сравните это с await block , а не с await expr .

@mituhiko Согласен! Postfix кажется более простоватым из-за цепочки.

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

Я не знаю. Похоже, было бы здорово иметь оба:

await foo.bar();
foo.bar().await;

Некоторое время я использовал Scala, и мне тоже очень нравилось, что многие вещи работают подобным образом. Особенно неплохо было бы иметь match и if в позициях постфикса в Rust.

foo.bar().await.match {
   Bar1(x, y) => {x==y},
   Bar2(y) => {y==7},
}.if {
   bazinga();
}

Резюме на данный момент

Матрицы опций:

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

| Имя | Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
| --- | --- | --- | --- |
| ПРЕФИКС | - | - | - |
| Макрос ключевого слова | await!(fut) | await!(fut)? | await!(fut?) |
| Функция ключевого слова | await(fut) | await(fut)? | await(fut?) |
| Полезный приоритет | await fut | await fut? | await (fut?) |
| Очевидный приоритет | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn с ключевым словом | fut(await) | fut(await)? | fut?(await) |
| Поле ключевого слова | fut.await | fut.await? | fut?.await |
| Метод ключевого слова | fut.await() | fut.await()? | fut?.await() |
| Макрос ключевого слова Postfix | fut.await!() | fut.await!()? | fut?.await!() |
| Ключевое слово Space | fut await | fut await? | fut? await |
| Ключевое слово Sigil | fut@await | fut@await? | fut?@await |
| Сигил | fut@ | fut@? | fut?@ |

Сигил "Sigil Keyword" _cannot_ быть # , поскольку тогда вы не могли бы сделать это с будущим, называемым r . ... в качестве сигилы не пришлось бы менять токенизацию, как я беспокоился в первую очередь .

Более реальное использование (напишите мне другие _real_ варианты использования с несколькими await на urlo, и я добавлю их):

| Имя | (reqwest) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json | > ? |
| --- | --- |
| ПРЕФИКС | - |
| Макрос ключевого слова | await!(client.get("url").send())?.json()? |
| Функция ключевого слова | await(client.get("url").send())?.json()? |
| Полезный приоритет | (await client.get("url").send()?).json()? |
| Очевидный приоритет | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn с ключевым словом | client.get("url").send()(await)?.json()? |
| Поле ключевого слова | client.get("url").send().await?.json()? |
| Метод ключевого слова | client.get("url").send().await()?.json()? |
| Макрос ключевого слова Postfix | client.get("url").send().await!()?.json()? |
| Ключевое слово Space | client.get("url").send() await?.json()? |
| Ключевое слово Sigil | client.get("url").send()@await?.json()? |
| Сигил | client.get("url").send()@?.json()? |

РЕДАКТИРОВАТЬ ПРИМЕЧАНИЕ: Мне было указано, что для Response::json может иметь смысл также возвращать Future , где send ожидает исходящего ввода-вывода и json (или другая интерпретация результата) ожидает входящего ввода-вывода. Я собираюсь оставить этот пример как есть, так как считаю полезным показать, что проблема цепочки применяется даже с одной точкой ожидания ввода-вывода в выражении.

Похоже, что существует приблизительный консенсус, что из вариантов префикса очевидный приоритет (вместе с await? сахаром) является наиболее желательным. Однако многие люди высказались в пользу постфиксного решения, чтобы упростить связывание, как указано выше. Хотя по поводу выбора префикса существует приблизительный консенсус, похоже, нет единого мнения о том, какое решение для постфикса лучше всего. Все предложенные варианты легко вводят в заблуждение (смягчается выделением ключевых слов):

  • Fn с ключевым словом => вызов fn с аргументом await
  • Поле ключевого слова => доступ к полю
  • Метод ключевого слова => вызов метода
  • Макрос (префикс или постфикс) => await ключевое слово или нет?
  • Ключевое слово пробела => разбивает группировку на одну строку (лучше на несколько строк?)
  • Sigil => добавляет новые сигилы к языку, который уже воспринимается как тяжелый

Другие более радикальные предложения:

  • Разрешить как префикс (очевидный приоритет), так и постфиксное «поле» (это может быть применено к большему количеству ключевых слов, таких как match , if и т. Д. В будущем, чтобы сделать это обобщенным шаблоном, но это не обязательно дополнение к этому обсуждению) [[ссылка] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await в шаблонах для разрешения фьючерсов (без цепочки) [[ссылка] (https://github.com/rust-lang/rust/issues/57640)]
  • Используйте префиксный оператор, но разрешите его отложить [[ссылка] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

Стабилизация с помощью макроса ключевого слова await!(fut) конечно, совместима в будущем практически со всем вышеперечисленным, хотя для этого требуется, чтобы макрос использовал ключевое слово вместо обычного идентификатора.

Если у кого-то есть пример, в основном реальный, в котором в одной цепочке используются два await , я бы хотел его увидеть; пока никто не поделился ни одной. Однако постфикс await также полезен, даже если вам не нужно await более одного раза в цепочке, как показано в примере reqwest.

Если я пропустил что-то примечательное над этим сводным комментарием, напишите мне в личку на urlo, и я постараюсь добавить его. (Хотя я потребую, чтобы он добавлял чужие комментарии, чтобы избежать громкого фаворитизма).

Лично я исторически был за ключевое слово prefix с очевидным приоритетом. Я все еще думаю, что стабилизация с помощью макроса ключевого слова await!(fut) была бы полезна для сбора реальной информации о том, где происходит ожидание в реальных случаях использования, и все же позволила бы нам позже добавить немакроспективный префикс или вариант постфикса .

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

client
    .get("url")
    .send() await?
    .json()?

но в одной строке он делает неудобный разрыв, который плохо группирует выражение: client.get("url").send() await?.json()? . Однако с полем ключевого слова он хорошо выглядит в обеих формах: client.get("url").send().await?.json()?

client
    .get("url")
    .send()
    .await?
    .json()?

хотя я полагаю, что «метод ключевых слов» будет лучше, поскольку это действие. Мы _ могли бы_ даже сделать его "настоящим" методом на Future если бы захотели:

trait Future<..> {
    ..
    extern "rust-await" fn r#await(self) -> _;
}

( extern "rust-await" , конечно, подразумевает всю магию, необходимую для фактического выполнения ожидания, и на самом деле это не будет настоящая fn, это будет в основном просто потому, что синтаксис выглядит как метод, если ключевое слово используется метод.)

Разрешить как префикс (очевидный приоритет), так и постфиксное «поле» ...

Если выбран какой-либо постфиксный синтаксис (независимо от того, вместе он или вместо префиксного), это определенно будет аргументом для будущего обсуждения: теперь у нас есть ключевые слова, которые работают как в префиксной, так и в постфиксной нотации, именно потому, что иногда одно предпочтительнее, чем другой, так что, возможно, мы могли бы просто разрешить и то, и другое там, где это имеет смысл, и увеличить гибкость синтаксиса, унифицируя правила. Может быть, это плохая идея, может быть, она будет отклонена, но это определенно обсуждение, которое состоится в будущем, если для await используется запись postix.

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

: +1:


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

† Если вы используете что-то, что не выделяет ключевые слова


@ejmahler

Я против любого синтаксиса, который не похож на английский

Что-то вроде request.get().await читается так же хорошо, как body.lines().collect() . В «кучке связанных методов итератора», я думаю, _prefix_ на самом деле читается хуже, так как вы должны помнить, что они сказали «подождите» еще в самом начале, и никогда не знать, когда вы что-то услышите, будет ли это то, что вы Ожидание, вроде приговора в

И после многих лет использования их в производственном коде я считаю, что знание того, где находятся ваши точки доходности, с первого взгляда, является абсолютно важным. Когда вы просматриваете строку отступа в своей функции, вы можете выбрать каждый возврат co_await / yield в функции из 200 строк за считанные секунды без когнитивной нагрузки.

Это означает, что внутри выражения никогда не должно быть ничего, что является ограничением, которое я категорически не поддерживал, учитывая характер Rust, ориентированный на выражения. И, по крайней мере, с await C # абсолютно правдоподобно иметь CallSomething(argument, await whatever.Foo() .

Учитывая, что async _ появится в середине выражений, я не понимаю, почему в префиксе легче увидеть, чем в постфиксе.

К нему следует относиться с таким же уважением, как и к «если, пока, совпадение и возврат». Представьте, что если бы это были постфиксные операторы, чтение кода на Rust было бы кошмаром.

returncontinue и break ) и while примечательны как _полностью_ бесполезны для цепочки, поскольку они всегда возвращают ! и () . И хотя по какой-то причине вы пропустили for , мы видели код, написанный отлично с использованием .for_each() без плохих эффектов, особенно в районе .

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

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

Мы также должны понимать, что что касается конструкций потока управления, await - это совсем другой вид животных. Такие конструкции, как return , break , continue и даже yield можно интуитивно понять в терминах jmp . Когда мы видим их, наши глаза бегают по экрану, потому что поток управления, о котором мы заботимся, перемещается в другое место. Однако, хотя await влияет на поток управления машины, он не перемещает поток управления, который важен для наших глаз и для нашего интуитивного понимания кода.

У нас нет соблазна связывать безусловные вызовы с return или break потому что это не имеет смысла. По тем же причинам у нас нет соблазна изменить наши правила приоритета, чтобы приспособить их. У этих операторов низкий приоритет. Они берут все вправо и куда-то возвращают, заканчивая выполнение в этой функции или блоке. Однако оператор await хочет быть привязанным. Это неотъемлемая часть выражения, а не его конец.

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

Кандидат в преследующую лошадь, похоже, сейчас использует await!(expr) и надеется, что позже что-то получше. До того, как прочитать примечания @Centril , я, вероятно, поддержал бы это в интересах реализации этой важной функции практически с любым синтаксисом. Однако его аргументы убеждают меня, что это просто поможет. Мы знаем, что цепочка вызовов методов важна в Rust. Это привело к принятию ключевого слова ? , которое широко популярно и пользуется огромным успехом. Использование синтаксиса, который, как мы знаем, нас разочарует, на самом деле просто добавляет технический долг.

В начале этой беседы @withoutboats указал, что только четыре существующих варианта кажутся жизнеспособными. Из них только постфиксный синтаксис expr await вероятно, сделает нас счастливыми в долгосрочной перспективе. Этот синтаксис не создает странных сюрпризов с приоритетом. Это не заставляет нас создавать префиксную версию оператора ? . Он прекрасно работает с цепочкой методов и не разбивает поток управления слева направо. Наш успешный оператор ? служит прецедентом для оператора постфикса, а await ? на практике больше похож на return , break или yield . Хотя постфиксный несимвольный оператор может быть новым в Rust, использование async/await будет достаточно широко распространенным, чтобы он быстро стал привычным.

Хотя все варианты постфиксного синтаксиса кажутся работоспособными, expr await имеет некоторые преимущества. Этот синтаксис проясняет, что await - ключевое слово, которое помогает выделить магический поток управления. По сравнению с expr.await , expr.await() expr.await! , expr.await!() и т. Д., Здесь не нужно объяснять, что это похоже на поле / метод / макрос, но на самом деле не в этом особом случае. Мы бы все привыкли здесь к разделителю пробелов.

Заманчиво написание await как @ или использование другого символа, который не вызывает проблем с синтаксическим анализом. Это, безусловно, достаточно важный оператор, чтобы это гарантировать. Но если, в конце концов, это будет написано await , это будет нормально. Пока он в позиции postfix.

Как кто-то упомянул _реальные_ примеры ... Я поддерживаю (согласно tokei) кодовую базу ржавчины из 23 858 строк, которая очень сильно асинхронна и использует фьючерсы 0.1 await (я знаю, что это экспериментально). Пойдем (отредактировано) Spelunking (обратите внимание, что все было пропущено через rustfmt):

// A
if !await!(db.is_trusted_identity(recipient.clone(), message.key.clone()))? {
    info!("recipient: {}", recipient);
}

// B
match await!(db.load(message.key))? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await!(client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())?
.error_for_status()?;

// D
let mut res =
    await!(client.get(inbox_url).headers(inbox_headers).send())?.error_for_status()?;

let mut res: InboxResponse = await!(res.json())?;

// E
let mut res = await!(client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())?
.error_for_status()?;

let res: Response = await!(res.json())?;

// F
#[async]
fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await!(self.request(url, Method::GET, None, true))?;
    let user = await!(res.json::<UserResponse>())?
        .user
        .into();

    Ok(user)
}

Теперь давайте превратим это в самый популярный вариант префикса, очевидный приоритет с сахаром. По очевидным причинам это не было запущено через rustfmt, поэтому извиняюсь, если есть лучший способ написать это.

// A
if await? db.is_trusted_identity(recipient.clone(), message.key.clone()) {
    info!("recipient: {}", recipient);
}

// B
match await? db.load(message.key) {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = (await? client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())
.error_for_status()?;

// D
let mut res =
    (await? client.get(inbox_url).headers(inbox_headers).send()).error_for_status()?;

let mut res: InboxResponse = await? res.json();

// E
let mut res = (await? client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())
.error_for_status()?;

let res: Response = await? res.json();

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await? self.request(url, Method::GET, None, true);
    let user = (await? res.json::<UserResponse>())
        .user
        .into();

    Ok(user)
}

Наконец, давайте превратим это в мой любимый вариант постфикса, «поле постфикса».

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()).await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key).await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send().await?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send().await?
    .error_for_status()?
    .json().await?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json().await?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true).await?
        .res.json::<UserResponse>().await?
        .user
        .into();

    Ok(user)
}

После этого упражнения я обнаружил несколько вещей.

  • Я категорически против await? foo . Он хорошо читается для простых выражений, но ? теряется в сложных выражениях. Если мы должны сделать префикс, я бы предпочел иметь «полезный» приоритет.

  • Использование постфиксной нотации заставляет меня объединять операторы и сокращать ненужные привязки let.

  • Использование постфиксной записи _field_ приводит меня к тому, что я решительно предпочитаю, чтобы .await? появлялся в строке объекта await, а не в отдельной строке на языке rustfmt.

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

let resultValue = doSomethingAndReturnResult()...?;
let resultValue = doSomethingAndReturnResult()...?.doSomethingOnResult()...?;
let value = doSomethingAndReturnValue()....doSomethingOnValue()...;
let arrayOfValues = vec![doSomethingA(),doSomethingB()]...?;
// Showing stacking
let value = doSomethingWithVeryLongFunctionName()...?
                 .doSomethingWithResult()...?;

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

А

let mut res: Response = (await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json())?;

B

let mut res: Response = await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json());
let res = res.unwrap();

Следует ли считать хорошим тоном длинные цепочки ожиданий?

Почему бы просто не использовать обычные комбинаторы Future ?

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

Лично я думаю так:

let value = await some_op()
                 .and_then(|v| v.another_op())
                 .and_then(|v2| v2.final_op())
                 .or_else(|| backup_op());

value.unwrap()

читается гораздо естественнее, чем это:

let value = match await some_op() {
    Ok(v) => match await v.another_op() {
        Ok(v2) => await v2.final_op(),
        Err(_) => await backup_op(),
    },
    Err(_) => await backup_op(),
};

value.unwrap()

В конце концов, у нас все еще есть вся мощь фьючерсов с нулевой стоимостью.

Считают, что.

@EyeOfPython Я хотел бы подчеркнуть, что у нас есть другой выбор, кроме @ в future@await . Мы можем написать future~await , где ~ работает как полудефис и будет работать для любых возможных постфиксных операторов.

Дефис - уже использовался как оператор минус и отрицательный оператор. Уже не хорошо. Но ~ использовался для обозначения объектов кучи в Rust, а в остальном он почти не использовался на каких-либо языках программирования. Это должно меньше запутать людей, говорящих на других языках.

@earthengine Хорошая идея, но, возможно, просто используйте future~ что означает ожидание будущего, где ~ работает как ключевое слово await . (например, символ ?

Будущее | Будущее результата | Результат будущего
- | - | -
будущее ~ | будущее ~? | будущее? ~

Также связанные фьючерсы, такие как:

let res: MyResponse = client.get("https://my_api").send()~?.json()~?;

Я посмотрел, как Go реализует асинхронное программирование, и нашел там кое-что интересное. Ближайшей альтернативой фьючерсам в Go являются каналы. И вместо await или другого кричащего синтаксиса для ожидания значений каналы Go просто предоставляют для этой цели оператор <- . Для меня это выглядит довольно чисто и просто. Раньше я часто видел, как люди хвалят Go за его простой синтаксис и хорошие асинхронные возможности, так что определенно неплохо было бы чему-нибудь научиться на его опыте.

К сожалению, у нас не могло быть точно такого же синтаксиса, потому что в исходном коде Rust намного больше угловых скобок, чем в Go, в основном из-за дженериков. Это делает оператор <- очень тонким и неприятным в работе. Другой недостаток заключается в том, что его можно рассматривать как противоположность -> в сигнатуре функции, и нет никаких оснований считать это так. И еще одним недостатком является то, что сигил <- должен был быть реализован как placement new , чтобы люди могли его неправильно интерпретировать.

Итак, после некоторых экспериментов с синтаксисом я остановился на <-- sigil:

let output = <-- future;

В async context <-- довольно прост, хотя и меньше <- . Но вместо этого он дает большое преимущество перед <- а также перед префиксом await - он хорошо работает с отступами.

async fn log_service(&self) -> T {
   let service = self.myService.foo();
   <-- self.logger.log("beginning service call");
   let output = <-- service.exec();
   <-- self.logger.log("foo executed with result {}.", output));
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = <-- acquire_lock();
    let length = <-- logger.log_into(message)?;
    <-- logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    <-- (<-- partial_computation()).unwrap_or_else(or_recover);
}

Для меня этот оператор выглядит даже более уникальным и более заметным, чем await (который больше похож на любое другое ключевое слово или переменную в контексте кода). К сожалению, на Github он выглядит тоньше, чем в моем редакторе кода, но я думаю, что это не фатально, и мы можем с этим жить. Даже если кто-то чувствует себя некомфортно, другая подсветка синтаксиса или лучший шрифт (особенно с лигатурами) решат все проблемы.

Еще одна причина, по которой такой синтаксис нравится, состоит в том, что концептуально его можно выразить как «что-то, чего еще нет». Направление стрелки справа налево противоположно направлению, в котором мы читаем текст, что позволяет нам описать его как «вещь, пришедшую из будущего». Длинная форма оператора <-- также предполагает, что «начинается некая длительная операция». Угловая скобка и два дефиса, направленные от future могут символизировать «непрерывный опрос». И мы все еще можем читать это как «ожидание», как это было раньше.
Не какое-то просветление, но может быть весело.


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

До конца, я не уверен, уместно ли здесь отложенное слово, может, нам стоит назвать его по-другому.

client.get("https://my_api").<--send()?.<--json()?

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

Приоритет операторов выглядит довольно очевидным: слева направо.

Я надеюсь, что этот синтаксис уменьшит потребность в написании функций is_not_* цель которых только отрицать и возвращать свойство bool . И логические выражения / выражения разыменования в некоторых случаях будут более понятными при его использовании.


Наконец, я применил его к примерам из реального мира, опубликованным @mehcode, и мне нравится, как <-- делает правильный акцент на функции async внутри цепочек вызовов методов. Напротив, постфикс await просто выглядит как обычный доступ к полю или вызов функции (в зависимости от синтаксиса), и их почти невозможно различить без специальной подсветки или форматирования синтаксиса.

// A
if db.<--is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.<--load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .<--send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .<--send()?
    .error_for_status()?
    .<--json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .<--send()?
    .error_for_status()?
    .<--json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.<--request(url, Method::GET, None, true)?
        .res.<--json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

В конце концов: это тот синтаксис, который я хочу использовать.

@novacrazy

Следует ли считать хорошим тоном длинные цепочки ожиданий? Почему бы просто не использовать обычные комбинаторы Future?

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

let value = some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())
    .or_else(|| backup_op())
    .await;

Но для того, чтобы это сработало, предложение and_then нужно набирать как FnOnce(T) -> impl Future<_> , а не только как FnOnce(T) -> U . Выполнение цепочечных комбинаторов для будущего и результата работает чисто без парантезов в постфиксе:

let result = load_local_file()
    .or_else(|_| request_from_server()) // Async combinator
    .await
    .and_then(|body| serde_json::from_str(&body)); // Sync combinator

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

  • Вызов метода ( future.await() )
  • Выражение поля ( future.await )
  • Вызов функции ( future(await) )

Различия в функциональности небольшие, но они есть. Обратите внимание, что я бы принял все это, в основном это тонкая настройка. Чтобы показать их все, нам понадобится несколько типов. Не комментируйте, пожалуйста, надуманность примера, это наиболее сжатая версия, которая показывает сразу все различия.

struct Foo<A, F, S> where A: Future<Output=F>, F: FnOnce(usize) -> S {
    member: A,
}

// What we want to do, in macro syntax:
let foo: Foo<_, _, _> = …;
(await!(foo.member))(42)
  • Вызов метода: foo.member.await()(42)
    Связывается наиболее сильно, поэтому парантезов вообще нет
  • Участник: (foo.member.await)(42)
    Если это вызываемый объект, требуется парантез вокруг ожидаемого результата, это соответствует наличию вызываемого объекта в качестве члена, в противном случае путаница с вызовом функции-члена. Это также предполагает, что можно деструктурировать с помощью шаблонов: let … { await: value } = foo.member; value(42) как-нибудь?
  • Вызов функции: (foo.member)(await)(42)
    Требуется парантез для деструктуризации (мы перемещаем член), поскольку он ведет себя как вызов функции.

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

Лучшей параллелью к вызову метода должен быть просто вызов другого метода с self .

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

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

@HeroicKatora Мы могли бы использовать специальный случай .await в libsyntax, чтобы разрешить foo.await(42) но это было бы непоследовательно / специально. Однако (foo.await)(42) кажется пригодным для использования, поскольку, хотя фьючерсы, выводящие замыкания, существуют, они, вероятно, не так уж и распространены. Таким образом, если мы оптимизируем для общего случая, без необходимости добавлять () к .await вероятно, выигрывает по балансу.

@Centril Я согласен, последовательность важна. Когда я вижу пару похожих моделей поведения, я хочу вывести другие с помощью синтеза. Но здесь .await кажется неудобным, особенно с приведенным выше примером, ясно показывающим, что он параллелен неявному деструктурированию (если вы не можете найти другой синтаксис, где возникают эти эффекты?). Когда я вижу деструктуризацию, я сразу же задаюсь вопросом, могу ли я использовать это с let-bindings и т. Д. Это, однако, было бы невозможно, потому что мы деструктурировали бы исходный тип, у которого нет такого члена, или особенно когда тип просто impl Future<Output=F> (В основном несущественное примечание: выполнение этой работы вернет нас к альтернативному префиксу await _ = вместо let _ = , забавно).

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


¹ Это может быть согласовано, позволяя ? , разрешая ? за именами в шаблоне

  • await value? = failing_future();

чтобы соответствовать части Ok в Result . Это кажется интересным для изучения в других контекстах, но, скорее, не по теме. Это также приведет к совпадению синтаксиса префикса и суффикса для await одновременно.

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

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

В случае, если пользователь пишет foo.await(42) ...

struct HasClosure<F: FnOnce(u8)> { closure: F, }
fn _foo() {
    let foo: HasClosure<_> = HasClosure { closure: |x| {} };

    foo.closure(42);
}

... мы уже предоставляем хорошую диагностику:

5 |     foo.closure(42);
  |         ^^^^^^^ field, not a method
  |
  = help: use `(foo.closure)(...)` if you meant to call the function stored in the
          `closure` field

Подстроить это под foo.await(42) кажется вполне достижимым. На самом деле, насколько я понимаю, мы знаем, что пользователь имеет в виду (foo.await)(42) когда написано foo.await(42) так что это может быть cargo fix ed в MachineApplicable манера. В самом деле, если мы стабилизируем foo.await но не разрешаем foo.await(42) я считаю, что мы можем даже изменить приоритет позже, если нам это нужно, поскольку foo.await(42) сначала не будет законным.

Дальнейшие вложения будут работать (например, будущее результата закрытия - не то, чтобы это было обычным явлением):
ржавчина
struct HasClosure fn _foo () -> Результат <(), ()> {
let foo: HasClosure <_ i = "27"> = HasClosure {closure: Ok (| x | {})};

foo.closure?(42);

Ok(())

}

например, будущее результата закрытия - не то, чтобы это было обычным явлением

Дополнительный суффикс ? operator делает это однозначным без изменений синтаксиса - в любом из примеров после исправления. Настройка не требуется . Проблемы заключаются только в том, что .member явно является полем, и в первую очередь необходимо применить деструктуризацию перемещения. И я действительно не хочу сказать, что это было бы сложно написать. Я в основном хочу сказать, что это кажется несовместимым с другими использованиями .member которые, например, могут быть преобразованы в сопоставление. В оригинальном посте в этом отношении взвешивались как положительные, так и отрицательные стороны.

Изменить: настройка для соответствия future.await(42) имеет, вероятно, непреднамеренный дополнительный риск сделать это: a) несовместимым с закрытием, когда это не так из-за того, что методы с тем же именем, что и член, разрешены; б) запрещение будущих разработок, в которых мы хотели бы привести аргументы await . Но, как вы ранее упоминали, настройка Future возвращающая закрытие, не должна быть самой острой проблемой.

@novacrazy Почему бы просто не использовать обычные комбинаторы Future?

Я не уверен, сколько у вас опыта работы с Futures 0.3, но общее ожидание состоит в том, что комбинаторы будут использоваться нечасто, и основное / идиоматическое использование будет async / await.

Async / await имеет несколько преимуществ перед комбинаторами, например, он поддерживает заимствование через точки доходности.

Комбинаторы существовали задолго до async / await, но async / await все равно изобрели, и не зря!

Async / await здесь, чтобы остаться, а это значит, что он должен быть эргономичным (в том числе с цепочками методов).

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

Как сказал @cramertj , давайте постараемся сосредоточить обсуждение на async / await, а не на альтернативах async / await.

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

Ваш пример можно значительно упростить:

let value = try {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => await backup_op(),
}.unwrap()

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

Это одна из замечательных особенностей async / await: он хорошо работает с другими частями языка, включая циклы, ветки, match , ? , try и т. Д.

Фактически, помимо await , это тот же код, который вы бы написали, если бы не использовали Futures.

Другой способ написать это, если вы предпочитаете использовать комбинатор or_else :

let value = await async {
    try {
        let v = await some_op()?;
        let v2 = await v.another_op()?;
        await v2.final_op()?
    }
}.or_else(|_| backup_op());

value.unwrap()

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

async fn doit() -> Result<Foo, Bar> {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()
}
let value = await doit().or_else(|_| backup_op());

value.unwrap()

(Это ответ на комментарий @joshtriplett ).

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

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

Кроме того, перечитав ваш комментарий, возможно, вы подумали, что я выступаю за префикс await ? Я не был, в моем примере использовался постфикс await . В целом мне нравится postfix await , хотя мне нравятся и другие синтаксисы.

Я начинаю привыкать к fut.await , думаю, первая реакция людей будет: «Подожди, вот как ты ждешь? Странно». но позже им это понравится за удобство. Конечно, то же самое верно и для @await , который выделяется гораздо больше, чем .await .

Используя этот синтаксис, мы можем пропустить некоторые возможности в примере:

`.await``@ await`
let value = try {
    some_op().await?
        .another_op().await?
        .final_op().await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op().await,
}.unwrap()
let value = try {
    some_op()@await?
        .another_op()@await?
        .final_op()@await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op()<strong i="21">@await</strong>,
}.unwrap()

Это также проясняет, что разворачивается с помощью ? , для await some_op()? неочевидно, разворачивается ли some_op() или ожидаемый результат.

@Pauan

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

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

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

Что касается await, все эти префиксы / постфиксные сигилы / ключевые слова bikeshedding - это замечательно, но, возможно, мы должны быть прагматичными и выбрать самый простой вариант, который наиболее знаком пользователям, приходящим в Rust. Т.е.: ключевое слово префикса

Этот год пройдет быстрее, чем мы думаем. Даже январь в основном готов. Если окажется, что пользователям не нравится ключевое слово префикса, его можно изменить в версии 2019/2020. Мы даже можем пошутить «ретроспективно - 2020 год».

@novacrazy

Общее мнение, которое я видел, заключается в том, что самое раннее_ время, которое мы хотели бы выпустить третье издание, - это 2022 год. Мы определенно не хотим планировать выпуск другого издания; издание 2018 года было отличным, но не без затрат. (И одним из пунктов выпуска 2018 года является возможность async / await, представьте, что вы вернете это и скажете: «Нет, вам нужно перейти на версию 2020 года сейчас!»)

В любом случае, я не думаю, что в редакции возможен переход ключевого слова prefix -> postfix, даже если это было бы желательно. Правило для редакций состоит в том, что в редакции X должен быть способ написать идиоматический код, чтобы он компилировался без предупреждений и работал так же в редакции X + 1.

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

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

Всего два цента (я никто, но за обсуждениями слежу довольно долго). Моим любимым решением была бы версия с постфиксом @await . Может быть, вы могли бы рассмотреть постфикс !await , как какой-нибудь новый синтаксис макроса постфикса?

Пример:

let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()!await?
    .error_for_status()?
    .json()!await?;

После нескольких языковых итераций было бы здорово реализовать наши собственные макросы постфикса.

... все новые предложения Sygils

В Rust уже много синтаксиса / sygil, зарезервировано ключевое слово await , stuff@await (или любой другой sygil) выглядит странно / некрасиво (субъективно, я знаю), и это просто специальный синтаксис, который не интегрируется ни с чем другим в языке, что является большим красным флагом.

Я посмотрел, как Go реализует
... <- ... предложение

@ I60R : У Go ужасный синтаксис, полный специальных решений, и он абсолютно необходим, в отличие от Rust. Это предложение снова является сложным с точки зрения синтаксиса и синтаксиса и полностью адаптировано только для этой конкретной функции.

@ I60R : у Go ужасный синтаксис

Пожалуйста, воздержитесь от критики здесь других языков. «У X ужасный синтаксис» не ведет к просветлению и консенсусу.

Как пользователь Python / JavaScript / Rust и студент, изучающий информатику, я лично предпочитаю, чтобы префикс await + f.await() присутствовал в обоих языках.

  1. И Python, и JavaScript имеют префикс await . Я ожидал увидеть await в начале. Если мне нужно глубоко прочитать строку, чтобы понять, что это асинхронный код, я чувствую себя очень неловко. Благодаря возможности WASM Rust он может привлечь многих разработчиков JS. Я считаю, что знакомство и комфорт действительно важны, учитывая, что в Rust уже есть много других новых концепций.

  2. Postfix await кажется удобным в настройке цепочки. Однако мне не нравятся такие решения, как .await , @await , f await потому что они выглядят как специальное решение для синтаксиса await хотя есть смысл подумать .await() как вызов метода future .

Rust уже является отходом от javascript, а await - это не что иное, как вызов функции (т.е. функциональность не может быть эмулирована через функцию), использование функций для обозначения await сбивает с толку тех, кто впервые вводит таймеры в async-await. Следовательно, я думаю, что синтаксис должен быть другим.

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

Причина этого в том, что await!(fut) должен _consume fut по значению_. Делать его похожим на доступ к полю - это _ плохо_, потому что это не означает перемещения, как это делает ключевое слово префикса, или возможности перемещения, как макрос или вызов метода.

Интересно, что синтаксис метода ключевых слов делает его почти похожим на неявный дизайн async fn() -> T использовалось идентично fn() -> Future<T> , вместо того, чтобы активировать поведение «неявного ожидания».

Тот факт, что синтаксис .await() _ выглядит_ как неявная система ожидания (как в Kotlin), может быть недоброжелателем и почти давать ощущение "неявного асинхронного, неявного ожидания" из-за магии, связанной с не- действительно-вызов-метод .await() синтаксис. Сможете ли вы использовать это как await(fut) с UFCS? Будет ли это Future::await(fut) для UFCS? Любой синтаксис, который выглядит как другое измерение языка, вызывает проблемы, если он не может быть в некоторой степени унифицирован с ним хотя бы синтаксически, даже если не функционально.

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

Я немного удивлен, что эта ветка полна предложений, которые, кажется, сделаны потому, что они возможны, а не потому, что они, кажется, приносят какие-либо существенные преимущества по сравнению с первоначальными предложениями.
Можем ли мы перестать говорить о $ , # , @ , ! , ~ т. Д., Не приводя веских аргументов в пользу того, что не так с await , который хорошо разбирается и зарекомендовал себя на различных других языках программирования?

Я думаю, что в сообщении https://github.com/rust-lang/rust/issues/57640#issuecomment -455361619 уже перечислены все хорошие варианты.

От тех:

  • Обязательные разделители-разделители кажутся прекрасными, по крайней мере, очевидно, каков приоритет, и мы можем читать код других без большей ясности. И ввести две круглые скобки не так уж и плохо. Возможно, единственным недостатком является то, что это выглядит как вызов функции, хотя это другая операция потока управления.
  • Предпочтительным вариантом может быть полезный приоритет. Кажется, это путь, по которому пошло большинство других языков, поэтому он знаком и доказан.
  • Я лично считаю, что ключевое слово postfix с пробелами выглядит странно с несколькими ожиданиями в одном выражении:
    client.get("url").send() await?.json()? . Этот пробел между ними выглядит неуместным. Со скобками для меня было бы немного больше смысла: (client.get("url").send() await)?.json()?
    Но мне все еще труднее следовать за потоком управления, чем с префиксным вариантом.
  • Мне не нравится поле постфикса. await выполняет очень сложную операцию - Rust не имеет вычислимых свойств, и в противном случае доступ к полям является очень простой операцией. Так что, похоже, создается неверное представление о сложности этой операции.
  • Метод Postfix может быть в порядке. Может подтолкнуть некоторых людей к написанию очень длинных операторов с несколькими ожиданиями в них, что может больше скрыть точки доходности. Это также снова делает вещи похожими на вызов метода, хотя это что-то другое.

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

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

@dpc , если вы полностью прочитаете предложение <-- , вы увидите, что этот синтаксис основан только на Go, однако он довольно отличается и может использоваться как в императивном контексте, так и в контексте связывания функций. Я также не понимаю, почему синтаксис await не является специальным решением, для меня он более конкретный и более неуклюжий, чем <-- . Это похоже на deref reference / reference.deref / etc вместо *reference или try result / result.try / etc вместо result? . Я либо не вижу никаких преимуществ использования ключевого слова await кроме знакомства с JS / Python / и т. Д., Что в любом случае должно быть менее значительным, чем наличие последовательного и составного синтаксиса. И я не вижу никаких недостатков в наличии сигилы <-- исключением того, что это не await который в любом случае не так прост, как простой английский, и пользователи должны сначала понять, что он делает.

Изменить: это также может быть хорошим ответом на сообщение @ Matthias247 , поскольку он предоставляет некоторые аргументы против await и предлагает возможную альтернативу, не затронутую теми же проблемами


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

Давайте упомянем актуальные особенности приоритета:

Таблица приоритетов в ее нынешнем виде :

Оператор / выражение | Ассоциативность
- | -
Пути |
Вызов методов |
Полевые выражения | слева направо
Вызов функций, индексация массива |
? |
Унарный - * ! & &mut |
as | слева направо
* / % | слева направо
+ - | слева направо
<< >> | слева направо
& | слева направо
^ | слева направо
\| | слева направо
== != < > <= >= | Требовать скобки
&& | слева направо
\|\| | слева направо
.. ..= | Требовать скобки
= += -= *= /= %= &= \|= ^= <<= >>= | справа налево
return break закрытия |

Полезный приоритет помещает await перед ? так что он связывается более плотно, чем ? . Таким образом, ? в цепочке связывает `await 'со всем, что находится перед ним.

let res = await client
    .get("url")
    .send()?
    .json();

Да, с полезным приоритетом, это «просто работает». Вы знаете, что это делает с первого взгляда? Это плохой стиль (наверное)? Если да, может ли rustfmt исправить это автоматически?

При очевидном приоритете await _somewhere_ ниже ? . Я не уверен, где именно, хотя эти особенности, вероятно, не имеют большого значения.

let res = await? (client
    .get("url")
    .send())
    .json();

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


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

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

@ CAD97 Для ясности помните, что .json() также является будущим (по крайней мере, в reqwests ).

let res = await await client
    .get("url")
    .send()?
    .json()?;
let res = await? await? (client
    .get("url")
    .send())
    .json();

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

Это _все_ из-за оператора ? . Ни в одном другом языке нет постфиксных операторов потока управления _and_ await , которые, по сути, всегда связаны в реальном коде.


Я предпочитаю по-прежнему постфиксное поле . Как оператор постфиксного управления, я чувствую, что ему нужна жесткая визуальная группировка, которую future.await обеспечивает более future await . И по сравнению с .await()? , я предпочитаю, как .await? выглядит _weird_, поэтому это будет _заметно_, и пользователи не будут считать, что это простая функция (и поэтому не будут спрашивать, почему UFCS не работает) .


В качестве еще одного аргумента в пользу постфикса, когда он стабилизируется, очень приветствуется rustfix для перехода от await!(...) к тому, что мы решим. Я не понимаю, как что-либо, кроме синтаксиса постфикса, может быть однозначно переведено без ненужного обертывания в ( ... ) .

Я думаю, сначала мы должны ответить на вопрос «хотим ли мы поощрять использование await в контекстах цепочки?». Я считаю, что преобладающий ответ - «да», поэтому он становится сильным аргументом в пользу вариантов постфикса. Хотя await!(..) будет проще всего добавить, я считаю, что нам не следует повторять историю try!(..) . Также я лично не согласен с аргументом, что «цепочка скрывает потенциально дорогостоящую операцию», у нас уже есть много методов цепочки, которые могут быть _ очень_ тяжелыми, поэтому цепочка не влечет за собой лени.

Хотя ключевое слово с префиксом await будет наиболее знакомо пользователям с других языков, я не думаю, что мы должны принимать решение на его основе, и вместо этого мы должны сосредоточиться на более долгосрочной перспективе, то есть на удобстве использования, удобстве и удобочитаемости. . @withoutboats говорили о «бюджете ознакомления», но я твердо уверен, что мы не должны вводить неоптимальные решения только для ознакомления.

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

Сначала давайте начнем с fut await , мне очень не нравится этот вариант, потому что он серьезно повлияет на то, как люди анализируют код, и будет постоянным источником путаницы при чтении кода. (Не забывайте, что код в основном предназначен для чтения)

Следующие fut.await , fut.await() и fut.await!() . Я думаю, что наиболее последовательным и менее запутанным будет вариант постфиксного макроса. Я не думаю, что стоит вводить новую сущность «функция ключевого слова» или «метод ключевого слова» только для того, чтобы сохранить пару символов.

Наконец, варианты на основе сигил: fut@await и fut@ . Мне не нравится вариант fut@await , если мы вводим сигилу, зачем беспокоиться о части await ? Есть ли у нас планы на будущие расширения fut@something ? Если нет, это просто кажется излишним. Мне нравится вариант fut@ , он решает проблемы с приоритетом, код становится простым для понимания, написания и чтения. Проблемы с видимостью можно решить путем выделения кода. Нетрудно объяснить эту особенность как «@ for await». Конечно, самым большим недостатком является то, что мы будем платить за эту функцию из очень ограниченного «бюджета сигил», но, учитывая важность этой функции и то, как часто она будет использоваться в базах асинхронного кода, я считаю, что в долгосрочной перспективе это будет стоить . И, конечно, можно провести определенные параллели с ? . (Хотя мы должны быть готовы к шуткам Perl от критиков Rust)

В заключение: на мой взгляд, если мы готовы обременять «сигил-бюджет», мы должны использовать fut@ , а если не fut.await!() .

Говоря о знакомстве, я не думаю, что нам следует слишком заботиться о знакомстве с JS / Python / C #, поскольку Rust находится в другой нише и уже во многих вещах выглядит по-разному. Предоставление синтаксиса, аналогичного этим языкам, является краткосрочной целью с низким вознаграждением. Никто не выберет Rust только для использования знакомого ключевого слова, когда под капотом он работает совершенно иначе.

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

ИМО, в этом смысле синтаксис <-- здесь самый сильный

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

Напомню опыт команды разработчиков C #:

Основное возражение против синтаксиса C # - приоритет оператора await foo?

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

Тенденция к желанию «продолжить» с «await» внутри выражения была редкой. Иногда мы видим такие вещи, как (await expr) .M (), но они кажутся менее распространенными и менее желательными, чем количество людей, выполняющих await expr.M ().

и

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

Это хороший аргумент против сигилы вместо специального (ключевого) слова.

https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Вам действительно стоит послушать ребят с миллионами пользователей.

Таким образом, вы не хотите ничего связывать, вы просто хотите иметь несколько await , и мой опыт такой же. Пишу код async/await более 6 лет, и я никогда не хотел такой возможности. Синтаксис Postfix выглядит действительно чуждым, и считается, что он разрешает ситуацию, которая, скорее всего, никогда не случится. Async call - действительно смелая вещь, поэтому несколько ожиданий в одной строке слишком тяжелы.

Тенденция к желанию «продолжить» с «await» внутри выражения была редкой. Иногда мы видим такие вещи, как (await expr) .M (), но они кажутся менее распространенными и менее желательными, чем количество людей, выполняющих await expr.M ().

Это похоже на апостериорный анализ. Возможно, одна из причин, по которой они не продолжаются, заключается в том, что это чрезвычайно неудобно делать в префиксном синтаксисе (сравнимо с нежеланием try! несколько раз в инструкции, потому что это остается читаемым с помощью оператора ? ). Вышеизложенное в основном учитывает (насколько я могу судить) приоритет, а не позицию. И я хотел бы напомнить вам, что C # - это не Rust, и члены трейтов могут немного изменить желание вызывать методы по результатам.

@ I60R ,

  1. Я считаю, что знакомство важно. Rust - относительно новый язык, и люди будут переходить с других языков, и если Rust будет казаться знакомым, им будет легче принять решение о выборе Rust.
  2. Я не очень люблю связывание методов - длинные цепочки гораздо сложнее отлаживать, и я считаю, что цепочка просто усложняет читаемость кода и может быть разрешена только как дополнительная опция (например, как макросы .await!() ). Форма префикса заставит разработчиков извлекать код в методы вместо объединения в цепочки, например:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

примерно так:

let body: MyResponse = await client.get_json("http://api")?;

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

Нет, это о внутренних экспериментах команды C #, когда в нем были и префиксные, и постфиксные, и неявные формы. И я говорю о своем опыте, который не является просто привычкой, из-за которой я не вижу плюсов постфиксных форм.

@mehcode Ваш пример меня не мотивирует. reqwest сознательно решает сделать начальный цикл запроса / ответа и последующую обработку ответа (основной поток) отдельными параллельными процессами, поэтому их следует ожидать дважды, как показывает

reqwest может полностью открыть следующие API:

let res = await client
    .get("url")
    .json()
    .send();

или же

let res = await client
    .get("url")
    .send()
    .json();

(Последний - простой сахар над and_then ).

Меня беспокоит, что во многих примерах постфиксов здесь используется эта цепочка в качестве примера, поскольку лучшее решение api для reqwest s и hyper - это разделение этих вещей.

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

@andreytkachenko

Форма префикса заставит разработчиков извлекать код в методы вместо объединения в цепочки, например:

Это хорошо или плохо? В случаях, когда существует N методов, каждый из которых может привести к M повторным действиям, разработчик должен предоставить N * M методы? Мне лично нравятся составные решения, даже если они немного длиннее.

примерно так:

let body: MyResponse = await client.get_json("http://api")?;
let body: MyResponse = client.get("http://api").await?.into_json().await?;

@Pzixel

Я не думаю, что в C # вы получите столько функционального кода, сколько в Rust. Или я не прав? Это делает опыт разработчиков / пользователей C # интересным, но не обязательно применимым для Rust. Хотелось бы, чтобы мы контрастировали с Ocaml или Haskell.

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

@dpc

Я не думаю, что в C # вы получите столько функционального кода, сколько в Rust. Или я не прав? Это делает опыт разработчиков / пользователей C # интересным, но не обязательно применимым для Rust. Хотелось бы, чтобы мы контрастировали с Ocaml или Haskell.

LINQ и функциональный стиль довольно популярны в C #.

@dpc ,
если вы о удобочитаемости - я считаю, что столько явного кода - тем лучше, и мой опыт говорит, что если имена методов / функций хорошо описывают себя, нет необходимости выполнять последующие действия.

если вы о накладных расходах - компилятор Rust довольно умен, чтобы встраивать их (в любом случае у нас всегда есть #[inline] ).

@dpc

let body: MyResponse = client.get("http://api").await?.into_json().await?;

Мне кажется, что это в основном повторяет проблему Futures API: это упрощает цепочку, но становится труднее проникнуть в тип этой цепочки и отладить ее.

Разве тот же аргумент не применим к? оператор хоть? Это позволяет использовать больше цепочек и, таким образом, усложняет отладку. Но почему мы выбрали? за попытку! тогда? И почему Rust предпочитает API с шаблоном построителя? Итак, Rust уже сделал несколько решений в пользу цепочки в API. И да, это может усложнить отладку, но разве это не должно происходить на уровне ворса - может быть, новый линт для clippy для слишком больших цепочек? Я все еще должен увидеть достаточную мотивацию, чтобы понять, чем здесь отличается ожидание.

Это хорошо или плохо? В случаях, когда существует N методов, каждый из которых может привести к M повторным действиям, разработчик должен предоставить N * M методы? Мне лично нравятся составные решения, даже если они немного длиннее.

N и M не обязательно большие, а также не все из них могут быть интересны / полезны для извлечения.

@andreytkachenko ,

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

  2. Невозможность отлаживать длинные цепочки - это ограничение отладчиков не стиля кода. Что касается удобочитаемости, я думаю, это зависит от обстоятельств, и применение императивного стиля здесь излишне ограничительно. Если async вызовы функций по-прежнему являются вызовами функций, то удивительно, что они не поддерживают цепочку. В любом случае, <-- - это префиксный оператор с возможностью использования его в цепочках функций в качестве дополнительной опции, как вы сказали.

@skade Согласен.

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

Я по-прежнему поклонник как ключевого слова префикса await и макроса await!(...) / .await!() в сочетании с функциями генератора async и #[async] , как описано в моем комментарии здесь .

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

@skade ,
Для меня большим недостатком использования комбинаторов в future является то, что они будут скрывать вызовы функций async и их будет невозможно отличить от обычных функций. Я хочу увидеть все точки приостановки, поскольку очень вероятно, что они будут использоваться внутри цепочек в стиле конструктора.

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

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

@novacrazy хороший момент, теперь, когда вы упомянули об этом, как бывший javascripter, я НИКОГДА не использую цепочки ожидания, я всегда использовал блоки then

Я думаю, это будет выглядеть примерно так

let result = (await doSomethingAsync()
          .then(|result| {
                     match result {
                          Ok(v) => doSomethingAsyncWithFirstResponse(v)
                          Err(e) => Future.Resolve(Err(e))
                      }
            }).then(|result| {
                  Ok(result.unwrap())
            })).unwrap();

что я даже не знаю, возможно ли это, мне нужно посмотреть фьючерсы в Rust

@richardanaya это вполне возможно (другой синтаксис).

Как коробка подразумевает:

impl<T> Box<T> {
    #[inline]
    pub fn new(x: T) -> Box<T> {
        box x
    }
    ...
}

Мы можем ввести новое ключевое слово await и признак Await например:

impl<T> Await for T {
    #[inline]
    pub fn await(self) -> T {
        await self
    }
    ...
}

И используйте await как метод и как ключевое слово:

let result = await foo();
first().await()?.second().await()?;

@richardanaya Согласен. Я был одним из первых разработчиков, которые применили async / await для моих веб-разработок много лет назад, и настоящая сила пришла от объединения async / await с существующими Promises / Futures. Кроме того, даже ваш пример, вероятно, можно было бы упростить как:

let result = await doSomethingAsync()
                  .and_then(doSomethingAsyncWithFirstResponse);

let value = result.unwrap();

Если у вас есть вложенные фьючерсы или результаты фьючерсов или фьючерсы результатов, комбинатор .flatten() может значительно упростить это. Было бы плохим тоном распаковывать и ждать каждого вручную.

@XX Это избыточно и недействительно. await существует только в функциях async и в любом случае может работать только с типами, реализующими Future / IntoFuture , поэтому нет необходимости в новом черта.

@novacrazy , @richardanaya

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

@novacrazy хороший момент, теперь, когда вы упомянули об этом, как бывший javascripter, я НИКОГДА не использую цепочки ожидания, я всегда использовал блоки then

Комбинаторы имеют совершенно разные свойства и возможности в Rusts async / await, например, в отношении заимствования между точками доходности. Вы не можете безопасно писать комбинаторы, которые имеют те же возможности, что и асинхронные блоки. Давайте не будем обсуждать комбинатор в этой ветке, поскольку он бесполезен.

@ I60R

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

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

Имейте в виду, что в C # нет проблемы, при которой большая часть кода, использующего префикс await будет объединять его с (уже существующим) оператором постфикса ? . В частности, вопросы о приоритете и общая неудобство наличия двух одинаковых «декораторов» появляются на противоположных сторонах выражения.

@ Matthias247 Если вам действительно нужно заимствовать данные, конечно, не стесняйтесь использовать несколько операторов await . Однако часто вам просто нужно переместить данные, и комбинаторы вполне подходят для этого и могут компилироваться в более эффективный код. Иногда полностью оптимизируется.

Опять же, настоящая сила заключается в объединении вещей вместе. Нет единого правильного способа делать такие сложные вещи. Отчасти поэтому я считаю, что весь этот синтаксис bikeshedding будет именно таким, если он не поможет новым пользователям, переходящим с другого языка, и поможет создать поддерживаемый, производительный и читаемый код . В 80% случаев я сомневаюсь, что даже коснусь async / await вне зависимости от синтаксиса, просто чтобы обеспечить наиболее стабильные и производительные API с использованием простых фьючерсов.

В этом отношении префиксное ключевое слово await и / или смешанный макрос await!(...) / .await!() являются наиболее читаемыми, знакомыми и простыми для отладки параметрами.

@andreytkachenko

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

  1. Синтаксис await выглядит как аналогичные языки, в которых много await. Javascript здесь довольно большой и очень важен для наших возможностей WASM как сообщества.

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

Одна вещь, которую я хотел бы предложить из моих дней асинхронности javascript, что было гораздо более полезным для меня как разработчика в ретроспективе, так это возможность группировать обещания / будущее вместе. Promise.all (p1 (), p2 ()), чтобы я мог легко распараллелить работу. Цепочка then () всегда была просто отголоском прошлых обещаний Javascript, но в значительной степени архаичной и ненужной сейчас, когда я думаю об этом.

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

@novacrazy Асинхронная функция возвращает тип impl Future , верно? Что мешает добавить метод await к трейту Future ? Как это:

pub fn await(self) -> Self::Output {
    await self
}
...

@XX Насколько я понимаю, функции async в Rust преобразуются в конечные автоматы с помощью генераторов. Эта статья - хорошее объяснение. Итак, await нужна функция async для работы, чтобы компилятор мог правильно преобразовать их обе. await не может работать без части async .

Future действительно есть wait метод, который похож на то , что вы предлагаете, но блокирует текущий поток.

@skade ,

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

@skade , @ Matthias247 , @XX

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

@ I60R let привязки - это точки соединения для вывода типов и помощи в сообщении об ошибках.

@novacrazy

Так что для работы await нужна асинхронная функция.

Можно ли это выразить в возвращаемом типе? Например, тип возвращаемого значения - impl Future + Async вместо impl Future .

@skade Я всегда думал, что . в синтаксисе вызова метода служит точно той же цели

@dpc

Я не думаю, что в C # вы получите столько функционального кода, сколько в Rust. Или я не прав? Это делает опыт разработчиков / пользователей C # интересным, но не обязательно применимым для Rust. Хотелось бы, чтобы мы контрастировали с Ocaml или Haskell.

вы получите столько же, сколько Rust. Посмотрите на любой код LINQ, и вы его увидите.

Типичный асинхронный код выглядит так:

async Task<List<IGroping<int, PageMetadata>>> GetPageMetadata(string url, DbSet<Page> pages)
{
    using(var client = new HttpClient())
    using(var r = await client.GetAsync(new Uri(url)))
    {
        var content = await r.Content.ReadAsStringAsync();
                return await pages
                   .Where(x => x.Content == content)
                   .Select(x => x.Metadata)
                   .GroupBy(x => x.Id)
                   .ToListAsync();
    }
}

Или, в более общем смысле

let a = await!(service_a);
let b = await!(some_method_on(a, some, other, params));
let c = await!(combine(somehow, a, b));

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


Я могу согласиться с тем, что вы можете связать фьючерсы в одной единственной ситуации. Если вы хотите обработать ошибку, которая может возникнуть во время вызова. например, let a = await!(service_a)? . Это единственная ситуация, когда альтернатива postfix лучше. Я видел здесь преимущества, но не думаю, что они перевешивают все недостатки.

Другая причина: как насчет impl Add for MyFuture { ... } и let a = await a + b; ?

Напомню о RFC с описанием обобщенного типа в контексте ключевого слова postfix await. Это позволит следующий код:

let x = (0..10)
    .map(some_computation)
    .collect() : Result<Vec<_>, _>
    .unwrap()
    .map(other_computation) : Vec<usize>
    .into() : Rc<[_]>;

Очень похоже на ключевое слово postfix await:

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

С теми же недостатками при плохом форматировании:

foo.iter().map(|x| x.bar()).collect(): Vec<_>.as_ref()
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Я думаю, что если мы проглотим пилюлю Type Ascription RFC, мы должны проглотить fut await для согласованности.

Кстати, знаете ли вы, что это допустимый синтаксис:

fn main() {
    println
    !("Hello, World!");
}

Тем не менее, в реальном коде я не видел этого.

Позвольте мне немного отвлечься. Я думаю, что мы должны дать макросам постфикса, как предлагает @BenoitZugmeyer , еще одну мысль. Это можно сделать либо как expr!macro или как expr@macro , или даже как expr.macro!() . Я думаю, что первый вариант будет предпочтительнее. Я не уверен, что это хорошая идея, но если мы хотим извлечь общую концепцию, а не специальное решение, при этом все еще получая postfix await , мы должны хотя бы подумать о макросах postfix как о потенциальном решении.

Имейте в виду, что даже если бы мы сделали await постфиксным макросом, он все равно был бы волшебным (например, compile_error! ). Однако @jplatte и другие уже установили, что это не проблема.

Как бы я поступил, если бы мы пошли по этому пути, я бы сначала установил, как именно будут работать постфиксные макросы, затем разрешить только await в качестве волшебного постфиксного макроса, а затем разрешить собственные определенные постфиксные макросы.

Lexing

Что касается лексирования / синтаксического анализа, это может быть проблемой. Если мы посмотрим на expr!macro , компилятор может подумать, что есть макрос с именем expr! а после него есть недопустимые буквы macro . Тем не менее, expr!macro должно быть возможно лексировать с опережением на единицу, и что-то становится постфиксным макросом, когда есть expr за которым следует ! , за которым непосредственно следует identifier . Я не разработчик языка и не уверен, что это делает лексирование слишком сложным. Я просто предполагаю, что постфиксные макросы могут иметь форму expr!macro .

Были бы полезны макросы postfix?

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

  • stream!await_all : для ожидающих потоков
  • option!or_continue : когда option равно None, продолжить цикл
  • monad!bind : для выполнения =<< без привязки к имени

stream!await_all

Это позволяет нам ждать не только фьючерсов, но и потоков.

event_stream("ws://some.stock.exchange/usd2eur")
    .and_then(|exchange_response| {
        let exchange_rate = exchange_response.json()?;
        stream::once(UpdateTickerAction::new(exchange_rate.value))
    })

будет эквивалентно (в блоке async -stream-esque):

let exchange_rate = event_stream("ws://some.stock.exchange/usd2eur")
    !await_all
    .json()?;

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

Это позволяет нам развернуть опцию, и если это None , продолжить цикл.

loop {
    let event = match engine.event() {
        Some(event) => event,
        None => continue,
    }
    let button = match event.button() {
        Some(button) => button,
        None => continue,
    }
    handle_button_pressed(button);
}

будет эквивалентно:

loop {
    handle_button_pressed(
        engine.event()!or_continue
            .button()!or_continue
    );
}

monad!bind

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

Я взял отсюда следующее.

nameDo :: IO ()
nameDo = do putStr "What is your first name? "
            first <- getLine
            putStr "And your last name? "
            last <- getLine
            let full = first ++ " " ++ last
            putStrLn ("Pleased to meet you, " ++ full ++ "!")

будет эквивалентно:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    let last = getLine()!bind;
    let full = first + " " + &last
    putStrLn("Pleased to meet you, " + &full + "!")!bind;
}

Или, более встроенный, с меньшим количеством let s:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    putStrLn(
        "Pleased to meet you, " + &first + " " + &getLine()!bind + "!"
    )!bind;
}

Оценка

Я очень разделен по этому поводу. Математическая часть моего мозга хочет найти обобщенное решение для await , и постфиксные макросы могут быть одним из способов их решения. Прагматическая часть моего мозга думает: зачем создавать язык, макросистему которого уже никто не понимает, еще более сложным.

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

Что, вы парни, думаете?

@EyeOfPython

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

let view = array.slice(s![.., ..]);

но гораздо лучше было бы

let view = array.slice![.., ..];
// or like you suggested
let view = array!slice[.., ..];
// or like in PHP
let view = array->slice![.., ..];

и многие комбинаторы могут исчезнуть, например, с постфиксом _with или _else :

opt!unwrap_or(Error::new("Error!"))?; //equal to .unwrap_or_else(||Error::new("Error!"));

@EyeOfPython @andreytkachenko постфиксные макросы в настоящее время не являются функцией в Rust, и ИМХО потребуется полная фаза реализации RFC + FCP +.

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

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

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

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

Мои причины не использовать здесь макросы:

  1. Макросы предназначены для конкретных предметных областей, и их использование любым способом, изменяющим поток управления программой или имитирующим основные языковые функции, является излишним.
  2. Макросы Postfix сразу же будут использоваться для реализации пользовательских операторов или других функций эзотерического языка, что приведет к плохому коду.
  3. Они будут препятствовать развитию правильных языковых функций:

    • stream.await_all - идеальный вариант использования комбинатора

    • option.or_continue и замена комбинаторов _else - идеальный вариант использования оператора объединения с нулевым значением

    • monad.bind - идеальный вариант использования для цепочек if-let

    • ndarray slicing - идеальный вариант использования const generics

  4. Они поставили бы под сомнение уже реализованный оператор ?

@collinanderson

которые также могут быть связаны

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

@Pzixel Вероятно, что в конечном итоге Generator::resume примет значение, и, следовательно, yield expr будет иметь тип, отличный от () .

@vlaff Я не вижу четко аргумента в пользу того, что await должен соответствовать описанию типа. Это очень разные вещи.

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

Чтение множества комментариев, добавление дальнейшего резюме о том, почему await! (...) кажется очень идеальным путем:

  1. он выглядит наиболее знакомым для существующего кода, потому что макросы знакомы
  2. есть уже существующая работа, которая использует await! (...) https://github.com/alexcrichton/futures-await и может помочь уменьшить количество перезаписываемого кода
  3. поскольку постфиксные макросы отсутствуют, они могут никогда не быть частью языка, и "ждите!" в нестандартном контексте не похоже, что это даже возможно в соответствии с RFC
  4. это дает сообществу больше времени, чтобы обдумать долгосрочное направление после широкого распространения стабильного использования, обеспечивая при этом что-то, что не совсем выходит за рамки нормы (например, попробуйте! ())
  5. можно было бы использовать в качестве аналогичного шаблона для предстоящего yield! (), пока мы не выясним официальный путь, который может удовлетворить await и yield
  6. цепочка может быть не такой ценной, как хотелось бы, и ясность, вероятно, даже улучшится за счет наличия нескольких ожиданий на нескольких строках
  7. для подсветки синтаксиса IDE не должно происходить никаких изменений
  8. люди с других языков, вероятно, не будут сбиты с толку макросом await! (...) после просмотра других макросов (меньше когнитивной перегрузки)
  9. это, вероятно, путь с наименьшими усилиями из всех путей продвижения к стабилизации

Ждите! макрос здесь уже есть, речь идет о цепочке.

Чем больше я об этом думаю, тем больше мне нравится постфикс. Например:

let a = foo await;
let b = bar await?;
let c = baz? await;
let d = booz? await?;
let e = kik? + kek? await? + kuk? await?;
// a + b is `impl Add for MyFuture {}` which alises to `a.select(b)`

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

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

@Pzixel , @HeroicKatora , @skade

См., Например, выражения Python yield и yield from : там внешняя функция может предоставить значение, которое станет результатом yield при возобновлении работы генератора. Это то, что имел в виду Следовательно, yield также будет иметь тип, отличный от ! или () .

С точки зрения сопрограммы, yield и await приостанавливают ее и могут (в конечном итоге) вернуть значение. Поскольку это всего лишь две стороны одной медали.

И чтобы исключить еще одну возможность синтаксиса, _square-brackets-with-keyword_, частично чтобы выделить это (используя yield для выделения синтаксиса):

let body: MyResponse = client.get("http://api").send()[yield]?.into_json()[yield]?

"ключевое слово postfix" имеет для меня наибольший смысл. постфикс с символом, отличным от пробела, разделяющего выражение будущего, и await также имеет смысл для меня, но не '.' поскольку это для методов, а await не является методом. Однако, если вы хотите await в качестве ключевого слова префикса, мне нравится предложение @XX для Trait или метода, который просто вызывает await self для тех, кто хочет связать кучу вещей вместе (хотя мы, вероятно, не мог назвать метод await ; просто wait подойдет, хотя я думаю). Лично я, вероятно, в конечном итоге сделаю ожидание для каждой строки, а не цепочку так много, потому что я считаю длинные цепочки менее читаемыми, поэтому префикс или постфикс мне подойдут.

[править] Я забыл, что wait уже существует и блокирует будущее, так что отбросьте эту мысль.

@roland Я специально говорю об этом, где обсуждаются описания типов: https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@rolandsteiner так ты пишешь

let body: MyResponse = client.get("http://api").send() await?.into_json() await?;

Когда я писал это так:

let response = client.get("http://api").send() await?;
let body: MyResponse = response.into_json() await?;

@skade О, вы имели в виду другой комментарий, чем я думал, извините. : stuck_out_tongue:

@Pzixel Вероятно, что в конечном итоге Generator::resume примет значение, и, следовательно, yield expr будет иметь тип, отличный от () .

Следовательно, yield также будет иметь тип, отличный от ! или () .

@valff , @rolandsteiner Я считаю маловероятным, что yield вернет значение возобновления, которое трудно уместить в статически типизированном языке, не сделав синтаксис и / или черту генератора раздражающим при работе. В исходном прототипе с аргументами возобновления использовалось ключевое слово gen arg для ссылки на этот аргумент, что-то подобное с большей вероятностью будет работать хорошо, ИМО. С этой точки зрения yield прежнему будет возвращать () поэтому не должно сильно влиять на обсуждение await .

Я думаю, что await должен быть префиксным оператором, потому что async есть (и я ожидаю, что yield также будет префиксом). Или используйте его как обычный метод в постфиксной позиции.

Тем не менее, у меня не все работает здесь: зачем даже рассматривать ключевое слово postfix, если его нет ни в одном другом Rust? Это сделало бы язык таким странным.

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

  1. Выберите метод (например, foo.await() ). Он не вводит странное ключевое слово postfix, и все мы знаем, что оно означает. Мы также можем использовать это.
  2. Если нам действительно нужно ключевое слово / loloperator, мы можем ответить на этот вопрос позже.

Кроме того, чтобы выразить свое личное мнение, я ненавижу await в позиции ключевого слова postfix.

Префикс await используется в других языках не просто так - он соответствует естественному языку. Вы не говорите: «Я буду ждать вашего прибытия». Хотя, написав JavaScript, я могу быть немного предвзятым.

Я также скажу, что считаю await -цепь переоценкой. Ценность async/await сравнению с будущими комбинаторами заключается в том, что он позволяет писать асинхронный код последовательно, используя стандартные языковые конструкции, такие как if , match и т. Д. В любом случае вам захочется разбить ожидания.

Не давайте придумывать слишком много магического синтаксиса для async/await , это лишь небольшая часть языка. Синтаксис предлагаемого метода (например, foo.await() ) IMO слишком ортогонален обычным вызовам метода и выглядит слишком волшебным. Механизм proc-macro на месте, почему бы не замаскировать его?

Как насчет использования ключевого слова await вместо async в определении функции? Например:

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future
}

Этот await fn можно вызвать только в async контексте:

async {
    let n = foo(bar());
}

И десугаринг до let n = (await foo(bar())); .
Затем, помимо ключевого слова await , признак Future может реализовать await -метод для использования логики await в постфиксной позиции, например:

async {
    let n = bar().awaited();
}

Кроме того, может кто-нибудь объяснить мне отношения с генераторами? Я удивлен, что async / await реализованы раньше, чем генераторы (даже стабилизированы).

Генераторы @phaazon были добавлены в Rust как деталь внутренней реализации для async / await . В настоящее время нет планов по их стабилизации, и для них по-прежнему требуется не экспериментальный RFC. (Лично мне бы хотелось, чтобы они стабилизировались, но, вероятно, они появятся как минимум через год или два, возможно, не будут RFCed до тех пор, пока async / await станет стабильным, и некоторый опыт работы с ней).

@XX Я думаю, вы нарушаете семантику async если используете await в определении функции. Давайте придерживаться стандартов, не так ли?

@phaazon Не могли бы вы подробнее указать, как это нарушает семантику async ?

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

@phaazon Нет, не вместо этого, а в дополнение. Полный пример:

async fn bar() -> i32 {
    5 // will be "converted" to impl Future<Output = i32>
}

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future // will be "converted" to i32 in async context
}

async {
    let a = await bar(); // correct, a == 5
    let b = foo(bar()); // correct, b == 5
}

let c = foo(bar()); // error, can't call desugaring statement `await foo(bar())`

если он действителен, то можно будет реализовать await-метод для использования в цепочке:

async {
    let n = first().awaited()?.second().awaited()?;
    // let n = (await (await first())?.second())?;
}

На мой взгляд, не имеет значения, если postfix await "кажется" слишком волшебным и незнакомым для языка. Если мы примем синтаксис метода .await() или .await!() тогда останется лишь объяснить, что Future s имеют метод .await() или .await!() которые вы также можете использовать для их ожидания в тех случаях, когда это имеет смысл. Это не так сложно понять, если вы потратите 5 минут на размышления об этом, даже если вы никогда этого раньше не видели.

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

trait AwaitChainable {
    fn await(self) -> impl Future;
}

impl<T: Future> AwaitChainable for T {
    fn await(self) -> impl Future {
        await self
    }
}

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

@ivandardi Я думал о том же. Но это определение

fn await(self) -> impl Future {
    await self
}

не говорит, что функция может быть вызвана только в async -контексте.

@XX Да, как я уже сказал, игнорируйте сломанный псевдокод: у PI в настоящее время нет инфраструктуры, чтобы посмотреть, как там должен быть написан правильный синтаксис и характеристики :( Представьте, что я написал то, что делает его вызываемым только в async контексты.

@XX , @ivandardi Согласно определению, await работает только в контексте async . Следовательно, это незаконно:

fn await(self) -> impl Future {
    await self
}

Это должно быть

async fn await(self) -> impl Future {
    await self
}

Это можно вызвать только в контексте async следующим образом:

await future.await()

Что за поражение в целом.

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

@ivandardi Эта эволюция моей версии:

fn await(self) -> T { // How to indicate using in async-context only?
    await self
}

ржавчина
await fn await (self) -> T {// Поскольку async уже выполнен
ждать себя
}

```rust
await fn await(self) -> T {
    self // remove excess await
}

@CryZe @XX Почему ты меня освистываешь? Я прав.

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

@EyeOfPython Это определение работает не так, как ожидалось:

async fn await(self) -> impl Future {
    await self
}

Скорее:

#[call_only_in_async_context_with_derived_await_prefix]
fn await(self) -> impl Future {
    self
}

Я голосую против, потому что вы проигнорировали, что это гипотетический псевдосинтаксис. По сути, их точка зрения заключается в том, что Rust мог бы добавить специальные черты, такие как Drop и Copy, которые понимает язык, что добавляет возможность вызывать .await () для типа, который затем взаимодействует с типом так же, как ключевое слово await. Кроме того, голосование в любом случае считается нерелевантным для RFC, поскольку цель состоит в том, чтобы найти объективное решение, а не решение, основанное на субъективных ощущениях, таких как визуализация голосов за / против.

Я не понимаю, что должен делать этот код. Но это не имеет ничего общего с тем, как в настоящее время работает async await. Если вам это интересно, пожалуйста, держите его подальше от этой темы и напишите для него специальный RFC.

@CryZe Этот тип доступен. Это называется «Будущее». И давно было согласовано, что async / await - это механизм, предназначенный ТОЛЬКО для преобразования асинхронного кода. Это не означает общего обозначения привязки.

@ivandari ваш postfix await не ждет, а создает новое будущее. По сути, это функция идентичности. Вы не можете неявно ждать, потому что это не обычная функция.

@ Matthias247 Они пытались предложить способ, чтобы postfix await имел синтаксис вызова метода. Таким образом, в Rust не нужно вводить произвольные новые символы, такие как #, @ или ... как мы это сделали с? оператор, и по-прежнему выглядят довольно естественно:

let result = some_operation().await()?.some_method().await()?;

Таким образом, идея заключалась бы в том, чтобы каким-то образом использовать await как особый метод для черты Future, который компилятор видит так же, как обычное ключевое слово await (которое в этом случае вам вообще не нужно), и преобразовать async код в генератор оттуда. Тогда у вас будет правильный логический поток управления слева направо, а не слева направо налево с await some_future()? и вам не нужно вводить новые странные символы.

(Итак, tl; dr: это похоже на вызов метода, но на самом деле это просто postfix await)

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

В частности, такие как:

let result = await reqwest::get(..).send();
let response = result.map_err(|e| add_context(e))?;
let parser = await response.json();
let parsed = parser.map_err(|e| add_context(e))?;

менее читабелен, чем (меня не волнует, какой синтаксис постфикса считается):

let parsed = reqwest::get(..)
    .send() await
    .map_err(add_context)?
    .json() await
    .map_err(add_context)?;

_Даже_ это форматирование по умолчанию содержит больше строк, чем подход с несколькими переменными. Выражения, у которых нет нескольких ожиданий, кажется, что они должны занимать меньше места по вертикали с postfix-await.

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

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

@XX Если это не может быть выражено в пользовательском коде, нам придется прибегнуть к компилятору для реализации этой функции. Но с точки зрения понимания того, как работает постфикс, объяснение, которое я опубликовал ранее, все еще работает, даже если это просто поверхностное понимание.

@CryZe , @XX ,

На первый взгляд это выглядит гладко, но абсолютно не соответствует философии Rust. Даже метод sort на итераторах не возвращает self и прерывает цепочку вызовов методов, чтобы сделать выделение явным. Но вы ожидаете, что что-то не менее эффективное будет реализовано совершенно неявным образом, неотличимым от обычных вызовов функций. ИМО, шансов нет.

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

Почему это должно быть целью? Rust не поддерживает это для всех других потоков управления (например, break , continue , if и т. Д.). Нет особой причины, кроме "это может выглядеть лучше по своему мнению.

В общем, я хотел бы напомнить всем, что await - это особенный, а не обычный вызов метода:

  • Отладчики могут вести себя странно при переходе через await, потому что стек может раскручиваться и восстанавливаться. Насколько я помню, потребовалось много времени, чтобы заставить асинхронную отладку работать в C # и Javascript. А у тех есть платные команды, которые работают над отладчиками!
  • Локальные объекты, которые не проходят через точки ожидания, могут храниться в реальном стеке ОС. Те, что не должны быть перемещены в сгенерированное будущее, что, в конечном итоге, повлияет на требуемую память кучи (где будет жить будущее).
  • Заимствования между точками ожидания являются причиной того, что сгенерированные фьючерсы должны быть !Unpin , и вызывают множество неудобств с некоторыми из существующих комбинаторов и механизмов. Это может быть потенциально возможным генерировать Unpin фьючерсы с async методы , которые не позаимствовать через ждет в будущем. Однако, если await s невидимы, этого никогда не произойдет.
  • Могут возникнуть другие неожиданные проблемы с проверкой заимствований, которые не покрываются текущим состоянием async / await.

@Pzixel @lnicola

Примеры, которые вы приводите для C #, абсолютно необходимы, и ничего похожего на длинные цепочки функционального стиля, которые мы часто видим в Rust. Этот синтаксис LINQ является просто DSL и не похож на foo().bar().x().wih_boo(x).camboom().space_flight(); Я немного погуглил, и примеры кода C # выглядят на 100% обязательными, как и большинство популярных языков программирования. Вот почему мы не можем просто взять то, что сделал langue X, потому что это не то же самое.

Обозначение префикса IMO идеально подходит для императивного стиля кодирования. Но Rust поддерживает оба стиля.

@skade

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

@ Matthias247 Нет, есть особая причина, кроме того, насколько красиво это выглядит. Это потому, что Rust поощряет создание цепочек. И вы могли бы сказать: «О, другие механизмы потока управления не имеют постфиксного синтаксиса, так почему await должен быть особенным?». Что ж, это неправильная оценка. У нас ДЕЙСТВИТЕЛЬНО есть постфиксный поток управления. Они просто внутренние по отношению к вызываемым нами методам. Option::unwrap_or похож на оператор сопоставления постфикса. Iterator::filter - это как постфиксный оператор if. Тот факт, что поток управления не является частью самого синтаксиса цепочки, не означает, что у нас еще нет потока управления постфиксом. С этой точки зрения добавление постфикса await фактически соответствовало бы тому, что у нас есть. В этом случае мы могли бы даже иметь что-то более похожее на то, как работает Iterator и вместо простого постфикса await мы могли бы иметь несколько комбинаторов await, например Future::await_or . В любом случае наличие postfix await - это не просто вопрос внешнего вида - это вопрос функциональности и качества жизни. В противном случае мы бы все равно использовали макрос try!() , верно?

@dpc

Примеры, которые вы приводите для C #, абсолютно необходимы, и ничего похожего на длинные цепочки функционального стиля, которые мы часто видим в Rust. Этот синтаксис LINQ - это просто DSL и не похож на foo (). Bar (). X (). Wih_boo (x) .camboom (). Space_flight (); Я немного погуглил, и примеры кода C # выглядят на 100% обязательными, как и большинство популярных языков программирования. Вот почему мы не можем просто взять то, что сделал langue X, потому что это не то же самое.

Это неправда (вы можете проверить любой фреймворк, например, polly ), но спорить не буду. Я вижу столько же связанного кода на обоих языках. Однако на самом деле есть одна вещь, которая отличает все от других. И это называется Try trait. В C # нет ничего похожего, поэтому он не предполагает делать что-либо после точки await . Если вы получите исключение, оно будет автоматически упаковано и поднято для вызывающего.

Это не относится к Rust, где вам нужно вручную использовать оператор ? .

Если у вас должно быть что-то после точки await вы должны либо создать "комбинированный" оператор await? , расширить языковые правила и т.д. и т.д. ИЛИ вы можете просто сделать постфикс ожидания, и все станет более естественным (кроме немного чуждый синтаксис, но кого это волнует?).

Поэтому в настоящее время я считаю postfix await более жизнеспособным решением. Единственное, что у меня есть, должно быть выделенное ключевое слово, разделенное пробелами, а не await() или await!() .

Думаю, я нашел вескую причину, оправдывающую использование обычного метода .await (). Если задуматься, это ничем не отличается от любого другого способа блокировки. Вы вызываете метод .await (), выполнение (возможно зеленого) потока приостанавливается, и в какой-то момент исполняющаяся среда выполнения возобновляет выполнение потока в какой-то момент. Итак, для всех намерений и целей, есть ли у вас Mutex или стандартный канал, например:

let result = my_channel().recv()?.iter().map(...).collect();

или такое будущее:

let result = my_future().await()?.iter().map(...).collect();

ничем не отличается. Оба они блокируют выполнение в соответствующих функциях recv () / await (), и обе цепочки выполняются одинаково. Единственное отличие состоит в том, что await (), возможно, работает на другом исполнителе, который не является тяжелым потоком ОС (но это вполне может быть в случае однопоточного исполнителя). Таким образом, отказ от большого количества цепочек влияет и на то, и на другое, и, вероятно, должен привести к образованию действительно обрезанных волокон.

Однако я хочу сказать, что с точки зрения человека, который пишет этот код, нет большой разницы в .recv () или .await (), оба являются методами, которые блокируют выполнение и возвращаются, как только результат будет доступен. . Так что для всех намерений и целей это может быть в значительной степени обычным методом и не требует полного ключевого слова. У нас также нет ключевого слова recv или lock для Mutex и канала std.

Однако rustc явно хочет превратить весь код в генератор. Но действительно ли это семантически необходимо с языковой точки зрения? Я почти уверен, что можно было бы написать альтернативный компилятор для rustc, который реализует await путем блокировки фактического потока ОС (при вводе async fn потребуется создать поток, а затем awaiting заблокирует этот поток). Хотя это была бы чрезвычайно наивная и медленная реализация, семантически она будет вести себя точно так же. Таким образом, тот факт, что на самом деле rustc превращает await в генератор, семантически не является необходимым. Поэтому я бы на самом деле утверждал, что преобразование rustc вызова метода .await () в генератор можно рассматривать как оптимизирующую деталь реализации rustc. Таким образом, вы можете оправдать, что .await () - это своего рода полный метод, а не полное ключевое слово, но при этом rustc все равно превращает все это в генератор.

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

@ I60R Это не менее явно, чем ключевое слово await. Вы по-прежнему можете выделить его в среде IDE, если хотите. Он просто перемещает ключевое слово в положение постфикса, где, я бы сказал, его труднее пропустить (поскольку оно находится именно в том положении, где выполнение останавливается).

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

@CryZe

Думаю, я нашел вескую причину, оправдывающую использование обычного метода .await (). Если задуматься, это ничем не отличается от любого другого способа блокировки.

Пожалуйста, прочтите https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515 еще раз

@ Matthias247 Это очень хорошие моменты, кажется, я их пропустил.

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

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

Это должно быть ясно.

Он должен быть как можно более очевидным как ключевое слово или явно генерировать код с помощью макроса.

Магические методы, черты, члены и т. Д. Слишком легко неправильно истолковать и понять, особенно для новых пользователей.

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

@novacrazy

Магические методы, черты, члены и т. Д. Слишком легко неправильно истолковать и понять, особенно для новых пользователей.

Можете ли вы подробнее рассказать об этом? Точнее, по варианту .await!() , если это возможно.

На мой взгляд, иметь foo.await!() несложно, ОСОБЕННО с правильной подсветкой синтаксиса, которую нельзя игнорировать.

Не понимаете его смысл? По сути, это следующее (игнорируйте тип mystakes):

trait Future {
    fn await!(self) -> Self::Item {
        await self
    }
}

AKA await this_foo и this_foo.await!() в точности равны. Что в этом легко понять неправильно?

А по теме новых пользователей: новые пользователи к чему? Программирование в целом или новые пользователи Rust как языка программирования, но уже имеющие опыт работы с языком программирования? Потому что, если первое, то я сомневаюсь, что они сразу начнут заниматься асинхронным программированием. А если второе, то легче без путаницы объяснить семантику postfix await (как описано выше).

В качестве альтернативы, если добавлен только префикс await, ничто из того, что я знаю, не останавливает создание программы, которая принимает в качестве входных данных код Rust формы

foo.bar().baz().quux().await!().melo().await!()

и превращает его в

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() подходит для некоторых случаев и, вероятно, будет работать вместе с await!(...) например:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

Однако макросы постфиксных методов сейчас не существуют и могут никогда не существовать.

Если мы думаем, что это возможно в будущем, мы должны использовать макрос await!(...) на данный момент и просто добавить постфикс в будущем, когда это будет реализовано.

Было бы идеально иметь оба макроса, но если нет намерения внедрять постфиксные макросы в будущем, префиксное ключевое слово await , вероятно, будет лучшим выбором.

@novacrazy Я согласен с этим, и это мое первоначальное предложение. Сейчас нам нужно добавить await!() и по ходу разобраться в форме постфикса. Потенциально также обсудите возможность постфиксных макросов, и не могли бы мы специально добавить постфиксный .await!() в язык до того, как мы получим полную поддержку постфиксных макросов в языке. Вроде как то, что произошло с ? и трейтом Try : оно было добавлено сначала как особый случай, а затем оно было расширено до более общего случая. Единственное, на что нам нужно быть осторожным при принятии решения, это как будет выглядеть общий синтаксис макроса постфикса, который, возможно, заслуживает отдельного обсуждения.

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

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

  • Вот как это делают другие языки

    • Это приятно, но мы уже делаем вещи по-другому, например, not-running-except- poll ed вместо running-up-to-the-first- await , потому что ржавчина - это принципиально другое язык

  • Людям нравится видеть await в начале строки

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

    • await s в foo(aFuture.await, bFuture.await) так же легко увидеть, как если бы они были префиксом

    • В ржавчине уже сканируется _end_ строки на предмет ? если вы посмотрите на поток управления

Я что-то пропустил?

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

@scottmcm Да, «да, они все примерно одинаковы». По крайней мере, с точки зрения пользователя.

Таким образом, мы должны найти достойный баланс удобочитаемости, понятности, удобства обслуживания и производительности. Следовательно, мое утверждение в моем последнем комментарии остается верным, с макросом await!() если мы собираемся добавить макросы метода постфикса ( .await!() ) в будущем, или просто скучным префиксным ключевым словом await противном случае.

Я говорю скучно, потому что скучно - это хорошо. Мы хотим отвлечься от самого синтаксиса при написании кода с их помощью.

Если f.await() не подходит, я предпочитаю синтаксис префикса.

  1. Как пользователь, я надеюсь, что язык, который я использую, имеет только несколько синтаксических правил, и, используя эти правила, я могу надежно сделать вывод, что он может делать. НЕ исключения здесь и там. В Rust async в начале, await в начале не будет исключением. Однако f await , форма ключевого слова сообщения будет. f.await похоже на доступ к полю, исключение . f.await!() имеет макрос постфикса, который никогда не появлялся в языке, и не знаю, для каких других случаев он был бы хорош, исключение . У нас нет ответа, как эти синтаксисы станут правилами, а не единовременными исключениями .

  2. Когда делается исключение, я надеюсь, что это имеет интуитивный смысл. В качестве примера возьмем ? , которое можно рассматривать как исключение, поскольку не часто встречается в других языках. f()?.map() читается почти как compute f (), и хороший ли этот результат? ? здесь объясняет себя. Но для f await я спрашиваю, почему это постфикс ?, для f.await я спрашиваю, await поле, f.await!() Я спрашиваю, почему макрос появляется в этой позиции? Они не дают мне убедительного / интуитивного ощущения, по крайней мере, на первый взгляд.

  3. Расширяя первый пункт, Rust больше всего хотел бы быть системным языком. Все основные игроки, C / C ++ / Go / Java, в некоторой степени необходимы. Я также считаю, что большинство ребят начинают свою карьеру с императивного языка, C / Python / Java, а не с Haskell и т. Д. Я полагаю, что для того, чтобы убедить разработчиков систем и разработчиков следующего поколения принять Rust, Rust должен сначала преуспеть в императивном стиле, а затем в функциональном, а не быть очень функциональным, но не иметь привычного ощущения повеления.

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

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

// syntax below is exactly the same as with prefix `await`
let response = go client.get("https://my_api").send();
let body: MyResponse = go response.into_json();

На первом этапе мы реализуем его как обычный префиксный оператор без каких-либо ошибок, обрабатывающих определенные надстройки:

// code below don't compiles because `?` takes precedence over `go`
let response = go client.get("https://my_api").send()?;
let body: MyResponse = go response.into_json()?;

На втором этапе мы реализуем синтаксис оператора отложенного префикса, который также позволит правильно обрабатывать ошибки.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

Это все.


Теперь давайте посмотрим на несколько дополнительных примеров, которые дают больше контекста:


// A
if db.go is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .go send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .go send()?
    .error_for_status()?
    .go json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .go send()?
    .error_for_status()?
    .go json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.go request(url, Method::GET, None, true)?
        .res.go json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   go self.logger.log("beginning service call");
   let output = go service.exec();
   go self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = go acquire_lock();
    let length = logger.go log_into(message)?;
    go logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    go (go partial_computation()).unwrap_or_else(or_recover);
}

/// J
let res = client.get("https://my_api").go send()?.go json()?;

Под спойлером скрыта версия с включенной подсветкой синтаксиса


// A
if db.as is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.as load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .as send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .as send()?
    .error_for_status()?
    .as json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .as send()?
    .error_for_status()?
    .as json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.as request(url, Method::GET, None, true)?
        .res.as json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   as self.logger.log("beginning service call");
   let output = as service.exec();
   as self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = as acquire_lock();
    let length = logger.as log_into(message)?;
    as logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    as (as partial_computation()).unwrap_or_else(or_recover);
}

/// J
let res = client.get("https://my_api").as send()?.as json()?;


IMO этот синтаксис лучше, чем альтернативы во всех аспектах:

✓ Последовательность: выглядит очень органично внутри кода Rust и не требует нарушения стиля кода.
✓ Возможность компоновки: хорошо интегрируется с другими функциями Rust, такими как создание цепочек и обработка ошибок.
✓ Простота: краткая, описательная, легкая для понимания и с которой легко работать.
✓ Возможность повторного использования: синтаксис оператора отложенного префикса также может быть полезен в других контекстах.
✓ Документация: подразумевает, что отправка на стороне вызова куда-то течет и ждет, пока она не вернется
✓ Знакомство: предоставляет уже знакомый шаблон, но делает это с меньшими компромиссами
✓ Читаемость: читается как обычный английский и не искажает значение слов
✓ Видимость: его положение очень сложно замаскировать где-нибудь в коде
✓ Доступность: можно очень легко погуглить
✓ Хорошо протестирован: сейчас популярен голанг с похожим синтаксисом
✓ Сюрпризы: этот синтаксис трудно понять неправильно или злоупотребить.
✓ Удовлетворение: после небольшого количества обучения каждый пользователь будет доволен результатом


Изменить: как @ivandardi указал в комментарии ниже, следует уточнить некоторые вещи:

1. Да, этот синтаксис немного сложно разобрать на глаз, но в то же время здесь невозможно изобрести синтаксис, который не имел бы проблем с читабельностью. Синтаксис go мне кажется здесь менее злым, так как в позиции префикса он точно такой же, как и с префиксом await , а в отложенной позиции IMO более читабелен, чем постфикс await например:

match db.go load(message.key) await {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

где not await выглядит неоднозначно и имеет другую ассоциативность, чем все существующие ключевые слова, но также может стать блокирующим, если мы решим реализовать блоки await когда-нибудь в будущем.

2. Потребуется создание RFC, реализация и стабилизация синтаксиса «оператора отложенного префикса». Этот синтаксис не является специфическим для асинхронного кода, однако его использование в асинхронном режиме будет для него основной мотивацией.

3. В синтаксисе «оператор отложенного префикса» ключевое слово имеет значение, потому что:

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

В любом случае, вполне возможно начать использовать await вместо go а затем переименовать его в редакции 2022 года. К тому времени весьма вероятно, что будет изобретено другое ключевое слово или оператор. И мы даже можем решить, что переименование вообще не требуется. Я обнаружил, что await тоже можно читать.

Под спойлером скрыта версия с await вместо go


// A
if db.await is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.await load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .await send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .await send()?
    .error_for_status()?
    .await json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .await send()?
    .error_for_status()?
    .await json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.await request(url, Method::GET, None, true)?
        .res.await json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   await self.logger.log("beginning service call");
   let output = await service.exec();
   await self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = await acquire_lock();
    let length = logger.await log_into(message)?;
    await logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    await (as partial_computation()).unwrap_or_else(or_recover);
}

/// J
let res = client.get("https://my_api").await send()?.await json()?;


Если вы здесь, чтобы проголосовать против, убедитесь:

  • что вы правильно поняли, так как я, возможно, не лучший оратор для объяснения новых вещей
  • что ваше мнение непредвзято, так как есть много предрассудков, откуда этот синтаксис происходит
  • что вы поставите действительную причину ниже, так как молчаливые downvotes чрезвычайно токсичны
  • что ваша причина не в непосредственных ощущениях, поскольку я пытался разработать здесь долгосрочную функцию

@ I60R

У меня есть несколько претензий к этому.

match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

Это немного сложно разобрать глазами. На первый взгляд можно подумать, что мы соответствуем по db.go , но тут есть кое-что еще, и вы говорите: «Ооо, хорошо, load - это метод db». ИМО, этот синтаксис в первую очередь не будет работать, потому что методы всегда должны быть близки к объекту, которому они принадлежат, без пробелов и прерывания ключевого слова между ними.

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

В-третьих, я не думаю, что ключевое слово вообще имеет значение. Однако мы должны отдать предпочтение await поскольку ключевое слово зарезервировано для выпуска 2018 года, тогда как ключевое слово go не имеет и должно пройти более глубокий поиск в crates.io перед тем, как быть предложенным.

Увидев, как обсуждение непрерывно движется из одного направления в другое (префикс против постфикса), я пришел к твердому убеждению, что с ключевым словом await что-то не так, и мы должны полностью отказаться от него.

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

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

let mut res: Response = await { client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()?
    .error_for_status()?
    .json()?
};

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

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

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

Кроме того, неявное ожидание, показанное в Future , которые, как я уже показал, по-прежнему невероятно мощны. Мы не можем просто взять Future и ждать, это было бы крайне неэффективно.

Явные, знакомые, удобочитаемые, поддерживаемые и эффективные: ключевое слово префикса await или макрос await!() с потенциально постфиксным макросом .await!() в будущем.

@novacrazy Даже если бы был выбран идеальный синтаксис, я все равно хотел бы использовать собственные фьючерсы и комбинаторы в некоторых ситуациях. Мысль о том, что это может быть мягко устаревшим, заставляет меня сомневаться в направлении Rust в целом.

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

Это та же ситуация, что и ? : новый код обычно будет использовать ? (потому что он лучше, чем комбинаторы), но комбинаторы Option / Result все еще можно использовать (и более забавные, например or_else до сих пор регулярно используются).

В частности, async / await может полностью заменить map и and_then , но более забавные комбинаторы Future могут (и будут) по-прежнему использоваться.

Приведенные вами примеры по-прежнему выглядят ужасно по сравнению с комбинаторами,

Я так не думаю, я думаю, что они намного понятнее, потому что используют стандартные функции Rust.

Ясность редко зависит от количества символов, но она имеет прямое отношение к мысленным концепциям.

С постфиксом await это выглядит еще лучше:

some_op().await?
    .another_op().await?
    .final_op().await

Вы можете сравнить это с вашим оригиналом:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

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

Почему вы утверждаете, что async / await приведет к дополнительным накладным расходам? И генераторы, и async / await специально разработаны с учетом нулевой стоимости и значительной оптимизации.

В частности, async / await компилируется в сильно оптимизированный конечный автомат, точно так же, как комбинаторы Future.

вместо того, чтобы создавать под капотом эти спагетти-генераторы.

Комбинаторы Future также создают «машину состояний спагетти» под капотом, для чего они и были созданы.

Они принципиально не отличаются от async / await.

[Будущие комбинаторы] могут компилироваться в более эффективный код.

Пожалуйста, прекратите распространять дезинформацию. В частности, async / await может быть быстрее, чем комбинаторы Future.

Если окажется, что пользователям не нравится ключевое слово префикса, его можно изменить в версии 2019/2020.

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

И, как заметил @Centril , создание плохого синтаксиса, чтобы заменить его позже, - очень неэффективный способ делать что-то.

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

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

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

@mehcode предоставил нам обширный реальный код с await в позициях префикса и постфикса: https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

@Centril убедительно доказывал, что стабилизация await!(expr) означает сознательное добавление технического долга: https://github.com/rust-lang/rust/issues/57640#issuecomment -455806584

@valff напомнил нам, что синтаксис ключевого слова expr await postfix наиболее точно соответствует описанию обобщенного типа : https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@quodlibetor подчеркнул, что синтаксический эффект комбинаторов Error и Option уникален для Rust, и выступает в пользу постфиксного синтаксиса: https://github.com/rust-lang/rust/issues/57640#issuecomment -456143523

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

  • @Centril выразил поддержку постфиксного синтаксиса и исследовал ряд вариаций этого билета. Centril особо не хочет стабилизировать await!(expr) .

  • @cramertj выразил поддержку синтаксиса постфикса и, в частности, синтаксиса ключевого слова expr await postfix.

  • @joshtriplett выразил поддержку постфиксного синтаксиса и предложил также предоставить префиксную версию.

  • @scottmcm выразил поддержку постфиксного синтаксиса.

  • @withoutboats не хочет стабилизировать синтаксис макроса. Беспокоясь об исчерпании нашего «бюджета на незнакомство», без лодки считает, что синтаксис ключевого слова expr await postfix имеет «реальный шанс».

  • @aturon , @eddyb , @nikomatsakis и @pnkfelix еще не выразили никакой позиции относительно синтаксиса в выпусках № 57640 или № 50547.

Итак, на что нам нужно ответить да / нет:

  • Нужен ли нам префиксный синтаксис?
  • Нужен ли постфиксный синтаксис?
  • Должны ли мы иметь префиксный синтаксис сейчас и разобраться в постфиксном синтаксисе позже?

Если мы пойдем с синтаксисом префикса, есть два основных соперника, которые, как мне кажется, люди больше всего принимают: полезный и очевидный, как видно из этого комментария . Аргументы против await!() довольно сильны, поэтому я считаю, что это исключено. Итак, нам нужно выяснить, нужен ли нам синтаксис: полезный или очевидный. На мой взгляд, мы должны использовать тот синтаксис, в котором используется скобка меньше.

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

И, наконец, если мы будем использовать и префикс, и постфикс, нам нужно выяснить и записать семантику обоих синтаксисов, чтобы увидеть, как они взаимодействуют друг с другом и как один будет преобразован в другой. Это должно быть возможно, поскольку выбор между postfix или prefix await не должен влиять на выполнение кода.

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

  1. await должно оставаться ключевым словом для будущего языкового дизайна.
  2. Синтаксис должен быть первоклассным.
  3. Ожидание должно быть последовательным, чтобы хорошо сочетаться с ? и методами в целом, поскольку они широко распространены в Rust. Не следует принуждать к созданию временных привязок let которые могут не быть значимыми подразделениями.
  4. За очки ожидания должно быть легко получить grep .
  5. Должна быть возможность сразу увидеть точки ожидания.
  6. Приоритет синтаксиса должен быть интуитивно понятным.
  7. Синтаксис должен хорошо сочетаться с IDE и мышечной памятью.
  8. Синтаксис должен быть простым для изучения.
  9. Синтаксис должен быть эргономичным для написания.

Определив (и, вероятно, забыв ...) некоторые из моих целей, вот краткое изложение и моя оценка некоторых предложений в отношении них:

  1. Сохранение await в качестве ключевого слова затрудняет использование синтаксиса на основе макросов, будь то await!(expr) или expr.await!() . Чтобы использовать синтаксис макроса, await в качестве макроса либо становится жестко закодированным и не интегрируется с разрешением имен (т.е. use core::await as foo; становится невозможным), либо await отказывается от ключевого слова.

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

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

  4. Grepping проще всего, когда используется ключевое слово await будь то постфикс, префикс, макрос и т. Д. Когда используется синтаксис на основе сигил, например, # , @ , ~ , поиск становится сложнее. Разница невелика, но .await немного легче найти, чем await и await поскольку любой из них может быть включен в качестве слов в комментарии, тогда как это маловероятно для .await .

  5. Сигилы, вероятно, труднее обнаружить с первого взгляда, тогда как await легче увидеть и особенно выделен синтаксис. Было высказано предположение, что .await или другой синтаксис постфикса труднее определить с первого взгляда или что постфиксный await плохо читается. Однако, на мой взгляд, @scottmcm справедливо отмечает, что фьючерсы на результаты являются обычным явлением и что .await? помогает привлечь к себе дополнительное внимание. Более того, Скотт отмечает, что префикс await ведет к предложениям "садовой дорожки", и этот префикс await в середине выражений не более читабелен, чем postfix. С появлением оператора ? программистам на Rust уже нужно сканировать конец строк в поисках потока управления .

  6. Приоритет .await и .await() примечателен тем, что он полностью предсказуем; они работают как их аналоги доступа к полю и вызова методов. Предположительно, постфиксный макрос будет иметь тот же приоритет, что и вызов метода. Между тем, префикс await имеет либо последовательный, предсказуемый и бесполезный приоритет по отношению к ? (т.е. await (expr?) ), либо приоритет непоследователен и полезен (например, (await expr)? ). Сигилы можно дать желаемый приоритет (например, взяв ее интуицию из ? ).

  7. В 2012 году @nikomatsakis отметил, что Саймон Пейтон Джонс однажды заметил (стр. 56), что он завидует «силе точки» и тому, как она обеспечивает волшебство IDE, с помощью которого вы можете сузить функцию или поле, которые вы имеете в виду. Поскольку сила точки присутствует во многих популярных языках (например, Java, C #, C ++, ..), это привело к тому, что «стремление к точке» укоренилось в мышечной памяти. Чтобы проиллюстрировать, насколько сильна эта привычка, вот скриншот Visual Studio с Re # er, созданный при помощи
    Re#er intellisense

    В C # нет postfix await. Тем не менее, это настолько полезно, что отображается в списке автозаполнения без допустимого синтаксиса. Однако многим, включая меня, может не прийти в голову попробовать .aw когда .await не является поверхностным синтаксисом. Рассмотрите преимущества опыта IDE, если бы это было так. Синтаксисы await expr и expr await не дают такого преимущества.

  8. Сигилы, вероятно, будут плохо знакомы. Префикс await имеет преимущество от знакомства с C #, JS и т.д. Однако они не разделяют await и ? на отдельные операции, в отличие от Rust. Переход от await expr к expr.await также не является натяжкой - это изменение не так радикально, как использование сигилы. Более того, из-за вышеупомянутой точки, вполне вероятно, что .await будет изучено, набрав expr. и увидев await в качестве первой опции во всплывающем окне автозаполнения.

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

Поскольку ни один синтаксис не является лучшим для одновременного достижения всех целей, необходимо пойти на компромисс. Для меня синтаксис с наибольшими преимуществами и наименьшими недостатками - .await . Примечательно, что он может быть связан в цепочку, сохраняет await в качестве ключевого слова, является greppable, имеет степень точки и, следовательно, является обучаемым и эргономичным, имеет очевидный приоритет и, наконец, читается (особенно с хорошим форматированием и выделением).

@Centril Просто чтобы прояснить несколько вопросов. Во-первых, я просто хочу подтвердить, что вам нужен только postfix await, верно? Во-вторых, как подойти к двойственности того, что .await является доступом к полю? Было бы нормально иметь .await качестве этого, или следует отдать предпочтение .await() со скобками, чтобы подразумевать, что там происходит какая-то операция? И в-третьих, с синтаксисом .await , будет ли это await ключевым словом или просто идентификатором "доступа к полю"?

Во-первых, я просто хочу подтвердить, что вам нужен только postfix await, верно?

Ага. По крайней мере, прямо сейчас.

Во-вторых, как подойти к двойственности того, что .await является доступом к полю?

Это небольшой недостаток; В силе точки есть большой плюс. Я верю, что пользователи быстро научатся различать это различие, особенно потому, что оно выделено ключевым словом и конструкция будет часто использоваться. Более того, как заметил Скотт, .await? будет наиболее распространенным явлением, что должно еще больше облегчить ситуацию. Также должно быть легко добавить новую запись ключевого слова для await в rustdoc так же, как мы сделали, скажем, для fn .

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

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

И, в-третьих, с синтаксисом .await , будет ли это await ключевым словом или просто идентификатором "доступа к полю"?

await останется ключевым словом. Предположительно, вы бы изменили libsyntax таким образом, чтобы не возникала ошибка при обнаружении await после . а затем представляли его по-другому либо в AST, либо при понижении до HIR ... но это в основном детали реализации .

Спасибо, что все прояснили! 👍

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

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

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

Учитывая это, похоже, нам нужно услышать от @withoutboats , который, несомненно, внимательно следил за этой

Упс, только что заметил, что @Centril уже прокомментировал, поэтому я

@ivandardi Учитывая цель (1) из сообщения, я понимаю, что await все равно будет ключевым словом, поэтому никогда не будет доступом к полю, точно так же, как loop {} никогда не является выражением структурного литерала. И я ожидал бы, что это будет выделено, чтобы сделать более очевидным (например, https://github.com/rust-lang/rust-enhanced/issues/333 ждет своей очереди в Sublime).

Единственное, что меня беспокоит, так это то, что создание синтаксиса await, похожего на другой доступ, может потенциально вызвать путаницу при использовании кодовой базы версии 2015, не осознавая. (2015 - это значение по умолчанию, если не указано иное, открытие чужого проекта и т. Д.) Пока в выпуске 2015 года есть явная ошибка, когда нет поля await (и предупреждения, если есть), я думаю это (вместе с замечательным аргументом Centril) устраняет мои личные опасения по поводу поля (или метода) ключевого слова.

О, и еще одна вещь, которую мы также должны решить, пока мы находимся на этом, - это то, как rustfmt будет его форматировать.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Использует await - проверьте
  2. Первый класс - проверить
  3. Цепочка - проверка
  4. Grepping - проверить
  5. Без сигил - проверить
  6. Приоритет - ¹проверьте
  7. Сила точки - ²check
  8. Легко учиться - ³check
  9. Эргономичность - проверьте

¹Приоритет: пробел после await делает его неочевидным в отложенной позиции. Однако это точно так же, как в следующем коде: client.get("https://my_api").await_send()?.await_json()? . Для всех англоговорящих это даже естественнее, чем со всеми другими предложениями

²Сила точек: потребуется дополнительная поддержка для IDE, чтобы переместить await слева от вызова метода после того, как будут набраны . , ? или ; . Не кажется слишком сложным для реализации

«Легко освоить: позиция префикса уже знакома программистам. В отложенном положении после стабилизации синтаксиса станет очевидно


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

  • Его нельзя спутать с доступом к собственности
  • Он имеет очевидный приоритет с ?
  • Он всегда будет иметь одинаковое форматирование
  • Соответствует человеческому языку
  • Это не влияет на появление переменной по горизонтали в цепочке вызовов методов *

* объяснение скрыто под спойлером


С вариантом постфикса трудно предсказать, какая функция будет async и где будет следующее вхождение await на горизонтальной оси:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

С отложенным вариантом почти всегда будет так же:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

Не было бы. между ожиданием и следующим вызовом метода? подобно
get().await?.json() .

В среду, 23 января 2019 г., 05:06 I60R < [email protected] написал:

let val = ждать будущего;
let res = client.get ("https: // my_api") .await send () ?. await json () ?;

  1. Использует ожидание - проверьте
  2. Первый класс - проверить
  3. Цепочка - проверка
  4. Grepping - проверить
  5. Без сигил - проверить
  6. Приоритет - ¹проверьте
  7. Сила точки - ²check
  8. Легко учиться - ³check
  9. Эргономичность - проверьте

¹Приоритет: пробел делает это неочевидным в отложенной позиции. Однако это
точно так же, как в следующем коде: client.get ("https: // my_api
") .await_send () ?. await_json () ?. Для всех англоговорящих это даже больше
естественно, чем со всеми другими предложениями

² Точечная мощность: для перехода на await в среду IDE потребуется дополнительная поддержка.
слева от вызова метода после. или же ? набирается следующим. Не кажется слишком
сложно реализовать

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

стабилизированный

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

  • Его нельзя спутать с доступом к собственности
  • У него очевидный приоритет с?
  • Он всегда будет иметь одинаковое форматирование
  • Соответствует человеческому языку
  • Это не влияет на появление переменной по горизонтали при вызове метода
    цепь *

* объяснение скрыто под спойлером

При использовании постфиксного варианта трудно предсказать, какая функция будет асинхронной, а какая -
где будет следующее вхождение await на горизонтальной оси:

let res = client.get ("https: // my_api")

.very_long_method_name(param, param, param).await?

.short().await?;

С отложенным вариантом почти всегда будет так же:

let res = client.get ("https: // my_api")

.await very_long_method_name(param, param, param)?

.await short()?;

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

мы также должны решить, пока мы находимся на этом, как rustfmt будет его форматировать

Это домен https://github.com/rust-dev-tools/fmt-rfcs , поэтому я бы сказал, что это не по теме данной проблемы. Я уверен, что будет что-то, что согласуется с выбранными исправлениями и приоритетами.

@ I60R

let res = client.get("https://my_api").await send()?.await json()?;

Как бы это выглядело для бесплатных функций? Если я правильно прочитал, на самом деле это префикс await .

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

Однако многим, включая меня, может не прийти в голову попробовать .aw когда .await не является поверхностным синтаксисом.

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

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

@lnicola Я просто хочу указать на тот случай, если вы и другие пропустили это (его легко пропустить в потоке комментариев): https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

Несколько реальных примеров из реального производственного кода.

Учитывая @Centril «s комментарий выше , кажется , варианты Постфиксные в сильнейшей конкуренции являются expr await постфикса ключевых слов и синтаксис expr.await постфикса поле синтаксис (и , возможно , постфикс синтаксис метод не из еще).

По сравнению с ключевым словом postfix, @Centril утверждает, что поле postfix выигрывает от "силы точки" - что IDE могут лучше предлагать его как автозаполнение - и что примеры его использования легче найти с помощью grep, потому что ведущая точка обеспечивает устранение неоднозначности использования этого слова в комментариях.

С другой стороны , @cramertj уже утверждал в пользу постфикс ключевого слова синтаксиса на том основании , что такой синтаксис становится ясно , что это не вызов метода или доступ к полю. @withoutboats утверждает, что ключевое слово postfix является наиболее знакомым из вариантов постфикса.

Что касается «силы точки», мы должны учитывать, что IDE все еще может предлагать await в качестве завершения после точки, а затем просто удалять точку, когда завершение выбрано. Достаточно умная IDE (например, с RLS) могла бы даже заметить будущее и предложить await после пробела.

Параметры поля и метода постфикса, кажется, предлагают большую поверхность для возражений из-за двусмысленности по сравнению с ключевым словом постфикс. Поскольку некоторая поддержка поля / метода postfix вместо ключевого слова postfix, по-видимому, происходит из-за небольшого беспокойства по поводу наличия пробела в цепочке методов, мы должны рассмотреть и принять наблюдении @valff о том, что ключевое слово postfix ( foo.bar() await ) будет выглядеть не более удивительно, чем приписывание обобщенного типа ( foo.bar() : Result<Vec<_>, _> ). Для обоих цепочка продолжается после этой разделенной пробелами интерлюдии.

@ivandardi , @lnicola ,

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

В интересах обсуждения future.await? против future await? ...

То, что особо не обсуждается, - это визуальная группировка или объединение цепочки методов.

рассмотреть ( match вместо await для выделения синтаксиса)

post(url).multipart(form).send().match?.error_for_status()?.json().match?

по сравнению с

post(url).multipart(form).send() match?.error_for_status()?.json() match?

Когда я визуально просматриваю первую цепочку на предмет await я четко и быстро идентифицирую send().await? и вижу, что мы ожидаем результата send() .

Однако, когда я визуально просматриваю вторую цепочку на предмет await , я сначала вижу await?.error_for_status() и мне нужно идти, нет, резервное копирование, а затем соединять send() await вместе.


Да, некоторые из тех же аргументов применимы для определения обобщенного типа, но это еще не принято. Хотя мне нравится приписывание типов в некотором смысле, я чувствую, что в контексте _общего_ выражения (я знаю, расплывчато) его следует заключать в скобки. Однако это все не по теме нашего обсуждения. Также имейте в виду, что сумма await в базе кода сильно отличается от того, что она значительно превышает количество приписываемых типов.

Общее описание типа также связывает тип с приписываемым выражением с помощью отдельного оператора : . Как и в случае с другим бинарным оператором, его чтение позволяет (визуально) понять, что два выражения представляют собой единое дерево. В то время как future await не имеет оператора и может быть легко ошибочно принят за future_await из-за привычки редко видеть два имени, не разделенных оператором, за исключением случаев, когда первое является ключевым словом (применяются исключения, это не для скажите, что я предпочитаю синтаксис префикса, я нет).

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

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

Я не думаю, что «так быстро» и «дискредитировать» здесь справедливы. Я думаю, здесь происходит то же самое, что и в самом async / await RFC: тщательно обдумывая, почему это было сделано одним способом, и выясняя, получается ли баланс таким же образом для нас. Команда C # даже прямо упоминается в одном:

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

let future = task()
но
let await result = task()

поэтому для связывания вы должны сделать либо

task().chained_method(|future| { /* do something with future */ })

но

task().chained_method(|await result| { /* I've got the result */ })
- foo.await             // NOT a real field
- foo.await()           // NOT a real method
- foo.await!()          // NOT a real macro

Все они хорошо работают с цепочкой, и у всех есть минусы, которые не являются настоящими полями / методами / макросами.
Но поскольку await как ключевое слово уже является особенным, нам не нужно делать его более особенным.
Нам нужно просто выбрать самое простое, foo.await . И () и !() здесь избыточны.

@liigo

- foo await        // IS neither field/method/macro, 
                   // and clearly seen as awaited thing. May be easily chained. 
                   // Allow you to easily spot all async spots.

@mehcode

Когда я визуально просматриваю первую цепочку в поисках await, я четко и быстро определяю send (). Await? и видим, что мы ждем результата send ().

Мне больше нравится версия с интервалом, так как намного легче увидеть, где строится будущее, а где его ждут. Гораздо проще увидеть, что происходит, ИМХО

image

С разделением точек это очень похоже на вызов метода. Подсветка кода не очень поможет и не всегда доступна.

Наконец, я действительно считаю, что цепочка не является основным вариантом использования (за исключением ? , но foo await? максимально ясна), и с одиночным ожиданием она становится

post(url).multipart(form).send().match?

против

post(url).multipart(form).send() match?

Где последний выглядит намного стройнее, имо.

Итак, если мы сузились до future await и future.await , можем ли мы просто сделать волшебную «точку» необязательной? Чтобы люди могли выбрать, что им нужно, а главное, прекратить споры и двигаться вперед!

Наличие необязательного синтаксиса не вредно, таких примеров в Rust очень много. наиболее известный из них - последний sperator ( ; или , или что-то еще после последнего элемента, как в (A,B,) ) в большинстве случаев необязательны. Я не видел веской причины, почему мы не можем сделать точку необязательной.

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

Затем, прежде чем перейти в Stable, мы проверяем использование сообщества и решаем, выбрать ли мы одно или просто оставить оба.

Я полностью не согласен с мнением, что макросы недостаточно первоклассные для этой функции (я бы не стал считать это такой основной функцией), и хотел бы еще раз выделить свой предыдущий комментарий о await!() (независимо от того, префикс или постфикс) не является "настоящим макросом", но я думаю, в конце концов, слишком многие люди хотят, чтобы эта функция была стабилизирована как можно скорее, вместо того, чтобы блокировать это в макросах postfix, и prefix await действительно не является хорошо подходит для Rust.

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

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

Нет причин использовать синтаксис с . для лучшей интеграции IDE, так как мы могли бы расширить возможности любого другого синтаксиса с помощью . завершения, используя что-то вроде функции завершения постфикса, доступной в Intellij IDEA.

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

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        await?.res.json::<UserResponse>()
        await?.user
        .into();
    Ok(user)
}

Еще примеры

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    await?.error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()
    await?.error_for_status()?
    .json()
    await?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
    await?.error_for_status()?
    .json()
    await?;

С подсветкой синтаксиса

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        yield?.res.json::<UserResponse>()
        yield?.user
        .into();
    Ok(user)
}

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    yield? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    yield? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    yield?.error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()
    yield?.error_for_status()?
    .json()
    yield?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
    yield?.error_for_status()?
    .json()
    yield?;


Это также может быть ответом на вопрос @ivandardi о форматировании.

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

Сравните (с match как await для выделения синтаксиса):

post(url)?.multipart(form).send()?.match?.error_for_status()?.json().match?

к

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

Заметили отсутствие ожиданий посередине? Потому что цепочку обычно можно решить за счет лучшего дизайна API. Если send() возвращает настраиваемое будущее с дополнительными методами, скажем, для преобразования сообщений состояния, отличных от 200, в серьезные ошибки, тогда нет необходимости в дополнительных ожиданиях. Прямо сейчас, по крайней мере, в reqwest , кажется, что он работает на месте и создает простой Result а не новое будущее, в чем и заключается проблема.

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

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

Другой возможный способ подумать об этом - рассматривать выражения async / await и фьючерсы как итераторы. Оба они ленивы, могут иметь много состояний и могут завершаться синтаксисом ( for-in для итераторов, await для фьючерсов). Мы бы не предложили расширение синтаксиса для итераторов, чтобы объединить их в регулярные выражения, как мы здесь, верно? Они используют комбинаторы для связывания, и асинхронные операции / фьючерсы должны делать то же самое, только завершаясь один раз. Получается неплохо.

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

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

Async / await - это не удобство. Дело не в том, чтобы избегать комбинаторов.

Речь идет о возможности новых вещей, которые нельзя сделать с помощью комбинаторов.

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

Заметили отсутствие ожиданий посередине? Потому что цепочку обычно можно решить за счет лучшего дизайна API.

В реальном примере async красивой цепочки не получится. Реальный пример:

req.into_body().concat2().and_then(move |chunk| {
    from_slice::<Update>(chunk.as_ref())
        .into_future()
        .map_err(|_| {
            ...
        })
        .and_then(|update| {
            ...
        })
        .and_then(move |(user, file_id, chat_id, message_id)| {
            do_thing(&file_id)
                .and_then(move |file| {
                    if some_cond {
                        Either::A(do_yet_another-thing.and_then(move |bytes| {
                            ...

                            if another_cond {
                                ...
                                Either::A(
                                    do_other_thing()
                                        .then(move |res| {
                                            ...
                                        }),
                                )
                            } else {
                                Either::B(future::ok(()))
                            }
                        }))
                    } else {
                        Either::B(future::ok(()))
                    }
                })
                .map_err(|e| {
                    ...
                })
        })
        // ...and here we unify both paths
        .map(|_| {
            Response::new(Body::empty())
        })
        .or_else(Ok)
})

Комбинаторы здесь не помогают. Вложенность можно убрать, но это непросто (дважды пробовал и не получилось). У вас есть тонны Either:A(Either:A(Either:A(...))) в конце.

OTOH легко решить с помощью async / await .

@Pauan написал это до того, как я закончил свой пост. Да будет так. Не упоминайте комбинаторы, async/await не должно быть похожим. Ничего страшного, если это так, есть более важные вещи, которые следует учитывать.


Голосование против моего комментария не сделает комбинаторы более удобными.

Читая комментарий к описанию типа , я думал, как это будет выглядеть в сочетании с await . Учитывая оговорку, которую некоторые люди имеют с .await или .await() качестве запутанных полей члена / доступа к методам, мне интересно, может ли общий синтаксис «приписывания» быть вариантом (например, «дай мне, что await возвращает ").

Использование yield для await для выделения синтаксиса

let result = connection.fetch("url"):yield?.collect_async():yield?;

В сочетании с обозначением типа, например:

let result = connection.fetch("url") : yield?
    .collect_async() : yield Vec<u64>?;

Достоинства: все равно выглядит красиво (IMHO), синтаксис отличается от доступа к полю / методу.
Недостатки: потенциальные конфликты с описанием типа (?), Теоретически возможно множественное приписывание:

foo():yield:yield

Изменить: мне пришло в голову, что использование двух приписаний было бы более логичным в объединенном примере:

let result = connection.fetch("url") : yield?
    .collect_async() : yield : Vec<u64>?;

@Pauan, было бы неуместно использовать этот пост в блоге. Комбинаторы можно применять вместе с синтаксисом await, чтобы избежать проблем, которые он описывает.

Основная проблема всегда заключалась в том, что порождаемое будущее должно быть 'static , но если вы захватываете переменные по ссылке в этих комбинаторах, вы получаете будущее, которое не было бы 'static . Но ожидание не 'static future не делает асинхронный fn, в котором вы находитесь, не 'static . Итак, вы можете делать такие вещи:

ржавчина
// фрагмент: & [T]

пусть x = await future.and_then (| i | if slice.contains (i) {async_fn (slice)});

Итак, я планировал вообще не комментировать, но я хотел добавить несколько заметок от кого-то, кто не пишет много асинхронного кода, но все равно должен его прочитать:

  • Префикс await заметить намного проще. Это происходит как тогда, когда вы этого ожидаете (внутри async fn), так и снаружи (в выражении некоторого тела макроса).
  • Postfix await являющийся ключевым словом, может хорошо выделяться в цепочках практически без других ключевых слов. Но как только у вас появятся замыкания или что-то подобное с другими управляющими структурами, ваш awaits станет намного менее заметным и его будет легче не заметить.
  • Я нахожу .await как псевдополе очень странным. Это не похоже на другие области, которые имеют для меня значение.
  • Я не уверен, что использование IDE-обнаружения и подсветки синтаксиса - хорошая идея. Когда нет подсветки синтаксиса, имеет значение простота чтения. Кроме того, с учетом недавних дискуссий об усложнении грамматики и проблемах, которые это вызовет для инструментов, может быть не очень хорошей идеей полагаться на то, что IDE / редакторы все исправят.

Я не разделяю мнения о том, что макрос await!() «не выглядит первоклассным». Макросы - это абсолютно первоклассный гражданин в Rust. Они также предназначены не только для «изобретения синтаксиса в пользовательском коде». Многие «встроенные» макросы не определены пользователем и отсутствуют по чисто синтаксическим причинам. format!() дает компилятору возможность статической проверки типов строк формата. include_bytes!() также не определяется пользователем и не существует по чисто синтаксическим причинам, это макрос, потому что он должен быть расширением синтаксиса, поскольку, опять же, он выполняет особые функции внутри компилятора и не может разумно быть написанным любым другим способом, не добавляя его в качестве основной функции языка.

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

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

@ H2CO3 await! макрос хорош во всех отношениях, за исключением двух свойств:

  1. Дополнительные скобки действительно раздражают, когда вы пишете еще одну тысячу ожиданий. Rust удалил фигурные скобки из блоков if/while/... (я полагаю) по той же причине. Это может показаться неважным банкоматом, но это действительно так
  2. Асимметрия с ключевым словом async . Async должен делать что-то большее, чем просто разрешать один макрос в теле метода. В противном случае это может быть просто атрибут #[async] поверх функции. Я понимаю, что этот атрибут не позволит вам использовать его в блоках и т. Д., Но он дает некоторое ожидание встретить ключевое слово await . Это ожидание может быть вызвано опытом работы с другими языками, кто знает, но я твердо верю, что они существуют. Возможно, это не решено, но это необходимо учитывать.

В противном случае это может быть просто атрибут #[async] поверх функции.

Это было давно, и мне жаль, что это прошло. Мы вводим еще одно ключевое слово, тогда как атрибут #[async] работал отлично, и теперь, когда процедурные макросы, подобные атрибутам, стабильны, он даже будет согласован с остальной частью языка как синтаксически, так и семантически, даже если он все еще был известен (и обрабатываются специально) компилятором.

@ H2CO3 какова цель или преимущество использования макроса await с точки зрения пользователя ? Есть ли какие-либо встроенные макросы, которые изменяют поток управления программами под капотом?

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

Не понимаю, почему актуальна вторая часть - разве не может быть встроенного макроса, который делает что-то новое? На самом деле я думаю, что почти каждый встроенный макрос делает что-то «странное», чего нельзя (разумно / эффективно / и т. Д.) Достичь без поддержки компилятора. await!() не будет новым злоумышленником в этом отношении.

Ранее я предлагал неявное ожидание , а совсем недавно предлагал неорганизованное ожидание .

Сообщество, похоже, решительно поддерживает явное ожидание (я бы хотел, чтобы это изменилось, но…). Однако детализация явности - это то, что создает шаблон (то есть необходимость его многократного использования в одном и том же выражении, кажется, создает шаблон).

[ Предупреждение : следующее предложение будет спорным, но, пожалуйста, дайте ему шанс]

Мне любопытно, пойдет ли большая часть сообщества на компромисс и разрешит «частично-неявное ожидание»? Может быть, достаточно однозначного чтения await для каждой цепочки выражений?

await response = client.get("https://my_api").send()?.json()?;

Это что-то вроде деструктурирования ценностей, которое я называю деструктуризацией выражений.

// Value de-structuring
let (a, b, c) = ...;

// De-sugars to:
let _abc = ...;
let a = _abc.0;
let b = _abc.1;
let c = _abc.2;
// Expression de-structuring
await response = client.get("https://my_api").send()?.json()?;

// De-sugards to:
// Using: prefix await with explicit precedence
// Since send() and json() return impl Future but ? does not expect a Future, de-structure the expression between those sub-expressions.
let response = await (client.get("https://my_api").send());
let response = await (response?.json());
let response = response?;



md5-eeacf588eb86592ac280cf8c372ef434



```rust
// If needed, given that the compiler knows these expressions creating a, b are independent,
// each of the expressions would be de-structured independently.
await (a, b) = (
    client.get("https://my_api_a").send()?.json()?,
    client.get("https://my_api_b").send()?.json()?,
);
// De-sugars to:
// Not using await syntax like the other examples since await can't currently let us do concurrent polling.
let a: impl Future = client.get("https://my_api_a").send();
let b: impl Future = client.get("https://my_api_b").send();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;

let a: impl Future = a?.json();
let b: impl Future = b?.json();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;
let (a, b) = (a?, b?);

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

@yazaddaruvala Был предупреждение выше , так что будьте осторожны.

Мне любопытно, пойдет ли большая часть сообщества на компромисс и разрешит «частично-неявное ожидание»? Может быть, достаточно однозначного чтения await для каждой цепочки выражений?

  1. Я не думаю, что большая часть сообщества хочет что-то частичное-неявное. Нативный подход Rust больше похож на «все подробно». Даже приведение u8 -> u32 выполняется явно.
  2. Это не подходит для более сложных сценариев. Что компилятору делать с Vec<Future> ?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Следует ли выполнять await для каждого вложенного вызова? Хорошо, мы, вероятно, можем заставить работать await для одной строки кода (так что вложенные mapFunc не будут ожидать), но это слишком легко сломать такое поведение. Если await работает для одной строки кода, то любой рефакторинг, такой как «извлечь значение», нарушит его.

@yazaddaruvala

Я понимаю, что вы правы в том, что хотите, - это префикс await, который имеет приоритет над ? с добавленной возможностью использования await вместо ключевого слова let для изменения контекста, чтобы разбросать все имплименты Future <_ i = "7"> возвращается с ожиданием?

Эти три утверждения ниже означают, что они эквивалентны:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

И эти два утверждения ниже эквивалентны:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

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

На мой взгляд, префикс await для коротких однострочных строк имеет то преимущество, что он похож как на многие другие языки программирования, так и на многие человеческие языки. Если в конце концов будет решено, что await должен быть только суффиксом, я ожидаю, что иногда буду использовать макрос Await!() (или другую подобную форму, не противоречащую ключевым словам), когда я чувствую, что опираюсь на глубоко укоренившееся знакомство со структурой предложений на английском языке поможет снизить умственные издержки при чтении / написании кода. В суффиксных формах есть определенная ценность для более длинных цепочечных выражений, но моя точка зрения, явно не основанная на объективных фактах, заключается в том, что с человеческой сложностью префикса await, субсидируемой разговорным языком, преимущество простоты наличия только одной формы явно не перевешивают потенциальную ясность кода наличия обоих. Предполагая, что вы доверяете программисту выбрать, по крайней мере, то, что наиболее подходит для текущего фрагмента кода.

@Pzixel

Я не думаю, что большая часть сообщества хочет что-то частичное-неявное. Нативный подход Rust больше похож на «все подробно».

Я так не вижу. Я вижу, что это всегда компромисс между явным и неявным. Например:

  • Типы переменных требуются в области действия функции

    • Типы переменных могут быть неявными в пределах локальной области видимости.

    • Контексты Clousure неявны

    • Учитывая, что мы уже можем рассуждать о локальных переменных.

  • Требуются жизни, чтобы помочь контролеру заемщиков.

    • Но в локальном масштабе их можно сделать вывод.

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

  • Я уверен, что есть еще кое-что.

Использование await только один раз для выражения / оператора со всеми преимуществами цепочки - это очень разумный компромисс между явным и сокращающим шаблонным кодом.

Это не подходит для более сложных сценариев. Что компилятору делать с Vec?

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Я бы сказал, что компилятор должен ошибаться. Существует огромная разница в типах, которую невозможно вывести. Между тем, пример с Vec<impl Future> не решается ни одним из синтаксисов в этом потоке.

// Given that json() returns a Vec<imple Future>,
// do I use .await on the `Vec`? That seems odd.
// Given that the client.get() returns an `impl Future`,
// do I .await inside the .map? That wont work, given Iterator methods are not `async`
client.get("https://my_api").send().await?.json()[UNCLEAR]?.parse_as::<Vec<String>>()?.map(|x| client.get(x)[UNCLEAR])?;

Я бы сказал, что Vec<impl Future> - плохой пример, или мы должны придерживаться одного и того же стандарта для каждого предлагаемого синтаксиса. Между тем, предложенный мною синтаксис «частично-неявное ожидание» работает лучше, чем текущие предложения для более сложных сценариев, таких как await (a, b) = (client.get("a")?, client.get("b")?); с использованием обычного постфикса или префикса ожидания let (a, b) = (client.get("a") await?, client.get("b") await?); приводит к последовательным сетевым операциям, где частичное -неявная версия может быть запущена одновременно компилятором, соответствующим образом десахарируя (как я показал в своем исходном посте).

Еще в 2011 году, когда функция async в C # еще тестировалась, я спросил, является ли await префиксным оператором, и получил ответ от PM по языку C #. Поскольку обсуждается, следует ли в Rust использовать префиксный оператор, я подумал, что было бы полезно опубликовать этот ответ здесь. От Почему 'await' - это префиксный оператор вместо постфиксного? :

Как вы понимаете, синтаксис выражений await был предметом огромного обсуждения, прежде чем мы остановились на том, что сейчас есть :-). Однако мы не особо рассматривали постфиксные операторы. В postfix есть что-то такое (см. Вычислители HP и язык программирования Forth), что делает вещи более простыми в принципе и менее доступными или читаемыми на практике. Может быть, математические обозначения просто промывают нам мозги в детстве ...

Мы определенно обнаружили, что префиксный оператор (с его буквально императивным оттенком - «сделай это!») Был, безусловно, самым интуитивно понятным. Большинство наших унарных операторов уже являются префиксными, и await, похоже, с этим согласуется. Да, он тонет в сложных выражениях, но то же самое происходит с порядком оценки в целом из-за того, что, например, приложение функции также не является постфиксным. Сначала вы оцениваете аргументы (которые находятся справа), а затем вызываете функцию (которая находится слева). Да, есть разница в том, что синтаксис приложения функции в C # поставляется с уже встроенными круглыми скобками, тогда как для await их обычно можно опустить.

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

var bResult = await A().BAsync();
var dResult = await bResult.C().DAsync();
dResult.E()

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

Есть смысл?

Мадс Торгерсен, специалист по языку C #


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

@yazaddaruvala

Я так не вижу. Я вижу, что это всегда компромисс явного и неявного

Это по-прежнему явное. Кстати, могу

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

Почему произошла ошибка? Я счастлив, что у меня есть Vec<impl future> , и я могу присоединиться к нему. Вы предлагаете дополнительные ограничения без причины.

Я бы сказал, что Vec- плохой пример, иначе мы должны придерживаться одного и того же стандарта для каждого предложенного синтаксиса.

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

@chescock Я согласен с вами, но, как было сказано ранее, здесь Rust очень сильно отличается от C #: в C # вы никогда не пишете (await FooAsync()).Bar() . Но в Rust это так. И я говорю о ? . В C # у вас есть неявное исключение, которое распространяется через асинхронные вызовы. Но в ржавчине вы должны быть явными и писать ? в результате выполнения функции после того, как он был ожидаем.

Конечно, вы можете попросить пользователей написать

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

но это намного страннее, чем

let foo = await? bar();
let bar = await? foo.baz();

Это выглядит намного лучше, но требует введения новой комбинации await и ? doint (await expr)? . Более того, если бы у нас были другие операторы, мы могли бы написать await?&@# и все комбинации работали бы вместе ... Выглядит немного сложно.

Но тогда мы можем просто поместить await в качестве постфикса, и он, естественно, будет соответствовать текущему языку:

let foo = bar() await?;
let bar = foo.baz() await?;

теперь await? - это два отдельных токена, но они работают вместе. У вас может быть await?&@# и вы не будете беспокоиться о том, чтобы взломать его в самом компиляторе.

Конечно, это выглядит немного страннее, чем префиксный, но, как говорится, все дело в разнице между Rust и C #. Мы можем использовать опыт C #, чтобы убедиться, что нам нужен явный синтаксис ожидания, который, вероятно, будет иметь отдельное ключевое слово, но мы не должны слепо следовать тому же пути и игнорировать различия Rust / C #.

Я сам долгое время был сторонником префикса await и даже пригласил разработчиков языка C # в эту тему (так что у нас есть информация новее, чем в 2011 году 😄), но теперь я думаю, что постфикс await keyword - лучший подход.

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

Мне пришло в голову, что мы сможем получить лучшее из обоих миров с помощью комбинации:

  • префикс async ключевое слово
  • общая функция макроса постфикса

Все вместе это позволит реализовать постфиксный макрос .await!() без использования магии компилятора.

Прямо сейчас постфиксный макрос .await!() потребует магии компилятора. Однако, если был стабилизирован префиксный вариант ключевого слова await , то это больше не было бы правдой: постфиксный макрос .await!() можно тривиально реализовать как макрос, который ставит префикс своего аргумента (выражения) с ключевым словом await и заключил все в квадратные скобки.

Преимущества такого подхода:

  • Ключевое слово с префиксом await доступно и доступно пользователям, знакомым с этой конструкцией из других языков.
  • Ключевое слово prefix async может использоваться там, где желательно «выделить» асинхронный характер выражения.
  • Был бы вариант постфикса .await!() , который можно было бы использовать в цепочках или любой другой ситуации, когда синтаксис префикса неудобен.
  • Не было бы необходимости в (потенциально неожиданной) магии компилятора для варианта постфикса (поскольку в противном случае это было бы проблемой для поля постфикса, метода или параметров макроса).
  • Макросы Postfix также будут доступны для других ситуаций (например, .or_else!(continue) ), которые, кстати, нуждаются в синтаксисе макроса postfix по аналогичным причинам (в противном случае они требуют, чтобы предыдущее выражение было обернуто неудобным и нечитаемым образом).
  • Ключевое слово префикса await может быть стабилизировано относительно быстро (позволяя экосистеме развиваться) без необходимости ждать реализации постфиксных макросов. Но в среднесрочной и долгосрочной перспективе мы все равно оставим открытой возможность постфиксного синтаксиса для ожидания.

@nicoburns , @ H2CO3 ,

Мы не должны реализовывать операторы потока управления, такие как await!() и .or_else!(continue) как макросы, потому что с точки зрения пользователя для этого нет веских причин. В любом случае, в обеих формах пользователи будут иметь одни и те же функции и должны изучить их обе, однако, если эти функции будут реализованы как макросы, пользователи дополнительно будут беспокоиться о том, почему именно они реализованы таким образом. Это невозможно не заметить, потому что между обычными операторами первого класса и обычными операторами, реализованными в виде макросов, было бы просто причудливое и искусственное различие (поскольку сами по себе макросы первоклассны, а реализуемые ими вещи - нет).

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

Для обоего .await!() и .or_else!(continue) показывает существует надлежащие и эргономических решения с красивыми синтаксисами: await ключевым слова и none -coalescing оператора. Мы должны предпочесть их реализацию, а не что-то общее, редко используемое и некрасиво выглядящее как постфиксные макросы.

Нет причин использовать макросы, поскольку нет чего-то сложного, например format!() , нет чего-то редко используемого, например include_bytes!() , нет настраиваемого DSL, нет удаления дублирования в пользовательском коде, есть не требуется синтаксис, подобный vararg, нет необходимости в том, чтобы что-то выглядело иначе, и мы не можем использовать этот способ только потому, что это возможно.

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

try!() был реализован как макрос. Для этого была прекрасная причина.

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

нет ничего сложного типа format!()

Хочу не согласиться: format!() не о сложностях, а о проверке во время компиляции. Если бы не проверка во время компиляции, это могла бы быть функция.

Кстати, я не предлагал, чтобы это был постфиксный макрос. (Думаю, не должно.)

Для обеих функций существует правильное и эргономичное решение с красивым синтаксисом.

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

(Я сказал все, что мог; я больше не собираюсь отвечать на этот аспект вопроса.)

@nicoburns

Мне пришло в голову, что мы сможем получить лучшее из обоих миров с помощью комбинации:

  • префикс await ключевое слово
  • общая функция макроса постфикса

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

Все вместе это позволит реализовать постфиксный макрос .await!() без использования магии компилятора.

С точки зрения реализации это более сложное решение, чем foo await или foo.await (особенно последнее). Есть еще «магия», ровно столько же; вы только что провели бухгалтерский маневр .

Преимущества такого подхода:

  • Ключевое слово с префиксом await доступно и доступно пользователям, знакомым с этой конструкцией из других языков.

Если мы собираемся добавить .await!() позже, мы просто даем пользователям больше информации для изучения (как await foo и foo.await!() ), и теперь пользователи будут спрашивать, «что мне использовать когда..". Похоже, что это использует больше бюджета сложности, чем foo await или foo.await как это делают отдельные решения.

  • Макросы Postfix также будут доступны для других ситуаций (например, .or_else!(continue) ), которые, кстати, нуждаются в синтаксисе макросов postfix по тем же причинам (в противном случае они требуют, чтобы предыдущее выражение было обернуто неудобным и нечитаемым образом).

Я считаю, что постфиксные макросы имеют ценность и должны быть добавлены в язык. Однако это не означает, что их нужно использовать для await ing; Как вы упомянули, есть .or_else!(continue) и многие другие места, где могут быть полезны постфиксные макросы.

  • Ключевое слово префикса await может быть стабилизировано относительно быстро (позволяя экосистеме развиваться) без необходимости дожидаться реализации постфиксных макросов. Но в среднесрочной и долгосрочной перспективе мы все равно оставим открытой возможность постфиксного синтаксиса для ожидания.

Я не вижу смысла в том, чтобы «оставлять открытые возможности »; Сегодня известно значение постфикса для композиции, опыта работы с IDE и т.д. Меня не интересует «стабилизировать await foo сегодня и надеюсь, что мы сможем достичь консенсуса по foo.await!() завтра».


@ H2CO3

try!() был реализован как макрос. Для этого была прекрасная причина.

try!(..) устарело, что означает, что мы сочли его непригодным для данного языка, в частности потому, что он не был постфиксным. Использование этого в качестве аргумента - помимо того, что нам не следует делать - кажется странным. Более того, try!(..) определяется без какой-либо поддержки со стороны компилятора .

Кстати, я не предлагал, чтобы это был постфиксный макрос. (Думаю, не должно.)

await - это не "каждый .." - в частности, это не второстепенная функция для удобства; скорее, это основная добавляемая языковая функция.

@phaylon

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

Синтаксис foo.await предназначен для уменьшения проблем с инструментарием, поскольку любой редактор с любой видимостью хорошей поддержки Rust уже поймет форму .ident . Редактору нужно просто добавить await в список ключевых слов. Более того, в хороших IDE уже есть автозавершение кода на основе . - поэтому кажется проще расширить RLS (или его эквиваленты ...), чтобы предоставить await в качестве первого предложения, когда пользователь вводит . после my_future .

Что касается усложнения грамматики, .await на самом деле наименее вероятно усложнит грамматику, учитывая, что поддержка .await в синтаксисе, по сути, анализирует .$ident а затем не дает ошибок в ident == keywords::Await.name() .

Синтаксис foo.await предназначен для уменьшения проблем с инструментарием, так как любой редактор, имеющий хоть какое-то подобие хорошей поддержки Rust, уже поймет форму .ident. Редактору нужно просто добавить await в список ключевых слов. Более того, в хороших IDE уже есть автозавершение кода на основе. - так что кажется проще расширить RLS (или его эквиваленты ...), чтобы в качестве первого предложения при вводе пользователем было указано await. после my_future.

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

Что касается усложнения грамматики, то, на самом деле, .await вряд ли усложнит грамматику, учитывая, что поддержка .await в синтаксисе, по сути, анализирует. $ Identify, а затем не приводит к ошибкам в identity == keywords :: Await.name ().

Я считаю, что честь принадлежит await!(future) , так как это уже полностью поддерживается грамматикой.

@Centril try! конечном итоге стал избыточным, потому что оператор ? может делать больше. Это не «непригодный для языка». Однако вам это может не понравиться , и я согласен. Но для меня это на самом деле одна из лучших вещей, изобретенных Rust, и она была одним из аргументов в пользу продаж. И я знаю, что он реализован без поддержки компилятора, но я не понимаю, насколько это актуально при обсуждении того, выполняет ли он поток управления или нет. Так или иначе.

жду не "все до единого .."

Но другие упомянутые здесь (например, or_else ) относятся, и моя точка зрения в любом случае применима к основным функциям. Добавление синтаксиса только ради добавления синтаксиса не является преимуществом, поэтому всякий раз, когда есть что-то, что уже работает в более общем случае, его следует предпочесть изобретению новой нотации. (Я знаю, что другой аргумент против макросов состоит в том, что «они не постфиксные». Я просто не думаю, что преимущества await как собственного постфиксного оператора достаточно высоки, чтобы оправдать затраты. Мы пережили вызовы вложенных функций. Мы будет одинаково хорошо после написания пары вложенных макросов.)

В среду, 23 января 2019 г., в 21:59:36 +0000 Маздак Фаррохзад написал:

  • Макросы Postfix также будут доступны для других ситуаций (например, .or_else!(continue) ), которые, кстати, нуждаются в синтаксисе макросов postfix по тем же причинам (в противном случае они требуют, чтобы предыдущее выражение было обернуто неудобным и нечитаемым образом).

Я считаю, что постфиксные макросы имеют ценность и должны быть добавлены в язык. Однако это не означает, что их нужно использовать для await ing; Как вы упомянули, есть .or_else!(continue) и многие другие места, где могут быть полезны постфиксные макросы.

Основная причина использования .await!() заключается в том, что внешний вид макроса делает
ясно, что это может повлиять на поток управления.

.await выглядит как доступ к полю, .await() выглядит как функция
вызов, и ни доступ к полям, ни вызовы функций не могут повлиять на управление
течь. .await!() выглядит как макрос, и макросы могут влиять на управление
течь.

@joshtriplett Я не как ). С точки зрения выполнения локальной функции выполнение await похоже на вызовы большинства других функций. Вы продолжаете с того места, на котором остановились, и получаете возвращаемое значение в стеке.

Я просто считаю это противоречащим обсуждению грамматики. И проблема не сейчас, а через 5, 10, 20 лет, после пары изданий.

Понятия не имею, что вы имеете в виду под этим.

Я считаю, что честь принадлежит await!(future) , так как это уже полностью поддерживается грамматикой.

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

@Centril try! конечном итоге стал избыточным, потому что оператор ? может делать больше. Это не «непригодный для языка». Возможно, вам это не понравится, и я согласен.

Написание try!(expr) в Rust 2018 явно устарело и, кроме того, является серьезной ошибкой. Мы коллективно решили сделать это так, и поэтому он был признан непригодным.

Основная причина использования .await!() заключается в том, что внешний вид макроса дает понять, что он может повлиять на поток управления.

.await выглядит как доступ к полю, .await() выглядит как вызов функции, и ни доступ к полю, ни вызов функции не могут повлиять на поток управления.

Я считаю, что это различие - это то, чему пользователи научатся относительно легко, тем более, что за .await обычно следует ? (который является потоком управления, локальным для функции). Что касается вызовов функций, и как уже упоминалось выше, я думаю, будет справедливо сказать, что методы итератора являются формой потока управления. Более того, то, что макрос может повлиять на поток управления, не означает, что он это сделает. Многие макросы этого не делают (например, dbg! , format! , ...). Понимание того, что .await!() или .await повлияет на поток управления (хотя в гораздо более слабом смысле, чем ? , согласно примечанию @HeroicKatora ) вытекает из слова await Сам

@Centril

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

Eek. Это немного больно. Возможно, у макроса могло бы быть другое имя, например .wait!() или .awaited!() (последнее довольно неплохо, так как дает понять, что это применимо к предыдущему выражению).

«Что вместе позволило бы реализовать макрос postfix .await! () Без использования магии компилятора».
Это более сложное решение с точки зрения реализации, чем foo await или foo.await (особенно последнее). Есть еще «магия», ровно столько же; вы только что провели бухгалтерский маневр.

Более того, try! (..) определяется без какой-либо поддержки со стороны компилятора.

И если бы у нас было ключевое слово await и постфиксные макросы, то .await!() (возможно, с другим именем) также можно было бы реализовать без поддержки компилятора, верно? Конечно, само ключевое слово await по-прежнему повлечет за собой значительный объем волшебства компилятора, но это будет просто применяться к результату макроса и ничем не отличается от отношения try!() к return ключевое слово.

Если мы собираемся добавить .await! () Позже, мы просто даем пользователям больше информации для изучения (как await foo, так и foo.await! ()), И теперь пользователи будут спрашивать «что мне использовать, когда ...». Похоже, это израсходует больше бюджета сложности, чем foo await или foo.await, как это делали бы отдельные решения.

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

«Есть два способа ожидания будущего в Rust: await foo.bar(); и foo.bar().await!() . Какой использовать стилистические предпочтения, не влияющие на ход выполнения»

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

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

Понятия не имею, что вы имеете в виду под этим.

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

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

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

В среду, 23 января 2019 г., в 14:21 HeroicKatora [email protected]
написал:

@joshtriplett https://github.com/joshtriplett Я не влияет на управление
расход в стандартном понимании. Это основной факт для оправдания того, что
Проверка займов должна работать с фьючерсами, как определено. С точки зрения
выполнение локальной функции, выполнение await аналогично вызову любой другой функции.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

В среду, 23 января 2019 г., в 14:26:07 -0800 Маздак Фаррохзад написал:

Основная причина использования .await!() заключается в том, что внешний вид макроса дает понять, что он может повлиять на поток управления.

.await выглядит как доступ к полю, .await() выглядит как вызов функции, и ни доступ к полю, ни вызов функции не могут повлиять на поток управления.

Я считаю, что это различие - это то, чему пользователи научатся относительно легко.

Зачем им это нужно?

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

Часто да, но не всегда, и синтаксис определенно должен работать в тех случаях, когда это не так.

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

Не так, как return или ? (или даже break ). Было бы очень удивительно, если бы вызов функции мог возвращать на два уровня вверх из функции, которая ее вызвала (одна из причин, по которой в Rust нет исключений, именно потому, что это удивительно), или выход из цикла при вызове функция.

Более того, то, что макрос может повлиять на поток управления, не означает, что он это сделает. Многие макросы этого не делают (например, dbg !, format !, ...). Понимание того, что .await! () Или .await повлияет на поток управления (хотя в гораздо более слабом смысле, чем?, Согласно примечанию @HeroicKatora ), вытекает из самого слова await.

Мне это совсем не нравится. Влияние потока управления - действительно важный побочный эффект. Он должен иметь синтаксическую форму, отличную от конструкций, которые обычно не могут этого сделать. Конструкции, которые могут делать это в Rust: ключевые слова ( return , break , continue ), операторы ( ? ) и макросы (при оценке до одного других форм). Зачем мутить воду, добавляя исключение?

@ejmahler Я не понимаю, чем отличается удержание RefCell от обычного вызова функции. Также может быть, что внутренняя функция хочет получить ту же самую RefCell. Но, похоже, ни у кого не возникнет серьезной проблемы, если не сразу понять полный график потенциальных вызовов. Точно так же проблемы с исчерпанием стека не вызывают особого беспокойства, поскольку вам следует аннотировать предоставленное пространство стека. Здесь сравнение было бы выгодно для сопрограмм! Должны ли мы сделать вызовы обычных функций более заметными, если они не встроены? Нужны ли нам явные хвостовые вызовы для решения этой все более сложной проблемы с библиотеками?

Ответ, imho, самый большой риск при использовании RefCell и await - это незнание. Когда другие вышеупомянутые проблемы можно абстрагироваться от оборудования, я действительно считаю, что мы, как программисты, также можем научиться не держаться за RefCell и т. Д. В точках доходности, кроме тех, где это проверено.

В среду, 23 января 2019 г., в 22:30:10 +0000 Эллиот Малер написал:

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

: +1:

@HeroicKatora

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

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

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

В среду, 23 января 2019 г., в 14:44 Джош Триплетт [email protected]
написал:

В среду, 23 января 2019 г., в 22:30:10 +0000 Эллиот Малер написал:

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

: +1:

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

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

Если вы уроните ключ в SlotMap, у вас будет утечка памяти. Сам по себе язык в этом вам не поможет. Я хочу сказать, что ваши примеры того, как await неопределенно недостаточно видима (пока это ключевое слово, его появление будет уникальным), похоже, не распространяются на проблемы, которые не возникали для других функций. Я думаю, вам следует оценивать await меньше по тому, какие функции язык дает вам сразу, и больше по тому, как он позволяет вам выражать свои собственные функции. Оцените свои инструменты и примените их. Предлагая решение для игрового состояния и то, как это решается достаточно хорошо: напишите обертку, которая утверждает при возврате, что игровое состояние на самом деле отметило требуемую сумму, async позволяет вам заимствовать ваше собственное состояние для этой цели. Запрет других ожидающих в этом изолированном коде.

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

Удержание RefCell мало чем отличается от hodling Mutex при блокировке io. Можно попасть в тупик, который хуже и труднее отлаживать, чем панику. И все же мы не аннотируем операции блокировки явным ключевым словом block . :).

Совместное использование RefCell между асинхронными функциями значительно ограничит их общее удобство использования ( !Send ), поэтому я не думаю, что это будет обычным явлением. И RefCell обычно следует избегать, а при использовании брать взаймы на как можно более короткий срок.

Так что я не думаю, что случайное превышение предела текучести RefCell - это большое дело.

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

Другое дело - сопрограммы и асинхронные функции. Мы не пытаемся сделать yield неявным.

Всем, кто выступает за синтаксис макросов вместо синтаксиса ключевых слов: опишите, что это означает await что означает, что это должен быть макрос, и чем он отличается, скажем, от while , который _could_ просто был макросом while! ( даже с использованием только macro_rules! ; никакого специального регистра вообще).

Изменить: это не риторический характер. Я искренне заинтересован в этом, точно так же, как я думал, что хочу run-to-first-await (как C #), но RFC убедил меня в обратном.

Всем, кто выступает за синтаксис макросов вместо синтаксиса ключевых слов: пожалуйста, опишите, что это значит, что это должен быть макрос, и чем он отличается, скажем, от while, который мог быть только что давно! макрос (даже с использованием только macro_rules!; вообще без специального регистра).

Я в основном согласен с этим рассуждением и хочу использовать ключевое слово для синтаксиса префикса.

Однако, как указано выше (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), я предпочитаю также иметь постфиксный макрос для постфиксного синтаксиса (возможно, .awaited!() чтобы избежать конфликтов имен с ключевым словом prefix). Мое рассуждение отчасти состоит в том, что проще иметь ключевое слово, которое можно использовать только одним способом, и сохранять дополнительные варианты для макросов, но в основном это то, что, насколько я могу видеть, никто не смог придумать постфикс синтаксис ключевых слов, который я считаю приемлемым:

  • foo.bar().await и foo.bar().await() технически будут ключевыми словами, но они выглядят как доступ к полю или вызов метода, и я не думаю, что такую ​​конструкцию изменения потока управления следует скрывать таким образом.
  • foo.bar() await просто сбивает с толку, особенно с дальнейшей цепочкой, такой как foo.bar() await.qux() . Я никогда не видел, чтобы ключевое слово использовалось в качестве такого постфикса (я уверен, что они существуют, но на самом деле я не могу придумать ни одного известного мне примера ключевых слов, которые работают подобным образом). И я думаю, что это очень сбивает с толку, как будто ожидание применяется к qux() не к foo.bar() .
  • foo.bar()@await или другие знаки препинания могут работать. Но это не особо приятно, и кажется довольно специфичным. На самом деле я не думаю, что с этим синтаксисом есть какие-либо серьезные проблемы (в отличие от приведенных выше вариантов). Мне просто кажется, что как только мы начинаем добавлять сигилы, баланс начинает сдвигаться в сторону отказа от пользовательского синтаксиса и превращения его в макрос.

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

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

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

// imaginary generator syntax stolen from JavaScript
fn* my_generator() -> T {
    yield some_value;

    // explicit return statements are only included to 
    // make it clear the generator/async functions are finished.
    return another_value;
}

// `await` keyword would not be allowed here, but the `yield` keyword is
#[async]
fn* my_async_generator() -> Result<T, E> {
    let item = some_op().await!()?; // uses the `.await!()` macro
    // which would really just use `yield` internally, but with the pinning API
    // same as the current nightly macro.

    yield future::ok(item.clone());

    return Ok(item);
}

// `yield` would not be allowed here, but the `await` keyword is.
async fn regular_async() -> Result<T, E> {
   let some_op = async || { /*...*/ };

   let item = await? some_op();

   return Ok(item);
}

Я считаю, что это достаточно прозрачно для чрезвычайно продвинутых пользователей, которые хотят делать забавные вещи, но позволяет новым и умеренным пользователям использовать только то, что необходимо. 2 и 3, если они используются без каких-либо операторов yield , фактически идентичны.

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

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

В идеале async fn и #[async] fn* могли бы даже совместно использовать код реализации для преобразования базового генератора в асинхронный конечный автомат.

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

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

Не знаю, практично ли это в Rust, учитывая гигиенические макросы. Если я вызываю foo!(x.await) или foo!(await { x }) когда он хочет expr , я думаю, должно быть недвусмысленно, что требуется ключевое слово await не await field или литеральное выражение await struct с сокращением поля init - даже в синхронном методе.

позволяет использовать три эквивалентных способа выполнения ожиданий

Пожалуйста, нет. (По крайней мере, в core . Очевидно, что люди могут создавать макросы в своем собственном коде. Если они хотят)

Прямо сейчас для макроса postfix .await! () Потребуется магия компилятора. Однако, если префиксный вариант ключевого слова await был стабилизирован, то это больше не было бы правдой: макрос постфикса .await! () Можно было бы тривиально реализовать как макрос, который перед своим аргументом (выражением) ставил ключевое слово await и помещал его в оболочку. все в скобках.

Замечу, что это так же верно - но проще! - в другом направлении: если мы стабилизируем синтаксис ключевого слова .await , люди уже могут создать макрос префикса awaited!() если им достаточно не нравится постфикс.

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

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

Я знаю, что Вы имеете ввиду. Однако невозможно помешать программистам определять свои собственные макросы, выполняя такие вещи, как await!() . Всегда можно будет построить симуляцию. Так что действительно варианты БУДУТ разные.

Ну, хотя бы не await!(...) поскольку это будет ошибкой; но если пользователь определяет macro_rules! wait { ($e:expr) => { e.await }; } а затем использует это как wait!(expr) тогда он будет выглядеть явно унидиоматичным и, вероятно, быстро выйдет из моды. Это значительно снижает вероятность вариаций в экосистеме и позволяет пользователям изучать меньше стилей. Таким образом, я думаю, что точка зрения @yasammez уместна .

@Centril Если кто-то хочет делать плохие вещи, их вряд ли можно остановить. А как насчет _await!() или awai!() ?

или, если включен идентификатор Unicode, что-то вроде àwait!() .

...

@earthengine Цель состоит в том, чтобы установить нормы сообщества (аналогично тому, что мы делаем со стилями и rustfmt ), а не препятствовать тому, чтобы разные люди намеренно делали странные вещи. Здесь мы имеем дело с вероятностями , а не с абсолютными гарантиями не увидеть _await!() .

Подведем итог и рассмотрим аргументы для каждого синтаксиса постфикса:

  • __ expr await (ключевое слово postfix) __: Синтаксис ключевого слова Postfix использует ключевое слово await мы уже зарезервировали. Await - это волшебное преобразование, и использование ключевого слова помогает выделиться соответствующим образом. Это не похоже на доступ к полю, вызов метода или макроса, и нам не нужно объяснять это как исключение. Он хорошо соответствует текущим правилам синтаксического анализа и оператору ? . Пространство в цепочке методов, возможно, не хуже, чем в случае с Generalized Type Ascription , RFC, который, вероятно, будет принят в той или иной форме. С другой стороны, IDE может потребоваться больше волшебства, чтобы обеспечить автозаполнение для await . Люди могут найти место в цепочке методов слишком @withoutboats утверждает, что это может быть преимуществом), особенно если код не отформатирован так, что await заканчивает каждую строку. В нем используется ключевое слово, которое, возможно, мы могли бы избежать, если бы выбрали другой подход.

  • __ expr.await (поле постфикса) __: Синтаксис поля постфикса использует "силу точки" - он выглядит естественно в цепочке и позволяет интегрированным средам разработки выполнять автозаполнение await без выполнения других операций (например, как автоматическое удаление точки ). Это так же лаконично, как ключевое слово postfix. Точка перед await может упростить поиск экземпляров с помощью grep. С другой стороны, это похоже на полевой доступ. Как очевидный доступ к полю, нет визуального сигнала, что «это что-то делает».

  • __ expr.await() (метод постфикса) __: Синтаксис метода постфикса аналогичен полю постфикса. С другой стороны, синтаксис вызова () указывает читателю: «это что-то делает». При локальном рассмотрении это почти так же имеет смысл , как вызов метода блокировки приведет к выполнению в многопоточной программе. С другой стороны, он немного длиннее и шумнее, и маскировка магического поведения await как метода может сбить с толку. Можно сказать, что await - это метод в том же ограниченном смысле, что и call/cc в схеме - функция. В качестве метода нам нужно подумать, будет ли Future::await(expr) согласованно работать с UFCS .

  • __ expr.await!() (макрос постфикса) __: синтаксис макроса постфикса аналогичным образом использует «силу точки». Макрос ! bang указывает, что «это может творить чудеса». С другой стороны, это еще более шумно, и хотя макросы творит чудеса, они обычно не делают волшебства с окружающим кодом, как await . С другой стороны, если предположить, что мы стандартизируем универсальный синтаксис макроса постфикса , могут возникнуть проблемы с продолжением обработки await как ключевого слова.

  • __ expr@ , expr# , expr~ и другие односимвольные символы __: использование одного символа, как мы делаем с ? увеличивает краткость и, возможно, делает синтаксис постфиксного кажутся более естественными. Как и в случае с ? , мы можем обнаружить, что ценим эту лаконичность, если await начинает проникать в наш код. С другой стороны, до тех пор, пока боль от того, что наш код завален await станет проблемой, трудно увидеть формирование консенсуса вокруг компромиссов, присущих принятию такого синтаксиса.

Я хотел написать сообщение, чтобы поблагодарить

У меня есть идея, что добавляя «операторы канала», такие как F #, пользователи могут использовать либо префикс, либо постфикс (с явной разницей в синтаксисе).

// use `|>` for instance, Rust can choose other sigils if there are conflicts with current syntax
await expr
expr |> await

// and we can use this operator on normal function calls too
f(g(h(x))) 
x |> h |> g |> f
// this is more convenient than "postfix macro"
x.h!().g!().f!()

@traviscross Отличное резюме. Также было некоторое обсуждение объединения сигилы и ключевого слова, например, fut@await , поэтому я просто добавлю это здесь для людей, приходящих в эту ветку.

Я перечислил плюсы и минусы этого синтаксиса здесь . @earthengine говорит, что возможны другие сигилы, кроме @ , например ~ . @BenoitZugmeyer отдает предпочтение @await и спрашивает, будет ли хорошей идеей использование макросов postfix ala expr!await . @dpc утверждает, что @await слишком специфичен и плохо интегрируется с тем, что у нас уже есть, а также что Rust уже содержит много сигил; @cenwangumass соглашается, что это слишком спонтанно . @newpavlov говорит, что часть await кажется излишней, особенно если мы не добавляем другие похожие ключевые слова в будущем. @nicoburns говорит, что синтаксис может работать и что с ним не так много проблем, но это слишком специальное решение.

@traviscross отличное резюме!

Мои 0,02 $ в порядке от худшего к лучшему, на мой взгляд:

  • 3 - однозначно недопустимый вариант, потому что это даже не вызов метода.
  • 2 не поле, это очень запутанно, особенно для новичков. наличие await в списке завершения не очень полезно. После того, как вы напишете их тонны, вы просто напишите это как fn или extern . Дополнительное завершение даже хуже, чем ничего, потому что вместо полезных методов / полей мне будет предложено ключевое слово.
  • 4 Макро здесь подходит, но мне он не подходит. Я имею в виду асимметрию с ключевым словом async , как я упоминал выше.
  • 5 Сигил может быть слишком кратким и трудным для обнаружения, но это отдельная сущность, и с ней можно так обращаться. Не похож ни на что другое, поэтому не вызывает путаницы у пользователей
  • 1 Лучший подход ИМХО, это просто сигил, который легко обнаружить и который уже является зарезервированным ключевым словом. Разделение пространства, как упоминалось выше, является преимуществом, а не недостатком. Когда форматирование не выполняется, это нормально, но с rustfmt это еще менее важно.

Как человек, который с нетерпением ждал этого момента, вот и мои 0,02 доллара.

По большей части я согласен с @Pzixel , за исключением последних двух пунктов, в том смысле, что я бы поменял их местами или, возможно, даже дал бы оба варианта в качестве опции, как в случае с try!() / ? пара. Я все еще видел, как люди спорят в пользу try!() ? для ясности, и я думаю, что наличие какого-либо агентства над внешним видом вашего кода в этом конкретном случае является плюсом, а не минусом. , даже за счет двух разных синтаксисов.

В частности, ключевое слово await postfix отлично подходит, если вы пишете свой асинхронный код как явную последовательность шагов, например

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

С другой стороны, вы можете предпочесть сделать что-то более сложное, в типичном стиле цепочки методов Rust-y, например

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

Я думаю, что в этом случае уже достаточно ясно из контекста, что мы имеем дело с будущим, и многословие клавиатуры await излишне. Сравните:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

Еще одна вещь, которая мне нравится в синтаксисе постфикса с одним символом, заключается в том, что он дает понять, что символ принадлежит выражению , тогда как (лично для меня) отдельно стоящий await выглядит немного ... потерянным ? :)

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

Во всяком случае, это только мои мысли.

Поскольку это уже мать всех потоков, связанных с велосипедами, я хотел добавить еще одну сигилу, которая до сих пор не упоминалась AFAICT: -> .

Идея состоит в том, что он отражает -> из объявления типа возвращаемого значения функции, за которой он следует, которая - функция async - является ожидаемым возвращаемым типом.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

let body: MyResponse = client.get("http://api").send()->?.into_json()->?;

В приведенном выше примере из send()-> получается Result<Response, HttpError> , как написано в объявлении функции.

Это мои 0,02 доллара после прочтения большей части обсуждения выше и обдумывания предложенных вариантов в течение нескольких дней. Нет причин придавать моему мнению какое-либо значение - я просто случайный человек в Интернете. Я, вероятно, не буду больше комментировать, поскольку я осторожен, чтобы не добавлять шума в обсуждение.


Мне нравится постфиксная сигилла. У постфиксных сигил есть приоритет, и я думаю, что это согласуется с остальной частью языка. У меня нет особых предпочтений относительно того, какой именно символ используется - ни один из них не является убедительным, но я думаю, что он исчезнет по мере знакомства. Сигил обеспечивает наименьший шум по сравнению с другими вариантами постфикса.

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

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

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

Мне нравится приставка await { .. } . Этот подход имеет явный приоритет, но имеет проблемы (которые уже подробно обсуждались). Несмотря на это, я думаю, что это будет полезно для тех, кто предпочитает использовать комбинаторы. Я бы не хотел, чтобы это был единственный реализованный вариант, поскольку он неэргономичен с цепочкой методов, но я думаю, что он хорошо дополнит постфиксный сигил.

Мне не нравятся другие варианты префиксов, они не кажутся совместимыми с другими конструкциями потока управления в языке. Как и в случае с постфиксным методом, префиксная функция несовместима, нет никаких других глобальных функций, встроенных в язык, используемый для потока управления. Также нет никаких макросов, используемых для потока управления (за исключением try!(..) но он устарел, потому что у нас есть лучшее решение - постфиксный сигил).


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

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

tl; dr. Сигилы Postfix - это естественный способ выразить ожидание (из-за приоритета) и краткий, последовательный подход. Я бы добавил префикс await { .. } и постфикс @ (который можно использовать в контексте как ? ). Для меня очень важно, чтобы Rust оставался внутренне последовательным.

@SamuelMoriarty

Я думаю, что в этом случае уже достаточно ясно из контекста, что мы имеем дело с будущим, и многословие клавиатуры ожидания избыточно. Сравните:

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

Еще один аргумент против сигил состоит в том, что Rust становится J. Например:

let res: MyResponse = client.get("https://my_api").send()?@?.json()?@?;`

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

Я бы хотел иметь больше

let res: MyResponse = client.get("https://my_api").send()? await?.json()? await?;`

@rolandsteiner

Поскольку это уже основа всех веток, сбрасывающих велосипеды, я хотел добавить еще один символ, который до сих пор не упоминался AFAICT: ->.

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

У нас есть отдельное значение для -> , оно не имеет ничего общего с async/await .

Я бы предпочел префикс await { .. } и, если возможно, постфиксный знак ! .
Восклицательный знак тонко подчеркивал бы лень будущего. Они не будут выполняться, пока не будет дана команда с восклицательным знаком.

Приведенный выше пример будет выглядеть так:

let res: MyResponse = client.get("https://my_api").send()?!?.json()?!?;

Извините, если мое мнение не актуально, поскольку у меня мало опыта работы с асинхронными функциями и нет опыта работы с экосистемой асинхронных функций Rust.

Однако, глядя на .send()?!?.json()?!?; и другие подобные комбинации, я понимаю основные причины, по которым предложение на основе сигил мне кажется неправильным.

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

Во-вторых, в целом мне кажется, что точки ожидания встречаются реже, чем точки распространения ошибок, и более значительны. Точки ожидания достаточно значительны для меня, чтобы я заслужил быть отдельной стадией в цепочке, а не «второстепенным преобразованием», прикрепленным к другому этапу (и особенно не «просто одним из незначительных преобразований»). Я, вероятно, был бы в порядке даже с формой ключевого слова prefix (которая почти заставляет await разорвать цепочку), но в целом это кажется слишком ограничительным. Моим идеальным вариантом, вероятно, был бы макрос .await!() подобный методу, с будущей возможностью расширения макросистемы, чтобы разрешить пользовательские макросы, подобные методам.

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

expr....await будет отражать контекст чего-то, что происходит до await и согласуется с операторами rustlang. Также async await - это параллельный шаблон. Ожидание не может быть выражено как метод или подобное свойство

У меня есть склонность к await!(foo) , но поскольку другие указали, что это заставит нас снять резервирование await и, таким образом, исключить его будущее использование в качестве оператора до 2021 года, я выберу await { foo } как мое предпочтение. По поводу postfix await особого мнения у меня нет.

Я знаю, что, возможно, это не лучшее место для разговора о неявном ожидании, но сработает ли что-то вроде явного неявного ожидания? Итак, сначала у нас есть префикс ожидания приземления:

await { future }?

А позже добавим нечто похожее на

let result = implicit await { client.get("https://my_api").send()?.json()?; }

или же

let result = auto await { client.get("https://my_api").send()?.json()?; }

При выборе неявного режима все между {} ожидаются автоматически.

Он имеет унифицированный синтаксис await и уравновешивает необходимость префикса await, цепочки и максимальной явности.

Я решил rg --type csharp '[^ ]await' чтобы просмотреть примеры мест, где префикс был неоптимальным. Возможно, не все из них идеальны, но это настоящий код, прошедший проверку кода. (Примеры слегка обработаны, чтобы удалить некоторые вещи из модели предметной области.)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Использование FluentAssertions как более задач, чем обычный MSTest assert_eq! Assert.Equal .

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

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

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Другой: «Мне нужен был только один объект». (В сторону: «Боже, я рад, что Расту не понадобятся CancellationToken s.)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

Тот же .collect() тот, который люди упоминали в Rust.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

Я думал о том, что, возможно, мне понравится ключевое слово postfix с символом новой строки rustfmt после него (и после ? , если он есть), но я так думаю, что это заставило меня подумать, что новая строка в целом не годится.

else if (!await container.ExistsAsync())

Один из тех редких, где префикс действительно пригодился.

var response = (HttpWebResponse)await request.GetResponseAsync();

Было несколько приведений, хотя, конечно, приведения - это еще одно место, где Rust - это постфикс, а C # - префикс.

using (var response = await this.httpClient.SendAsync(requestMsg))

Этот префикс против постфикса не имеет значения, но я думаю, что это еще одно интересное отличие: поскольку в C # нет Drop , куча вещей в конечном итоге _ нужно_ использовать переменные, а не цепочки.

некоторые из @scottmcm «s примеров rustified в различном постфиксе вариантов:

// keyword
response.content.read_as_string()) await?.should().be(text);
// field
response.content.read_as_string()).await?.should().be(text);
// function
response.content.read_as_string()).await()?.should().be(text);
// macro
response.content.read_as_string()).await!()?.should().be(text);
// sigil
response.content.read_as_string())@?.should().be(text);
// sigil + keyword
response.content.read_as_string())@await?.should().be(text);
// keyword
let previous = branch.list_history(timestamp_utc, None, 1) await?.history_entries.single_or_default();
// field
let previous = branch.list_history(timestamp_utc, None, 1).await?.history_entries.single_or_default();
// function
let previous = branch.list_history(timestamp_utc, None, 1).await()?.history_entries.single_or_default();
// macro
let previous = branch.list_history(timestamp_utc, None, 1).await!()?.history_entries.single_or_default();
// sigil
let previous = branch.list_history(timestamp_utc, None, 1)@?.history_entries.single_or_default();
// sigil + keyword
let previous = branch.list_history(timestamp_utc, None, 1)@await?.history_entries.single_or_default();
// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;
// field
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await?.identity).await?;
// function
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await()?.identity).await()?;
// macro
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await!()?.identity).await!()?;
// sigil
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@?.identity)@?;
// sigil + keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@await?.identity)@await?;

(спасибо @ Nemo157 за исправления)

// keyword
let pending = transaction.connection.query(command) await.into_iter().collect::<Vec<EventView>>();
// field
let pending = transaction.connection.query(command).await.into_iter().collect::<Vec<EventView>>();
// function
let pending = transaction.connection.query(command).await().into_iter().collect::<Vec<EventView>>();
// macro
let pending = transaction.connection.query(command).await!().into_iter().collect::<Vec<EventView>>();
// sigil
let pending = transaction.connection.query(command)@.into_iter().collect::<Vec<EventView>>();
// sigil + keyword
let pending = transaction.connection.query(command)@await.into_iter().collect::<Vec<EventView>>();

Для меня, после прочтения этого, сигил @ отсутствует, так как он просто невидим, особенно перед ? .

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

Было бы обидно, если бы мы приняли решение, которое блокировало await в Streams.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

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

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(или более прямой перевод кода с использованием if let вместо комбинатора)

@ Nemo157 Да, но мы, наверное, справимся и без дополнительных функций:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Но подход if let действительно кажется мне более естественным.

Для меня, после прочтения этого, @ sigil не используется, так как он просто невидим, особенно перед?.

Не забывайте об альтернативных схемах подсветки кода, для меня этот код выглядит так:
1

Лично я не думаю, что "невидимость" здесь более серьезная проблема, чем для автономного ? .

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

@newpavlov вы не можете выбрать цветовую схему во внешних инструментах, например, во вкладках обзора gitlab / github.

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

Привет, ребята, я новичок в изучении Rust из C ++. Просто хочу прокомментировать, что не похоже

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

или же

id = id.or_else_async(async || { 
    self.storage_coordinator.get_default_widget_async() await?.identity 
}) await?;

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

id =  await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

или же

id = auto await {
    id.or_else_async(async || { self.storage_coordinator.get_default_widget_async()?.identity })
}?;

предложенное ранее кажется мне намного лучше.

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

Я убежден, что для размещения ? await? foo достаточно отчетлив и прост в изучении.

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

Я хотел бы предложить вариант использования как префикса , так и постфикса ожидания
У нас могло быть 2 вещи:

  • префикс await ключевое слово (например, вариант с более сильной привязкой, чем ? , но это менее важно)
  • новый метод для std::Future , например fn awaited(self) -> Self::Output { await self } . Его имя также может быть block_on , или blocking , или что-то лучшее, что придумает кто-то другой.

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

Технически второй пункт маркера также может быть выполнен с помощью постфиксного макроса, и в этом случае мы должны написать .awaited!() .

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

let done = await delayed;

let value = await delayed_result?;

let value2 = await some.thing()?;

let value3 = some.other().thing().awaited()?;

let value4 = promise
        .awaited()
        .map_err(|e| e.into())?
        .obtain_other_future()
        .awaited();

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


В качестве альтернативы, более волшебная версия могла бы полностью отказаться от ключевого слова await и полагаться на волшебный метод .awaited() в std :: Future, который не может быть реализован другими, пишущими свои собственные фьючерсы, но я думаю это было бы довольно нелогично и слишком частным случаем.

новый метод для std::Future , например fn awaited(self) -> Self::Output { await self }

Я почти уверен, что это невозможно (без создания магической функции), потому что для ожидания внутри него должно быть async fn awaited(self) -> Self::Output { await self } , что само по себе должно быть await ed. И если мы думаем о создании волшебной функции, это может быть просто ключевое слово, IMO.

Уже существует Future::wait (хотя, очевидно, в 0.3 его еще нет?), Который запускает будущее, блокируя поток.

Проблема в том, что _ суть await том, чтобы _не_ блокировать поток_. Если синтаксис ожидания - префикс, а нам нужна опция постфикса, это должен быть макрос постфикса, а не метод.

И если вы собираетесь использовать магический метод, просто назовите его .await() , который уже неоднократно обсуждался в потоке, как метод ключевого слова, так и волшебный extern "rust-await-magic" "real "фн.

РЕДАКТИРОВАТЬ: scottmcm ninja'd меня, а GitHub не проинформировал меня (потому что мобильный?), Но все равно оставлю это.

@scottmcm Можете ли вы также await казались нормальными по сравнению с неоптимальными? Я думаю, что исследование количества префиксов и постфиксов может помочь ответить на несколько вопросов.

  1. У меня сложилось впечатление, что пока лучший вариант использования постфикса await был
client.get("https://my_api").send() await?.json() await?

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

  1. Как я уже говорил, если кто-то напишет
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

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

  1. Ключевое слово Postfix потребовало бы изобретения чего-то, чего не существует в других основных языках. Если это не дает значительно лучшего результата, изобретать его не стоит.

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

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

Однако не факт, что они используют один и тот же шаблон для кода. Например, этот другой на странице звучит так, как будто он может лучше соответствовать тому, как люди ищут ? , и, следовательно, может искать await :

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

Для сравнения возьмем тот же пример, если он вернул Result вместо impl Future :

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

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

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

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

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

@scottmcm

Однако не факт, что они используют один и тот же шаблон для кода. Например, этот другой на странице звучит так, как будто он может лучше соответствовать тому, как люди ищут ? , и, следовательно, может искать await :

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

Также, конечно, нет доказательств того, что использование пятнистого шаблона помогает лучше / быстрее понимать код. Особенно, когда люди чаще всего используют F-образный узор, о котором я расскажу позже.

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

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Что касается меня, я начал, когда увидел async , затем я перехожу к сначала await? , затем к self.storage_coordinator.get_default_widget_async() , затем к .identity , затем я, наконец, понял всю строку асинхронный. Я бы сказал, что это определенно не то впечатление от чтения, которое мне нравится. Одна из причин заключается в том, что в нашей системе письменного языка нет такого чередования прямого и обратного перехода . Когда вы прыгаете, это прерывает построение ментальной модели того, что делает эта линия.

Для сравнения, что делает эта строка, откуда вы это знаете?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

Как только я достиг await? , я сразу понял, что это асинхронно. Затем я прочитал let widget = await? , снова без каких-либо трудностей, я знал, что это асинхронно, что-то происходит. Я чувствую, что следую образцу формы F. Согласно https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/ , форма F является наиболее часто используемым узором. Итак, собираемся ли мы разработать систему, которая соответствует человеческой природе, или изобрести какую-то систему, которая требует специального образования и будет работать против нашей природы ? Я предпочитаю первое. Разница будет еще сильнее, если асинхронные и нормальные линии чередуются следующим образом

await? id.or_else_async(async || {
    let widget1 = await? self.storage_coordinator.get_default_widget_async();
    let result1 = do_some_wierd_computation_on(widget1.identity);
    let widget2 = await? self.network_coordinator.get_default_widget_async();
    let result2 = do_some_strange_computation_on(widget2.identity);
});

Я должен искать await? по строкам?

let id = id.try_or_else (|| Хорошо (self.storage_coordinator.try_get_default_widget () ?. identity)) ?;

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

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

Как это на самом деле будет выглядеть с одним предложенным стилем rustfmt и подсветкой синтаксиса ( while -> async , match -> await ):

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

Не знаю, как вы, но я сразу замечаю эти match .

(Привет, @ CAD97 , я исправил!)

@ dowchris97

Если вы собираетесь утверждать, что ? не меняет ментальную модель, что не так с моим аргументом, что await не должен изменять вашу ментальную модель кода? (Вот почему я раньше отдавал предпочтение неявным параметрам ожидания, хотя был убежден, что это не подходит для Rust.)

В частности, вы читаете async в _начале_ заголовка функции. Если функция настолько велика, что не помещается на одном экране, она почти наверняка слишком велика, и у вас есть другие, более серьезные проблемы с удобочитаемостью, чем поиск точек await .

Как только вы станете async , вам нужно будет удерживать этот контекст. Но в этот момент все await - это преобразование, которое превращает отложенное вычисление Future в результат Output путем парковки текущей последовательности выполнения.

Неважно, что это означает сложную трансформацию конечного автомата. Это деталь реализации. Единственное отличие от потоков ОС и блокировки заключается в том, что другой код может выполняться в текущем потоке, пока вы ожидаете отложенного вычисления, поэтому Sync не защищает вас так сильно. (Во всяком случае, я бы прочитал это как требование, чтобы async fn было Send + Sync а не предполагалось и позволяло быть небезопасным для потоков.)

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

    let widget1 = await(get_default_widget_async(storage_coordinator(self)));
    let result1 = do_some_wierd_computation_on(identity(widget1));
    let widget2 = await(get_default_widget_async(network_coordinator(self)));
    let result2 = do_some_strange_computation_on(identity(widget2));

Но поскольку это обратный порядок конвейера процесса, толпа функциональных разработчиков изобрела оператор «конвейера», |> :

    let widget1 = self |> storage_coordinator |> get_default_widget_async |> await;
    let result1 = widget1 |> identity |> do_some_wierd_computation_on;
    let widget2 = self |> network_coordinator |> get_default_widget_async |> await;
    let result2 = widget2 |> identity |> do_some_strange_computation_on;

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

    let widget1 = self.storage_coordinator.get_default_widget_async().await();
    let result1 = widget1.identity.do_some_wierd_computation_on();
    let widget2 = self.network_coordinator.get_default_widget_async().await;
    let result2 = widget2.identity.do_some_strange_computation_on();

Когда вы думаете о . как о конвейерной передаче данных так же, как |> , более длинные цепочки, часто встречающиеся в Rust, начинают иметь больше смысла, а при правильном форматировании (как в примере Centril) вы не делаете этого. Не упустите удобочитаемость, потому что у вас есть только вертикальный конвейер преобразований данных.

await не говорит вам «эй, это асинхронно». async делает. await - это просто то, как вы оставляете и ожидаете отложенное вычисление, и вполне имеет смысл сделать его доступным для оператора конвейерной обработки Rust.

(Привет, @Centril, ты забыл сделать это async fn (или while fn ), что несколько разбавляет мою точку зрения 😛)

можем ли мы переопределить вызов макроса

m!(item1, item2)

такой же как

item1.m!(item2)

поэтому мы можем использовать как префиксный, так и постфиксный стиль ожидания

await!(future)

и

future.await!()

@ CAD97

await не говорит вам "эй, это асинхронно". async делает. await - это как раз то, как вы оставляете отложенное вычисление и ожидаете его, и вполне имеет смысл сделать его доступным для оператора конвейерной обработки Rust.
Да, думаю, я правильно понимаю, но не писал строго.

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

Я знаю, что |> используется в других языках для обозначения чего-то еще , но в Rust это выглядит довольно хорошо и предельно ясно вместо префикса await :

// A
if |> db.is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match |> db.load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = |> client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()?
    .error_for_status()?;

// D
let mut res: InboxResponse =
    |> client.get(inbox_url)
        .headers(inbox_headers)
        .send()?
        .error_for_status()?
    |> .json()?;

// E
let mut res: Response =
    |> client.post(url)
        .multipart(form)
        .headers(headers.clone())
        .send()?
        .error_for_status()?
    |> .json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = |> self.request(url, Method::GET, None, true)?
               |> .res.json::<UserResponse>()?
                  .user
                  .into();

    Ok(user)
}

Аргумент о порядке чтения будет одинаково хорошо применим к оператору ? заменяющему try!() . В конце концов, «эй, это может произойти» важно, но «эй, это может вернуться раньше» тоже важно, если не больше. И действительно, опасения по поводу видимости неоднократно поднимались в обсуждениях велосипедной беседы по поводу ? (включая эту внутреннюю ветку и эту проблему GitHub ). Но сообщество в конце концов согласилось одобрить это, и люди к этому привыкли. Было бы странно, если бы теперь модификаторы ? и await появлялись на противоположных сторонах выражения только потому, что, по сути, сообщество изменило свое мнение о том, насколько важна видимость.

Аргумент о порядке чтения будет одинаково хорошо применим к оператору ? заменяющему try!() . В конце концов, «эй, это может произойти» важно, но «эй, это может вернуться раньше» тоже важно, если не больше. И действительно, опасения по поводу видимости неоднократно поднимались в обсуждениях велосипедной беседы по поводу ? (включая эту внутреннюю ветку и эту проблему GitHub ). Но сообщество в конце концов согласилось одобрить это, и люди к этому привыкли. Было бы странно, если бы теперь модификаторы ? и await появлялись на противоположных сторонах выражения только потому, что, по сути, сообщество изменило свое мнение о том, насколько важна видимость.

На самом деле дело не в том, что делает ваш код. Речь идет о вашей ментальной модели того, что делает ваш код, легко ли построить модель, есть ли потрясения или нет, интуитивно ли это или нет. Они могут сильно отличаться друг от друга.

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

Mozilla создает Firefox, верно? Все дело в UI / UX! А как насчет серьезного исследования этой проблемы HCI? Так что мы действительно можем убедить друг друга, используя данные, а не домыслы.

@ dowchris97 В этом два примера сравнения реального кода: одно сравнение ~ 24k строк с базами данных и reqwest и одно сравнение, которое показывает , почему C # не является точным сравнением с базовым синтаксисом Rust . В обоих случаях оказывается, что в реальном коде Rust ожидание в постфиксе кажется более естественным и не страдает от проблем, которые есть в других языках без семантики перемещения естественных значений. Если кто-то не представит другой пример реального мира приличного размера, который имеет тенденцию показывать обратное, я вполне убежден, что синтаксис префикса - это необходимость, навязанная самими другими языками, потому что им не хватает четкой семантики значений конвейерной обработки Rust (где чтение идиоматического кода слева направо почти всегда делает именно то, что подсказывает ментальная модель).

Изменить: Если это недостаточно ясно, ни один из C #, Python, C ++, Javascript не имеет методов-членов, которые принимают self по значению вместо ссылки. C ++ ближе всего к rvalue-ссылкам, но порядок деструктора по-прежнему сбивает с толку по сравнению с Rust.

Я думаю, что аргумент в пользу того, что await лучше в качестве префикса, связан не с тем, как вы должны изменить свою мысленную модель кода, а с тем, что в ржавчине у нас есть ключевые слова префикса, но нет ключевых слов постфикса postfixes, ржавчина использует сигилы, как показано на примере ? ). Вот почему await foo() легче читать, чем foo() await и почему некоторые люди хотят @ в конце утверждения и не любят, когда там await .

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

Лично я хотел бы видеть либо префикс await либо сигил постфикса (не обязательно @ ).

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

Это спорное утверждение. Пример reqwest представляет только одну версию синтаксиса постфикса.

С другой стороны, если это обсуждение сводится к голосованию о том, кому что больше нравится, просьба упомянуть об этом на Reddit, чтобы люди не жаловались так, как они это делали с impl Trait в аргументах функции.

@ eugene2k Для фундаментального обсуждения того, будет ли postfix соответствовать ментальной модели программистов Rust, большинство или все синтаксисы postfix находятся примерно в одном масштабе по сравнению с prefix. Я не думаю, что существует такая значительная разница в удобочитаемости между вариантами постфикса, как между префиксом и постфиксом. См. Также мое низкоуровневое сравнение приоритета операторов, из которого делается вывод, что их семантика одинакова в большинстве случаев использования, поэтому очень важно, какой оператор лучше всего передает смысл (в настоящее время я бы предпочел фактический синтаксис вызова функции, но не имеют сильное предпочтение перед другими).

@ eugene2k Решения в Rust никогда не принимаются голосованием. Rust - это не демократия, это меритократия.

Команды core / lang рассматривают все различные аргументы и точки зрения, а затем принимают решение. Это решение принимается консенсусом (среди членов команды), а не голосованием.

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

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

Повторение существующих аргументов или высказывание «я тоже» (или подобное) не увеличивает шансы на принятие предложения. Предложения никогда не принимаются на основании популярности.

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

(Я не имею в виду вас конкретно, я объясняю, как все работает, ради всех в этой ветке.)

@Pauan Ранее было сказано, что команды core / lang рассматривают различные предложения и затем принимают решение. Но решения того, «что легче читать» - это личные решения. Никакие логические аргументы, представленные лицу, принимающему решение, не изменят его личное мнение о том, что он предпочитает лучше всего. Кроме того, такие аргументы, как «так люди читают результаты поиска» и «исследование, проведенное такими-то и такими-то исследованиями, показало, что люди предпочитают то и это», легко оспариваются (что неплохо). Что может изменить мнение человека, принимающего решения, так это то, что он видит и не любит результаты применения своего решения в контексте, о котором он не задумывался. Итак, когда все эти контексты изучены и лица, принимающие решения в команде, придерживаются одного подхода, в то время как большинство других пользователей, не входящих в команду, предпочитают другой подход, каким должно быть окончательное решение?

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

Решение всегда принимает команда. Период. Так были намеренно разработаны правила.

И функции часто реализуются членами команды. И члены команды также установили доверие в сообществе. Так что у них есть как де-юре, так и де-факто власть.

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

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

Любое обсуждение того, следует ли управлять Rust по-другому, не по теме и должно быть обсуждено в другом месте.

(PS Я не нахожусь в основной или языковой команде, поэтому у меня нет никаких полномочий в этом решении, поэтому я вынужден подчиняться им, так же, как и вы)

@HeroicKatora

Я не думаю, что существует значительная разница в удобочитаемости между вариантами постфикса

Я не согласен. Я считаю, что foo().await()?.bar().await()? легче читать, чем foo() await?.bar() await? или даже foo()@?.bar()@? Несмотря на это, я чувствую, что использование методов, которые на самом деле не являются методами, создает плохой прецедент.

Хочу предложить другую идею. Я согласен, что префикс await непросто связать с другими функциями. Как насчет синтаксиса постфикса: foo(){await}?.bar()?{await} ? Его нельзя спутать с вызовами функций, и мне кажется, что его достаточно легко читать по цепочке.

И еще одно предложение, написанное мной. Рассмотрим следующий синтаксис вызова метода:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

Что делает его уникальным среди других предложений:

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

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

  1. Отсрочка всех префиксных операторов (включая await - так и должно работать):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Функционал оператора конвейера:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
  1. Переопределение возврата функции:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Функциональность увядания:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. Расщепление цепочки:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
  1. Макросы Postfix:
let x = long().method().[dbg!(it)].chain();

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

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

Перепишу в обязательные разделители:

// A
if await {db.is_trusted_identity(recipient.clone(), message.key.clone())}? {
    info!("recipient: {}", recipient);
}

// B
match await {db.load(message.key)}  {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await { client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
}?.error_for_status()?;

// D
let mut res = await {client.get(inbox_url).headers(inbox_headers).send()}?.error_for_status()?;

let mut res: InboxResponse = await {res.json()}?;

// E
let mut res = await { client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
}?.error_for_status()?;

let res: Response = await {res.json()}?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await {self.request(url, Method::GET, None, true)}?;
    let user = await {res.json::<UserResponse>()}?
        .user
        .into();

    Ok(user)
}

Он почти идентичен await!() . Итак, выглядит красиво! После использования await!() год или два, зачем вам вдруг придумывать postfix await, который нигде не встречается в истории языков программирования.

Ага. Синтаксис expr.[await it.foo()] @ I60R с контекстным ключевым словом it довольно удобен. Я не ожидал, что мне понравятся какие-то новые модные предложения по синтаксису, но это действительно очень приятно, это умное использование синтаксического пространства (потому что IIRC .[ в настоящее время нигде не является допустимым синтаксисом) и решит гораздо больше проблемы, чем просто ожидание.

Согласились, что для этого определенно потребуется RFC, и он может оказаться не лучшим вариантом. Но я думаю, что это еще один момент в пользу использования префиксного синтаксиса для await на данный момент, зная, что существует ряд вариантов решения проблемы "префиксные операторы неудобно связывать" в более общем плане, который в будущем принесет больше пользы, чем просто async / await.

И еще одно предложение, написанное мной. Рассмотрим следующий синтаксис вызова метода:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

Что делает его уникальным среди других предложений:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

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

1. Deferring of all prefix operators (including `await` - it's how it supposed to work):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
1. Pipeline operator functionality:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
1. Overriding function return:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
1. Wither functionality:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
1. Chain splitting:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
1. Postfix macros:
let x = long().method().[dbg!(it)].chain();

Поистине непонятно.

@tajimaha Глядя на ваш пример, я думаю, что await {} самом деле может быть намного лучше, чем await!() если мы будем использовать обязательные разделители, потому что это позволяет избежать проблемы "слишком много скобок", которая может привести к удобочитаемости проблемы с синтаксисом await!() .

Сравните:
`` С #
ждать {foo.bar (URL, ложь, qux.clone ())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps Вы можете получить подсветку синтаксиса async и await для простых примеров, установив язык на C #.)

@nicoburns С макросом можно использовать любое из () , {} или [] .

@sgrif Это хороший момент. И в этом случае я бы предположил, что вариант «ключевого слова префикса с обязательными разделителями» имеет очень мало смысла. Поскольку это нарушает согласованность с другими макросами, в основном без пользы.

(FWIW, я все еще думаю, что «префикс без разделителей» с общим решением, таким как макросы постфикса или предложение @I60R для постфикса, имеет наибольший смысл. Но опция «просто придерживаться существующего макроса» растет во мне ...)

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

Почему в этом мало смысла и почему ключевое слово, не имеющее того же синтаксиса, что и макрос, является проблемой?

@tajimaha

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

Почему в этом мало смысла и почему ключевое слово, не имеющее того же синтаксиса, что и макрос, является проблемой?

Хорошо, если бы await был макросом, это имело бы то преимущество, что не добавляло никакого дополнительного синтаксиса к языку. Таким образом снижается сложность языка. Однако есть хороший аргумент в пользу использования ключевого слова: return , break , continue и другие конструкции, изменяющие поток управления, также являются ключевыми словами. Но все они работают без разделителей, поэтому для согласованности с этими конструкциями await также должны работать без разделителей.

Если у вас есть ожидающие с обязательными разделителями, то у вас есть:

// Macros using `macro!(foo);` syntax 
format!("{}", foo);
println!("hello world");

// Normal keywords using `keyword foo;`
continue foo;
return foo;

// *and* the await keyword which is kind of in between the other two syntaxes:
await(foo);
await{foo};

Это потенциально сбивает с толку, поскольку теперь вам нужно запомнить 3 формы синтаксиса вместо двух. И, учитывая, что ключевое слово с обязательными разделителями не дает никаких преимуществ по сравнению с синтаксисом макросов, я думаю, что было бы предпочтительнее просто придерживаться стандарта макросинтаксис, если мы хотим принудительно использовать разделители (что я совершенно не уверен, что мы должны это делать).

Вопрос к тем, кто сегодня использует много async / await в Rust: как часто вы ожидаете функции / методы по сравнению с переменными / полем?

Контекст:

Я знаю, что в C # принято делать то, что сводится к следующему шаблону:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

Таким образом, оба работают параллельно. (Некоторые скажут, что здесь следует использовать Task.WhenAll , но разница в производительности крошечная, и это делает код более беспорядочным, поскольку требует прохождения индексов массива.)

Но насколько я понимаю, в Rust это вообще не будет работать параллельно, поскольку poll для fooTask не будет вызываться, пока bar получит свое значение, и _needs_ использовать комбинатор, возможно

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

Так что, учитывая это, мне любопытно, регулярно ли у кого-то оказывается будущее в переменной или поле, которое вам нужно ждать, или если он почти всегда ожидает выражений вызова. Потому что, если это последнее, есть крошечный вариант форматирования ключевого слова postfix, который мы на самом деле не обсуждали: ключевое слово postfix _no-space_.

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

client.get("https://my_api").send()await?.json()await?

(На самом деле я не хочу обсуждать rustfmt, как я уже сказал, но я помню, что одной из причин, почему мне не нравится ключевое слово postfix _ было_ пробел, разрушающий визуальные фрагменты.)

Если мы пойдем с этим, мы можем также использовать синтаксис .await для использования
сила точки, не так ли?

Вопрос к тем, кто сегодня использует много async / await в Rust: как часто вы ожидаете функции / методы по сравнению с переменными / полем?

Но насколько я понимаю, в Rust это вообще не будет работать параллельно [...]

Верный. Вот этот пример из той же кодовой базы, что и раньше:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(move |id| {
        self__.request(URL, Method::GET, vec![("info".into(), id.into())])
            .and_then(|mut response| response.json().from_err())
    });

    await!(futures_unordered(futures).collect())?
};

Если бы я переписал закрытие с закрытием async :

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(async move |id| {
        let mut res =
            await!(self__.request(URL, Method::GET, vec![("info".into(), id.into())]))?;

        Ok(await!(res.json())?)
    });

    await!(futures_unordered(futures).collect())?
};

Если бы мне пришлось переключиться на синтаксис .await (и связать его вместе):

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Есть ли репозиторий, который уже активно использует await? Я бы предложил переписать больший кусок в каждом предложенном стиле

@gralpli К сожалению, ничто из того, что я могу открыть с открытым исходным кодом, активно использует await! . В настоящий момент он определенно больше подходит для кода приложения (особенно из-за его нестабильности).

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Эти строки в точности показывают, как код портится из-за чрезмерного использования постфикса и цепочки .

Посмотрим версию приставки:

let func = async move |id| {
    let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
    Ok(await(req.json())?)
}
let responses: Vec<Response> = await {
    futures_unordered(all_ids.into_iter().map(func)).collect()
}?;

Обе версии используют 7 строк, но, IMO, вторая чище. Есть также два вывода по использованию обязательных разделителей:

  1. await { future }? не выглядит шумным, если часть future длинная. См. let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. Когда строка короткая, лучше использовать await(future) . См. Ok(await(req.json())?)

ИМО, переключаясь между двумя вариантами, читаемость этого кода намного лучше, чем раньше.

Первый пример плохо отформатирован. Я не думаю, что rustfmt форматирует это как
тот. Не могли бы вы запустить на нем rustfmt и снова опубликовать здесь?

@ivandardi @mehcode Вы могли бы это сделать? Я не знаю, как отформатировать синтаксис .await . Я просто скопировал код. Благодаря!

Хочу добавить, что этот пример показывает:

  1. Производственный код - это не просто красивые и простые цепочки вроде:
client.get("https://my_api").send().await?.json().await?
  1. Люди могут злоупотреблять цепочкой или злоупотреблять ею.
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Здесь асинхронное закрытие обрабатывает каждый идентификатор, оно не имеет ничего общего с управлением более высокого уровня futures_unordered . Их объединение значительно снижает вашу способность понимать это.

Все _was_ прошло через rustfmt из моего сообщения (с небольшими изменениями, чтобы он компилировался). Где находится .await? еще не решено, и в настоящее время я помещаю его в конец ожидаемой строки.


Теперь я согласен, что все выглядит ужасно. Это код, написанный в срок, и все будет выглядеть ужасно, когда у вас есть комната поваров, которые пытаются что-то достать.

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

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

А теперь давайте повеселимся и сделаем это намного лучше, используя ретроспективу и некоторые новые адаптеры в Futures v0.3.

Префикс с «очевидным» приоритетом (и сахар)
let responses: Vec<Response> = await? stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect();
Префикс с приоритетом «Полезный»
let responses: Vec<Response> = await stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect()?;
Префикс с обязательными разделителями
let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;
Поле постфикса
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect().await?;
Ключевое слово Postfix
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect() await?;

Мелочь здесь. TryStreamExt::and_then нет, наверное должно быть. Звучит как легкий пиар для любого, у кого есть время, и он хочет внести свой вклад.


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

  • Я также хочу выразить свою растущую неприязнь к await .... ? (полезный приоритет), поскольку я хочу подумать, что произойдет, если у нас будет fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

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

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

На самом деле это не проблема, потому что в Python javascript люди с большей вероятностью будут писать их в отдельных строках. На самом деле я не видел await (await f) в python.

Префикс с обязательными разделителями

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

Это тоже похоже на использование комбинаторов. Хотя цель введения async / await - уменьшить его использование там, где это необходимо.

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

@ivandardi @mehcode Копирование из Rust RFC на async / await:

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

... использовать с синтаксическим сахаром, который стал обычным для многих языков с асинхронным вводом-выводом - ключевые слова async и await.

С точки зрения пользователя, они могут использовать async / await, как если бы это был синхронный код, и им нужно только аннотировать свои функции и вызовы.

Итак, весь смысл внедрения async await заключается в сокращении цепочки и создании асинхронного кода, как если бы они были синхронизированы . В этом RFC цепочка упоминается только дважды: «требуется либо дуги, либо объединение в цепочку» и «по-прежнему часто приводит к беспорядочным наборам вложенных и связанных обратных вызовов». Для меня это звучит не слишком позитивно.

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

@tajimaha Вы неправильно поняли RFC. Он конкретно говорит о комбинаторах Future ( map , and_then и т. Д.), Но не говорит о цепочках в целом (например, о методах, возвращающих impl Future ).

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

Кроме того, вы неправильно понимаете процесс: RFC не нужно переписывать. RFC - это отправная точка, но не спецификация. Ни один из RFC не высечен в камне (да и не должен быть!)

RFC-процесс плавный, он не такой жесткий, как вы предполагаете. Ничто в RFC не мешает нам обсуждать постфикс await .

Кроме того, любые изменения будут внесены в RFC стабилизации , а не в исходный RFC (который уже был принят и поэтому не будет изменен).

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

Я здесь шучу.

При написании RFC, вероятно, верно одно. Люди специально хотели новый инструмент, «который может использовать async / await, как если бы это был синхронный код». Синтаксис, следующий за традициями, лучше выполнит это обещание.
И вот видите, это не комбинаторы будущего,

let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

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

Люди специально хотели новый инструмент, «который может использовать async / await, как если бы это был синхронный код». Синтаксис, следующий за традициями, лучше выполнит это обещание.

Не понимаю, как это правда: и префикс, и постфикс await выполняют это желание.

Фактически, postfix await вероятно, лучше выполняет это желание, потому что это очень естественно для цепочек методов (которые очень часто встречаются в синхронном коде Rust!)

Использование префикса await сильно поощряет использование множества временных переменных, что часто не является идиоматическим стилем Rust.

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

Я вижу ровно одно закрытие, и это не обратный вызов, он просто вызывает map на Iterator (ничего общего с Futures!)

Пожалуйста, не пытайтесь искажать слова RFC, чтобы оправдать префикс await .

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

Решение будет принято на основе достоинств различных предложений, исходный RFC не имеет никакого значения (кроме как полезной исторической справки).

Обратите внимание: последний пример @mehcode в основном использует комбинаторы _stream_ (и один будущий комбинатор можно тривиально заменить на асинхронный блок). Это эквивалент использования комбинаторов итераторов в синхронном коде, поэтому может использоваться в некоторых ситуациях, когда они более подходят, чем циклы.

Это оффтоп, но большую часть разговоров здесь ведет около дюжины комментаторов. Из 383 комментариев на момент написания этого выпуска было только 88 уникальных плакатов. Чтобы не выгореть / не обременять любого, кому придется пойти и прочитать эти комментарии, я бы рекомендовал быть как можно тщательнее в своих комментариях и убедиться, что это не повторение предыдущего пункта.


Гистограмма комментариев

HeroicKatora:(32)********************************
Centril:(22)**********************
ivandardi:(21)*********************
I60R:(21)*********************
Pzixel:(16)****************
novacrazy:(15)***************
scottmcm:(13)*************
EyeOfPython:(11)***********
mehcode:(11)***********
Pauan:(10)**********
XX:(9)*********
nicoburns:(9)*********
tajimaha:(9)*********
skade:(8)********
CAD97:(8)********
Laaas:(8)********
dpc:(8)********
ejmahler:(7)*******
Nemo157:(7)*******
yazaddaruvala:(6)******
traviscross:(6)******
CryZe:(6)******
Matthias247:(5)*****
dowchris97:(5)*****
rolandsteiner:(5)*****
earthengine:(5)*****
H2CO3:(5)*****
eugene2k:(5)*****
jplatte:(4)****
lnicola:(4)****
andreytkachenko:(4)****
cenwangumass:(4)****
richardanaya:(4)****
chpio:(3)***
joshtriplett:(3)***
phaylon:(3)***
phaazon:(3)***
ben0x539:(2)**
newpavlov:(2)**
comex:(2)**
DDOtten:(2)**
withoutboats:(2)**
valff:(2)**
darkwater:(2)**
tanriol:(1)*
liigo:(1)*
yasammez:(1)*
mitsuhiko:(1)*
mokeyish:(1)*
unraised:(1)*
mzji:(1)*
swfsql:(1)*
spacekookie:(1)*
sgrif:(1)*
nikonthethird:(1)*
edwin-durai:(1)*
norcalli:(1)*
quodlibetor:(1)*
chescock:(1)*
BenoitZugmeyer:(1)*
F001:(1)*
FuGangqiang:(1)*
Keruspe:(1)*
LegNeato:(1)*
MSleepyPanda:(1)*
SamuelMoriarty:(1)*
Swoorup:(1)*
Uristqwerty:(1)*
alexmaco:(1)*
arabidopsis:(1)*
arielb1:(1)*
axelf4:(1)*
casey:(1)*
lholden:(1)*
cramertj:(1)*
crlf0710:(1)*
davidtwco:(1)*
dyxushuai:(1)*
eaglgenes101:(1)*
AaronFriel:(1)*
gralpli:(1)*
huxi:(1)*
ian-p-cooke:(1)*
jonimake:(1)*
josalhor:(1)*
jsdw:(1)*
kjetilkjeka:(1)*
kvinwang:(1)*

Обратите внимание: последний пример @mehcode в основном использует комбинаторы _stream_ (и один будущий комбинатор можно тривиально заменить на асинхронный блок). Это эквивалент использования комбинаторов итераторов в синхронном коде, поэтому может использоваться в некоторых ситуациях, когда они более подходят, чем циклы.

Здесь же можно утверждать, что я могу / должен использовать префикс await там, где они более уместны, чем цепочка.

@Pauan Похоже, дело не в том, чтобы выкручивать слова. Я показываю актуальную проблему с кодом, написанную сторонником постфиксного синтаксиса. И, как я уже сказал, код стиля префикса лучше иллюстрирует ваше намерение, хотя не обязательно имеет много временных файлов, как жалуются сторонники постфикса (по крайней мере, в этом случае). Кроме того, предположим, что в вашем коде есть цепочка с одним лайнером и двумя ожиданиями, как я могу отладить первое? (это правильный вопрос, и я не знаю).
Во-вторых, сообщество ржавчины становится больше, люди разного происхождения (как и я, чаще всего использую python / c / java) не все согласятся с тем, что цепочки методов - лучший способ делать что-то. Я надеюсь, что при принятии решения оно (не должно быть) основано только на точке зрения первых последователей.

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

let get_one_id = async move |id| {
    self.request(URL, Method::GET, vec![("info".into(), id.into())])
        .await?
        .json().await
};

let responses: Vec<Response> = futures_unordered(all_ids.into_iter().map(get_one_id))
    .collect().await?;

Но в постфиксе let -binding и Ok в результате могут быть удалены вместе с последним ? чтобы напрямую предоставить результат, и тогда блок кода также не нужен в зависимости от на личный вкус. Это вообще не работает с префиксом из-за двух ожиданий в одном операторе.

Я не понимаю часто высказываемого мнения о том, что let bindings унидиоматичны в коде Rust. Они довольно часто встречаются в примерах кода, особенно при обработке результатов. Я редко вижу больше 2 ? в коде, с которым имею дело.

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

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

let result = await client.get("url").send()?.json()?

где get , send и json являются асинхронными.

Для меня (не имеющего опыта работы с другими языками программирования) постфикс expr await выглядит естественным: «Сделай это, _тогда_ жди результата».

Были некоторые опасения, что следующие примеры выглядят странно:

client.get("https://my_api").send() await?.json() await? // or
client.get("https://my_api").send()await?.json()await?

Однако я бы сказал, что это следует разделить на несколько строк:

client.get("https://my_api").send() await?
    .json() await?

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

В среде IDE этому синтаксису не хватает «силы точки», но он все же лучше, чем версия с префиксом: когда вы вводите точку, а затем замечаете, что вам нужно await , вам просто нужно удалить точку и введите « await ». То есть, если IDE не предлагает автозаполнение для ключевых слов.

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

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

Как насчет постфикса then ?

Я не понимаю часто высказываемого мнения о том, что let bindings унидиоматичны в коде Rust. Они довольно часто встречаются в примерах кода, особенно при обработке результатов. Я редко вижу больше 2 ? в коде, с которым имею дело.

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

Это вдохновило меня на обзор текущего кода Rust, в котором два или более ? находятся в одной строке (может быть, кто-то может исследовать использование многострочного кода). Я просмотрел xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, yew, Rocket, exa, iron, parity-ethereum, tikv. Это проекты Rust с наибольшим количеством звезд.

Я обнаружил, что примерно только 40 строк из 585562 строк используют две или более ? в одной строке. Это 0,006% .

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

Предположим, вам дали задание взаимодействовать с новым API или вы новичок в использовании запросов. Вы наверняка напишете

client.get("https://my_api").send().await?.json().await?

одним выстрелом ? Если вы новичок в API, я сомневаюсь, что вы захотите убедиться, что вы правильно составили запрос, увидеть статус возврата, проверить свое предположение о том, что возвращает этот API, или просто поиграйте с API следующим образом:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

Сетевой API не похож на данные памяти, вы не знаете, что там. Это вполне естественно для прототипирования. И, когда вы создаете прототип, вы беспокоитесь о том, что все будет хорошо, НЕ слишком много временных переменных . Вы можете сказать, что мы можем использовать синтаксис постфикса, например:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = request.send().await?;
dbg!(response);
let data = response.json().await?;
dbg!(data);

Но, если у вас уже есть это:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

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

Я обнаружил, что примерно 40 строк из 585562 используют две или более? в одну строку.

Я хотел бы предположить, что это бесполезное измерение. Что действительно важно, так это наличие нескольких операторов потока управления _ на выражение_. Согласно типичному стилю (rustfmt) они почти всегда оказываются в разных строках файла, хотя принадлежат к одному и тому же выражению, и, таким образом, будут связаны в той степени, в которой постфиксный (теоретически) имеет значение для await .

мы не должны впадать в крайности, говоря, что все должно выполняться в цепочке

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

только около 40 линий из 585562 используют две или более? в одну строку.

Я не уверен, что это относится к префиксу и постфиксу. Я отмечу, что _ ни один_ из моих примеров C # желающих, чтобы постфикс не включал несколько await в строку, или даже несколько await в инструкции. И пример потенциального постфиксного макета @Centril также не помещал несколько await в строку.

Лучшее сравнение могло бы быть с вещами, связанными с ? , такими как эти примеры из компилятора:

Ok(&self.get_bytes(cx, ptr, size_with_null)?[..size])
self.try_to_scalar()?.to_ptr().ok()
let idx = decoder.read_u32()? as usize;
.extend(self.at(cause, param_env).eq(v1, v2)?.into_obligations());
for line in BufReader::new(File::open(path)?).lines() {

Изменить: Похоже, на этот раз ты победил меня, @ CAD97 :

Это шокирующе похоже на код обещания javascipt с большим количеством then s. Я бы не назвал это синхронным. Почти наверняка цепочка имеет await и притворяется синхронной.

Префикс с обязательными разделителями

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

@ CAD97 @scottmcm Хорошее замечание. Я знал, что есть ограничения на то, что я измеряю:

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

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

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

Я говорю, что если добавлен только постфикс, это было бы неэргономично, когда стиль C / Python выглядит хорошо (конечно, imo). Я также могу не использовать цепочку адресов при

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

Вместо этого я хочу призвать всех посмотреть, чем асинхронный код отличается от синхронных вариантов и как это повлияет на использование и использование ресурсов. Менее 5 комментариев из 400 в этой ветке упоминают эти различия. Если вы не знаете об этих различиях, возьмите текущую ночную версию и попробуйте написать приличный кусок асинхронного кода. В том числе попытаться получить идиоматическую (некомбинаторную) версию async / await части компиляции кода, о которой говорилось в последних 20 сообщениях.

Будет иметь значение, существуют ли вещи только между точками yield / await, сохраняются ли ссылки в точках ожидания, и часто некоторые особые требования, касающиеся фьючерсов, делают невозможным написать код настолько кратким, как это представляется в этом потоке. Например, мы не можем помещать произвольные асинхронные функции в произвольные комбинаторы, потому что они могут не работать с типами !Unpin . Если мы создаем фьючерсы из асинхронных блоков, они могут не быть напрямую совместимы с комбинаторами, такими как join! или select! , потому что им нужны закрепленные и объединенные типы, поэтому дополнительные вызовы pin_mut! и .fuse() может потребоваться внутри.

Кроме того, для работы с асинхронными блоками новые утилиты на основе макросов работают намного лучше, чем старые варианты комбинаторов. join! и select! . И это чрезмерно, что здесь часто приводится в качестве примеров.

Я не знаю, как postfix await может работать с .unwrap() в этом простейшем примере токио

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

Если будет принят префикс, он станет

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

Но если будет принят постфикс,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

Есть ли какое-нибудь интуитивное объяснение, которое мы можем дать пользователям? Это противоречит существующим правилам. await - это поле? или await - это привязка, у которой есть метод unwrap() ? ОЧЕНЬ ПЛОХО! Мы очень много разворачиваем, когда начинаем проект. Нарушил несколько правил дизайна в «Дзен Python».

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

Есть ли какое-нибудь интуитивное объяснение, которое мы можем дать пользователям? Это противоречит существующим правилам. await - это поле? или await - это привязка, у которой есть метод unwrap() ? ОЧЕНЬ ПЛОХО! Мы очень много разворачиваем, когда начинаем проект. Нарушил несколько правил дизайна в «Дзен Python».

Я бы сказал, хотя в docs.rs слишком много документов, unwrap , unwrap ? во многих реальных случаях следует заменить на

Я обнаружил, что примерно 40 строк из 585562 используют две или более? в одну строку.

Я хотел бы предположить, что это бесполезное измерение. Что действительно важно, так это наличие нескольких операторов потока управления _ на выражение_. Согласно типичному стилю (rustfmt) они почти всегда оказываются в разных строках файла, хотя принадлежат к одному и тому же выражению, и, таким образом, будут связаны в той степени, в которой постфиксный (теоретически) имеет значение для await .

Я не утверждаю, что у этого подхода может быть предел.

Я снова поискал xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, yew, Rocket, exa, iron, parity-ethereum, tikv. Это проекты Rust с наибольшим количеством звезд. На этот раз я поискал шаблон:

xxx
  .f1()
  .f2()
  .f3()
  ...

и есть ли в этих выражениях несколько операторов потока управления.

Я определил, что ТОЛЬКО 15 из 7066 цепочек имеют несколько операторов потока управления. Это 0,2% . Эти строки охватывают 167 из 585562 строк кода. Это 0,03% .

@cenwangumass Спасибо, что дали количественную оценку. :сердце:

Одно из соображений заключается в том, что, поскольку в Rust есть перепривязка переменных с помощью let, он может стать убедительным аргументом для префикса await , поскольку при последовательном использовании у вас будет отдельная строка кода для каждого await await -точка. Преимущество двоякое: трассировки стека, поскольку номер строки дает больший контекст относительно того, где возникла проблема, и легкость отладки точки останова, поскольку обычно требуется установить точку останова на каждой отдельной точке ожидания для проверки переменных, может перевесить краткость одна строка кода.

Лично я разрываюсь между префиксным стилем и постфиксной сигилой, хотя после прочтения https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 я, вероятно, в основном предпочитаю постфиксную сигилу.

Стиль префикса визуализации:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = await? self.request(url, Method::GET, None, true));
    let user = await? user.res.json::<UserResponse>();
    let user = user.user.into();

    Ok(user)
}

Стиль визуализации постфикса:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)) await?;
    let user = user.res.json::<UserResponse>() await?;
    let user = user.user.into();

    Ok(user)
}

Визуализированный постфиксный сигил @:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))@?;
    let user = user.res.json::<UserResponse>()@?;
    let user = user.user.into();

    Ok(user)
}

Визуализированный постфиксный сигил №:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))#?;
    let user = user.res.json::<UserResponse>()#?;
    let user = user.user.into();

    Ok(user)
}

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

Скорее всего, нам понадобится что-то вроде этого:
Синтаксис префикса

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Синтаксис Postfix

for response in stream await {
    let response = response?;
    ...
}
for response in stream.await!() {
    let response = response?;
    ...
}

// Or a specialized variant of `await` and `?`
//     Note (Not Obvious): The `?` actually applies to each response of `await`
for response in stream await? {
    ...
}
for response in stream.await!()? {
    ...
}

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

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

Я просто хочу, чтобы эти примеры обсуждались хотя бы немного, потому что, хотя постфикс хорош для связывания Future , на первый взгляд он кажется наименее интуитивным для Stream . Я не уверен, какое здесь правильное решение. Может быть, постфикс await для Future и другой стиль await для Stream ? Но для разных синтаксисов нам нужно убедиться, что синтаксический анализ не является неоднозначным.

Это всего лишь мои начальные мысли, возможно, стоит подумать о Stream с точки зрения постфикса. У кого-нибудь есть мысли о том, как postfix может хорошо работать с Stream , или нужно ли иметь два синтаксиса?

Синтаксис для итерации потока - это не то, что произойдет в течение некоторого времени, как я себе представляю. Довольно просто использовать цикл while let и делать это вручную:

Поле постфикса
while let Some(value) = stream.try_next().await? {
}
Префикс с «постоянным» приоритетом и сахаром
while let Some(value) = await? stream.try_next() {
}
Префикс с обязательными разделителями
while let Some(value) = await { stream.try_next() }? {
}

Был бы текущий способ делать что-то (плюс await ).


Замечу, что в этом примере мне особенно плохо выглядит «префикс с разделителями». Хотя await(...)? может выглядеть немного лучше, если бы мы использовали «префикс с разделителями», я бы надеялся, что мы разрешили бы только _один_ тип разделителя (например, try { ... } ).

Быстрая касательная, но не будет ли "ждать с разделителями" просто ... обычным префиксом await? await ожидает выражения, поэтому наличие await expr и await { expr } по сути одно и то же. Было бы бессмысленно иметь только await с разделителями и не иметь его без них, тем более что любое выражение может быть окружено {} и оставаться тем же выражением.

Вопрос о потоках привел меня к мысли, что для Rust вполне естественно иметь awaiting не только как оператор в выражениях, но и как модификатор в шаблонах:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

которые затем могут естественно работать с for

for async x in my_stream { ... }

@ivandardi

Проблема, которую решает "prefix await с обязательными разделителями", - это вопрос о приоритете ? . С _ обязательными_ разделителями нет вопроса о приоритете.

См. try { .... } для _similar_ (стабильного) синтаксиса. У try есть обязательные разделители по той же причине - как он взаимодействует с ? - поскольку ? внутри фигурных скобок сильно отличается от символа снаружи.

@yazaddaruvala Я не думаю, что должен быть специальный асинхронный способ обработки цикла for с лучшими результатами, который должен исходить только от некоторой общей функции, которая также имеет дело с Iterator<Item = Result<...>> .

// postfix syntax
for response in stream await {
    ...
}

Это означает, что stream: impl Future<Output = Iterator> и вы ждете, пока будет доступен итератор, а не каждый элемент. Я не вижу особых шансов на что-то лучшее, чем async for item in stream (без более общих функций, таких как упоминание @tanriol ).


@ivandardi разница заключается в приоритете после того, как вы начнете связывать конец разделителей, вот два примера, которые анализируют по-разному с помощью «очевидного префикса приоритета», «полезного префикса приоритета» (по крайней мере, мое понимание того, что такое «полезный» приоритет) и «обязательный префикс разделителей».

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

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

// obvious precedence prefix
await ((foo.bar()).baz()?);
await ({ let foo = quux(); foo.bar() }.baz()?);

// useful precedence prefix
(await ((foo.bar()).baz()))?;
(await ({ let foo = quux(); foo.bar() }.baz())?;

// mandatory delimiters prefix
(await (foo.bar())).baz()?;
(await { let foo = quux(); foo.bar() }).baz()?;

На мой взгляд, примеры @scottmcm из C # в https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 выглядят нормально с разделителями, перемещенными с позиции (await foo).bar на позиция await(foo).bar :

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

И так далее. Этот макет знаком по обычным вызовам функций, знаком по существующему потоку управления на основе ключевых слов и знаком по другим языкам (включая C #). Это позволяет избежать нарушения бюджета на странность и, похоже, не вызывает никаких проблем для цепочки после await .

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

и знаком с другими языками (включая C #)

В C # (или Javascript, или Python) приоритет не работает.

await(response.Content.ReadAsStringAsync()).Should().Be(text);

эквивалентно

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

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

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

Этот макет [...] знаком по существующему потоку управления на основе ключевых слов.

Я не согласен. Фактически мы _ предостерегаем_ от использования этого макета в существующем потоке управления на основе ключевых слов:

warning: unnecessary parentheses around `return` value
 --> src/lib.rs:2:9
  |
2 |   return(4);
  |         ^^^ help: remove these parentheses
  |
  = note: #[warn(unused_parens)] on by default

И rustfmt изменяет это на return (4); , _добавляя_ пробел.

На самом деле это не влияет на то, что я пытаюсь сформулировать, и похоже на то, что волосы раскалываются. return (4) , return(4) , это не более чем проблема стиля.

Я прочитал весь выпуск и почувствовал, что фигурные скобки и префикс {} в порядке, но затем он исчез.

Посмотрим:

let foo = await some_future();
let bar = await {other_future()}?.bar

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

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

ИМХО выглядит намного хуже чем

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

Однако, как говорится, я не верю в асинхронную цепочку, как написано в сообщении @cenwangumass . Вот мой пример использования async/await на гипер-сервере. Покажите, пожалуйста, где цепочка может быть полезна на этом конкретном примере. Я очень соблазнен, без шуток.


Что касается предыдущих примеров, большинство из них каким-то образом "испорчены" комбинаторами. Они вам почти никогда не понадобятся, так как вы ждете. Например

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

просто

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())]) await?;
      Ok(res.json() await?)
   })
   .join_all() await
   .collect()?

Если фьючерсы могут вернуть ошибку, это так же просто, как:

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())])? await?;
      Ok(res.json()? await?)
   })
   .join_all()? await
   .collect()?

@lnicola , @nicoburns ,

Я создал поток Pre-RFC для синтаксиса val.[await future] на https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await -цепь / 9304

Процедурные вопросы @Dowwie следует обсуждать в другом месте, например https://internals.rust-lang.org

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

Async в Rust, около 2018 г., содержит следующую аннотацию:

Нотация async / await - это способ сделать асинхронное программирование более похожим на синхронное программирование.

Возьмем простой пример токио из выше (изменение unwrap() на ? ):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

и применение постфиксной сигилы приводит к

let response = client.get(uri).timeout(Duration::from_secs(10))!?

который очень похож на синхронное программирование и не имеет отвлекающих перемежающихся await префиксной и постфиксной нотации.

Я использовал ! качестве постфикса в этом примере, хотя это предложение вызвало у меня несколько отрицательных голосов в предыдущем комментарии. Я сделал это, потому что восклицательный знак имеет для меня врожденное значение в этом контексте, которого не хватает как @ (который мой мозг читает как "at"), так и # . Но это просто вопрос вкуса, а не моя точка зрения.

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

Другими словами: вы в основном заботитесь о точках доходности при написании асинхронного кода, а не столько при его чтении. Поскольку код читается чаще, чем пишется, будет полезен ненавязчивый синтаксис для await .

Напомню опыт команды C # (подчеркиваю - мои):

По этой же причине мы не использовали «неявную» форму для «await». На практике это было то, о чем люди хотели очень четко подумать, и что они хотели, чтобы они занимали центральное место в своем коде, чтобы они могли обратить на это внимание. Интересно, что даже спустя годы эта тенденция сохранилась. т.е. иногда спустя много лет мы сожалеем о том, что что-то излишне многословно . Некоторые функции хороши в этом отношении на раннем этапе, но когда люди освоятся с ними, им лучше подойдет что-то более компактное. В случае с await этого не произошло. Людям все еще нравится тяжелый характер этого ключевого слова.

Сигиловые символы почти невидимы без надлежащего выделения и менее дружелюбны для новичков (как я, вероятно, уже говорил ранее).


Но говоря о сигилах, @ вероятно, не запутает не говорящих по-английски (например, меня), потому что мы не читаем это как at . У нас есть совершенно другое имя для него, которое не запускается автоматически, когда вы читаете код, поэтому вы просто понимаете его как целый иероглиф со своим собственным значением.

@huxi У меня такое чувство, что сигилы уже практически исключены. См. Сообщение Centril еще раз, чтобы await .


Кроме того, для всех, кто не любит postfix await так много, и используйте прагматический аргумент «ну, не так много реального кода, который можно было бы связать, поэтому postfix await не следует добавлять», вот небольшой анекдот (который я, вероятно, убью из-за плохой памяти):

Еще во время Второй мировой войны одна из воюющих стран теряла много самолетов. Им нужно было как-то укрепить свои самолеты. Таким образом, очевидный способ узнать, на чем они должны сосредоточиться, - это посмотреть на вернувшиеся самолеты и увидеть, куда пули попали больше всего. Выяснилось, что в самолете в среднем 70% дыр приходилось на крылья, 10% - в двигатель и 20% - в другие части самолета. Так что с этой статистикой есть смысл усилить крылья, верно? Неправильно! Причина в том, что вы смотрите только на вернувшиеся самолеты. И в этих самолетах кажется, что пулевые повреждения крыльев не так уж и плохи. Однако все вернувшиеся самолеты получили лишь незначительные повреждения в области двигателя, что позволяет сделать вывод о том, что серьезные повреждения в области двигателя являются фатальными. Поэтому вместо этого следует укрепить область двигателя.

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

Поэтому я думаю, что лучше всего было бы использовать гибкость, которую предоставляют нам ночные сборки, и выбрать один синтаксис префикса и один синтаксис постфикса для добавления к языку. Я голосую за префикс «Очевидный приоритет» await и постфикс .await await. Это не окончательный вариант синтаксиса, но нам нужно выбрать один для начала, и я думаю, что эти два варианта синтаксиса обеспечат более новый опыт по сравнению с другими вариантами синтаксиса. После того, как мы внедрили их в ночное время, мы можем получить статистику использования и мнения о работе с реальным кодом с использованием обоих вариантов, а затем мы можем продолжить обсуждение синтаксиса, на этот раз подкрепленное более прагматическим аргументом.

@ivandardi Анекдот довольно тяжелый, и ИМХО не подходит: это мотивирующая история в начале пути, чтобы напомнить людям искать неочевидное и охватить все стороны, это _не_ тот, который можно использовать против оппозиции в обсуждение. Он подходил для Rust 2018, где он был поднят и не привязан к конкретной проблеме. Использовать его против другой стороны в дебатах невежливо, вам нужно отстоять позицию человека, который видит больше или имеет больше видения, чтобы заставить его работать. Не думаю, что ты этого хочешь. Кроме того, оставаясь на картинке, возможно, postfix не находится ни на одном из языков, потому что postfix так и не появился;).

Люди действительно измерили и посмотрели: у нас действительно есть цепочный оператор в Rust ( ? ), который редко используется для цепочки. https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

Также было хорошо сказано , что это @cenwangumass красиво написал здесь: https://github.com/rust-lang/rust/issues/57640#issuecomment -457962730. Так что люди не используют это как окончательные цифры.

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

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

@Pzixel из-за let rebinding, думаю, можно было бы написать

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

в виде

`` ''
пусть foo = ждать some_future ();
пусть bar = ждать {other_future ()} ?. bar_async ();
let bar = await {bar} ?;

@Pzixel У вас есть какой-нибудь источник цитаты об опыте команды C #? Потому что я смог найти только этот комментарий . Это не обвинение или что-то в этом роде. Я просто хочу прочитать полный текст.

Мой мозг переводит @ в "at" из-за его использования в адресах электронной почты. Этот символ на моем родном языке называется «Кламмерафф», что примерно переводится как «цепляющаяся обезьяна». Я действительно ценю, что мой мозг вместо этого остановился на «при».

Мои 2 цента, как относительно новый пользователь Rust (с опытом работы на C ++, но это не особо помогает).

Некоторые из вас упомянули новичков, вот что я думаю:

  • Во-первых, макрос await!( ... ) кажется мне незаменимым, поскольку он чрезвычайно прост в использовании и не может быть неправильно понят. Похоже на println!() , panic!() , ... и именно это в конце концов произошло с try! .
  • Обязательные разделители также просты и однозначны.
  • версия постфикса, будь то поле, функция или макрос, не составит труда прочитать или написать ИМХО, поскольку новички просто скажут: «Хорошо, вот как вы это делаете». Этот аргумент справедлив и для ключевого слова postfix, это «необычно», но «почему бы и нет».
  • что касается обозначения префикса с полезным приоритетом, я думаю, что это может сбивать с толку. Тот факт, что await связывает более жестко, поразит некоторых, и я думаю, что некоторые пользователи просто предпочтут помещать много скобок, чтобы прояснить это (цепочка или нет).
  • очевидный приоритет без сахара прост для понимания и обучения. Затем, чтобы представить сахар вокруг него, просто назовите await? полезным ключевым словом. Полезно, потому что устраняет необходимость в громоздких скобках:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids? await ... unwraps the future, so you have to use ? Оператор to unwrap the Result // but there is some sugar if you want, thanks to the await? `
    пусть ответ = ждать? http :: get ("https://www.rust-lang.org/");
    // но вы не должны связывать, потому что этот синтаксис не приводит к читаемому связанному коду
- sigils can be understood quite easily *if the chosen character makes sense* if it is introduced to be "the `?` for futures".

That being said, since no agreement seems to be reached, I think it would be reasonable to ship `await!()` to stable Rust. Then this discussion can be extended without blocking the whole process. Same that what happened for `try!()`/`?`, so again newcomers won't be lost. And if [Simple postfix macros](https://github.com/rust-lang/rfcs/pull/2442) get accepted, the problem will disappear since we'll get postfix macro for "free".

---

Just a thought, what about a postfix keyword, but which can be put as prefix as well, similar in some ways to the `const` keyword of C++? (I don't know if that was already proposed) In prefix position, it behaves like "prefix `await` with obvious precedence and optional sugar":
```c#
// preferred without chaining:
let response = await? http::get("https://www.rust-lang.org/");

// but also possible: (rustfmt warning)
let response = http::get("https://www.rust-lang.org/") await?;
let response = (http::get("https://www.rust-lang.org/") await)?;
let response = (await http::get("https://www.rust-lang.org/"))?;

// chains well
let matches = http::get("https://www.rust-lang.org/") await?
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;

// any of these are also allowed, but arguably ugly (rustfmt warning again)
let matches = await ((http::get("https://www.rust-lang.org/") await?)
    .body?
    .async_regex_search("(?=(\d+))\w+\1"));
let matches = (await? http::get("https://www.rust-lang.org/"))
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;
let matches = await http::get("https://www.rust-lang.org/") await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1");
let matches = await (await http::get("https://www.rust-lang.org/"))?
    .body?
    .async_regex_search("(?=(\d+))\w+\1");
let matches = await!(
    http::get("https://www.rust-lang.org/")) await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
);
let matches = await { // <-- parenthesis or braces optional here, but they clarify
    (await? http::get("https://www.rust-lang.org/"))
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
};

Как научить этому:

  • (Возможен макрос await!() )
  • приставка рекомендуется, когда не происходит цепочки, с сахаром (см. выше)
  • постфикс рекомендуется с цепочкой
  • их можно смешивать, но не рекомендуется
  • можно использовать префикс при объединении с комбинаторами

По своему личному опыту могу сказать, что префикс await не является проблемой для связывания.
Цепочка добавляет много добавлений в код Javascript с префиксом await и комбинаторами, такими как f.then(x => ...) на мой взгляд, без потери читабельности, и они, похоже, не чувствуют необходимости менять комбинаторы на postfix await.

Делать:

let ret = response.await!().json().await!().to_string();

такой же как:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

Я действительно не вижу преимуществ postfix await над цепочками комбинаторов.
Мне легче понять, что происходит во втором примере.

Я не вижу проблем с удобочитаемостью, цепочкой или приоритетом в следующем коде:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Я бы поддержал этот префикс await, потому что:

  • знания других языков.
  • согласования с другими ключевыми словами (return, break, continue, yield и т. д.).
  • он по-прежнему похож на Rust (с комбинаторами, как мы с итераторами).

Async / await станет отличным дополнением к языку, и я думаю, что больше людей будут использовать больше вещей, связанных с будущим, после этого добавления и его стабилизации.
Некоторые даже могут впервые обнаружить асинхронное программирование с Rust.
Таким образом, может быть полезно снизить сложность языка, в то время как концепции, лежащие в основе async, уже могут быть трудными для изучения.

И я не думаю, что доставка как prefix, так и postfix await - хорошая идея, но это всего лишь личное мнение.

@huxi Ага, я уже связал это выше, но, конечно, я могу переделать: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Мой мозг переводит @ в "at" из-за его использования в адресах электронной почты. Этот символ на моем родном языке называется «Кламмерафф», что примерно переводится как «цепляющаяся обезьяна». Я действительно ценю, что мой мозг вместо этого остановился на «при».

У меня похожая история: на моем языке это «собака», но это не влияет на чтение электронной почты.
Тем не менее, приятно видеть ваш опыт.

@llambda вопрос был о цепочке. Конечно, вы можете просто ввести дополнительные переменные. Но в любом случае это await { foo }? вместо await? foo или foo await? выглядит неправильным.

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

@Hirevo async/await должны устранить необходимость в комбинаторах. Не будем возвращаться к «но это можно сделать только с комбинаторами». Ваш код такой же, как

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

Итак, давайте полностью удалим async/await ?

Как уже говорилось, комбинаторы менее выразительны, менее удобны, и иногда вы просто не можете выразить то, что может await , например, заимствование между точками ожидания (для чего был разработан Pin ).

Я не вижу проблем с читабельностью, цепочкой или приоритетом в следующем коде

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Я делаю:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = serde_json::from_str(user);
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = serde_json::from_str(permissions );
    Ok(user)
}

Что-то странное происходит? Нет проблем:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = dbg!(fetch(format!("/user/{0}", name).as_str()) await?);
    let user: User = dbg!(serde_json::from_str(user));
    let permissions = dbg!(fetch(format!("/permissions/{0}", x.id).as_str()) await?);
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions));
    Ok(user)
}

С комбинаторами труднее заставить его работать. Не говоря уже о том, что ваши ? в функциях then / map не будут работать должным образом, и ваш код не будет работать без into_future() и некоторых других странные вещи, которые вам не нужны в потоке async/await .

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

Но опять же, это будет означать множество допустимых вариаций:

  • await!(...) , для согласованности с try!() и хорошо известными макросами, такими как println!()
  • await , await(...) , await { ... } , т.е. синтаксис префикса без сахара
  • await? , await?() , await? {} , т.е. синтаксис префикса с сахаром
  • ... await , т.е. постфиксный синтаксис (плюс ... await? , но это не сахар)
  • все их комбинации, но которые не приветствуются (см. мой предыдущий пост )

Но на практике мы ожидаем увидеть только:

  • префикс без Result s: await или await { ... } для уточнения с помощью длинных выражений
  • префикс с Result s: await? или await? {} для уточнения с помощью длинных выражений
  • постфикс при связывании: ... await , ... await?

+1 для отправки макроса await! () В стабильную. Завтра, если можно 😄

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

Если внедрение async Rust требует больших усилий для крупных ящиков / проектов, чем любые последующие попытки _optional_ рефакторинга, направленные на переход к более лаконичному / выразительному синтаксису, то я настоятельно рекомендую, чтобы мы позволили начать это усилие по внедрению прямо сейчас. Продолжающаяся неопределенность причиняет боль.

@Pzixel
(Во-первых, я допустил ошибку при вызове функции fetch_user, исправлю в этом посте)

Я не говорю, что async / await бесполезен и что мы должны удалить его для комбинаторов.
Await позволяет привязать значение из будущего к переменной в той же области после того, как оно будет разрешено, что действительно полезно и даже невозможно с одними комбинаторами.
Удаление await было не моей задачей.
Я только что сказал, что комбинаторы могут очень хорошо работать с await, и что проблема связывания может быть решена только путем ожидания всего выражения, созданного комбинаторами (устраняя необходимость в await (await fetch("test")).json() или await { await { fetch("test") }.json() } ).

В моем примере кода ? действительно ведет себя так, как задумано путем короткого замыкания, в результате чего замыкание возвращает Err(...) , а не всю функцию (эта часть обрабатывается await? по всей цепочке).

Вы эффективно переписали мой пример кода, но удалили из него цепочку (выполняя привязки).
Например, что касается отладочной части, следующее имеет точно такое же поведение только с необходимым dbg! и не более того (даже лишних скобок):

async fn fetch_permissions(name: &str) -> Result<Vec<Permission>, Error> {
    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| dbg!(serde_json::from_str::<User>(dbg!(x)?)))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str())))
        .map(|x| dbg!(serde_json::from_str::<Vec<Permission>>(dbg!(x)?)));
    Ok(user)
}

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

И, наконец, из любопытства, почему бы код не работал без .into_future() , разве это не фьючерсы (я не эксперт в этом, но я ожидал бы, что они уже будут фьючерсами)?

Я хотел бы выделить серьезную проблему с fut await : это серьезно мешает тому, как люди читают код. Есть определенная ценность в выражениях программирования, похожих на фразы, используемые в естественном языке, это одна из причин, почему у нас есть конструкции типа for value in collection {..} , почему на большинстве языков мы пишем a + b ("a плюс b") вместо a b + , и запись / чтение "await something" гораздо более естественна для английского (и других языков SVO ), чем "something await". Только представьте, что вместо ? мы использовали бы постфиксное ключевое слово try : let val = foo() try; .

fut.await!() и fut.await() не имеют этой проблемы, потому что они выглядят как знакомые вызовы блокировки (но «макрос» дополнительно подчеркивает связанную «магию»), поэтому они будут восприниматься иначе, чем через пробел. ключевое слово. Сигил также будет восприниматься по-другому, более абстрактно, без каких-либо прямых параллелей с фразами на естественном языке, и по этой причине я думаю, что не имеет значения, как люди читают предложенные сигилы.

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

@rpjohnst Тогда я не actual_fun(a + b)? и break (a + b)? для меня важны из-за разного приоритета, поэтому я не знаю, что await(a + b)? должно быть.

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

Мой первый комментарий заключается в том, что я считаю, что макрос .await!() postfix удовлетворяет всем основным целям Centril, за исключением первого пункта:

« await должно оставаться ключевым словом для будущего языкового дизайна».

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


Изменить : я неправильно понял, как именно работает await . Я вычеркнул свои неправильные утверждения и обновил примеры.

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

async function waitFor6SecondThenReturn6(){
  let result1 = await waitFor1SecondThenReturn1(); // executes first
  let result2 = await waitFor2SecondThenReturn2(); // executes second
  let result3 = await waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

Я считаю, что префикс async делает гораздо более ясную работу, указывая, что значения async являются частями конечного автомата, созданного компилятором:

async function waitFor6SecondThenReturn6(){
  let async result1 = waitFor1SecondThenReturn1(); // executes first
  let async result2 = waitFor2SecondThenReturn2(); // executes second
  let async result3 = waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

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

У этого синтаксиса есть некоторые нерешенные вопросы и явные проблемы с компоновкой, но он проясняет, где происходит асинхронная работа, и служит дополнением в синхронном стиле к более компоновке и цепочке .await!() .

Мне было трудно заметить постфикс await в конце некоторых выражений в более ранних примерах процедурного стиля, поэтому вот как это будет выглядеть с предложенным синтаксисом:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let async user = dbg!(fetch(format!("/user/{0}", name).as_str()));
    let user: User = dbg!(serde_json::from_str(user?));
    let async permissions = dbg!(fetch(format!("/permissions/{0}", user.id).as_str()));
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions?));
    Ok(user)
}

(Также можно привести аргумент в пользу легко компонуемого и связного макроса .dbg!() , но это для другого форума.)

29 января 2019 г., 23:31:32 -0800, Сферикон написал:

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

Мой первый комментарий заключается в том, что я считаю, что макрос .await!() postfix удовлетворяет всем основным целям Centril, за исключением первого пункта:

« await должно оставаться ключевым словом для будущего языкового дизайна».

Как один из основных сторонников синтаксиса .await!() , я
абсолютно считаю, что await должно оставаться ключевым словом. Учитывая, что мы
нужно, чтобы .await!() в любом случае был встроен в компилятор, это кажется
банально.

@Hirevo

Вы эффективно переписали мой пример кода, но удалили из него цепочку (выполняя привязки).

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

Например, что касается отладочной части, следующее имеет точно такое же поведение только с необходимым dbg! и не более того (даже лишних скобок)

Нет ты сделал это

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = dbg!(serde_json::from_str(dbg!(user)));
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
    Ok(user)
}

Это не одно и то же.

Я только что сказал, что комбинаторы могут очень хорошо работать с await, и что проблема связывания может быть решена только путем ожидания всего выражения, созданного комбинаторами (устраняя необходимость в await (await fetch ("test")). Json () или await {ожидание {выборка ("тест")} .json ()}).

Это проблема только для префикса await , для других его форм не существует.

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

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

Наконец, вы фактически не удалили никаких привязок. Вы только что использовали синтаксис labmda |user| я - let user = ... . Это вас ничего не спасло, но теперь этот код сложнее читать, сложнее отлаживать, и у него нет доступа к родительскому элементу (например, вам нужно обернуть ошибки извне в цепочке методов вместо того, чтобы делать это, вызывая самого себя, вероятно).


В двух словах: цепочка сама по себе не дает ценности. Это может быть полезно в некоторых сценариях, но это не один из них. И так как я пишу асинхронные / ОЖИДАНИЕ код для более чем шести лет, я полагаю , вы не хотите писать прикована асинхронной код никогда. Не потому, что вы не могли, а потому, что это неудобно и привязка всегда приятнее читать и часто писать. Нельзя сказать, что комбинаторы вам вообще не нужны. Если у вас есть async/await , вам не нужны эти сотни методов в futures / streams , вам нужно всего два: join и select . Все остальное можно сделать с помощью итераторов / привязок / ..., т.е. общеязыковых инструментов, которые не заставляют вас изучать еще одну инфраструктуру.

Сообщество @Sphericon AFAIK согласилось использовать await качестве ключевого слова или сигилы, поэтому, как мне кажется , ваши идеи о async требуют другого RFC.

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

Можно поподробнее? Я не видел никакой разницы между JS / C # await, кроме фьючерсов, основанных на опросах, но на самом деле он не имеет ничего общего с async/await .

Что касается синтаксиса префикса await? предложенного здесь в нескольких местах:
`` С #
let foo = ждать? bar_async ();

How would this look with ~~futures of futures~~ result of results *) ? I.e., would it be arbitrarily extensible:
```C#
let foo = await?? double_trouble();

IOW, префикс await? выглядит слишком специфичным для меня синтаксисом.

) * отредактировал.

Как это будет выглядеть с фьючерсами на фьючерсы? Т.е. будет ли он произвольно расширяемым:

let foo = await?? double_trouble();

IOW, await? выглядит слишком специфичным для меня синтаксисом.

@rolandsteiner под "фьючерсами на фьючерсы" вы имеете в виду impl Future<Output = Result<Result<_, _>, _>> (один await + два ? для меня подразумевает "разворачивание" одного будущего и двух результатов, а не ожидание вложенных фьючерсов ).

await? _is_ особый случай, но это особый случай, который, вероятно, будет применяться к более чем 90% использованию await . Вся суть фьючерсов - это способ ожидания асинхронных операций _IO_, ввод-вывод может ошибаться, поэтому 90% + от async fn , скорее всего, вернет io::Result<_> (или какой-либо другой тип ошибки, который включает вариант ввода-вывода. ). Функции, возвращающие Result<Result<_, _>, _> , в настоящее время довольно редки, поэтому я не ожидал, что они потребуют синтаксиса особого случая.

@ Nemo157 Вы, конечно, правы: результат результатов. Обновил мой комментарий.

Сегодня мы пишем

  1. await!(future?) за future: Result<Future<Output=T>,E>
  2. await!(future)? за future: Future<Output=Result<T,E>>

И если мы напишем await future? мы должны выяснить, что это значит.

Но всегда ли случай 1 можно превратить в случай 2? В случае 1 выражение либо дает будущее, либо ошибку. Но ошибку можно отложить и перенести в будущее. Таким образом, мы можем просто обработать случай 2 и выполнить автоматическое преобразование здесь.

С точки зрения программиста, Result<Future<Output=T>,E> гарантирует ранний возврат в случае ошибки, но за исключением того, что оба имеют одинаковый сегментный код. Я могу представить, что компилятор может сработать и избежать дополнительного вызова poll если случай ошибки возникнет.

Итак, предложение:

await exp? может интерпретироваться как await (exp?) если exp равно Result<Future<Output=T>,E> , и интерпретироваться как (await exp)? если exp равно Future<Output=Result<T,E>> . В обоих случаях он вернется раньше по ошибке и вернет истинный результат, если все будет нормально.

Для более сложных случаев мы можем применить что-то вроде автоматического разыменования приемника метода:

> При вмешательстве await exp???? мы сначала проверяем exp а если Result , try и продолжаем, пока результат все еще будет Result до тех пор, пока не закончатся ? или не будет чего-то отличного от Result . Тогда это должно быть будущее, и мы await на нем, а остальные ? s применяем.

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

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

await? - это особый случай, но это особый случай, который, вероятно, будет применяться к более чем 90% использованию await . Вся суть фьючерсов - это способ ожидания асинхронных операций ввода-вывода, ввод-вывод может ошибаться, поэтому 90% + от async fn , скорее всего, вернет io::Result<_> (или какой-либо другой тип ошибки, который включает вариант ввода-вывода. ). Функции, возвращающие Result<Result<_, _>, _> , в настоящее время довольно редки, поэтому я не ожидал, что они потребуют синтаксиса особого случая.

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

Можно ли было бы реализовать Future за Result<T, E> where T: Future ? Таким образом, вы можете просто await result_of_future без необходимости разворачивать его с помощью ? . И это, конечно, вернет Result, поэтому вы бы назвали его await result_of_future , что означало бы (await result_of_future)? . Таким образом, нам не понадобится синтаксис await? синтаксис префикса будет немного более согласованным. Дай мне знать, если с этим что-то не так.

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

  • Нет специального регистра оператора ? , нет await? или await??
  • Соответствует существующим операторам потока управления, таким как loop , while и for , которые также требуют обязательных разделителей
  • Чувствует себя как дома с похожими существующими конструкциями Rust
  • Отсутствие специального корпуса помогает избежать проблем при написании макросов
  • Не использует сигилы или постфиксы, избегая расходов из бюджета странностей

Пример:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

Однако, поиграв с этим в редакторе, он все еще кажется громоздким. Я бы предпочел await и await? без разделителей, например break и return .

Можно ли было бы реализовать Future for Resultгде Т: Будущее?

Вы бы хотели обратного. Наиболее распространенным awaitable является Future, где его тип вывода - это Result.

Тогда есть явный аргумент против сокрытия или иного поглощения ? в await. Что делать, если вы хотите сопоставить результат и т. Д.

Если у вас есть Result<Future<Result<T, E2>>, E1> , ожидание вернет Result<Result<T, E2>, E1> .

Если у вас есть Future<Result<T, E1>> , то ожидание просто вернет Result<T, E1> .

Невозможно скрыть или поглотить ? в ожидании, и после этого вы можете делать все, что нужно с Результатом.

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


Ой. Предполагается, что синтаксис await? подразумевает (await future)? что является обычным случаем.

Точно. Поэтому мы просто сделаем привязку await более плотной в await expr? , и если это выражение будет Result<Future<Result<T, E2>>, E1> тогда оно будет оцениваться как нечто вроде Result<T, E2> . Это означало бы, что для типов результатов нет специального корпуса. Он просто следует за обычными реализациями трейтов.

@ivandardi что насчет Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Обратите внимание, такой формы будущего больше нет. Теперь существует единственный связанный тип - Выход (который, вероятно, будет результатом).


@ivandardi Хорошо. Теперь я это вижу. Единственное, что у вас есть против вас, это то, что приоритет - это что-то странное, что вам нужно там изучить, поскольку это отклонение, но так же и с await я полагаю, почти все.

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

@ivandardi что насчет Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

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


@mehcode Что ж, я бы сказал, что он решает некоторые из ранее

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

почему бы и нет?

fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
    match val {
        0 => Ok(ok(15)),
        1 => Ok(err(100500)),
        _ => Err("Coulnd't create a future"),
    }
}

@Pzixel См. Https://doc.rust-lang.org/std/future/trait.Future.html

Вы говорите о старой особенности, которая была в ящике futures .

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

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (yield self.request(url, Method::GET, None, true)))?;
    let user = (yield user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

имеет сильное преимущество перед наличием сигилы в той же позиции:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (*self.request(url, Method::GET, None, true))?;
    let user = (*user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

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


Мы можем попробовать использовать сигил с расширенным точечным синтаксисом ( Pre-RFC ), который решает проблемы с глубоко вложенными областями:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?;
    let user = user.res.[*json::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}

а также добавляет возможность цепочечных методов:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?
        .res.[*json::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

И, очевидно, давайте заменим * на @ что здесь имеет больше смысла:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (@self.request(url, Method::GET, None, true))?;
    let user = (@user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?;
    let user = user.res.[<strong i="27">@json</strong>::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?
        .res.[<strong i="30">@json</strong>::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

Что мне здесь нравится, так это то, что @ отражает await которое помещается в LHS объявления функции, а ? отражает Result<User> которое помещается в RHS объявления функции. Это делает @ чрезвычайно совместимым с ? .


Есть мысли по этому поводу?

Есть мысли по этому поводу?

Любые лишние брекеты просто недопустимы

@mehcode Ага, я не понимал, что фьючерсам теперь не хватает типа Error . Но суть остается в силе: у вас может быть функция, которая, вероятно, вернет функцию, которая при завершении может вернуть результат или ошибку.

Есть мысли по этому поводу?

Да! Согласно комментарию Centril , сигилы не очень-то совместимы. Я бы начал рассматривать сигилы только в том случае, если можно было создать регулярное выражение, которое идентифицирует все точки ожидания.

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


Но суть остается в силе: у вас может быть функция, которая, вероятно, вернет функцию, которая при завершении может вернуть результат или ошибку.

Значит, у вас будет Future<Item=Result<T, E>> , верно? Что ж, в таком случае вы просто ... ждете будущего и разбираетесь с результатом 😐

Например, предположим, что foo имеет тип Future<Item=Result<T, E>> . Тогда await foo будет Result<T, E> и вы можете использовать следующее для устранения ошибок:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

Так что у тебя будет будущее> да? Что ж, в таком случае вы просто ... ждете будущего и разбираетесь с результатом 😐

Нет, я имею в виду Result<Future<Item=Result<T, E>>, OtherE>

С вариантом постфикса вы просто делаете

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

@melodicstream Я обновил свой пост и добавил ссылку на предварительный RFC для этой функции

Ой ... Я просто в третий раз все комментарии перечитал ...

Единственное постоянное чувство - это моя неприязнь к постфиксной нотации .await во всех ее вариациях, потому что я серьезно ожидал, что await будет частью Future с таким синтаксисом. «Сила точки» может работать для IDE, но совсем не работает для меня. Я, конечно, мог бы адаптироваться к нему, если бы это был стабилизированный синтаксис, но я сомневаюсь, что он когда-либо будет действительно "подходящим" мне.

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

Удаление сахара с await? future на (await future)? было бы хорошо и ценно, но все остальное кажется мне все более и более беспроблемным решением. Простое повторное связывание let улучшило читаемость кода в большинстве примеров, и я лично, вероятно, пошел бы по этому пути при написании кода, даже если бы легкое связывание было вариантом.

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

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

... по крайней мере, я честно попробую это сделать ...

@Pzixel Предполагая, что foo имеет тип Result<Future<Item=Result<T, E1>>, E2> , тогда await foo будет иметь тип Result<Result<T, E1>, E2> и тогда вы можете просто обработать этот Результат соответствующим образом.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream нет, не будет. Вы не можете ждать Result, вы можете ждать Future. So you have to do foo ()? to unwrap Future from Result , then do await to get a result, then again ? `, Чтобы развернуть результат из будущего.

Постфиксным способом это будет foo? await? , в префиксе ... Я не уверен.

Так что ваши примеры просто не работают, особенно последний, потому что он должен быть

(await foo.unwrap()).unwrap()

Однако @huxi может быть прав, мы решаем проблему, которой, вероятно, не существует. Лучший способ понять это - разрешить макрос postfix и увидеть реальную кодовую базу после базового принятия async/await .

@Pzixel Вот почему я предложил реализовать Future для всех типов Result<Future<Item=T>, E> . Это позволит то, о чем я говорю.

Хотя меня устраивает await foo?? за Result<Future<Output=Result<T, E1>>, E2> , я НЕ доволен await foo.unwrap().unwrap() . В моей первой модели мозга это должно быть

(await foo.unwrap()).unwrap()

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

Синтаксис постфикса, foo.unwrap() await.unwrap() , мне тоже подходит, поскольку я знаю, что await - это просто ключевое слово, а не объект, поэтому он должен быть частью выражения перед unwrap() .

Макрос в стиле постфикс прекрасно решает многие из этих проблем, но вопрос только в том, хотим ли мы сохранить знакомство с существующими языками и оставить его с префиксом. Я бы проголосовал за постфиксный стиль.

Я прав, что следующий код не равен:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

и

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

Первый вызывает панику при попытке создать будущее, а второй - при первом опросе. Если да, то что с этим делать? Это можно было бы обойти как

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

Но это намного менее удобно.

@Pzixel : это решение принято. async fn полностью ленивы и не запускаются до тех пор, пока не будут опрошены, и, таким образом, захватывают все время жизни ввода для всего будущего времени жизни. Обходной путь - функция упаковки, возвращающая явное значение impl Future и использующая блок async (или fn) для отложенного вычисления. Это _обязательно_ для объединения комбинаторов Future без распределения между ними, поскольку после первого опроса их нельзя переместить.

Это уже принятое решение, и почему неявные системы ожидания не работают (хорошо) для Rust.

Здесь разработчик C #, и я собираюсь защищать стиль префикса, так что будьте готовы проголосовать против.

Синтаксис Postfix с точкой ( read().await или read().await() ) вводит в заблуждение и предполагает доступ к полю или вызов метода, а await не является ни тем, ни другим; это языковая особенность. Я ни в коем случае не опытный Rustacean, но я не знаю других случаев .something , которые фактически являются ключевым словом, которое будет переписано компилятором. Ближе всего я могу вспомнить суффикс ? .

Имея async / await в C # уже несколько лет, стандартный синтаксис prefix-with-a-space ( await foo() ) за все это время не вызвал у меня никаких проблем. В тех случаях, когда требуется вложенный await , не обременительно использовать скобки, которые являются стандартным синтаксисом для всех явных приоритетов операторов и, следовательно, их легко проверить, например

`` С #
var list = (ждать GetListAsync ()). ToList ();


`await` is essentially a unary operator, so treating it as such makes sense.

In C# 8.0, currently in preview, we have async enumerables (iterators), which comes with an `IAsyncEnumerable<T>` interface and `await foreach (var x in QueryAsync())` syntax. The lack of async enumerables has been an issue since async was added in C# 5; for example, we have ORMs that return a `Task<IEnumerable>` which is only half asynchronous; we have to block on calls to `MoveNext()`. Having proper async enumerables is a big deal.

If a postfix `await` is used and similar async iterator support is added to Rust, that would look something like

```rust
for val in v_async_iter {
    println!("Got: {}", val);
} await // or .await or .await()

Это кажется странным.

Я думаю, что согласованность с другими языками также имеет значение. В настоящее время ключевое слово await перед асинхронным выражением является синтаксисом для C #, VB.NET, JavaScript и Python, а также предлагаемым синтаксисом для C ++. Это пять из семи лучших языков мира; В C и Java пока нет async / await, но я был бы удивлен, если бы они пошли другим путем.

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

@markrendle Я тоже разработчик C #, с 2013 года пишу async/await . И мне тоже нравятся префиксы. Но Rust сильно отличается.

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

Но ржавчина - другое дело. У нас здесь ? и мы должны распространять ошибки. В C # async / await автоматически переносит исключения, передает их через точки ожидания и так далее. У тебя его нет в ржавчине. Считайте, что каждый раз, когда вы пишете await на C #, вы должны писать

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

Иметь все эти брекеты очень утомительно.

OTOH с постфиксом у вас есть

var response = client.GetAsync("www.google.com") await.HandleException();
var json =  response.ReadAsStreamAsync() await.HandleException();
var somethingElse = DoMoreAsyncStuff(json) await.HandleException();
...

или в терминах ржавчины

let response = client.get_async("www.google.com") await?;
let json =  response.read_as_Stream_async() await?;
let somethingElse = do_more_async_stuff(json) await?;
...

И, наверное, у нас будет еще какой-нибудь трансформатор, помимо ? и await , назовем его @ . Если мы изобретем дополнительный синтаксис для await? , тогда нам понадобится await@ , @? , await@? , ... чтобы сделать его согласованным.


Мне тоже нравится префикс await , но я не совсем уверен, подходит ли он Rust. Добавление дополнительных правил в язык может быть нормальным, но это бремя, которое, вероятно, не стоит облегчения чтения.

Перечитайте все комментарии, включая ветку проблем с отслеживанием (на это потрачено почти 3 часа). И мое резюме таково: постфикс @ выглядит лучшим решением, и, к сожалению, самым недооцененным.

Есть много (предвзятых) мнений по этому поводу, и ниже я им пишу:


async-await будет незнаком с постфиксом @

На самом деле это было бы незнакомо только наполовину, потому что будет предоставлен тот же синтаксис async fn , и те же функции, что и с await , будут по-прежнему доступны.

В Rust много сигил, и нам не следует вводить еще одну сигилу

Но сигилы хороши до тех пор, пока они не читаются и не соответствуют другому синтаксису. И введение новой сигилы строго не означает, что это как-то ухудшит ситуацию. С точки зрения согласованности синтаксис постфикса @ намного лучше, чем у любого префикса / постфикса await .

Фрагменты вроде ?!@# напоминают мне Perl

На самом деле мы редко встретим какую-либо другую комбинацию, кроме @? которая, по ИМО, выглядит довольно простой и эргономичной. Типы возврата, такие как impl Try<impl Future<T>> , impl Future<impl Try<impl Try<T>>> и даже impl Future<T> , будут встречаться максимум в 10% случаев использования, как мы могли видеть на реальных примерах ITT. Более того, скопление сигил в 90% из этих 10% будет указывать на запах кода, когда вы должны либо исправить свой API, либо вместо этого ввести временную привязку, чтобы прояснить его.

Postfix @ так сложно заметить!

И это действительно большое преимущество в контексте async-await . Этот синтаксис предоставляет возможность выражать асинхронный код как синхронный, поэтому более навязчивый await только мешает его первоначальной цели. Примеры выше ясно показывают, что появление точек доходности может быть достаточно плотным, чтобы раздуть код, когда они слишком явные. Конечно, мы должны иметь их на виду, однако для этого подойдет @ sigil, а также await без раздувания кода.

Ожидание будущего - дорогостоящая операция, и мы должны прямо сказать, что

Это действительно имеет смысл, однако я не думаю, что мы должны говорить это так часто и так громко. Думаю, можно было бы привести аналогию с синтаксисом мутации: mut требуется явно только один раз, и в дальнейшем мы можем использовать изменяемую привязку неявно. Другая аналогия может быть обеспечена с небезопасной синтаксисом: явный unsafe требуется только один раз и в дальнейшем мы можем разыменовывают сырые указатели, вызывать опасные функции и т.д. И еще одна аналогия может быть обеспечена с оператором пузыря: явный impl Try return или предстоящий блок try требуются только один раз, и в дальнейшем мы можем уверенно использовать оператор ? . Таким же образом явный async аннотирует возможные долговременные операции только один раз, и далее мы можем применить оператор @ который действительно сделает это.

Это странно, потому что я читаю это иначе, чем await , и я не могу легко его напечатать

Если вы читаете & как "ref", а ! как "not", то я не думаю, что то, как вы читаете @ настоящее время, является веским аргументом в пользу отказа от этого условное обозначение. Также не имеет значения, как быстро вы его набираете, потому что чтение кода всегда имеет более высокий приоритет. В обоих случаях перейти на @ очень просто.

Это неоднозначно, потому что @ уже используется в сопоставлении с образцом

Я не думаю, что это проблема, потому что ! уже используется в макросах, & уже используется при заимствовании, а в операторе && используется | в замыканиях, в шаблонах и в операторе || , + / - можно также применять в позиции префикса, а . можно использовать еще больше разных контекстов. Нет ничего удивительного и ничего плохого в повторном использовании одних и тех же символов в разных синтаксических конструкциях.

Было бы сложно использовать grep

Если вы хотите проверить, содержат ли некоторые источники асинхронные шаблоны, то rg async было бы гораздо более простым и понятным решением. Если вы действительно хотите найти все точки доходности таким образом, вы можете написать что-то вроде rg "@\s*[;.?|%^&*-+)}\]]" что непросто, но это также не является чем-то потрясающим. Учитывая, как часто это будет требоваться, IMO это регулярное выражение абсолютно нормально.

@ не подходит для новичков, а гуглить сложно

Я думаю, что на самом деле это легче понять новичкам, потому что он последовательно работает с оператором ? . Напротив, префикс / постфикс await не будет согласован с другим синтаксисом Rust, дополнительно вводя головную боль с ассоциативностью / форматированием / значением. Я также не думаю, что await добавит некоторой самодокументируемости, потому что одно ключевое слово не может описать всю основную концепцию, и новички все равно узнают его из книги. И Rust никогда не был языком, который давал вам подсказки в виде простого текста для каждой из возможных синтаксических конструкций. Если кто-то забудет, что означает символ @ (я действительно сомневаюсь, что это будет проблемой, потому что есть также много ассоциаций, чтобы правильно его запомнить), тогда поиск в Google также должен быть успешным, потому что в настоящее время rust @ symbol возвращает всю необходимую информацию о @ в сопоставлении с образцом.

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

Почему? Неиспользованное зарезервированное ключевое слово - это нормально, когда оказывается, что окончательная реализация функции лучше без него. Кроме того, не было обещано, что будет введено именно ключевое слово await . И этот идентификатор не очень распространен в реальном коде, поэтому мы ничего не потеряем, если он останется зарезервированным до следующего выпуска.

? - ошибка, @ - вторая ошибка

Вероятно, вы мыслите более лексически, и поэтому предпочитаете работать со словами, а не символами. И из-за этого вы не видите смысла в более тонком синтаксисе. Но имейте в виду, что все больше людей думают более визуально, и для них труднее работать с назойливыми словами. Вероятно, хорошим компромиссом здесь будет предоставление макроса await!() рядом, например, try!() был предоставлен вместе с ? , поэтому вы сможете использовать его вместо @ если вам действительно неудобно с @ .

^^^^^^^ вы тоже перечитали мой комментарий?

Я получил большое откровение, прочитав комментарий @I60R, который убедил меня в пользу постфиксной сигилы, и до сих пор мне не нравилось использовать сигилу (вместо этого я предпочитаю некоторую форму postfix await).

Наша операция ожидания не await . По крайней мере, не так, как await используется в C # и JavaScript, двух крупнейших источниках знакомства с async / await .

В этих языках семантика запуска Task (C #) / Promise (JS) заключается в том, что задача немедленно помещается в очередь задач для выполнения. В C # это многопоточный контекст, в JS - однопоточный.

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

Вот почему вы получаете параллелизм с отдельными await s после создания нескольких Task / Promise s (даже для однопоточных исполнителей): семантика await aren 'Не "запускать эту задачу", а скорее "запускать какую-то задачу", пока не завершится ожидание, и мы не сможем продолжить.

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

У нас уже есть путаница в этой строке у пользователей из ленивых итераторов и текущих фьючерсов. Я утверждаю, что синтаксис await не приносит нам здесь никакой пользы.

Таким образом, наш await!(foo) не является await foo "традиционным" способом C # или JS. Итак, какой язык более близок к нашей семантике? Теперь я твердо верю, что это ( @matklad поправьте меня, если я ошибся в деталях) Kotlin suspend fun . Или, как упоминалось в обсуждениях, связанных с Rust, "явный асинхронный, неявный ожидание".

Краткий обзор: в Kotlin вы можете вызывать suspend fun из контекста suspend . Это немедленно запускает вызываемую функцию до завершения, приостанавливая сложенный контекст, как того требует дочерняя функция. Если вы хотите запустить дочерний suspend fun параллельно, вы создаете эквивалент асинхронных замыканий и комбинируете их с комбинатором suspend fun для параллельного запуска нескольких закрытий suspend . (Это немного сложнее.) Если вы хотите запустить дочерний объект suspend fun в фоновом режиме, вы создаете тип задачи, который помещает эту задачу в исполнитель и предоставляет .await() suspend fun чтобы объединить резервную копию вашей задачи с фоновой задачей.

Эта архитектура, хотя и описывается другими глаголами, чем Future Rust, звучит очень знакомо.

Ранее я объяснял, почему неявное ожидание не работает для Rust, и придерживаюсь этой позиции. Нам нужно, чтобы async fn() -> T и fn() -> Future<T> были эквивалентны в использовании, чтобы можно было использовать шаблон «выполнить некоторую начальную работу» (чтобы время жизни работало в сложных случаях) с ленивыми фьючерсами. Нам нужны ленивые фьючерсы, чтобы мы могли складывать фьючерсы без слоя косвенного обращения между ними для закрепления.

Но теперь я убежден, что «явный, но тихий» синтаксис foo@ (или какой-либо другой сигилы) имеет смысл. Как бы мне ни не хотелось произносить здесь это слово, @ в данном случае является монадической операцией в async , как ? - монадической операцией в try .

Бытует мнение, что добавление ? в ваш код - это просто чтобы заставить компилятор перестать жаловаться, когда вы возвращаете Result . И в некотором смысле вы это делаете. Но это сделано для того, чтобы сохранить наш идеал локальной ясности кода. Мне нужно знать, распаковывается ли эта операция через монаду или берется буквально локально.

(Если я ошибся в деталях монады, я уверен, что кто-нибудь поправит меня, но я считаю, что моя точка зрения все еще остается в силе.)

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

TL; DR: если вы возьмете одну вещь из этого мини-сообщения в блоге, пусть будет так: await!() Rust - это не await как на других языках. Результаты похожи, но семантика того, как обрабатываются задачи, сильно различается. Отсюда мы получаем выгоду от отказа от общего синтаксиса, так как нам не хватает общего поведения.

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

РЕДАКТИРОВАТЬ: если вы хотите обсудить это более синхронизированно, напишите мне @ CAD о Discord разработки ржавчины (ссылка 24 часа) или внутренних компонентах или пользователях (@ CAD97). Я хотел бы подвергнуть свою позицию некоторой прямой проверке.

РЕДАКТИРОВАТЬ 2: Извините, GitHub @ cad за случайный пинг; Я пытался убежать от @ и не хотел втягивать тебя в это.

@ I60R

Фрагменты вроде ?!@# напоминают мне Perl

На самом деле мы редко встретим какую-либо другую комбинацию, кроме @? которая, по ИМО, выглядит довольно простой и

Вы не можете этого узнать, пока он не появится. Если бы у нас был еще один преобразователь кода - например, yield sigil - это был бы настоящий Perl-скрипт.

Postfix @ так сложно заметить!

И это действительно большое преимущество в контексте async-await .

Трудность заметить await очков не может быть преимуществом.

Ожидание будущего - дорогостоящая операция, и мы должны прямо сказать, что

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

Перечитайте, пожалуйста, мои ссылки об опыте работы с C #. На самом деле так важно говорить это так громко и так часто.


Другие моменты вполне уместны, но те, на которые я ответил, представляют собой неустранимые пробелы.

@ CAD97 Как разработчик C #, который может сказать, чем async отличается в Rust ... Ну, это не так уж и отличается, как вы описываете. ИМХО ваши выводы основаны на ложной предпосылке

Таким образом, наш await!(foo) не является await foo "традиционным" способом C # или JS.

То же самое и в сознании разработчиков C #. Конечно, вы должны помнить, что фьючерсы не будут выполняться до тех пор, пока не будет проведен опрос, но в большинстве случаев это не имеет значения. В большинстве случаев await просто означает «Я хочу получить развернутое значение из будущего F в переменную x ». Когда future не запускается до опроса, это на самом деле облегчение после мира C #, поэтому люди, к счастью, будут счастливы. Кроме того, C # имеет аналогичные концепции с Iterators / generatos, которые тоже ленивы, поэтому толпа C # хорошо знакома со всем этим.

Короче говоря, обнаружить, что await работает немного иначе, проще, чем заставить сигилу работать так же, как await на их языке% younameit%, но не совсем. Людей не смутит разница, поскольку они могут подумать, что дело не в этом. Ситуации, когда нетерпеливое / ленивое выполнение имеет значение, действительно редко, поэтому это не должно быть основной проблемой дизайна.

@Pzixel Это то же самое в сознании разработчиков C #. Конечно, вы должны помнить, что фьючерсы не будут выполняться до тех пор, пока не будет проведен опрос, но в большинстве случаев это не имеет значения.

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

К сожалению, это не так: шаблон «создать несколько обещаний, а затем await их» очень распространен и желателен , но он не работает с Rust .

Это очень большая разница, которая даже застала врасплох некоторых участников Rust!

async / await в Rust действительно ближе к монадической системе, он в принципе не имеет ничего общего с JavaScript / C #:

  • Реализация совершенно другая (создание конечного автомата и его выполнение в Задаче, что согласуется с монадами).

  • API совершенно другой (pull vs push).

  • По умолчанию поведение ленивости совершенно иное (что согласуется с монадами).

  • Может быть только один потребитель, а не несколько (что соответствует монадам).

  • Разделение ошибок и использование ? для их обработки совершенно другое (что согласуется с преобразователями монад).

  • Модель памяти совершенно другая, что оказывает огромное влияние как на реализацию Rust Futures, так и на то, как пользователи на самом деле используют Futures.

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

Много много людей упоминали C #. Мы знаем.

Мы знаем, что сделал C #, мы знаем, что сделал JavaScript. Мы знаем, почему эти языки приняли такие решения.

Официальные члены команды C # поговорили с нами и подробно объяснили, почему они приняли эти решения с помощью async / await.

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

Но Rust действительно отличается от C # или JavaScript (или любого другого языка). Поэтому мы не можем слепо копировать то, что делают другие языки.

Но на любом из этих языков await foo семантически означает «запарковать эту задачу и дождаться завершения задачи foo.

Точно так же и в Rust. Семантика асинхронных функций такая же. Если вы вызываете async fn (который создает Future), он еще не запускает выполнение. Это действительно отличается от мира JS и C #.

Его ожидание приведет к завершению будущего, как и везде.

К сожалению, это не так: шаблон «создать несколько обещаний, а затем await их» очень распространен и желателен , но он не работает с Rust .

Не могли бы вы поподробнее? Я прочитал сообщение, но не нашел, что не так

let foos = (1..10).map(|x| some_future(x)); // create futures, unlike C#/JS don't actually run anything
let results = await foos.join(); // awaits them

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

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

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


PS

  • API совершенно другой (pull vs push).

Это имеет значение только при реализации вашего собственного будущего. Но в 99 (100?)% Случаев вы просто используете комбинаторы фьючерсов, которые скрывают эту разницу.

  • Модель памяти совершенно другая, что оказывает огромное влияние как на реализацию Rust Futures, так и на то, как пользователи на самом деле используют Futures.

Не могли бы вы поподробнее? На самом деле я уже играл с асинхронным кодом при написании простого веб-сервера на Hyper и не заметил никакой разницы. Поместите async сюда, await туда, готово .

Довод о том, что async / await в rust имеет иную реализацию, нежели другие языки, и тогда мы должны сделать что-то другое, довольно неубедителен. C

for (int i = 0; i < n; i++)

и Python

for i in range(n)

определенно не используют один и тот же базовый механизм, но почему Python решил использовать for ? Он должен использовать i in range(n) @ или i in range(n) --->> или что-то еще, чтобы показать эту важную разницу!

Вопрос в том, важна ли разница для повседневной работы пользователя!

Повседневная жизнь типичного пользователя ржавчины:

  1. Взаимодействие с некоторыми HTTP API
  2. Напишите сетевой код

и он действительно заботится о

  1. Он может закончить работу быстро и эргономично.
  2. Руст отлично выполняет свою работу.
  3. У него низкий уровень контроля, если он хочет

НЕ rust имеет миллион деталей реализации, отличных от C #, JS . Задача дизайнера языка - скрывать несущественные различия, показывая пользователям только полезные .

Кроме того, в настоящее время никто не жалуется на await!() по таким причинам, как «Я не знаю, что это конечный автомат», «Я не знаю, что он основан на вытягивании».

Пользователи не будут обращать внимания на эти различия.

Его ожидание приведет к завершению будущего, как и везде.

Ну, не совсем так? Если я просто напишу let result = await!(future) в async fn и вызову эту функцию, ничего не произойдет, пока она не будет помещена в исполнитель и не будет опрошена им.

@ ben0x539 да, перечитал ... Но пока добавить нечего.

@ CAD97 рад видеть, что мой пост вдохновил. Я могу ответить по поводу Kotlin: по умолчанию он работает так же, как и другие языки, хотя вы можете добиться ленивого поведения, подобного Rust, если хотите (например, val lazy = async(start=LAZY) { deferred_operation() }; ). И о подобной семантике: IMO наиболее близкая семантика предоставляется в реактивном программировании, поскольку реактивные наблюдаемые по умолчанию холодные (ленивые) (например, val lazy = Observable.fromCallable { deferred_operation() }; ). Подписка на них позволяет планировать фактическую работу так же, как await!() в Rust, но есть большая разница в том, что подписка по умолчанию является неблокирующей операцией, а результаты вычисляются асинхронно, почти всегда обрабатываются в закрытии отдельно от текущего потока управления. И есть большая разница в том, как работает отмена. Итак, я думаю, что поведение Rust async уникально, и я полностью поддерживаю ваш аргумент о том, что await сбивает с толку, а другой синтаксис здесь только преимущество!

@Pzixel Я почти уверен, что никакой новой сигилы не появится. Реализация yield качестве сигилы не имеет никакого смысла, потому что это конструкция потока управления, например if / match / loop / return . Очевидно, что все конструкции потока управления должны использовать ключевые слова, потому что они описывают бизнес-логику, а бизнес-логика всегда имеет приоритет. Напротив, @ а также ? являются конструкциями обработки исключений, и логика обработки исключений менее важна, поэтому она должна быть тонкой. Я не нашел никаких веских аргументов в пользу того, что наличие большого ключевого слова await - это хорошо (учитывая количество текста, возможно, я их пропущу), потому что все они апеллируют к авторитету или опыту (что, однако, не означает, что я уволить чью власть или опыт - он просто не может быть применен здесь в полном Манере).

@tajimaha приведенный вами пример Python содержит больше «деталей реализации». Мы можем рассматривать for как async , (int i = 0; i < n; i++) как await и i in range(n) как @ и здесь очевидно, что Python представил новое ключевое слово - in (в Java это фактически сигил - : ) и дополнительно ввел новый синтаксис диапазона. Он мог бы повторно использовать что-то более знакомое, например (i=0, n=0; i<n; i++) вместо того, чтобы вводить множество деталей реализации. Но в настоящее время влияние на пользовательский опыт только положительное, синтаксис проще, и пользователи действительно заботятся о нем.

@Pzixel Я почти уверен, что никакой новой сигилы не появится. Реализация yield качестве сигилы не имеет никакого смысла, потому что это конструкция потока управления, такая как if / match / loop / return . Очевидно, что все конструкции потока управления должны использовать ключевые слова, потому что они описывают бизнес-логику, а бизнес-логика всегда имеет приоритет. Напротив, @ а также ? являются конструкциями обработки исключений, и логика обработки исключений менее важна, поэтому она должна быть тонкой.

await не является «конструкцией обработки исключений».
Основываясь на неверной предпосылке, ваши выводы также ложны.

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

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

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

Я не хочу спорить о синтаксисе в Rust, но я видел много аргументов постфикса и префикса, и, возможно, мы можем получить лучшее из обоих миров. И кто-то другой уже пытался предложить это на C ++. Предложение C ++ и Coroutine TS здесь упоминалось несколько раз, но, на мой взгляд, альтернативное предложение под названием Core Coroutines заслуживает большего внимания.

Авторы Core Coroutines предлагают заменить co_await новым токеном операторского типа.
С помощью того, что они называют синтаксисом оператора развертывания , можно использовать как префиксную, так и постфиксную нотацию (без двусмысленности):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

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

Но Rust действительно отличается от C # или JavaScript (или любого другого языка). Поэтому мы не можем слепо копировать то, что делают другие языки.

Пожалуйста, не используйте «различие» в качестве оправдания вашей личной склонности к неортодоксальному синтаксису.

Сколько там отличий? Возьмите любой популярный язык, Java, C, Python, JavaScript, C #, PHP, Ruby, Go, Swift и т. Д., Статический, динамический, скомпилированный, интерпретируемый. Они сильно различаются по набору функций, но все же имеют много общего по синтаксису. Есть ли момент, когда вы чувствуете, что какой-либо из этих слов похож на «мозги»?

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

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

ИМО, вы проиграли споры о потребностях постфиксного синтаксиса. Можно только прибегнуть к «разнице». Это еще одна отчаянная попытка.

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


И, честно говоря, если есть много людей, которые против постфиксного синтаксиса, тогда мы должны просто выбрать синтаксис префикса на данный момент, подождать, чтобы увидеть, как будет написан код с синтаксисом префикса, а затем провести анализ, чтобы увидеть, сколько написанного кода может выиграть от наличия postfix await. Таким образом, мы успокаиваем всех, кого не устраивает такое новаторское изменение, как postfix await, а также получаем хороший прагматический аргумент в пользу использования postfix await.

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

И я думаю, что все это обсуждение синтаксиса действительно сводится к цепочке. Если бы цепочки не было, тогда postfix await полностью исчез бы, и было бы намного проще остановиться только на prefix await. Тем не менее, цепочка - это очень важная вещь в Rust, и поэтому она открывает обсуждение следующих тем:

if we should have only postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
else if we should have only prefix await:
    what's the best syntax for it that:
         isn't ambiguous in the sense of order of operation (useful vs obvious)
else if we should have both prefix and postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
         isn't ambiguous in the sense of order of operation (useful vs obvious)
    should it be a single unified syntax that somehow works for both prefix and postfix?
    would there be clear situations where prefix syntax is favored over postfix?
    would there be a situation where postfix syntax isn't allowed, but prefix is, and vice-versa?

Или что-то вроде того. Если кто-то может предложить лучший образец решения с лучшими пунктами, чем я, пожалуйста, сделайте это! XD

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

@tensorduruk

ИМО, вы проиграли споры о потребностях постфиксного синтаксиса. Можно только прибегнуть к «разнице». Это еще одна отчаянная попытка.

Серьезно, почему бы просто не использовать указанные вами языки вместо неприкрытой враждебности здесь?
Используя вашу логику, у ржавчины должны быть классы, потому что они есть в других традиционных языках. В Rust не должно быть заимствований, потому что в других языках его нет.

Не обрабатывать цепочки, было бы упущенной возможностью. i, e Я бы не хотел создавать привязки для временной переменной, если цепочка может сделать ее анонимной.

Однако я считаю, что мы должны просто придерживаться текущего ключевого слова макроса для await, как в nightly. Сделайте постфиксные макросы еженощной функцией и позвольте людям поиграть с ними. Да, после того, как мы уладим, будут отталкивания, но это можно исправить с помощью rustfix.

@tensorduruk

ИМО, вы проиграли споры о потребностях постфиксного синтаксиса. Можно только прибегнуть к «разнице». Это еще одна отчаянная попытка.

Серьезно, почему бы просто не использовать указанные вами языки вместо неприкрытой враждебности здесь?
Используя вашу логику, у ржавчины должны быть классы, потому что они есть в других традиционных языках. В Rust не должно быть заимствований, потому что в других языках его нет.

Не обрабатывать цепочки, было бы упущенной возможностью. i, e Я бы не хотел создавать привязки для временной переменной, если цепочка может сделать ее анонимной.

Однако я считаю, что мы должны просто придерживаться текущего ключевого слова макроса для await, как в nightly. Сделайте постфиксные макросы еженощной функцией и позвольте людям поиграть с ними. Да, после того, как мы уладим, будут отталкивания, но это можно исправить с помощью rustfix.

пожалуйста, прочтите внимательно. Я сказал, что мы должны предоставить полезные функции, а не странный синтаксис. структура? заимствовать? функции!

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

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

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

Следующее будет приложением к моему сообщению об использовании постфикса @ вместо await


Вместо этого мы должны использовать опыт многих других языков

И мы действительно этим пользуемся. Но это не означает, что мы должны полностью повторить тот же опыт. Кроме того, рассуждая таким образом против @ вы, скорее всего, не добьетесь какого-либо полезного результата, потому что апелляция к предыдущему опыту неубедительна :

Генетические описания проблемы могут быть правдивыми, и они могут помочь пролить свет на причины, по которым проблема приняла свою нынешнюю форму, но они не являются окончательными при определении ее достоинств.

Мы должны использовать await потому что многим это нравится

Эти люди могут любить await по многим другим причинам, а не только потому, что это await . Этих причин может не быть и в Rust. И такие аргументы против @ скорее всего, не принесут никаких новых моментов в обсуждение, потому что обращение к публике неубедительно :

Аргумент ad populum может быть действительным аргументом в индуктивной логике; например, опрос большого числа людей может обнаружить, что 100% предпочитают одну марку продукта другой. Затем можно привести убедительный (сильный) аргумент, что следующий рассматриваемый человек также, скорее всего, предпочтет этот бренд (но не всегда на 100%, поскольку могут быть исключения), и опрос является веским доказательством этого утверждения. Однако это не подходит в качестве аргумента для дедуктивного рассуждения в качестве доказательства, например, чтобы сказать, что опрос доказывает, что предпочтительный бренд превосходит конкурентов по своему составу или что все предпочитают этот бренд другому.

@Pzixel

Но ржавчина - другое дело. У нас здесь ? и мы должны распространять ошибки. В C # async / await автоматически переносит исключения, передает их через точки ожидания и так далее. У тебя его нет в ржавчине. Учтите, что каждый раз, когда вы пишете await на C #, вы должны писать

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

Иметь все эти брекеты очень утомительно.

В C # вы пишете это:

try
{
  var response = await client.GetAsync("www.google.com");
  var json =  await response.ReadAsStreamAsync();
  var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
  // handle exception
}

Что касается Propagate ошибок-цепочки аргументов, например foo().await? , есть ли причина , почему ? не может быть добавлены в await оператора в приставке?

let response = await? getProfile();

Еще одна вещь, которая только что пришла мне в голову: что, если вы хотите match на Future<Result<...>> ? Что из этого легче читать?

// Prefix
let userId = match await response {
  Ok(u) => u.id,
  _ => -1
};
// Postfix
let userId = match response {
  Ok(u) => u.id,
  _ => -1
} await;

Кроме того, могло бы быть использовано выражение async match ? Например, хотите ли вы, чтобы тело выражения совпадения было асинхронным? В таком случае будет разница между match await response и await match response . Поскольку match и await являются эффективными унарными операторами, а match уже является префиксом, было бы легче отличить, если бы await также было префиксом. С одним префиксом и одним постфиксом становится сложно указать, ожидаете ли вы совпадения или ответа.

let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await; // Are we awaiting match or response here?

Если вам нужно дождаться обоих, вы увидите что-то вроде

// Prefix - yes, double await is weird and ugly but...
let userId = await match await response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;
// Postfix - ... this is weirder and uglier
let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await await;

Хотя я думаю, что это могло быть

// Postfix - ... this is weirder and uglier
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;

(Дизайн языка программирования сложен.)

Тем не менее, я собираюсь повторить, что Rust имеет приоритет, когда унарные операторы имеют префикс match , а await - это унарный оператор.

C# // Postfix - ... this is weirder and uglier let userId = match response await { ... } await;

Красота в глазах смотрящего.

Тем не менее, я собираюсь повторить, что Rust имеет приоритет, когда унарные операторы имеют префикс match , а await - это унарный оператор.

? с другой стороны унарный, но постфиксный.

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

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

@markrendle Я не знаю, на что ты

В C # вы пишете это:

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

Что касается Propagate ошибок-цепочки аргументов, например foo().await? , есть ли причина , почему ? не может быть добавлены в await оператора в приставке?

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

Еще одна вещь, которая только что пришла мне в голову: что, если вы хотите match на Future<Result<...>> ? Что из этого легче читать?

// Real postfix
let userId = match response await {
  Ok(u) => u.id,
  _ => -1
};
// Real Postfix 2 - looks fine, except it's better to be
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => ok(-1)
} await;
// Real Postfix 2
let userId = match response await {
  Ok(u) => somethingAsync(u) await,
  _ => -1
};

Как другой пользователь C #, я скажу, что его синтаксисы префиксов: new , await и приведение типов в стиле C больше всего поразили мою интуицию. Я полностью поддерживаю вариант постфиксного оператора.

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

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

@Pzixel

@markrendle Я не знаю, на что ты

В C # вы пишете это:

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

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

В любом случае, как сказал @rolandsteiner , важно то, что мы получаем некоторую форму async / await, поэтому я рад дождаться решения основной команды, и все поклонники постфикса могут дождаться решения основной команды. 😛 ❤️ ☮️

@yasammez Перейдите на C #. В v8.0 мы можем просто использовать new () без имени типа :)

Я просто выскажу несколько идей для оператора postfix.

foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator

Я не говорю, подходит ли постфиксный оператор или нет, но лично я считаю @ одним из самых шумных и странных вариантов из всех возможных. ~ из комментария @phaux кажется более элегантным и менее «загруженным». Кроме того, если я ничего не упускаю, мы еще ни для чего не используем это в Rust.

Мне предложили ~ перед @phaux, хотя я не хочу претендовать на патент; P

Я предложил это, потому что это похоже на говорящее эхо:

Hi~~~~~
Where r u~~~~~

Hay~~~~~
I am in another mountain top~~~~~

~ иногда используется после предложения, чтобы указать завершение, что подходит для await!

Я не могу сказать, достигла ли эта тема пика нелепости или мы что-то здесь находим.

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

Может быть:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};

Мы могли бы ввести триграф вроде ... для пользователей с раскладками клавиатуры, где ~ неудобно.

Сначала я был твердо сторонником использования синтаксиса префикса, необходимого для использования разделителя, как await(future) или await{future} поскольку он настолько однозначен и прост для визуального анализа. Тем не менее, я понимаю предложения других о том, что Rust Future не похож на большинство других Futures на других языках, поскольку он не передает задачу исполнителю немедленно, а является скорее структурой потока управления, которая преобразует контекст в то, что гомоморфна цепочке вызовов монад.

Это заставляет меня думать, что это несколько прискорбно, что сейчас есть путаница в попытке сравнить его с другими языками в этом отношении. Ближайшим аналогом на самом деле является нотация монады do в Haskell или понимание for в Scala (это единственные, с которыми я хорошо знаком). Внезапно я благодарен за рассмотрение предложения уникального синтаксиса, но боюсь, что существование оператора ? одновременно поощряет и препятствует использованию с ним других сигил. Любые другие операторы на основе сигил рядом с ? делают его шумным и сбивающим с толку, например future@? , но прецедент, созданный с помощью постфиксного сигилового оператора, означает, что другой не так уж и смешон.

Таким образом, я убедился в достоинствах оператора постфиксной сигилы. Обратной стороной этого является то, что сигил, который я бы предпочел больше всего, взят типом never. Я бы предпочел ! так как думаю, что future!? заставит меня посмеяться, когда я его напишу, и для меня это имеет самый визуальный смысл. Я полагаю, что следующим будет $ , так как он визуально различим, future$? . Вид ~ прежнему напоминает мне ранние дни Rust, когда ~ был префиксным оператором для распределения кучи. Однако все это очень личное, поэтому я не завидую лицам, принимающим окончательные решения. Будь я ими, я бы, вероятно, просто выбрал безобидный выбор оператора префикса с необходимыми разделителями.

Тем не менее, я понимаю предложения других о том, что Rust Future не похож на большинство других Futures на других языках, поскольку он не передает задачу исполнителю немедленно, а является скорее структурой потока управления, которая преобразует контекст в то, что гомоморфна цепочке вызовов монад.

Я склонен не соглашаться. Указанное вами поведение не является свойством await , а является свойством окружающей async функции или области действия. Выполнение предшествующего кода задерживает не await , а область, содержащая указанный код.

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

Тогда предоставление лучшего глифа и некоторых лигатур для популярных программных шрифтов (или, по крайней мере, для Fira Code Mozilla) может немного улучшить ситуацию.

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


Например, в следующем коде используется отличный от @ символ - :


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()@?
    .error_for_status()?
    .json()@?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

Разверните для сравнения, как это выглядит с обычным ANSI @


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()@?
    .error_for_status()?
    .json()@?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

@norcalli

Сначала я был твердо сторонником использования синтаксиса префикса, необходимого для использования разделителя, как await(future) или await{future} поскольку он настолько однозначен и прост для визуального анализа.

Тогда вам, вероятно, понадобятся однозначные if { cond } , while { cond } и match { expr } ...

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

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

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

@Pzixel

Разница между «запускать до первого ожидания при порождении» и «запускать до первого ожидания при опросе» не так уж велика.

Однако мы говорим не об этом. Мы не говорим о ленивом и нетерпеливом ожидании.

Мы говорим о том, что await присоединяется к ожидаемому Promise (JS) / Task (C #) в исполнителе на других языках, который уже был помещен в исполнитель при конструировании (так что он уже работал в "фоновом режиме"), но в Rust Future s являются инертными конечными автоматами до тех пор, пока await! запущен.

Promise / Task - это дескриптор выполняющейся асинхронной операции. Future - это отложенное асинхронное вычисление. Люди, в том числе известные в Rust, уже делали эту ошибку раньше, и примеры уже приводились в середине этих 500+ комментариев.

Лично я считаю, что это несоответствие семантики достаточно велико, чтобы противодействовать привычности await . Наши Future достигают той же цели, что и Promise / Task , но с другим механизмом.


Как ни странно, для меня, когда я впервые изучил async / await в JavaScript, async было «просто» тем, что я написал, чтобы получить await суперсилу. И способ, которым меня учили получать параллелизм, был a = fa(); b = fb(); /* later */ await [a, b]; (или что бы там ни было, это был век с тех пор, как мне пришлось писать JS). Я считаю, что мнение других людей о async совпадает со мной, поскольку семантика Rust не совпадает с async (дает вам await суперсилу), но с Future строительство и await! .


На данный момент я считаю, что обсуждение различий в семантике async / Future / await в Rust исчерпано, и никакой новой информации не представлено. Если у вас нет новой позиции и / или идеи для обсуждения, вероятно, будет лучше для обсуждения, если мы оставим это обсуждение здесь. (Я был бы рад перенести это на Internals и / или Discord.)

@ CAD97 Да, я понимаю вашу позицию, но думаю, что это не так уж и много.

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

@ CAD97

Люди, в том числе известные в Rust, уже делали эту ошибку раньше, и примеры уже приводились в середине этих 500+ комментариев.

Если даже люди, хорошо знакомые с Rust, совершают эту ошибку, действительно ли это ошибка?

Итак, на Rust All Hands у нас было несколько дискуссий об async-await. В ходе этих обсуждений стало ясно несколько вещей:

Во-первых, в команде разработчиков lang нет единого мнения по поводу синтаксиса await . Очевидно, что существует множество возможностей и веских аргументов в пользу каждого из них. Мы потратили много времени на изучение альтернатив и произвели довольно много интересного. Я думаю, что ближайшим следующим шагом в этом обсуждении будет преобразование этих заметок (вместе с другими комментариями из этой ветки) в своего рода сводный комментарий, в котором излагается суть каждого варианта, а затем продолжить с этого момента. Я работаю над этим с @withoutboats и @cramertj .

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

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

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

Отчет о состоянии async-await:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

По сравнению с моим предыдущим комментарием, он содержит результаты сортировки и некоторые мысли о синтаксисе (но еще не полное описание синтаксиса).

Пометить эту проблему как блокировку для стабилизации async-await, по крайней мере, на время.

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

Краткое описание того, где сейчас стоит async-await

Во-первых, мы надеемся стабилизировать async-await в версии 1.37 , ответвление которой состоится 4 июля 2019 года. Поскольку мы не хотим стабилизировать макрос await! , мы должны решить синтаксический вопрос до этого. Обратите внимание, что эта стабилизация не означает конец пути, а скорее начало. Остается доработать функции (например, async fn в типажах), а также поработать над имплементацией (постоянная оптимизация, исправление ошибок и т. Д.). Тем не менее, стабилизация async / await станет важной вехой!

Что касается синтаксиса, план решения следующий:

  • Для начала мы публикуем обзор дебатов по синтаксису до сих пор - пожалуйста, посмотрите.
  • Мы хотим быть совместимыми с очевидными будущими расширениями синтаксиса: обработка потоков с циклами for в частности (например, цикл JavaScript for await ). Вот почему я работал над серией публикаций по этой проблеме ( первая публикация здесь и другие в будущем).
  • На предстоящем 2 мая собрании lang-team мы планируем обсудить взаимодействие с циклами for, а также разработать план своевременного принятия окончательного решения по синтаксису для стабилизации async / await в 1.37. Мы опубликуем обновление после встречи в этой внутренней ветке .

Рецензия

Запись представляет собой бумажный документ Dropbox, доступный здесь . Как вы увидите, он довольно длинный и приводит множество аргументов взад и вперед. Мы будем признательны за отзывы об этом; вместо того, чтобы повторно открывать этот выпуск (который уже содержит более 500 комментариев), я создал для этой цели внутреннюю ветку .

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

Синтаксис async / await, вероятно, является наиболее ожидаемой функцией, которую Rust получил после 1.0, и синтаксис для await, в частности, был одним из решений, по которому мы получили больше всего отзывов. Спасибо всем, кто участвовал в этих обсуждениях за последние несколько месяцев! Это выбор, по которому у многих людей сильно расходятся чувства; мы хотим заверить всех, что ваши отзывы будут услышаны и окончательное решение будет принято после долгих вдумчивых и внимательных обсуждений.

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