Rust: 解决`await`语法

创建于 2019-01-15  ·  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()

斯卡拉

val result = Await.result(task, timeout)

蟒蛇

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()

斯卡拉

val result = Await.result(task, timeout)

蟒蛇

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的最高帖子中得到了提及(在复选框“ await的最终语法”旁边)。

科特林

val result = task.await()

Kotlin的语法是:

val result = doTask()

await只是suspendable function ,不是一流的东西。

谢谢你提到那件事。 Kotlin感到更加隐含,因为默认情况下期货急切。 但是,在延迟块中使用该方法等待其他延迟块仍然是常见的模式。 我当然已经做过几次了。

@cramertj既然在https://github.com/rust-lang/rust/issues/50547中有276条评论,您可以总结一下其中所做的论点,以使其在此处不再重复吗? (也许在这里将它们添加到OP中?)

Kotlin感到更加隐含,因为默认情况下期货急切。 但是,在延迟块中使用该方法等待其他延迟块仍然是常见的模式。 我当然已经做过几次了。

也许您应该在两个用例中添加一些上下文/描述。

还有其他使用隐式等待的langs,例如go-lang呢?

支持后缀语法的一个原因是,从调用者的角度来看,await的行为与函数调用非常相似:您放弃流控制,当您将其返回时,结果正在等待堆栈。 无论如何,我更喜欢通过包含函数形容词来包含类函数行为的语法。 并且有充分的理由想要将协程的构造与首次执行分开,以使此行为在同步块和异步块之间保持一致。

但是,尽管对隐式协程样式进行了辩论,而我站在显性立场,但.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
}

为避免代码不可读并有助于解析,请仅在问号后留空格,不要在问号和开放的paran之间留空格。 因此, future(? )很好,而future( ?)不好。 在future(await)的情况下不会出现此问题,在该情况下所有当前令牌都可以像以前一样使用。

与其他后缀运算符(例如当前的? -try)的交互也就像在函数调用中一样:

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!()
  • 这可以推广到生成器和流,以及期望在恢复时提供更多参数的生成器。 本质上,这表现为FnOnceStreams表现为FnMut 。 附加的参数也很容易容纳。
  • 对于那些谁使用当前Futures之前,该如何捕获一个?Poll应该(在此不幸的稳定序列)所有的工作一起。 作为学习步骤,这也与期望基于?的运算符转移控制流相一致。 另一方面, (await)不能满足此要求,但毕竟该功能总是希望在发散点处恢复。
  • 它确实使用类似函数的语法,尽管仅当您同意我的意见时,此参数才有用::mile:

和不喜欢的原因:

  • ?似乎是一个参数,但它甚至不应用于表达式。 我相信这可以通过教学解决,因为它似乎应用到的标记是函数调用本身,这在某种程度上是正确的概念。 我希望这也肯定地意味着语法是明确的。
  • 括号和?更多(和不同)组合可能很难解析。 特别是当您有一个未来返回另一个未来的结果时: construct_future()(?)?(?)? 。 但是您可以为能够得到fn对象的结果做出相同的论点,从而导致这样的表达式: foobar()?()?()? 。 但是,由于我从未见过这种用法或抱怨,因此在这种情况下分割成单独的语句似乎很少需要。 construct_future()(await)?(await)?也不存在此问题-
  • future(?)是我的最佳选择,语法仍然很简洁。 但是,其推理基于协同程序中的实现细节(临时返回并在简历上分派),这可能使其不适合抽象。 future(await)将是一个替代方案,在将await作为关键字内部化之后,仍然可以解释,但是参数位置对我来说有点难以接受。 可能很好,并且当协程返回结果时,它当然更具可读性。
  • 是否干扰其他函数调用建议?
  • 你自己? 您不需要喜欢它,只是至少不提出这种简短的后缀语法感觉很浪费。

future(?)

Result没什么特别的:期货可以返回任何Rust类型。 碰巧有些期货返回Result

那么,对于不返回Result期货,它将如何工作?

似乎不清楚我的意思。 future(?)是先前讨论的future.await!()或类似内容。 将来也返回结果的分支将是future(?)? (我们可以尽早放弃控制流的两种不同方式)。 这使得将来轮询(?)和结果测试?正交。 编辑:为此添加了一个额外的示例。

在也返回结果的Future上分支将是future(?)?

感谢您的澄清。 在那种情况下,我绝对不喜欢它。

这意味着调用返回Future<Output = Result<_, _>>的函数将类似于foo()(?)?

这是非常繁重的语法,它使用?来实现两个完全不同的目的。

如果它特别是给运算符?的提示,那么当然可以用新保留的关键字替换它。 我只是在最初才认为这感觉太像是一个令人费解的实际争论,但是这种权衡可以帮助精神分析语句。 因此,相同的impl Future<Output = Result<_,_>>语句将变为:

  • foo()(await)?

为什么?合适的最好论据是所使用的内部机制有些相似(否则我们不能在当前库中使用Poll ),但这可能会错过成为一个好的抽象的意义。

语法非常繁重

我认为这就是明确等待的全部内容吗?

它用 ? 用于两个完全不同的目的。

是的,所以foo()(await) -syntax会好很多。

这种语法就像调用返回闭包的函数,然后在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) 。 键入不是没有道理的,并且仍然保留了本应引起的所有函数调用语法的清晰度。

您希望看到多少次->暗示未来>在异步代码库中?

我希望能一直看到与之等效的类型(返回结果的异步fn),甚至可能是所有异步fn中的大多数,因为如果您正在等待的甚至是IO,则几乎肯定会抛出IO错误向上。


链接到我先前关于跟踪问题的文章,并添加了更多想法。

我认为不包含字符串await语法接受这种语法的可能性很小。 我认为,在为此功能工作了一年之后,尝试权衡已知可行替代方案的优缺点,以找出最佳方案比提出新语法要好得多。 考虑到我以前的帖子,我认为语法是可行的:

  • 前缀带有强制定界符。 这也是什么定界符(括号或括号或接受两者;所有这些都有其优缺点)的决定。 即await(future)await { future } 。 这完全解决了优先权问题,但是在语法上比较嘈杂,并且两个定界符选项都可能引起混淆。
  • 前缀以?的“有用”优先级等待。 (也就是说,等待绑定比?更紧密)。 这可能会使某些用户阅读代码感到惊讶,但是我认为返回结果期货的功能将比返回期货结果的功能更为普遍。
  • 前缀以?的“明显”优先级等待。 (也就是说,绑定比等待绑定更紧密)。 额外的语法糖await?用于等待和? 操作员。 我认为必须使用这种语法糖才能使该优先顺序完全可行,否则每个人都会一直在写(await future)? ,这是我列举的第一个选项的较差变体。
  • 使用语法空间等待Postfix。 这通过在两个运算符之间具有清晰的视觉顺序来解决优先级问题。 在很多方面,我对此解决方案感到不安。

每次检查问题时,我对这些选择的排名都会改变。 到目前为止,我认为在糖上使用明显的优先级似乎是人体工程学,熟悉程度和理解力之间的最佳平衡。 但是在过去,我偏爱其他两种前缀语法中的任何一种。

为了便于讨论,我将为这四个选项命名:

姓名| 未来| 结果的未来| 未来的结果
--- | --- | --- | ---
强制分隔符| 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?
后缀关键字| future await | future await? | future? await

(我专门使用“ postfix关键字”将该选项与“ postfix宏”之类的其他postfix语法区分开。)

在“有用”优先级中“祝福” 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根本上改变了?语法的上下文中,给定类型T的部分表达式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的其余语言都竭尽全力避免这种情况,以使“表达式从左到右进行评估”,其中一个示例是自动引用魔术。

示例显示,对于具有多个等待/尝试/组合点的较长链,情况会变得更糟。

// 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)?;
}

我希望避免出现的情况是,创建C的类型解析的Rust模拟,在您之间跳转
“指针”和“数组”组合器的表达式的左侧和右侧。

@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,但我不喜欢@withoutboats所示的变体,主要是出于相同的原因。 例如。 foo await.method()令人困惑。

首先让我们看一个类似的表,但是添加几个其他的postfix变体:

| 姓名| 未来| 结果的未来| 未来的结果
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| 强制分隔符| await { future } | await { future }? | await { future? } |
| 有用的优先级| await future | await future? | await (future?) |
| 优先顺序| await future | await? future | await future? |
| 后缀关键字| future await | future await? | future? await |
| 后缀字段| future.await | future.await? | future?.await |
| 后缀方法| future.await() | future.await()? | future?.await() |

现在,让我们看一下连锁的未来表达:

| 姓名| 结果链式期货|
| ---------------------- | -------------------------- ----------- |
| 强制分隔符| await { await { foo() }?.bar() }? |
| 有用的优先级| await (await foo()?).bar()? |
| 优先顺序| await? (await? foo()).bar() |
| 后缀关键字| foo() await?.bar() await? |
| 后缀字段| foo().await?.bar().await? |
| 后缀方法| foo().await()?.bar().await()? |

现在以一个真实示例为例,从reqwests ,在该位置您可能要等待链式的未来结果(使用我首选的await表单)。

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宏需要较少的精力来实现,并且易于理解和使用。

另外,postfix宏需要较少的精力来实现,并且易于理解和使用。

而且它只是使用一个通用的lang功能(或者至少看起来像一个普通的postfix宏)。

postfix宏会很好,因为它结合了postfix的简洁性和可链接性以及非魔术属性和宏的明显存在,并且非常适合第三方用户宏,例如某些.await_debug!().await_log!(WARN).await_trace!()

postfix宏会很不错,因为它结合了宏的非魔术属性

@novacrazy这个参数的问题是任何await!宏_would_都是魔术,它执行的操作在用户编写的代码中是不可能的(当前基于底层生成器的实现有些公开,但是我的理解是,在稳定之前,此功能将完全隐藏(并且实际上与之交互目前需要使用一些rustc -反正内部夜间功能)。

@ Nemo157嗯。 我不知道它原本是如此的不透明。

重新考虑使用#[async]类的过程宏来进行从“异步”功能到生成器功能而不是魔术关键字的转换是否为时已晚? 它是要键入的三个额外字符,可以在文档中以#[must_use]#[repr(C)]相同的方式进行标记。

我真的不喜欢隐藏这么多直接控制执行流程的抽象层的想法。 感觉与Rust是对立的。 用户应该能够完全跟踪代码并弄清楚一切工作原理以及执行的位置。 应该鼓励他们擅自闯入并欺骗系统,如果他们使用安全的Rust,那就应该是安全的。 如果我们失去低水平的控制权,这并没有改善任何事情,我也最好坚持使用原始期货。

我坚信Rust(不是std / core )的语言,仅在用户(或极不切实际)或std无法做到的情况下,才应提供抽象和语法。 。 在这方面,整个异步事情已经失控了。 除了rustc的pin API和生成器,我们真的需要更多东西吗?

@novacrazy我通常同意这种观点,但不同意结论。

仅当用户或std无法(或高度不切实际)执行抽象和语法时,才应提供抽象和语法。

语言中有for -loops的原因是什么,因为它们也可能是变成带有中断的loop的宏。 || closure可能是专用特征和对象构造函数的原因是什么? 为什么我们已经有try!()时引入? try!() 。 我不同意这些问题和您的结论的原因是一致性。 这些抽象的重点不仅在于它们封装的行为,还在于其可访问性。 for -replacement在可变性,主代码路径和可读性方面发生了故障。 || -replacement在声明的详细程度上有所下降,类似于当前的Futurestry!()按预期的表达式和可组合性顺序分解。

考虑到async不仅是函数的装饰器,还考虑了通过aync-blocksasync ||提供其他模式的想法。 由于它适用于不同的语言项目,因此宏的可用性似乎不是最佳的。 如果它必须是用户可见的,那就不用考虑实现了。

用户应该能够完全跟踪代码并弄清楚一切工作原理以及执行的位置。 应该鼓励他们擅自闯入并欺骗系统,如果他们使用安全的Rust,那就应该是安全的。

我认为这种说法不适用,因为完全使用std api实现协程可能会严重依赖unsafe 。 然后有一个相反的论点,因为尽管它可行的,并且即使语言有句法和语义的方式,您也不会停下来,但是任何更改都将有极大的可能打破用unsafe假设的风险。 extern "rust-call" ,它是当前的魔术,它清楚地表明函数调用没有任何这种保证。 我们可能实际上从未有return ,即使stackful协程的命运还没有到决定。 我们可能将优化挂接到编译器中。

旁白:说到这,从理论上讲,还不如完全严肃的想法,可以协同程序的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!()

公平地说,我也反对这一观点,尽管它已经在我身上发展了。 如果Try曾经稳定过,那会更容易接受,但这是另一个话题。

您给出的“糖”示例的问题在于它们是非常非常稀薄的糖。 即使impl MyStruct是多了还是少糖为impl <anonymous trait> for MyStruct 。 这些是生活用糖,可以增加零开销。

相比之下,生成器和异步函数会增加很多无关紧要的开销和大量的精神开销。 特别地,生成器很难以用户的身份实现,并且可以更有效,更轻松地用作语言本身的一部分,而异步可以在此之上轻松实现。

不过,关于异步块或闭包的观点很有趣,我承认关键字在那里会更有用,但我仍然反对在必要时无法访问较低级别的项目。

理想情况下,支持async关键字#[async]属性/过程宏将是很棒的选择,前者允许对生成的(无双关)生成器进行低级访问。 同时yield应在使用块或功能被禁止async作为关键字。 我确信他们甚至可以共享实现代码。

至于await ,如果两个以上是可能的,我们可以做同样的事情,并限制关键字awaitasync关键字功能/块,并使用某种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

    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对于一些成员而言最为简洁,因此,在迁移到其他成员之前先对其进行强烈评估似乎是可行的。

比较将是syntaxexample (来自@mehcode的reqwest看起来像是在这方面可用的真实基准),然后是表格( 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::特性? |

      | 不是函数调用 |

  • 后缀通话foo()(await)?

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

    • | 关注| 分辨率|

      |-|-|

      | 可以与实际参数混淆关键字+突出显示+不重叠|

  • Postfix宏foo().await!()?

    • 示例: client.get("https://my_api").send().await!()?.json().await!()?

    • | 关注| 分辨率|

      |-|-|

      | 实际上不会是宏……| |

      | …或await不再是关键字| |

从可能合并生成器的角度出发,对后缀与前缀的另一种思考:考虑值, yieldawait占据两种相反的语句。 前者从您的函数到外部提供一个值,后者接受一个值。

旁注:好吧,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?;

我有点像这样,因为您可以说它是模式的一部分。

与等待绑定有关的问题是错误的故事不太好。

// 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();

awaitawait?都需要作为关键字添加,或者我们也将其扩展到let ,即let? result = 1.divide(0);

考虑到Rust代码中使用链接的频率,我完全同意,对于链接的等待者来说,尽可能清晰地了解链接很重要。 对于await的postfix变体:

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

这通常与我期望Rust代码的行为类似。 我确实有一个问题,在这种情况下await()感觉就像一个函数调用,但是在表达式的上下文中具有神奇的(非函数类)行为。

后缀宏版本将使这一点更加清晰。 人们习惯了锈蚀感叹号,意思是“这里有魔术”,因此我当然更喜欢此版本。

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

话虽如此,值得考虑的是,我们已经try!(expr)的语言,并且它是?前身。 添加一个await!(expr)现在的宏将与如何完全一致try!(expr)?被引入的语言。

使用await!(expr)版本的await,我们可以选择稍后迁移到postfix宏,或者添加新的?样式运算符,以使链接变得容易。 一个类似于?的示例,但等待:

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

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

我认为我们现在应该使用await!(expr)await!{expr} ,因为它既合理又实用。 然后,我们可以计划在是否将一次postfix宏变成一个东西之后,再迁移到await的postfix版本(即.await!.await!() )。 (或者最终在增加了关于主题的问题之后,添加了一个额外的?样式运算符...的路线:)

仅供参考,Scala的语法不是Await.result因为这是一个阻塞调用。 Scala的Futures是monad,因此使用常规方法调用或for monad理解:

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
}

这强烈反映了我希望rust代码的样子,并使用了强制定界符,因此,我想与其他人保持一致,以保持当前语法await!() 。 我认为,早期的Rust是沉重的象征,并且出于充分的理由而被移开了。 和往后一样,以后缀运算符(或您所拥有的形式)形式使用语法糖是向后兼容的,并且await!(future)的清晰度是明确的。 如前所述,它也反映了我们使用try!的进度。

将其保留为宏的好处是,一眼就可以看出它是语言功能,而不是普通的函数调用。 如果不添加! ,则突出显示编辑器/查看器的语法将是发现调用的最佳方法,我认为依赖这些实现是一个较弱的选择。

我的两分钱(不是定期供款人,fwiw)我最喜欢复制try! 。 它曾经做过一次,并且运行良好,并且在它变得非常流行之后,有足够的用户考虑使用后缀运算符。

所以我的投票是:用await!(...)稳定,然后在一个postfix运算符上平移,以基于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!() ,现在并最终决定如何对其进行后缀处理。 如果我们可以牢记支持将自动将await!()调用转换为后缀等待的rustfix,这还有待确定,甚至更好。

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 } | 强大:

  • 非常简单
  • 平行forloopasync等。
弱:
  • 非常冗长(5个字母,2个大括号,3个可选的,但可能是倾斜的空格)
  • 链接会产生许多嵌套的括号( await { await { foo() }?.bar() }?
|
| await f | 强大:
  • 从Python,JS,C#和Dart并行await语法
  • 简单,简短
  • 有用的优先级和明显的优先级在?await fut? vs. await? fut )中表现良好
弱:
  • 模棱两可:必须学习有用与明显的优先顺序
  • 链接也很麻烦( await (await foo()?).bar()? vs. await? (await? foo()).bar()
|
| fut.await
fut.await()
fut.await!() | 强大:
  • 允许非常容易的链接
  • 很好的代码完成
弱:
  • 愚弄用户以为这是在某个地方定义的字段/功能/宏。 编辑:我同意@jplatte await!()觉得最神奇
|
| fut(await) | 强大:
  • 允许非常容易的链接
弱:
  • 愚弄用户以为在某个地方定义了await变量,并且可以像函数一样调用Future
|
| 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不必成为关键字
  • forwards-compatible:允许以@operator的形式添加新的postfix运算符。 例如, ?可以作为@try
  • 我的大脑将client.get("https://my_api").send()@await.unwrap().json()@await.unwrap()分组为正确的组(首先按.分组,然后按@分组)
弱:
  • 与所有其他语言使用@的方式不同
  • 可能会增加太多不必要的后缀运算符
|

我的分数:

  • 熟悉程度(fam):此语法与已知语法(Rust和其他语法,例如Python,JS,C#)的距离有多近
  • 明显性(obv):如果您是第一次使用别人的代码阅读此书,您是否可以猜测其含义,优先顺序等?
  • 详细程度(vrb):要写多少个字符
  • 可见性(可见性):在代码中发现(相对于忽略)有多么容易
  • chaining(cha):与.和其他await s链接起来很容易
  • 分组(grp):我的大脑是否将代码分组为正确的块
  • forwards-compatibility(fwd):是否允许以后以不间断的方式进行调整

| 语法| 家族| 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前缀与? (通常会使用)一起使用时,会导致不明显的优先级或周围的括号。

Postfix .await!()很好地链接了一下,其含义立刻就很明显了,其中包括!来告诉我它会去做魔术,并且在语法上不太新颖,因此采用“下一个剪切”方法个人会喜欢这个。 就是说,对我来说,与第一次削减await! (expr) ,它将改善实际代码多少还有待观察。

对于简单情况,我更喜欢使用前缀运算符:
let result = await task;
考虑到我们不写结果的类型,这感觉更自然,因此从左到右阅读以了解结果是等待任务时,await在精神上会有所帮助。
像这样想象:
let result = somehowkindoflongtask await;
直到您未完成任务的末尾,您才意识到必须等待它返回的类型。 还请记住(尽管这可能会发生变化,并且不会直接与语言的未来联系在一起),作为Intellij的IDE会在名称和等号之间插入类型(如果可能的话,即使没有任何个性化设置)。
图片如下:
6voler6ykj

这并不意味着我的观点是对前缀的百分之一百。 当涉及结果时,我非常喜欢将来的后缀版本,因为这感觉更自然。 在没有任何上下文的情况下,我可以轻松分辨出其中的含义是:
future await?
future? await
相反,从新手的角度来看这两个是正确的:
await future? === await (future?)
await future? === (await future)?

我赞成使用前缀关键字: await future

它是大多数具有异步/等待功能的编程语言所使用的语言,因此,熟悉其中一种语言的人会立即熟悉它。

至于await future?的优先顺序,常见情况是什么?

  • 返回Result<Future>的函数,必须等待。
  • 必须等待的Result返回Future<Result>

我认为在处理典型方案时第二种情况更为常见,因为I / O操作可能会失败。 因此:

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函数,该函数会占用将来,并仅在其上运行await!()宏。

C ++(并发TR)

auto result = co_await task;

这是在协程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可能是保证TCO的关键字

感谢@EyeOfPython的[概述]。 这对于刚加入自行车棚的人们来说尤其有用。

就个人而言,我希望我们远离f await ,因为它在语法上非常简单,并使其显得有些特殊和神奇。 这将成为Rust的新用户感到困惑的许多事情之一,我觉得这值得增加,即使对于Rust的退伍军人也没有增加太多的清晰度。

@novacrazy这个参数的问题是任何await!宏_would_都是魔术,它执行的操作在用户编写的代码中是不可能的

@ Nemo157我同意await!宏是神奇的,但是我认为这不是问题。 std已经有多个这样的宏,例如compile_error!而且我还没有看到有人抱怨它们。 我认为使用宏仅了解其作用而不是其作用是正常的。

我同意以前的评论者的观点,后缀将是最符合人体工程学的,但是我宁愿从前缀宏await!(expr) ,一旦有可能就过渡到后缀宏而不是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如何通过语法突出显示来呈现它,让我们用match替换await ,因为它们的长度相等:

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使用IMHO的样式确实很陌生。 它与具有类似功能的任何现有语法都不相似。

@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关键字的任何其他语言。

恕我直言,唯一不会显着增加Rust的陌生性的postfix语法是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&?

对我来说, @感觉像个character肿的角色,分散了阅读量。 另一方面, &?感觉很愉快,不会分散我的阅读兴趣,并且在&?之间有一个不错的顶部空间。

我个人认为应该使用一个简单的await!宏。 如果后置宏进入该语言,则可以简单地扩展该宏,不是吗?

@Laaas正如我希望的那样,Rust中还没有postfix宏,因此它们是新语法。 另请注意, foo.await!.barfoo.await!().bar不是相同的表面语法。 在后一种情况下,将有一个实际的后缀和内置宏(需要放弃await作为关键字)。

@jplatte

有很多前缀关键字,其中一些是在Rust 1.0之后添加的(例如union ),

union不是一元表达式运算符,因此在此比较中无关紧要。 Rust中恰好有3个稳定的一元前缀运算符( returnbreakcontinue ),所有这些都以!键入。

嗯...有了@以前的建议看起来会好一点:

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个稳定的一元前缀运算符( returnbreakcontinue ),所有这些都以!键入。

这仍然意味着即使关键字键入不同,前缀关键字变体也适合一元表达式运算符组。 postfix-keyword变体与现有语法的差异要大得多,在我看来,相对于await在该语言中的重要性而言,差异太大。

关于前缀宏和过渡到后缀的可能性: await需要保持关键字才能起作用。 然后,该宏将是一些特殊的lang项目,其中允许使用关键字作为名称而无需额外的r# ,而r#await!()可以但可能不应调用同一宏。 除此之外,它似乎是最实用的解决方案。

也就是说,将await保留为关键字,但使await!解析为lang-item宏。

@HeroicKatora为什么需要保留一个关键字才能起作用?

@Laaas因为如果要保持转换的可能性,我们需要保持开放状态,以备将来使用它作为关键字的

@Centril

还要注意,foo.await!.bar和foo.await!()。bar是不同的表面语法。 在后一种情况下,将有一个实际的后缀和内置宏(这需要放弃await作为关键字)。

将关键字await!为一个内部宏(无法通过常规方法定义),是否可以解决? 然后,它仍然是关键字,但以其他宏语法解析为宏。

@HeroicKatora为什么x.await!()中的await是保留关键字?

并非如此,但是如果我们不解决补丁后问题,那么它不一定是我们在以后的讨论中得到的解决方案。 如果那是唯一商定的最佳可能性,那么我们首先应该采用这种确切的后缀语法。

这是我们又一次遇到了作为后缀运算符工作得更好的东西。 最大的例子是try! ,我们最终给了它自己的符号? 。 但是,我认为这不是最后一次使后缀运算符更优化,并且我们不能完全赋予其自己的特殊字符。 因此,我认为我们至少不应以@ 。 如果我们有办法做这种事情,那就更好了。 这就是为什么我支持postfix宏样式.await!()

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

但是要使后缀宏有意义,就必须引入它们自身。 因此,我认为最好从正常的await!(foo)宏语法开始。 如果我们真的觉得这很重要,可以保证使用自己的符号,则可以稍后将其扩展为foo.await!()甚至foo@
这个宏需要一点魔术对std来说并不陌生,对我来说也不是什么大问题。
正如@jplatte所说:

@ Nemo157我同意await!宏是神奇的,但是我认为这不是问题。 std已经有多个这样的宏,例如compile_error!而且我还没有看到有人抱怨它们。 我认为使用宏仅了解其作用而不是其作用是正常的。

我在这里讨论的一个经常出现的问题是关于链接以及如何使用
等待链接表达式。 也许还有其他解决方案?

如果我们不想将await用于链接而仅将await用于
分配,我们可能会像只是将let替换为await:
await foo = future

然后对于链接,我们可以想象某种操作,例如await res = fut1 -> fut2await res = fut1 >>= fut2

唯一缺少的情况是等待并返回结果,一些快捷方式
await res = fut; res
使用简单的await fut可以轻松完成此操作

我认为我没有见过这样的其他建议(至少对于
链接),因此将其放到我认为很好用的位置。

@HeroicKatora我已将fut(await)到列表中,并根据我的意见对其进行了排名。

如果有人觉得我的得分不对,请告诉我!

在语法方面,我认为.await!()是干净的,并允许链接,而不会在语法中添加任何奇怪之处。

但是,如果我们得到了“真实的”后缀宏,那将有些奇怪,因为大概.r#await!()可能会被遮盖,而.await!()不会。

我强烈反对“ postfix keyword”选项(以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版本替换此语法。

一旦解决了该问题并将其实现到语言中,我们就可以继续讨论后缀等待语法,希望它具有更成熟的经验,想法以及可能的其他编译器功能。

@HeroicKatora我对它的评分较高,因为它可以自然地释放await用作普通标识符,并允许添加其他后缀运算符,而我认为fut.await!()最好是await已保留。 但是,我不确定这是否合理, fut.await!()可能更高也似乎是肯定的。

对于await { future }await!(future) ,后一个选项保持打开状态,几乎可以更改为其他任何选项,而第一个选项仅允许await future变体中的任何一个(如@withoutboats的博客条目中所述)。 因此,我认为绝对应该是fwd(await { future }) < fwd(await!(future))

至于详细程度,您是正确的,在将案例划分为子组之后,我没有重新评估详细程度,这应该是最客观的一项。

我将对其进行编辑以考虑您的评论,谢谢!

稳定await!(future)是我能想到的最坏的选择:

  1. 这意味着我们必须保留await ,这进一步意味着将来的语言设计会变得更加困难。
  2. 有意地,它采用与我们弃用的try!(result)相同的路线(并且需要在Rust 2018中编写r#try!(result) )。

    • 如果我们知道await!(future)是错误的语法,那么我们最终打算弃用它,这将故意增加技术负担。

    • 而且, try!(..)是在Rust中定义的,而await!(future)不能,而将是编译器魔术。

    • 弃用try!(..)并不容易,并且会给社会造成巨大的损失。 再次经历这种磨难似乎没有吸引力。

  3. 这将使用宏语法作为语言的核心和重要部分; 这显然不是一流的。
  4. await!(future)很吵; 与await future您需要编写!( ... )
  5. Rust API,尤其是标准库,以方法调用语法为中心。 例如,在处理Iterator s时链接方法是很平常的事。 与await { future }await future类似, await!(future)语法将使方法链接变得困难,并导致临时的let绑定。 这对人体工程学和我的可读性都是不利的。

@Centril我想同意,但有一些未解决的问题。 您确定有关1.吗? 如果我们将其设置为“魔术”,是否可以通过允许其引用宏而不将其作为关键字删除而使它变得更魔术?

我加入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#协程,它们都使用与之非常相似的语法。 经过多年在生产代码中使用它们的经验,我的信念是,一目了然知道您的屈服点在哪里至关重要。 当您在函数的缩进行上略过时,您可以在几秒钟内挑选出200行函数中的每个co_await / yield return ,而没有任何认知负担。

事后等待的点运算符或某些其他后缀“符号堆”语法不一样。

我相信await是基本的控制流操作。 应该给予它与'ifwhilematchreturn相同的尊重水平。 想象一下,如果其中任何一个是后缀运算符-读取Rust代码将是一场噩梦。 就像我等待参数一样,您可以浏览任何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.吗? 如果我们将其设置为“魔术”,是否可以通过允许其引用宏而不将其作为关键字删除而使它变得更魔术?

从技术上讲? 可能吧但是,在特殊情况下应该有充分的理由,在这种情况下,魔术般地魔术似乎是不合理的。

这就提出了一个问题,即我们是否打算以async/await作为激励来迁移到版本2018还是耐心等待而不重复此操作。

我不知道我们是否曾经说过要使用新的模块系统, async / awaittry { .. }作为奖励; 但不管我们的意图是什么,我认为这是一件好事。 我们希望人们最终开始使用新的语言功能来编写更好,更惯用的库。

另外两个(imho)中央功能非常类似于3.vec![]format! / println! 。 前者非常重要,因为没有稳定的盒装结构afaik,

前者存在,并被写入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读为“ then 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,通常的格式设置规则用后缀语法来弥补这一点。 我还认为yieldpoint在前缀语法中比较晦涩,因为await实际上并没有放置在您屈服的代码点,而是相反。

如果您采用这种方式,让我们为清楚起见进行实验,而不是本着await代替let 。 没有其他语法扩展,因为它们看起来像是一个延伸。

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

但是对于这些都不是,我没有看到足够的好处来解释它们在语法上的可组合性损失和复杂性。

嗯...既要有前缀又要有后缀形式的await吗? 还是只是后缀形式?

在谈论时,链接方法的调用特别常见
迭代器和较小程度的选择,但除此之外,锈是
首先是命令式语言,然后是功能性语言。

我不是在说后缀实际上是难以理解的,我叫
认知负载的论点隐藏了顶层控制流操作,
具有与“返回”相同的重要性,当
与将其尽可能靠近行的开头相比-我
基于多年的生产经验来提出这一论点。

2019年1月19日星期六,上午11:59 Mazdak Farrokhzad [email protected]
写道:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril我想同意,但是有
一些悬而未决的问题。 您确定1.吗? 如果我们使它“神奇”可以
我们不能通过允许它引用宏而不使其变得更神奇
将其作为关键字删除?

从技术上讲? 可能吧但是,应该有充分的理由
特殊情况下,在这种情况下似乎无法魔咒
有道理。

这就提出了一个问题,我们是否打算进行异步/等待
迁移到2018版的诱因或宁愿耐心而不是
重复一遍。

我不知道我们是否曾经说过要用于新的模块系统async /
等待,并尝试{..}作为奖励; 但无论我们的意图如何
是的,我认为这是一件好事。 我们希望人们最终
开始使用新的语言功能来编写更好,更惯用的语言
库。

其他两个(imho)中央功能非常类似于3 .: vec![]和format!/
println!。 前者非常多,因为没有稳定的盒装
建筑afaik,

前者存在,并被写入vec![1、2、3,..]以模仿数组
文字表达式,例如[1、2、3,..]。

@ejmahler https://github.com/ejmahler

“ x.await”的读法像英语一样诱人,但当x为a时却不会
不平凡的行,例如带有长名称的成员函数调用或一堆
链式迭代器方法等

一堆链式迭代器方法有什么问题? 那很明显
惯用的Rust。
rustfmt工具还将在不同行上格式化方法链,因此您可以
获取(再次使用match显示语法高亮显示):

让foo = alpha()。match? //或alpha() match?alpha()#match?alpha().match!()?
.beta
.some_other_stuff()。match?
.even_more_stuff()。match
.stuff_and_stuff();

如果您将.await读为“ then 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
未来-> T和? :impl试试->T。

有例外,但它们是例外,不是我们应该做的事情
争取。

例外是? ?

@andreytkachenko https://github.com/andreytkachenko

我同意@ejmahler https://github.com/ejmahler 。 我们不应该
忘记开发的另一面-代码审查。 包含源代码的文件为
读后写的次数要多得多,因此我觉得应该更容易
阅读并理解后再写作。

围绕最佳可读性和最佳性的分歧
人机工程学。

更容易理解,而不是这样:

...让正文:MyResponse = client.get(“ https:// my_api”).send()。await?.into_json()。await ?;

这不是格式化的方式。 惯用的rustfmt格式
是:

let正文:MyResponse =客户端
.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一词而不是符号,则更容易将grep代码用于正在等待的任何内容如@或#,其含义与语法有关。

你们这不是火箭科学吗

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

postfix ...具有极高的可读性,一目了然,而且非常直观,因为您自然地将其读为代码,它在等待未来结果出现时会逐渐消失。 由于每个人以前都已经看到过椭圆形,因此不需要优先级/宏观的恶作剧,也不会因陌生的信号而产生额外的线路噪音。

(对@solson表示歉意)

@ ben0x539这是否意味着我可以访问结果的成员,例如future()....start ? 还是等待range().....这样的范围结果? 从目前的省略号..到现在,您对no precedence/macro shenanigans necessary and确切含义是必须是右侧的二元运算符或括号,因此一目了然。

是的 ? 运算符存在。 我已经承认有
例外。 但这是一个例外。 任何情况下的绝大多数控制流程
Rust程序通过前缀关键字进行。

在2019年1月19日星期六1:51 PM Benjamin Herr [email protected]
写道:

你们这不是火箭科学吗

让正文:MyResponse = client.get(“ https:// my_api”).send()...?。into_json()...?;

postfix ...可读性极强,一目了然,不容错过
直观,因为您自然而然地将其读为代码形式
在等待未来的结果出现时。 没有
优先/宏恶作剧是必需的,并且没有来自
陌生的信号,因为每个人以前都看过椭圆。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在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 exprbreak expr 。 前者比后者更普遍。 continue 'label表单实际上并没有计数,因为它是一个表达式,但不是keyword expr表单。 因此,现在您有2个整个前缀关键字一元表达式形式和1个后缀一元表达式形式。 在我们甚至没有考虑?awaitawaitreturn更相似之前,我几乎不称呼return/break expr ?作为例外的规则。

任何Rust程序中的绝大多数控制流都是通过前缀关键字发生的。

如前所述, break expr并不是那么普遍( break;是更典型的,而return;是更典型的,它们不是一元表达形式)。 剩下的只是早期的return expr; s,对我来说似乎一点也不清楚,这比match? ,只是嵌套if let更普遍else s,以及for循环。 一旦try { .. }稳定下来,我期望?会被更多使用。

@ ben0x539我认为一旦我们准备好使用它们,我们应该为可变参数泛型保留...

我真的很喜欢在这里使用后缀语法进行创新的想法。 它对流程的意义更大,我记得当我们从前缀try!变为后缀?时,代码变得更好

如果我们不喜欢.await的想法,我敢肯定,可以找到一些真正的后缀运算符的创意。 一个示例可能只是使用++@等待。

:(我只是不想再等了。

每个人都对宏语法感到满意,这个线程中大多数以其他观点开头的人似乎最终都赞成宏语法。

当然这将是一个“魔术宏”,但用户很少关心宏扩展的外观,对于那些这样做的人来说,很容易在文档中解释细微差别。

常规宏语法有点像苹果派,它是每个人的第二喜欢的选择,但因此成为家庭的最喜欢的选择[0]。 重要的是,喜欢尝试! 我们随时可以更改它。 但最重要的是,我们所有人都越早同意,我们所有人就越早可以开始实际使用它并提高生产力!

[0](在第一分钟引用) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=zh-CN

匹配,如果是否允许,则何时,何时让和为所有控制流
使用前缀。 假装中断和继续是唯一的控制流程
关键字令人误解。

2019年1月19日星期六,下午3:37 Yazad Daruvala [email protected]
写道:

:(我只是不想再等了。

每个人都熟悉宏语法,该线程中的大多数人认为
从其他观点开始似乎最终倾向于使用宏语法。

当然它将成为“魔术宏”,但用户很少关心宏
扩展看起来像,对于那些这样做的人,很容易解释
文档中的细微差别。

常规宏语法有点像苹果派,这是每个人的第二个
最喜欢的选项,但结果是家庭最喜欢的选项[0]。
重要的是,喜欢尝试! 我们随时可以更改它。 但最
重要的是,我们都越早同意就越早开始
实际使用它并提高生产力!

[0](在第一分钟引用)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=zh-CN

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba

@mitsuhiko我同意! 由于链接的原因,Postfix显得更加粗糙。 我认为我建议的fut@await语法是另一个有趣的选择,似乎没有其他建议那么多的缺点。 我不确定是否离它太远,而更扎实的版本会更好。

@ejmahler

如果使用if,if let,while,while let和for进行匹配,那么所有匹配操作都使用前缀。 假装中断和继续是唯一的控制流关键字,令人沮丧。

完全没有误导。 这些构造的相关语法大致为:

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 blockfor pat in expr blockmatch expr { pat0 => expr0, .., patn => exprn } 。 在所有这些形式的后面都有一个关键字。 我想这就是您所说的“使用前缀”的意思。 但是,这些都是块形式,不是一元前缀运算符。 由于没有一致性或规则可言,因此与await expr的比较具有误导性。 如果要与块形式保持一致,请将其与await block而不是await expr

@mitsuhiko我同意! 由于链接的原因,Postfix显得更加粗糙。

锈是二元的。 它支持命令式和功能性方法。 我认为这很好,因为在不同情况下,每种情况都可能更适合。

我不知道。 两者都感觉很棒:

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

使用Scala已有一段时间,我也很喜欢许多类似的功能。 特别是matchif在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() |
| 后缀关键字宏| fut.await!() | fut.await!()? | fut?.await!() |
| 太空关键字| fut await | fut await? | fut? await |
| 关键字| fut@await | fut@await? | fut?@await |
| 印章| fut@ | fut@? | fut?@ |

“ Sigil关键字”的符号_cannot_是# ,因为那样的话,您就无法使用称为r的未来来做到。 ...作为标记,不必像我第一次担心的那样更改令牌化

更多现实生活中的使用(我在urlo上有多个await其他_real_用例,我将其添加):

| 姓名| (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()? |
| 后缀关键字宏| client.get("url").send().await!()?.json()? |
| 太空关键字| client.get("url").send() await?.json()? |
| 关键字| client.get("url").send()@await?.json()? |
| 印章| client.get("url").send()@?.json()? |

编辑说明:已向我指出, Response::json还返回Future可能有意义,其中send等待输出IO和json (或结果的其他解释)等待传入的IO。 不过,我将保持原样,因为我认为有意义的是,即使表达式中只有一个IO等待点,链接问题仍然适用。

对于前缀选项似乎有一个大致的共识,即最明显的优先级(以及await?糖)是最可取的。 但是,许多人表示支持后缀解决方案,以使上述链接更加容易。 尽管前缀选择有一个粗略的共识,但似乎对哪种后缀解决方案最好没有共识。 所有建议的选项都容易引起混淆(通过关键字突出显示来缓解):

  • 带关键字的Fn =>使用名为await的参数调用fn
  • 关键字字段=>字段访问
  • 关键字方法=>方法调用
  • 宏(前缀或后缀)=> await是关键字?
  • 空格关键字=>可以使单行分组(多行更好吗?)
  • Sigil =>在已经被认为重于Sigil的语言中添加新的信号词

其他更激烈的建议:

  • 允许同时使用前缀(明显的优先级)和后缀“字段”(将来可以将其应用于更多关键字,例如matchif等,以使其成为通用模式,但这是不必要的此辩论的附录)[[参考](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)将有助于收集有关现实用例中等待发生位置的真实信息,并且仍然允许我们稍后添加非宏前缀或后缀选项。

但是,在编写以上摘要的过程中,我开始喜欢“关键字字段”。 沿多行分割时,“ Space Keyword”感觉很好:

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,它主要是在那里,因为语法看起来像方法,如果是关键字方法。)

允许同时使用前缀(明显的优先级)和后缀“字段” ...

如果选择了任何后缀语法(无论是一起使用,还是代替前缀一),这绝对是以后讨论的一个论据:我们现在拥有既可以使用前缀也可以使用后缀表示法的关键字,正是因为有时优于另一个,所以也许我们可以在合理的情况下都允许它,并在统一规则的同时增加语法的灵活性。 也许这是一个坏主意,也许它会被拒绝,但是如果postix表示法用于await ,那么这绝对是将来的讨论。

我认为不包含等待字符串的语法的可能性很小。

:+1:


在看到一堆示例(例如@mehcode的示例)后,我有一个随机的想法:我记得关于.await的抱怨之一是它太难看了†,但考虑到等待的事情是通常是容易出错的事实,因为它经常是.await? ,无论如何都会吸引更多的注意力。

†如果您使用的内容不会突出显示关键字


@ejmahler

我反对任何语法读起来都不像英语的语法

request.get().await内容与body.lines().collect() 。 在“一堆链接的迭代器方法”中,我认为_prefix_实际上读起来更糟,因为您必须记住,它们在一开始就说过“ wait”,并且永远不知道什么时候会听到您的声音等待,有点像花园小路的句子

经过多年在生产代码中使用它们之后,我的信念是,一目了然地知道屈服点在哪里绝对至关重要。 当您在函数的缩进行上浏览时,您可以在几秒钟内挑选出200行函数中的每个co_await / yield返回值,而没有任何认知负担。

这意味着在表达式内部永远不会有任何东西,考虑到Rust具有面向表达式的性质,这是我绝对不会支持的限制。 至少对于C#的await ,拥有CallSomething(argument, await whatever.Foo()是绝对合理的。

鉴于async _will_出现在表达式的中间,我不明白为什么在前缀中看到比在后缀中看到更容易的原因。

应该给予与“如果,则,匹配和返回”同等的尊重。 想象一下,如果其中任何一个是后缀运算符-读取Rust代码将是一场噩梦。

return (和continuebreak )和while值得注意的是_completely_无用链接,因为它们总是返回!() 。 尽管出于某种原因您省略了for ,但是我们已经看到使用.for_each()编写的代码很好,并且没有不良影响,尤其是在人造丝中

我们可能需要使async/await成为主要的语言功能,才能使和平。 它会出现在各种代码中。 它将遍及整个生态系统-在某些地方,它与?一样普遍。 人们将不得不学习它。

因此,我们可能要有意识地关注长时间使用后的语法选择,而不是一开始的感觉。

我们还需要了解,就控制流构造而言, await是另一种动物。 像returnbreakcontinue甚至yield这样的构造都可以通过jmp直观地理解。 当我们看到这些时,我们的眼睛会在屏幕上弹起,因为我们关心的控制流正在向其他地方移动。 但是,尽管await影响机器的控制流,但它并不会移动对我们的眼睛和我们对代码的直观理解很重要的控制流。

我们不打算将无条件调用链接到returnbreak因为这没有任何意义。 出于类似的原因,我们不打算更改优先级规则以适应它们。 这些运算符的优先级较低。 他们将所有内容移到右侧,然后将其返回到某个位置,从而结束该函数或块中的执行。 但是, await运算符希望被链接。 它是表达式不可或缺的一部分,而不是结尾。

在考虑了本主题中的讨论和示例之后,我留下了一种昧的感觉,即我们将后悔令人惊讶的优先规则。

潜行的马候选者现在似乎正在使用await!(expr) ,希望以后能解决更多问题。 在阅读@Centril的评论之前,我可能会支持此功能,以使几乎所有语法都可以使用此重要功能。 但是,他的论点使我相信,这将刚刚结束。 我们知道方法调用链接在Rust中很重要。 这推动了?关键字的采用,该关键字广受欢迎并且非常成功。 使用我们知道会令我们失望的语法确实只是在增加技术负担。

在此线程的早期, @ withoutboats表示只有四个现有选项似乎可行。 其中,只有后缀expr await语法可能使我们长期开心。 此语法不会引起奇怪的优先顺序意外。 它不会强制我们创建?运算符的前缀版本。 它与方法链接很好地协同工作,并且不会破坏从左到右的控制流。 我们成功的?运算符是后缀运算符的先例,与实际中的returnbreak相比, await实际上更像? breakyield 。 尽管后缀非符号运算符可能是Rust中的新功能,但是async/await用法已经足够广泛,可以使其迅速熟悉。

尽管后缀语法的所有选项似乎都可行,但是expr await具有一些优点。 此语法清楚表明await是关键字,这有助于强调魔术控制流程。 与expr.awaitexpr.await() expr.await!expr.await!()等相比,这避免了解释,它看起来像字段/方法/宏,但实际上不在这一种特殊情况下。 我们都会习惯这里的空格分隔符。

await拼写为@或使用其他不会引起解析问题的符号吸引人。 当然,这是一个足够重要的运营商,对此予以保证。 但是,如果最后必须将其拼写为await ,那就没问题了。 只要它处于后缀位置。

正如某人提到的_real_示例...我维护(根据tokei)23,858行锈代码库,该库非常异步,并使用期货0.1 await (我知道是高度实验性的)。 让我们去(编辑过的)拼写(注意一切都已经通过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)
}

最后,让我们将其转换为我最喜欢的postfix变体“ postfix字段”。

// 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?出现在其等待的事物的行中,而不是用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())?;

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上下文中, <--非常简单,尽管少于<- 。 但是相反,它提供了超过<-和超过前缀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”。
不是某种启发,但可能很有趣。


该建议中最重要的是,人体工程学方法链接也是可能的。 我之前提出的延迟前缀运算符的想法在这里很合适。 这样,我们将在前缀和后缀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;

但这要起作用,不仅需要在FnOnce(T) -> U处,还需要在FnOnce(T) -> impl Future<_>处键入and_then子句。 将来使用链式组合器其结果只能在没有postfix异常的情况下正常运行:

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 ,因此通过从该成员进行迁移而对其进行结构分解,并且此迁移隐式地等待着将来。 这感觉像最难听的。

与函数并行的是调用是闭包的行为。 我希望此解决方案是语言语料库中最干净的一种(因为在语法上与该类型的可能性最相似),但是对于方法调用来说是一些积极的观点,并且.await永远不会比其他方法长。

@HeroicKatora我们可以在libsyntax中使用.await特殊情况,以允许foo.await(42)但这是不一致的/临时的。 但是, (foo.await)(42)似乎是有用的,因为尽管存在期货输出闭包,但它们可能并不那么普遍。 因此,如果我们针对常见情况进行优化,则不必在()上添加.await可能会赢得平衡。

@Centril我同意,一致性很重要。 当我看到几种相似的行为时,我想通过综合推断其他行为。 但是这里.await似乎很尴尬,特别是上面的示例清楚地表明它与隐式解构并行(除非您能在发生这些效果的地方找到不同的语法?)。 当我看到解构时,我立即想知道是否可以将其与let-bindings等一起使用。但是,这将是不可能的,因为我们将解构没有此类成员的原始类型,或者特别是当类型只是impl Future<Output=F> (最不相关的旁注:完成这项工作将带我们回到替代前缀await _ =代替let _ = ,funlyly¹)。

但这并不能阻止我们使用语法本身,我想我可以学习它,如果最终证明它,我将大力使用它,但这似乎是一个明显的弱点。


¹这可能是一致的,同时允许?通过允许?的模式背后的名字

  • 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在编MachineApplicable方式。 确实,如果我们稳定foo.await但不允许foo.await(42)我相信我们甚至可以在以后需要更改优先级,因为起初foo.await(42)不合法的。

可以进行进一步的嵌套(例如,关闭结果的未来-但这并不常见):

struct HasClosure fn _foo()->结果<(),()> {
let foo:HasClosure <_ i =“ 27”> = HasClosure {闭包:Ok(| x | {})};

foo.closure?(42);

Ok(())

}

例如,结案结果的未来-并非这会很普遍

在任何后缀示例中,多余的后缀?无需修改语法即可使这一点明确。 无需调整。 问题仅在于.member明确地是一个字段,并且需要首先应用移动解构。 我真的不想说这很难写。 我最想说的是,这似乎与其他.member用法不一致,例如可以转换为匹配。 最初的职位在这方面权衡利弊。

编辑:调整以适合future.await(42)可能会导致意外的额外风险,从而使它成为可能a)与闭包不一致,在这种情况下,由于与成员具有相同名称的方法,情况并非如此; b)在我们想对await论证的地方抑制未来的发展。 但是,正如您前面提到的,调整Future返回关闭应该不是最紧迫的问题。

@novacrazy为什么不简单地使用常规的Future组合器?

我不确定您有多少使用Futures 0.3的经验,但是总体期望是不会大量使用组合器,并且主要/惯用用法将是异步/等待。

与组合器相比,异步/等待具有多个优点,例如,它支持跨屈服点的借用。

组合器早在异步/等待之前就存在了,但是异步/等待还是被发明了,这是有充分理由的!

异步/等待在这里保持不变,这意味着它需要符合人体工程学(包括方法链)。

当然,如果愿意,人们可以自由使用组合器,但是为了获得良好的人体工程学,它们不是必需的。

正如@cramertj所说,让我们尝试将讨论重点放在异步/等待,而不是异步/等待的替代方法上。

实际上,如果您希望在故障时具有备份行为,则某些表达式不能很好地转换为等待链。

您的示例可以大大简化:

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()是解包还是期待的结果并不明显。

Pa

我并不是想将重点从这里转移开来,而是要指出它不存在于泡沫中。 我们要考虑的事情是如何协同工作。

即使选择了理想的语法,在某些情况下,我仍然想使用自定义的期货和组合器。 那些可以被软弃用的想法使我质疑Rust的整个方向。

与合并器相比,您给出的示例仍然看起来很糟糕,并且生成器开销可能会更慢并且产生更多的机器代码。

就目前而言,所有这些前缀/后缀标记/关键字bikeshedding都是很棒的,但也许我们应该务实,并采用来Rust的用户最熟悉的最简单的选项。 即:前缀关键字

今年将比我们想象的要快。 甚至一月份都完成了。 如果事实证明用户对前缀关键字不满意,则可以在2019/2020版中对其进行更改。 我们甚至可以开一个“事后看来是2020年”的笑话。

@novacrazy

我所看到的普遍共识是,我们希望最早的第三版是2022年。 2018年版很棒,但并非没有代价。 (并且,2018版的要点之一是使异步/等待成为可能,请想象一下,然后说“不,您需要立即升级到2020版!”)

无论如何,即使希望这样做,在版本中也不可能使用前缀关键字->后缀关键字过渡。 有关版本的规则是,需要有一种在X版本中编写惯用代码的方法,这样它可以在没有警告的情况下进行编译,并且在X + 1版本中可以相同地工作。

同样的原因是,如果我们可以就其他解决方案达成共识,我们宁愿不使用关键字宏来保持稳定。 故意稳定一个我们所不希望的解决方案本身就是有问题的。

我认为我们已经表明,即使对于仅具有一个等待点的表达式,后缀解决方案也是最佳的。 但是我怀疑任何提议的后缀解决方案显然都比其他解决方案要好。

只是我的两分钱(我是一个没人,但是我很长时间都在讨论)。 我最喜欢的解决方案是@await后缀版本。 也许您可以考虑使用postfix !await ,例如一些新的postfix宏语法?

例:

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

经过几次语言迭代之后,能够实现我们自己的postfix宏将非常棒。

...所有新的赛格计划

Rust已经是语法/语法沉重的内容,我们保留了await关键字, stuff@await (或其他任何语法)看起来很古怪/难看(我知道是主观的),并且只是临时语法不会与该语言中的其他任何东西集成在一起,这是一个很大的危险信号。

我看一下Go如何实现
... <-...建议

@ I60R :Go具有非常糟糕的语法,其中充满了即席解决方案,并且是绝对必要的,非常不同于Rust。 该建议再次针对语法/语法非常严格,并且仅针对此特定功能是临时的。

@ I60R :Go的语法很糟糕

让我们不要在这里抨击其他语言。 “ X具有可怕的语法”不会导致启发和达成共识。

作为Python / JavaScript / Rust用户和计算机科学专业的学生,​​我个人更喜欢前缀await + f.await()两种语言。

  1. Python和JavaScript都有前缀await 。 我希望看到await出现在开头。 如果必须深入阅读才能意识到这是异步代码,我会感到非常不安。 凭借Rust的WASM功能,它可能会吸引许多JS开发人员。 考虑到Rust已经有了很多其他新概念,我相信熟悉舒适非常重要。

  2. Postfix await在链接设置中似乎很方便。 但是,我不喜欢.await@awaitf await类的解决方案,因为它们看起来像await语法的临时解决方案,而认为.await()作为在future上调用方法。

Rust已经与javascript背道而驰,而await类似于使用函数来表示await来调用函数(即无法通过函数模拟功能),使其与引入async-await的第一定时器混淆。 因此,我认为语法应有所不同。

我已经说服自己,与.await.await()可能更可取,尽管本文的其余部分对这一位置进行了对冲。

这样做的原因是await!(fut)必须通过value_消耗fut 。 使它看起来像字段访问是_bad_,因为它没有像前缀关键字那样移动的含义,也没有像宏或方法调用那样移动的含义。

有趣的是,关键字方法语法使它几乎看起来像是隐式的await设计。 不幸的是,Rust无法使用“显式异步,隐式等待”(因此,_please_不要在此线程上重新对其进行诉讼),因为我们希望将async fn() -> Tfn() -> Future<T>相同地使用,而不是激活“隐式等待”行为。

.await()语法看起来像一个隐式等待系统(如Kotlin所用)的事实可能会成为贬低者,并且由于周围的魔力,几乎可以感觉到“隐式异步,隐式等待”的感觉。真正的方法调用.await()语法。 您是否可以在UFCS中将其用作await(fut) ? UFCS是Future::await(fut)吗? 除非它在功能上至少可以在语法上与之统一,否则任何看起来像该语言另一维度的语法都会引起问题。

尽管任何后缀解决方案的概念通常比前缀解决方案更可取,但我仍对任何后缀解决方案的优点胜过相同解决方案的缺点表示怀疑。

我有点惊讶,因为该线索充满了似乎是由于可能而提出的建议,而不是因为它们似乎比最初的建议产生了重大的好处。
我们能否停止谈论$#@!~等,而又不提出明显的争论是什么错了与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没有可计算的属性,否则字段访问是一个非常简单的操作。 因此,似乎对该操作的复杂性有一个错误的印象。
  • 后缀方法可以。 可能会鼓励某些人写很长的陈述,其中有许多等待,这可能会更多地隐藏屈服点。 即使它有所不同,它也再次使事情看起来像方法调用。

由于这些原因,我更喜欢“有用的优先级”后跟“强制性定界符”

Go具有充满特殊解决方案的可怕语法,并且绝对必要,与Rust完全不同。 该建议再次针对语法/语法非常严格,并且仅针对此特定功能是临时的。

@dpc ,如果您完全阅读<--提案,您会看到此语法仅受Go启发,但是在命令式和函数链接上下文中却大不相同且可用。 我也看不到await语法不是临时解决方案,对我来说,它比<--更具体,更笨拙。 类似于使用deref reference / reference.deref / etc代替*reference ,或者使用try result / result.try / etc代替result? 。 除了熟悉JS / Python / etc之外,我或者没有看到使用await关键字的任何优势,无论如何,它应该比具有一致且可组合的语法重要。 我没有看到<--印章有什么缺点,除了它不是await ,这不是普通英语那么简单,用户应该首先了解它的作用。

编辑:这也可能是@ Matthias247帖子的一个很好的答案,因为它提供了一些反对await论点,并提出了一个不受相同问题影响的替代方案


对于我来说,阅读针对<--语法的评论对我来说真的很有趣,而没有引起历史和偏见原因的论据。

让我们提及有关优先级的实际细节:

今天的优先级图表:

运算符/表达式| 关联性
-| -
路径|
方法调用|
字段表达式| 左到右
函数调用,数组索引|
? |
一元- * ! & &mut |
as | 左到右
* / % | 左到右
+ - | 左到右
<< >> | 左到右
& | 左到右
^ | 左到右
\| | 左到右
== != < > <= >= | 需要括号
&& | 左到右
\|\| | 左到右
.. ..= | 需要括号
= += -= *= /= %= &= \|= ^= <<= >>= | 右到左
return break关闭|

有用的优先看跌期权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();

我越喜欢转换复杂的rust表达式(即使那些仅需要1个等待,但是,请注意,在我的20,000+将来的代码库中,几乎每个异步表达式都是一个等待,直接跟着另一个等待),我越不喜欢Rust的前缀。

由于?运算符,因此全部为_。 没有其他语言具有后缀控制流运算符_and_ await ,它们基本上总是与实代码配对。


我的首选仍然是postfix字段。 作为后缀控制运算符,我觉得它需要future.await提供超过future await的紧密视觉分组。 与.await()? ,我更喜欢.await?外观_weird_,所以它会被_noticed_,并且用户不会认为它是一个简单的函数(因此不会问为什么UFCS不起作用) 。


作为支持后缀的另一个数据点,当后缀稳定时,将使rustfixawait!(...)变为我们决定的任何值将非常感激。 我没有看到任何其他信息,但可以毫无疑问地翻译postfix语法,而无需将内容不必要地包装在( ... )

我认为首先我们应该回答以下问题:“我们是否要鼓励在链接上下文中使用await ?”。 我相信普遍的答案是“是”,因此它成为后缀变体的有力论据。 虽然await!(..)是最容易添加的,但我相信我们不应该重复try!(..)故事。 我个人也不同意“链接隐藏了潜在的昂贵操作”这一论点,我们已经有很多链接方法可能非常繁重,因此链接不会带来懒惰。

虽然前缀await关键字对于其他语言的用户来说是最熟悉的,但我认为我们不应该基于该关键字做出决定,而应该专注于长期性,即可用性,便利性和可读性。 @withoutboats讨论了“熟悉预算”,但是我坚信我们不应该仅仅出于熟悉而引入次优解决方案。

现在,我们可能不希望使用两种方法来做同一件事,因此我们不应该同时引入postfix和prefix变体。 假设我们将选项的范围缩小到了后缀变体。

首先让我们从fut await ,我非常不喜欢这个变体,因为它会严重干扰人类解析代码的方式,并且在阅读代码时会引起混乱。 (不要忘记该代码主要用于阅读)

接下来的fut.awaitfut.await()fut.await!() 。 我认为最一致且最不易混淆的变体将是后缀宏。 我认为仅仅为了保存几个字符而引入一个新的“关键字函数”或“关键字方法”实体是不值得的。

最后基于标记的变体: fut@awaitfut@ 。 我不喜欢fut@await变体,如果我们介绍了印记,为什么要麻烦await部分呢? 我们是否有未来扩展fut@something ? 如果没有,那简直是多余的。 所以我喜欢fut@变体,它解决了优先级问题,代码变得易于理解,编写和阅读。 可见性问题可以通过突出显示代码来解决。 将此功能解释为“ @等待中”并不难。 当然,最大的缺点是我们将通过非常有限的“预算”来支付此功能,但是考虑到该功能的重要性以及它在异步代码库中使用的频率,我相信从长远来看,它是值得的。 当然,我们可以使用?绘制某些相似之处。 (尽管我们必须为Rust评论家的Perl笑话做好准备)

结论:在我看来,如果我们准备负担“严格的预算”,我们应该选择fut@ ,否则不建议使用fut.await!()

在谈论熟悉度时,我认为我们不应该太在乎对JS / Python / C#的熟悉度,因为Rust处于不同的领域,并且在许多方面已经看起来有所不同。 提供类似于这些语言的语法是短期的,并且奖励目标很低。 当幕后工作原理完全不同时,没有人会选择仅使用熟悉的关键字来选择Rust。

但是要熟悉Go知识,因为Rust处于类似的领域,甚至在哲学上,与Go相比,它与其他语言也更接近。 尽管存在所有偏见,但它们中最有力的优点之一是它们不会盲目复制功能,而是实施确实有理由的解决方案。

IMO,从这个意义上说, <--语法在这里最强

所有这些,让我们记住,Rust表达式可以产生几种链式方法。 大多数语言都不会这样做。

我想提醒一下C#开发团队的经验:

反对C#语法的主要考虑因素是运算符优先级await foo?

我确实觉得我可以对此发表评论。 我们对'await'优先考虑了很多,并且在设置所需的表单之前尝试了许多表单。 我们发现的核心内容之一是,对于我们来说,以及想要使用此功能的客户(内部和外部),人们很少真正希望通过异步调用来“链接”任何东西。

人们想要在expr中继续“等待”的趋势很少见。 我们偶尔会看到类似(await expr).M()的事物,但是这些事情似乎比做await expr.M()的人数少一些,而且不太受欢迎。

这也是为什么我们不使用任何“隐式”形式表示“等待”的原因。 在实践中,这是人们想清楚考虑的事情,并且他们希望在代码中居于首位,以便他们可以关注它。 有趣的是,甚至几年后,这种趋势仍然存在。 也就是说,有时我们多年后感到遗憾,因为有些东西过于冗长。 某些功能在早期就以这种方式很好,但是一旦人们对此感到满意,便会更适合于使用其他功能。 “等待”情况并非如此。 人们似乎仍然非常喜欢该关键字的重量级性质以及我们选择的优先级。

抵制印记而不是专用(关键字)字词是一个好方法。

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

您应该真正听那些拥有数百万用户的家伙。

因此,您不想链接任何东西,只想拥有几个await ,我的经验是相同的。 编写async/await代码已有6年以上了,而我从不想要这样的功能。 Postfix语法看起来确实很陌生,被认为可以解决可能永远不会发生的情况。 Async呼叫确实是一件大胆的事情,因此单行上的几个等待太重了。

人们想要在expr中继续“等待”的趋势很少见。 我们偶尔会看到类似(await expr).M()的事物,但是这些事情似乎比做await expr.M()的人数少一些,而且不太受欢迎。

这似乎是后验分析。 可能导致它们不继续的原因之一是因为在前缀语法中这样做非常尴尬(相当于不想多次在语句中使用try! ,因为它仍然可以通过运算符?读取。

@ 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,并且trait成员可能会改变在结果上调用方法的愿望。

不,这是关于内部C#团队实验的,因为它同时具有前缀/后缀/隐式形式。 我说的是我的经历,这不仅是我无法看到postfix表单专家的习惯。

@mehcode您的榜样并没有激励我。 reqwest有意识地决定使初始请求/响应周期与响应(主体流)的后续处理分开并发进程,因此应等待两次,如@andreytkachenko所示。

reqwest可以完全公开以下API:

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

要么

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

(后者是and_then之上的简单糖)。

我感到不安的是,这里的许多后缀示例都使用此链作为示例,因为reqwest s和hyper的最佳api决定是将这些东西分开。

老实说,我相信这里大多数等待后缀的示例都应该使用组合器(必要时添加糖)进行重写,或者保留类似的操作并等待多次。

@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?;

我的感觉是,这基本上重复了期货API的问题:它使链接更容易,但是该链的类型变得更难以渗透和调试。

难道不是同一论点适用吗? 算了吗? 它允许更多的链接,从而使调试更加困难。 但是我们为什么选择呢? 过度尝试! 然后? 为什么Rust偏爱带有构建器模式的API? 因此,Rust已经做出了很多选择来支持API中的链接。 是的,这可能会使调试更加困难,但是这不是应该在皮棉级别上发生的吗?也许是对于链太大的clippy的新皮棉? 我仍然需要看到足够的动力,以了解这里的等待方式有何不同。

那是好事还是坏事? 对于有N种方法的情况,每种方法都可以导致M次跟踪,开发人员应该提供N * M方法? 我个人喜欢可组合的解决方案,即使它们更长一些。

N和M不一定很大,并且也不是全部都感兴趣/有用。

@andreytkachenko

  1. 我不同意,这里的熟悉程度被高估了。 从其他语言迁移时,人们首先会寻求进行async-await风格编程的能力,但语法却不完全相同。 如果以不同的方式实现,但可以提供更好的编程体验,那么它将成为另一个优势。

  2. 无法调试长链是调试器的局限性,而不是代码风格。 关于可读性,我认为这取决于可读性,强制命令样式在此不必要地受到限制。 如果async函数调用仍然是函数调用,那么不支持链接是令人惊讶的。 而且无论如何, <--是一个前缀运算符,可以在功能链中将其用作附加选项,如您所说

@skade同意。

所有等待较长时间的示例都是代码气味。 可以使用组合器或更新的API更好地解决它们,而不用在后台创建这些生成器意大利面条状态机。 极端速记语法比目前的期货更难以调试。

我仍然的风扇前缀关键词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这是完全可能的(不同的语法)。

像Box impl:

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进行Webdev开发的人之一,真正的力量来自async / await与现有的Promises / Futures

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

let value = result.unwrap();

如果您嵌套了期货或期货的结果或期货的结果,则.flatten()组合器可以进一步大大简化该操作。 手动打开和等待每个包装都是不好的形式。

@XX这是多余且无效的。 await仅存在于async函数中,并且仅能在实现Future / IntoFuture的类型上工作,因此不需要新的特征。

@novacrazy@richardanaya

可以使用组合器或更新的API更好地解决它们,而不用在后台创建这些生成器意大利面条状态机。 极端速记语法比目前的期货更难以调试。

@novacrazy好点,现在您提到它,作为一名前javascripter,我从不使用等待链,我一直使用then块

组合器在Rusts异步/等待中具有完全不同的属性和功能,例如,关于跨屈服点的借用。 您不能安全地编写与异步块具有相同功能的组合器。 让我们将组合器讨论放在这个线程之外,因为它没有帮助。

@ I60R

对我来说,将来使用组合器的一个很大的缺点是它们会隐藏异步函数调用,并且不可能将它们与常规函数区分开。 我想查看所有可悬挂点,因为很有可能将它们用于构建器样式链中。

您无法隐藏挂起点。 组合者唯一允许做的就是创建其他期货。 在某个时间点,这些必须等待。

请记住,C#不会出现大多数使用前缀await会将其与(已存在的)后缀?运算符组合的问题。 特别是,关于优先级的问题以及具有两个类似“装饰符”的一般尴尬出现在表达式的相对侧。

@ Matthias247当然,如果确实需要借用数据,请随时使用多个await语句。 但是,通常您只需要移动数据,并且组合器对此非常有效,并且有可能编译为更有效的代码。 有时会被完全优化掉。

同样,真正的力量在于事物结合在一起。 没有一种正确的方法可以使事情变得如此复杂。 这就是部分原因,我认为所有这些语法都是正确的,如果它不能帮助其他语言的新用户并帮助创建可维护,高性能和可读性的代码。 80%的时间我都怀疑,无论语法是什么,我都会碰到async / await只是为了使用普通期货提供最稳定,性能最佳的API。

在这方面,前缀关键字await和/或混合宏await!(...) / .await!()是最易读,熟悉和易于调试的选项。

@andreytkachenko

实际上,既然我对“熟悉”有所了解,我已经看到了您的帖子并完全同意,我认为有两种类型的熟悉:

  1. 使await语法看起来像使用大量await的类似语言。 Javascript在这里非常大,与我们作为社区的WASM功能非常相关。

  2. 第二种类型的熟悉度是使仅使用同步代码的开发人员对代码感到熟悉。 我认为等待的最大方面是使异步代码LOOK同步。 这实际上是javascript真正做错的一件事,就是它被规范化了很久,然后对主要是同步开发人员来说完全陌生。

我在JavaScript异步时代提供的一件事,对我来说,回想起来,对开发人员来说更有用的是将诺言/未来分组在一起的能力。 Promise.all(p1(),p2()),这样我就可以轻松地使工作并行化。 then()链接始终只是对Javascript过去的承诺的回响,但现在考虑一下,它几乎是过时的和不必要的。

我可能会提供这种等待的想法。 “尝试使异步代码和同步代码之间的差异尽可能小”

@novacrazy异步函数返回一个impl Future类型,对吗? 是什么使我们无法将await方法添加到Future特征中? 这样:

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

@XX据我了解,Rust中的async函数使用生成器转换为状态机。 本文是一个很好的解释。 因此await需要一个async函数才能使用,因此编译器可以正确地将它们都转换。 没有async部分, await无法工作。

Future确实具有wait方法,该方法与您的建议类似,但是会阻止当前线程。

@skade

但是代码风格如何影响类型推断? 我看不到用链接式和命令式风格编写的同一代码的区别。 类型应该完全相同。 如果调试器无法解决问题,那肯定是调试器中的问题,而不是代码中的问题。

@ skade@ Matthias247

使用组合器,您将在功能链的开始处恰好标记了一个悬浮点。 所有其他内容将隐式包含在内部。 这与使用隐式mut ,对我个人而言,这是Rust中最大的困惑点之一。 一些API会返回期货,而另一些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)? 。 这是后缀替代更好的唯一情况。 我在这里可以看到它的好处,但我认为它不会超过所有缺点。

另一个原因: impl Add for MyFuture { ... }let a = await a + b;大约是多少?

我想提醒一下在postfix await关键字的上下文中的通用类型归类RFC 。 它将允许以下代码:

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!");
}

但是我在实际代码中看到了零次出现。

请允许我离开。 我认为应该给@fix宏,就像那样。 这些可以作为expr!macroexpr@macro甚至是expr.macro!() 。 我认为第一种选择是可取的。 我不确定它们是否是一个好主意,但是如果我们想提取一个一般概念而不是一个临时解决方案,同时又要等待一个后缀,我们至少应该考虑将后缀宏作为一种潜在的解决方案。

请记住,即使我们将await用作后缀宏,它仍然是一个神奇的宏(例如compile_error! )。 但是, @ jplatte和其他人已经确定这不是问题。

如果要走那条路线,我将如何处理它,我将首先确切确定后缀宏的工作方式,然后仅允许await作为魔术后​​缀宏,然后再允许自己定义的后缀宏。

乐兴

至于词法分析,这可能是一个问题。 如果我们看expr!macro ,编译器可能会认为有一个名为expr!的宏,然后有一些无效的字母macro 。 但是,应该可以将expr!macro提前到lex的位置,并且当expr后接! ,某些东西将成为postfix宏,直接跟着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还没有像monads这样的东西,我也不会因为将它们添加到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中的postfix宏是时间问题。
每当处理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中的功能,恕我直言,IMHO将需要完整的RFC + FCP +实现阶段。

这不是RFC讨论,而是对需要实施的公认RFC的讨论。

因此,在这里讨论它们或为异步语法提出建议并不可行。 这将进一步严重延迟该功能。

为了遏制这一已经很大的讨论,我认为在这里进行讨论是没有用的,可以将其视为已讨论问题之外的内容。

只是有些想法:尽管此线程专门用于await ,但我认为我们以后会对yield表达式进行同样的讨论,它们也可以链接在一起。 因此,我更希望看到可以通用化的语法,但是看起来却如此。

我在这里不使用宏的原因:

  1. 为特定于域的内容提供了宏,并且以任何方式使用它们来更改程序控制流或模仿核心语言功能都是过大的
  2. Postfix宏将立即被滥用来实现自定义运算符或其他易导致错误代码的esotheric语言功能
  3. 他们不鼓励开发适当的语言功能:

    • stream.await_all是组合器的完美用例

    • option.or_continue和替换_else组合器是空合并运算符的完美用例

    • monad.bindif-let链的完美用例

    • ndarray slicingconst泛型的完美用例

  4. 他们会质疑已经实施的?运算符

@collinanderson

同样可以被链接

为什么在地球上您认为它们可以被束缚? 真实代码中出现的次数为零,就像上面的println示例一样。

@Pzixel最终Generator::resume可能会取一个值,因此yield expr将具有非()类型。

@vlaff我看不到清楚看到await的参数必须与Type Ascription一致。 他们是完全不同的东西。

另外,Type Ascription是反复尝试的其他功能之一,不能保证_this_会成功。 虽然我不想反对,但TA是将来的RFC,这是关于已接受的功能和已经提出的语法的讨论。

通读许多评论,以添加更多关于为什么等待的摘要(...)似乎是一条非常理想的路径:

  1. 它看起来是现有代码最熟悉的,因为熟悉宏
  2. 有使用await的现有工作!(...) https://github.com/alexcrichton/futures-await可以帮助减少代码重写
  3. 由于postfix宏不在表中,因此它可能永远不会成为语言的一部分,因此请“等待”! 在非标准情况下,按照RFC似乎甚至没有可能
  4. 它为社区提供了更多时间来考虑广泛稳定使用后的长期发展方向,同时提供了一些并非完全不合常规的内容(例如try!())
  5. 可以用作即将到来的yield!()的类似模式,直到我们找出可以满足等待和yield的正式路径为止
  6. 链接可能没有期望的那样有价值,并且通过在多个行上进行多次等待,甚至可以提高清晰度
  7. IDE语法荧光笔无需更改
  8. (...)宏在看到其他宏用法之后(可能不会减少认知超载)可能不会被其他语言的人抛弃。
  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 yieldyield from表达式:外部函数可以提供一个值,当生成器恢复时,该值将成为yield的结果。 这就是@valff的意思,与类型归属无关。 因此, yield也将具有!()

从协程的角度来看, yieldawait将其挂起,并且可以(最终)返回一个值。 因为它们只是同一枚硬币的两个面。

并抛出另一种语法可能性_square-brackets-with-keyword_,以部分突出显示此内容(使用yield进行语法突出显示):

let body: MyResponse = client.get("http://api").send()[yield]?.into_json()[yield]?

“ postfix关键字”对我来说最有意义。 后缀使用除空格以外的其他字符分隔future-expression和await对我来说也很有意义,但不适用。 因为那是方法,而await不是方法。 但是,如果您希望将await作为前缀关键字,我喜欢Trait或方法的@XX建议,该建议仅将await self调用那些想要将一堆东西链接在一起的方法(尽管我们可能无法命名方法await ;虽然我认为只是wait就可以了)。 就个人而言,我可能最终会按行进行等待而不是进行过多的链接,因为我发现长链的可读性较差,因此前缀或后缀将对我有用。

[edit]我忘记了wait已经存在并阻碍了未来,所以请轻描淡写。

@roland我专门指的是它,它讨论类型归因: https :

@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来引用此参数,类似这样的东西在IMO上更可能有效。 从这个角度来看, yield仍将返回()因此不应对await的讨论产生太大影响。

我认为await应该是前缀运算符,因为async是(而且我希望yield也要作为前缀)。 或者将其作为常规方法,然后在后缀位置使用。

但是,我并没有在这里进行所有的研究:为什么还要考虑一个postfix关键字,而Rust没有其他关键字呢? 这会使语言变得很奇怪。

另外,链接期货,我明白了为什么它会很有趣。 但是连锁等待吗? 这到底是什么意思? 未来会返回新的未来吗? 我们是否希望Rust将这一成语作为头等公民真的是一个如此普遍的成语? 如果我们真的很在意,我认为我们应该:

  1. 选择方法(即foo.await() )。 它没有引入奇怪的后缀关键字,我们都知道这意味着什么。 我们也可以这样做。
  2. 如果我们真的想要一个关键字/破译者,我们可以稍后解决这个问题。

另外,出于个人观点,我讨厌await在postfix关键字中的位置。

前缀await在其他语言中使用是有原因的-它与自然语言匹配。 您不会说“我将等待您的到来”。 尽管已经编写了JavaScript,但我可能还是有些偏颇。

我还要说的是,我认为await -chaining被高估了。 async/await在将来的组合器上的价值在于,它允许使用标准语言构造(例如ifmatch等)来顺序地编写异步代码。 无论如何,您将想分手等待。

不要让async/await构成太多的魔术语法,这只是该语言的一小部分。 提议的方法语法(即foo.await() )与IMO太正交,与常规方法调用正交,并且太神奇了。 proc-macro机制已经到位,为什么不掩盖它呢?

如何在功能定义中使用await关键字而不是async ? 例如:

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future
}

只能在async上下文中调用此await fn

async {
    let n = foo(bar());
}

然后减到let n = (await foo(bar()));
然后,除了await关键字之外,特征Future可以实现await -方法,以便在后缀位置使用await逻辑,例如:

async {
    let n = bar().awaited();
}

另外,有人可以向我解释与发电机的关系吗? 我很惊讶async / await在生成器(甚至稳定器)之前实现。

@phaazon生成器已作为async / await的内部实现详细信息添加到Rust中。 当前没有稳定它们的计划,并且它们仍然需要非实验性的RFC。 (我个人希望使它们稳定下来,但是似乎它们至少要间隔一到两年,可能要等到async / await稳定并且出现了一些经验)。

@XX我认为,如果在函数定义中使用await ,则您正在破坏async的语义。 让我们坚持标准,不是吗?

@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())`

如果有效,则有可能实现在链中使用的等待方法:

async {
    let n = first().awaited()?.second().awaited()?;
    // let n = (await (await first())?.second())?;
}

我认为,后缀是否等待“感觉”太神奇并且对语言不熟悉并不重要。 如果我们采用方法语法.await().await!()那么只需解释一下Future拥有.await().await!()方法在有意义的情况下,您还可以使用它来等待他们。 花5分钟思考它并不难理解,即使您以前从未见过。

此外,如果我们使用prefix关键字,是什么使我们无法编写带有以下内容的板条箱(不完整的伪代码,因为我现在没有基础结构来处理细节):

trait AwaitChainable {
    fn await(self) -> impl Future;
}

impl<T: Future> AwaitChainable for T {
    fn await(self) -> impl Future {
        await self
    }
}

这样,如果需要,我们可以等待后缀。 我的意思是,即使这不可能,并且我们必须通过编译器神奇地实现后缀等待,我们仍然可以使用上面的示例来解释它的大部分工作原理。 学习起来并不难。

@ivandardi我也这么想。 但是这个定义

fn await(self) -> impl Future {
    await self
}

并不表示只能在async -context中调用该函数。

@XX是的,就像我说的那样,请忽略损坏的伪代码:PI当前没有基础结构来查找应如何在其中编写正确的语法和特征:(试想一下,我写的东西使它只能在异步中被调用上下文。

@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 {//因为已经采用异步
等待自我
}

```rust
await fn await(self) -> T {
    self // remove excess await
}

@CryZe @XX你为什么要嘘我? 我是正确的。

@XX您想要做的是改变异步的含义并完全等待(例如,创建与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不相关,因为重点是要找到一种客观的解决方案,而不是基于主观感受(例如通过赞成/反对投票可视化)的解决方案。

我不明白这段代码应该做什么。 但是与异步等待当前的工作方式无关。 如果您关心它,请将其排除在该线程之外,并为其编写专用的RFC。

@CryZe该类型可用。 叫做未来。 很久以前就达成共识,异步/等待是一种仅用于转换异步代码的机制。 它不是一般的绑定符号。

@ivandari您的后缀等待未等待,但创建了新的未来。 本质上是身份功能。 您不能隐式等待,因为它不是常规函数。

@ Matthias247他们试图建议的是一种让后缀等待方法调用语法的方法。 这样,Rust不需要像我们对?那样引入任意的新符号,例如#,@或...。 运算符,看起来仍然很自然:

let result = some_operation().await()?.some_method().await()?;

因此,想法是在某种程度上将Future作为一种特殊的方法来等待特质,使编译器将其视为与普通await关键字相同的方式(在这种情况下,您根本不需要使用)并转换异步从那里将代码导入生成器。 然后,您将具有从左到右的适当逻辑控制流程,而不是使用await some_future()?从左到右,并且不需要引入奇怪的新符号。

(所以tl; dr:它看起来像方法调用,但实际上只是后缀等待中)

考虑到后缀语法,我尚未看到的明确提及是错误和选项组合符的普遍性。 在这些地方,rust与所有等待使用的语言最不同-我们拥有.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不支持所有其他控制流(例如breakcontinueif等)。除了“这可能在某些意见中看起来更好。

总的来说,我想提醒大家, await是非常特殊的,不是正常的方法调用:

  • 跳过等待时,调试器的行为可能会很奇怪,因为堆栈可能会散开并重新建立。 据我所知,使用C#和Javascript进行异步调试需要花费很多时间。 那些付钱给从事调试器工作的团队!
  • 不能在等待点上建立的本地对象可以存储在实际的OS堆栈中。 不必将那些移动到生成的Future中,这最终将改变所需的堆内存(Future所在的位置)。
  • 跨等待点借入是为什么生成的期货要求为!Unpin原因,并且给某些现有的组合器和机制带来了很多不便。 可能有可能从将来不会借用的async方法中生成Unpin期货。 但是,如果await是不可见的,那将永远不会发生。
  • 可能还有其他意外的借阅检查器问题,当前的async / await状态未涵盖这些问题。

@Pzixel @lnicola

您为C#提供的示例是绝对必要的,没有像我们在Rust中经常看到的功能样式长链一样。 LINQ语法只是一种DSL,与我在Google上搜索过的foo().bar().x().wih_boo(x).camboom().space_flight();几乎没有什么不同,就像大多数流行的编程语言一样,C#代码示例看起来是100%必要的。 这就是为什么我们不能只接受langue X的原因,因为它并不相同。

IMO前缀表示法非常适合命令式编码。 但是Rust支持两种样式。

@skade

我不同意您(和其他人)关于功能样式问题的评论。 简而言之-让我们只同意有些人对另一个人很偏爱。

@ Matthias247不,除了看起来好看之外还有其他原因。 这是因为Rust鼓励链接。 您可能会说:“哦,其他控制流机制没有后缀语法,那么为什么还要特别?”。 好吧,这是一个错误的评估。 我们确实有后缀控制流程。 它们只是我们调用的方法的内部。 Option::unwrap_or就像做一个后缀匹配语句。 Iterator::filter就像做一个后缀if语句。 仅仅因为控制流不是链接语法本身的一部分,并不意味着我们还没有后缀控制流。 从这种角度来看,添加一个后缀等待实际上将与我们拥有的保持一致。 在这种情况下,我们甚至可以拥有类似于Iterator工作方式,而不仅仅是等待原始的后缀,我们还可以拥有一些等待组合器,例如Future::await_or 。 无论哪种方式,等待后缀都不只是外观问题,而是功能和生活质量的问题。 否则,我们仍将使用try!()宏,对吗?

@dpc

您为C#提供的示例是绝对必要的,没有像我们在Rust中经常看到的功能样式长链一样。 LINQ语法只是一种DSL,与foo()。bar()。x()。wih_boo(x).camboom()。space_flight();无关。 我已经用Google搜索了一下,就像大多数流行的编程语言一样,C#代码示例看起来是100%必要的。 这就是为什么我们不能只接受langue X的原因,因为它并不相同。

这是不正确的(您可以检查任何框架,例如polly ),但是我不会对此争论。 我看到两种语言都有许多链接代码。 但是,实际上一种东西可以使一切变得不同。 它被称为Try特质。 C#与它没有任何相似之处,因此它不会做任何超出await点的事情。 如果收到异常,它将被自动包装并引发给调用者。

Rust并非如此,您必须手动使用?运算符。

如果您必须拥有超过await点的任何东西,则应创建“组合”运算符await? ,扩展语言规则等,或者您可以仅使postfix等待并且事情变得更加自然(一点外来语法,但谁在乎呢?)。

因此,我目前认为将postfix作为更可行的解决方案。 我唯一的建议应该是使用专用的空格分隔关键字,而不是await()await!()

我想我提出了一个合理的理由来证明正常的.await()方法是正确的。 如果您考虑一下,它与其他任何阻止方式都没有什么不同。 您调用.await()方法,(可能是绿色的)线程的执行将被暂存,并且在某个时刻执行运行时,然后在某个时刻恢复线程的执行。 因此,出于所有意图和目的,无论您是否有Mutex或std通道都像这样:

let result = my_channel().recv()?.iter().map(...).collect();

或像这样的未来:

let result = my_future().await()?.iter().map(...).collect();

没什么不同它们都在各自的recv()/ await()处阻止执行,并且都以相同的方式链接。 唯一的区别是await()可能在不是操作系统繁重线程的其他执行器上运行(但很可能是在单线程执行器的情况下)。 因此,不鼓励大量的链接会影响完全相同的方式,并且可能会导致实际的短绒。

但是,我的意思是,从编写此代码的人的角度来看,.recv()或.await()并没有太大区别,这两种方法都会阻止执行并在结果可用后返回。 因此,出于所有意图和目的,它几乎可以是一种正常方法,不需要完整的关键字。 我们也没有Mutex和std频道的recv或lock关键字。

但是,rustc显然希望将整个代码转换为生成器。 但是,从语言角度来看,这实际上在语义上是否必要? 我很确定可以编写一个替代的rustc编译器,该编译器通过阻塞实际的OS线程来实现await(输入async fn需要生成一个线程,然后等待将阻塞该线程)。 尽管这将是一个非常幼稚且缓慢的实现,但它在语义上的行为完全相同。 因此,从语义上讲,实际的rustc等待变为生成器这一事实是没有必要的。 因此,我实际上认为rustc将对.await()方法的调用转换为生成器可以看作是rustc的优化实现细节。 这样,您就可以证明.await()是一个完整的方法而不是一个完整的关键字,但是仍然有失误地将整个事情变成了生成器。

@CryZe.recv()我们可以从程序中的任何其他位置安全地中断await ,然后await旁边的代码将不会执行。 这是一个很大的不同,这就是为什么我们不应该隐式地执行await的最有价值的原因。

@ I60R它的

疯狂的主意:隐式等待。 我已移至另一个线程,因为该线程已经太忙于后缀和前缀讨论。

@CryZe

我想我提出了一个合理的理由来证明正常的.await()方法是正确的。 如果您考虑一下,它与其他任何阻止方式都没有什么不同。

请再次阅读https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515

@ Matthias247这些都是非常好的观点,好像我错过了那些。

从根本上讲,所有关于将await变成函数,成员或其他隐式的说法都是无效的。 这不是一个行动,而是一个转变。

高层编译器,无论是通过关键字还是宏,都将await表达式转换为特殊的生成器yield表达式,就地解决了借用和其他问题。

应该是明确的。

它要么尽可能像关键字一样美观,要么显然使用宏生成代码。

魔术方法,特质,成员等都容易被误解和误解,尤其是对于新用户而言。

Prefix关键字await或prefix宏await似乎是执行此操作的最可接受的方法,这与许多其他语言一样是有意义的。 我们不必太特殊才能使其变得更好。

@novacrazy

魔术方法,特质,成员等都容易被误解和误解,尤其是对于新用户而言。

您可以对此进行扩展吗? 如果可能,更具体地讲.await!()变体。

在我看来,拥有foo.await!()并不难读,尤其是带有适当的语法突出显示,这一点不容忽视。

误会了吗? 本质上是做什么的(忽略类型mystakes):

trait Future {
    fn await!(self) -> Self::Item {
        await self
    }
}

又名await this_foothis_foo.await!()完全相等。 有什么容易误解的呢?

关于新用户的话题:新用户要做什么? 总体而言是编程,还是将Rust用作语言但具有编程语言背景的新用户? 因为如果是前者,那么我怀疑他们是否会涉足异步编程。 如果是后者,则更容易解释后缀等待的语义(如上所述),而不会造成混淆。

或者,如果仅添加前缀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!()并在我们进行时找出一个后缀形式。 可能还会讨论postfix宏的可能性,以及如果我们可以在语言中完全支持postfix宏之前将postfix .await!()临时添加到该语言中。 Kinda就像?Try特性发生的事情一样:它是作为特殊情况首先添加的,之后又被扩展为更一般的情况。 决定时,我们唯一需要注意的是一般的postfix宏语法如何,这可能值得单独讨论。

Prefix关键字await或prefix宏await似乎是执行此操作的最可接受的方法,这与许多其他语言一样是有意义的。

显然是_workable_,但是我只记得两个参数,但我认为它们没有说服力:

  • 其他语言就是这样

    • 很好,但是我们已经在做一些不同的事情,例如not-running-unless- poll ed而不是运行到第一个await ,因为锈是根本不同的语言

  • 人们喜欢在行首看到await

    • 但这并不总是存在的,所以如果这是一个唯一的地方,一个人就会遇到问题

    • 就像在foo(aFuture.await, bFuture.await)看到await一样容易

    • 在锈病中,如果您要查看控制流程,已经在$ _ ?的行的_end_进行扫描

我错过了什么?

如果辩论是“嗯,他们几乎都是一样的”,我绝对同意“好吧,如果我们不在乎我们也可以做其他人所做的事情”。 但是我不认为这是我们要去的地方。

@scottmcm是的,“嗯,它们

因此,我们应该在可读性,熟悉性,可维护性和性能之间找到适当的平衡。 因此,我的最后一条评论中的语句为true,如果以后打算添加后缀方法宏( .await!() ),或者只是一个无聊的前缀关键字await ,则使用宏await!()否则await

我说无聊,因为无聊是件好事。 当我们用这些编写代码时,我们想让自己的思维远离语法本身。

如果f.await()不是一个好主意,那么我更喜欢前缀语法。

  1. 作为用户,我希望我使用的语言只有少数语法规则,并且使用这些规则,我可以可靠地推断出它可能正在做什么。 在这里和那里都不例外。 在Rust中, async开头, await开头也不例外。 但是, f await可以是post关键字形式。 f.await看起来像字段访问,是一个例外f.await!()具有postfix宏,该宏从未在该语言中出现,并且不知道它在其他情况下会带来什么好处(例外) 。 我们没有答案如何将这些语法变成规则而不是一次性例外

  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功能(如链接和错误处理)很好地集成
✓简单:简短,描述性强,易于理解且易于使用
✓可重用性:延迟前缀运算符语法在其他情况下也将很有用
✓文档:意味着呼叫方将流分派到某个地方并等待直到返回
✓熟悉:提供了已经熟悉的模式,但折衷较少
✓可读性:读为普通英语,不会扭曲单词的含义
✓可见性:其位置使得很难掩饰在代码中的某处
✓可达性:可以很容易地用谷歌搜索
✓经过良好测试:golang如今以某种相似的语法广受欢迎
✓惊喜:很难误解以及滥用该语法
✓满意:少量学习后,每个用户都会对结果感到满意


编辑:正如@ivandardi在下面的评论中指出的,应该澄清一些事情:

1.是的,这种语法有点难以理解,但同时不可能在此处发明不会带来任何可读性问题的语法。 go语法对我来说似乎不太邪恶,因为在前缀位置它与前缀await完全相同,而在延迟位置中,IMO比后缀await更具可读性例如:

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

其中await看起来并不模棱两可,并且与所有现有关键字都不具有不同的关联性,但是如果我们决定将来某个时候实现await块,则也有可能成为阻碍者。

2.将需要创建RFC,实现和稳定“延迟前缀运算符”语法。 该语法不是特定于异步代码的,但是在异步中使用它是其主要动机。

3.在“延迟前缀运算符”中,语法关键字很重要,因为:

  • long关键字会增加变量和方法之间的空间,不利于可读性
  • 长关键字是前缀运算符的奇怪选择
  • 当我们希望异步代码看起来像同步时,long关键字不必要地冗长

无论如何,很有可能开始使用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()?;


如果您不赞成投票,请确保:

  • 您理解得正确,因为我可能不是解释新事物的最佳人选
  • 您的观点没有偏见,是因为语法有很多偏见
  • 您将在下面提出一个正当的理由,因为无声小票的毒性很大
  • 您的原因不是关于立即的感受,因为我在这里尝试设计了长期功能

@ I60R

我确实对此有所了解。

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

这有点难以理解。 乍一看,您会认为我们会在db.go上进行匹配,但是接下来还有其他事情,您会发现“哦,好的,加载是db的方法”。 IMO认为语法主要是行不通的,因为方法应始终靠近它们所属的对象,并且它们之间没有空格和关键字中断。

第二,您的建议要求定义什么是“延迟前缀运算符语法”,并可能首先在语言中对其进行概括。 否则,它将仅用于异步代码,我们回到“为什么不后缀宏?”的讨论。 太。

第三,我认为关键字根本不重要。 但是,我们应该偏向于await因为该关键字已为2018年版保留,而go关键字没有并且应该进行更深入的crates.io搜索,然后再提出。

当看到讨论如何不断地从一个方向转到另一个方向(前缀与后缀)时,我强烈认为await关键字存在问题,我们必须完全退出。

我们可能会(半)隐式等待,一切都会好起来的,但是,尽管事实上异步功能的全部要点是隐藏实现细节并使它们看起来只是一个事实,但在Rust社区这将是一件很难的事。像阻止io一样。

@dpc我的意思是,我能想到的最好方法是在半隐式等待和希望显式等待代码段之间达成妥协,类似于unsafe

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

await块中的所有内容都将自动等待。 这样就解决了需要后缀等待链接的需求,因为您可以等待整个链。 但同时,我不确定是否需要这种形式。 也许其他人可以找到一个很好的理由反对这一点。

@dpc异步功能的重点不是隐藏细节,而是使它们更易于使用和使用。 我们绝不应该隐藏潜在的高成本运营,并应尽可能利用明确的期货。

任何人都无法将异步功能与同步功能混为一谈。 纯粹是以更具可读性和可维护性的形式组织操作序列,而不是手写状态机。

另外,如@ivandardi的示例所示,隐式等待将使无法使用Future组合器,正如我已经显示的那样,它们仍然非常强大。 我们不能只抓住任何Future并等待它,那将是非常低效的。

显式,熟悉,可读,可维护和高性能:前缀关键字await或宏await!() ,将来可能带有后缀宏.await!()

@novacrazy即使选择了理想的语法,在某些情况下我仍然想使用自定义的

当然,您可以继续使用组合器,不推荐使用任何东西。

这与?情况相同:新代码通常将使用? (因为它比组合器要好),但是Option / Result组合器仍然可以使用(和or_else类的时髦器

特别是,async / await可以完全替换mapand_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())

生成器的开销可能会稍慢一些,并产生更多的机器代码。

为什么您声称异步/等待会产生额外的开销? 生成器和异步/等待都经过专门设计,以实现零成本和高度优化。

特别是,异步/等待编译为高度优化的状态机,与Future组合器相同。

而不是在后台创建这些发电机意大利面条状态机。

Future组合器还在引擎盖下创建了一个“意大利面条状态机”,这正是他们的设计意图。

从根本上说,它们与异步/等待没有什么不同。

[未来组合器]具有编译为更高效代码的潜力。

请停止散布错误信息。 特别是,异步/等待可能比Future组合器更快

如果事实证明用户对前缀关键字不满意,则可以在2019/2020版中对其进行更改。

正如其他人所说,版本并不是我们随心所欲地制作的随心所欲的东西,它们是专门为每隔几年(最早)而设计的。

正如@Centril指出的那样,想出一种不好的语法来稍后替换它是一种非常低效的处理方式。

目前讨论的速度很高。 因此,为了减慢速度并允许人们赶上来,我暂时锁定了此问题。 它将在一天内解锁。 届时,请尽量保持建设性。

一天后解锁问题...请考虑不要提出要点,并保留有关主题的评论(例如,这里不是考虑隐式等待或其他超出范围的地方。)

由于这是一个冗长的话题,因此让我们重点介绍其中包含的一些最有趣的评论。

@mehcode为我们提供了广泛的真实世界代码,在前缀和后缀位置均带有awaithttps :

@Centril有说服力地指出,稳定await!(expr)等于故意增加技术债务: https :

@valff提醒我们expr await postfix关键字语法最适合一般类型的用法: https :

@quodlibetor强调了Error和Option组合https :

最终,lang团队需要在这里打电话。 为了确定可能导致共识的共识,总结lang团队成员在后缀语法关键问题上的立场可能会有所帮助:

  • @Centril表示支持后缀语法,并探索了该票证中的许多变体。 Centril特别不想稳定await!(expr)

  • @cramertj表示支持后缀语法,特别是对expr await后缀关键字语法的支持。

  • @joshtriplett表达了对后缀语法的还应提供一个前缀版本。

  • @scottmcm表示支持后缀语法。

  • @withoutboats不想稳定宏语法。 在担心耗尽我们的“陌生预算”的同时,withoutboats认为expr await postfix关键字语法具有“真正的优势”。

  • @aturon@eddyb@nikomatsakis@pnkfelix尚未就问题#57640或#50547中的语法表达任何立场。

因此,我们需要找出是/否答案:

  • 我们应该有前缀语法吗?
  • 我们应该有后缀语法吗?
  • 我们现在应该拥有前缀语法,之后再找出后缀语法吗?

如果我们使用前缀语法,那么我认为人们最接受两个主要的竞争者:有用和显而易见,如本注释所示。 反对await!()的论点非常强烈,因此我考虑将其排除在外。 因此,我们需要弄清楚是想要有用的语法还是显而易见的语法。 我认为,一般而言,无论哪种语法都应使用较少的括号。

至于后缀语法,这比较棘手。 带有空格的后缀await关键字也有很强的参数。 但这很大程度上取决于代码的格式。 如果代码格式不正确,则带有空格的后缀await关键字看起来确实很糟糕。 因此,首先,我们需要假设所有Rust代码都将使用rustfmt正确格式化,并且让rustfmt始终将链式等待拆分成不同的行,即使它们适合在一行中也是如此。 如果我们能够做到这一点,那么该语法就可以了,因为它解决了单行链中间的可读性和空格混淆的问题。

最后,如果我们同时使用前缀和后缀,则需要弄清楚并写下这两种语法的语义,以了解它们如何相互影响以及如何将它们转换为另一种。 应该可以这样做,因为在后缀或前缀等待之间进行选择不应更改代码的运行方式。

为了阐明我的思考过程,以及我如何处理和评估表面句法建议。 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. 使用await关键字(无论它是后缀,前缀,宏等)时,最简单的进行抓包。使用基于符号的语法时,例如#@~ ,grepping变得更加困难。 差别不大,但是.awaitawaitawait稍微更容易grep,因为这两个都可以作为注释中的单词包含进来,而对于.await

  5. 印记可能很难一目了然,而await更容易看清,尤其是语法突出显示。 已经建议.await或其他后缀语法很难一目了然,或者后缀await提供的可读性很差。 但是,在我看来, @ scottmcm正确地指出,结果的未来很普遍, .await?有助于引起人们更多的注意。 此外,Scott指出,前缀await导致花园路径语句,并且表达式中间的await比后缀可读性更高。 随着?运算符的出现,Rust程序员已经需要扫描行尾以查找控制流

  6. .await.await()的优先级是可以完全预测的。 它们充当字段访问和方法调用的对等对象。 后缀宏的优先级可能与方法调用的优先级相同。 同时,相对于? (即await (expr?) ),前缀await具有一致,可预测且无用的优先级,或者优先级不一致且有用(即(await expr)? )。 可以给标记指定所需的优先级(例如,从?获得直觉)。

  7. @nikomatsakis在2012年指出,西蒙·佩顿·琼斯 Simon Peyton Jones “点的力量”以及它如何提供IDE魔术,从而使您可以缩小想要的功能或范围。 由于点的力量存在于许多流行的语言(例如Java,C#,C ++,..)中,因此导致“伸手去拿点”根深蒂固在肌肉记忆中。 为了说明这种习惯的强大功能,以下是带有Re#er的Visual Studio @scottmcm的屏幕截图:
    Re#er intellisense

    C#没有后缀等待。 但是,它非常有用,以至于它在自动完成列表中显示时都没有有效的语法。 但是,当.await不是表面语法时,尝试使用.aw可能不会发生,包括我自己在内。 如果可以的话,请考虑对IDE体验的好处。 await exprexpr await语法没有提供这种好处。

  8. 印记可能会提供较差的熟悉度。 前缀await受益于C#,JS等的熟悉程度。但是,这些不会将await?分成不同的操作,而Rust不会。 从await exprexpr.await也不是一件容易的事-这种变化并不像使用符印符号那样激进。 而且,由于上述的点功率,很可能通过键入expr.并将await作为自动完成弹出窗口中的第一个选项来学习.await

  9. 印记易于编写,并提供良好的人体工程学设计。 但是,虽然.await键入时间更长,但它还带有点功率,可以使书写流程更好。 不必闯入let语句也有助于更好的人体工程学。 前缀await ,或者更糟糕的是await!(..) ,既缺乏点功能,也缺乏链接能力。 比较.await.await().await!() ,第一个提议最简洁。

由于没有一种语法最能同时实现所有目标,因此必须做出权衡。 对我而言,具有最大优势和最少缺陷的语法是.await 。 值得注意的是,它可以被链接,保留await作为关键字,可以抓握,具有点运算力,因此是可学习的和符合人体工程学的,具有明显的优先级,并且最终是可读的(尤其是具有良好的格式和突出显示)。

@Centril只是为了澄清一些问题。 首先,我只想确认您只想等待后缀,对吗? 其次,如何处理.await作为字段访问的二元性? 可以将.await为原样,还是应该使用带有括号的.await()来暗示那里正在发生某种操作? 第三,使用.await语法,await是关键字还是只是“字段访问”标识符?

首先,我只想确认您只想等待后缀,对吗?

是的至少现在。

其次,如何处理.await作为字段访问的二元性?

这是一个小缺点。 点的力量有很大的上升空间。 我相信这种区别是用户会很快学到的,特别是因为它突出显示了关键字并且该结构将被频繁使用。 而且,正如Scott所指出的, .await?将是最常见的,这将进一步缓解这种情况。 就像我们已经说过fn一样,在rustdoc中为await添加一个新的关键字条目也应该很容易。

可以将.await为原样,还是应该使用带有括号的.await()来暗示那里正在发生某种操作?

我更喜欢.await没有尾巴() ; 在大多数情况下,结尾处有()似乎在大多数情况下都是不必要的盐,并且我猜想,这会使用户更倾向于寻找该方法。

第三,使用.await语法,await是关键字还是仅仅是“字段访问”标识符?

await仍然是关键字。 大概您将libsyntax更改await.之后遇到await时不会出错,然后在AST中或降为HIR时以不同的方式表示它...但是这主要是实现细节。

感谢您清理所有内容! 👍

此时,关键问题似乎是是否采用后缀语法。 如果那是方向,那么我们当然可以缩小范围。 阅读会议室,大多数postfix语法的支持者都会接受比前缀语法更合理的postfix语法。

进入此线程后缀语法似乎是失败者。 但是,它吸引了四名lang团队成员的明确支持和第五名的开放性(到目前为止,其他四名成员都保持沉默)。 另外,它还吸引了广大社区的大力支持。

而且,后缀语法的支持者似乎已经明确地认为这是Rust的最佳途径。 似乎有必要对迄今提出的论点提出一些深刻的问题或令人信服的反驳,以扭转这种支持。

鉴于此,似乎我们需要倾听@withoutboats的声音,他肯定一直在密切关注此主题,并听到其他四个lang团队成员的声音。 他们的观点可能会推动这一话题。 如果他们有顾虑,那么讨论这些将是优先事项。 如果他们确信后缀语法是正确的,那么我们可以继续寻求共识。

糟糕,只是注意到@Centril已发表评论,因此我将其帖子隐藏以保持整洁。

@ivandardi给定目标中的目标(1),我的理解是, loop {}永远不是结构文字表达式一样。 而且我希望它能被突出显示以使其更加明显(例如https://github.com/rust-lang/rust-enhanced/issues/333正在等待Sublime中的操作)。

我担心的是,在2015年版本的代码库中没有意识到的情况下,使await语法看起来像是一种不同的访问可能会引起混乱。 (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. 抓-检查
  5. 非印章-检查
  6. 优先顺序-¹检查
  7. 点功率-²检查
  8. 易于学习-³检查
  9. 人机工程学-检查

¹优先顺序: await之后的空格使其在延后位置中不明显。 但是,它与以下代码完全相同: client.get("https://my_api").await_send()?.await_json()? 。 对于所有说英语的人来说,它比其他所有提案都更加自然。

²点电源:接下来键入.?;后,需要IDE的额外支持才能将await到方法调用的左侧。 似乎并不难实施

³易于学习:在前缀位置,程序员已经很熟悉。 处于稳定位置之后,语法稳定后就很明显


但是有关此语法的最强之处在于它将非常一致:

  • 不能与属性访问混淆
  • ?具有明显的优先级
  • 它将始终具有相同的格式
  • 它与人类语言相符
  • 它不受方法调用链中变量水平出现的影响*

*说明隐藏在扰流板下


对于postfix变体,很难预测哪个函数是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()

2019年1月23日,星期三,05:06 I60R < [email protected]写道:

让val =等待未来;
let res = client.get(“ https:// my_api”).await send()?. await json()?;

  1. 使用等待-检查
  2. 头等舱-检查
  3. 链接-检查
  4. 抓-检查
  5. 非印章-检查
  6. 优先顺序-¹检查
  7. 点功率-²检查
  8. 易于学习-³检查
  9. 人机工程学-检查

¹优先顺序:空格使它在延后位置不明显。 但是它是
与以下代码完全相同:client.get(“ https:// my_api
“).await_send()?. await_json()?。对于所有说英语的人来说,甚至更多
比其他所有提案自然

²点电源:可能需要IDE的额外支持才能将其移至
方法调用之后的左侧。 要么 ? 接下来输入。 似乎也不是
难以实施

³易于学习:在前缀位置,程序员已经很熟悉了,
递延位置也将是显而易见之后的语法是

稳定的

但是有关此语法的最强点是它将非常
一致的:

  • 不能与属性访问混淆
  • 它具有明显的优先级?
  • 它将始终具有相同的格式
  • 它与人类语言相符
  • 在方法调用中不受水平变量的影响
    链*

*说明隐藏在扰流板下

使用postfix变体很难预测哪个函数是异步的,
水平轴上的下一次等待将出现在哪里:

让res = client.get(“ https:// my_api”)

.very_long_method_name(param, param, param).await?

.short().await?;

对于延迟变体,几乎总是一样的:

让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#语言团队的经验。 微软在可用性研究上投入了很多精力,我们在这里基于一行代码来做出决定,并且前提是Rust足够特殊,可以具有与所有其他语言不同的语法。 我不同意这个前提-我在上面提到了LINQ和扩展方法在C#中普遍存在。

但是,当.await不是表面语法时,尝试使用.aw可能不会发生,包括我自己在内。

〜我不认为其他语言的熟悉者会遇到这种情况。〜除此之外,您是否希望完成弹出窗口同时包含awaitFuture方法?

[...]我们在这里基于一行代码来做出决定,并且前提是Rust足够特殊,可以具有与所有其他语言不同的语法。 我不同意这个前提[...]

@lnicola我只是想指出一下,以防您和其他人错过它(在大量的评论中很容易错过): https :

实际生产代码中的几个真实示例。

鉴于上面的@Centril评论,似乎竞争最激烈的postfix选项是expr await postfix关键字语法和expr.await postfix字段语法(也许postfix方法语法还没有出来)。

与postfix关键字相比, @ Centril认为postfix字段受益于“点的力量”(IDE可能更好地将其建议为自动

相反, @ cramertj主张使用postfix关键字语法,因为该语法清楚表明它不是方法调用或字段访问。 @withoutboats认为postfix关键字是postfix选项中最熟悉的。

关于“点的力量”,我们应该考虑到IDE仍然可以在点之后提供await作为完成,然后在选择完成时将其删除。 足够聪明的IDE(例如带有RLS的IDE)甚至可以注意到Future,并在空格后提供await

与postfix关键字相比,由于模棱两可,postfix字段和方法选项似乎为异议提供了更大的表面积。 由于对postfix关键字的postfix字段/方法的某些支持似乎来自对方法链中存在空格的轻微不安,因此,我们应该查看@valff意见,并与postval关键字( foo.bar() await )看起来不会比广义类型归属( foo.bar() : Result<Vec<_>, _> )令人惊讶。 对于这两者,在此间隔开的插曲之后,链接继续。

@ivandardi@lnicola

我已经用缺少免费函数调用的示例更新了我最近的评论。 如果您需要更多示例,可以参考我之前的评论中的最后一个破坏者

为了辩论future.await?future await? ...

讨论不多的是方法链的可视化分组或grepping。

考虑( 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连接在一起。


是的,其中一些相同的参数适用于通用类型归类,但尚不被接受。 从某种意义上来说,尽管我喜欢键入归属,但我认为在_general_表达式上下文中(模糊,我知道),应该将其包装在括号中。 但是,这对于本次讨论来说都是题外话。 还请记住,代码库中的await数量具有很大的变化,即显着高于类型说明的数量。

通用类型归属还通过使用不同的运算符:将类型绑定到归属表达式。 就像其他二进制运算符一样,读取它可以使(视觉上)清楚地知道两个表达式是一棵树。 鉴于future await没有运算符,并且很容易被误认为future_await的习惯,因为很少会看到两个名称之间没有用运算符分隔,除非第一个是关键字(除非有例外,否则说我更喜欢前缀语法,但我不喜欢)。

如果愿意,可以将其与英语进行比较,其中使用连字符( - )直观地对单词进行分组,否则这些单词很容易被解释为单独的单词。

我认为这表明我们不应该那么快就抹杀C#语言团队的经验。

我认为“这么快”和“信誉”在这里不公平。 我认为在这里发生的事情与异步/等待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

当我以可视方式扫描第一条链以等待时,我可以清楚快速地识别send()。await? 并看到我们正在等待send()的结果。

我更喜欢间隔版本,因为更容易看到将来的构建位置和等待的位置。 恕我直言,更容易看到发生了什么

image

使用点分隔,看起来非常像方法调用。 突出显示代码不会有太大帮助,而且并非始终可用。

最后,我确实相信链接不是主要的用例( ?除外,但foo await?尽可能清晰),并且

post(url).multipart(form).send().match?

post(url).multipart(form).send() match?

后者看起来更精简。

因此,如果我们确实缩小到future awaitfuture.await ,我们是否可以使魔术“点”成为可选内容? 这样人们就可以选择自己想要的一个,最重要的是,停止争论并继续前进!

拥有可选的语​​法没有害处,Rust有很多这样的例子。 最有名的是最后一个Sperator( ;,或其他,在最后一个项目之后,例如(A,B,) )在大多数情况下都是可选的。 我没有看到为什么不能将点设为可选的强烈理由。

我认为现在是在Nightly进行此操作的好时机,让生态系统确定哪一个最适合他们。 我们可以用棉绒来强制使用首选样式,使其可自定义。

然后,在登陆Stable之前,我们会检查社区的使用情况,并决定我们应该选择其中一项,还是只保留两者。

我完全不同意宏对于该功能的感觉并不一流(一开始我不认为它是核心功能),并想再次强调我先前await!()评论。 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的示例都比它更反对它。 漫长的等待链让人感到不对,应将链保留给将来的组合器。

比较(将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()返回带有其他方法的自定义Future,例如,将非200状态消息转换为硬错误,则无需额外等待。 现在,至少在reqwest中,它似乎就位运行并产生普通的Result而不是新的未来,这就是这里的问题所在。

在我看来,等待链接是一种强烈的代码味道,而.await.await()会使很多新用户感到困惑,甚至不好笑。 .await尤其感觉像Python或PHP之外的东西,其中变量成员访问可以神奇地具有特殊的行为。

await应该是显而易见的,好像在说,“在继续之前,我们等待这个”而不是“然后,我们等待这个”。 后者实际上是and_then组合器。

考虑它的另一种潜在方法是将async / await表达式和期货视为迭代器。 他们都是懒惰的,可以有很多的状态,并能与语法被终止( for-in的迭代, await期货)。 我们不会为迭代器提供语法扩展,以像我们在这里那样将它们链接在一起作为正则表达式,对吗? 他们使用组合器进行链接,异步操作/功能也应这样做,仅终止一次。 效果很好。

最后,最重要的是,我希望能够垂直浏览一下函数的前几个缩进,并清楚地看到异步操作发生在哪里。 我的视野不是很好,等待在行尾或停留在中间会使事情变得更加困难,我可能会完全呆在原始期货上。

大家在发表评论之前,请先阅读此博客文章

异步/等待与便利无关。 这与避免合并器无关

这是关于启用组合器无法完成的功能。

因此,任何有关“让我们使用组合器代替”的建议都是题外话,应在其他地方讨论。

注意到中间缺少等待吗? 因为链接通常可以通过更好的API设计来解决。

在真正的异步示例中,您不会得到一个漂亮的链。 真实的例子:

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()作为保留的成员字段/方法访问的保留,我想知道是否可以使用通用的“ ascription”语法,(例如,“给我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

编辑:在我看来,在合并的示例中使用2个归属会更合乎逻辑:

let result = connection.fetch("url") : yield?
    .collect_async() : yield : Vec<u64>?;

@Pauan过度应用该博客文章是不合适的。 组合符可以与await语法结合使用,以避免它描述的问题。

核心问题始终是,可产生的将来必须是'static ,但是如果您通过引用在这些组合器中捕获变量,则最终会得到不是'static的将来。 但是等待非'static未来并不会使您不在'static的异步。 因此,您可以执行以下操作:


//切片:&[T]

让x =等待future.and_then(| i |如果slice.contains(i){async_fn(slice)});

因此,我原本打算完全不发表评论,但我想补充一些未编写大量异步代码但仍然必须阅读的人的注释:

  • 前缀await很容易注意到。 这既是您期望的(在async fn内部)又是外部(在某些宏主体的表达式中)。
  • 后缀await作为关键字可能在其他关键字很少甚至没有的情况下脱颖而出。 但是,一旦您与其他控制结构发生了闭包或类似操作,您的awaits就会变得不那么明显,并且更容易被忽略。
  • 我发现.await作为伪字段非常奇怪。 在任何方面,对我而言,这都不像其他任何领域。
  • 我不确定依靠IDE检测和语法突出显示是否是个好主意。 没有语法高亮显示时,易于阅读很有价值。 加上最近有关使语法复杂化及其对工具造成的问题的讨论,依靠IDE /编辑器正确处理可能不是一个好主意。

我不同意await!()作为宏“不会感到一流”的观点。 宏是Rust中完全一流的公民。 它们也不仅仅用于“发明用户代码中的语法”。 许多“内置”宏不是用户定义的,也不是仅出于语法原因而存在。 format!()可以为编译器提供静态类型检查格式字符串的功能。 include_bytes!()也不是用户定义的,也不是仅出于语法原因而存在,它是一个宏,因为它需要进行语法扩展,因为它再次在编译器内部执行特殊操作,并且不能合理地以任何其他方式编写,而无需将其添加为核心语言功能。

这就是我的观点:宏是一种以统一且毫不奇怪的方式引入和隐藏编译器魔术的完美

因此,我根本看不到需要特殊关键字。 就是说,如果它将成为关键字,那么应该仅仅是一个裸关键字–我真的不了解将其伪装成一个字段的动机。 这似乎是错误的,它不是字段访问,甚至不是字段访问那样的远程访问(不像说用糖的getter / setter方法可能那样),因此将其视为一个字段与当今语言在语言中的工作方式不一致。

@ H2CO3 await!宏是每一种理想的方法,除了两个属性外,它都是好的:

  1. 当您写又一千个等待时,多余的花括号确实很烦人。 出于相同的原因,Rust从if/while/...块中删除了括号。 看起来atm并不重要,但事实确实如此
  2. async为关键字的不对称性。 异步应该做的事情不仅仅是在方法主体中允许一个宏。 否则,它可能只是函数顶部的#[async]属性。 我知道该属性不允许您在块等中使用它,但是它提供了遇到await关键字的一些期望。 这种期望可能是由其他语言的经验引起的,谁知道,但我坚信它的存在。 它可能没有解决,但必须加以考虑。

否则,它可能只是函数顶部的#[async]属性。

很长一段时间了,很抱歉看到它。 我们正在引入另一个关键字,而#[async]属性运行得很好,并且现在具有稳定的类似属性的过程宏,即使在语法和语义上,它甚至与其他语言都保持一致(并由编译器专门处理)。

@ H2CO3从用户角度看,将await作为宏的目的或优点是什么? 是否有任何内置宏可以更改程序在后台的控制流?

@ I60R的优点是与其他语言的统一性,因此不必担心另一个关键字以及它带来的所有

我看不出为什么第二部分很重要–不能有一个内置的宏可以执行新操作吗? 实际上,我认为几乎每个内置宏都会产生一些“怪异”的事情,如果没有编译器的支持就无法(合理/有效/如此)实现。 在这方面, 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?);

我也对核心思想的其他实现感到满意,“每个表达链读一次等待就足够明确了”。 因为它减少了样板,但也使链接成为可能,这是每个人都最熟悉的基于前缀的语法。

@yazaddaruvala上面有一个警告

我很好奇大多数社区是否会妥协并允许“部分隐式等待”? 也许每个表达式链的显式阅读等待一次?

  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 ? 好的,我们可能可以使一行代码的等待工作(因此不会等待嵌套的mapFunc ),但是打破这种行为太容易了。 如果等待对一条代码行有效,则任何重构(例如“提取值”)都会破坏它。

@yazaddaruvala

我是否理解您正确的意图是使用前缀? wait优先于

这意味着以下这三个语句是等效的:

// 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())?;

我不确定语法是否理想,但是也许具有更改为“等待隐式上下文”的关键字会使前缀关键字的问题减少。 同时,允许在需要时使用更精细的控制来控制等待位置。

在我看来,简短的单行前缀等待具有与许多其他编程语言和许多人类语言相似的好处。 如果最后决定只将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”语法我建议工程比更复杂的场景,如目前的提议await (a, b) = (client.get("a")?, client.get("b")?);使用正常的后缀或前缀的await let (a, b) = (client.get("a") await?, client.get("b") await?);结果在连续的网络操作,其中部分隐式版本可以通过编译器适当地并发运行(如我在原始帖子中所示)。

早在2011年,当C#中的async功能仍在测试中时,我询问await是前缀运算符,并从C#语言PM中得到了答复。 既然讨论了Rust是否应该使用前缀运算符,所以我认为在此处发布该响应可能很有价值。 来自为什么“ await”是前缀运算符而不是后缀运算符?

正如您可以想象的那样,在我们确定现在存在的内容之前,等待表达式的语法是一个非常重要的讨论:-)。 但是,我们对后缀运算符的考虑并不多。 关于postfix的某些内容(请参阅HP计算器和Forth编程语言)使原则上更简单,实践中更难于理解或理解。 也许这只是数学符号洗净我们小时候的方式...

我们肯定发现前缀运算符(具有其命令性的味道-“做到这一点!”)到目前为止是最直观的。 我们大多数的一元运算符已经是前缀,并且等待似乎与之匹配。 是的,它淹没在复杂的表达式中,但是评估顺序通常也是如此,因为例如函数应用程序也不是后缀。 首先评估参数(在右侧),然后调用函数(在左侧)。 是的,C#中的函数应用程序语法已经内置了括号,这是有区别的,而在等待时通常可以删除它们。

我在await中大致看到的(并且被我自己采纳的)是一种使用大量临时变量的样式。 我倾向于

var bResult = await A().BAsync();
var dResult = await bResult.C().DAsync();
dResult.E()

或类似的东西。 通常,我通常会避免在最简单的表达式之外的所有表达式中都等待一个以上的等待(即它们都是同一函数或运算符的参数;它们可能很好),并且我会避免在等待表达式中加上括号,宁愿对这两个作业都使用额外的局部变量。

说得通?

Mads Torgersen,C#语言PM


我作为C#开发人员的个人经验是,语法促使我使用这种样式的多条语句,这使得异步代码的读写比同步等效代码笨拙得多。

@yazaddaruvala

我不一样。 我看到它始终是显式和隐式的折衷

它仍然是明确的。 我可以链接有关btw的好文章

我认为编译器应该出错。 存在巨大的类型差异,无法推断。 同时,以Vec为例不能通过该线程中的任何语法解决。

为什么错误呢? 我很高兴拥有Vec<impl future> ,然后我可以一起加入。 您无缘无故地提出了额外的限制。

我认为Vec这是一个很差的例子,否则我们应该使所有提议的语法都遵循相同的标准。

我们应该检查每一种情况。 我们不能仅仅忽略边缘情况,因为“嗯,无论如何,没人会写这个”。 语言设计主要是关于边缘情况。

@chescock我同意您的(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关键字是最好的方法。

别忘了每天晚上,所以如果我们找到更好的方法,我们可以稍后再做。

在我看来,结合以下各项,我们可能能够同时兼顾两个方面:

  • 前缀async关键字
  • 一般的postfix宏功能

两者在一起将允许实现后缀.await!()宏而无需编译器魔术。

现在,后缀.await!()宏将需要编译器魔术。 但是,如果稳定了await关键字的前缀变体,则将不再是这样:后缀.await!()宏可以简单地实现为以其参数(表达式)为前缀的宏使用await关键字并将所有内容括在方括号中。

这种方法的优点:

  • 前缀await关键字可用,并且熟悉其他语言的用户也可以使用。
  • 可以在需要使表达式的异步特性“脱颖而出”的地方使用前缀async关键字。
  • 将有一个postfix变体.await!() ,该变体可用于链接情况或前缀语法不方便的任何其他情况。
  • 对于postfix变体,不需要(可能是意外的)编译器魔术(否则,对于postfix字段,方法或宏选项将是一个问题)。
  • 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关键字
  • 一般的postfix宏功能

这需要使await成为上下文关键字,而不是当前的真实关键字。 我不认为我们应该这样做。

两者一起可以实现后缀.await!()宏,而无需编译器魔术。

这是比foo awaitfoo.await (尤其是后者)更复杂的解决方案实施方式。 仍然有很多“魔术”。 您刚刚进行了会计核算

这种方法的优点:

  • 前缀await关键字可用,并且熟悉其他语言的用户也可以使用。

如果以后要添加.await!() ,我们只是给用户更多的知识( await foofoo.await!() ),现在用户会问“我应该使用哪个什么时候..”。 与单个解决方案相比,这似乎比foo awaitfoo.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本质上是解析。$ ident,然后在ident == keyword :: Await.name()上不会出错。

我认为荣誉属于await!(future) ,因为语法已经完全支持它。

@Centril try!最终变得多余,因为?运算符可以严格执行更多操作。 这不是“不适合该语言”。 但是,您可能不喜欢它,但我接受。 但是对我来说,这实际上是Rust发明的最好的东西之一,也是卖点之一。 而且我知道它是在没有编译器支持的情况下实现的,但是在讨论它是否执行控制流程时,我看不到它的意义。 不管。

等待不是“每一个..”

但是这里提到的其他内容(例如or_else )是,我的观点仍然适用于主要功能。 仅出于添加语法的目的而添加语法并不是优势,因此,只要在更一般的情况下已有某些东西已经起作用,就应该优先于发明新的表示法。 (我知道反对宏的另一种说法是“它们不是后缀”。我根本不认为,等待成为其自己的后缀运算符的好处足以证明其成本是合理的。我们在嵌套函数调用中幸免于难。在编写了几个嵌套宏之后,效果会一样好。)

在2019年1月23日,星期三,09:59:36PM + 0000,Mazdak Farrokhzad写道:

  • Postfix宏也可用于其他情况(例如.or_else!(continue) ),出于类似的原因,它们偶然碰巧需要postfix宏语法(否则,它们需要以尴尬且难以理解的方式包装前面的表达式)。

我相信后缀宏具有价值,应该添加到语言中。 但这并不意味着它们需要用于await ing; 如您所述,在.or_else!(continue)和许多其他地方,后缀宏将很有用。

使用.await!()主要原因是看起来像一个宏
很明显,它会影响控制流。

.await看起来像字段访问, .await()看起来像函数
调用,字段访问和函数调用都不会影响控制
流。 .await!()看起来像一个宏,并且宏影响控制
流。

@joshtriplett我在标准意义上不影响控制流。 这是证明借位检查程序应按定义的期货运行的基本事实(而pin则是推理方式)。 从本地函数执行的角度来看,执行等待就像大多数其他函数调用一样。 您从上次中断的地方继续,并在堆栈上有一个返回值。

我只是发现这与语法讨论不符。 问题不仅仅在于现在,还在于几版之后的5年,10年,20年。

我不知道你这是什么意思。

我认为荣誉属于await!(future) ,因为语法已经完全支持它。

我理解,但是在这一点上,由于该语法还有许多其他缺点,我认为它已被有效排除。

@Centril try!最终变得多余,因为?运算符可以严格执行更多操作。 这不是“不适合该语言”。 但是,您可能不喜欢它,但我接受。

它已明显过时,并且在Rust 2018上写入try!(expr)也是一个硬错误。我们共同决定这样做,因此被认为不适合。

使用.await!()主要原因是看起来像一个宏可以清楚地表明它会影响控制流。

.await看起来像字段访问, .await()看起来像函数调用,并且字段访问和函数调用都不会影响控制流。

我相信,区别是用户会相对容易地学习到的东西,尤其是.await通常后面跟着? (这函数局部控制流)。 至于函数调用,如上所述,我认为可以说迭代器方法是控制流的一种形式。 此外,仅仅因为一个宏可以影响控制流,并不意味着它这么做。 许多宏都没有(例如dbg!format! ,...)。 的理解,即.await!().await将影响控制流量(寿比弱得多的感? ,按照@HeroicKatora注)从单词流动await本身。

@Centril

这需要使上下文关键字与当前的真实关键字相反。 我不认为我们应该这样做。

真是的那有点痛苦。 也许宏可以使用不同的名称,例如.wait!().awaited!() (后者非常好,因为它清楚地表明它适用于前面的表达式)。

“这两者在一起将允许实现后缀.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是
在暂停点之前释放。 在大型程序中
这样的无数微妙之处在于当前功能
暂停是重要的信息。

2019年1月23日,星期三,2:21 PM HeroicKatora [email protected]
写道:

@joshtriplett https://github.com/joshtriplett我不影响控制
标准意义上的流量。 这是证明
借方检查程序应按定义在期货中运行。 从这个角度来看
本地函数执行,等待执行就像其他任何函数调用一样。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba

在2019年1月23日,星期三,00-08:02:26:07 PM,Mazdak Farrokhzad写道:

使用.await!()主要原因是看起来像一个宏可以清楚地表明它会影响控制流。

.await看起来像字段访问, .await()看起来像函数调用,并且字段访问和函数调用都不会影响控制流。

我相信区别是用户将相对容易学习的东西

他们为什么要这么做?

我相信区别是用户会相对容易地学习到的东西,尤其是.await通常会跟在后面吗? (这是功能局部控制流)。

通常是,但并非总是如此,并且语法绝对适用于并非如此的情况。

至于函数调用,如上所述,我认为可以说迭代器方法是控制流的一种形式。

return? (甚至break )的方式不同。 这将是非常惊人的,如果一个函数调用可以在两个水平恢复起来了调用它的功能(的原因,锈没有例外之一正是因为这是令人惊讶的),或在调用破环了功能。

而且,仅仅因为宏会影响控制流并不意味着它会这样做。 许多宏都没有(例如dbg!,format!,...)。 .await!()或.await的理解将影响控制流(根据@HeroicKatora的注释,其含义比?弱得多)来自于await本身。

我一点都不喜欢。 影响控制流是非常重要的副作用。 它应该具有通常无法执行的语法格式。 在Rust中可以做到这一点的构造有:关键字( returnbreakcontinue ),运算符( ? )和宏(通过求一个其他形式)。 为什么通过添加例外来使水变得浑浊?

@ejmahler我不认为与普通函数调用相比,持有RefCell有何不同。 内部函数也可能想要获取相同的RefCell。 但是似乎没有一个主要问题是不能立即掌握全部潜在的调用图。 同样,堆栈耗尽问题也无需特别注意,您必须注释提供的堆栈空间。 这里的比较将有利于协程! 如果未内联常规函数调用,是否应该使它们更加可见? 我们是否需要显式的尾部调用来解决图书馆日益棘手的问题?

答案是,恕我直言,使用RefCell并等待时最大的风险是不熟悉。 当上面的其他问题可以从硬件中抽象出来时,我确实相信,作为程序员,我们也可以学会在屈服点上不保留RefCell等,除非经过审查。

Elliott Mahler在2019年1月23日星期三晚上10:30:10 PM +0000写道:

理论上是的,对于琐碎的程序是,但是实际上程序员
需要知道他们的暂停点在哪里。

:+1:

@HeroicKatora

一个关键的区别是您必须检查多少代码,多少代码
您必须考虑的可能性。 程序员已经习惯了
确保他们调用的所有内容都不使用已检查的RefCell
出来。 但是等待着,我们没有检查单个调用堆栈,而是没有
控制在等待期间执行的内容-实际上是
程序可能已运行。 我的直觉告诉我,一般的程序员是
因为他们没有足够的能力来应对这种可能性的爆炸式增长
不必频繁地处理它。

在游戏中,悬停点是关键信息的另一个示例
编程,我们通常会编写要恢复的协程
每帧一次,我们在每次简历上更改游戏状态。 如果我们忽略了
这些协程之一中的暂停点,我们可能会使游戏陷入困境
多个帧的状态。

所有这些都可以通过说“变得更聪明,更少做些”来解决。
错误”,但从历史上看,这种方法行不通。

在2019年1月23日星期三下午2:44 Josh Triplett [email protected]
写道:

Elliott Mahler在2019年1月23日星期三晚上10:30:10 PM +0000写道:

理论上是的,对于琐碎的程序是,但是实际上程序员
需要知道他们的暂停点在哪里。

:+1:

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba

再举一个关键点,即暂停点是关键信息,在游戏编程中,我们通常编写协程,以便每帧恢复一次,并在每次恢复时更改游戏状态。 如果我们忽略了其中一个协程中的暂停点,则可能会使游戏在多帧状态下处于破裂状态。

如果将密钥放在SlotMap上,则会泄漏内存。 语言本身并不能帮助您解决此问题。 我想说的是,您关于等待的方式的示例模糊不清(只要它是一个关键字,它的出现将是唯一的)似乎并不能推广到其他功能尚未发生的问题。 我认为您应该减少对语言立即提供给您的功能的评价,而应更多地考虑如何使您能够表达自己的功能。 评估您的工具,然后应用它们。 建议一种游戏状态的解决方案,以及如何很好地解决该问题:编写一个包装程序,该包装程序在返回时断言游戏状态实际上已达到要求的数量,异步允许您为此原因借用您自己的状态。 在该分隔的代码中禁止其他人等待。

RefCell中的状态爆炸不是来自您未检查是否未在其他地方使用过它,而是来自其他代码点,这些代码点不知道您依赖于该不变量并将其静默添加。 这些以相同的方式处理,包括文档,注释和正确的包装。

阻塞io时,持有RefCell与暂存Mutex并没有太大区别。 您可能会遇到死锁,这比紧急情况更糟,更难调试。 但是,我们不使用显式的block关键字注释阻塞操作。 :)

在异步函数之间共享RefCell将大大限制它们的一般可用性( !Send ),因此我认为这并不常见。 通常应避免使用RefCell ,并且在使用时尽可能短地借用。

因此,我认为在收益点上意外地暂存RefCell并不重要。

如果我们忽略了其中一个协程中的暂停点,则可能会使游戏在多帧状态下处于破裂状态。

协程和异步函数是另一回事。 我们不是要使yield隐式。

对于任何支持宏语法而不是关键字语法的人,请在此处进行说明:请描述它与await含义,这意味着它应该是一个宏,以及与_could_具有的while有何不同只是一个while!宏(即使仅使用macro_rules! ;也没有任何特殊框)。

编辑:这不是修辞。 我对这种事情真的很感兴趣,就像我以为我想运行到第一次等待(就像C#)一样,但是RFC还是说服了我。

对于任何支持宏语法而不是关键字语法的人,请在这里:请描述await的含义,这意味着它应该是一个宏,以及它与(while)的区别(可能只有一段时间了)! 宏(甚至只使用macro_rules!;根本没有特殊框)。

我大部分都同意这种推理,并且我确实想使用关键字作为前缀语法。

但是,如上所述(https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831),我支持在postfix语法中使用postfix宏(也许.awaited!()以避免名称与prefix关键字冲突)。 我的部分原因是,拥有只能以一种方式使用的关键字会使事情变得更简单,并且为宏保存了进一步的变体,但据我所知,主要是,没有人能够提出后缀我认为可以接受的关键字语法:

  • foo.bar().await技术上讲, foo.bar().await()是关键字,但是它们看起来像是字段访问或方法调用,我不认为这样的控制流修改结构应该被隐藏。
  • foo.bar() await只是令人困惑,尤其是与进一步的链接(例如foo.bar() await.qux() 。 我从未见过像这样的后缀用作关键字(我敢肯定它们存在,但实际上我想不出任何一种我所知道的语言都可以像这样工作的示例)。 而且我认为它的理解非常混乱,好像等待适用于qux()而不是foo.bar()
  • foo.bar()@await或其他一些标点符号也可以使用。 但这不是特别好,而且感觉很特别。 我实际上认为此语法没有任何重大问题(与上述选项不同)。 我只是觉得,一旦我们开始添加sigils,那么平衡就开始朝着放弃自定义语法,而朝着成为宏的方向发展。

我将再次重申较早的想法,尽管经过了新的思想和考虑因素的调整,然后尝试保持沉默。

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?是将?到异步操作结果中的简便快捷方式,但这并不是绝对必要的。

如果postfix宏成为官方的东西(我希望他们会这样做),则await!(...).await!()可以并存,从而针对特定的用例和样式允许三种等效的等待方式。 我认为这不会增加任何认知开销,但会提供更大的灵活性。

理想情况下, async fn#[async] fn*甚至可以共享用于将基础生成器转换为异步状态机的实现代码。

这些问题已经清楚地表明,没有一种真正的首选样式,因此我所能期望的最好是提供干净,灵活,易读且易于接近的复杂性级别。 我认为上述方案是对未来的一个很好的妥协。

await作为关键字无论如何仅在async函数内有效,因此我们应该让标识符在其他地方等待。

考虑到卫生宏,我不知道在Rust中是否可行。 如果我把foo!(x.await)foo!(await { x })当它想的expr ,我想应该是明确的是, await关键字是想-不是await字段或带有字段init简写的await结构体文字表达式-甚至在同步方法中。

允许三种等效的等待方式

请不。 (至少在core 。显然,如果需要,人们可以在自己的代码中创建宏。)

现在,后缀.await!()宏将需要编译器魔术。 但是,如果稳定了await关键字的前缀变体,那么它将不再成立:postfix .await!()宏可以简单地实现为以await关键字为参数(表达式)加上前缀并包装的宏括号中的所有内容。

我会注意到这同样正确-但更容易! -从另一个方向来看:如果我们稳定.await关键字语法,那么人们如果已经足够不喜欢后缀,就可以制作一个awaited!()前缀宏。

我不喜欢添加多个允许的变体(例如前缀和后缀)的想法,但是出于与用户要求略有不同的原因。 我在一家相当大的公司工作,并且为代码风格而战是真实的。 我只想使用一种显而易见的正确形式。 毕竟,就这一点而言,Python的Zen可能是正确的。

我不喜欢添加多个允许的变体(例如前缀和后缀)的想法,但是出于与用户要求略有不同的原因。 我在一家相当大的公司工作,并且为代码风格而战是真实的。 我只想使用一种显而易见的正确形式。 毕竟,就这一点而言,Python的Zen可能是正确的。

我明白你的意思。 但是,没有办法阻止程序员定义自己的宏来执行诸如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关键字。 等待是一个神奇的转变,使用关键字可以使它突出。 它看起来并不像字段访问,方法或宏调用,并且我们不必将其解释为例外。 它非常适合当前的解析规则和?运算符。 可以说,方法链接中的空间不会通用类型Ascription,这似乎是某种形式的RFC。 不利的一面是,IDE可能需要做更多的事情才能为await提供自动补全功能。 人们可能会发现方法链接中的空间太麻烦了(尽管@withoutboats认为这可能是一个优势),特别是如果代码的格式没有使await结束每一行。 它使用一个关键字,如果采用另一种方法,也许我们可以避免使用该关键字。

  • __ expr.await (后缀字段)__:后缀字段语法利用了“点的力量” -看起来和感觉上很自然,并且允许IDE自动完成await而无需执行其他操作(例如如自动删除点)。 就像后缀关键字一样简洁。 在await前面的点可能会使使用grep查找它的实例更容易。 不利的一面是现场访问。 作为一种明显的现场访问方式,没有视觉上的暗示“这可以做某事”。

  • __ expr.await() (后缀方法)__:后缀方法的语法类似于后缀字段。 从好的方面来说, ()调用语法向读者表明“这可以做些事情”。 从本地来看,这await的魔术行为伪装为一种方法可能会造成混淆。 我们可以说await是一种方法,在有限的意义上,Scheme中的call/cc是一个函数。 作为一种方法,我们需要考虑Future::await(expr)是否应与UFCS保持一致。

  • __ expr.await!() (后缀宏)__:后缀宏语法类似地利用“点的力量”。! bang表示“这可能会做一些魔术。” 不利的一面是,它甚至更嘈杂,而宏虽然具有魔力,但它们通常不会像await那样对周围的代码产生魔力。 不利的一面是,假设我们对通用后缀宏语法进行了标准化,那么继续将await作为关键字可能会出现问题

  • __ expr@expr#expr~和其他单字符符号__:与我们使用?一样使用单个字符可以使简洁性最大化,并且可能使后缀语法看起来更自然。 与? ,我们可能会发现,如果await开始遍及我们的代码,我们将非常感谢您的简洁。 不利的一面是,除非而且直到用await乱码的麻烦成为问题为止,否则很难就采用这种语法所固有的权衡取舍达成共识。

我想发表一个帖子,对@traviscross的这些摘要帖子表示感谢! 它们一直都写得很好并且交联得很好。 非常感谢。 :心:

我有一个想法,就是像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很棒的总结。 围绕sigil和关键字的组合也进行了一些讨论,例如fut@await ,所以我只在这里为那些来此线程的人添加这个。

我在这里列举了这种语法的优缺点。 @earthengine表示,除@以外的其他信号也可以,例如~@BenoitZugmeyer支持@await ,并询问postfix宏ala expr!await是否是一个好主意。 @dpc认为@await太特殊,并且与我们现有的资源集成得不好,Rust已经很重。 @cenwangumass同意这太临时了。 @newpavlovawait部分让人感到多余,尤其是如果我们将来不添加其他类似的关键字。 @nicoburns语法可以工作,并且它没有很多问题,但是它只是解决方案的临时方案。

@traviscross很棒的总结!

我认为我的0.02 $从坏到高的顺序是:

  • 3是绝对不可行的,因为它几乎不是方法调用。
  • 2不是一个领域,这非常令人困惑,尤其是对于新手。 在完成列表上包含await并没有真正的帮助。 写完成千上万个之后,您只需将其写为fnextern 。 额外的完成甚至比没有更糟糕,因为我将通过关键字建议我而不是有用的方法/字段。
  • 4 Macro非常适合这里使用,但对我而言并不理想。 我的意思是不对称,其中async是关键字,如上所述
  • 5 Sigil可能过于简洁且难以发现,但它是一个独立的实体,因此可能会被对待。 看起来与其他任何东西都不相似,因此不会给用户造成混乱
  • 1最好的方法恕我直言,它只是一个易于识别的标记,并且已经是保留关键字。 如上所述,空间分隔是一个优势,而不是缺陷。 没有格式化就可以存在,但是使用rustfmt则意义不大。

当有人一直在等待这一刻的到来时,这也是我的0.02美元。

在大多数情况下,我同意@Pzixel ,除了最后两点,在某种意义上,我会交换它们,或者甚至可以同时选择两者,例如try!() ?对。 为了清晰起见,我仍然看到人们争论try!()胜过? ,并且我认为在这种特殊情况下让代码外观具有一定的代理权是有利的,而不是不利的,即使以拥有两种不同的语法为代价。

特别是,如果您将异步代码编写为明确的步骤序列,则await后缀关键字非常有用,例如

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语句中看起来更好,而单符号后缀在表达式中看起来更好。

无论如何,这些只是我的想法。

由于这已经是所有骑自行车的主题之母,因此我想添加到目前为止尚未提及的另一种标记: ->

其想法是从其后继函数的返回类型声明中镜像-> ,该函数(即函数为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,经过阅读以上大部分讨论并考虑了几天的拟议选择。 没有理由给我任何意见-我只是互联网上的一个随意人。 我可能不会再发表更多评论了,因为我谨慎地考虑不要在讨论中增加噪音。


我喜欢后缀符号。 后缀信号有优先级,我认为它会与其他语言保持一致。 对于使用哪种特殊的印记,我没有任何特别的偏爱-没有什么令人信服,但我认为这会随着熟悉而逐渐消失。 与其他postfix选项相比,sigil引入的噪声最小。

我不介意将. (链接时)替换->以便等待。 它仍然简洁明了,但是我更喜欢可以在与?相同的上下文中使用的东西。

我强烈不喜欢已经提出的其他postfix选项。 await既不是字段也不是方法,因此对于像这样呈现控制流构造的其他语言感到完全不一致。 语言中没有任何后缀宏或关键字,因此看起来也不一致。

如果我是该语言的新手,但并不完全了解它,那么我会假设.await.await()不是特殊的语言功能,而是类型上的字段或方法-就像这种情况用户将看到的语言中的其他所有字段和方法。 一旦获得了更多的经验,如果选择了.await!() ,则用户可能会努力学习如何定义自己的后缀宏(就像他们已经学会了定义自己的前缀宏一样)-他们不能。 如果该用户看到了印记,他们可能需要查找文档(就像? ),但是他们不会将其与其他任何东西混淆,并且浪费时间尝试查找.await()或字段.await文档。

我喜欢前缀await { .. } 。 这种方法具有明确的优先级,但确实存在问题(已经详细讨论过)。 尽管如此,我认为这对于那些喜欢使用组合器的人是有益的。 我不希望这是唯一实现的选项,因为它在方法链接方面不符合人体工程学,但是我认为它会很好地补充后缀符号。

我不喜欢其他前缀选项,它们与该语言中的其他控制流构造不一样。 与后缀方法类似,前缀函数不一致,用于控制流的语言没有内置任何其他全局函数。 也没有用于控制流的任何宏( try!(..)除外,但已弃用,因为我们有更好的解决方案-后缀标记)。


我几乎所有的偏好都归结为对我而言自然而一致的事物。 无论选择哪种解决方案,都应在稳定之前给其时间进行试验-亲身经验将比推测更好地判断哪种选择最佳。

还值得考虑的是,可能会有一个沉默的多数成员,他们可能有完全不同的意见-参与这些讨论的人员(包括我自己)不一定代表所有Rust用户(这并不意味着是轻微的意思就是以任何方式冒犯)。

tl; postfix信号是一种表达等待的自然方法(由于优先级高),并且是一种简洁,一致的方法。 我要添加一个前缀await { .. }和一个后缀@ (可以在上下文中用作? )。 对我来说最重要的是Rust保持内部一致性。

@SamuelMoriarty

我认为在这种情况下,从上下文中已经很清楚我们正在处理未来,而await键盘的冗长性是多余的。 比较:

抱歉,但我连看都没有发现~ 。 我重读了整个评论,而我的二读更成功了。 很好地解释了为什么我认为印记更糟。 这只是一个意见,但再次得到了证实。

反对sigils的另一点是Rust变成了J。例如:

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

?@?代表“可能返回错误或将来的函数,应等待,将错误(如果有的话)传播给调用者”

我想要更多

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

罗兰斯坦

由于这已经是所有自行车脱落技术的源泉,因此我想添加另一种迄今为止尚未提及的标志,即AFAICT:->。

使语法依赖于上下文不会使事情变得更好,而只会变得更糟。 这会导致更严重的错误和较慢的编译时间。

对于->我们有一个单独的含义,它与async/await无关。

我希望使用前缀await { .. }并在可能的情况下使用后缀!标记。
感叹号会巧妙地强调期货的惰性。 在给出带有感叹号的命令之前,它们不会执行。

上面的示例将如下所示:

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

很抱歉,如果我的意见不相关,因为我几乎没有异步经验,也没有使用Rust的异步功能生态系统的经验。

但是,看看.send()?!?.json()?!?;以及类似的其他组合,我明白了基于sigil的提议对我来说错的基本原因。

首先,在我看来,无论是?!?还是?~??->? ,链式签名都会很快变得不可读。 这将是初学者偶然发现的另一件事,猜测它是一个运算符还是多个运算符。 信息过于紧凑。

其次,总的来说,在我看来,等待点比错误传播点更不常见,而更重要。 等待点对于我来说足够重要,应该自己成为链中的一个阶段,而不是附加到另一个阶段的“次要转变”(尤其是“仅是次要转变之一”)。 即使仅使用前缀关键字形式(这几乎会迫使人们打破链条),我也可能会很好,但是总的来说,这种限制太过严格了。 我的理想选择可能是类似方法的.await!()宏,将来有可能扩展宏系统以允许类似方法的用户宏。

我认为,稳定的最小基准是前缀运算符await my_future 。 其他一切都可以跟随。

expr....await会反映出直到rustlang运算符正在等待的事物的上下文。 另外async await是并行模式。.await不能表示为method或类似的属性

我倾向于await!(foo) ,但是由于其他人指出,这样做将迫使我们取消等待,因此将其将来用作运算符直到2021年,我将使用await { foo }作为我的偏爱。 至于等待的后缀,我没有特别意见。

我知道这可能不是谈论隐式等待的最佳场所,但是像显式的隐式等待这样的工作会起作用吗? 因此,我们首先有一个前缀等待登陆:

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语法,并且可以平衡对前缀等待,链接和尽可能明确的需求。

我决定以rg --type csharp '[^ ]await'来调查前缀不理想的地方的示例。 并非所有这些都是完美的,但它们是经过代码审查的真实代码。 (对示例进行了稍微的清理,以删除某些域模型内容。)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

与普通的MSTest assert_eq! Assert.Equal相比,使用FluentAssertions作为更好的方法。

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

“看,我真的只需要一件事”这个通用的东西就是一堆。

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

另一个“我只需要一个属性”。 (除了:“我很高兴Rust不需要CancellationToken s。)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

人们在Rust中提到的同一个.collect()

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 ,所以一堆东西最终需要_needing_进入变量而不是链接。

@scottmcm的一些示例在各种postfix变体中变得生锈:

// 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复习选项卡中。

话虽如此,仅依靠突出显示不是一个好习惯。 其他人可能有其他偏好。

嗨,大家好,我是C ++的新Rust学习者。 只想发表评论,就像

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足够独特并且易于学习。

如果现在就稳定下来,并且在使用了两年之后,我们决定确实希望更好地进行链接,那么可以考虑将postfix宏作为一般功能。

我想提出一种想法,让@nicoburns等待前置和后缀。
我们可能有两件事:

  • 一个前缀await关键字(例如,具有比?绑定更强的风味,但这并不重要)
  • std::Future上的新方法,例如fn awaited(self) -> Self::Output { await self } 。 它的名称也可以是block_onblocking ,或者其他更好的名称。

这样既可以使用“简单”前缀,也可以进行链接,同时避免必须等待上下文关键字。

从技术上讲,第二个要点也可以使用postfix宏来完成,在这种情况下,我们将编写.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关键字,并依靠std :: Future上的魔术.awaited()方法,该方法不能由其他人编写自己的期货来实现,但我认为这将是违反直觉的,并且在特殊情况下太多了。

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" “ “ fn。

编辑: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?吗?

让id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?. identity))?;

为此,我认为比较不是很好。 首先, ?不会太大地改变心理模型。 其次, ?的成功在于它不需要您在单词之间来回跳转。 读书时,我有一种感觉?.try_get_default_widget()?是,“好了,你得到一些好的结果吧。” 而已。 我不必跳回去阅读其他内容即可理解这一行。

因此,我的总体结论是,后缀在编写代码时可能会提供一些有限的便利。 但是,这可能会导致阅读代码时遇到更大的问题,我认为这比编写更为常见。

提议的rustfmt样式和语法突出显示( while -> asyncmatch -> await )的实际外观:

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

我不了解您,但我立即发现了match es。

(嘿@ CAD97 ,我已修复!)

@ dowchris97

如果您要争辩说?不会改变思维模型,那么我的论点是await不应该改变代码的思维模型呢? (这就是为什么我以前偏爱隐式等待选项的原因,尽管我一直坚信这不适合Rust。)

具体来说,您在函数头的_beginning_处读取async 。 如果该功能太大而不能在一个屏幕上显示,则几乎可以肯定它太大了,并且与查找await点相比,您还会遇到其他更大的可读性问题。

一旦您成为async ,就需要保留该上下文。 但是到那时,所有等待的是一个转换,该转换通过暂存当前执行序列将延迟的计算Future转换为结果Output

没关系,这意味着复杂的状态机转换。 这是一个实现细节。 与OS线程和阻塞的唯一区别是,在等待延迟计算时,其他代码可能在当前线程上运行,因此Sync不能为您提供太多保护。 (如果有的话,我会读到这是将async fn设为Send + Sync而不是被推断为允许线程不安全。)

以一种更加面向函数的风格,您的窗口小部件和结果看起来像(是的,我知道这不是使用Monad,而真正的纯正等原谅我):

    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放在前面会更好。

我知道|>在其他汉语语言是指用于别的东西,但它看起来非常伟大,非常清楚,我在锈病发生前缀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的! 对这个问题进行认真的人机交互研究怎么样? 因此,我们确实可以使用数据而非推测来说服对方。

@ dowchris97在该线程中只有(两个)比较实际代码的实例:一项〜24k行与数据库和reqwest的比较一项比较说明了C#不能与基于Rust语法的准确比较。 两者都证明,在现实世界中的Rust代码中,在后缀中等待似乎更为自然,并且不会遇到其他没有自然值移动语义的语言所带来的问题。 除非有人出现另一个大小适中的现实世界例子,而这个例子往往显示相反的意思,否则我相当确信前缀语法是其他语言强加给自己的必要条件,因为它们缺乏Rust管道的清晰值语义(在其中读取惯用代码)从左到右几乎总是准确地执行心智模型的建议)。

编辑:即使不够清楚,C#,Python,C ++,Javascript都没有成员方法按值而不是引用接受self 。 C ++具有最接近的右值引用,但与Rust相比,析构函数的顺序仍然令人困惑。

我认为将await作为前缀更好的说法并非源于您如何更改代码的思维模型,而是更多地了解了锈在我们如何使用前缀关键字,而没有后缀关键字(对于后缀,rust使用sigils,例如? )。 这就是为什么await foo()foo() await读起来更容易的原因,以及为什么有些人在语句末尾想要@而又不喜欢在那里有await

出于类似的原因, .await使用感觉很奇怪:点运算符用于访问结构的字段和方法(这也是为什么不能将其视为纯管道运算符的原因),因此使用.await就像说“点用于访问结构的字段和方法,或访问既不是字段也不是函数的await ”。

就个人而言,我希望看到前缀await或后缀符号(不一定是@ )。

两者都证明在现实世界的Rust代码中,在postfix中等待似乎更自然

那是一个有争议的声明。 reqwest示例仅显示后缀语法的一种版本。

另一方面,如果此讨论只取决于谁喜欢什么,请在reddit上提及,以便人们不会像在函数参数中使用impl Trait那样抱怨。

@ eugene2k为了对后缀是否适合Rust程序员的心理模型进行基本讨论,与前缀相比,大多数或所有后缀语法的比例大致相同。 我认为postfix变体之间的可读性差异与前缀和postfix之间没有显着差异。 另请参见我对运算符优先级的低级比较,得出的结论是,它们在大多数用法中的语义是相同的,因此,哪个运算符传达最佳含义至关重要(我目前更喜欢实际的函数调用语法,但是都比其他人有强烈的偏好)。

@ eugene2k Rust中的决策永远不会通过投票做出。 鲁斯特不是民主国家,而是精英阶层。

核心/语言团队会研究所有各种论点和观点,然后做出决定。 该决定是通过(在团队成员中)达成共识,而不是投票。

尽管Rust团队绝对会考虑社区的整体需求,但最终他们还是根据他们认为对Rust最好的方式做出决定。

影响Rust的最好方法是展示信息,提出观点或展示观点。

重复现有的论点或说“我也”(或类似观点)不会增加接受提案的机会。 提案不会根据受欢迎程度接受。

这也意味着,接受该提议的线程中的各种否决票根本无关紧要。

(我不是在专门指代您,而是在此线程中为每个人解释事情的工作方式。)

@Pauan之前有人说过,核心/语言团队会研究各种建议,然后再决定。 但是“更容易阅读”的决定是个人决定。 呈现给决策者的逻辑论据不会改变他们对自己最喜欢的东西的个人看法。 此外,诸如“这是人们如何阅读搜索结果”和“这样的研究进行的论证”这样的论点很容易引起人们的争议(这不是一件坏事)。 可能会使决策者改变主意的是,在他们未曾想到的情况下,他们看到并厌恶了决策的应用结果。 因此,在考虑了所有这些情况之后,团队中的决策者喜欢一种方法,而其他大多数不在团队中的用户则喜欢另一种方法,那么最终的决定应该是什么?

因此,在考虑了所有这些情况之后,团队中的决策者喜欢一种方法,而其他大多数不在团队中的用户则喜欢另一种方法,那么最终的决定应该是什么?

决定始终由团队来决定。 期。 这就是规则的有意设计。

而且这些功能通常是由团队成员实现的。 团队成员还建立了社区内部的信任。 因此,他们拥有法律上和事实上的权威。

如果情况发生变化(可能基于反馈)并且团队成员改变了主意,则他们可以更改其决定。 但是即使这样,决策仍始终由团队成员来完成。

正如您所说,决策通常涉及一些主观性,因此不可能取悦所有人,但必须做出决策。 为了做出决定,Rust使用的系统基于团队成员达成共识。

关于Rust是否以不同的方式进行管理的任何讨论都是题外话,应在其他地方进行讨论。

(PS我不在核心团队或lang团队中,所以我在此决策中没有任何权限,因此我必须服从他们,就像您所做的一样)

@HeroicKatora

我认为postfix变体之间在可读性上没有太大的区别

我不同意。 我发现foo().await()?.bar().await()?foo() await?.bar() await?甚至foo()@?.bar()@?更容易阅读,尽管如此,我觉得拥有不是真正方法的方法树立了不好的先例。

我想提出另一个想法。 我同意前缀等待不容易与其他功能链接在一起。 后缀语法如何: 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. 后缀宏:
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等待。

@ I60Rexpr.[await it.foo()]语法(带有上下文it关键字)非常简洁。 我没想到会喜欢任何新颖的语法建议,但这确实非常不错,巧妙地使用了语法空间(因为IIRC .[目前在任何地方都不是有效的语法),并且可以解决更多问题问题不仅仅是等待。

同意绝对需要RFC,并且它可能并非最佳选择。 但是我认为这是暂时解决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!()语法有问题。

比较:
C#
等待{foo.bar(url,false,qux.clone())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps可以通过将语言设置为c#来获得简单示例的asyncawait语法突出显示。)

@nicoburns您可以将(){}[]与宏一起使用。

@sgrif很好。 而且,在这种情况下,我建议“带强制性分隔符的前缀关键字”作为一种选择几乎没有意义。 由于它破坏了与其他宏的一致性,因此基本上没有好处。

(FWIW,我仍然认为带有后缀宏或@ I60R的后缀建议之类的通用解决方案的“不带合理的选择,但是“只

我建议“带强制性分隔符的前缀关键字”作为一种选择几乎没有意义。 由于它破坏了与其他宏的一致性,因此基本上没有好处。

为什么没有意义,为什么关键字的语法与宏的语法不同?

@tajimaha

我建议“带强制性分隔符的前缀关键字”作为一种选择几乎没有意义。 由于它破坏了与其他宏的一致性,因此基本上没有好处。

为什么没有意义,为什么关键字的语法与宏的语法不同?

好吧,如果await是一个宏,那么它将具有不向该语言添加任何额外语法的优点。 从而降低了语言的复杂性。 但是,使用关键字有一个很好的论据: returnbreakcontinue ,其他控制流修改构造也是关键字。 但是所有这些都没有定界符,因此,为了与这些构造保持一致, 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种语法形式而不是2种形式。并且看到关键字与强制性分隔符相比宏语法没有任何好处,我认为仅坚持标准将是可取的如果我们希望强制使用定界符(我根本不认为应该这样做),请使用宏语法。

对于那些今天在Rust中使用大量异步/等待的人来说,是一个问题:您多久等待一次函数/方法与变量/字段?

内容:

我知道在C#中,通常会做一些归结为这种模式的事情:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

这样,两者并行运行。 (有人会说这里应该使用Task.WhenAll ,但是性能差异很小,因为它需要通过数组索引,所以它使代码更混乱。)

但它是我的理解是,在防锈,不会真正并行运行在所有,因为pollfooTask不会被调用,直到bar得到了它的价值,并且需要使用组合器,也许

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关键字_was_的原因之一,该空间会破坏可视块。)

如果我们这样做,不妨采用.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?;

是否有任何已经大量使用的存储库? 我建议用每种提议的样式重写一个更大的块

@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. 如果future部分较长,则await { future }?看起来并不嘈杂。 查看let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. 当行短时,使用await(future)可能更好。 查看Ok(await(req.json())?)

IMO,通过在两个变体之间切换,此代码的可读性比以前好得多。

第一个示例格式错误。 我不认为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?;

在这里,异步闭包处理每个ID,它与更高级别的控制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())?;

现在让我们玩得开心,并使用Futuresight 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?;
后缀关键字
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中,人们更有可能将它们写在单独的行中。 我实际上没有在python中看到await (await f)

带强制分隔符的前缀

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是在适当的情况下减少其使用。

好吧,这就是等待后缀的全部要点。 由于这是Rust,因此由于Rust鼓励链接,因此人们不太可能将它们写在单独的行中。 因此,后缀语法本质上是强制性的,因此指令流遵循相同的行读取流。 如果我们没有postfix语法,那么会有很多带有临时文件的代码也被链接在一起,而如果我们有postfix等待,那么它们都可以简化为一条链。

@ivandardi @mehcode在异步/等待时从Rust RFC复制:

在获得基于期货的生态系统的经验和用户反馈后,我们发现了某些人体工程学挑战。 使用需要在等待点之间共享的状态是非常不符合人体工程学的-需要弧线或联接链接-并且尽管组合器通常比手动编写Future更符合人体工程学,但它们仍经常导致混乱的嵌套和链接回调集。

...与语法糖配合使用,这种语法糖已在许多使用异步IO的语言中普遍使用-异步和等待关键字。

从用户的角度来看,他们可以像使用同步代码一样使用async / await,并且只需注释其功能和调用即可。

因此,引入异步等待的全部目的是减少链接并制作异步代码,就好像它们是sync一样。 在此RFC中仅两次提到了链接,即“需要Arcs或Join链接”和“仍然经常导致混乱的嵌套和链接回调集”。 对我来说听起来不太积极。

为链接而争论,因此为postfix关键字争论可能需要对该RFC进行重大重写。

@tajimaha您误解了RFC。 它只是在谈论Future组合器( mapand_then等),而不是在谈论一般的链接(例如返回impl Future )。

我认为可以肯定地说async方法很普遍,因此链接确实非常重要。

另外,您会误解该过程:不需要重写RFC。 RFC是一个起点,但不是规范。 没有一个RFC是一成不变的(也不应该这样!)

RFC流程是不稳定的,几乎没有您所暗示的那样严格。 RFC中没有任何内容可以阻止我们讨论后缀await

另外,所有更改都将放入稳定RFC中,而不是原始RFC(已被接受,因此不会被更改)中。

为链接而争论,因此为postfix关键字争论可能需要对该RFC进行重大重写。

我在这里开玩笑。

编写RFC时可能是一回事。 人们特别想要一种新工具“可以像使用同步代码一样使用异步/等待”。 遵循语法的传统将更好地实现这一承诺。
在这里,您看到的不是未来的组合器,

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满足了这一愿望。

实际上,后缀await可能会更好地满足该需求,因为方法链非常自然(在同步Rust代码中非常常见!)

使用前缀await极大地鼓励大量临时变量,这些变量通常不是惯用的Rust风格。

但是,“它们仍然经常导致混乱的嵌套和链接回调集。”

我看到的恰好是一个闭包,它不是回调,而是在map上调用Iterator (与期货完全无关!)

请不要试图绕过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_组合器(将来的一个组合器可以用一个异步块来代替)。 这等效于在同步代码中使用迭代器组合器,因此可以在某些情况下使用它们,而不是使用循环。

在这里可以争论的是,我可以/应该在比链接更合适的地方使用前缀等待。

@Pauan显然,这不仅是在扭曲文字。 我正在显示由后缀语法支持者编写的实际代码问题。 就像我说的那样,前缀样式代码可以更好地说明您的意图,而后缀支持者抱怨的时候并不一定要有很多临时职位(至少在这种情况下)。 另外,假设您的代码有一个等待两个的线性链,我该如何调试第一个? (这是一个真实的问题,我不知道)。
其次,锈社区正在变得越来越大,来自不同背景的人(像我一样,我使用python / c / java最多)不会完全同意方法链是做事的最佳方法。 我希望在做决定时,不是(应该)只是基于最早采用者的观点。

@tajimaha

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绑定和结果上的Ok一起删除,最后的?可以直接提供结果,然后代码块也是不必要的,具体取决于根据个人口味。 由于在同一条语句中有两次等待,因此前缀根本不能很好地工作。

我不理解通常所说的让绑定在Rust代码中变得单调的观点。 它们在代码示例中非常常见且常见,尤其是在结果处理方面。 我很少在我处理的代码中看到超过2个?

另外, idiomatic是在语言的整个生命周期内发生的变化,因此我会非常小心地将其用作参数。

我不知道以前是否曾提出过类似的建议,但是前缀await关键字可以应用于整个表达式吗? 以之前提出的示例为例:

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

其中getsendjson是异步的。

对于我来说(在其他编程语言中几乎没有异步经验),后缀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怎么样?

我不理解通常所说的让绑定在Rust代码中变得单调的观点。 它们在代码示例中非常常见且常见,尤其是在结果处理方面。 我很少在我处理的代码中看到超过2个?

另外, idiomatic是在语言的整个生命周期内发生的变化,因此我会非常小心地将其用作参数。

这启发了我研究当前的Rust代码,其中一行中有两个或多个? (也许有人可以调查多行使用情况)。 我调查了xi-editor,alacritty,ripgrep,bat,xray,fd,鞭炮,紫杉,火箭,exa,铁,parity-ethereum,tikv。 这些是大多数明星的Rust项目。

我发现,在585562只有大约40行在一行中使用了两个或多个? 。 即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);

您所要做的可能只是将其包装在一个函数中,并且您的工作已完成,在此过程中甚至没有出现链接。

我发现,在585562条总行中,只有大约40条使用了两个或更多行? 在一排。

我想建议这对您没有帮助。 真正重要的是每个表达式不止一个控制流运算符。 按照典型的(rustfmt)样式,尽管它们属于同一个表达式,但它们几乎总是在文件的不同行结尾,并因此以await后缀量(理论上)来链接。

我们不应该过分地说所有事情都应该通过链接来完成

有没有人说过一切都应该通过链接完成? 到目前为止,我所看到的是,在有意义的情况下,应该_ergonomic_进行链接,这与同步代码中的情况相同。

585562总行中只有大约40行使用两个或更多? 在一排。

我不确定这与前缀vs后缀有关。 我会注意到,我的想要后缀的C#示例的_none_在一行中包含多个await s,甚至在语句中也不包含多个await s。 @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 :slightly_smiling_face:

令人震惊地类似于javascipt的promise代码,其中包含许多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),它将不符合人体工程学。 我还解决了原型制作时可能不需要链接的问题

我感到此线程的方向过于关注过多的链接,以及如何使等待代码尽可能简洁。

我想鼓励大家看看异步代码与同步变量有何不同,以及这将如何影响使用率和资源利用率。 在该主题的400条中,只有不到5条评论提到了这些差异。 如果您不知道这些差异,请获取当前的夜间版本,然后尝试编写一个不错的异步代码块。 包括尝试获取最近20篇文章中讨论的一段惯用的(非组合器)异步/等待版本的代码。

纯粹在屈服/等待点之间是否存在事物,跨等待点之间是否存在引用,以及期货周围的一些特殊要求,这往往会有所不同,因为期货的某些特殊要求使得无法编写此线程中想象的简洁代码。 例如,我们不能将任意异步函数放在任意组合器中,因为它们可能不适用于!Unpin类型。 如果我们从异步块创建期货,它们可能无法与join!select!类的组合器直接兼容,因为它们需要固定和融合类型,因此对pin_mut!.fuse()附加调用内可能需要

另外,对于使用异步块,新的基于宏的实用程序join!select!工作原理比旧的combinator变体更好。 这些都是过分的方式,在这里经常作为示例提供

在这个最简单的tokio示例中,我不知道postfix等待如何与.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()吗? 太糟糕了! 启动项目时,我们需要进行很多包装。 违反了《 The Zen of Python》中的多个设计规则。

特殊情况不足以违反规则。
如果实现难以解释,那是个坏主意。
面对模棱两可的想法,拒绝猜测的诱惑。

我们可以给用户一些直观的解释吗? 它与现有规则冲突。 await是一个字段? 或await绑定的绑定方法名为unwrap()太糟糕了! 启动项目时,我们需要进行很多包装。 违反了《 The Zen of Python》中的多个设计规则。

我会说,尽管docs.rs中有太多文档正在调用unwrap ,但在许多实际情况下,应将unwrap替换为? 。 至少,这是我的实践。

我发现,在585562条总行中,只有大约40条使用了两个或更多行? 在一排。

我想建议这对您没有帮助。 真正重要的是每个表达式不止一个控制流运算符。 按照典型的(rustfmt)样式,尽管它们属于同一个表达式,但它们几乎总是在文件的不同行结尾,并因此以await后缀量(理论上)来链接。

我确实否认这种方法会有局限性。

我再次调查了xi-editor,alacritty,ripgrep,bat,xray,fd,鞭炮,紫杉,火箭,exa,铁,平价以太坊,tikv。 这些是大多数明星的Rust项目。 这次我寻找模式:

xxx
  .f1()
  .f2()
  .f3()
  ...

以及这些表达式中是否有多个控制流运算符。

我确定7066链中只有15个具有多个控制流运算符。 就是0.2% 。 这些行跨越585562行代码中的167行。 就是0.03%

@cenwangumass感谢您抽出

一个考虑因素是,由于Rust具有与let一起进行的变量重新绑定,因此它可能会为前缀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)
}

我还没有看到足够多的人谈论Streamawait 。 尽管超出范围,但在await周围做出决策并从Stream获得一点远见可能是值得的。

我们可能需要类似以下内容:
前缀语法

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

后缀语法

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 {
    ...
}

我只想确保至少对这些示例进行了讨论,因为虽然postfix非常适合链接Future ,但乍一看对于Stream来说似乎最不直观。 我不确定这里正确的解决方案是什么。 也许postfix的awaitFuture和不同风格的awaitStream ? 但是对于不同的语法,我们将需要确保解析是明确的。

这些只是我最初的想法,从后缀的角度来看,可能值得给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 exprawait { expr }本质上是相同的。 只等待定界符而没有定界符就没有意义,尤其是因为任何表达式都可以被{}包围并继续成为同一表达式。

关于流的问题使我想到,对于Rust来说,等待不仅可以用作表达式中的运算符,还可以用作模式中的修饰符,这是很自然的事情:

// 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

“带有强制定界符的前缀等待”解决的问题是?周围的优先级问题。 使用_mandatory_分隔符,没有优先级问题。

参见try { .... }了解_like_(稳定)语法。 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

我已经在https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await中val.[await future]语法创建了Pre-RFC线程-链/ 9304

@Dowwie程序性问题应在其他地方讨论,例如https://internals.rust-lang.org

我认为我们在讨论中有两个阵营:想要强调屈服点的人和不想再强调屈服点的人。

Rust的异步,大约2018年包含以下内容:

异步/等待表示法是一种使异步编程更类似于同步编程的方法。

从上面以简单的tokio示例(将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#团队的经验(强调是我的):

这也是为什么我们不使用任何“隐式”形式表示“等待”的原因。 在实践中,这是人们想清楚考虑的事情,并且他们希望在代码中居于首位,以便他们可以关注它。 有趣的是,甚至几年后,这种趋势仍然存在。 即有时我们多年后感到后悔某件事过于冗长。 某些功能在早期就以这种方式很好,但是一旦人们对此感到满意,便会更适合于使用其他功能。 “等待”情况并非如此。 人们似乎仍然非常喜欢该关键字的重量级性质

印记字符几乎不可见,没有适当突出显示,并且对新来者不太友好(就像我之前说过的)。


但是谈论信号时, @可能不会混淆非英语发言者(例如我),因为我们不会将其视为at 。 我们有一个完全不同的名称,当您阅读代码时不会自动触发,因此您只是将其理解为一个完整的象形文字,并具有自己的含义。

@huxi我觉得Centril的帖子,以了解为什么我们应该使用单词await


另外,对于每个不喜欢后缀的人都这么等待,并使用务实的论点“好吧,现实中没有太多可以链接的代码,因此不应添加后缀”,这是一个小故事(由于记忆不佳,我可能会屠夫):

早在第二次世界大战中,其中一个战斗国家就在那里损失了许多飞机。 他们不得不以某种方式加固飞机。 因此,了解他们应该集中精力的明显方法是查看返回的飞机,看看子弹在哪里击中最多。 事实证明,在飞机中,机翼上平均有70%的孔,发动机区域中有10%,飞机其他区域中有20%。 因此,根据这些统计数据,有必要加强机翼,对吗? 错误! 原因是您只看着回来的飞机。 在这些飞机上,机翼上的子弹损坏似乎还算不错。 但是,所有返回的飞机仅对发动机区域造成了很小的损坏,这可以得出结论,对发动机区域的重大损坏是致命的。 因此,应改为加强发动机区域。

关于这个轶事,我的观点是:也许没有很多可以利用后缀等待的实际代码示例,因为没有其他语言的后缀等待。 因此,每个人都习惯于编写前缀等待代码,并且已经习惯了,但是我们永远无法知道如果我们有后缀等待,人们是否会开始链接等待更多。

因此,我认为最好的做法是利用夜间构建为我们提供的灵活性,并选择一种前缀语法和一种后缀语法添加到该语言中。 我投票支持“明显优先”前缀,并等待.await后缀。 这些不是最终的语法选择,但是我们需要选择一个开始,并且我认为与其他语法选择相比,这两个语法选择将提供新的体验。 在每晚实现它们之后,我们可以使用这两个选项获取使用情况统计信息和使用实际代码的意见,然后我们可以继续进行语法讨论,这次以更好的实用论据为后盾。

@ivandardi这部轶事非常笨拙,恕我直言不适合:这是一个旅程开始时的鼓舞人心的故事,提醒人们寻找非显而易见的事物并涵盖所有角度,在反对派反对派时,这是一个_不_讨论。 它适用于Rust 2018,在那里提出并且不受特定问题的限制。 要在辩论的另一端使用它是不礼貌的,您需要声明看到更多或更有远见的人的立场才能使其发挥作用。 我认为这不是您想要的。 另外,留在图片中,也许postfix没有使用任何语言,因为postfix从未使它成为家;)。

人们实际上已经进行了测量和观察:我们在Rust( ? )中确实有一个可链接的运算符,该运算符很少用于链接。 https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

还很好地涵盖了这仅是对当前状态的度量,因为@cenwangumass很好地将其放在此处: https

我确实想要一个故事,尽管如果postfix确实是可行的方法,那么链接将成为一种半途而废的风格。 我不相信这一点。 上面我还提到过,这里使用的主要示例( reqwest )仅需要2个等待,因为API选择了这种方式,并且不需要2个等待的便捷链式API今天就可以轻松构建。

尽管我确实希望有一个研究阶段,但我想指出等待已经严重拖延了,任何研究阶段都会使情况变得更糟。 我们也无法收集许多代码库的统计信息,我们必须自己汇编一个演讲集。 我希望在这里进行更多的用户研究,但这需要花费时间,尚不存在的设置以及需要人们运行的设置。

@Pzixel因为重新绑定,我想你可以写

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

```
让foo =等待some_future();
let bar = await {other_future()} ?. bar_async();
让bar =等待{bar} ?;

@Pzixel您有该C#团队经验报价的任何来源吗? 因为我唯一能找到的就是这一评论。 这并不是指责。 我只想阅读全文。

我的大脑将@为“ at”,因为它在电子邮件地址中使用。 在我的母语中,该符号称为“ Klammeraffe”,大致翻译为“抱抱猴子”。 我实际上很感激我的大脑适应了“ at”。

我的2美分,是一个相对较新的Rust用户(具有C ++背景,但这并没有真正的帮助)。

你们中有些人提到了新来者,这是我的想法:

  • 首先, await!( ... )宏对我来说似乎是必不可少的,因为它非常简单易用并且不能被误解。 看起来类似于println!()panic!() ,...,这毕竟是try!事情。
  • 强制定界符也很简单明了。
  • 后缀版本,无论是字段,函数还是宏,都不会很难读或写恕我直言,因为新来者只会说“好吧,这就是您的做法”。 此参数也适用于postfix关键字,该关键字“不寻常”,但“为什么不这样”。
  • 关于具有有用优先级的前缀表示法,我认为这会令人困惑。 await绑定更紧密的事实会让您大吃一惊,我认为有些用户只会喜欢在括号中加上清楚的名称(是否链接)。
  • 加糖的明显优先顺序很容易理解和教导。 然后,要引入周围的糖,只需将await?称为一个有用的关键字即可。 有用,因为它不需要繁琐的括号:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids?等待中... unwraps the future, so you have to useto unwrap the Result // but there is some sugar if you want, thanks to the等待吗?
    让响应=等待? 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并不是链接的问题。
在我看来,Chaining在前缀前缀等待和f.then(x => ...)类的组合器后面附加了大量Javascript代码,但我认为并没有失去任何可读性,而且他们似乎不需要将组合器换为后缀等待。

去做:

let ret = response.await!().json().await!().to_string();

是相同的:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

我真的没有看到在组合链之上等待后缀的好处。
我发现更容易理解第二个示例中发生的情况。

我在以下代码中没有看到任何可读性,链接或优先级问题:

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)
}

我赞成使用此前缀,因为:

  • 对其他语言的熟悉程度。
  • 与其他关键字(return,break,continue,yield等)的对齐方式。
  • 仍然感觉像Rust(像使用迭代器一样使用组合器)。

异步/等待将是该语言的一个很好的补充,在这种补充及其稳定之后,我认为更多的人会使用更多与期货相关的东西。
有些人甚至可能首次使用Rust发现异步编程。
因此,降低语言复杂性可能是有益的,而异步背后的概念可能已经很难学习。

而且我不认为同时发送前缀和后缀是一个好主意,但这只是个人观点。

@huxi是的,我已经在上面链接了它,但是我当然可以重做: https :

我的大脑将@转换为“ at”,因为它在电子邮件地址中使用了。 在我的母语中,该符号称为“ Klammeraffe”,大致翻译为“抱抱猴子”。 我实际上很感激我的大脑适应了“ at”。

我有一个类似的故事:用我的语言来说是“狗”,但它不会影响电子邮件阅读。
很高兴看到您的经历。

@llambda问题是关于链接。 当然,您可以引入其他变量。 但是无论如何,这个await { foo }?而不是await? foofoo 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!()等知名宏保持一致
  • awaitawait(...)await { ... } ,即不加糖的前缀语法
  • await?await?()await? {} ,即。 带有糖的前缀语法
  • ... await ,即后缀语法(加上... await? ,但这不是糖)
  • 所有这些组合,但不鼓励使用(请参阅我以前的文章

但实际上,我们只希望看到:

  • 不带Result的前缀: awaitawait { ... }来说明长表达式
  • Result s的前缀: await?await? {}来说明长表达式
  • 链接时的后缀: ... await... await?

+1发送await!()宏,使其稳定。 明天,如果可能的话

人们对如何使用这种模式以及在这种情况下对人体工程学的担忧引起了很多猜测。 我很感谢这些关注,但是我没有看到一个令人信服的理由,那就是这不是一个迭代式的更改。 这将允许生成实际使用量度,该量度以后可以通知优化(假设需要)。

如果采用异步Rust对于大型板条箱/项目来说比在以后进行任何更简洁/更具表现力的语法的_optional_重构工作上花费更多的精力,那么我强烈建议我们允许现在就开始采用这种工作。 持续的不确定性正在引起痛苦。

@Pzixel
(首先,我在调用函数fetch_user时出错,我将在本文中对其进行修复)

我并不是说异步/等待是没有用的,我们应该将其删除以用于组合器。
解析后,Await允许将将来的值绑定到同一范围内的变量,这确实有用,甚至单独使用组合器也不可能。
删除等待不是我的意思。
我刚刚说过,combinators可以很好地与await配合使用,并且可以通过仅等待combinators构建的整个表达式来解决链接问题(无需使用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 + ,而英语(和其他SVO语言)的书写/阅读“等待某事”比“等待某事”自然得多。 试想一下,我们将使用后缀try而不是? try关键字: let val = foo() try;

fut.await!()fut.await()没有这个问题,因为它们看起来像熟悉的阻塞调用(但是“宏”还强调了相关的“魔术”),因此它们与空格分隔的感觉会有所不同关键词。 Sigil也将以一种更加抽象的方式得到不同的理解,而不会与自然语言短语有任何直接的相似之处,因此,我认为人们阅读拟议的签名并不重要。

结论:如果决定使用该关键字,我坚信我们应该仅从前缀变体中进行选择。

@rpjohnst我不确定您的意思是什么。 actual_fun(a + b)?break (a + b)?对我很重要,因为优先顺序不同,所以我不知道await(a + b)?应该是什么。

我一直在关注这个讨论,并且有一些问题和评论。

我的第一个评论是,我相信.await!()后缀宏可以满足Centril的所有主要目标,但第一点除外:

await应该仍然是关键字,以支持将来的语言设计。”

我们将来还会看到await关键字还有哪些其他用途?


编辑:我误解了await工作的。 我克服了我的错误陈述并更新了示例。

我的第二个评论是,Rust中的await关键字与其他语言的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 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!()宏也有论点,但这是针对另一个论坛的。)

Sphericon在2019年1月29日星期二晚上11:31:32 PM -0800写道:

我一直在关注这个讨论,并且有一些问题和评论。

我的第一个评论是,我相信.await!()后缀宏可以满足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的需要)来解决链接问题。 {等待{fetch(“ test”)} .json()})。

前缀await只是一个问题,它的其他形式不存在。

我不知道如何在没有组合器且没有其他绑定或括号的情况下执行相同的操作。

为什么不创建这些绑定? 例如,对于读者来说,绑定总是更好的选择,如果您有一个调试器可以打印一个绑定值,但是却不能对表达式求值,那么它总是更好。

最后,您实际上并未删除任何绑定。 你只用labmda |user|语法,其中我做了let user = ... 。 它并没有为您节省任何费用,但是现在这段代码更难阅读,更难调试,并且无法访问父级代码(例如,您必须将错误包装在方法链的外部,而不是自己将其包装起来,大概)。


简而言之:链接本身并不能提供价值。 在某些情况下它可能很有用,但不是其中一种。 而且因为我写了六年多异步/的await的代码,我相信你不想链异步代码以往任何时候都写。 不是因为您做不到,而是因为它不方便,而且绑定方法总是更易于阅读和编写。 并不是说您根本不需要组合器。 如果您有async/await ,则不需要futures / streams上的这数百种方法,只需两个: joinselect 。 其他所有操作都可以通过迭代器/绑定/ ...(即通用语言工具)完成,而这些工具不会使您学习其他基础架构。

@Sphericon AFAIK社区同意使用await作为关键字或标记,因此,您对async想法需要另一个RFC。

我的第二个评论是,Rust中的await关键字与其他语言中的await完全不同,这有可能引起混乱和意外的竞争状况。

您能详细一点吗? 除了期货是基于民意调查的,我没有看到JS / C#等待之间的任何区别,但是与async/await无关。

关于在这里几个地方建议的await?前缀语法:
C#
让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_是一种特殊情况,但这是一种特殊情况,可能适用于await 90%以上的使用。 整个期货都是等待异步_IO_操作的一种方式,IO容易出错,因此async fn 90%+可能会返回io::Result<_> (或其他包含IO变量的错误类型)。 返回Result<Result<_, _>, _>函数当前很少见,因此我不希望它们需要特殊情况的语法。

@ Nemo157您当然是正确的:结果的结果。 更新了我的评论。

今天,我们写

  1. await!(future?) future: Result<Future<Output=T>,E> 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调用。

因此建议是:

如果exp是Result<Future<Output=T>,E> ,则await exp?可以解释为await (exp?)如果exp是Result<Future<Output=T>,E> ,则可以解释为(await exp)? Future<Output=Result<T,E>> 。 在这两种情况下,它将尽早返回错误,如果运行正常,则将解析为真实结果。

对于更复杂的情况,我们可以应用类似方法自动接收器取消引用:

>当插入await exp????我们首先检查exp ,如果是Resulttry并在结果仍然为Result直到用完?或有不是Result 。 然后它必须是一个未来,我们在上面await并应用其余的? s。

我曾经是postfix关键字/标记支持者,现在仍然如此。 但是,我只想表明前缀优先级在实践中可能不是大问题,并且有解决方法。

我知道Rust团队成员不喜欢隐式事物,但是在这种情况下,潜在的专家之间的差异太小了,我们有一个很好的方法来确保我们做正确的事。

await?是特例,但它是特例,很可能适用于await 90%以上的使用。 整个期货都是等待异步IO操作的一种方式,IO容易出错,因此async fn 90%+可能会返回io::Result<_> (或其他包含IO变量的错误类型) )。 返回Result<Result<_, _>, _>函数当前很少见,因此我不希望它们需要特殊情况的语法。

特殊情况不利于撰写,扩展或学习,最终变成行李。 在单个理论可用性用例中对语言规则进行例外并不是一个很好的妥协。

是否可以为Result<T, E> where T: Future实施Future? 这样一来,您只需await result_of_future而不用?拆开包装。 那当然会返回一个Result,所以您将其称为await result_of_future ,这意味着(await result_of_future)? 。 这样,我们将不需要await?语法,并且前缀语法会更加一致。 让我知道这有什么问题。

带有强制定界符的await其他参数包括(个人不确定我总体上最喜欢哪种语法):

  • 没有?运算符的特殊大小写,没有await?await??
  • 与现有控制流运算符(例如loopwhilefor ,它们也需要强制定界符
  • 感觉最像现存的类似Rust构造
  • 消除特殊的大小写形式有助于避免在编写宏时遇到麻烦
  • 不使用签名或后缀,避免从陌生预算中支出

例:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

但是,在编辑器中进行此操作后,感觉仍然很麻烦。 我想我宁愿有没有定界符的awaitawait? ,例如breakreturn

是否有可能实施未来成果T:未来?

您将需要逆运算。 最常见的等待是Future,其输出类型是Result。

然后有显式参数agaisnt隐藏或以其他方式吸收?进入等待状态。 如果要匹配结果,该怎么办?

如果您有Result<Future<Result<T, E2>>, E1> ,则等待它会返回Result<Result<T, E2>, E1>

如果您有Future<Result<T, E1>> ,那么等待它只需返回Result<T, E1>

没有将?隐藏或吸收到等待中,之后您可以使用Result做任何需要的事情。

哦。 那我一定误会了你。 我看不出有什么帮助,因为我们仍然需要将?与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注意,未来的形式已经消失了。 现在只有一个关联类型,即Output(可能是Result)。


@ivandardi好的。 我现在看到了。 您唯一要面对的是优先事项,因为您必须在这里学习一些奇怪的东西,因为这是一个偏差,但是我想,大多数带有await的东西都是这样。

尽管返回未来的结果是如此罕见,但我在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 )的sigil来解决深度嵌套范围的问题:

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)
}

我在这里喜欢的是@反映到函数声明的LHS上的await ,而?反映到RHS上的Result<User>函数声明。 这使得@?非常一致。


有什么想法吗?

有什么想法吗?

任何多余的花括号都是不行的

@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>

使用postfix变体,您只需要

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

@melodicstream我更新了我的文章,并为该功能添加了指向pre-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将来的from结果, then do等待to get a result, then again ?`来包装将来的结果。

以后缀的方式将是foo? await? ,在前缀中...我不确定。

因此,您的示例(尤其是最后一个示例)不起作用,因为它应该

(await foo.unwrap()).unwrap()

但是, @ huxi可能是正确的,我们正在解决可能不存在的问题。 弄清楚它的最佳方法是允许postfix宏并在采用基本async/await之后查看真实的代码库。

@Pzixel这就是为什么我提出对所有类型的Result<Future<Item=T>, E>类型实现Future的建议。 这样做将允许我说的话。

虽然我可以接受Result<Future<Output=Result<T, E1>>, E2> await foo?? Result<Future<Output=Result<T, E1>>, E2> ,但我对await foo.unwrap().unwrap()感到不满意。 在我的第一个大脑模型中,这必须是

(await foo.unwrap()).unwrap()

否则我会很困惑。 原因是?是常规运算符,而unwrap是方法。 编译器可以为.类的运算符做一些特殊的事情,但是如果它是一个普通方法,我将假定它始终仅与左侧最接近的表达式有关。

后缀语法foo.unwrap() await.unwrap()对我来说也可以,因为我知道await只是一个关键字,而不是对象,因此它必须是unwrap()之前表达式的一部分

postfix样式宏很好地解决了许多此类问题,但是仅仅是我们想保留对现有语言的熟悉程度并为其添加前缀的问题。 我会为后缀样式投票。

我对吗,以下代码不相等:

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#开发人员,我将提倡使用前缀样式,因此请做好投票的准备。

带点号( read().awaitread().await() )的Postfix语法极具误导性,并建议使用字段访问或方法调用,而await都不是其中的一种; 这是语言功能。 无论如何,我都不是经验丰富的Rustacean,但我不知道其他任何.something大小写有效地被编译器重写的关键字。 我能想到的最接近的是?后缀。

在C#中进行异步/等待已经有几年了,标准的带空格前缀( await foo() )一直没有给我带来任何问题。 在需要嵌套的await情况下,使用parens并不繁琐,parens是所有显式运算符优先级的标准语法,因此很容易理解,例如

C#
var list =(await 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还没有异步/等待,但是如果它们采用另一种方式,我会感到惊讶。

除非真的有很好的理由去用不同的方式,或者换一种说法,如果前缀和后缀掂量出同样的优势和妥协方面,我建议有很多,为内定说“标准”语法。

@markrendle我也是C#开发人员,自2013年以来一直写async/await 。我也喜欢前缀。 但是Rust差别很大。

您可能知道(await Foo()).ToList()是一种罕见的情况。 在大多数情况下,您只需编写await Foo() ,并且如果需要其他类型,则可能要修复Foo签名。

但是生锈是另一回事。 我们这里有? ,我们必须传播错误。 在C#中,异步/等待自动包装异常,将其跨等待点抛出,依此类推。 您不会生锈。 考虑每次用C#编写await时都必须写的

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?;
...

imaine除了?await ,还有另外一个变压器,我们将其命名为@ 。 如果我们为await?发明了额外的语法,则必须使await@@?await@? ,...保持一致。


我也喜欢await前缀,但是我不太确定Rust是否适合它。 在语言中添加额外的规则可能没问题,但这是一个负担,可能不值得增加可读性。

重新阅读所有评论,包括跟踪问题线程(为此花费了近3个小时)。 我的总结是:后缀@看起来是最好的解决方案,但不幸的是,它是最被人们忽视的。

关于它有很多(偏见)意见,以下是我对他们的意见:


async-await将不熟悉后缀@

实际上,只有一半的人不熟悉,因为将提供相同的async fn语法,并且仍然可以使用与await相同的功能。

Rust有很多信号,我们不应该再引入另一种信号

但是,除非能读懂并与其他语法保持一致,否则信号就可以了。 严格来说,引入新的标志意味着它会使事情变得更糟。 从一致性的角度来看,后缀@比任何前缀/后缀await更好。

?!@#类的片段让我想起了Perl

实际上,除了@?以外,我们很少会看到IMO看起来非常简单且符合人体工程学的任何其他组合。 在真实示例ITT中可以看到,返回类型(如impl Try<impl Future<T>>impl Future<impl Try<impl Try<T>>>甚至impl Future<T>最多会出现10%的用法。 此外,在这10%中的90%中,锡吉尔集团会在您必须修复API或引入临时绑定而不是为了澄清它的情况下表示代码异味。

后缀@很难注意到!

async-await上下文中,这实际上是一个很大的优势。 此语法引入了像异步代码一样表达异步代码的功能,因此更具侵入性的await仅会干扰其原始用途。 上面的示例清楚地表明,屈服点的出现可能过于密集,以至于当它们太显式时会膨胀代码。 当然,我们必须将它们放在眼前,但是@锡吉尔和await很好,而不会膨胀代码。

等待未来是昂贵的操作,我们应该明确地说

这确实是有道理的,但是我不认为我们应该如此频繁和大声地说出来。 我认为可以为类比提供突变语法: mut仅显式地需要一次,而且我们可以隐式使用可变绑定。 可以用不安全的语法提供另一个类比:显式unsafe只需要一次,而且我们可以取消引用原始指针,调用不安全的函数等。还可以使用冒泡运算符来提供另一个类比:显式impl Try返回或即将到来的try块仅需要一次,而且我们可以放心地使用?运算符。 因此,以同样的方式,显式async可能仅对持久性操作进行一次注释,并且进一步,我们可以应用@运算符来执行此操作。

这很奇怪,因为我的阅读方式与await阅读方式不同,而且我无法轻松输入

如果您将&读为“ ref”,将!读为“ not”,那么我不认为您目前如何阅读@是不使用此方法的有力论据符号。 键入速度也无关紧要,因为阅读代码始终是更重要的优先事项。 在两种情况下,采用@都很简单。

这是模棱两可的,因为@已在模式匹配中使用

我不认为这是个问题,因为在宏中已经使用了! ,在借用中已经使用了& ,在&&运算符中,已经使用了|在闭包,模式和||运算符中,也可以将+ / -应用于前缀位置,并且还可以在.中使用更多不同的环境。 在不同的语法结构中重复使用相同的符号没有什么奇怪也没有错。

grep很难

如果要检查某些源中是否包含异步模式,则rg async是更简单明了的解决方案。 如果您真的想以这种方式找到所有的屈服点,那么您将能够编写像rg "@\s*[;.?|%^&*-+)}\]]"这样的东西,虽然不那么简单,但是也不会让人感到困惑。 考虑到需要多长时间,IMO此正则表达式绝对正确。

@不适合初学者使用,很难用Google搜索

我认为对于初学者来说实际上更容易理解,因为它可以与?运算符一致地工作。 相反,前缀/后缀await与其他Rust语法不一致,另外还会带来关联/格式/含义的麻烦。 我也不认为await会增加一些自我记录的价值,因为单个关键字无法描述整个基础概念,而且无论如何初学者都可以从书中学到。 而且Rust从来不是一种能够为您提供每种可能的语法构造的纯文本提示的语言。 如果有人忘记了@符号的含义(我真的很怀疑这会是一个问题,因为还有很多协会可以正确地记住它)-那么对其进行谷歌搜索也应该会成功,因为目前rust @ symbol在模式匹配中返回有关@所有相关信息。

我们已经保留了await关键字,因此无论如何我们都应该使用它

为什么? 事实证明,如果没有保留关键字,则最终功能实现会更好,这是可以的。 同样,也没有保证会引入确切的await关键字。 这个标识符在现实世界的代码中不是很常见,因此当它保留到下一个版本时,我们将一无所获。

?是一个错误, @是第二个错误

也许您的思维方式更为词汇化,因此,您更喜欢使用单词而不是符号。 因此,在较细的语法中您看不到任何值。 但是请记住,更多的人从视觉上进行更多思考,而对他们而言,打扰性单词更难以使用。 这里大概一个很好的妥协会提供await!()宏一起,像try!()被一起使用提供? ,因此,你将能够代替使用@如果您真的对@

^^^^^^^^您也重读了我的评论吗?

我有一个很大的启示,阅读@ I60R的注释使我赞成postfix标记,并且直到现在我都不喜欢使用sigil(而宁愿等待某种形式的postfix)。

我们的等待操作不是await 。 至少不是在C#和JavaScript中使用await的方式,这是async / await的两个最大来源。

在这些语言中,启动Task (C#)/ Promise (JS)的语义是将任务立即放入要执行的任务队列中。 在C#中,这是一个多线程上下文,在JS中,它是单线程。

但是在这两种语言中, await foo语义含义是“暂存此任务并等待任务foo ,与此同时,此任务使用的计算资源可以由其他人使用。任务”。

这就是为什么在构造多个Task / Promise s(甚至在单线程执行程序上)之后使用单独的await s并行化的原因: await aren的语义不要“运行此任务”,而是“运行某些任务”,直到等待完成,我们才能继续。

创建任务时,这与懒惰或急于等待的任务完全分开,尽管它们之间有着密切的关系。 在同步工作完成后,我要对其余任务进行排队。

我们已经对懒惰迭代器和当前期货的用户产生了混淆。 我认为await语法对我们没有任何帮助。

因此,我们的await!(foo)不是“传统” C#或JS方式的await foo 。 那么哪种语言与我们的语义更紧密相关? 我现在坚信这是( @matklad请纠正我,如果我有错误的细节)Kotlin的suspend fun 。 或者像在Rust相邻的讨论中提到的那样,“显式异步,隐式等待”。

简短评论:在Kotlin中,您只能在suspend上下文中调用suspend fun 。 这将立即运行被调用的函数直至完成,并根据子函数的要求挂起堆栈上下文。 如果要并行运行子suspend fun ,则创建等效的异步闭包,然后将它们与suspend fun组合器组合以并行运行多个suspend闭包。 (比这要复杂得多。)如果要在后台运行孩子suspend fun ,则可以创建一个任务类型,将该任务放在执行程序上并公开.await() suspend fun方法将您的任务与后台任务一起备份。

这种架构虽然用与Rust的Future不同的动词描述,但听起来非常熟悉。

前面我已经解释了为什么隐式等待对Rust无效,并坚持这一立场。 我们需要async fn() -> Tfn() -> Future<T>使用率相等,以便可以使用懒惰的期货进行“做一些初始工作”模式(使终生工作在棘手的情况下)。 我们需要懒惰的期货,这样我们就可以堆叠期货,而无需在每个期货之间进行间接固定。

但是我现在相信foo@ (或其他符号)的“显式但安静”的语法是有意义的。 我不愿在这里说这个词,在这种情况下, @async的单子运算,就像?try的单子运算

有观点认为,在代码中撒上?只是为了使编译器在返回Result时不再抱怨。 从某种意义上说,您正在这样做。 但这是为了保留我们对代码局部清晰度的理想。 我需要知道此操作是通过monad解包还是直接从本地进行。

(如果我弄错了monad详细信息,我相信有人会纠正我,但我相信我的观点仍然成立。)

如果@行为相同,我认为这是一件好事。 这不是要完成一些任务,而是要完成await任务,而是要建立一个状态机,别人需要将其完成以完成任务,无论是将其构建到自己的状态机中还是将其置于执行上下文中。

TL; DR:如果您从这篇迷你博客文章中摘取一件事,那就这样:Rust的await!()不是await ,而是其他语言。 结果是相似的,但是如何处理任务的语义却大不相同。 由于缺乏通用的行为,因此我们不使用通用的语法而受益。

(在先前的讨论中提到过,该语言已从懒惰变为渴望的期货。我相信这是因为await foo暗示foo已经在发生,而我们只是在等待它产生结果。)

编辑:如果您想以更同步的方式进行讨论,请在锈开发Discord (链接为24h)或内部或用户(@ CAD97)上对我@CAD进行ping操作。 我很乐意反对一些直接的审查。

编辑2:抱歉GitHub @ cad的意外ping; 我试图逃避@并不想让您陷入困境。

@ I60R

?!@#类的片段让我想起了Perl

实际上,除了@?以外,我们很少会看到IMO看起来很简单,

直到它出现,您才知道。 如果我们还有另一个代码转换器-例如yield sigil-那将是一个真正的Perl脚本。

后缀@很难注意到!

async-await上下文中,这实际上是一个很大的优势。

难以注意到await分不是优势。

等待未来是昂贵的操作,我们应该明确地说

这确实是有道理的,但是我不认为我们应该如此频繁和大声地说出来。

请重新阅读我有关C#体验的链接。 它实际上这一重要说它这个响亮的,往往。


其他观点也很有效,但是我已经回答的是无法解决的差距。

@ CAD97作为C#开发人员,他可以说async在Rust中有何不同。.嗯,这与您所描述的没什么不同。 恕我直言,你的暗示是基于错误的前提

因此,我们的await!(foo)不是“传统” C#或JS方式的await foo

在C#开发人员头脑中,这是完全相同的事情。 当然,您应该记住,只有在投票之前才执行期货,但是在大多数情况下都没有关系。 在大多数情况下, await只是意味着“我想从未来的F获取展开的值到变量x ”。 在C#世界之后,让未来一直运转直到民意测验实际上是一种解脱,因此人们将非常高兴。 除此之外,C#与Iterators / generatos具有相似的概念,它们也很懒,因此C#人群对整个过程非常熟悉。

简而言之,捕获await工作方式要比使用%younameit%语言的await标记工作要容易得多,但效果却不尽相同。 人们不会对这种差异感到困惑,因为这并不像他们想的那样重要。 急切/懒惰的执行很重要,这种情况确实很少见,因此它不应该是主要的设计问题。

@Pzixel在C#开发人员头脑中是完全一样的。 当然,您应该记住,只有在投票之前才执行期货,但是在大多数情况下都没有关系。

急切/懒惰的执行很重要,这种情况很少发生,因此它不应该是主要的设计问题。

不幸的是,这是不对的:“先生成一些Promise,然后再生成await它们”的模式是很常见且可取的,但不适用于Rust

这是一个很大的差异,甚至使一些Rust成员感到惊讶!

Rust中的async / await确实更接近于monadic系统,它与JavaScript / C#基本上没有共同点:

  • 实现是完全不同的(构建状态机,然后在Task上执行它,这与monad一致)。

  • API完全不同(拉动与推入)。

  • 懒惰的默认行为是完全不同的(与monad一致)。

  • 只能有一个消费者,而不是很多(与monad一致)。

  • 分离错误并使用?处理错误是完全不同的(这与monad转换器是一致的)。

  • 内存模型完全不同,这对Rust期货的实施以及用户实际使用期货的方式都具有巨大影响。

唯一的共同点是所有系统都旨在简化异步代码。 而已。 这确实一点也不相同。

很多很多人都提到C#。 我们知道。

我们知道C#做了什么,我们知道JavaScript做了什么。 我们知道为什么这些语言会做出这些决定。

C#的官方团队成员与我们进行了交谈,并详细解释了为什么他们使用async / await做出那些决定。

没有人会忽略那些语言,那些数据点或那些用例。 他们被考虑在内。

但是Rust确实不同于C#或JavaScript(或任何其他语言)。 因此,我们不能盲目地复制其他语言的功能。

但是在这两种语言中,await foo在语义上都意味着“将任务停泊并等待foo任务完成

在Rust中完全相同。 异步函数的语义是相同的。 如果您调用异步fn(它会创建一个Future),则它尚未开始执行。 这确实与JS和C#世界不同。

等待它将会推动未来的完成,这与其他地方一样。

不幸的是,这是不对的:“先生成一些Promise,然后再生成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一词在这里有意义。

没有人会忽略那些语言,那些数据点或那些用例。 他们被考虑在内。

我不会一遍又一遍地重复同样的争论。 我想我已经实现了预期的目标,所以我不再这样做了。


聚苯乙烯

  • API完全不同(拉动与推入)。

仅在实施自己的期货时,它会有所不同。 但是在99(100?)%的情况下,您只使用期货的组合器pf隐藏了这种差异。

  • 内存模型完全不同,这对Rust期货的实施以及用户实际使用期货的方式都具有巨大影响。

您能详细一点吗? 实际上,在Hyper上编写简单的Web服务器时,我已经在使用异步代码,并且没有发现任何区别。 将async放在这里, await放在这里,完成

关于rust的async / await的说法与其他语言的实现方式不同,然后我们必须做一些不同的事情,这令人信服。 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. 如果愿意,他的控制力很低

NOT rust具有与C#,JS不同的一百万个实现细节隐藏不相关的差异,只向用户展示有用的差异是语言设计师的工作

此外,目前没有人抱怨await!()的原因,例如“我不知道它是状态机”,“我不知道它基于拉”。

用户不会也不会在乎这些差异。

等待它将会推动未来的完成,这与其他地方一样。

好吧,不是吗? 如果我只是在async fn写入let result = await!(future)并调用该函数,则将其放置在执行程序上并对其进行轮询之前不会发生任何事情。

@ ben0x539是的,我已经重新阅读了。。。但是目前我没有什么可添加的。

@ CAD97很高兴看到我的帖子鼓舞人心。 我可以回答有关Kotlin的问题:默认情况下,它的工作方式与其他语言相同,尽管您可以根据需要实现类似Rust的懒惰行为(例如val lazy = async(start=LAZY) { deferred_operation() }; )。 关于类似的语义:IMO在响应式编程中提供了最接近的语义,因为响应式可观察变量默认情况下是冷的(惰性)(例如val lazy = Observable.fromCallable { deferred_operation() }; )。 订阅它们的计划实际工作的方式与Rust中的await!()相同,但是还有一个很大的区别,即默认情况下订阅是非阻塞操作,异步计算的结果几乎总是在闭包中与当前控制流分开处理。 取消的工作方式也有很大的不同。 因此,我认为Rust async行为是唯一的,我完全支持您的论点,即await令人困惑,并且仅在这里使用不同的语法!

@Pzixel我很确定不会出现任何新的印记。 将yield为sigil毫无意义,因为它是控制流构造,如if / match / loop / return 。 显然,所有控制流构造都应使用关键字,因为它们描述业务逻辑并且业务逻辑始终处于优先地位。 相反, @?是异常处理构造,并且异常处理逻辑不太重要,因此它必须是微妙的。 我还没有发现任何有力的论据,认为拥有一个大的await关键字是一件好事(考虑到大量的文字,我可能会想念它们),因为它们都吸引了权威或经验(但这并不意味着我驳斥某人的权威或经验-不能完全将其应用于此处)。

@tajimaha您提供的Python示例实际上具有更多的“实现细节”。 我们可以将for视为async(int i = 0; i < n; i++)作为awaiti in range(n)作为@ ,在这里很明显Python引入了新关键字- in (在Java中实际上是sigil- : ),此外,它还引入了新的范围语法。 它可以重用(i=0, n=0; i<n; i++)类的更熟悉的东西,而不用引入很多实现细节。 但是以目前的方式,对用户体验的影响只是积极的,语法更简单,用户真的很在乎它。

@Pzixel我很确定不会出现任何新的印记。 将yield为sigil毫无意义,因为它是控制流结构,例如if / match / loop / return 。 显然,所有控制流构造都应使用关键字,因为它们描述业务逻辑并且业务逻辑始终处于优先地位。 相反, @?是异常处理构造,并且异常处理逻辑不太重要,因此它必须是微妙的。

await不是“异常处理构造”。
基于无效的前提,您的含义也是错误的。

异常处理也是一个控制流,但是没有人认为?是一件坏事。

我没有发现任何有力的论据,认为拥有一个大的await关键字是一件好事(考虑到大量的文字,我可能会想念它们),因为它们都吸引了权威或经验(但这并不意味着我解雇某人的权威或经验-不能完全在此处应用它)。

由于Rust没有自己的经验,因此只能看到其他语言对此的看法,其他团队的经验等等。 您对标记非常有信心,但是我不确定您是否真的尝试过使用它。

我不想对Rust中的语法进行争论,但是我已经看到许多后缀和前缀参数,也许我们可以两全其美。 还有其他人已经尝试用C ++提出这个建议。 这里多次提到C ++和Coroutine TS提案,但在我看来,一个名为Core Coroutines的替代提案值得更多关注。

Core Coroutines的作者建议用新的类似于运算符的令牌替换co_await
使用他们所谓的unwrap运算符语法,可以同时使用前缀和后缀表示法(无歧义):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

我认为这可能很有趣,或者至少它将使讨论更加完整。

但是Rust确实不同于C#或JavaScript(或任何其他语言)。 因此,我们不能盲目地复制其他语言的功能。

请不要以“差异”作为您偏爱非正统语法的借口。

有多少区别? 可以采用静态,动态,编译,解释的任何流行语言,包括Java,C,Python,JavaScript,C#,PHP,Ruby,Go,Swift等。 它们在功能集上有很大的不同,但是它们在语法上仍然有很多共同点。 有一会儿,您会觉得这些语言中的任何一种都像“ brainf * ck”吗?

我认为我们应该专注于提供不同但有用的功能,而不是奇怪的语法。

阅读主题后,我还认为,当有人使对链的迫切需求无效,有人使人们对过多的临时变量感到烦恼的说法无效时,辩论就已经结束了。

IMO,您已经失去了对后缀语法需求的争论。 您只能诉诸“差异”。 这是另一种绝望的尝试。

@tensorduruk我觉得您的话对其他用户来说太敌视了。 请尝试检查它们。


老实说,如果有很多人反对后缀语法,那么我们现在应该暂时确定一个前缀语法,等待看看带有前缀语法的代码是如何编写的,然后进行分析以查看编写了多少代码可以从等待后缀中受益。 这样,我们就可以使每个不适应创新的人(例如,等待postfix的人)感到安抚,同时也获得了是否等待postfix的良好实用论据。

最糟糕的情况是,如果我们做所有这些事情,并且想出一种后缀等待语法,在某种程度上比前缀等待更好。 如果人们不愿意切换到更好的等待状态,那将导致大量的代码混乱。

而且我认为所有这些语法讨论实际上都归结为链接。 如果链接不是问题,那么后缀等待将完全不在窗口之内,而仅靠前缀等待将更容易解决。 但是,链接在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

IMO,您已经失去了对后缀语法需求的争论。 您只能诉诸“差异”。 这是另一种绝望的尝试。

认真地讲,为什么不只使用您所说的语言,而不要使用此处所说的敌意?
使用您的逻辑,锈应该具有类,因为其他常规语言也具有锈。 Rust不应有借用,因为其他语言没有。

不处理链接,将是一个错失的机会。 即,如果链可以将其匿名化,我就不想为临时变量创建绑定。

但是,我觉得我们应该像夜间一样,只使用当前的宏关键字等待。 将postfix宏作为夜间功能,让人们玩弄它。 是的,一旦我们解决了,就会有些混乱,但是可以通过rustfix处理。

@tensorduruk

IMO,您已经失去了对后缀语法需求的争论。 您只能诉诸“差异”。 这是另一种绝望的尝试。

认真地讲,为什么不只使用您所说的语言,而不要使用此处所说的敌意?
使用您的逻辑,锈应该具有类,因为其他常规语言也具有锈。 Rust不应有借用,因为其他语言没有。

不处理链接,将是一个错失的机会。 即,如果链可以将其匿名化,我就不想为临时变量创建绑定。

但是,我觉得我们应该像夜间一样,只使用当前的宏关键字等待。 将postfix宏作为夜间功能,让人们玩弄它。 是的,一旦我们解决了,就会有些混乱,但是可以通过rustfix处理。

请仔细阅读。 我说过我们应该提供有用的功能,而不是奇怪的语法。 结构? 借用吗? 特征!

也请解决确实存在的问题。 人们通过示例或统计数据表明,临时绑定可能不是问题。 你们支持f await f.await曾经尝试使用证据说服对方吗?

也对我投反对票是没有用的。 为了使postfix被接受,您需要争论postfix在什么地方有用(可能不重复链式争论,这是一个死胡同;可能是一个经常引起人们烦恼的问题,有证据表明,这不是玩具问题)。

我们可以获得像C#或JS这样的语法吗? 世界上大多数开发人员都在使用它,我不喜欢rust使用新语法或不一致,Rust也很难让新人学习。

以下将是我的帖子的附录,有关在await使用后缀@ await


我们应该使用许多其他语言的经验来代替

我们实际上使用它。 但这并不意味着我们必须完全重复同样的经历。 另外,当以这种方式与@争论时,您很可能不会获得任何有用的结果,因为吸引以前的经验并不令人信服

问题的遗传学解释可能是正确的,它们可能有助于阐明该问题采用当前形式的原因,但在确定其优缺点时并没有定论。

我们应该使用await因为很多人喜欢它

那些人可能会因为许多其他原因而喜欢await ,而不仅仅是因为它是await 。 这些原因可能在Rust中也不存在。 以这种方式与@争论极有可能不会在讨论中带来任何新观点,因为对公众的吸引力并不令人信服

ad populum自变量可以是归纳逻辑中的有效自变量。 例如,对大量人口进行的民意调查可能会发现100%的消费者更喜欢某个品牌的产品。 然后可以提出有力的(强有力的)论点,即要考虑的下一个人也很可能会更喜欢该品牌(但并非总是100%,因为可能会有例外),而民意调查是该主张的有效证据。 但是,它不适合作为演绎推理作为证据的论据,例如说民意调查证明了首选品牌在成分上优于竞争对手,或者每个人都比其他品牌更喜欢该品牌。

@Pzixel

但是生锈是另一回事。 我们这里有? ,我们必须传播错误。 在C#中,异步/等待自动包装异常,将其跨等待点抛出,依此类推。 您不会生锈。 考虑每次用C#编写await时都必须写

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
}

关于繁殖,错误链接参数,如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表达式的主体是异步的? 如果是这样, match await responseawait match response之间会有差异。 因为matchawait都是有效的一元运算符,并且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没有。

关于传播错误链链接参数,例如foo().await? ,是否有任何理由不能将?添加到await前缀运算符中?

已经讨论过两次或三次,请阅读该主题。 简而言之:这是一个人工构造,如果我们除了?之外还有其他东西,它将无法很好地工作。 当await?作为前缀需要编译器中的其他支持时,使用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#的另一个用户,我会说它的前缀语法: newawait和C样式强制转换使我的直觉最大。 我强烈支持后缀运算符选项。

但是,任何语法都比显式链接期货(甚至是伪宏)更好。 我欢迎任何决议。

@orthoxerox您提出了一个很好的观点。 在我的日常工作中,我主要写Java,并且鄙视新的运算符,使我所有需要显式实例化的类(当您使用生成器模式和依赖项注入时,发生这种情况极为罕见)都具有静态工厂方法,只是我可以隐藏运算符。

@Pzixel

@markrendle我不确定您在回答什么

在C#中,您可以这样编写:

我知道我如何用C#编写。 我说:“想象一下,我们没有例外,看起来如何”。 因为Rust没有。

我猜这可能是语言障碍,因为那根本不是您所说的,但我会接受,这可能就是您的意思。

无论如何,正如@rolandsteiner所说的,重要的是我们得到了某种形式的异步/等待,所以我很高兴等待核心团队的决定,并且所有后缀支持者都可以等待核心团队的决定。 ❤❤️☮️

@yasammez来到C#。 在v8.0中,我们只使用不带类型名称的new():)

我只是为后缀运算符抛出一些想法。

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~~~~~

~有时在句子后使用以指示尾随,这很容易等待!

我不知道这个线程是否达到了峰值的荒谬性,还是我们在这里。

我认为~在某些键盘上很难回答,特别是在一些小巧精致的机械键盘上。

可能:

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与其他语言中的大多数Future并不一样,因为它不会立即将任务放在执行程序上,而是更多的是一种控制流程结构,可以将上下文转换为与monad调用链基本同构。

这让我认为,在这方面尝试将其与其他语言进行比较时,有些令人困惑。 最接近的类比实际上是Haskell中的monad do符号或Scala中的for理解力(这是我头顶上唯一熟悉的一种)。 突然,我很欣赏提出一个独特语法的考虑,但是我担心?运算符的存在会鼓励并不鼓励使用其他信号。 ?旁边的其他任何基于sigil的运算符都会使它看起来嘈杂且令人困惑,例如future@? ,但是通过具有后缀sigil运算符而设置的先例意味着另一个并不是那么可笑。

因此,我已经确信后缀符号操作符的优点。 不利的一面是,我最喜欢的标记是“永不”类型。 我宁愿选择!因为我认为future!?每当我写它时都会让我轻笑,而这对我来说最直观。 我想接下来是$ ,因为它在视觉上可以辨别future$? 。 看到~仍然让我想起Rust的早期,那时~是堆分配的前缀运算符。 不过,这都是非常个人化的,所以我不羡慕最终决策者。 如果是它们,我可能只会选择带有必需的定界符的前缀运算符。

但是,我理解其他人的建议,即Rust Future与其他语言中的大多数Future并不一样,因为它不会立即将任务放在执行程序上,而是更多的是一种控制流程结构,可以将上下文转换为与monad调用链基本同构。

我倾向于不同意。 您提到的行为不是await的属性,而是周围的async函数或范围的属性。 不是await会延迟前面代码的执行,而是包含所述代码的作用域。

奇怪地看起来@符号的问题可能在于,以前从未在这种情况下使用过它,因此,在大多数字体中,它提供的形状对我们来说非常不舒服。

然后为流行的编程字体(或至少对于Mozilla的Fira Code )提供更好的字形和一些连字可能会改善情况。

在所有其他情况下,对我而言,看起来在编写或维护代码时@不会引起任何实际问题是很奇怪的。


例如,以下代码使用与@符号不同的符号-


// 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与其他语言的大多数其他Future不同

这不是真的。 实际上就像其他语言的大多数其他期货一样。 “运行时直到第一次等待时运行”与“运行时直到第一次等待时轮询”之间的差异并不大。 我知道,因为我和他们一起工作。 如果您实际上认为“当这种差异开始发挥作用时”,您将只会发现一些极端情况。 例如,当您在第一次民意测验中创建而不是在创建民意测验时会出错。

它们在内部可能会有所不同,但是从用户角度来看它们是完全相同的,因此在这里做出区分对我来说没有任何意义。

@Pzixel

“运行时直到第一次等待时运行”与“运行时直到第一次等待时轮询”之间的差异并不大。

不过,这并不是我们要讨论的区别。 我们不是在谈论懒惰与急于等待。

我们正在谈论的是await加入了其他语言的执行器上等待已久的Promise (JS)/ Task (C#)在构建时(因此已经在“后台”运行),但是在Rust中, Future是惰性状态机,直到await!驱动。

Promise / Task是正在运行的异步操作的句柄。 Future是延迟的异步计算。 人们,包括Rust中的著名名字,以前都犯了这个错误,并且在这500多个评论的中间已经链接了示例。

我个人认为,这种语义上的不匹配足以抵消await的熟悉度。 我们的Future实现了与Promise / Task相同的目标,但是机制不同。


有趣的是,对我来说,当我第一次学习JavaScript中的async / await时, async只是我写的获得await超级能力的东西。 教我如何获得并行性的方法是a = fa(); b = fb(); /* later */ await [a, b]; (或者无论如何,这是我必须编写JS以来的一个时代)。 我的假设是,其他人对async的看法与我一致,因为Rust的语义在async上没有错配(给您await superpower),但在Future建筑和await!


在这一点上,我相信关于Rust的async / Future / await语义上的差异的讨论正在进行中,并且没有提供新的信息。 除非您有新的职位和/或新见解,否则如果我们不在此讨论,则可能是最适合的话题。 (我很乐意将它带到Internals和/或Discord。)

@ CAD97是的,我看到您的位置,但是我认为区别并不大。

你懂我,我懂你。 因此,让讨论顺其自然。

@ CAD97

人们,包括Rust中的著名名字,以前都犯了这个错误,并且在这500多个评论的中间已经链接了示例。

如果连熟悉Rust的人都犯了这个错误,那真的是错误的吗?

因此,我们在Rust All Hands进行了许多有关异步等待的讨论。 在这些讨论过程中,一些事情变得很清楚:

首先, lang团队尚未就await语法达成共识。 显然有很多可能性和有力的理由支持所有这些可能性。 我们花了很长时间探索替代方案,并产生了很多有趣的来回信息。 我认为,此讨论的下一步是将这些注释(以及该线程中的其他注释)转换为一种摘要注释,列出每种变体的情况,然后从那里继续。 我正在使用@withoutboats@cramertj

从语法问题退后一步,我们计划要做的另一件事是对实现状态进行总体分类。 当前存在许多限制(例如,该实现目前需要在引擎盖下使用TLS来线程化有关唤醒程序的信息)。 这些可能或可能不是稳定的障碍,但是无论它们是确实需要最终解决的问题,这都需要一些共同的努力(部分来自编译器团队)。 然后,下一步是进行此分类并生成报告。 我希望我们下周进行此分类,然后再进行更新。

同时,我将继续锁定该线程,直到我们有机会生成上述报告。 我觉得该线程已经达到了深入探索可能的设计空间的目的,在此阶段,进一步的评论不会特别有用。 掌握了上述报告后,我们还将制定最终决定的后续步骤。

(在最后一段中进行了扩展,我非常有兴趣探索讨论方法,而不是讨论冗长的讨论。这是一个比我在此评论中可以解决的话题要大得多的主题,因此我将不做详细介绍,但现在就足以说出我对尝试找到更好的方法来解决这一问题以及将来的语法辩论非常感兴趣。

异步等待状态报告:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

相对于我之前的评论,本文包含了分类的结果以及有关语法的一些想法(但还没有完整的语法记载)。

至少暂时将此问题标记为阻止异步等待稳定。

不久前,Niko承诺我们将在语言团队和社区中编写有关await运算符最终语法的讨论摘要。 很抱歉,我们漫长的等待。 下面是讨论情况的书面记录。 但是,在此之前,让我也提供讨论的最新情况以及我们从这里将要去的地方的最新信息。

异步等待当前位置的简要概述

首先,我们希望在1.37版本中稳定async-await,该版本在2019年7月4日分支。由于我们不想稳定await!宏,因此在此之前我们必须解决语法问题。 请注意,这种稳定并不代表路的尽头,而是更多的开始。 仍然有待完成的功能工作(例如,特征中的异步fn)以及隐式工作(持续的优化,错误修复等)。 尽管如此,稳定异步/等待仍将是一个重要的里程碑!

就语法而言,解决方案如下:

  • 首先,我们将发布到目前为止有关语法辩论的文章-请看一看。
  • 我们希望与语法的未来扩展保持向前兼容:特别是处理带有for循环的流(例如JavaScript的for await循环)。 这就是为什么我一直在撰写有关此问题的一系列文章(第一篇文章在这里,将来还会有更多)。
  • 在5月2日即将举行的lang-team会议上,我们计划讨论与for循环的交互作用,并制定一个计划,以便及时就语法达成最终决定,以将异步/等待时间稳定在1.37。 会议结束后,我们将对此内部线程发布更新。

文字

本文是一个保管箱文件,可在此处获得。 正如您将看到的,它相当长,并且反复提出了许多论点。 我们希望收到反馈。 而不是重新打开此问题(该问题已经有500多个评论),我为此目的创建了一个内部线程

正如我之前所说,我们计划在不久的将来做出最终决定。 我们还认为讨论已基本达到稳定状态:预计接下来的几周将是此语法讨论的“最终评论期”。 会议结束后,我们希望可以有更详细的时间表来分享如何做出此决定。

异步/等待语法可能是Rust自1.0以来最令人期待的功能,尤其是等待语法已成为我们获得最多反馈的决定之一。 感谢过去几个月来参与这些讨论的所有人! 这是许多人有强烈分歧的选择。 我们希望向大家保证,您的反馈会在听取,经过深思熟虑和仔细的考虑后,我们才能做出最终决定。

此页面是否有帮助?
0 / 5 - 0 等级