Rust: Resolva a sintaxe `await`

Criado em 15 jan. 2019  ·  512Comentários  ·  Fonte: rust-lang/rust

Antes de comentar neste tópico, verifique https://github.com/rust-lang/rust/issues/50547 e tente verificar se você não está duplicando argumentos que já foram feitos lá.


Notas de pastores:

Se você é novo neste tópico, considere começar em https://github.com/rust-lang/rust/issues/57640#issuecomment -456617889, que foi seguido por três ótimos comentários de resumo, o último deles https: //github.com/rust-lang/rust/issues/57640#issuecomment -457101180. (Obrigado, @traviscross!)

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

Comentários muito úteis

Achei que poderia ser útil escrever como outras linguagens lidam com uma construção de await.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Pitão

result = await task

JavaScript

let result = await task;

C ++ (corrotinas TR)

auto result = co_await task;

Hackear

$result = await task;

Dardo

var result = await task;

Com tudo isso, vamos lembrar que expressões Rust podem resultar em vários métodos encadeados. A maioria das línguas tende a não fazer isso.

Todos 512 comentários

Achei que poderia ser útil escrever como outras linguagens lidam com uma construção de await.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Pitão

result = await task

JavaScript

let result = await task;

C ++ (corrotinas TR)

auto result = co_await task;

Hackear

$result = await task;

Dardo

var result = await task;

Com tudo isso, vamos lembrar que expressões Rust podem resultar em vários métodos encadeados. A maioria das línguas tende a não fazer isso.

Com tudo isso, vamos lembrar que expressões Rust podem resultar em vários métodos encadeados. A maioria das línguas tende a não fazer isso.

Eu diria que as linguagens que suportam métodos de extensão tendem a tê-los. Isso incluiria Rust, Kotlin, C # (por exemplo, sintaxe de método LINQ e vários construtores) e F #, embora o último use intensamente o operador de pipe para o mesmo efeito.

Puramente anedótico da minha parte, mas eu regularmente executo uma dúzia de expressões encadeadas de método no código Rust em estado selvagem e ele é lido e executado bem. Eu não experimentei isso em outro lugar.

Gostaria de ver que esse problema foi referido na postagem superior de # 50547 (ao lado da caixa de seleção "Sintaxe final para espera.").

Kotlin

val result = task.await()

A sintaxe de Kotlin é:

val result = doTask()

O await é apenas suspendable function , não uma coisa de primeira classe.

Obrigado por mencionar isso. Kotlin parece mais implícito porque os futuros são ansiosos por padrão. No entanto, ainda é um padrão comum em um bloco adiado usar esse método para esperar por outros blocos adiados. Certamente já fiz isso várias vezes.

@cramertj Visto que há 276 comentários em https://github.com/rust-lang/rust/issues/50547 , você poderia resumir os argumentos apresentados ali para tornar mais fácil não repeti-los aqui? (Talvez adicioná-los ao OP aqui?)

Kotlin parece mais implícito porque os futuros são ansiosos por padrão. No entanto, ainda é um padrão comum em um bloco adiado usar esse método para esperar por outros blocos adiados. Certamente já fiz isso várias vezes.

talvez você deva adicionar ambos os casos de uso com um pouco de contexto / descrição.

Além disso, o que há com outros langs usando espera implícita, como go-lang?

Uma razão para ser a favor de uma sintaxe pós-correção é que, da perspectiva dos chamadores, um await se comporta muito como uma chamada de função: você abre mão do controle de fluxo e, quando o recebe de volta, um resultado está esperando em sua pilha. Em qualquer caso, eu preferiria uma sintaxe que abrace o comportamento semelhante a uma função, contendo parênteses de função. E há boas razões para querer dividir a construção de co-rotinas de sua primeira execução para que esse comportamento seja consistente entre blocos de sincronização e assíncronos.

Mas, embora o estilo de co-rotina implícito tenha sido debatido e eu esteja do lado da explicitação, chamar uma co-rotina não seria suficientemente explícito? Provavelmente, isso funciona melhor quando a co-rotina não é usada diretamente onde foi construída (ou pode funcionar com fluxos). Em essência, em contraste com uma chamada normal, esperamos que uma co-rotina demore mais do que o necessário em uma ordem de avaliação mais relaxada. E .await!() é mais ou menos uma tentativa de diferenciar entre chamadas normais e chamadas de co-rotina.

Então, depois de ter fornecido uma abordagem um tanto nova sobre por que a pós-correção poderia ser preferida, uma humilde proposta de sintaxe:

  • future(?)
  • ou future(await) que vem com suas próprias compensações, é claro, mas parece ser aceito como menos confuso, consulte o final da postagem.

Adaptando um exemplo bastante popular de outro thread (assumindo que logger.log também seja uma co-rotina, para mostrar o que a chamada imediata parece):

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
}

E com a alternativa:

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
}

Para evitar código ilegível e ajudar na análise, só permita espaços após o ponto de interrogação, não entre ele e o paranormal aberto. Portanto, future(? ) é bom, enquanto future( ?) não seria. Este problema não surge no caso de future(await) onde todos os tokens atuais podem ser usados ​​como anteriormente.

A interação com outros operadores pós-correção (como o atual ? -try) também é semelhante às chamadas de função:

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

Ou

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

Alguns motivos para gostar disso:

  • Comparado a .await!() , não faz alusão a um membro que poderia ter outros usos.
  • Ele segue a precedência natural de chamadas, como encadeamento e uso de ? . Isso mantém o número de classes anteriores mais baixo e ajuda no aprendizado. E as chamadas de função sempre foram um tanto especiais na linguagem (embora tenham uma característica), de modo que não há expectativa de que o código do usuário seja capaz de definir seu próprio my_await!() que tem sintaxe e efeitos muito semelhantes.
  • Isso poderia generalizar para geradores e fluxos, bem como geradores que esperam que mais argumentos sejam fornecidos na retomada. Em essência, isso se comporta como FnOnce enquanto Streams se comporta como FnMut . Argumentos adicionais também podem ser acomodados facilmente.
  • Para aqueles que usaram os Futures atuais antes, isso mostra como ? com Poll deveria ter funcionado o tempo todo (infeliz sequência de estabilização aqui). Como uma etapa de aprendizado, também é consistente com a expectativa de que um operador baseado em ? desvie o fluxo de controle. (await) por outro lado não satisfaria isso, mas afinal a função sempre esperará retomar no ponto divergente.
  • Ele usa uma sintaxe semelhante a uma função, embora este argumento só seja bom se você concordar comigo: smile:

E razões para não gostar disso:

  • ? parece ser um argumento, mas nem mesmo se aplica a uma expressão. Eu acredito que isso poderia ser resolvido através do ensino, já que o símbolo ao qual parece ser aplicado é a própria chamada de função, que é a noção um tanto correta. Isso também significa positivamente que a sintaxe não é ambígua, espero.
  • Uma mistura maior (e diferente) de parênteses e ? pode ser difícil de analisar. Especialmente quando você tem um futuro retornando o resultado de outro futuro: construct_future()(?)?(?)? . Mas você poderia usar o mesmo argumento para ser capaz de resultar em um objeto fn , levando a uma expressão como esta sendo permitida: foobar()?()?()? . Uma vez que, no entanto, nunca vi isso nem reclamar, a divisão em declarações separadas em tais casos parece raramente ser necessária. Este problema também não existe para construct_future()(await)?(await)? -
  • future(?) é minha melhor chance de uma sintaxe concisa e ainda um tanto concisa. Ainda assim, seu raciocínio é baseado em detalhes de implementação em corrotinas (retornando temporariamente e despachando na retomada), o que pode torná-lo inadequado para uma abstração. future(await) seria uma alternativa que ainda poderia ser explicada após await ter sido internalizado como uma palavra-chave, mas a posição do argumento é um pouco difícil de engolir para mim. Pode funcionar e certamente é mais legível quando a co-rotina retorna um resultado.
  • Interferência com outras propostas de chamadas de funções?
  • Seu próprio? Você não precisa gostar disso, apenas pareceu um desperdício não pelo menos propor essa sintaxe pós-correção concisa.

future(?)

Não há nada de especial em Result : Futures podem retornar qualquer tipo de Rust. Acontece que alguns futuros retornam Result

Então, como isso funcionaria para Futuros que não retornam Result ?

Parece que não ficou claro o que eu quis dizer. future(?) é o que foi discutido anteriormente como future.await!() ou similar. Ramificar em um futuro que também retorna um resultado seria future(?)? (duas maneiras diferentes de como podemos abrir mão do fluxo de controle mais cedo). Isso torna a pesquisa futura (?) e o teste de resultados ? ortogonais. Editar: adicionado um exemplo extra para isso.

Ramificar em um futuro que retornasse um resultado também seria future(?)?

Obrigado por esclarecer. Nesse caso, definitivamente não sou fã disso.

Isso significa que chamar uma função que retorna Future<Output = Result<_, _>> seria escrita como foo()(?)?

Tem uma sintaxe ? para duas finalidades completamente diferentes.

Se for especificamente a dica para o operador ? que é pesado, é possível substituí-la pela palavra-chave recém-reservada. Eu tinha apenas inicialmente considerado que isso parecia muito com um argumento real do tipo intrigante, mas a compensação poderia funcionar em termos de ajudar a analisar mentalmente a declaração. Portanto, a mesma declaração para impl Future<Output = Result<_,_>> seria:

  • foo()(await)?

O melhor argumento para que ? seja apropriado é que o mecanismo interno usado é um tanto semelhante (caso contrário, não poderíamos usar Poll nas bibliotecas atuais), mas isso pode perder o ponto de ser uma boa abstração.

É uma sintaxe muito pesada

Eu pensei que esse é todo o ponto de espera explícita?

ele usa? para dois propósitos completamente diferentes.

sim, então a sintaxe de foo()(await) seria muito melhor.

essa sintaxe é como chamar uma função que retorna um encerramento e, em seguida, chamar esse encerramento em JS.

Minha leitura de "sintaxe pesada" foi mais próxima de "sigilo pesado", ver uma sequência de ()(?)? é bastante chocante. Isso foi trazido na postagem original:

Uma mistura maior (e diferente) de parênteses e ? pode ser difícil de analisar. Especialmente quando você tem um futuro retornando o resultado de outro futuro: construct_future()(?)?(?)?

Mas você poderia usar o mesmo argumento para ser capaz de resultar em um objeto fn , levando a uma expressão como esta sendo permitida: foobar()?()?()? . Uma vez que, no entanto, nunca vi isso nem reclamar, a divisão em declarações separadas em tais casos parece raramente ser necessária.

Acho que a refutação aqui é: quantas vezes você viu -> impl Fn na natureza (quanto mais -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _> )? Quantas vezes você espera ver -> impl Future<Output = Result<_, _>> em uma base de código assíncrona? Ter que nomear um valor de retorno impl Fn raro para tornar o código mais fácil de ler é muito diferente de ter que nomear uma fração significativa de valores de retorno impl Future temporários.

Ter que nomear um valor de retorno Fn impl raro para tornar o código mais fácil de ler é muito diferente de ter que nomear uma fração significativa de valores de retorno Futuro impl temporário.

Não vejo como essa escolha de sintaxe influencia o número de vezes que você precisa nomear explicitamente o tipo de resultado. Não acho que isso não influencie a inferência de tipo diferente de await? future .

No entanto, todos vocês fizeram observações muito boas aqui e quanto mais exemplos eu inventar com isso (editei o post original para sempre conter as duas versões de sintaxe), mais me inclino para future(await) . Não é irracional digitar e ainda retém toda a clareza da sintaxe de chamada de função que isso pretendia evocar.

Quantas vezes você espera ver -> impl Futuro> em uma base de código assíncrona?

Espero ver o tipo equivalente a este (um fn assíncrono que retorna um Resultado) o tempo todo , provavelmente até mesmo a maioria de todos os fns assíncronos, pois se o que você está esperando é um IO par, você quase certamente lançará Erros de IO para cima.


Ligando para meu post anterior sobre o problema de rastreamento e adicionando mais algumas idéias.

Acho que há muito pouca chance de uma sintaxe que não inclua a cadeia de caracteres await ser aceita para esta sintaxe. Acho que neste ponto, após um ano de trabalho neste recurso, seria mais produtivo tentar pesar os prós e os contras das alternativas viáveis ​​conhecidas para tentar encontrar qual é melhor do que propor novas sintaxes. As sintaxes que considero viáveis, dadas minhas postagens anteriores:

  • O prefixo aguarda com delimitadores obrigatórios. Aqui, também é uma decisão de quais delimitadores (colchetes ou parênteses ou aceitar ambos; todos esses têm seus próprios prós e contras). Ou seja, await(future) ou await { future } . Isso resolve completamente os problemas de precedência, mas é sintaticamente ruidoso e ambas as opções de delimitador apresentam possíveis fontes de confusão.
  • O prefixo aguarda com a precedência "útil" em relação a ? . (Ou seja, que espera liga mais forte do que?). Isso pode surpreender alguns usuários que leem código, mas acredito que funções que retornam resultados futuros de resultados serão muito mais comuns do que funções que retornam resultados futuros.
  • O prefixo aguarda com a precedência "óbvia" em relação a ? . (Ou seja, isso? Liga mais forte do que esperar). Sintaxe adicional sugar await? para uma combinação de await e? operador. Acho que esse açúcar de sintaxe é necessário para tornar essa ordem de precedência viável, caso contrário, todos estarão escrevendo (await future)? o tempo todo, que é uma variante pior da primeira opção que enumerei.
  • Postfix await com o espaço de sintaxe await. Isso resolve o problema de precedência por ter uma ordem visual clara entre os dois operadores. Eu me sinto desconfortável com essa solução em muitos aspectos.

Minha própria classificação entre essas escolhas muda toda vez que examino a questão. A partir deste momento, acho que usar a precedência óbvia com o açúcar parece o melhor equilíbrio entre ergonomia, familiaridade e compreensão. Mas no passado eu favoreci qualquer uma das outras duas sintaxes de prefixo.

Para fins de discussão, darei a essas quatro opções esses nomes:

Nome Futuro | Futuro do Resultado | Resultado do Futuro
--- | --- | --- | ---
Delimitadores obrigatórios | await(future) ou await { future } | await(future)? ou await { future }? | await(future?) ou await { future? }
Precedência útil | await future | await future? | await (future?)
Precedência óbvia com açúcar | await future | await? future ou (await future)? | await future?
Palavra-chave Postfix | future await | future await? | future? await

(Eu usei especificamente "palavra-chave postfix" para distinguir esta opção de outras sintaxes postfix como "macro postfix".)

Uma das deficiências de 'abençoar' await future? em precedência útil, mas também outras que não funcionam como pós-correção, seria que os padrões usuais de conversão manual de expressões com ? podem não se aplicar mais, ou exigir que Future replique explicitamente os Result -métodos de uma forma compatível. Acho isso surpreendente. Se eles forem replicados, de repente se torna tão confuso quais dos combinadores funcionam em um futuro retornado e quais estão ansiosos. Em outras palavras, seria tão difícil decidir o que um combinador realmente faz quanto no caso de espera implícita. (Editar: na verdade, veja dois comentários abaixo, onde tenho uma perspectiva mais técnica do que quero dizer com substituição surpreendente de ? )

Um exemplo onde podemos nos recuperar de um caso de erro:

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

Esse problema não ocorre para operadores reais de pós-correção:

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

Estas são expressões diferentes, o operador ponto tem uma precedência mais alta do que o operador de "precedência óbvia" await , então o equivalente é:

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

Isso tem exatamente a mesma ambiguidade de or_recover ser assíncrono ou não. (O que eu afirmo que não importa, você sabe que a expressão como um todo é assíncrona e pode examinar a definição de or_recover se, por algum motivo, precisar saber se essa parte específica é assíncrona).

Isso tem exatamente a mesma ambiguidade de or_recover ser assíncrono ou não.

Não exatamente igual. unwrap_or_else deve produzir uma co-rotina porque é esperada, então a ambigüidade é se get_result é uma co-rotina (então um combinador é construído) ou um Result<impl Future, _> (e Ok já contém uma co-rotina e Err cria uma). Ambos não têm as mesmas preocupações de ser capaz de identificar rapidamente o ganho de eficiência movendo um ponto de sequência await para um join , que é uma das principais preocupações de implícito espera. A razão é que, em qualquer caso, esse cálculo intermediário deve ser sincronizado e deve ter sido aplicado ao tipo antes de esperar e deve ter resultado na co-rotina esperada. Há uma outra preocupação maior aqui:

Estas são expressões diferentes, o operador ponto tem uma precedência mais alta do que o operador de espera de "precedência óbvia", então o equivalente é

Isso é parte da confusão, substituir ? por uma operação de recuperação mudou a posição de await fundamentalmente. No contexto da sintaxe ? , dada uma expressão parcial expr do tipo T , espero a seguinte semântica de uma transformação (presumindo que T::unwrap_or_else exista) :

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

No entanto, em 'Precedência útil' e await expr? ( await expr produz T ), em vez disso obtemos

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

ao passo que na precedência óbvia essa transformação não se aplica mais sem parênteses extras, mas pelo menos a intuição ainda funciona para 'Resultado do Futuro'.

E o caso ainda mais interessante em que você espera em dois pontos diferentes em uma seqüência combinadora? Com qualquer sintaxe de prefixo isso, eu acho, requer parênteses. O resto da linguagem Rust tenta evitar isso ao máximo para fazer 'expressões avaliadas da esquerda para a direita' funcionarem, um exemplo disso é a mágica auto-ref.

Exemplo para mostrar que isso fica pior para cadeias mais longas com vários pontos de espera / tentativa / combinação.

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

O que eu gostaria de ver evitado, é a criação do análogo Rust da análise de tipo C onde você pula entre
lado esquerdo e direito da expressão para combinadores de 'ponteiro' e 'matriz'.

Tabela de entrada no estilo @withoutboats :

| Nome Futuro | Futuro do Resultado | Resultado do Futuro |
| - | - | - | - |
| Delimitadores obrigatórios | await(future) | await(future)? | await(future?) |
| Precedência útil | await future | await future? | await (future?) |
| Precedência óbvia | await future | await? future | await future? |
| Chamada Postfix | future(await) | future(await)? | future?(await) |

| Nome Acorrentado |
| - | - |
| Delimitadores obrigatórios | await(await(foo())?.bar())? |
| Precedência útil | await(await foo()?).bar()? |
| Precedência óbvia | await? (await? foo()).bar() |
| Chamada Postfix | foo()(await)?.bar()(await) |

Eu sou fortemente a favor de um Postfix Aguardar por vários motivos, mas eu não gosto da variante mostrada por @withoutboats , principalmente parece pelos mesmos motivos. Por exemplo. foo await.method() é confuso.

Primeiro, vamos olhar para uma tabela semelhante, mas adicionando mais algumas variantes Postfix:

| Nome Futuro | Futuro do Resultado | Resultado do Futuro |
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| Delimitadores obrigatórios | await { future } | await { future }? | await { future? } |
| Precedência útil | await future | await future? | await (future?) |
| Precedência óbvia | await future | await? future | await future? |
| Palavra-chave Postfix | future await | future await? | future? await |
| Campo Postfix | future.await | future.await? | future?.await |
| Método Postfix | future.await() | future.await()? | future?.await() |

Agora vamos olhar para uma expressão futura encadeada:

| Nome Futuros de resultados em cadeia |
| ---------------------- | -------------------------- ----------- |
| Delimitadores obrigatórios | await { await { foo() }?.bar() }? |
| Precedência útil | await (await foo()?).bar()? |
| Precedência óbvia | await? (await? foo()).bar() |
| Palavra-chave Postfix | foo() await?.bar() await? |
| Campo Postfix | foo().await?.bar().await? |
| Método Postfix | foo().await()?.bar().await()? |

E agora, para um exemplo do mundo real, de reqwests , de onde você pode querer esperar um futuro encadeado de resultados (usando meu formulário de espera preferido).

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

Na verdade, acho que todos os separadores parecem bons para sintaxe postfix, por exemplo:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
Mas não tenho uma opinião forte sobre qual usar.

A macro Postfix (por exemplo, future.await!() ) ainda pode ser uma opção? É claro, conciso e inequívoco:

| Futuro | Futuro do Resultado | Resultado do Futuro |
| --- | --- | --- |
| futuro.esperado! () | futuro.autorizar! ()? | futuro? .await! () |

Além disso, a macro postfix requer menos esforço para ser implementada e é fácil de entender e usar.

Além disso, a macro postfix requer menos esforço para ser implementada e é fácil de entender e usar.

Além disso, está apenas usando um recurso de linguagem comum (ou pelo menos se pareceria com uma macro postfix normal).

Uma macro postfix seria legal, pois combina a sucinta e encadeada do postfix com as propriedades não mágicas e a presença óbvia de macros, e se encaixaria bem com macros de usuários de terceiros, como .await_debug!() , .await_log!(WARN) ou .await_trace!()

Uma macro pós-fixada seria boa, pois combina as [...] propriedades não mágicas das macros

@novacrazy, o problema com este argumento é que qualquer macro await! _seria_ mágica, está executando uma operação que não é possível no código escrito pelo usuário (atualmente a implementação baseada no gerador subjacente está um pouco exposta, mas meu entendimento é que antes da estabilização, ele ficará completamente oculto (e interagir com ele no momento requer o uso de alguns rustc -recursos noturnos internos)).

@ Nemo157 Hmm. Eu não sabia que era para ser tão opaco.

É tarde demais para reconsiderar o uso de uma macro procedural como #[async] para fazer a transformação da função "assíncrona" para a função do gerador, em vez de uma palavra-chave mágica? São três caracteres extras para digitar e podem ser marcados nos documentos da mesma forma que #[must_use] ou #[repr(C)] .

Não estou gostando da ideia de esconder tantas camadas de abstração que controlam diretamente o fluxo de execução. Parece contrário ao que Rust é. O usuário deve ser capaz de rastrear totalmente o código e descobrir como tudo funciona e para onde vai a execução. Eles devem ser encorajados a hackear coisas e trapacear os sistemas, e se usarem Rust seguro, deve ser seguro. Isso não vai melhorar nada se perdermos o controle de baixo nível, e posso muito bem me limitar aos futuros brutos.

Acredito firmemente que Rust, a linguagem (não std / core ), deve fornecer abstrações e sintaxe apenas se forem impossíveis (ou altamente impraticáveis) de fazer pelos usuários ou std . Essa coisa toda assíncrona saiu do controle a esse respeito. Precisamos realmente de algo mais do que pin API e geradores em rustc ?

@novacrazy Geralmente concordo com o sentimento, mas não com a conclusão.

deve fornecer abstrações e sintaxe apenas se forem impossíveis (ou altamente impraticáveis) de fazer pelos usuários ou std.

Qual é a razão para ter for -loops na linguagem quando eles também poderiam ser uma macro que se transforma em loop com quebras. Qual é a razão de || closure quando poderia ser um traço dedicado e construtores de objeto. Por que introduzimos ? quando já tínhamos try!() . A razão pela qual discordo dessas perguntas e de suas conclusões é a consistência. O objetivo dessas abstrações não é apenas o comportamento que encapsulam, mas também a acessibilidade delas. for -substituição se divide em mutabilidade, caminho de código primário e legibilidade. || -a substituição se divide na verbosidade da declaração - semelhante a Futures atualmente. try!() decompõe-se na ordem esperada de expressões e composição.

Considere que async não é apenas o decorador de uma função, mas também existem outras idéias de fornecer padrões adicionais por aync-blocks e async || . Uma vez que se aplica a itens de idiomas diferentes, a usabilidade de uma macro parece subótima. Nem pensar na implementação se ela tiver que ser visível para o usuário.

O usuário deve ser capaz de rastrear totalmente o código e descobrir como tudo funciona e para onde vai a execução. Eles devem ser encorajados a hackear coisas e trapacear os sistemas, e se usarem Rust seguro, deve ser seguro.

Não acho que esse argumento se aplique porque implementar co-rotinas usando inteiramente std api provavelmente dependeria muito de unsafe . E então há o argumento inverso porque embora seja factível - e você não será interrompido mesmo se houver uma maneira sintática e semântica na linguagem de fazer isso - qualquer mudança terá um grande risco de quebrar as suposições feitas em unsafe -code. Eu defendo que o Rust não deveria dar a impressão de que está tentando oferecer uma interface padrão para a implementação de bits que não pretende estabilizar em breve, incluindo os internos dos Coroutines. Um análogo a isso seria extern "rust-call" que serve como a mágica atual para deixar claro que as chamadas de função não têm essa garantia. Podemos nunca querer realmente ter que return , mesmo que o destino das corrotinas cheias ainda esteja para ser decidido. Podemos querer conectar uma otimização mais profundamente no compilador.

À parte: Falando nisso, em teoria, uma ideia não tão séria, poderia a co-rotina esperar ser denotada como um hipotético extern "await-call" fn () -> T ? Se assim for, isso permitiria no prelúdio um

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

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

também conhecido como future.await() em itens documentados no espaço do usuário. Ou, por falar nisso, outra sintaxe de operador também poderia ser possível.

@HeroicKatora

Por que introduzimos ? quando já tínhamos try!()

Para ser justo, eu também era contra isso, embora tenha crescido em mim. Seria mais aceitável se Try algum dia fosse estabilizado, mas isso é outro tópico.

O problema com os exemplos de "açúcar" que você dá é que eles são muito, muito finos. Mesmo impl MyStruct é mais ou menos açúcar por impl <anonymous trait> for MyStruct . Esses são açúcares de qualidade de vida que adicionam zero despesas gerais.

Em contraste, geradores e funções assíncronas adicionam sobrecarga não totalmente insignificante e sobrecarga mental significativa. Os geradores, especificamente, são muito difíceis de implementar como um usuário e podem ser usados ​​de forma mais eficaz e fácil como parte da própria linguagem, enquanto o async pode ser implementado em cima disso com relativa facilidade.

O ponto sobre bloqueios ou fechamentos assíncronos é interessante, porém, e admito que uma palavra-chave seria mais útil lá, mas ainda me oponho à incapacidade de acessar itens de nível inferior, se necessário.

Idealmente, seria maravilhoso suportar a palavra - chave async e uma macro de atributo / procedural #[async] , com a primeira permitindo acesso de baixo nível ao gerador gerado (sem trocadilhos). Enquanto isso, yield deve ser permitido em blocos ou funções usando async como palavra-chave. Tenho certeza de que eles podem até compartilhar o código de implementação.

Quanto a await , se ambos os itens acima forem possíveis, poderíamos fazer algo semelhante e limitar a palavra-chave await a async funções / blocos de palavras-chave e usar algum tipo de await!() macro no #[async] funções.

Pseudo-código:

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

O melhor dos dois mundos.

Isso parece uma progressão mais natural de complexidade para apresentar ao usuário e pode ser usado de forma mais eficaz para mais aplicativos.

Lembre-se de que este problema é especificamente para discussão da sintaxe de await . Outras conversas sobre como async funções e blocos são implementados estão fora do escopo, exceto para o propósito de lembrar às pessoas que await! não é algo que você possa ou jamais será capaz de escrever no Rust linguagem superficial.

Eu gostaria de pesar especificamente os prós e os contras de todas as propostas de sintaxe pós-correção. Se uma das sintaxes se destaca com uma pequena quantidade de contras, talvez devêssemos ir em frente. Se nenhum, entretanto, seria melhor oferecer suporte a sintaxe delimitada por prefixo de sintaxe que seja compatível com uma pós-correção ainda a ser determinada, se necessário. Como o Postfix parece ser mais conciso para alguns membros, parece prático avaliá-los fortemente antes de passar para outros.

A comparação será syntax , example (o reqwest de @mehcode parece um benchmark do mundo real utilizável a este respeito), em seguida, uma tabela de ( concerns e resolution opcional await provavelmente parecerá estranha para novatos e usuários experientes, mas todos os listados atualmente a incluem.

Exemplo em uma sintaxe de prefixo apenas para referência, não use essa parte do ciclismo:

let sent = (await client.get("https://my_api").send())?;
let res: MyResponse = (await sent.json())?;
  • Palavra-chave Postfix foo() await?

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

    • | Preocupação | Resolução |

      | - | - |

      | O encadeamento sem ? pode ser confuso ou não permitido | |

  • Campo Postfix foo().await?

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

    • | Preocupação | Resolução |

      | - | - |

      | Parece um campo | |

  • Método Postfix foo().await()?

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

    • | Preocupação | Resolução |

      | - | - |

      | Parece um método ou característica | Pode ser documentado como ops:: trait? |

      | Não é uma chamada de função | |

  • Postfix chamada foo()(await)?

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

    • | Preocupação | Resolução |

      | - | - |

      | Pode ser confundido com o argumento real | palavra-chave + destaque + sem sobreposição |

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

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

    • | Preocupação | Resolução |

      | - | - |

      | Na verdade não será uma macro… | |

      | … Ou await não é mais uma palavra-chave | |

Um pensamento adicional sobre pós-correção vs. prefixo do ponto de vista de possivelmente incorporar geradores: considerando valores, yield e await ocupam dois tipos opostos de declarações. O primeiro fornece um valor de sua função para o exterior, o último aceita um valor.

Sidenote: Bem, Python tem geradores interativos onde yield pode retornar um valor. Simetricamente, as chamadas para esse gerador ou fluxo precisam de argumentos adicionais em uma configuração de tipo forte. Não vamos tentar generalizar muito, e veremos que o argumento provavelmente se transfere em qualquer um dos casos.

Então, eu argumento que não é natural que essas declarações devam ser semelhantes. Aqui, semelhante às expressões de atribuição, talvez devêssemos nos desviar de uma norma definida por outras linguagens quando essa norma é menos consistente e menos concisa para Rust. Conforme expresso em outro lugar, contanto que incluamos await e existam semelhanças com outras expressões com a mesma ordem de argumentos, não deve haver nenhum obstáculo importante para a transição até mesmo de outro modelo.

Já que implícito parece fora de questão.

Por usar async / await em outras línguas e olhar as opções aqui, nunca achei sintaticamente agradável encadear futuros.

Há uma variante não encadeada na mesa?

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

Eu meio que gosto disso, pois você poderia argumentar que é parte do padrão.

O problema de fazer esperar por uma ligação é que a história de erro não é nada agradável.

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

Precisamos ter em mente que quase todos os futuros disponíveis na comunidade para aguardar são falíveis e devem ser combinados com ? .

Se realmente precisamos do açúcar da sintaxe:

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

Ambos await e await? precisariam ser adicionados como palavras-chave ou estendemos isso para let também, ou seja, let? result = 1.divide(0);

Considerando a frequência com que o encadeamento é usado no código Rust, concordo inteiramente que é importante que o encadeamento aguarda seja o mais claro possível para o leitor. No caso da variante postfix de await:

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

Isso geralmente se comporta de forma semelhante a como eu espero que o código do Rust se comporte. Eu tenho um problema com o fato de que await() neste contexto parece apenas uma chamada de função, mas tem um comportamento mágico (semelhante a uma função não) no contexto da expressão.

A versão da macro postfix tornaria isso mais claro. As pessoas estão acostumadas com pontos de exclamação enferrujados que significam "tem magia aqui" e certamente tenho uma preferência por esta versão por esse motivo.

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

Dito isso, vale a pena considerar que temos try!(expr) na linguagem e foi nosso precursor de ? . Adicionar uma macro await!(expr) agora seria totalmente consistente com a forma como try!(expr) e ? foram introduzidos na linguagem.

Com a versão await!(expr) de await, temos a opção de migrar para uma macro postfix mais tarde ou adicionar um novo operador com o estilo ? forma que o encadeamento se torne fácil. Um exemplo semelhante a ? mas para esperar:

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

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

Acho que devemos usar await!(expr) ou await!{expr} por enquanto, pois é muito razoável e pragmático. Podemos então planejar a migração para uma versão postfix de await (ou seja, .await! ou .await!() ) mais tarde se / uma vez que macros postfix se tornarem uma coisa. (Ou eventualmente seguir o caminho de adicionar um operador de estilo ? ... depois de muito andar de bicicleta sobre o assunto: P)

Para sua informação, a sintaxe do Scala não é Await.result porque é uma chamada de bloqueio. Os Futuros de Scala são mônadas e, portanto, usam chamadas de método normais ou a compreensão de for mônada:

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

Como resultado dessa notação horrível, uma biblioteca chamada scala-async foi criada com a sintaxe que sou mais a favor, que é a seguinte:

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
}

Isso reflete fortemente como eu gostaria que o código do Rust se parecesse, com o uso de delimitadores obrigatórios e, como tal, eu gostaria de concordar com os outros em manter a sintaxe atual de await!() . Early Rust era um símbolo pesado e foi afastado por um bom motivo, eu presumo. O uso de açúcar sintático na forma de um operador postfix (ou o que quer que seja) é, como sempre, compatível com versões anteriores, e a clareza de await!(future) é inequívoca. Ele também reflete a progressão que tivemos com try! , conforme mencionado anteriormente.

Um benefício de mantê-lo como uma macro é que fica mais imediatamente óbvio que se trata de um recurso de linguagem em vez de uma chamada de função normal. Sem a adição de ! , o destaque de sintaxe do editor / visualizador seria a melhor maneira de detectar as chamadas, e acho que confiar nessas implementações é uma escolha mais fraca.

Meus dois centavos (não sou um contribuidor regular, fwiw) Tenho a maior preferência por copiar o modelo de try! . Isso já foi feito uma vez antes, funcionou bem e depois que se tornou muito popular, havia usuários suficientes para considerar um operador postfix.

Então, meu voto seria: estabilizar com await!(...) e punt em um operador postfix para um bom encadeamento baseado em uma enquete de desenvolvedores do Rust. Esperar é uma palavra-chave, mas ! indica que é algo "mágico" para mim e os parênteses não deixam nada ambíguo.

Também uma comparação:

| Postfix | Expression |
| --- | --- |
| .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()$? |

Meu terceiro centavo é que gosto de @ (para "await") e # (para representar multi-threading / simultaneidade).

Também gosto do postfix @ ! Acho que não é uma opção ruim, embora pareça haver algum sentimento de que não é viável.

  • _ @ for await_ é um mnemônico agradável e fácil de lembrar
  • ? e @ seriam muito semelhantes, portanto, aprender @ depois de aprender ? não deveria ser um salto tão grande
  • Ele ajuda a examinar uma cadeia de expressões da esquerda para a direita, sem ter que avançar para encontrar um delimitador de fechamento para entender uma expressão

Sou totalmente a favor da sintaxe await? foo e acho que é semelhante a alguma sintaxe vista em matemática, onde, por exemplo, sin² x pode ser usado para significar (sin x) ². Parece um pouco estranho no início, mas acho que é muito fácil de se acostumar.

Como disse acima, sou favorável em adicionar await!() como uma macro, assim como try!() , por agora e, eventualmente, decidir como pós-corrigir. Se pudermos manter em mente o suporte para um rustfix que converte automaticamente await!() chamadas para o postfix await que ainda está para ser decidido, melhor ainda.

A opção de palavra-chave postfix é uma clara vencedora para mim.

  • Não há problema de precedência / ordem, mas a ordem ainda pode ser explicitada com parênteses. Mas principalmente não há necessidade de aninhamento excessivo (argumento semelhante para preferir o postfix '?' Em vez de 'try ()!').

  • Parece bom com o encadeamento de várias linhas (consulte o comentário anterior de @earthengine) e, novamente, não há confusão sobre o pedido ou o que está sendo esperado. E nenhum aninhamento / parênteses extras para expressões com vários usos de await:

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;
  • Ele se presta a uma macro await! () Simples (consulte o comentário anterior de @novacrazy):
macro_rules! await {
    ($e:expr) => {{$e await}}
}
  • Mesmo de linha única, simples (sem o '?'), Postfix await keyword chaining não me incomoda porque lê da esquerda para a direita e estamos aguardando o retorno de um valor que o método subsequente opera (embora eu apenas prefira código rustfmt'ed multilinha). O espaço quebra a linha e é suficiente como um indicador visual / dica de que a espera está acontecendo:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Para sugerir outro candidato que eu ainda não vi (talvez porque não seria analisável), que tal um operador pós-fixado divertido de ponto duplo '..'? Isso me lembra que estamos esperando por algo (o resultado!) ...

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

Também gosto do postfix @ ! Acho que não é uma opção ruim, embora pareça haver algum sentimento de que não é viável.

  • _ @ for await_ é um mnemônico agradável e fácil de lembrar
  • ? e @ seriam muito semelhantes, portanto, aprender @ depois de aprender ? não deveria ser um salto tão grande
  • Ele ajuda a examinar uma cadeia de expressões da esquerda para a direita, sem ter que avançar para encontrar um delimitador de fechamento para entender uma expressão

Não sou fã de usar @ for await. É estranho digitar em um teclado de layout fin / swe, pois tenho que pressionar alt-gr com meu polegar direito e, em seguida, pressionar a tecla 2 na linha de números. Além disso, @ tem um significado bem estabelecido (at), então não vejo por que devemos confundir o significado dele.

Prefiro simplesmente digitar await , é mais rápido, pois não requer nenhuma acrobacia de teclado.

Aqui está minha própria avaliação, muito subjetiva. Também adicionei future@await , o que me parece interessante.

| sintaxe | notas |
| --- | --- |
| await { f } | Forte:

  • muito direto
  • paralela for , loop , async etc.
fraco:
  • muito prolixo (5 letras, 2 colchetes, 3 opcionais, mas provavelmente espaços linted)
  • o encadeamento resulta em muitas chaves aninhadas ( await { await { foo() }?.bar() }? )
|
| await f | Forte:
  • compara await sintaxe de Python, JS, C # e Dart
  • direto, curto
  • tanto a precedência útil quanto a precedência óbvia se comportam bem com ? ( await fut? vs. await? fut )
fraco:
  • ambíguo: precedência útil vs. óbvia deve ser aprendida
  • o encadeamento também é muito complicado ( await (await foo()?).bar()? vs. await? (await? foo()).bar() )
|
| fut.await
fut.await()
fut.await!() | Forte:
  • permite um encadeamento muito fácil
  • baixo
  • bom completamento de código
fraco:
  • engana os usuários fazendo-os pensar que é um campo / função / macro definido em algum lugar. Edit: Eu concordo com @jplatte que await!() parece menos mágico
|
| fut(await) | Forte:
  • permite um encadeamento muito fácil
  • baixo
fraco:
  • engana os usuários fazendo-os pensar que há uma variável await definida em algum lugar e que os futuros podem ser chamados como uma função
|
| f await | Forte:
  • permite um encadeamento muito fácil
  • baixo
fraco:
  • não se compara a nada na sintaxe de Rust, não é óbvio
  • meu cérebro agrupa client.get("https://my_api").send() await.unwrap().json() await.unwrap() em client.get("https://my_api").send() , await.unwrap().json() e await.unwrap() (agrupados por primeiro, depois . ) o que não é correto
  • para Haskellers: parece currying, mas não é
|
| f@ | Forte:
  • permite um encadeamento muito fácil
  • muito curto
fraco:
  • parece um pouco estranho (pelo menos no início)
  • consome @ que pode ser mais adequado para outra coisa
  • pode ser fácil de ignorar, especialmente em grandes expressões
  • usa @ de uma maneira diferente de todos os outros idiomas
|
| f@await | Forte:
  • permite um encadeamento muito fácil
  • baixo
  • bom completamento de código
  • await não precisa se tornar uma palavra-chave
  • compatível com forwards: permite que novos operadores Postfix sejam adicionados na forma @operator . Por exemplo, ? poderia ter sido feito como @try .
  • meu cérebro agrupa client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() nos grupos corretos (agrupados por . primeiro, depois @ )
fraco:
  • usa @ de uma maneira diferente de todos os outros idiomas
  • pode incentivar a adição de muitos operadores postfix desnecessários
|

Minhas pontuações:

  • familiaridade (fam): quão próxima esta sintaxe é de sintaxes conhecidas (Rust e outras, como Python, JS, C #)
  • obviedade (obv): Se você lesse isso no código de outra pessoa pela primeira vez, seria capaz de adivinhar o significado, a precedência, etc.?
  • verbosidade (vrb): quantos caracteres são necessários para escrever
  • visibilidade (vis): quão fácil é identificar (em vez de ignorar) no código
  • encadeamento (cha): como é fácil encadear com . e outros await s
  • agrupamento (grp): se meu cérebro agrupa o código nas partes corretas
  • compatibilidade com versões futuras (fwd): se isso permite ser ajustado posteriormente de uma maneira ininterrupta

| sintaxe | fam | obv | vrb | vis | cha | 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 |

Parece-me que deveríamos espelhar a sintaxe try!() no primeiro corte e obter algum uso real do uso de await!(expr) antes de introduzir alguma outra sintaxe.

No entanto, se / quando construirmos uma sintaxe alternativa ..

Eu acho que @ parece feio, "at" para "async" não parece tão intuitivo para mim, e o símbolo já é usado para correspondência de padrões.

async prefixo ? (o que será freqüentemente).

Postfix .await!() chains muito bem, parece imediatamente óbvio em seu significado, inclui o ! para me dizer que vai fazer mágica e é menos inovador sintaticamente, portanto, o "próximo corte" se aproxima. pessoalmente favoreceria este. Dito isso, para mim, resta saber o quanto isso melhoraria o código real em relação ao primeiro corte await! (expr) .

Eu prefiro o operador de prefixo para casos simples:
let result = await task;
Parece muito mais natural levando em conta que não escrevemos o tipo de resultado, então o await ajuda mentalmente ao ler da esquerda para a direita para entender que o resultado é uma tarefa com o await.
Imagine assim:
let result = somehowkindoflongtask await;
até que você não chegue ao fim da tarefa, você não percebe que o tipo que ele retorna tem que ser aguardado. Lembre-se também (embora isso esteja sujeito a alterações e não diretamente vinculado ao futuro da linguagem) que IDEs como Intellij inline o tipo (sem qualquer personalização, se isso for possível) entre o nome e os iguais.
Imagine assim:
6voler6ykj

Isso não significa que minha opinião seja cem por cento voltada para o prefixo. Eu prefiro muito a versão pós-fixada do futuro quando os resultados estão envolvidos, pois parece muito mais natural. Sem qualquer contexto, posso facilmente dizer o que significa o quê:
future await?
future? await
Em vez disso, olhe para este, qual dos dois é verdadeiro, do ponto de vista de um novato:
await future? === await (future?)
await future? === (await future)?

Sou a favor da palavra-chave de prefixo: await future .

É o usado pela maioria das linguagens de programação que possuem async / await e, portanto, é imediatamente familiar para quem conhece um deles.

Quanto à precedência de await future? , qual é o caso comum?

  • Uma função retornando um Result<Future> que deve ser aguardado.
  • Um futuro que deve ser aguardado que retorna Result : Future<Result> .

Acho que o segundo caso é muito mais comum ao lidar com cenários típicos, uma vez que as operações de E / S podem falhar. Portanto:

await future? <=> (await future)?

No primeiro caso menos comum, é aceitável ter parênteses: await (future?) . Isso poderia até ser um bom uso para a macro try! se ela não tivesse sido descontinuada: await try!(future) . Dessa forma, o await e o operador do ponto de interrogação não estão em lados diferentes do futuro.

Por que não tomar await como o primeiro async parâmetro de função?

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

Aqui future.run(await) é uma alternativa a await future .
Poderia ser apenas a função async normal que leva o futuro e simplesmente executa a macro await!() nela.

C ++ (TR de simultaneidade)

auto result = co_await task;

Isso está no TS das corrotinas, não na simultaneidade.

Outra opção poderia ser usar a palavra-chave become vez de 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 poderia ser uma palavra-chave para TCO garantido embora

Obrigado @EyeOfPython por essa [visão geral]. Isso é especialmente útil para as pessoas que estão entrando no depósito de bicicletas agora mesmo.

Pessoalmente, espero que fiquemos longe de f await , só porque é uma sintaxe nada rústica e faz com que pareça especial e mágica. Seria uma dessas coisas que os usuários novos no Rust ficarão confusos e acho que não acrescenta muita clareza, mesmo para veteranos do Rust, para valer a pena.

@novacrazy, o problema com este argumento é que qualquer macro await! _seria_ mágica, está executando uma operação que não é possível no código escrito pelo usuário

@ Nemo157 Concordo que uma macro await! seria mágica, mas eu diria que isso não é um problema. Já existem várias macros em std , por exemplo, compile_error! e não vi ninguém reclamar delas. Acho que é normal usar uma macro apenas para entender o que ela faz, não como faz.

Concordo com comentários anteriores que postfix seria o mais ergonômico, mas prefiro começar com prefix-macro await!(expr) e, potencialmente, fazer a transição para postfix-macro, uma vez que é uma coisa em vez de expr.await (campo integrado do compilador mágico) ou expr.await() (método integrado do compilador mágico). Ambos introduziriam uma sintaxe completamente nova puramente para este recurso, e IMO que apenas faz a linguagem parecer inconsistente.

@EyeOfPython Importa-se de adicionar future(await) às suas listas e tabela? Todos os aspectos positivos da sua avaliação de future.await() parecem se transferir sem a fraqueza

Já que alguns argumentaram que, apesar do destaque de sintaxe, foo.await parece muito com um acesso de campo, poderíamos mudar o token . para # e escrever foo#await . Por exemplo:

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

Para ilustrar como o GitHub renderizaria isso com destaque de sintaxe, vamos substituir await por match pois eles têm o mesmo comprimento:

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

Isso parece claro e ergonômico.

A justificativa para # vez de algum outro token não é específica, mas o token é bastante visível, o que ajuda.

Então, outro conceito: se futuro fosse referência :

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;

Isso seria muito feio e inconsistente. Deixe pelo menos remover a ambiguidade dessa sintaxe:

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;

Melhor, mas ainda feio (e ainda pior, torna o realce de sintaxe do github). No entanto, para lidar com isso, vamos introduzir a capacidade de atrasar o operador de prefixo :

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;

É exatamente isso que eu quero! Não apenas para await ( $ ), mas também para deref ( * ) e negate ( ! ).

Algumas advertências nesta sintaxe:

  1. Precedência de operador: para mim é óbvio, mas para outros usuários?
  2. Interoperabilidade das macros: o símbolo $ causaria problemas aqui?
  3. Inconsistência de expressão: o operador de prefixo à esquerda aplica-se a toda a expressão, enquanto o operador de prefixo . ( await_chain fn demonstra isso), é confuso?
  4. Análise e implementação: essa sintaxe é válida?

@Centril
IMO # está muito próximo da sintaxe literal bruta r#"A String with "quotes""#

IMO # está muito próximo da sintaxe literal bruta r#"A String with "quotes""#

Parece bastante claro a partir do contexto qual é a diferença neste caso.

@Centril
Sim, mas a sintaxe também é muito estranha ao estilo que Rust usa IMHO. Não se assemelha a nenhuma sintaxe existente com uma função semelhante.

@Laaas Nem ? quando foi introduzido. Eu ficaria feliz em ir com .await ; mas outros parecem insatisfeitos com isso, então estou tentando encontrar algo que funcione (ou seja, seja claro, ergonômico, suficientemente fácil de digitar, encadeado, tenha boa precedência) e foo#await parece satisfazer tudo isso.

? tem vários outros pontos positivos, enquanto #await parece bastante arbitrário. Em qualquer caso, se você quiser algo como .await , por que não .await! ?

@Centril Essa era a principal razão por trás de future(await) , para evitar o acesso ao campo sem ter que adicionar nenhum operador extra ou sintaxe estrangeira.

enquanto #await parece bastante arbitrário.

# é arbitrário, sim - isso é tudo ou nada? Quando uma nova sintaxe é inventada em algum ponto, ela deve ser arbitrária porque alguém achou que parecia boa ou fazia sentido.

por que não .await! ?

Isso é igualmente arbitrário, mas não tenho nenhuma oposição a isso.

@Centril Essa era a principal razão por trás de future(await) , para evitar o acesso ao campo sem ter que adicionar nenhum operador extra ou sintaxe estrangeira.

Em vez disso, parece um aplicativo de função em que você está passando await para future ; isso me parece mais confuso do que "acesso de campo".

Em vez disso, parece um aplicativo de função de onde você está passando o Wait para o futuro; isso me parece mais confuso do que "acesso de campo".

Para mim, isso fazia parte da concisão. Await é em muitos aspectos como uma chamada de função (callee é executado entre você e seu resultado), e passar uma palavra-chave para essa função deixou claro que se trata de um tipo diferente de chamada. Mas vejo como pode ser confuso que a palavra-chave seja semelhante a qualquer outro argumento. (Menos destaque de sintaxe e mensagens de erro do compilador tentando reforçar essa mensagem. Futuros não podem ser chamados de outra forma senão sendo esperados e vice-versa).

@Centril

Ser arbitrário não é uma coisa negativa , mas ter semelhança com uma sintaxe existente é uma coisa positiva. .await! não é tão arbitrário, pois não é uma sintaxe inteiramente nova; afinal, é apenas uma macro pós-correção chamada await .

Para esclarecer / adicionar ao ponto sobre a nova sintaxe especificamente para await , o que quero dizer com isso é a introdução de uma nova sintaxe que é diferente da sintaxe existente. Existem muitas palavras-chave de prefixo, algumas das quais foram adicionadas após Rust 1.0 (por exemplo, union ), mas AFAIK não uma única palavra-chave postfix (não importa se separada por um espaço, um ponto ou outra coisa) . Eu também não consigo pensar em nenhuma outra linguagem que tenha palavras-chave Postfix.

IMHO, a única sintaxe pós-fixada que não aumenta significativamente a estranheza do Rust é a macro pós-fixada, porque ela espelha chamadas de método, mas pode ser claramente identificada como uma macro por qualquer pessoa que já tenha visto uma macro Rust antes.

Também gostei da macro await!() padrão. É muito claro e simples.
Eu prefiro um acesso semelhante a um campo (ou qualquer outro postfix) para coexistir como awaited .

  • Await sente que você estará aguardando o que vem a seguir / certo. Esperado, ao que veio antes / partiu.
  • Coerente com o método cloned() em iteradores.

E também gostei de @? como ? como açúcar.
Isso é pessoal, mas na verdade eu prefiro a combinação &? , porque @ tende a ser muito alto e ultrapassar a linha, o que causa muita distração. & também é alto (bom), mas não é inferior (também é bom). Infelizmente & já tem um significado, embora sempre seja seguido por ? .. Eu acho?

Por exemplo.

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

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

Para mim, o @ parece um personagem inchado, desvia o fluxo de leitura. Por outro lado, &? é agradável, não distrai (minha) leitura e tem um bom espaço superior entre & e ? .

Pessoalmente, acho que uma macro await! simples deve ser usada. Se macros pós-correção entrarem na linguagem, então a macro poderia simplesmente ser expandida, não?

@Laaas Por mais que eu gostaria que houvesse, ainda não há macros pós-fixadas no Rust, então elas são uma nova sintaxe. Observe também que foo.await!.bar e foo.await!().bar não têm a mesma sintaxe de superfície. No último caso, haveria um postfix real e uma macro embutida (o que implica desistir de await como palavra-chave).

@jplatte

Existem muitas palavras-chave de prefixo, algumas das quais foram adicionadas após Rust 1.0 (por exemplo, union ),

union não é um operador de expressão unário, portanto, é irrelevante nesta comparação. Existem exatamente 3 operadores de prefixo unário estáveis ​​em Rust ( return , break e continue ) e todos eles são digitados em ! .

Hmm ... Com @ minha proposta anterior parece um pouco melhor:

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 Esse é o meu ponto, uma vez que ainda não temos macros pós-correção, deveria ser simplesmente await!() . Também quis dizer .await!() FYI. Não acho que await precise ser uma palavra-chave, embora você possa reservá-la se achar que é problemática.

union não é um operador de expressão unário, portanto, é irrelevante nesta comparação. Existem exatamente 3 operadores de prefixo unário estáveis ​​em Rust ( return , break e continue ) e todos eles são digitados em ! .

Isso ainda significa que a variante prefixo-palavra-chave se encaixa no grupo de operadores de expressão unária, mesmo se digitada de forma diferente. A variante da palavra-chave postfix é uma divergência muito maior da sintaxe existente, uma que é muito grande em relação à importância de await na linguagem na minha opinião.

Com relação ao prefixo macro e possibilidade de transição para pós-correção: await precisa permanecer uma palavra-chave para que isso funcione. A macro seria então algum item de linguagem especial onde é permitido usar uma palavra-chave como o nome sem um r# extra, e r#await!() poderia, mas provavelmente não deveria, invocar a mesma macro. Fora isso, parece a solução mais pragmática para torná-lo disponível.

Isto é, mantenha await como uma palavra-chave, mas faça await! resolver para uma macro lang-item.

@HeroicKatora por que ela precisa permanecer como uma palavra-chave para funcionar?

@Laaas Porque se quisermos manter a possibilidade de transição aberta, precisamos permanecer abertos para uma futura sintaxe pós-correção que a use como uma palavra-chave. Portanto, precisamos manter a reserva de palavra-chave para que não precisemos de uma pausa de edição para a transição.

@Centril

Observe também que foo.await! .Bar e foo.await! (). Bar não são a mesma sintaxe de superfície. No último caso, haveria um postfix real e uma macro embutida (o que implica desistir de await como palavra-chave).

Isso não poderia ser resolvido fazendo a palavra-chave await combinada com ! resolver para uma macro interna (que não pode ser definida por meios normais)? Em seguida, ele permanece uma palavra-chave, mas resolve para uma macro na sintaxe de macro.

@HeroicKatora Por que await em x.await!() seria uma palavra-chave reservada?

Não seria, mas se mantivermos a pós-correção sem solução, não precisa ser a solução a que chegaremos em discussões posteriores. Se essa fosse a melhor possibilidade única acordada, então deveríamos adotar essa sintaxe exata pós-correção em primeiro lugar.

Esta é outra vez que encontramos algo que funciona muito melhor como um operador Postfix. O grande exemplo disso é try! qual eventualmente demos seu próprio símbolo ? . No entanto, acho que esta não é a última vez em que um operador Postfix é mais ideal e não podemos dar a tudo seu próprio caráter especial. Portanto, acho que não devemos pelo menos começar com @ . Seria muito melhor se tivéssemos uma maneira de fazer esse tipo de coisas. É por isso que apoio o estilo de macro Postfix .await!() .

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

Mas para que isso faça sentido, as próprias macros pós-fixadas teriam que ser introduzidas. Portanto, acho que seria melhor começar com uma sintaxe de macro await!(foo) normal. Poderíamos posteriormente expandir isso para foo.await!() ou até foo@ se realmente acharmos que isso é importante o suficiente para justificar seu próprio símbolo.
Que essa macro precise de um pouco de mágica não é novidade para std e para mim não é um grande problema.
Como @jplatte colocou:

@ Nemo157 Concordo que uma macro await! seria mágica, mas eu diria que isso não é um problema. Já existem várias macros em std , por exemplo, compile_error! e não vi ninguém reclamar delas. Acho que é normal usar uma macro apenas para entender o que ela faz, não como faz.

Um problema recorrente que vejo discutido aqui é sobre o encadeamento e como usar
aguardar em expressões de encadeamento. Talvez possa haver outra solução?

Se não quisermos usar o await para encadeamento e apenas usar o await para
atribuições, poderíamos ter algo como apenas substituir let por await:
await foo = future

Então, para o encadeamento, poderíamos imaginar algum tipo de operação como await res = fut1 -> fut2 ou await res = fut1 >>= fut2 .

O único caso que falta é esperar e retornar o resultado, algum atalho
por await res = fut; res .
Isso poderia ser feito facilmente com um simples await fut

Eu não acho que tenha visto outra proposta como esta (pelo menos para o
encadeamento), deixando isso aqui, pois acho que seria bom usar.

@HeroicKatora Eu adicionei fut(await) à lista e classifiquei de acordo com minha opinião.

Se alguém achar que minha pontuação está errada, por favor, me diga!

Em termos de sintaxe, acho que .await!() é limpo e permite o encadeamento, mas não adiciona estranhezas à sintaxe.

No entanto, se alguma vez obtivermos macros Postfix "reais", será um pouco estranho, porque presumivelmente .r#await!() poderia ser sombreado, enquanto .await!() não poderia.

Eu sou fortemente contra a opção de "palavra-chave postfix" (separada por um espaço como: foo() await?.bar() await? ), porque acho o fato de que await está unido à parte seguinte da expressão, e não a parte em que opera. Eu preferiria praticamente qualquer símbolo diferente de espaço em branco aqui, e até mesmo prefiro sintaxe de prefixo sobre isso, apesar de suas desvantagens com cadeias longas.

Eu acho que "precedência óbvia" (com o await? sugar) é claramente a melhor opção de prefixo, já que delimitadores obrigatórios são uma dor para o caso muito comum de esperar uma única instrução, e "precedência útil" não é intuitivo e, portanto, confuso.

Em termos de sintaxe, acho que .await!() é limpo e permite o encadeamento, mas não adiciona estranhezas à sintaxe.

No entanto, se alguma vez obtivermos macros Postfix "reais", será um pouco estranho, porque presumivelmente .r#await!() poderia ser sombreado, enquanto .await!() não poderia.

Se obtivermos macros postfix e usarmos .await!() , podemos cancelar a reserva de await como uma palavra-chave e apenas torná-la uma macro postfix. A implementação dessa macro ainda exigiria um pouco de magia, mas seria uma macro real, exatamente como compiler_error! é hoje.

@EyeOfPython Você poderia explicar em detalhes quais mudanças você considera em forward-compatibility ? Não tenho certeza de que maneira fut@await teria uma classificação superior a fut.await!() e await { future } inferior a await!(future) . A coluna verbosity também parece um pouco estranha, algumas expressões são curtas, mas têm avaliação inferior, ela considera declarações encadeadas, etc. Todo o resto parece equilibrado e como a avaliação extensiva mais bem condensada até agora.

Depois de ler esta discussão, parece que muitas pessoas querem adicionar uma macro await!() normal e descobrir a versão postfix mais tarde. Isso pressupõe que realmente queremos uma versão pós-fixada, o que considerarei verdadeiro no restante deste comentário.

Portanto, gostaria apenas de pesquisar a opinião de todos aqui: Devemos todos concordar com a sintaxe await!(future) POR AGORA? Os prós são que não há ambigüidade de análise com essa sintaxe, e também é uma macro, portanto, nenhuma alteração da sintaxe da linguagem para oferecer suporte a essa alteração. Os contras são que ficará feio para o encadeamento, mas isso não importa, pois essa sintaxe pode ser facilmente substituída por uma versão postfix automaticamente.

Depois de estabelecermos isso e implementá-lo na linguagem, podemos continuar a discussão sobre a sintaxe do postfix await com experiências, ideias e possivelmente outros recursos do compilador mais maduros.

@HeroicKatora Eu await para ser usado como identificador comum naturalmente e permitiria que outros operadores postfix fossem adicionados, enquanto eu acho que fut.await!() seria melhor se await foi reservado. No entanto, não tenho certeza se isso é razoável, também parece definitivamente válido que fut.await!() possa ser mais alto.

Para await { future } vs await!(future) , o último mantém a opção aberta para mudar para quase qualquer uma das outras opções, enquanto o primeiro apenas permite qualquer uma das variantes await future ( conforme descrito na entrada do blog @withoutboats ). Portanto, acho que definitivamente deveria ser fwd(await { future }) < fwd(await!(future)) .

Quanto à verbosidade, você está correto, após dividir os casos em subgrupos não reavaliei a verbosidade, que deve ser a mais objetiva de todas.

Vou editá-lo para levar em consideração seus comentários, obrigado!

Estabilizar await!(future) é a pior opção que posso imaginar:

  1. Isso significa que temos que cancelar a reserva de await que significa que o design da linguagem futura ficará mais difícil.
  2. Ele está conscientemente tomando o mesmo caminho de try!(result) que suspendemos (e que requer a gravação de r#try!(result) no Rust 2018).

    • Se soubermos que await!(future) é uma sintaxe incorreta que pretendemos descontinuar, isso estará criando deliberadamente uma dívida técnica.

    • Além disso, try!(..) é definido em Rust, enquanto await!(future) não pode ser e, em vez disso, seria a mágica do compilador.

    • Reprovar try!(..) não foi fácil e teve um impacto social. Passar por aquela provação novamente não parece atraente.

  3. Isso usaria sintaxe macro para uma parte central e importante da linguagem; isso não parece claramente de primeira classe.
  4. await!(future) é barulhento ; ao contrário de await future você precisa escrever !( ... ) .
  5. APIs Rust, e especialmente a biblioteca padrão, são centradas em torno da sintaxe de chamada de método. Por exemplo, é comum encadear métodos ao lidar com Iterator s. Semelhante a await { future } e await future , a sintaxe await!(future) tornará o encadeamento de métodos difícil e induzirá ligações temporárias de let . Isso é ruim para a ergonomia e, na minha opinião, para a legibilidade.

Eu gostaria de concordar com @Centril, mas há algumas questões em aberto. Tem certeza sobre 1. ? Se o tornássemos 'mágico', não poderíamos torná-lo ainda mais mágico permitindo que isso se referisse a uma macro sem descartá-la como uma palavra-chave?

Entrei para Rust tarde demais para avaliar a perspectiva social de 2. . Repetir um erro não parece atraente, mas como alguns códigos ainda não foram convertidos de try! , deve ficar evidente que foi uma solução. Isso levanta a questão de saber se pretendemos ter async/await como um incentivo para migrar para a edição 2018 ou, se preferir, seja paciente e não repita isso.

Dois outros recursos centrais (imho) muito semelhantes a 3. : vec![] e format! / println! . O primeiro porque não existe uma construção estável em caixa, o último devido à construção da string de formato e por não ter expressões digitadas de forma dependente. Acho que essas comparações também colocam parcialmente 4. em outra perspectiva.

Eu me oponho a qualquer sintaxe que não seja parecida com o inglês. Ou seja, "await x" significa algo como inglês. "x # !! @! &" não. "x.await" tem uma leitura tentadora como o inglês, mas não será quando x for uma linha não trivial, como uma chamada de função de membro com nomes longos ou um monte de métodos iteradores encadeados etc.

Mais especificamente, eu apoio a "palavra-chave x", onde a palavra-chave é provavelmente await . Eu vim de usar as co-rotinas C ++ TS e as co-rotinas c # da unidade, que usam uma sintaxe muito semelhante a essa. E depois de anos usando-os no código de produção, minha crença é que saber onde estão seus pontos de rendimento, de relance, é absolutamente crítico . Quando você desliza para baixo na linha de recuo da sua função, você pode selecionar cada co_await / yield return em uma função de 200 linhas em questão de segundos, sem carga cognitiva.

O mesmo não é verdade para o operador ponto com await depois, ou alguma outra sintaxe pós-fixada de "pilha de símbolos".

Eu acredito que await é uma operação de fluxo de controle fundamental. Deve receber o mesmo nível de respeito que 'if , while , match e return . Imagine se qualquer um desses fossem operadores postfix - ler o código do Rust seria um pesadelo. Como com meu argumento para await, já que você pode deslizar rapidamente a linha de indentação de qualquer função Rust e selecionar imediatamente todo o fluxo de controle. Existem exceções, mas são exceções, e não é algo pelo qual devemos nos esforçar.

Eu concordo com @ejmahler. Não devemos esquecer do outro lado do desenvolvimento - a revisão do código. Arquivos com código-fonte são lidos com muito mais frequência do que escritos, portanto, acho que deveria ser mais fácil de ler e entender do que escrever. Encontrar os pontos de rendimento é muito importante na revisão do código. E eu pessoalmente votaria em Useful precedence .
Eu acredito que isso:

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

é mais fácil de entender do que isso:

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

@HeroicKatora

Eu gostaria de concordar com @Centril, mas há algumas questões em aberto. Tem certeza sobre 1. ? Se o tornássemos 'mágico', não poderíamos torná-lo ainda mais mágico permitindo que isso se referisse a uma macro sem descartá-la como uma palavra-chave?

Tecnicamente? Possivelmente. No entanto, deve haver uma forte justificativa para casos especiais e, neste caso, ter magia sobre magia não parece justificado.

Isso levanta a questão de saber se pretendemos ter async/await como um incentivo para migrar para a edição 2018 ou, se preferir, seja paciente e não repita isso.

Não sei se já dissemos que pretendíamos que o novo sistema de módulo, async / await , e try { .. } fossem incentivos; mas, independentemente de nossa intenção, eles são, e acho que isso é uma coisa boa. Queremos que as pessoas comecem a usar novos recursos de linguagem para escrever bibliotecas melhores e mais idiomáticas.

Dois outros recursos centrais (imho) muito semelhantes a 3. : vec![] e format! / println! . O primeiro porque não há um afaik de construção em caixa estável,

O primeiro existe e é escrito vec![1, 2, 3, ..] , para imitar expressões literais de array, por exemplo, [1, 2, 3, ..] .

@ejmahler

"x.await" tem uma leitura tentadora como o inglês, mas não será quando x for uma linha não trivial, como uma chamada de função de membro com nomes longos ou um monte de métodos iteradores encadeados etc.

O que há de errado com vários métodos iteradores encadeados? Isso é Rust claramente idiomático.
A ferramenta rustfmt também formatará cadeias de métodos em linhas diferentes para que você obtenha (novamente usando match para mostrar o realce de sintaxe):

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

Se você leu .await como "então espere", a leitura é perfeita, pelo menos para mim.

E depois de anos usando-os no código de produção, minha crença é que saber onde estão seus pontos de rendimento, de relance, é absolutamente crítico .

Não vejo como o postfix await nega isso, especialmente na formatação rustfmt acima. Além disso, você pode escrever:

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

se você gosta disso.

em uma função de 200 linhas em questão de segundos, sem carga cognitiva.

Sem saber muito sobre a função particular, parece-me que 200 LOC provavelmente viola o princípio de responsabilidade única e faz muito. A solução é fazer menos e dividir. Na verdade, acho que é o mais importante para a manutenção e a legibilidade.

Eu acredito que await é uma operação fundamental de controle de fluxo.

E também ? . Na verdade, await e ? são ambas operações de fluxo de controle eficazes que dizem "extrair valor fora do contexto". Em outras palavras, no contexto local, você pode imaginar esses operadores tendo o tipo await : impl Future<Output = T> -> T e ? : impl Try<Ok = T> -> T .

Existem exceções, mas são exceções, e não é algo pelo qual devemos nos esforçar.

E a exceção aqui é ? ?

@andreytkachenko

Eu concordo com @ejmahler. Não devemos esquecer do outro lado do desenvolvimento - a revisão do código. Arquivos com código-fonte são lidos com muito mais frequência do que escritos, portanto, acho que deveria ser mais fácil de ler e entender do que escrever.

A divergência é em torno do que seria melhor para legibilidade e ergonomia.

é mais fácil de entender do que isso:

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

Não é assim que seria formatado; executá-lo em rustfmt permite que você:

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

@ejmahler @andreytkachenko Eu concordo com o @Centril aqui, a maior mudança (alguns podem dizer melhoria, eu não) que você ganha com a sintaxe do prefixo é que os usuários são incentivados a dividir suas declarações em várias linhas porque todo o resto é ilegível. Isso não é Rust-y e as regras de formatação usuais compensam isso na sintaxe pós-correção. Eu também considero o ponto de rendimento mais obscuro na sintaxe do prefixo porque await não é realmente colocado no ponto de código onde você produz, em vez de se opor a ele.

Se você seguir este caminho, vamos experimentar para soletrar, em vez de await como um substituto para let no espírito da ideia de

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

Mas para nenhum desses eu vejo benefício suficiente para explicar sua perda de composibilidade e complicações na gramática.

Hm ... é desejável ter uma forma de prefixo e sufixo de await? Ou apenas a forma de sufixo?

Encadear chamadas de método é idiomático especificamente quando se fala sobre
iteradores e, em muito menos opções, mas, fora isso, a ferrugem é
uma linguagem imperativa primeiro e uma linguagem funcional depois.

Não estou argumentando que um postfix é literalmente incompreensível, estou fazendo um
argumento de carga cognitiva que esconde uma operação de fluxo de controle de nível superior,
carregando a mesma importância que 'retornar', aumenta a carga cognitiva quando
em comparação a colocá-lo o mais próximo possível do início da linha - e estou
fazendo esse argumento com base em anos de experiência em produção.

No sábado, 19 de janeiro de 2019 às 11h59 Mazdak Farrokhzad [email protected]
escrevi:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril Eu gostaria de concordar, mas existem
algumas questões abertas. Você tem certeza sobre 1.? Se fizermos isso, 'mágica' poderia
não tornamos isso ainda mais mágico ao permitir que isso se refira a uma macro sem
descartando-o como uma palavra-chave?

Tecnicamente? Possivelmente. No entanto, deve haver uma forte justificativa para
casos especiais e, neste caso, ter magia sobre magia não parece
justificado.

Isso levanta a questão de saber se pretendemos ter assíncrono / aguardar como
um incentivo para migrar para a edição 2018 ou melhor, seja paciente e não
repita isso.

Não sei se alguma vez dissemos que pretendíamos um novo sistema de módulo, async /
espere, e tente {..} ser incentivos; mas independentemente da nossa intenção
eles são, e eu acho que isso é uma coisa boa. Queremos que as pessoas eventualmente
comece a usar novos recursos de linguagem para escrever melhor e mais idiomático
bibliotecas.

Dois outros recursos centrais (imho) muito parecidos com 3 .: vec! [] E formato! /
println !. O primeiro muito porque não há estábulo encaixotado
afaik de construção,

O primeiro existe e é escrito vec! [1, 2, 3, ..], para imitar a matriz
expressões literais, por exemplo [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" tem uma leitura tentadoramente semelhante ao inglês, mas não será quando x for um
linha não trivial, como uma chamada de função de membro com nomes longos ou um monte
de métodos iteradores encadeados, etc.

O que há de errado com vários métodos iteradores encadeados? Isso é distintamente
Rust idiomática.
A ferramenta rustfmt também formatará cadeias de métodos em linhas diferentes para que você
get (novamente usando match para mostrar o realce de sintaxe):

deixe foo = alpha (). corresponder? // ou alpha() match? , alpha()#match? , alpha().match!()?
.beta
.some_other_stuff (). match?
.even_more_stuff (). match
.stuff_and_stuff ();

Se você ler .await como "then Wait", é perfeito, pelo menos para mim.

E depois de anos usando-os no código de produção, minha convicção é que saberonde seus pontos de rendimento estão, à primeira vista, é absolutamente crítico .

Não vejo como o postfix await nega isso, especialmente no rustfmt
formatação acima. Além disso, você pode escrever:

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

se você gosta disso.

em uma função de 200 linhas em questão de segundos, sem carga cognitiva.

Sem saber muito sobre a função particular, parece-me
que 200 LOC provavelmente viola o princípio de responsabilidade única e faz
demais. A solução é fazer menos e dividir. Na verdade, eu
acho que é a coisa mais importante para manutenção e legibilidade.

Acredito que esperar é uma operação fundamental de controle de fluxo.

Então é? Na verdade, espera e? são ambas operações de controle de fluxo eficazes
que dizem "extrair valor fora do contexto". Em outras palavras, no local
contexto, você pode imaginar esses operadores tendo o tipo await: impl
Futuro-> T e? : impl Try-> T.

Existem exceções, mas são exceções e não devemos
se esforçar.

E a exceção aqui é? ?

@andreytkachenko https://github.com/andreytkachenko

Eu concordo com @ejmahler https://github.com/ejmahler . Nós não deveriamos
esqueça o outro lado do desenvolvimento - revisão de código. Arquivo com código-fonte é
sendo lido com muito mais frequência do que escrito, portanto, acho que deveria ser mais fácil
leia-e-entenda então para escrever.

A discordância é sobre o que seria melhor para legibilidade e
ergonomia.

é mais fácil de entender do que isso:

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

Não é assim que seria formatado; a formatação idiomática rustfmt
é:

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

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@ejmahler Discordamos re. "se escondendo"; os mesmos argumentos foram feitos errados.

O operador ? é muito baixo e foi criticado no passado por "esconder" um retorno. Na maioria das vezes, o código é lido, não escrito. Renomeá-lo para ?! o tornará duas vezes mais longo e, portanto, mais difícil de ignorar.

No entanto, finalmente estabilizamos ? e, desde então, acho que a profecia não se concretizou.
Para mim, o postfix await segue a ordem natural de leitura (pelo menos para falantes da esquerda para a direita). Em particular, segue a ordem do fluxo de dados.

Para não mencionar o realce de sintaxe: qualquer coisa relacionada à espera pode ser realçada com uma cor brilhante, para que possam ser encontrados rapidamente. Portanto, mesmo se tivéssemos um símbolo em vez da palavra await real, ele ainda seria muito legível e localizável no código destacado pela sintaxe. Dito isso, eu ainda prefiro usar a palavra await apenas por motivos de grep - é mais fácil fazer o grep do código para qualquer coisa que esteja sendo aguardada se usarmos apenas a palavra await vez de um símbolo como @ ou #, cujo significado depende da gramática.

vocês, isso não é ciência de foguetes

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

O postfix ... é extremamente legível, difícil de perder à primeira vista e super intuitivo, já que você o lê naturalmente como o código que vai perdendo o controle enquanto espera que o resultado do futuro esteja disponível. sem necessidade de precedência / macro shenanigans e nenhum ruído de linha extra de sigilos desconhecidos, uma vez que todo mundo já viu elipses antes.

(desculpas a @solson)

@ ben0x539 Isso significa que posso acessar um membro do meu resultado como future()....start ? Ou aguardar um resultado de intervalo como range()..... ? E como exatamente você quer dizer no precedence/macro shenanigans necessary and já que atualmente as reticências .. exigem ser um operador binário ou parêntese à direita e isso é terrivelmente próximo à primeira vista.

Sim o ? Operador existe. Eu já reconheci que havia
exceções. Mas é uma exceção. A grande maioria do fluxo de controle em qualquer
O programa Rust acontece por meio de palavras-chave de prefixo.

Sábado, 19 de janeiro de 2019 às 13h51 Benjamin Herr [email protected]
escrevi:

vocês, isso não é ciência de foguetes

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

Postfix ... é extremamente legível, difícil de perder à primeira vista e super
intuitivo, já que você o lê naturalmente como um tipo de código
enquanto espera que o resultado do futuro esteja disponível. não
precedência / travessuras macro necessárias e nenhum ruído de linha extra de
sigilos desconhecidos, já que todo mundo já viu elipses antes.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora Isso parece um pouco artificial, mas com certeza. Eu quis dizer que, como é uma operação postfix, como as outras soluções postfix sugeridas, evita a necessidade de precedência contra-intuitiva para await x? e não é uma macro.

@ejmahler

Sim o ? Operador existe. Já reconheci que havia exceções. Mas é uma exceção.

Existem duas formas de expressão que cabem em keyword expr , nomeadamente return expr e break expr . O primeiro é mais comum do que o último. A forma continue 'label realmente não conta porque, embora seja uma expressão, ela não tem a forma keyword expr . Então agora você tem 2 formas de expressão unária de palavra-chave com prefixo inteiro e 1 forma de expressão unária pós-fixada. Antes mesmo de levarmos em conta que ? e await são mais semelhantes do que await e return são, eu dificilmente chamaria return/break expr uma regra para ? ser uma exceção contra.

A grande maioria do fluxo de controle em qualquer programa Rust acontece por meio de palavras-chave de prefixo.

Como mencionado anteriormente, break expr não é tão comum ( break; é mais típico e return; são mais típicos e não são formas de expressão unárias). O que resta são os primeiros return expr; s e não me parece nada claro que isso é muito mais comum do que match , ? , apenas aninhado if let else s, e for loops. Assim que try { .. } estabilizar, espero que ? seja usado ainda mais.

@ ben0x539 Acho que devemos reservar ... para genéricos variados, assim que estivermos prontos para recebê- los

Eu realmente gosto da ideia de inovar com a sintaxe postfix aqui. Faz muito mais sentido com o fluxo e me lembro de como o código ficou muito melhor quando passamos do prefixo try! para o pós-fixado ? . Acho que muitas pessoas experimentaram na comunidade Rust quantas melhorias o código fez.

Se não gostamos da ideia de .await , tenho certeza de que alguma criatividade pode ser feita para encontrar um operador Postfix real. Um exemplo poderia ser apenas usar ++ ou @ para esperar.

:( Eu só não quero esperar mais.

Todos se sentem confortáveis ​​com a sintaxe macro, a maioria das pessoas neste tópico que começam com outras opiniões parecem preferir a sintaxe macro.

Claro que será uma “macro mágica”, mas os usuários raramente se preocupam com a aparência da expansão da macro e, para aqueles que se preocupam, é fácil explicar a nuance nos documentos.

A sintaxe regular da macro é como uma torta de maçã, é a segunda opção favorita de todos, mas, como resultado, a opção favorita da família [0]. É importante ressaltar que como com try! sempre podemos mudar isso mais tarde. Mas o mais importante, quanto mais cedo concordarmos, mais cedo poderemos começar a usá-lo e ser produtivos!

[0] (Referenciado no primeiro minuto) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

match, if, if let, while, while let e for são todos fluxos de controle generalizados
que usam prefixos. Fingir pausa e continuar são o único fluxo de controle
palavras-chave são frustrantemente enganosas.

No sábado, 19 de janeiro de 2019 às 15:37 Yazad Daruvala [email protected]
escrevi:

:( Eu só não quero esperar mais.

Todos estão confortáveis ​​com a sintaxe macro, a maioria das pessoas neste tópico que
começar com outras opiniões parece acabar favorecendo a sintaxe macro.

Claro, será uma "macro mágica", mas os usuários raramente se preocupam com o que a macro
expansão parece e para aqueles que o fazem, é fácil explicar o
nuance nos documentos.

A sintaxe regular da macro é como uma torta de maçã, é a segunda de todos
opção favorita, mas, como resultado, a opção favorita da família [0].
É importante ressaltar que como com try! sempre podemos mudar isso mais tarde. Mas a maioria
importante, quanto mais cedo concordarmos, mais cedo todos podemos começar
realmente usá-lo e ser produtivo!

[0] (referenciado no primeiro minuto)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mitsuhiko eu concordo! Postfix parece mais rústico devido ao encadeamento. Acho que a sintaxe fut@await que propus é outra opção interessante que não parece ter tantas desvantagens quanto outras propostas. Não tenho certeza se é muito longe e uma versão mais realista seria preferível.

@ejmahler

match, if, if let, while, while let e for são todos fluxos de controle generalizados que usam prefixos. Fingir quebras e continuar são as únicas palavras-chave do fluxo de controle é frustrantemente enganoso.

Não é enganoso de forma alguma. A gramática relevante para essas construções é aproximadamente:

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 ","?;

Aqui, os formulários são if/while expr block , for pat in expr block e match expr { pat0 => expr0, .., patn => exprn } . Existe uma palavra-chave que precede o que se segue em todas essas formas. Eu acho que é isso que você quer dizer com "usa prefixos". No entanto, todas essas são formas de bloco e não operadores de prefixo unário. A comparação com await expr é, portanto, enganosa, pois não há consistência ou regra para falar. Se você está buscando consistência com formas de bloco, então compare isso com await block , não await expr .

@mitsuhiko eu concordo! Postfix parece mais rústico devido ao encadeamento.

A ferrugem é dualista. Suporta abordagens imperativas e funcionais. E eu acho que está tudo bem, porque em casos diferentes, cada um deles pode ser mais adequado.

Eu não sei. Parece que seria ótimo ter os dois:

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

Tendo usado o Scala por um tempo, eu também gostava muito de muitas coisas que funcionavam assim. Especialmente match e if seria um bom ter em posições postfix em Rust.

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

Resumo até agora

Matrizes de opções:

Matriz de resumo de opções (usando @ como sigilo, mas pode ser principalmente qualquer coisa):

| Nome Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
| --- | --- | --- | --- |
| PREFIX | - | - | - |
| Macro de palavras-chave | await!(fut) | await!(fut)? | await!(fut?) |
| Função de palavra-chave | await(fut) | await(fut)? | await(fut?) |
| Precedência útil | await fut | await fut? | await (fut?) |
| Precedência óbvia | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn com palavra-chave | fut(await) | fut(await)? | fut?(await) |
| Palavra-chave Campo | fut.await | fut.await? | fut?.await |
| Método de palavra-chave | fut.await() | fut.await()? | fut?.await() |
| Macro de palavras-chave do Postfix | fut.await!() | fut.await!()? | fut?.await!() |
| Palavra-chave do espaço | fut await | fut await? | fut? await |
| Sigil Palavra-chave | fut@await | fut@await? | fut?@await |
| Sigil | fut@ | fut@? | fut?@ |

O sigilo de "Sigil Keyword" _não pode ser # , pois então você não poderia fazê-lo com um futuro chamado r . ... porque o sigilo não teria que mudar a tokenização como minha primeira preocupação .

Mais uso da vida real (envie-me por PM outros casos de uso _real_ com vários await no urlo e irei adicioná-los):

| Nome (reqwest) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json | > ? |
| --- | --- |
| PREFIX | - |
| Macro de palavras-chave | await!(client.get("url").send())?.json()? |
| Função de palavra-chave | await(client.get("url").send())?.json()? |
| Precedência útil | (await client.get("url").send()?).json()? |
| Precedência óbvia | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn com palavra-chave | client.get("url").send()(await)?.json()? |
| Palavra-chave Campo | client.get("url").send().await?.json()? |
| Método de palavra-chave | client.get("url").send().await()?.json()? |
| Macro de palavras-chave do Postfix | client.get("url").send().await!()?.json()? |
| Palavra-chave do espaço | client.get("url").send() await?.json()? |
| Sigil Palavra-chave | client.get("url").send()@await?.json()? |
| Sigil | client.get("url").send()@?.json()? |

NOTA DE EDIÇÃO: foi apontado para mim que pode fazer sentido para Response::json retornar também Future , onde send aguarda o pedido de veiculação de saída e json (ou outra interpretação do resultado) aguarda o IO de entrada. No entanto, vou deixar este exemplo como está, pois acho significativo mostrar que o problema de encadeamento se aplica mesmo com apenas um ponto de espera de E / S na expressão.

Parece haver um consenso aproximado de que, das opções de prefixo, a precedência óbvia (junto com o açúcar await? ) é a mais desejável. No entanto, muitas pessoas se manifestaram a favor de uma solução postfix, a fim de tornar o encadeamento como acima mais fácil. Embora a escolha do prefixo tenha um consenso aproximado, parece não haver consenso sobre qual solução pós-fixada é a melhor. Todas as opções propostas levam a uma confusão fácil (atenuada pelo destaque de palavras-chave):

  • Fn com palavra-chave => chamando um fn com um argumento chamado await
  • Campo de palavra-chave => acesso de campo
  • Método de palavra-chave => chamada de método
  • Macro (prefixo ou postfix) => await uma palavra-chave ou não?
  • Palavra-chave de espaço => quebra o agrupamento em uma linha (melhor em várias linhas?)
  • Sigil => adiciona novos sigilos a uma linguagem que já é percebida como cheia de sigilos

Outras sugestões mais drásticas:

  • Permita ambos prefixo (precedência óbvia) e pós-fixo "campo" (isso poderia ser aplicado a mais palavras-chave como match , if , etc. no futuro para tornar este um padrão generalizado, mas é desnecessário adendo a este debate) [[referência] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await em padrões para resolver futuros (sem encadeamento) [[referência] (https://github.com/rust-lang/rust/issues/57640)]
  • Use um operador de prefixo, mas permita atrasá-lo [[referência] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

Estabilizar com uma macro de palavra-chave await!(fut) é, obviamente, compatível com o futuro basicamente com todos os itens acima, embora isso exija que a macro use uma palavra-chave em vez de um identificador regular.

Se alguém tiver um exemplo predominantemente real que usa dois await em uma cadeia, eu adoraria vê-lo; ninguém compartilhou um até agora. No entanto, o postfix await também é útil mesmo se você não precisar await mais de uma vez em uma cadeia, como mostrado no exemplo reqwest.

Se perdi algo notável acima deste comentário resumido, envie-me por PM no urlo e tentarei adicioná-lo. (Embora exija que seja adicionado os comentários de outra pessoa para evitar favoritismo de voz alta.)

Pessoalmente, sempre fui a favor de palavras-chave de prefixo com precedência óbvia. Ainda acho que estabilizar com uma macro de palavra-chave await!(fut) seria útil para coletar informações do mundo real sobre onde a espera acontece em casos de uso do mundo real e ainda nos permitiria adicionar um prefixo não macro ou opção de pós-correção posteriormente .

No entanto, no processo de redação do resumo acima, comecei a gostar de "Campo de palavra-chave". A "palavra-chave do espaço" é agradável quando dividida em várias linhas:

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

mas em uma linha, faz uma quebra estranha que agrupa mal a expressão: client.get("url").send() await?.json()? . No entanto, com o campo de palavra-chave, parece bom nas duas formas: client.get("url").send().await?.json()?

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

embora eu suponha que um "método de palavra-chave" fluiria melhor, pois é uma ação. Nós _poderíamos_ até torná-lo um método "real" em Future se quiséssemos:

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

( extern "rust-await" certamente implicaria em toda a magia necessária para realmente fazer o await e não seria realmente um fn real, apenas estaria lá porque a sintaxe se parece com um método, se uma palavra-chave método é usado.)

Permitir prefixo (precedência óbvia) e pós-fixo "campo" ...

Se qualquer sintaxe pós-fixada for selecionada (não importa se junto ou em vez do prefixo), definitivamente seria um argumento para uma discussão futura: agora temos palavras-chave que funcionam tanto em notação prefixada quanto pós-fixada, precisamente porque às vezes uma é preferível a o outro, então talvez pudéssemos permitir ambos onde faz sentido e aumentar a flexibilidade da sintaxe, enquanto unificamos as regras. Talvez seja uma má ideia, talvez seja rejeitada, mas é definitivamente uma discussão a ser feita no futuro, se a notação Postix for usada para await .

Acho que há muito pouca chance de uma sintaxe que não inclua a string de caracteres await ser aceita para esta sintaxe.

: +1:


Um pensamento aleatório que tive depois de ver um monte de exemplos aqui ( como @mehcode 's ): Uma das reclamações de que me lembro sobre .await é que é muito difícil de ver †, mas considerando que as coisas esperadas são normalmente falível, o fato de que geralmente é .await? ajuda a chamar atenção extra para ele de qualquer maneira.

† Se você estiver usando algo que não destaca palavras-chave


@ejmahler

Eu me oponho a qualquer sintaxe que não seja parecida com o inglês

Algo como request.get().await lido tão bem quanto body.lines().collect() . Em "um monte de métodos iteradores encadeados", acho que _prefixo_ na verdade é pior, já que você tem que lembrar que eles disseram "espere" bem no início, e nunca saberá quando ouvir algo se será o que você esperando, meio que como uma frase no caminho do jardim .

E depois de anos usando-os no código de produção, minha convicção é que saber onde estão seus pontos de rendimento, à primeira vista, é absolutamente crítico. Quando você desliza para baixo a linha de indentação da sua função, você pode escolher cada retorno de co_await / yield em uma função de 200 linhas em questão de segundos, sem carga cognitiva.

Isso implica que nunca há nenhum dentro de uma expressão, o que é uma restrição que eu absolutamente não apoiaria, dada a natureza orientada por expressão de Rust. E pelo menos com await , é absolutamente plausível ter CallSomething(argument, await whatever.Foo() .

Dado que async _will_ aparecem no meio das expressões, não entendo por que é mais fácil ver no prefixo do que no pós-fixado.

Deve receber o mesmo nível de respeito de 'se, enquanto, corresponder e retornar. Imagine se qualquer um desses fossem operadores postfix - ler o código do Rust seria um pesadelo.

return (e continue e break ) e while são notáveis ​​como _completamente_ inúteis para encadear, já que sempre retornam ! e () . E embora por algum motivo você tenha omitido for , vimos código escrito muito bem usando .for_each() sem efeitos negativos, particularmente em rayon .

Provavelmente precisamos aceitar o fato de que async/await será um recurso importante da linguagem. Ele aparecerá em todos os tipos de código. Ele vai invadir o ecossistema - em alguns lugares, será tão comum quanto ? . As pessoas terão que aprender.

Conseqüentemente, podemos querer focalizar conscientemente em como a escolha da sintaxe parecerá depois de usá-la por um longo tempo, em vez de como será a princípio.

Também precisamos entender que, no que diz respeito às construções de fluxo de controle, await é um tipo diferente de animal. Construções como return , break , continue e até yield podem ser entendidas intuitivamente em termos de jmp . Quando os vemos, nossos olhos saltam pela tela porque o fluxo de controle com o qual nos importamos está se movendo para outro lugar. No entanto, embora await afete o fluxo de controle da máquina, ele não move o fluxo de controle que é importante para nossos olhos e para nossa compreensão intuitiva do código.

Não somos tentados a encadear chamadas incondicionais para return ou break porque isso não faria sentido. Por motivos semelhantes, não somos tentados a alterar nossas regras de precedência para acomodá-las. Esses operadores têm baixa precedência. Eles levam tudo à direita e o devolvem em algum lugar, encerrando a execução dentro daquela função ou bloco. O operador await , entretanto, deseja ser encadeado. É parte integrante de uma expressão, não o fim dela.

Tendo considerado a discussão e os exemplos neste tópico, fico com a sensação corrosiva de que viveríamos para nos arrepender das surpreendentes regras de precedência.

O candidato a stalking horse parece estar indo com await!(expr) por enquanto e esperando que algo melhor seja resolvido mais tarde. Antes de ler os comentários de @Centril , eu provavelmente teria apoiado isso no interesse de obter esse recurso importante com quase qualquer sintaxe. No entanto, seus argumentos me convencem de que isso seria apenas uma fuga. Sabemos que o encadeamento de chamadas de método é importante no Rust. Isso levou à adoção da palavra-chave ? , que é amplamente popular e extremamente bem-sucedida. Usar uma sintaxe que sabemos que nos desapontará é, na verdade, apenas adicionar uma dívida técnica.

No início deste tópico, @withoutboats indicou que apenas quatro opções existentes parecem viáveis. Destes, apenas a sintaxe pós-fixada expr await provavelmente nos fará felizes a longo prazo. Essa sintaxe não cria surpresas estranhas de precedência. Isso não nos força a criar uma versão de prefixo do operador ? . Ele funciona bem com encadeamento de métodos e não interrompe o fluxo de controle da esquerda para a direita. Nosso operador ? sucesso serve como precedente para um operador postfix, e await é mais parecido com ? na prática do que com return , break ou yield . Embora um operador postfix sem símbolo possa ser novo no Rust, o uso de async/await será amplamente difundido para torná-lo rapidamente familiar.

Embora todas as opções para uma sintaxe postfix pareçam viáveis, expr await tem algumas vantagens. Essa sintaxe deixa claro que await é uma palavra-chave, o que ajuda a enfatizar o fluxo de controle mágico. Comparado com expr.await , expr.await() expr.await! , expr.await!() , etc., isso evita ter que explicar que se parece com um campo / método / macro, mas realmente não está neste caso especial. Todos nós nos acostumaríamos com o separador de espaço aqui.

Soletrar await como @ ou usar algum outro símbolo que não cause problemas de análise é atraente. Certamente é um operador importante o suficiente para justificá-lo. Mas se, no final, tiver que ser escrito await , tudo bem. Contanto que esteja na posição postfix.

Como alguém mencionou exemplos _real_ ... Eu mantenho uma (de acordo com tokei) 23.858 base de código de ferrugem de linha que é altamente assíncrona e usa futuros 0.1 await (altamente experimental, eu sei). Vamos explorar (redigido) espeleologia (observe que tudo foi executado no 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)
}

Agora vamos transformar isso na variante de prefixo mais popular, precedência óbvia com açúcar. Por razões óbvias, isso não foi executado em rustfmt, então, desculpe se houver uma maneira melhor de escrevê-lo.

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

Finalmente, vamos transformar isso em minha variante postfix favorita, "campo 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)
}

Após este exercício, descobri várias coisas.

  • Agora sou fortemente contra await? foo . Ele tem uma boa leitura para expressões simples, mas ? parece perdido em expressões complexas. Se devemos usar prefixo, prefiro ter precedência "útil".

  • O uso de notação pós-fixada me leva a juntar instruções e reduzir ligações let desnecessárias.

  • Usar a notação postfix _field_ me leva a fortemente preferir .await? para aparecer na linha da coisa que está esperando ao invés de em sua própria linha na linguagem rustfmt.

Agradeço a notação pós-fixada de reticências "..." acima, tanto por sua concisão quanto simbolicamente na língua inglesa, representando uma pausa em antecipação a outra coisa. (Assim como funciona o comportamento assíncrono!), Ele também se encadeia bem.

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

Duvido que qualquer outra opção seja tão concisa e visualmente significativa.

UMA

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

B

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

Deve mesmo ser considerado uma boa forma ter longas cadeias de espera?

Por que não usar simplesmente combinadores Future regulares?

Na verdade, algumas expressões não se traduzem bem em cadeias de espera se você quiser ter comportamentos de backup em caso de falha.

Pessoalmente, penso o seguinte:

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

value.unwrap()

lê muito mais naturalmente do que isso:

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

Afinal, ainda temos todo o poder dos futuros a custo zero em nossas mãos.

Considere isso.

@EyeOfPython Eu gostaria de @ em future@await . Podemos escrever future~await , onde ~ funciona como um semi-hífen, e funcionaria para qualquer operador posfixo possível.

O hífen - já foi usado como o operador menos e operador negativo. Não é mais bom. Mas ~ foi usado para indicar objetos heap no Rust e, de outra forma, dificilmente foi usado em qualquer linguagem de programação. Deve causar menos confusão para pessoas de outras línguas.

@earthengine Boa ideia, mas talvez use apenas future~ que significa aguardar um futuro, onde ~ é trabalhado como a palavra-chave await . (como o símbolo ?

Futuro | Futuro do Resultado | Resultado do Futuro
- | - | -
futuro ~ | futuro ~? | futuro? ~

Também futuros acorrentados como:

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

Dei uma olhada em como Go implementa a programação assíncrona e descobri algo interessante lá. A alternativa mais próxima aos futuros em Go são os canais. E em vez de await ou outra sintaxe gritante para esperar por valores, os canais Go fornecem apenas o operador <- para esse propósito. Para mim, parece muito claro e direto. Já vi muitas vezes como as pessoas elogiam Go por sua sintaxe simples e bons recursos assíncronos, então é definitivamente uma boa ideia aprender algo com sua experiência.

Infelizmente, não poderíamos ter exatamente a mesma sintaxe porque há muito mais colchetes angulares no código-fonte do Rust do que no Go, principalmente por causa dos genéricos. Isso torna o operador <- realmente sutil e desagradável de se trabalhar. Outra desvantagem é que ele pode ser visto como oposto a -> na assinatura de função e não há razão para considerá-lo assim. E ainda outra desvantagem é que <- sigilo foi planejado para ser implementado como placement new , então as pessoas poderiam interpretá-lo mal.

Então, depois de alguns experimentos com a sintaxe, parei em <-- sigil:

let output = <-- future;

No contexto async <-- é bastante simples, embora seja inferior a <- . Mas, em vez disso, fornece uma grande vantagem sobre <- , bem como sobre o prefixo await - funciona bem com indentação.

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

Para mim, esse operador parece ainda mais exclusivo e fácil de detectar do que await (que se parece mais com qualquer outra palavra-chave ou variável no contexto do código). Infelizmente, no Github parece mais fino do que no meu editor de código, mas acho que não é fatal e podemos viver com isso. Mesmo que alguém se sinta desconfortável, diferentes realces de sintaxe ou fontes melhores (especialmente com ligaduras) resolverão todos os problemas.

Outro motivo para gostar dessa sintaxe é porque ela poderia ser expressa conceitualmente como "algo que ainda não está aqui". A direção da direita para a esquerda da seta é oposta à direção como lemos o texto, o que nos permite descrevê-lo como "coisa que vem do futuro". A forma longa do operador <-- também sugere que "alguma operação durável começa". O colchete angular e dois hifens direcionados a future podem simbolizar "votação contínua". E ainda podemos ler como "aguardar" como era antes.
Não é algum tipo de iluminação, mas pode ser divertido.


O mais importante nesta proposta é que o encadeamento de métodos ergonômicos também seria possível. A ideia de operador de prefixo atrasado que propus anteriormente se encaixa bem aqui. Desta forma, teríamos o melhor de ambos os mundos da sintaxe de prefixo e pós-fixado await . Eu realmente espero que também sejam introduzidos alguns extras úteis que eu pessoalmente desejei em muitas ocasiões antes: desreferenciação retardada e sintaxe de negação retardada .

Completo, não tenho certeza se palavra atrasada é adequada aqui, talvez devêssemos nomeá-la de forma diferente.

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

A precedência do operador parece bastante óbvia: da esquerda para a direita.

Espero que esta sintaxe reduza a necessidade de escrever is_not_* funções cujo objetivo é apenas negar e retornar uma propriedade bool . E expressões booleanas / desreferenciadas em alguns casos serão mais limpas ao usá-las.


Finalmente, apliquei em exemplos do mundo real postados por @mehcode e gosto de como <-- dá ênfase adequada à função async dentro das cadeias de chamada de método. Ao contrário, o postfix await apenas se parece com acesso de campo regular ou chamada de função (dependendo da sintaxe) e é quase impossível distingui-los sem um destaque especial de sintaxe ou formatação.

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

Afinal: essa é a sintaxe que desejo usar.

@novacrazy

Deve mesmo ser considerado uma boa forma ter longas cadeias de espera? Por que não usar simplesmente combinadores Future regulares?

Não tenho certeza se não entendi você mal, mas esses não são exclusivos, você ainda pode usar combinadores também

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

Mas para que isso funcione, a cláusula and_then precisa ser digitada em FnOnce(T) -> impl Future<_> , não apenas em FnOnce(T) -> U . Fazer combinadores encadeados no futuro e o resultado só funcionará perfeitamente sem parênteses no postfix:

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

Nesta postagem, vou me concentrar na questão da precedência do

  • Chamada de método ( future.await() )
  • Expressão de campo ( future.await )
  • Chamada de função ( future(await) )

As diferenças de funcionalidade são bastante pequenas, mas existentes. Observe que eu aceitaria tudo isso, isso é principalmente ajuste fino. Para mostrá-los todos, precisamos de alguns tipos. Por favor, não comente sobre as artimanhas do exemplo, esta é a versão mais compactada que mostra todas as diferenças de uma vez.

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)
  • Chamada de método: foo.member.await()(42)
    Vincula-se mais fortemente, então nenhuma parêntese
  • Membro: (foo.member.await)(42)
    Precisa de parênteses em torno do resultado esperado quando este é um chamável, isso é consistente com ter um chamável como um membro, caso contrário, confusão com a chamada à função de membro. Isso também sugere que se pode desestruturar com padrões: let … { await: value } = foo.member; value(42) alguma forma?
  • Chamada de função: (foo.member)(await)(42)
    Necessita de parênteses para desestruturação (movemos o membro), pois ele se comporta como uma chamada de função.

Todos eles parecem iguais quando não desestruturamos uma estrutura de entrada por meio da movimentação de um membro, nem chamamos o resultado como um chamável, pois essas três classes de precedência vêm diretamente uma após a outra. Como queremos que os futuros se comportem?

O melhor paralelo para a chamada de método deve ser apenas outra chamada de método usando self .

O melhor paralelo para o membro é uma estrutura que tem apenas o membro implícito await e, portanto, é desestruturada ao se mover a partir dele, e essa movimentação implicitamente espera o futuro. Este parece o menos óbvio.

O paralelo à função é chamada é o comportamento dos fechamentos. Eu preferiria esta solução como a adição mais limpa ao corpus da linguagem (como na sintaxe melhor paralela as possibilidades do tipo), mas são alguns pontos positivos para a chamada de método e .await nunca é maior que os outros.

@HeroicKatora Poderíamos .await em libsyntax para permitir foo.await(42) mas isso seria inconsistente / ad-hoc. No entanto, (foo.await)(42) parece útil, pois embora existam encerramentos de saída de futuros, eles provavelmente não são tão comuns. Portanto, se otimizarmos para o caso comum, não ter que adicionar () a .await provavelmente sairá vencedor.

@Centril Concordo, a consistência é importante. Quando vejo alguns comportamentos semelhantes, gostaria de inferir outros por meio de síntese. Mas aqui .await parece estranho, especialmente com o exemplo acima mostrando claramente que ele é paralelo à desestruturação implícita (a menos que você possa encontrar uma sintaxe diferente onde esses efeitos ocorrem?). Quando vejo a desestruturação, imediatamente me pergunto se posso usar isso com let-bindings etc. Isso, no entanto, não seria possível porque destruiríamos o tipo original que não tem tal membro ou especialmente quando o tipo é apenas impl Future<Output=F> (Nota notória irrelevante: fazer isso funcionar nos levaria de volta a um prefixo alternativo await _ = no lugar de let _ = , engraçado¹).

Isso não nos proíbe de usar a sintaxe em si, acho que poderia lidar com o aprendizado e se for a última vou usá-la com vigor, mas parece uma clara fraqueza.


¹ Isso pode ser consistente ao permitir ? ao permitir ? atrás de nomes em um padrão

  • await value? = failing_future();

para corresponder à parte Ok de Result . Parece interessante explorar também em outros contextos, mas fora do tópico. Isso também levaria à correspondência de sintaxe de prefixo e sufixo para await ao mesmo tempo.

Isso não nos proíbe de usar a sintaxe em si, acho que poderia lidar com o aprendizado e se for a última vou usá-la com vigor, mas parece uma clara fraqueza.

Acho que toda solução terá alguma desvantagem em alguma dimensão ou caso. consistência, ergonomia, capacidade de encadeamento, legibilidade, ... Isso torna uma questão de grau, importância dos casos, adequação ao código Rust típico e APIs, etc.

No caso de um usuário escrevendo foo.await(42) ...

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

    foo.closure(42);
}

... já fornecemos bons diagnósticos:

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

Ajustar isso para caber em foo.await(42) parece bastante atingível. Na verdade, pelo que posso ver, sabemos que o usuário pretende (foo.await)(42) quando foo.await(42) está escrito, então isso pode ser cargo fix ed em um MachineApplicable maneira. De fato, se estabilizarmos foo.await mas não permitirmos foo.await(42) , acredito que podemos até mesmo mudar a precedência mais tarde se precisarmos, já que foo.await(42) não será legal no início.

Outros aninhamentos funcionariam (por exemplo, futuro do resultado do fechamento - não que isso seja comum):
`` `ferrugem
struct HasClosure fn _foo () -> Resultado <(), ()> {
deixe foo: HasClosure <_ i = "27"> = HasClosure {encerramento: Ok (| x | {})};

foo.closure?(42);

Ok(())

}

por exemplo, futuro do resultado do fechamento - não que isso seja comum

O sufixo extra ? operador torna isso inequívoco, sem modificações na sintaxe - em qualquer um dos exemplos pós-correção. Não há necessidade de ajustes . Os problemas são apenas .member sendo explicitamente um campo e a necessidade de se aplicar a desestruturação de movimento primeiro. E eu realmente não quero dizer que isso seria difícil de escrever. Eu quero dizer principalmente que isso parece inconsistente com outros .member usos que, por exemplo, podem ser transformados em correspondência. A postagem original pesava pontos positivos e negativos a esse respeito.

Editar: Ajustar para caber future.await(42) tem o risco extra, provavelmente não intencional, de tornar isso a) inconsistente com fechamentos onde este não é o caso devido a métodos com o mesmo nome do membro serem permitidos; b) inibir desenvolvimentos futuros onde gostaríamos de dar argumentos para await . Mas, como você mencionou anteriormente, ajustar Future retornar um fechamento não deve ser a questão mais urgente.

@novacrazy Por que não usar simplesmente combinadores Future regulares?

Não tenho certeza de quanta experiência você tem com Futures 0.3, mas a expectativa geral é que os combinadores não serão muito usados, e o uso primário / idiomático será assíncrono / aguardar.

Async / await tem várias vantagens sobre os combinadores, por exemplo, oferece suporte a empréstimos em pontos de rendimento.

Os combinadores existiam muito antes de async / await, mas async / await foi inventado de qualquer maneira e por um bom motivo!

Async / await veio para ficar, o que significa que precisa ser ergonômico (inclusive com cadeias de métodos).

Claro que as pessoas são livres para usar os combinadores se quiserem, mas eles não deveriam ser necessários para se obter uma boa ergonomia.

Como @cramertj disse, vamos tentar manter a discussão focada em async / await, não alternativas para async / await.

Na verdade, algumas expressões não se traduzem bem em cadeias de espera se você quiser ter comportamentos de backup em caso de falha.

Seu exemplo pode ser simplificado significativamente:

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

Isso deixa claro quais partes estão lidando com o erro e quais partes estão no caminho normal de felicidade.

Esta é uma das melhores coisas sobre async / await: funciona bem com outras partes da linguagem, incluindo loops, branches, match , ? , try , etc.

Na verdade, além de await , este é o mesmo código que você escreveria se não estivesse usando Futuros.

Outra maneira de escrever, se você preferir usar o combinador 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()

E o melhor de tudo é mover o código normal para uma função separada, tornando o código de tratamento de erros ainda mais claro:

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

(Esta é uma resposta ao comentário de @joshtriplett ).

Para ser claro, você não precisa colocar parênteses, eu mencionei isso porque algumas pessoas disseram que é muito difícil ler sem os parênteses. Portanto, os parênteses são uma escolha estilística opcional (útil apenas para frases complexas).

Todas as sintaxes se beneficiam de parênteses em algumas situações, nenhuma das sintaxes é perfeita, é uma questão de quais situações queremos otimizar.

Além disso, depois de reler seu comentário, talvez você tenha pensado que eu estava defendendo o prefixo await ? Eu não estava, meu exemplo estava usando o postfix await . No geral, gosto do postfix await , embora também goste de algumas das outras sintaxes.

Estou começando a ficar confortável com fut.await , acho que a reação inicial das pessoas será "Espere, é assim que você espera? Estranho." mas, mais tarde, eles adorariam pela conveniência. Claro, o mesmo é verdadeiro para @await , que se destaca muito mais do que .await .

Com essa sintaxe, podemos deixar de fora algumas das deixa no exemplo:

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

Isso também torna mais claro o que está sendo desembrulhado com ? , para await some_op()? , não é óbvio se some_op() foi desembrulhado ou o resultado esperado.

@Pauan

Não estou tentando desviar o foco do tópico aqui, estou tentando apontar que ele não existe em uma bolha. Temos que considerar como as coisas funcionam juntas .

Mesmo que a sintaxe ideal fosse escolhida, eu ainda gostaria de usar futuros personalizados e combinadores em algumas situações. A ideia de que aqueles poderiam ser descontinuados me faz questionar toda a direção de Rust.

Os exemplos que você deu ainda parecem terríveis em comparação com os combinadores e, com a sobrecarga do gerador, provavelmente serão um pouco mais lentos e produzirão mais código de máquina.

Até onde vai, todo esse prefixo / sigilo pós-fixo / palavra-chave recriação de bicicletas é maravilhoso, mas talvez devêssemos ser pragmáticos e ir com a opção mais simples que é mais familiar para os usuários que vêm para o Rust. Ou seja: prefixo palavra-chave

Este ano vai passar mais rápido do que pensamos. Até janeiro está quase tudo pronto. Se descobrir que os usuários não estão satisfeitos com uma palavra-chave de prefixo, ela pode ser alterada em uma edição 2019/2020. Podemos até fazer uma piada do tipo “retrospectiva é 2020”.

@novacrazy

O consenso geral que vi é que o _primeiro_ que gostaríamos de uma terceira edição é 2022. Definitivamente, não queremos planejar outra edição; a edição de 2018 foi ótima, mas teve seus custos. (E um dos pontos da edição 2018 é tornar assíncrono / esperar possível, imagine pegar isso de volta e dizer "não, você precisa atualizar para a edição 2020 agora!")

Em qualquer caso, não acho que uma palavra-chave de prefixo -> transição de palavra-chave postfix seja possível em uma edição, mesmo que isso fosse desejável. A regra em torno das edições é que deve haver uma maneira de escrever código idiomático na edição X de forma que compile sem avisos e funcione da mesma forma na edição X + 1.

É o mesmo motivo pelo qual preferimos não nos estabilizar com uma macro de palavras-chave se pudermos gerar um consenso sobre uma solução diferente; estabilizar deliberadamente uma solução que sabemos ser indesejável é em si problemático.

Acho que mostramos que uma solução postfix é mais ideal, mesmo para expressões com apenas um ponto de espera. Mas eu duvido que qualquer uma das soluções postfix propostas seja obviamente melhor do que todas as outras.

Apenas meus dois centavos (não sou ninguém, mas acompanho a discussão há muito tempo). Minha solução favorita seria a versão postfix @await . Talvez você pudesse considerar um postfix !await , como alguma nova sintaxe de macro postfix?

Exemplo:

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

Depois de algumas iterações de linguagem, ser capaz de implementar nossas próprias macros postfix seria incrível.

... todas as novas propostas sygils

Rust já tem sintaxe / sygil pesada, temos await palavra-chave reservada, stuff@await (ou qualquer outro sygil) parece estranho / feio (subjetivo, eu sei), e é apenas uma sintaxe ad-hoc que não se integra com mais nada na língua, o que é uma grande bandeira vermelha.

Eu dei uma olhada em como Go implementa
... <- ... proposta

@ I60R : Go tem uma sintaxe terrível cheia de soluções ad-hoc e é totalmente imperativo, muito diferente do Rust. Esta proposta é novamente sygil / sintaxe pesada e totalmente ad-hoc apenas para este recurso específico.

@ I60R : Go tem uma sintaxe terrível

Vamos, por favor, evitar criticar outras línguas aqui. "X tem uma sintaxe terrível" não leva à iluminação e ao consenso.

Como um usuário Python / JavaScript / Rust e um estudante de ciência da computação, eu pessoalmente prefiro o prefixo await + f.await() para estar na linguagem.

  1. Tanto o Python quanto o JavaScript têm o prefixo await . Eu esperaria ver await aparecer no início. Se eu tiver que ler profundamente na linha para perceber que este é um código assíncrono, me sinto muito desconfortável. Com o recurso WASM do Rust, ele pode atrair muitos desenvolvedores JS. Acredito que familiaridade e conforto são muito importantes, considerando que Rust já tem muitos outros novos conceitos.

  2. Postfix await parece conveniente em configurações de encadeamento. No entanto, não gosto de soluções como .await , @await , f await porque se parecem com uma solução ad-hoc para a sintaxe await embora faça sentido pensar .await() como chamando um método no future .

Rust já é uma partida do javascript e await não é nada como chamar função (ou seja, a funcionalidade não pode ser emulada por meio de uma função), usando funções para denotar await torna confuso para os iniciantes que estão sendo introduzidos no async-await. Portanto, acho que a sintaxe deve ser diferente.

Eu me convenci de que .await() é provavelmente significativamente mais desejável do que .await , embora o restante deste post evite um pouco essa posição.

A razão para isso é que await!(fut) tem que _consumir fut por valor_. Fazer com que pareça que o acesso a um campo é _bad_, porque isso não tem a conotação de se mover da maneira que uma palavra-chave de prefixo, ou o potencial de se mover como uma macro ou uma chamada de método.

Curiosamente, a sintaxe do método de palavra-chave quase faz com que pareça um projeto de await implícito . Infelizmente, "Explicit Async, Implicit Await" não é possível para Rust (então _por favor, não litigue-o neste tópico), pois queremos que async fn() -> T seja usado de forma idêntica a fn() -> Future<T> , em vez de ativar um comportamento de "espera implícita".

O fato de que uma sintaxe .await() _parece_ como um sistema de espera implícito (como o Kotlin usa) poderia ser um detrator e quase daria a sensação de uma "Async implícita, espera implícita" devido à magia em torno do não sintaxe de chamada de método .await() . Você poderia usar isso como await(fut) com UFCS? Seria Future::await(fut) para o UFCS? Qualquer sintaxe que se pareça com outra dimensão da linguagem levanta problemas, a menos que possa ser um tanto unificada com ela, pelo menos sintaticamente, mesmo se não funcionalmente.

Continuo cético se os benefícios de qualquer solução postfix _individual_ superam as desvantagens dessa mesma solução, embora o conceito de uma solução postfix seja mais desejável do que um prefixo em geral.

Estou um pouco surpreso que este tópico esteja cheio de sugestões que parecem ser feitas porque são possíveis - e não porque parecem produzir quaisquer benefícios significativos em relação às sugestões iniciais.
Podemos parar de falar sobre $ , # , @ , ! , ~ etc, sem apresentar um argumento significativo sobre o que está errado com await , que é bem compreendida e se provou em várias outras linguagens de programação?

Acho que o post de https://github.com/rust-lang/rust/issues/57640#issuecomment -455361619 já listou todas as boas opções.

Daqueles:

  • Delimitadores obrigatórios delimitadores parecem estar bem, pelo menos é óbvio qual é a precedência e podemos ler o código de outros sem mais clareza. E digitar dois parênteses não é tão ruim. Talvez a única desvantagem seja que parece uma chamada de função, embora seja uma operação de fluxo de controle diferente.
  • A precedência útil pode ser a opção preferível. Esse parece ser o caminho que a maioria das outras línguas percorreram, por isso é familiar e comprovado.
  • Pessoalmente, acho que a palavra-chave postfix com espaço em branco parece estranha com vários awaits em uma única instrução:
    client.get("url").send() await?.json()? . Esse espaço em branco parece fora do lugar. Com parênteses, faria um pouco mais de sentido para mim: (client.get("url").send() await)?.json()?
    Mas acho o fluxo de controle ainda mais difícil de seguir do que com a variante de prefixo.
  • Eu não gosto de campo Postfix. await faz uma operação muito complexa - Rust não tem propriedades computáveis ​​e o acesso ao campo é uma operação muito simples. Portanto, parece dar uma impressão errada sobre a complexidade desta operação.
  • O método Postfix pode estar OK. Pode encorajar algumas pessoas a escrever declarações muito longas com múltiplas esperas nelas, o que pode ocultar ainda mais os pontos de rendimento. Também faz com que as coisas pareçam uma chamada de método, embora seja algo diferente.

Por essas razões, prefiro a "precedência útil" seguida de "delimitadores obrigatórios"

Go tem uma sintaxe terrível cheia de soluções ad-hoc e é totalmente imperativo, muito diferente do Rust. Esta proposta é novamente sygil / sintaxe pesada e totalmente ad-hoc apenas para este recurso específico.

@dpc , se você ler <-- proposta completamente, verá que essa sintaxe é inspirada apenas em Go, no entanto, é muito diferente e pode ser usada tanto no contexto de encadeamento de funções quanto no imperativo. Também não consigo ver como a sintaxe de await não é uma solução ad-hoc, para mim é muito mais específica e mais desajeitada do que <-- . É semelhante a deref reference / reference.deref / etc em vez de *reference , ou try result / result.try / etc em vez de result? . Eu também não vejo nenhuma vantagem em usar a palavra-chave await além da familiaridade com JS / Python / etc, que de qualquer forma deve ser menos significativa do que ter uma sintaxe consistente e combinável. E eu não vejo nenhuma desvantagem em ter <-- sigilo além de não ser um await que de qualquer forma não é tão simples como o inglês puro e os usuários devem entender o que ele faz primeiro.

Edit: esta também pode ser uma boa resposta para a postagem de @ Matthias247 , uma vez que fornece alguns argumentos contra await e propõe uma possível alternativa não afetada pelos mesmos problemas


É realmente interessante para mim, através, ler críticas contra a sintaxe <-- , livre de argumentos que apelam a razões históricas e preconceituosas.

Vamos mencionar detalhes reais sobre a precedência:

O gráfico de precedência como está hoje :

Operador / Expressão | Associatividade
- | -
Caminhos |
Chamadas de método |
Expressões de campo | da esquerda para direita
Chamadas de função, indexação de matriz |
? |
Unário - * ! & &mut |
as | da esquerda para direita
* / % | da esquerda para direita
+ - | da esquerda para direita
<< >> | da esquerda para direita
& | da esquerda para direita
^ | da esquerda para direita
\| | da esquerda para direita
== != < > <= >= | Requer parênteses
&& | da esquerda para direita
\|\| | da esquerda para direita
.. ..= | Requer parênteses
= += -= *= /= %= &= \|= ^= <<= >>= | direita para esquerda
return break encerramentos |

A precedência útil coloca await antes de ? para que se ligue mais firmemente do que ? . Um ? na cadeia, portanto, vincula um `await a tudo o que está antes dele.

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

Sim, com precedência útil, isso "simplesmente funciona". Você sabe o que isso faz à primeira vista? Isso é estilo ruim (provavelmente)? Em caso afirmativo, o rustfmt pode corrigir isso automaticamente?

A precedência óbvia coloca await _somewhere_ abaixo de ? . Não tenho certeza de onde exatamente, embora esses detalhes provavelmente não importem muito.

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

Você sabe o que isso faz à primeira vista? O rustfmt pode colocá-lo em um estilo útil que não desperdice muito espaço vertical e horizontal automaticamente?


Onde as palavras-chave do Postfix cairiam nisso? Provavelmente, método de palavra-chave com chamadas de método e campo de palavra-chave com expressões de campo, mas não tenho certeza de como os outros devem ser vinculados. Que opções levam ao mínimo de configurações possíveis onde await recebe um "argumento" surpreendente?

Para esta comparação, suspeito que os "delimitadores obrigatórios" (o que chamei de Função de palavra-chave no resumo ) ganham facilmente, pois seriam equivalentes a uma chamada de função normal.

@ CAD97 Para ser claro, lembre-se de que .json() também é um futuro (pelo menos em reqwests ).

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

Quanto mais eu brinco com a conversão de expressões ferrugem complexas (mesmo aquelas com apenas a necessidade de 1 await, no entanto, observe que em minha base de código de mais de 20.000 + quase todas as expressões assíncronas são um await, seguido diretamente por outro await), mais eu não gosto prefixo para Rust.

Isso é _tudo_ por causa do operador ? . Nenhuma outra linguagem tem um operador de fluxo de controle postfix _and_ await que são essencialmente sempre pareados em código real.


Minha preferência ainda é o campo Postfix . Como um operador de controle postfix, sinto que ele precisa do agrupamento visual estreito que future.await fornece em future await . E comparando com .await()? , eu prefiro como .await? parece _estranho_, então será _notado_ e os usuários não assumirão que é uma função simples (e, portanto, não perguntarão por que o UFCS não funciona) .


Como mais um ponto de dados a favor do postfix, quando ele estiver estabilizado, um rustfix ir de await!(...) para o que decidirmos seria muito apreciado. Não vejo como qualquer coisa, exceto a sintaxe pós-fixada, poderia ser traduzida de forma inequívoca sem envolver coisas em ( ... ) desnecessariamente.

Acho que primeiro devemos responder à pergunta "queremos encorajar o uso de await em contextos de encadeamento?". Eu acredito que a resposta predominante é "sim", então se torna um forte argumento para variantes pós-fixadas. Embora await!(..) seja o mais fácil de adicionar, acredito que não devemos repetir a história de try!(..) . Também, pessoalmente, discordo do argumento de que "o encadeamento oculta uma operação potencialmente custosa", já temos muitos métodos de encadeamento que podem ser _muito_ pesados, portanto, o encadeamento não implica preguiça.

Embora o prefixo await palavra-chave seja o mais familiar para usuários vindos de outras línguas, não acho que devemos tomar nossa decisão com base nele e, em vez disso, devemos nos concentrar em longo prazo, ou seja, usabilidade, conveniência e legibilidade . @withoutboats falou sobre "orçamento de familiaridade", mas acredito fortemente que não devemos introduzir soluções abaixo do ideal apenas por questão de familiaridade.

Agora, provavelmente não queremos duas maneiras de fazer a mesma coisa, então não devemos introduzir as variantes pós-fixada e pré-fixada. Então, digamos que restringimos nossas opções para variantes pós-fixadas.

Primeiro, vamos começar com fut await . Não gosto dessa variante, porque ela vai bagunçar seriamente a forma como os humanos analisam o código e será uma fonte constante de confusão durante a leitura do código. (Não se esqueça de que o código é principalmente para leitura)

Próximos fut.await , fut.await() e fut.await!() . Acho que a variante mais consistente e menos confusa será a macro postfix. Não acho que vale a pena introduzir uma nova entidade de "função de palavra-chave" ou "método de palavra-chave" apenas para salvar alguns caracteres.

Por último, variantes baseadas em sigilos: fut@await e fut@ . Não gosto da variante fut@await , se introduzirmos o sigilo, por que se preocupar com a parte await ? Temos planos para extensões futuras fut@something ? Se não, simplesmente parece redundante. Então eu gosto da variante fut@ , ela resolve os problemas de precedência, o código se torna fácil de entender, escrever e ler. Os problemas de visibilidade podem ser resolvidos realçando o código. Não será difícil explicar esse recurso como "@ for await". Obviamente, a maior desvantagem é que pagaremos por esse recurso com um "orçamento de sigilo" muito limitado, mas considerando a importância do recurso e a frequência com que será usado em bases de código assíncronas, acredito que valerá a pena a longo prazo . E, claro, podemos traçar certos paralelos com ? . (Embora tenhamos que estar preparados para piadas Perl dos críticos de Rust)

Concluindo: na minha opinião, se estamos prontos para sobrecarregar o "orçamento de sigilo", devemos ir com fut@ , e se não com fut.await!() .

Ao falar sobre familiaridade, não acho que devamos nos preocupar muito com a familiaridade com JS / Python / C #, uma vez que Rust está em um nicho diferente e já tem uma aparência diferente em muitas coisas. Fornecer sintaxe semelhante a essas linguagens é uma meta de curto prazo e de baixa recompensa. Ninguém selecionará Ferrugem apenas para usar palavras-chave familiares quando, por baixo do capô, ele funcionar de maneira completamente diferente.

Mas a familiaridade com Go importa, já que Rust está em um nicho semelhante e até mesmo por filosofia está mais próximo de Go do que de outras linguagens. E apesar de todo o ódio preconceituoso, um dos pontos mais fortes de ambos é que eles não copiam recursos cegamente, mas implementam soluções que realmente têm razão para.

IMO, neste sentido <-- sintaxe é mais forte aqui

Com tudo isso, vamos lembrar que expressões Rust podem resultar em vários métodos encadeados. A maioria das línguas tende a não fazer isso.

Eu gostaria de lembrar a experiência da equipe dev C #:

A principal consideração contra a sintaxe do C # é a precedência de operador await foo?

Isso é algo que sinto que posso comentar. Nós pensamos muito sobre precedência com 'esperar' e tentamos muitos formulários antes de definir o formulário que queríamos. Uma das coisas principais que descobrimos foi que, para nós e para os clientes (internos e externos) que queriam usar esse recurso, raramente as pessoas realmente queriam 'encadear' algo além de sua chamada assíncrona.

A tendência das pessoas de querer 'continuar' com o 'esperar' dentro de uma expr era rara. Ocasionalmente vemos coisas como (await expr) .M (), mas parecem menos comuns e menos desejáveis ​​do que a quantidade de pessoas que aguardam expr.M ().

e

É também por isso que não escolhemos nenhuma forma 'implícita' para 'esperar'. Na prática, era algo sobre o qual as pessoas queriam pensar com muita clareza e que queriam primeiro e centralizado em seu código, para que pudessem prestar atenção nele. Curiosamente, mesmo anos depois, essa tendência se manteve. isto é, às vezes, muitos anos depois, lamentamos que algo seja excessivamente prolixo. Alguns recursos são bons assim no início, mas uma vez que as pessoas se sintam confortáveis ​​com eles, são mais adequados para algo mais simples. Esse não foi o caso com 'esperar'. As pessoas ainda parecem realmente gostar da natureza pesada dessa palavra-chave e da precedência que escolhemos.

É um bom ponto contra sigilo em vez de palavra (chave) dedicada.

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

Você realmente deve ouvir os caras com milhões de usuários.

Então você não quer encadear nada, você só quer ter vários await , e minha experiência é a mesma. Escrevendo código async/await por mais de 6 anos, e nunca quis esse recurso. A sintaxe do Postfix parece realmente estranha e é considerada para resolver uma situação que provavelmente nunca acontecerá. Async chamada de

A tendência das pessoas de querer 'continuar' com o 'esperar' dentro de uma expr era rara. Ocasionalmente vemos coisas como (await expr) .M (), mas parecem menos comuns e menos desejáveis ​​do que a quantidade de pessoas que aguardam expr.M ().

Isso parece uma análise a posteriori. Talvez uma das razões pelas quais eles não continuem é porque é extremamente estranho fazê-lo na sintaxe de prefixo (comparável a não querer try! várias vezes em uma instrução porque isso permanece legível pelo operador ? ). O que foi dito acima considera principalmente (até onde posso dizer) a precendência, não a posição. E eu gostaria de lembrar a você que C # não é Rust, e os membros do trait podem mudar um pouco o desejo de chamar métodos nos resultados.

@ I60R ,

  1. Acho que a familiaridade é importante. Rust é uma linguagem relativamente nova e as pessoas migrarão de outras línguas e, se o Rust parecer familiar, será mais fácil para eles tomar a decisão de escolher o Rust.
  2. Não sou muito divertido com métodos de encadeamento - é muito mais difícil depurar cadeias longas e acho que o encadeamento está apenas complicando a legibilidade do código e pode ser permitido apenas como opção adicional (como macros .await!() ). A forma de prefixo forçará os desenvolvedores a extrair o código em métodos em vez de encadear, como:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

em algo assim:

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

Isso parece uma análise a posteriori. Talvez um dos motivos pelos quais eles não continuem seja porque é extremamente estranho fazer isso na sintaxe de prefixo. A descrição acima considera apenas a precendência, não a posição. E eu gostaria de lembrar a você que C # não é Rust, e os membros do trait podem mudar um pouco o desejo de chamar métodos nos resultados.

Não, trata-se de experimentos de equipe C # internos quando tinha formulários prefixo / postfix / implícito. E estou falando sobre minha experiência, que não é apenas um hábito em que não consigo ver os profissionais do formulário postfix.

@mehcode Seu exemplo não me motiva. reqwest decide conscientemente fazer o ciclo de solicitação / resposta inicial e o tratamento subsequente da resposta (fluxo do corpo) em processos simultâneos separados, portanto, eles devem ser esperados duas vezes, como mostra @andreytkachenko .

reqwest poderia expor totalmente as seguintes APIs:

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

ou

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

(O último sendo um açúcar simples acima de and_then ).

Acho preocupante que muitos dos exemplos de postfix aqui usem essa cadeia como exemplo, já que reqwest hyper s a melhor decisão de API é manter essas coisas separadas.

Eu honestamente acredito que a maioria dos exemplos de espera de postfix aqui devem ser reescritos usando combinadores (e açúcar, se necessário) ou ser mantidos em operações semelhantes e aguardados várias vezes.

@andreytkachenko

A forma de prefixo forçará os desenvolvedores a extrair o código em métodos em vez de encadear, como:

Isso é bom ou ruim? Para casos em que há N métodos, cada um deles podendo resultar em M acompanhamentos, o desenvolvedor deve fornecer N * M métodos? Eu, pessoalmente, gosto de soluções composíveis, mesmo que sejam um pouco mais longas.

em algo assim:

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

@Pzixel

Não acho que em C # você obteria tantos códigos encadeados / funcionais quanto em Rust. Ou eu estou errado? Isso torna as experiências de desenvolvedores / usuários C # interessantes, mas não necessariamente aplicáveis ​​ao Rust. Eu gostaria que pudéssemos contrastar com Ocaml ou Haskell.

Eu acho que a causa raiz da discordância é que alguns de nós gostamos de um estilo imperativo e outros funcional. E é isso. Rust oferece suporte a ambos, e ambos os lados do debate desejam que o assíncrono se encaixe bem na maneira como normalmente escrevem o código.

@dpc

Não acho que em C # você obteria tantos códigos encadeados / funcionais quanto em Rust. Ou eu estou errado? Isso torna as experiências de desenvolvedores / usuários C # interessantes, mas não necessariamente aplicáveis ​​ao Rust. Eu gostaria que pudéssemos contrastar com Ocaml ou Haskell.

LINQ e estilo funcional são muito populares em C #.

@dpc ,
se você é sobre legibilidade - acho que tanto código explícito - quanto melhor, e minha experiência diz que se os nomes de métodos / funções forem bem autodescritivos não é necessário fazer os acompanhamentos.

se você sobre overhead - o compilador Rust é bastante inteligente para embuti-los (de qualquer maneira, sempre temos #[inline] ).

@dpc

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

Minha sensação é que isso basicamente repete o problema da API de futuros: torna o encadeamento mais fácil, mas o tipo dessa cadeia se torna muito mais difícil de penetrar e depurar.

Esse mesmo argumento não se aplica ao? operador embora? Ele permite mais encadeamento e, portanto, torna a depuração mais difícil. Mas por que escolhemos? mais de tentar! então? E por que Rust prefere APIs com padrão de construtor? Portanto, Rust já fez um monte de escolhas favorecendo o encadeamento em APIs. E sim, isso pode tornar a depuração mais difícil, mas isso não deveria acontecer em um nível de lint - talvez um novo lint para clippy para correntes que são muito grandes? Eu ainda tenho que ver motivação suficiente para saber como esperar é diferente aqui.

Isso é bom ou ruim? Para casos em que há N métodos, cada um deles podendo resultar em M acompanhamentos, o desenvolvedor deve fornecer N * M métodos? Eu, pessoalmente, gosto de soluções composíveis, mesmo que sejam um pouco mais longas.

N e M não são necessariamente grandes e também nem todos podem ser de interesse / utilidade para extrair.

@andreytkachenko ,

  1. Não concordo, a familiaridade é superestimada aqui. Ao migrar de outra linguagem, primeiro as pessoas buscariam a capacidade de fazer programação no estilo async-await , mas não exatamente com a mesma sintaxe. Se fosse implementado de maneira diferente, mas proporcionasse uma melhor experiência de programação, então se tornaria mais uma vantagem.

  2. A incapacidade de depurar cadeias longas é uma limitação dos depuradores que não são do estilo do código. Sobre a legibilidade, acho que depende, e impor um estilo imperativo é desnecessariamente restritivo aqui. Se async chamadas de função ainda são chamadas de função, então é surpreendente não suportar encadeamento. E de qualquer maneira, <-- é um operador de prefixo com capacidade de usá-lo em cadeias de funções como opção adicional, como você disse

@skade Concordo.

Todos os exemplos com longas cadeias de espera são um cheiro de código. Eles poderiam ser resolvidos de maneira muito mais elegante com combinadores ou APIs atualizadas, em vez de criar essas máquinas de estado espaguete geradoras sob o capô. A sintaxe abreviada extrema é uma receita para depurar ainda mais difícil do que os futuros atuais.

Eu ainda sou um fã de ambos prefixo palavra-chave await e macro await!(...) / .await!() , em combinação com async e #[async] funções de gerador , conforme descrito em meu comentário aqui .

Se tudo correr corretamente, uma única macro provavelmente poderia ser criada para await! para lidar com prefixo e sufixo, e a palavra-chave await seria apenas uma palavra-chave dentro de async funções.

@skade ,
Para mim, uma grande desvantagem de usar combinadores em future é que eles esconderão async chamadas de função e seria impossível distingui-los de funções regulares. Quero ver todos os pontos que podem ser suspensos, pois é muito provável que sejam usados ​​dentro de cadeias de estilo construtor.

@ I60R depurar cadeias de chamadas longas não é totalmente um problema de depurador, porque o problema em escrever essas cadeias é obter a inferência de tipo correta. Dado que muitos desses métodos usam parâmetros genéricos e distribuem parâmetros genéricos, potencialmente vinculados a um encerramento, é um problema grave.

Não tenho certeza se tornar visíveis todos os pontos de suspensão é um objetivo do recurso. Isso cabe aos implementadores decidir. E é totalmente possível com todas as versões propostas da sintaxe await .

@novacrazy bom ponto, agora que você mencionou, como um ex-javascripter, eu NUNCA uso await chains, eu sempre usei blocos then

Eu acho que seria algo como

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

que nem sei se é possível, preciso pesquisar futuros em Rust

@richardanaya isso é totalmente possível (sintaxe diferente).

Como o Box impl:

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

Podemos inserir uma nova palavra-chave await e o traço Await com este impl:

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

E use await como método e também como palavra-chave:

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

@richardanaya Concordo. Eu estava entre alguns dos primeiros desenvolvedores a adotar async / await para meu material de webdev há muitos anos, e o poder real veio da combinação de async / await com as Promises / Futures existentes. Além disso, mesmo o seu exemplo provavelmente poderia ser simplificado como:

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

let value = result.unwrap();

Se você tiver aninhados futuros ou resultados de futuros ou futuros de resultados, o combinador .flatten() pode simplificar ainda mais isso drasticamente. Seria péssimo desembrulhar e aguardar cada um manualmente.

@XX Isso é redundante e inválido. await só existe em funções async , e só pode funcionar em tipos que implementam Future / IntoFuture qualquer maneira, então não há necessidade de um novo traço.

@novacrazy, @richardanaya

eles poderiam ser resolvidos de forma muito mais elegante com combinadores ou APIs atualizadas, em vez de criar essas máquinas de estado espaguete geradoras sob o capô. A sintaxe abreviada extrema é uma receita para depurar ainda mais difícil do que os futuros atuais.

@novacrazy bom ponto, agora que você mencionou, como um ex-javascripter, eu NUNCA uso await chains, eu sempre usei blocos then

Os combinadores têm propriedades e poderes totalmente diferentes em Rusts async / await, por exemplo, em relação a empréstimos em pontos de rendimento. Você não pode escrever combinadores com segurança que tenham as mesmas habilidades dos blocos assíncronos. Vamos deixar a discussão do combinador fora deste tópico, pois não é útil.

@ I60R

Para mim, uma grande desvantagem de usar combinadores no futuro é que eles esconderão as chamadas de função assíncronas e seria impossível distingui-las das funções regulares. Quero ver todos os pontos que podem ser suspensos, pois é muito provável que sejam usados ​​dentro de cadeias de estilo construtor.

Você não pode ocultar pontos de suspensão. A única coisa que os combinadores permitem é a criação de outros futuros. Em algum momento, eles devem ser aguardados.

Lembre-se de que C # não tem o problema de a maioria dos códigos que usam um prefixo await combiná-lo com o postfix (já existente) ? operador. Em particular, questões sobre precedência e a estranheza geral de ter dois "decoradores" semelhantes aparecem em lados opostos de uma expressão.

@ Matthias247 Se você precisar emprestar dados, com certeza, sinta-se à vontade para usar várias instruções await . No entanto, muitas vezes você simplesmente precisa mover os dados, e os combinadores são perfeitamente válidos para isso e têm o potencial de compilar para um código mais eficiente. Às vezes, sendo totalmente otimizado.

Novamente, o verdadeiro poder está em combinar as coisas. Não existe uma maneira certa de fazer coisas tão complicadas como esta. Em parte, é por isso que considero todo esse bicicletário de sintaxe exatamente isso, se não vai ajudar os novos usuários vindos de outra linguagem e ajudar a criar código de fácil manutenção, desempenho e legibilidade . 80% das vezes duvido que sequer toque em async / await não importa qual seja a sintaxe, apenas para fornecer APIs mais estáveis ​​e com desempenho usando futuros simples.

Nesse sentido, a palavra-chave de prefixo await e / ou macro mista await!(...) / .await!() são as opções mais legíveis, familiares e fáceis de depurar.

@andreytkachenko

Vi sua postagem e concordo totalmente, aliás, agora que reflito sobre "familiaridade", acho que existem dois tipos de familiaridade:

  1. Fazendo a sintaxe de await parecer com linguagens semelhantes que usam muito await. Javascript é muito grande aqui e muito relevante para nossos recursos WASM como uma comunidade.

  2. O segundo tipo de familiaridade é fazer com que o código pareça familiar para desenvolvedores que só trabalham com código síncrono. Acho que o maior aspecto do await é fazer o código assíncrono LOOK síncrono. Na verdade, isso é uma coisa que o javascript realmente faz de errado é que ele é normalizado em cadeias longas que parecem totalmente estranhas aos desenvolvedores principalmente síncronos.

Uma coisa que eu ofereceria dos meus dias de assíncrono em javascript, o que era muito mais útil para mim como desenvolvedor em retrospecto, era a capacidade de agrupar promessas / futuros. Promise.all (p1 (), p2 ()) para que eu pudesse facilmente paralisar o trabalho. O encadeamento then () sempre foi apenas um eco da promessa do Javascript do passado, mas muito arcaico e desnecessário agora que penso nisso.

Eu ofereceria talvez a ideia de esperar. "Tente fazer as diferenças entre o código assíncrono e o código de sincronização o mínimo possível"

@novacrazy A função assíncrona retorna um tipo impl Future , certo? O que nos impede de adicionar o método await a Future trait? Assim :

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

@XX Pelo que entendi, async funções em Rust são transformadas em máquinas de estado usando geradores. Este artigo é uma boa explicação. Portanto, await precisa de uma função async para funcionar, de forma que o compilador possa transformar ambos corretamente. await não pode funcionar sem a parte async .

Future tem um wait método, que é semelhante ao que você sugere, mas bloqueia o segmento atual.

@skade ,

Mas como o estilo do código pode afetar a inferência de tipo? Não consigo ver diferença no mesmo código que é escrito em estilo encadeado e imperativo. Os tipos devem ser exatamente iguais. Se o depurador não é capaz de descobri-los, isso definitivamente é um problema no depurador, não no código.

@skade , @ Matthias247 , @XX

Com os combinadores, você terá exatamente um ponto suspenso marcado no início da cadeia de funções. Todos os outros estariam implícitos dentro. Esse é exatamente o mesmo problema de pegar mut implícito, o que, pessoalmente, para mim foi um dos maiores pontos de confusão em Rust. Algumas APIs retornariam futuros, enquanto outras retornariam resultados de futuros - eu não quero isso. Os pontos de suspensão devem ser explícitos, se possível, e a sintaxe adequadamente combinável incentivaria que

@ I60R let ligações são pontos de junção para digitar inferência e ajudar no relatório de erros.

@novacrazy

Portanto, o await precisa de uma função assíncrona para funcionar

Isso pode ser expresso em tipo de retorno? Por exemplo, que o tipo de retorno é impl Future + Async , em vez de impl Future .

@skade Eu sempre pensei que . na sintaxe de chamada de método serve exatamente para o mesmo propósito

@dpc

Não acho que em C # você obteria tantos códigos encadeados / funcionais quanto em Rust. Ou eu estou errado? Isso torna as experiências de desenvolvedores / usuários C # interessantes, mas não necessariamente aplicáveis ​​ao Rust. Eu gostaria que pudéssemos contrastar com Ocaml ou Haskell.

você obtém tanto quanto Rust. Observe qualquer código LINQ e você o verá.

O código assíncrono típico se parece com isto:

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

Ou, mais geral

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

Você não encadeia chamadas, atribui a uma variável e usa de alguma forma. Isso é especialmente verdadeiro quando você lida com o tomador de empréstimo.


Posso concordar que você pode encadear futuros em uma única situação. Quando você deseja lidar com um erro que pode ocorrer durante a chamada. por exemplo, let a = await!(service_a)? . Esta é a única situação em que a alternativa postfix é melhor. Eu poderia ver que isso se beneficia, mas não acho que supere todos os contras.

Outra razão: o que é sobre impl Add for MyFuture { ... } e let a = await a + b; ?

Gostaria de lembrar sobre a RFC de atribuição de tipo generalizada no contexto da palavra-chave await do postfix. Isso permitiria o seguinte código:

let x = (0..10)
    .map(some_computation)
    .collect() : Result<Vec<_>, _>
    .unwrap()
    .map(other_computation) : Vec<usize>
    .into() : Rc<[_]>;

Muito parecido com a palavra-chave espera do postfix:

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

Com as mesmas desvantagens quando formatado incorretamente:

foo.iter().map(|x| x.bar()).collect(): Vec<_>.as_ref()
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Acho que se engolirmos a pílula da RFC de atribuição de tipo, devemos engolir fut await para manter a consistência.

BTW, você sabia que esta é uma sintaxe válida:

fn main() {
    println
    !("Hello, World!");
}

Mesmo assim, não vi nenhuma ocorrência disso no código real.

Permita-me divagar um pouco. Acho que devemos dar macros postfix, como sugerido por @BenoitZugmeyer , outro pensamento. Isso poderia ser feito como expr!macro ou como expr@macro , ou talvez até expr.macro!() . Acho que a primeira opção seria preferível. Não tenho certeza se eles são uma boa ideia, mas se quisermos extrair um conceito geral e não uma solução ad-hoc enquanto ainda temos

Tenha em mente que mesmo se fizéssemos await uma macro postfix, ela ainda seria mágica (como compile_error! ). No entanto, @jplatte e outros já estabeleceram que isso não é um problema.

Como eu faria se fôssemos seguir esse caminho, primeiro estabeleceria exatamente como as macros pós-fixadas funcionariam, então permitiria apenas await como uma macro pós-fixada mágica e depois permitiria as próprias macros pós-fixadas.

Lexing

Quanto a lexing / análise, isso pode ser um problema. Se olharmos para expr!macro , o compilador pode pensar que existe uma macro chamada expr! e então existem algumas letras inválidas macro depois disso. No entanto, expr!macro deve ser possível para lex por um lookahead de um, e algo se torna uma macro postfix quando há um expr seguido por um ! , seguido diretamente por identifier . Não sou um desenvolvedor de linguagem e não tenho certeza se isso torna o léxico excessivamente complexo. Vou apenas assumir que as macros postfix podem assumir a forma de expr!macro .

Macros postfix seriam úteis?

No topo da minha cabeça, eu criei esses outros casos de uso para macros pós-fixadas. Eu não acho que todos eles podem ser implementados por macros personalizadas, então não tenho certeza se esta lista é muito útil.

  • stream!await_all : para aguardar transmissões
  • option!or_continue : quando option for Nenhum, continue o loop
  • monad!bind : por fazer =<< sem vinculá-lo a um nome

stream!await_all

Isso nos permite não apenas esperar por futuros, mas também por fluxos.

event_stream("ws://some.stock.exchange/usd2eur")
    .and_then(|exchange_response| {
        let exchange_rate = exchange_response.json()?;
        stream::once(UpdateTickerAction::new(exchange_rate.value))
    })

seria equivalente a (em um bloco async -stream-esque):

let exchange_rate = event_stream("ws://some.stock.exchange/usd2eur")
    !await_all
    .json()?;

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

Isso nos permite desembrulhar uma opção e, se for None , continuar o loop.

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

seria equivalente a:

loop {
    handle_button_pressed(
        engine.event()!or_continue
            .button()!or_continue
    );
}

monad!bind

Este nos permitiria obter mônadas de uma forma bastante rústica (isto é, centrada na expressão). Rust ainda não tem nada parecido com mônadas e não estou convencido de que sejam adicionadas ao Rust. No entanto, se eles forem adicionados, essa sintaxe pode ser útil.

Eu peguei o seguinte a partir daqui .

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 ++ "!")

seria equivalente a:

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;
}

Ou, mais alinhado, com menos 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;
}

Avaliação

Estou muito dividido nisso. A parte matemática do meu cérebro quer encontrar uma solução generalizada para await , e macros pós-fixadas podem ser uma maneira de alcançá-las. A parte pragmática do meu cérebro pensa: Por que se preocupar em fazer uma linguagem cujo macrossistema ninguém entende ainda mais complicada.

No entanto, de ? e await , temos dois exemplos de operadores Postfix superúteis. E se encontrarmos outros que desejamos adicionar no futuro, talvez semelhantes aos que mencionei? Se os generalizamos, podemos adicioná-los ao Rust naturalmente. Do contrário, teríamos que criar outra sintaxe a cada vez, o que provavelmente aumentaria o volume da linguagem mais do que as macros pós-fixadas.

O que é que vocês acham?

@EyeOfPython

Tive uma ideia muito semelhante e tenho certeza de que macros postfix em Rust é questão de tempo.
Estou pensando nisso todas as vezes ao lidar com o fatiamento de ndarray :

let view = array.slice(s![.., ..]);

mas muito melhor seria

let view = array.slice![.., ..];
// or like you suggested
let view = array!slice[.., ..];
// or like in PHP
let view = array->slice![.., ..];

e muitos combinadores podem desaparecer, como aqueles com _with ou _else postfixes:

opt!unwrap_or(Error::new("Error!"))?; //equal to .unwrap_or_else(||Error::new("Error!"));

@EyeOfPython @andreytkachenko macros postfix não são atualmente um recurso no Rust e o IMHO precisaria de uma fase de implementação RFC + FCP + completa.

Esta não é uma discussão RFC, mas uma discussão de uma RFC aceita que precisa ser implementada.

Por esse motivo, não acho prático discuti-los aqui ou propô-los para sintaxe assíncrona. Isso atrasaria ainda mais o recurso massivamente.

Para refrear essa discussão já massiva, eu acho que _não_ é útil discuti-los aqui, eles podem ser vistos fora do assunto discutido.

Apenas um pensamento: embora este tópico seja especificamente para await , acho que teremos a mesma discussão para yield expressões mais adiante, que também podem ser encadeadas. Na medida em que eu prefiro ver uma sintaxe que possa ser generalizada, seja como for.

Minhas razões para não usar macros aqui:

  1. As macros são fornecidas para coisas específicas do domínio e usá-las de qualquer forma que altere o fluxo de controle dos programas ou emule os recursos básicos da linguagem é um exagero
  2. As macros Postfix seriam imediatamente utilizadas para implementar operadores personalizados ou outros recursos de linguagem esotérica que levam a código incorreto
  3. Eles desencorajariam o desenvolvimento de recursos de linguagem adequados:

    • stream.await_all é um caso de uso perfeito para combinador

    • option.or_continue e substituir _else combinadores é um caso de uso perfeito para o operador de coalescência nulo

    • monad.bind é o caso de uso perfeito para if-let correntes

    • ndarray slicing é o caso de uso perfeito para const genéricos

  4. Eles iriam colocar em questão o operador ? já implementado

@collinanderson

que da mesma forma poderia ser acorrentado

Por que diabos você acha que eles podem ser acorrentados? zero ocorrências no código real, assim como println exemplo acima.

@Pzixel É provável que eventualmente Generator::resume assuma um valor e, portanto, yield expr terá um tipo diferente de () .

@vlaff Não vejo claramente um argumento para await ter que ser consistente com a atribuição de tipo. São coisas muito diferentes.

Além disso, a atribuição de tipo é outro dos recursos que são tentados repetidamente, não há garantia de que _este_ será executado. Embora eu não queira me opor a isso, TA é um futuro RFC e esta é uma discussão sobre um recurso aceito com uma sintaxe já proposta.

Lendo os muitos comentários, para adicionar mais resumo sobre por que esperar! (...) parece um caminho muito ideal:

  1. parece o mais familiar ao código existente porque as macros são familiares
  2. há trabalho existente que usa o await! (...) https://github.com/alexcrichton/futures-await e pode ajudar a reduzir a reescrita de código
  3. uma vez que macros postfix não estão na mesa, podem nunca fazer parte da linguagem e "aguarde!" em um contexto fora do padrão não parece ser uma possibilidade de acordo com os RFCs
  4. dá à comunidade mais tempo para considerar a direção de longo prazo após o uso estável e generalizado, ao mesmo tempo em que fornece algo que não está totalmente fora do normal (por exemplo, tente! ())
  5. poderia ser usado como um padrão semelhante para o rendimento futuro! () até descobrirmos um caminho oficial que poderia satisfazer o await e o yield
  6. o encadeamento pode não ser tão valioso quanto se espera e a clareza provavelmente será ainda melhorada por ter várias esperas em várias linhas
  7. nenhuma mudança teria que acontecer para realçadores de sintaxe IDE
  8. pessoas de outras línguas provavelmente não serão prejudicadas por uma macro await! (...) depois de ver outros usos de macro (menos sobrecarga cognitiva)
  9. é provavelmente o caminho de menor esforço de todos os caminhos para avançar para a estabilização

aguardam! macro já está aqui, a questão é sobre encadeamento.

Quanto mais eu penso sobre isso, mais o postfix parece bom para mim. Por exemplo:

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

Permite aninhamento de qualquer nível e pode ser facilmente lido. Funciona perfeitamente com ? e possíveis futuros operadores. Parece um pouco estranho para os novatos, mas o lucro da gramática pode superá-lo.

O principal motivo é que await deve ser uma palavra-chave separada, não uma chamada de função. É muito importante, por isso deve ter seu próprio destaque e lugar no texto.

@Pzixel , @HeroicKatora , @skade

Veja por exemplo as expressões Python yield e yield from : lá, a função externa pode fornecer um valor que se tornará o resultado de yield quando o gerador for reiniciado. Isso é o que @valff significa e não tem nada a ver com as atribuições de tipo. Portanto, yield também terá um tipo diferente de ! ou () .

Do ponto da co-rotina, yield e await suspendem e podem (eventualmente) retornar um valor. Na medida em que são apenas 2 faces da mesma moeda.

E para descartar outra possibilidade de sintaxe, _square-brackets-with-keyword_, em parte para destacar isso (usando yield para o destaque de sintaxe):

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

"palavra-chave postfix" faz mais sentido para mim. postfix com um caractere diferente de espaço em branco separando a expressão futura e await também faz sentido para mim, mas não '.' já que isso é para métodos e await não é um método. No entanto, se você quiser await como uma palavra-chave de prefixo, gosto da sugestão @XX de uma Característica ou método que apenas chama await self para aqueles que desejam encadear um monte de coisas juntas (embora provavelmente não poderia nomear o método await ; apenas wait serviria, embora eu acho). Pessoalmente, eu provavelmente acabaria fazendo um await-per-line e não encadearia tanto porque acho cadeias longas menos legíveis, então prefixo ou pós-fixo funcionaria para mim.

[editar] Esqueci que wait já existe e bloqueia o futuro, então elimine esse pensamento.

@roland estou me referindo especificamente a isso, que discute as atribuições de tipo: https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@rolandsteiner então você escreve

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

Quando eu escreveria como:

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

@skade Oh, você quis dizer um comentário diferente do que eu pensei, desculpe. : stick_out_tongue:

@Pzixel É provável que eventualmente Generator::resume assuma um valor e, portanto, yield expr terá um tipo diferente de () .

Portanto, yield também terá um tipo diferente de ! ou () .

@valff , @rolandsteiner Acho improvável que yield retorne o valor de retomada, que é difícil de se ajustar em uma linguagem tipada estaticamente sem tornar a sintaxe do gerador e / ou característica irritante de trabalhar. O protótipo original com argumentos de currículo usava a palavra-chave gen arg para se referir a este argumento, algo assim é muito mais provável de funcionar bem IMO. Desse ponto de vista, yield ainda retornará () portanto, não deve afetar muito a discussão de await .

Acho que await deve ser um operador de prefixo, porque async é (e espero que yield seja um prefixo também). Ou então, use-o como um método regular, usado na posição postfix.

No entanto, eu não entendo toda a história de motociclismo aqui: por que considerar uma palavra-chave postfix onde nenhum outro de Rust a tem? Isso tornaria a linguagem tão estranha.

Além disso, encadeando futuros, eu entendo por que poderia ser interessante. Mas o encadeamento o espera? O que isso significa? Um futuro que retorna um novo futuro? É realmente um idioma tão comum que queremos que Rust tenha esse idioma como cidadão de primeira classe? Se realmente nos importamos com isso, acho que devemos:

  1. Vá para o método (ou seja, foo.await() ). Isso não introduz uma palavra-chave postfix estranha e todos nós sabemos o que significa. Nós também podemos nos conectar com isso.
  2. Se realmente queremos uma palavra-chave / loloperator, podemos resolver essa questão mais tarde.

Além disso, para dar minha opinião pessoal, odeio o await na posição da palavra-chave postfix.

O prefixo await é usado em outros idiomas por uma razão - ele corresponde ao idioma natural. Você não diz "Eu irei esperar sua chegada". Embora tenha escrito JavaScript, posso ser um pouco tendencioso.

Também direi que considero await -chaining superestimado. O valor de async/await sobre combinadores futuros é que ele permite escrever código assíncrono sequencialmente, utilizando as construções de linguagem padrão, como if , match , etc. Você vai querer quebrar suas esperas de qualquer maneira.

Não vamos inventar muita sintaxe mágica para async/await , é apenas uma pequena parte da linguagem. A sintaxe do método proposto (ou seja, foo.await() ) é IMO muito ortogonal às chamadas de método normais e parece muito mágica. A máquina proc-macro está instalada, por que não disfarçá-la atrás dela?

Que tal usar a palavra-chave await em vez de async na definição da função? Por exemplo:

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

Este await fn só pode ser chamado no contexto async :

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

E desugar para let n = (await foo(bar())); .
Então, além da palavra-chave await , o traço Future poderia implementar o método await para usar await lógica na posição postfix, por exemplo:

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

Além disso, alguém pode me explicar a relação com geradores? Estou surpreso que async / await sejam implementados antes dos geradores (mesmo estabilizados).

Os geradores async / await . Não há planos atuais para estabilizá-los e eles ainda exigem um RFC não experimental antes que possam ser. (Pessoalmente, eu adoraria tê-los estabilizados, mas parece provável que eles demorem pelo menos um ou dois anos, provavelmente não serão corrigidos até que async / await esteja estável e tenha havido alguma experiência com isso).

@XX Acho que você está quebrando a semântica de async se usar await na definição da função. Vamos seguir os padrões, não é?

@phaazon Você pode especificar com mais detalhes como isso quebra a semântica de async ?

A maioria das linguagens usa async para introduzir algo que vai ser assíncrono e await espera por isso. Então, ter await vez de async é meio estranho para mim.

@phaazon Não, não, mas adicionalmente. Exemplo completo:

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

se for válido, então será possível implementar o método await para uso em uma cadeia:

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

Na minha opinião, não importa se o postfix await "parece" muito mágico e não está familiarizado com a linguagem. Se adotarmos a sintaxe do método .await() ou .await!() então é apenas uma questão de explicar que Future s tem um método .await() ou .await!() que você também pode usar para aguardá-los nos casos que fizerem sentido. Não é tão difícil de entender se você passar 5 minutos pensando sobre isso, mesmo que nunca tenha visto antes.

Além disso, se usarmos a palavra-chave prefix, o que nos impede de escrever uma caixa com o seguinte (pseudocódigo incompleto porque não tenho a infraestrutura para lidar com os detalhes agora):

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

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

Dessa forma, podemos obter o Postfix Aguardar se quisermos. Quer dizer, mesmo que isso não seja possível e tenhamos que implementar postfix await magicamente pelo compilador, ainda podemos usar algo como o exemplo acima para explicar como ele geralmente funciona. Não seria difícil aprender.

@ivandardi Eu pensei o mesmo. Mas esta definição

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

não diz que a função pode ser chamada apenas no contexto async .

@XX Sim, como eu disse, ignore o pseudocódigo quebrado: PI atualmente não tem a infraestrutura para verificar como a sintaxe e os traços corretos devem ser escritos lá :( Imagine que eu escrevi o que o torna apenas chamável em assíncrono contextos.

@XX , @ivandardi Por definição, await só funciona em um contexto async . Portanto, isso é ilegal:

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

Deve ser

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

Que só pode ser chamado em um contexto async como este:

await future.await()

O que anula todo o propósito.

A única maneira de fazer isso funcionar é mudar async completamente, o que está fora de questão.

@ivandardi Esta evolução da minha versão:

fn await(self) -> T { // How to indicate using in async-context only?
    await self
}

`` `ferrugem
aguarde fn aguarde (self) -> T {// Porque async já foi usado
espera-se
}

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

@CryZe @XX Por que você está me vaiando? Eu estou certo.

@XX O que você está tentando fazer é alterar o significado de assíncrono e aguardar completamente (por exemplo, criar um contexto await que seja de alguma forma diferente de um contexto async ). Não acho que receberia muito suporte dos desenvolvedores de linguagem

@EyeOfPython Esta definição não funciona conforme o esperado:

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

Mais provável:

#[call_only_in_async_context_with_derived_await_prefix]
fn await(self) -> impl Future {
    self
}

Estou rejeitando porque você ignorou que esta é uma pseudo sintaxe hipotética. Essencialmente, o ponto é que Rust poderia adicionar um traço especial como Drop e Copy que a linguagem entende, o que adiciona a habilidade de chamar .await () no tipo que então interage com o tipo assim como uma palavra-chave await faria. Além disso, votar é considerado irrelevante para RFCs, pois o objetivo é descobrir uma solução objetiva, não baseada em sentimentos subjetivos, como os visualizados por votos positivos / negativos.

Não entendo o que este código deve fazer. Mas isso não tem nada a ver com o modo como o async await funciona atualmente. Se você se preocupa com isso, por favor, mantenha-o fora deste tópico e escreva um RFC dedicado para ele.

@CryZe Esse tipo está disponível. Chama-se Futuro. E foi acordado há muito tempo que async / await é um mecanismo que visa APENAS transformar o código assíncrono. Não é uma notação de ligação geral.

@ivandari seu postfix

@ Matthias247 O que eles estavam tentando sugerir era uma maneira de fazer o postfix await ter a sintaxe de uma chamada de método. Dessa forma, o Rust não precisa introduzir novos símbolos arbitrários, como #, @ ou ... como fizemos com o? operador, e ainda parece bastante natural:

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

Portanto, a ideia seria, de alguma forma, esperar ser um tipo especial de método no traço Future que o compilador vê da mesma forma que uma palavra-chave await normal (que você não precisa ter neste caso) e transformar o async código em um gerador a partir daí. Então você tem o fluxo de controle lógico adequado da esquerda para a direita em vez de esquerda direita esquerda com await some_future()? e não precisa introduzir novos símbolos estranhos.

(Então tl; dr: parece uma chamada de método, mas na verdade é apenas um postfix aguardando)

Algo que não vi mencionado explicitamente em consideração a uma sintaxe pós-fixada é a prevalência dos combinadores de erro e opção. Estes são os lugares onde a ferrugem é a mais diferente de todas as línguas que aguardam - em vez de rastreamentos automáticos, temos .map_err()? .

Em particular coisas como:

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

é menos legível do que (não me importa qual sintaxe postfix é considerada):

let parsed = reqwest::get(..)
    .send() await
    .map_err(add_context)?
    .json() await
    .map_err(add_context)?;

_mesmo que_ esta formatação padrão tem mais linhas do que a abordagem multivariável. As expressões que não têm vários awaits parecem que devem ocupar menos espaço vertical com postfix-await.

Eu não escrevo código assíncrono agora, então estou falando por ignorância, desculpe continuar - se isso não é um problema na prática, então isso é excelente. Só não quero que o tratamento de erros adequado seja menos ergonômico, apenas para ser mais semelhante a outras linguagens que fazem o tratamento de erros de maneira diferente.

Sim, é isso que quero dizer. Seria o melhor dos dois mundos, permitindo às pessoas escolher se querem usar o prefixo ou o postfix para esperar, dependendo da situação.

@XX Se isso não puder ser expresso no código do usuário, teremos que recorrer ao compilador para implementar essa funcionalidade. Mas em termos de compreensão de como funciona o postfix, a explicação que postei anteriormente ainda funciona, mesmo que seja apenas um entendimento superficial.

@CryZe , @XX ,

À primeira vista, ele solta suavemente, mas absolutamente não corresponde à filosofia Rust. Mesmo o método sort em iteradores não retorna self e quebra a cadeia de chamada de método para tornar a alocação explícita. Mas você espera que algo não menos impactante seja implementado de maneira completamente implícita, indistinguível das chamadas de função regulares. Não há chances IMO.

Sim, é isso que quero dizer. Seria o melhor dos dois mundos, permitindo às pessoas escolher se querem usar o prefixo ou o postfix para esperar, dependendo da situação.

Por que isso deveria ser uma meta? A ferrugem não suporta isso para todos os outros fluxos de controle (por exemplo, break , continue , if , etc. também. Não há um motivo específico, além de "isso pode parece melhor em sua opinião particular.

Em geral, gostaria de lembrar a todos que await é muito especial e não uma invocação de método normal:

  • Os depuradores podem se comportar de maneira estranha ao fazer uma espera, pois a pilha pode se desenrolar e ser restabelecida. Pelo que me lembro, demorou muito para que a depuração assíncrona funcionasse em C # e Javascript. E esses têm equipes pagas que trabalham em depuradores!
  • Objetos locais que não passam pelos pontos de espera podem ser armazenados na pilha real do SO. Aqueles que não são devem ser movidos para o futuro gerado, o que no final fará a diferença na memória heap necessária (que é onde o futuro viverá).
  • Os empréstimos em pontos de espera são a razão pela qual os futuros gerados precisam ser !Unpin e causam muitos transtornos com alguns dos combinadores e mecanismos existentes. Pode ser potencialmente possível gerar Unpin futuros a partir de async métodos que não contraem empréstimos em espera no futuro. No entanto, se await s forem invisíveis, isso nunca acontecerá.
  • Pode haver outros problemas inesperados do verificador de empréstimo, que não são cobertos pelo estado atual de async / await.

@Pzixel @lnicola

Os exemplos que você dá para C # são totalmente imperativos, e nada como as cadeias longas de estilo funcional que costumamos ver no Rust. Essa sintaxe LINQ é apenas uma DSL e não é nada como foo().bar().x().wih_boo(x).camboom().space_flight(); Pesquisei um pouco no Google e os exemplos de código C # parecem 100% imperativos, assim como a maioria das linguagens de programação populares. É por isso que não podemos simplesmente pegar o que a Langue X fez, porque simplesmente não é a mesma coisa.

A notação de prefixo IMO se encaixa perfeitamente no estilo imperativo de codificação. Mas Rust suporta os dois estilos.

@skade

Não concordo com alguns comentários seus (e outros) sobre problemas com estilo funcional. Para ser breve - vamos apenas concordar que existem pessoas com forte preferência por um ou outro.

@ Matthias247 Não, há um motivo específico diferente de como ele parece bom. É porque Rust incentiva o encadeamento. E você pode dizer "oh, outros mecanismos de fluxo de controle não têm uma sintaxe postfix, então por que esperar ser especial?". Bem, essa é uma avaliação errada. Nós TEMOS fluxo de controle postfix. Eles são apenas internos aos métodos que chamamos. Option::unwrap_or é como fazer uma declaração de correspondência postfix. Iterator::filter é como fazer um postfix if. Só porque o fluxo de controle não faz parte da sintaxe de encadeamento em si, não significa que ainda não temos fluxo de controle postfix. Nesse ponto de vista, adicionar um postfix await seria realmente consistente com o que temos. Neste caso, poderíamos até ter algo mais semelhante a como funciona Iterator e em vez de apenas ter um postfix bruto aguardar, poderíamos ter alguns combinadores aguardar, como Future::await_or . De qualquer forma, ter o Postfix Aguardando não é apenas uma questão de aparência - é uma questão de funcionalidade e qualidade de vida. Caso contrário, ainda estaríamos usando a macro try!() , certo?

@dpc

Os exemplos que você dá para C # são totalmente imperativos, e nada como as cadeias longas de estilo funcional que costumamos ver no Rust. Essa sintaxe LINQ é apenas uma DSL e não é nada como foo (). Bar (). X (). Wih_boo (x) .camboom (). Space_flight (); Pesquisei um pouco no Google e os exemplos de código C # parecem 100% imperativos, assim como a maioria das linguagens de programação populares. É por isso que não podemos simplesmente pegar o que a Langue X fez, porque simplesmente não é a mesma coisa.

Não é verdade (você pode verificar qualquer estrutura, por exemplo, polly ), mas não vou discutir sobre isso. Eu vejo tantos códigos encadeados em ambas as linguagens. No entanto, na verdade existe algo que torna tudo diferente. E é chamado de Try trait. C # não tem nada semelhante a ele, então não supõe fazer nada além de await ponto. Se você obtiver uma exceção, ela será automaticamente encapsulada e gerada para o chamador.

Não é o caso do Rust, onde você deve usar manualmente o operador ? .

Se você tiver que ter algo além de await ponto, você deve criar o operador "combinado" await? , estender as regras de linguagem etc etc OU você pode apenas fazer o postfix de esperar e as coisas se tornarem mais naturais (exceto sintaxe um pouco estranha, mas quem se importa?).

Portanto, atualmente vejo o Postfix Aguardar como uma solução mais viável. A única sugestão que tenho deve ser uma palavra-chave dedicada separada por espaço, não await() ou await!() .

Acho que encontrei um bom motivo para justificar um método normal .await (). Se você pensar bem, não é diferente de qualquer outra forma de bloqueio. Você chama o método .await (), a execução do thread (possivelmente verde) é interrompida e, em algum ponto, o tempo de execução em execução retoma a execução do thread em algum ponto. Portanto, para todos os efeitos, quer você tenha um canal Mutex ou std como este:

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

ou um futuro como este:

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

não é diferente. Ambos bloqueiam a execução em seus respectivos recv () / await () e ambos encadeiam da mesma forma. A única diferença é que await () está possivelmente rodando em um executor diferente que não é o threading pesado do SO (mas pode muito bem ser no caso de um único executor threaded). Portanto, desencorajar grandes quantidades de encadeamento afeta exatamente a mesma coisa e provavelmente deve levar a um fiapo cortado real.

No entanto, meu ponto aqui é que, do ponto de vista da pessoa que está escrevendo este código, não há muita diferença em .recv () ou .await (), ambos são métodos que bloqueiam a execução e retornam assim que o resultado estiver disponível . Portanto, para todos os efeitos, pode ser praticamente um método normal e não requer uma palavra-chave completa. Não temos uma palavra-chave recv ou lock para o canal Mutex e std.

No entanto, o rustc obviamente quer transformar todo o código em um gerador. Mas isso é realmente semanticamente necessário do ponto de vista da linguagem? Tenho certeza de que seria possível escrever um compilador alternativo para rustc que implemente o await por meio do bloqueio de um thread de sistema operacional real (inserir um fn assíncrono precisaria gerar um thread e, em seguida, aguardar bloquearia esse thread). Embora fosse uma implementação extremamente ingênua e lenta, semanticamente se comportaria da mesma maneira. Portanto, o fato de que o rustc real se transforma em um gerador não é necessário semanticamente. Portanto, eu diria que rustc transformar a chamada para o método .await () em um gerador pode ser visto como um detalhe de implementação de otimização de rustc. Dessa forma, você pode justificar .await () sendo uma espécie de método completo e não uma palavra-chave completa, mas também ter rustc transformando tudo em um gerador.

@CryZe, ao contrário de .recv() , podemos interromper await com segurança de qualquer outro lugar no programa e então o código próximo a await não seria executado. Essa é uma grande diferença e essa é a razão mais valiosa pela qual não devemos fazer await implicitamente.

@ I60R Não é menos explícito do que esperar como palavra-chave. Você ainda pode destacá-lo da mesma maneira no IDE, se desejar. Ele apenas move a palavra-chave para a posição pós-fixada, onde eu diria que é realmente menos fácil de perder (pois é exatamente na posição onde a execução é interrompida).

Idéia maluca: implícita espera . Mudei para um tópico separado, porque este já está muito ocupado com a discussão de postfix vs prefix.

@CryZe

Acho que encontrei um bom motivo para justificar um método normal .await (). Se você pensar bem, não é diferente de qualquer outra forma de bloqueio.

Leia https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515 novamente

@ Matthias247 Estes são pontos muito bons, parece que perdi aqueles.

Toda essa conversa sobre transformar o await em uma função, membro ou outro implícito é fundamentalmente inválido. Não é uma ação, é uma transformação.

O compilador, em um alto nível, seja por meio de uma palavra-chave ou macro, transforma expressões de espera em expressões de rendimento de gerador especiais, no local, contabilizando empréstimos e outras coisas ao longo do caminho.

Isso deve ser explícito.

Deve ser o mais aparente possível como uma palavra-chave ou, obviamente, gerar código com uma macro.

Métodos mágicos, características, membros, etc., são muito fáceis de interpretar mal e mal interpretar, especialmente para novos usuários.

A palavra-chave de prefixo await ou a macro de prefixo await parecem ser a maneira mais aceitável de fazer isso, o que faz sentido, já que muitas outras linguagens fazem dessa forma. Não precisamos ser especiais para torná-lo bom.

@novacrazy

Métodos mágicos, características, membros, etc., são muito fáceis de interpretar mal e mal interpretar, especialmente para novos usuários.

Você pode expandir isso? Mais especificamente na variante .await!() , se possível.

Ter um foo.await!() não é difícil de ler, na minha opinião, ESPECIALMENTE com realce de sintaxe adequado, que não deve ser ignorado.

Não entendeu o significado disso? O que faz é essencialmente o seguinte (ignore o tipo mystakes):

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

AKA await this_foo e this_foo.await!() são exatamente iguais. O que é fácil de entender mal sobre isso?

E no tópico de novos usuários: novos usuários para quê? Programação em geral ou novos usuários Rust como linguagem, mas com experiência em linguagem de programação? Porque se for o primeiro, então eu duvido que eles se interessem por programação assíncrona imediatamente. E se for o último, então é mais fácil explicar a semântica do postfix await (como explicado acima) sem confusão.

Alternativamente, se apenas o prefixo await for adicionado, nada que eu saiba interrompe a criação de um programa que recebe como entrada o código Rust da forma

foo.bar().baz().quux().await!().melo().await!()

e o transforma em

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() é bom para alguns casos, e provavelmente funcionaria junto com await!(...) como:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

No entanto, macros de método postfix não existem agora e podem nunca existir.

Se acharmos que é uma possibilidade no futuro, devemos ir com a macro await!(...) por enquanto e simplesmente adicionar o postfix no futuro, quando isso for implementado.

Ter as duas macros seria o ideal, mas se não houver intenção de implementar macros pós-fixadas no futuro, a palavra-chave de prefixo await é provavelmente a melhor aposta.

@novacrazy Posso concordar com isso e é minha proposta original. Devemos adicionar await!() por enquanto e descobrir uma forma de postfix conforme avançamos. Potencialmente, também discuta a possibilidade de macros postfix também, e se pudéssemos ad-hoc um postfix .await!() no idioma antes de ter suporte total a macro postfix no idioma. Mais ou menos como o que aconteceu com ? e o traço Try : ele foi adicionado primeiro como um caso especial e depois foi expandido para um caso mais geral. A única coisa que precisamos ter cuidado ao decidir é como seria a sintaxe geral da macro pós-fixada, que pode merecer uma discussão separada.

A palavra-chave de prefixo await ou a macro de prefixo await parecem ser a maneira mais aceitável de fazer isso, o que faz sentido, já que muitas outras linguagens fazem dessa forma.

É obviamente _executável_, mas só me lembro de dois argumentos para isso, e não os considero convincentes:

  • É assim que outras línguas fazem

    • Isso é bom, mas já estamos fazendo as coisas de forma diferente, como não funcionar a menos que poll ed em vez de correr até o primeiro- await , porque a ferrugem é fundamentalmente diferente língua

  • As pessoas gostam de ver await no início da linha

    • Mas nem sempre está lá, então, se esse for o único lugar para se olhar, terá problemas

    • É tão fácil ver os await s em foo(aFuture.await, bFuture.await) como se fossem prefixos

    • Na ferrugem, um já está escaneando o _end_ da linha para ? se você estiver olhando para o fluxo de controle

Perdi algo?

Se o debate fosse "meh, eles são todos iguais", eu concordaria absolutamente com "bem, se realmente não nos importamos, podemos também fazer o que todo mundo faz". Mas não acho que é onde estamos.

@scottmcm Sim para, "meh, eles são todos iguais." Pelo menos da perspectiva do usuário.

Portanto, devemos encontrar um equilíbrio decente entre legibilidade, familiaridade, facilidade de manutenção e desempenho. Portanto, minha afirmação em meu último comentário é verdadeira, com macro await!() se pretendemos adicionar macros de método postfix ( .await!() ) no futuro, ou apenas uma palavra-chave de prefixo chata await caso contrário.

Eu digo chato, porque chato é bom. Queremos manter nossa mente longe da sintaxe em si ao escrever código com eles.

Se f.await() não for uma boa ideia, então prefiro a sintaxe de prefixo.

  1. Como usuário, espero que a linguagem que uso tenha apenas algumas regras de sintaxe e, usando essas regras, posso inferir com segurança o que ela está fazendo. NÃO exceções aqui e ali. Em Rust, async está no início, await no início não será uma exceção. No entanto, f await , forma de palavra-chave post seria. f.await parece com acesso de campo, uma exceção . f.await!() tem macro postfix nunca apareceu na linguagem, e não sei para quais outros casos ela seria adequada, uma exceção . Não temos uma resposta de como essas sintaxes se tornariam regras, e não exceções únicas .

  2. Quando uma exceção é feita, espero que faça sentido intuitivamente. Tome ? como exemplo, que pode ser visto como uma exceção porque não é frequente em outras línguas. f()?.map() lê quase como compute f () e este resultado é bom? O ? aqui se explica. Mas para f await eu pergunto por que é postfix ?, para f.await eu pergunto se é await um campo, f.await!() eu pergunto por que a macro aparece nessa posição? Eles não fornecem um sentido convincente / intuitivo para mim, pelo menos à primeira vista.

  3. Estendendo o primeiro ponto, Rust gostaria muito de ser uma linguagem de sistemas. Os principais jogadores aqui, C / C ++ / Go / Java são todos imperativos. Também acredito que a maioria dos caras começa sua carreira com uma linguagem imperativa, C / Python / Java, não Haskell, etc. Acho que, para persuadir os desenvolvedores de sistemas e desenvolvedores de próxima geração a adotar o Rust, o Rust deve primeiro se sair bem no estilo imperativo e depois funcional, não sendo altamente funcional, mas não tem um sentimento imperativo familiar.

  4. Acho que não há nada de errado em dividir uma cadeia e escrevê-la em várias linhas. Não será prolixo. É apenas explícito .

Ao ver como a discussão se move continuamente de uma direção para outra (prefixo vs pós-fixo), desenvolvi uma opinião forte de que há algo errado com a palavra-chave await e devemos nos afastar completamente dela. Em vez disso, proponho a seguinte sintaxe que acho que será um bom compromisso entre todas as opiniões que foram publicadas no tópico atual:

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

Na primeira etapa, nós o implementaríamos como um operador de prefixo regular, sem nenhum erro ao lidar com superestruturas específicas:

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

Na segunda etapa, implementaríamos a sintaxe do operador de prefixo diferido que também permitiria o tratamento de erros adequado.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

Isso é tudo.


Agora vamos ver alguns exemplos extras que fornecem mais contexto:


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

Está escondido na versão spoiler com destaque de sintaxe habilitado


// 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, esta sintaxe é melhor do que alternativas em todos os aspectos:

✓ Consistência: parece muito orgânico dentro do código Rust e não requer quebra de estilo de código
✓ Composibilidade: integra-se bem com outros recursos do Rust, como encadeamento e tratamento de erros
✓ Simplicidade: é curto, descritivo, fácil de entender e fácil de trabalhar
✓ Reutilização: a sintaxe do operador de prefixo diferido também seria útil em outros contextos
✓ Documentação: implica que os despachos do lado da chamada fluem em algum lugar e espera até que ele retorne
✓ Familiaridade: fornece um padrão já familiar, mas faz isso com menos vantagens e desvantagens
✓ Legibilidade: é lido como um inglês simples e não distorce o significado das palavras
✓ Visibilidade: sua posição torna muito difícil ser disfarçado em algum lugar do código
✓ Acessibilidade: pode ser facilmente pesquisado no Google
✓ Bem testado: o golang é popular hoje em dia com uma sintaxe de aparência semelhante
✓ Surpresas: é difícil não entender bem como abusar dessa sintaxe
✓ Gratificação: após um pequeno aprendizado, cada usuário ficará satisfeito com o resultado


Edit: como @ivandardi apontou no comentário abaixo, alguns pontos devem ser esclarecidos:

1. Sim, essa sintaxe é um pouco difícil de analisar, mas ao mesmo tempo é impossível inventar uma sintaxe aqui que não tivesse problemas de legibilidade. go sintaxe para mim parece ser menos maléfica aqui, já que na posição do prefixo é exatamente a mesma que com o prefixo await , e na posição deferida é IMO mais legível do que o pós-fixado await por exemplo:

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

onde não await parece ambíguo e tem uma associatividade diferente de todas as palavras-chave existentes, mas também pode se tornar um bloqueador se decidirmos implementar blocos de await em algum momento no futuro.

2. Exigirá a criação de RFC, implementação e estabilização da sintaxe do "operador de prefixo adiado". Essa sintaxe não seria específica para código assíncrono, no entanto, usá-lo em assíncrono seria a principal motivação para isso.

3. Na sintaxe do "operador de prefixo adiado", a palavra-chave é importante porque:

  • a palavra-chave longa aumentaria o espaço entre a variável e o método, o que é ruim para a legibilidade
  • palavra-chave longa é escolha estranha para operador de prefixo
  • a palavra-chave longa é desnecessariamente detalhada quando queremos que o código assíncrono pareça sincronizado

De qualquer forma, é bem possível começar a usar await vez de go e renomeá-lo na edição de 2022. Naquela época, é muito provável que uma palavra-chave ou operador diferente seja inventado. E podemos até decidir que nenhuma renomeação é necessária. Descobri que await pode ser lido.

Está escondido sob a versão de spoiler com await usado em vez de 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()?;


Se você está aqui para votar negativamente, certifique-se de:

  • que você entendeu corretamente, uma vez que posso não ser o melhor orador para explicar coisas novas
  • que sua opinião não é tendenciosa, sine, há muitos preconceitos de onde essa sintaxe se origina
  • que você colocará um motivo válido abaixo, uma vez que votos negativos silenciosos são extremamente tóxicos
  • que sua razão não é sobre sentimentos imediatos, já que tentei projetar um recurso de longo prazo aqui

@ I60R

Eu tenho algumas queixas com isso.

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

Isso é um pouco difícil de analisar. À primeira vista, você pensaria que estaríamos combinando db.go , mas então há algo mais nisso e você diz "oooh, ok, carregar é um método de db". IMO, essa sintaxe basicamente não funcionará porque os métodos devem sempre estar próximos ao objeto ao qual pertencem, sem espaço e interrupção de palavra-chave entre eles.

Em segundo lugar, sua proposta requer definir o que é "sintaxe de operador de prefixo diferido" e, possivelmente, generalizá-la na linguagem primeiro. Caso contrário, será algo usado apenas para código assíncrono, e estamos de volta à discussão de "por que não corrigir macros?" também.

Em terceiro lugar, não acho que a palavra-chave tenha importância alguma. No entanto, devemos favorecer await porque a palavra-chave foi reservada para a edição de 2018, enquanto a palavra-chave go não foi e deve passar por uma pesquisa crates.io mais profunda antes de ser proposta.

Ao ver como a discussão se move continuamente de uma direção para outra (prefixo vs pós-fixo), desenvolvi uma forte opinião de que há algo errado com a palavra-chave await e devemos fugir completamente dela.

Poderíamos ter (semi-) esperar implícito e tudo ficaria bem, mas vai ser difícil de vender na comunidade Rust, mesmo apesar do fato de que todo o objetivo das funções assíncronas é ocultar os detalhes de implementação e torná-los justos como bloquear io um.

@dpc Quer dizer, a melhor maneira que posso pensar sobre isso funciona como um meio-termo entre espera semi-implícita e o desejo de ter seções de código explicitamente aguardadas é algo semelhante a unsafe .

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

Onde tudo dentro do bloco await seria automaticamente aguardado. Isso resolve a necessidade de postfix await para encadeamento, já que você pode simplesmente aguardar a cadeia inteira. Mas, ao mesmo tempo, não tenho certeza se essa forma seria desejável. Talvez outra pessoa possa encontrar um bom motivo contra isso.

@dpc O objetivo das funções assíncronas não é ocultar os detalhes, mas torná-los mais acessíveis e fáceis de usar. Nunca devemos ocultar operações potencialmente caras e devemos aproveitar as vantagens de futuros explícitos sempre que possível.

Ninguém confunde funções assíncronas com síncronas, em qualquer idioma. É puramente para organizar sequências de operações de uma forma mais legível e sustentável, em vez de escrever uma máquina de estado à mão.

Além disso, esperas implícitas, conforme mostrado no exemplo de Future , que, como já mostrei, ainda são incrivelmente poderosos. Não podemos simplesmente pegar qualquer Future e esperar por ele, isso seria altamente ineficiente.

Explícito, familiar, legível, sustentável e com desempenho: palavra-chave de prefixo await ou macro await!() , com macro potencialmente pós-fixada .await!() no futuro.

@novacrazy Mesmo que a sintaxe ideal fosse escolhida, eu ainda gostaria de usar futuros e combinadores personalizados em algumas situações. A ideia de que aqueles poderiam ser descontinuados me faz questionar toda a direção de Rust.

Claro que você pode continuar a usar combinadores, nada está sendo preterido.

É a mesma situação que ? : o novo código normalmente usará ? (porque é melhor que os combinadores), mas os combinadores Opção / Resultado ainda podem ser usados ​​(e os mais funk como or_else ainda são usados ​​regularmente).

Em particular, async / await pode substituir completamente map e and_then , mas os combinadores Future mais funk podem (e serão) usados.

Os exemplos que você dá ainda parecem terríveis em comparação com os combinadores,

Acho que não, acho que eles são muito mais claros, porque usam recursos padrão do Rust.

A clareza raramente se refere ao número de caracteres, mas tem tudo a ver com conceitos mentais.

Com o postfix await parece ainda melhor:

some_op().await?
    .another_op().await?
    .final_op().await

Você pode comparar isso ao seu original:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

e com a sobrecarga do gerador provavelmente será um pouco mais lento e produzirá mais código de máquina.

Por que você afirma que haverá sobrecarga extra de assíncrono / espera? Os geradores e async / await são projetados especificamente para ter custo zero e serem altamente otimizados.

Em particular, async / await compila em uma máquina de estado altamente otimizada, da mesma forma que os combinadores Future.

em vez de criar essas máquinas geradoras de estado espaguete sob o capô.

Os combinadores do Future também criam uma "máquina de estado espaguete" sob o capô, que é exatamente para o que foram projetados.

Eles não são fundamentalmente diferentes de async / await.

[Futuros combinadores] têm o potencial de compilar para um código mais eficiente.

Por favor, pare de espalhar desinformação. Em particular, async / await tem a possibilidade de ser mais rápido do que os combinadores Future.

Se descobrir que os usuários não estão satisfeitos com uma palavra-chave de prefixo, ela pode ser alterada em uma edição 2019/2020.

Como outros já disseram, as edições não são algo arbitrário que simplesmente fazemos sempre que temos vontade, elas são projetadas especificamente para acontecer apenas a cada poucos anos (no mínimo).

E como @Centril apontou, criar uma sintaxe incorreta apenas para substituí-la mais tarde é uma maneira muito ineficiente de fazer as coisas.

O ritmo da discussão está bastante alto agora; então, em um esforço para desacelerar as coisas e permitir que as pessoas acompanhem, bloqueei o problema temporariamente . Ele será desbloqueado em um dia. Nesse momento, tente ser o mais construtivo possível no futuro.

Desbloqueando o problema um dia depois ... Considere não fazer comentários já feitos e mantenha os comentários sobre o tópico (por exemplo, este não é o lugar para considerar espera implícita ou outras coisas fora do escopo ..).

Como este tem sido um longo tópico, vamos destacar alguns dos comentários mais interessantes enterrados nele.

@mehcode nos forneceu um código extenso do mundo real com await nas posições de prefixo e postfix: https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

@Centril argumentou de forma persuasiva que estabilizar await!(expr) equivale a conscientemente adicionar dívida técnica: https://github.com/rust-lang/rust/issues/57640#issuecomment -455806584

@valff nos lembrou que a sintaxe da palavra-chave postfix expr await se encaixaria mais perfeitamente com a atribuição de tipo generalizada : https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@quodlibetor destacou que o efeito sintático dos combinadores Error e Option é exclusivo do Rust e argumenta a favor da sintaxe postfix: https://github.com/rust-lang/rust/issues/57640#issuecomment -456143523

No final do dia, a equipe lang vai precisar fazer uma ligação aqui. No interesse de identificar um terreno comum que possa levar a um consenso, talvez seja útil resumir as posições declaradas dos membros da equipe lang sobre a questão-chave da sintaxe postfix:

  • @Centril expressou suporte para sintaxe postfix e explorou uma série de variações neste tíquete. Particularmente, a Centril não quer estabilizar await!(expr) .

  • @cramertj expressou suporte para sintaxe postfix e especificamente para a sintaxe de palavra-chave expr await postfix.

  • @joshtriplett expressou suporte para sintaxe postfix e sugeriu que também deveríamos fornecer uma versão prefixada .

  • @scottmcm expressou suporte para sintaxe postfix.

  • @withoutboats não quer estabilizar uma sintaxe macro. Embora preocupado em exaurir nosso "orçamento de falta de familiaridade", sem barcos considera a sintaxe da palavra-chave do postfix expr await tendo "um tiro real".

  • @aturon , @eddyb , @nikomatsakis e @pnkfelix ainda não expressaram qualquer posição em relação à sintaxe nas edições # 57640 ou # 50547.

Então, o que precisamos para descobrir uma resposta sim / não para:

  • Devemos ter sintaxe de prefixo?
  • Devemos ter sintaxe postfix?
  • Devemos ter a sintaxe de prefixo agora e descobrir a sintaxe de pós-fixada mais tarde?

Se formos com a sintaxe de prefixo, há dois concorrentes principais que acho que as pessoas aceitam mais: Útil e Óbvio, como visto neste comentário . Os argumentos contra await!() são muito fortes, então estou considerando isso descartado. Portanto, precisamos descobrir se queremos sintaxe útil ou óbvia. Em minha opinião, devemos escolher qualquer sintaxe que use menos parênteses em geral.

E quanto à sintaxe pós-fixada, isso é mais complicado. Existem também argumentos fortes para a palavra-chave pós-fixada await com espaço. Mas isso depende muito de como o código é formatado. Se o código estiver mal formatado, a palavra-chave postfix await com espaço pode parecer muito ruim. Portanto, primeiro, precisaríamos presumir que todo o código Rust será formatado corretamente com rustfmt e que rustfmt sempre dividirá as esperas encadeadas em linhas diferentes, mesmo se caberem em uma única linha. Se pudermos fazer isso, então essa sintaxe está muito boa, pois resolve o problema de legibilidade e confusão com espaços no meio de uma cadeia de uma linha.

E, finalmente, se formos com prefixo e pós-fixados, precisamos descobrir e escrever a semântica de ambas as sintaxes para ver como elas interagem entre si e como uma seria convertida na outra. Deve ser possível fazê-lo, já que a escolha entre postfix ou prefix await não deve alterar a forma como o código é executado.

Para esclarecer meu processo de pensamento, e como eu abordo e avalio as propostas de sintaxe de superfície escritas. await ,
aqui estão os objetivos que tenho (sem nenhuma ordem de importância):

  1. await deve permanecer uma palavra-chave para permitir o futuro design de linguagem.
  2. A sintaxe deve parecer de primeira classe.
  3. O Awaiting deve ser encadeado para compor bem com ? e métodos em geral, uma vez que são predominantes no Rust. Não se deve ser forçado a fazer ligações let temporárias, que podem não ser subdivisões significativas.
  4. Deve ser fácil grep para aguardar pontos.
  5. Deve ser possível ver os pontos de espera de relance.
  6. A precedência da sintaxe deve ser intuitiva.
  7. A sintaxe deve combinar bem com IDEs e memória muscular.
  8. A sintaxe deve ser fácil de aprender.
  9. A sintaxe deve ser ergonômica para escrever.

Tendo definido (e provavelmente esquecido ...) alguns dos meus objetivos, aqui está um resumo e minha avaliação de algumas propostas a respeito deles:

  1. Manter await como uma palavra-chave torna difícil usar uma sintaxe baseada em macro, seja await!(expr) ou expr.await!() . Para usar uma sintaxe de macro, await como macro fica codificado e não integrado com a resolução de nome (ou seja, use core::await as foo; torna-se impossível) ou await é abandonado como uma palavra-chave inteiramente.

  2. Eu acredito que as macros têm um sentimento distinto de não ser de primeira classe sobre elas, uma vez que são destinadas a inventar sintaxe no espaço do usuário. Embora não seja um ponto técnico, o uso da sintaxe macro em uma construção central da linguagem dá uma impressão pouco polida. Indiscutivelmente, as sintaxes .await e .await() também não são as sintaxes de primeira classe; mas não tão de segunda classe quanto as macros pareceriam.

  3. O desejo de facilitar o encadeamento faz com que qualquer sintaxe de prefixo funcione mal, enquanto as sintaxes pós-fixadas naturalmente compõem com cadeias de métodos e ? em particular.

  4. O grepping é mais fácil quando a palavra-chave await é usada, seja ela pós-fixada, prefixo, macro, etc. Quando uma sintaxe baseada em sigilo é usada, por exemplo, # , @ , ~ , o grep torna-se mais difícil. A diferença não é grande, mas .await é ligeiramente mais fácil de fazer grep do que await e await já que qualquer um deles pode ser incluído como palavras nos comentários, enquanto é mais improvável para .await .

  5. Sigilos são provavelmente mais difíceis de detectar de relance, enquanto await é mais fácil de ver e especialmente a sintaxe destacada. Foi sugerido que .await ou outras sintaxes de postfix são mais difíceis de detectar de relance ou que o postfix await oferece baixa legibilidade. No entanto, na minha opinião, @scottmcm corretamente observa que os resultados futuros são comuns e que .await? ajuda a chamar atenção extra para si mesmo. Além disso, Scott observa que o prefixo await leva a sentenças de caminho de jardim e que o prefixo esperar no meio das expressões não é mais legível do que o pós-fixo. Com o advento do operador ? , os programadores do Rust já precisam verificar o final das linhas para procurar o fluxo de controle .

  6. A precedência de .await e .await() são notáveis ​​por serem completamente previsíveis; eles funcionam como suas contrapartes de acesso de campo e chamada de método. Uma macro pós-fixada provavelmente teria a mesma precedência que uma chamada de método. Enquanto isso, o prefixo await tem uma precedência consistente, previsível e não útil em relação a ? (ou seja, await (expr?) ), ou a precedência é inconsistente e útil (ou seja, (await expr)? ). Um sigilo pode receber a precedência desejada (por exemplo, tomando sua intuição de ? ).

  7. Em 2012, @nikomatsakis notou que Simon Peyton Jones notou uma vez (p. 56) que ele tem inveja do "poder do ponto" e como ele fornece magia IDE, por meio da qual você pode restringir a função ou campo que você quer dizer. Como o poder do ponto existe em muitas linguagens populares (por exemplo, Java, C #, C ++, ..), isso fez com que "alcançar o ponto" fosse enraizado na memória muscular. Para ilustrar o quão poderoso é esse hábito, aqui está uma captura de tela, devido a @scottmcm , do Visual Studio com Re # er:
    Re#er intellisense

    C # não tem espera de postfix. No entanto, isso é tão útil que é mostrado em uma lista de preenchimento automático sem ser uma sintaxe válida. No entanto, pode não ocorrer a muitos, inclusive a mim, tentar .aw quando .await não é a sintaxe de superfície. Considere o benefício para a experiência IDE, se fosse. As sintaxes await expr e expr await não oferecem esse benefício.

  8. Sigilos provavelmente ofereceriam pouca familiaridade. O prefixo await traz o benefício da familiaridade com C #, JS, etc. No entanto, eles não separam await e ? em operações distintas, enquanto o Rust o faz. Também não é um exagero ir de await expr para expr.await - a mudança não é tão radical quanto ir com um sigilo. Além disso, devido ao ponto-power mencionado acima, é provável que .await seja aprendido digitando expr. e vendo await como a primeira opção no pop-up de preenchimento automático.

  9. Sigilos são fáceis de escrever e oferecem boa ergonomia. No entanto, embora .await seja mais longo para digitar, ele também vem com poderes de ponto que podem tornar o fluxo de escrita ainda melhor. Não ter que quebrar em let declarações também facilita uma melhor ergonomia. Prefixo await , ou pior ainda await!(..) , não possui poderes de pontos e habilidades de encadeamento. Ao comparar .await , .await() e .await!() , a primeira oferta é a mais concisa.

Como nenhuma sintaxe é a melhor para atingir todos os objetivos simultaneamente, deve-se fazer uma troca. Para mim, a sintaxe com mais benefícios e menos desvantagens é .await . Notavelmente, ele pode ser encadeado, preserva await como uma palavra-chave, é greppable, tem poderes de ponto e, portanto, pode ser aprendido e ergonômico, tem precedência óbvia e, finalmente, é legível (especialmente com boa formatação e destaque).

@Centril Só para esclarecer algumas dúvidas. Em primeiro lugar, gostaria de confirmar que você deseja apenas aguardar o postfix, certo? Em segundo lugar, como abordar a dualidade de .await sendo um acesso de campo? Estaria ok ter .await como ele, ou .await() com o parêntese ser favorecido de forma a implicar que algum tipo de operação está acontecendo lá? E em terceiro lugar, com a sintaxe .await , seria esperar ser a palavra-chave ou apenas um identificador de "acesso ao campo"?

Em primeiro lugar, gostaria de confirmar que você deseja apenas aguardar o postfix, certo?

Sim. Agora, pelo menos.

Em segundo lugar, como abordar a dualidade de .await sendo um acesso de campo?

É uma pequena desvantagem; há uma grande vantagem no poder do ponto. A distinção é algo que acredito que os usuários aprenderão rapidamente, especialmente porque é uma palavra-chave destacada e a construção será usada com frequência. Além disso, como Scott observou, .await? será o mais comum, o que deve aliviar ainda mais a situação. Também deve ser fácil adicionar uma nova entrada de palavra-chave para await em rustdoc, da mesma forma que fizemos para, digamos, fn .

Estaria ok ter .await como ele, ou .await() com o parêntese ser favorecido de forma a implicar que algum tipo de operação está acontecendo lá?

Eu prefiro .await sem cauda () ; ter () no final parece um sal desnecessário na maioria dos casos e, imagino, deixaria os usuários mais inclinados a procurar o método.

E em terceiro lugar, com a sintaxe .await , seria esperar ser a palavra-chave ou apenas um identificador de "acesso ao campo"?

await permaneceria uma palavra-chave. Presumivelmente, você alteraria a libsyntax de forma a não errar ao encontrar await após . e, em seguida, representaria de forma diferente em AST ou ao reduzir para HIR ... mas isso é principalmente um detalhe de implementação .

Obrigado por esclarecer tudo isso! 👍

Neste ponto, a questão principal parece ser se devemos seguir uma sintaxe pós-fixada. Se essa for a direção, então certamente podemos definir qual. Lendo a sala, a maioria dos defensores da sintaxe pós-fixada aceitaria qualquer sintaxe pós-fixada razoável sobre a sintaxe de prefixo.

Entrando neste tópico, a sintaxe pós-fixada parecia estar em desvantagem. No entanto, atraiu o apoio claro de quatro membros da equipe lang e a abertura de um quinto (os outros quatro permaneceram em silêncio até agora). Além disso, atraiu um apoio considerável da comunidade mais ampla que comentou aqui.

Além disso, os defensores da sintaxe postfix parecem ter uma clara convicção de que é o melhor caminho para o Rust. Parece que algum problema profundo ou refutações convincentes aos argumentos até agora apresentados seriam necessários para voltar atrás.

Diante disso, parece que precisamos ouvir @withoutboats , que certamente tem seguido esse tópico de perto, e dos outros quatro membros da equipe lang. As opiniões deles provavelmente irão impulsionar este tópico. Se eles tiverem preocupações, discuti-las seria a prioridade. Se eles estiverem convencidos do caso da sintaxe pós-fixada, então podemos prosseguir para encontrar um consenso para qual.

Opa, acabei de notar que o @Centril já comentou, então irei esconder meu post para limpeza.

@ivandardi Dado o objetivo (1) do post, meu entendimento é que await ainda seria uma palavra-chave, então nunca seria um acesso de campo, da mesma forma que loop {} nunca é uma expressão literal de struct. E eu esperava que fosse destacado para tornar o mais óbvio (como https://github.com/rust-lang/rust-enhanced/issues/333 está esperando para fazer no Sublime).

Minha única preocupação é que fazer a sintaxe de await parecer um acesso diferente pode causar confusão em uma base de código da edição 2015 sem perceber. (2015 é o padrão quando não especificado, abrindo o projeto de outra pessoa, etc.) Contanto que a edição de 2015 tenha um erro claro quando não há um campo await (e aviso se houver), eu acho isso (junto com o argumento maravilhoso de Centril) elimina minhas preocupações pessoais sobre um campo de palavra-chave (ou método).

Ah, e uma coisa que também devemos decidir enquanto estamos nisso é como rustfmt acabaria formatando-o.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Usa await - cheque
  2. Primeira classe - verificar
  3. Encadeamento - verificar
  4. Grepping - verificar
  5. Sem sigilo - verificar
  6. Precedência - ¹check
  7. Potência do ponto - ²check
  8. Fácil de aprender - ³check
  9. Ergonômico - verificar

¹Precedência: espaço após await torna não óbvio na posição diferida. No entanto, é exatamente o mesmo que no código a seguir: client.get("https://my_api").await_send()?.await_json()? . Para todos os falantes de inglês é ainda mais natural do que com todas as outras propostas

²Pot power: exigiria suporte adicional para IDE mover await para a esquerda da chamada do método após . , ? ou ; ser digitado em seguida. Não parece ser muito difícil de implementar

³Fácil de aprender: na posição de prefixo já é familiar para programadores. Na posição adiada , seria óbvio depois que a sintaxe fosse estabilizada


Mas o ponto mais forte dessa sintaxe é que ela será muito consistente:

  • Não pode ser confundido com acesso à propriedade
  • Tem precedência óbvia com ?
  • Sempre terá a mesma formatação
  • Corresponde à linguagem humana
  • Não afeta a ocorrência horizontal variável na cadeia de chamada de método *

* a explicação está escondida sob spoiler


Com a variante pós-fixada é difícil prever qual função é async e onde estaria a próxima ocorrência de await no eixo horizontal:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

Com a variante adiada, seria quase sempre o mesmo:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

Não haveria um. entre o await e a próxima chamada de método? Gostar
get().await?.json() .

Na quarta-feira, 23 de janeiro de 2019, 05:06 I60R < [email protected] escreveu:

deixe val = aguardar futuro;
let res = client.get ("https: // my_api") .await send () ?. await json () ?;

  1. Usos aguardam - verificar
  2. Primeira classe - verificar
  3. Encadeamento - verificar
  4. Grepping - verificar
  5. Sem sigilo - verificar
  6. Precedência - ¹check
  7. Potência do ponto - ²check
  8. Fácil de aprender - ³check
  9. Ergonômico - verificar

¹Precedência: o espaço torna não óbvio na posição diferida. No entanto é
exatamente o mesmo que no código a seguir: client.get ("https: // my_api
") .await_send () ?. await_json () ?. Para todos os falantes de inglês é ainda mais
natural do que com todas as outras propostas

² Potência do ponto: seria necessário suporte adicional para o IDE mover e aguardar para
à esquerda da chamada de método depois. ou? é digitado a seguir. Não parece ser também
difícil de implementar

³Fácil de aprender: na posição de prefixo já é familiar para programadores,
e na posição defendida , seria óbvio depois que a sintaxe fosse

estabilizado

Mas o ponto mais forte sobre essa sintaxe é que ela será muito
consistente:

  • Não pode ser confundido com acesso à propriedade
  • Ele tem precedência óbvia com?
  • Sempre terá a mesma formatação
  • Corresponde à linguagem humana
  • Não é afetado pela ocorrência horizontal variável na chamada do método
    cadeia*

* a explicação está escondida sob spoiler

Com a variante postfix, é difícil prever qual função é assíncrona e
onde seria a próxima ocorrência de await no eixo horizontal:

deixe res = client.get ("https: // my_api")

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

.short().await?;

Com a variante adiada, seria quase sempre o mesmo:

deixe res = client.get ("https: // my_api")

.await very_long_method_name(param, param, param)?

.await short()?;

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

também devemos decidir enquanto estamos nisso é como rustfmt acabaria formatando-o

Esse é o domínio de https://github.com/rust-dev-tools/fmt-rfcs , então eu diria que está fora do tópico para este assunto. Tenho certeza de que haverá algo consistente com qualquer correção e precedência escolhida.

@ I60R

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

Como isso procuraria funções de funções livres? Se estou lendo direito, na verdade é um prefixo await .

Acho que isso mostra que não devemos ser tão rápidos em desacreditar a experiência da equipe da linguagem C #. A Microsoft se preocupa muito com os estudos de usabilidade e estamos tomando uma decisão com base em uma única linha de código e na premissa de que Rust é especial o suficiente para ter uma sintaxe diferente de todas as outras linguagens. Não concordo com essa premissa - mencionei o LINQ acima e os métodos de extensão que estão difundidos no C #.

No entanto, pode não ocorrer a muitos, inclusive a mim, tentar .aw quando .await não é a sintaxe de superfície.

~ Não acho que isso ocorrerá a alguém familiarizado com os outros idiomas. ~ Tirando isso, você esperaria que o pop-up de conclusão incluísse os métodos await e Future ?

[...] e estamos tomando uma decisão aqui com base em uma única linha de código e na premissa de que Rust é especial o suficiente para ter uma sintaxe diferente de todas as outras linguagens. Eu discordo dessa premissa [...]

@lnicola , só quero destacar, caso você e outras pessoas tenham perdido (é fácil perder no dilúvio de comentários): https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

Vários exemplos reais do código de produção real.

Dado o comentário de @Centril acima , parece que as opções de postfix em contenção mais forte são expr await sintaxe de palavra-chave postfix e expr.await sintaxe de campo postfix (e talvez a sintaxe de método postfix ainda não tenha sido lançada).

Comparado com a palavra-chave postfix, @Centril argumenta que o campo postfix se beneficia do "poder do ponto" - que os IDEs podem ser mais capazes de sugeri-lo como um preenchimento automático - e que instâncias de seu uso podem ser mais fáceis de encontrar com grep porque o ponto inicial fornece desambiguação do uso da palavra nos comentários.

Por outro lado, @cramertj argumentou a favor da sintaxe de palavra-chave postfix com base em que tal sintaxe deixa claro que não é uma chamada de método ou acesso a campo. @withoutboats argumentou que a palavra-chave postfix é a mais familiar das opções do postfix.

Com relação ao "poder do ponto", devemos considerar que um IDE ainda pode oferecer await como conclusão após um ponto e simplesmente remover o ponto quando a conclusão for selecionada. Um IDE suficientemente inteligente (por exemplo, com RLS) poderia até perceber o futuro e oferecer await após um espaço.

As opções de campo e método postfix parecem oferecer a maior área de superfície para objeções, devido à ambigüidade, em comparação com a palavra-chave postfix. Como parte do suporte para campo / método postfix sobre palavra-chave postfix parece vir de um leve desconforto com a presença do espaço no encadeamento de métodos, devemos revisar e nos envolver com a observação de @valff de que a palavra-chave postfix ( foo.bar() await ) não parecerá mais surpreendente do que a atribuição generalizada de tipo ( foo.bar() : Result<Vec<_>, _> ). Para ambos, o encadeamento continua após esse interlúdio separado por espaço.

@ivandardi , @lnicola ,

Eu atualizei meu comentário recente com o exemplo ausente para chamada de função gratuita. Se você quiser mais exemplos, pode consultar o último spoiler em meu comentário anterior

No interesse de debater future.await? vs. future await? ...

Algo que não é muito discutido é o agrupamento visual ou grep de uma cadeia de métodos.

considere ( match vez de await para realce de sintaxe)

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

em comparação com

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

Quando examino visualmente await na primeira cadeia, identifico de forma clara e rápida send().await? e vejo que estamos aguardando o resultado de send() .

No entanto, quando examino visualmente await na segunda cadeia, primeiro vejo await?.error_for_status() e preciso desligar, não, voltar e conectar send() await .


Sim, alguns desses mesmos argumentos se aplicam à atribuição de tipo generalizada, mas esse ainda não é um recurso aceito. Embora eu goste de atribuição de tipo em certo sentido, sinto que em um contexto de expressão _geral_ (vago, eu sei) ele deve ser delimitado por parênteses. No entanto, tudo isso está fora do tópico desta discussão. Também tenha em mente que a quantidade de await em uma base de código tem uma grande mudança de ser significativamente maior do que a quantidade de atribuição de tipo.

A atribuição generalizada de tipo também vincula o tipo à expressão atribuída por meio do uso de um operador distinto : . Assim como outro operador binário, sua leitura torna (visualmente) claro que as duas expressões são uma única árvore. Considerando que future await não tem operador e pode facilmente ser confundido com future_await por motivo de raramente ver dois nomes não separados por um operador, exceto se o primeiro for uma palavra-chave (exceções se aplicam, que não é para dizer que prefiro sintaxe de prefixo, não).

Compare isso com o inglês, se desejar, onde um hífen ( - ) é usado para agrupar visualmente palavras que, de outra forma, seriam facilmente interpretadas como separadas.

Acho que isso mostra que não devemos ser tão rápidos em desacreditar a experiência da equipe da linguagem C #.

Não acho que "tão rápido" e "desacreditar" sejam justos aqui. Acho que está acontecendo aqui a mesma coisa que aconteceu na própria RFC async / await: considerar cuidadosamente por que isso foi feito de uma maneira e descobrir se o equilíbrio sai da mesma maneira para nós. A equipe C # é mencionada explicitamente em um:

Eu pensei que a ideia de await era que você realmente queria o resultado, não o futuro
então, talvez a sintaxe de await deva ser mais como a sintaxe de 'ref'

let future = task()
mas
let await result = task()

então para encadear você deve fazer

task().chained_method(|future| { /* do something with future */ })

mas

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

Todos eles funcionam bem com encadeamento e todos têm contras que não são campos / métodos / macros reais.
Mas como await , como palavra-chave, já é uma coisa especial, não precisamos torná-la mais especial.
Devemos apenas selecionar o mais simples, foo.await . Ambos () e !() são redundantes aqui.

@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

Quando examino visualmente a primeira cadeia de await, identifico de forma clara e rápida send (). Await? e ver que estamos aguardando o resultado do send ().

Gosto mais da versão espaçada, pois é muito mais fácil ver onde o futuro se constrói e onde o espera. É muito mais fácil ver o que acontece, IMHO

image

Com a separação de pontos, parece muito com uma chamada de método. O destaque de código não vai ajudar muito e nem sempre está disponível.

Finalmente, acredito que o encadeamento não é o caso de uso principal (exceto para ? , mas foo await? é o mais claro possível), e com espera simples torna-se

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

vs

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

Onde o último parece muito mais magro.

Portanto, se tivermos reduzido para future await e future.await , podemos apenas tornar o "ponto" mágico opcional? Para que as pessoas possam escolher o que desejam e, o mais importante, parem de discutir e sigam em frente!

Não é prejudicial ter sintaxe opcional, Rust tem muitos desses exemplos. o mais conhecido é o último esperador ( ; ou , ou o que quer que seja, após o último item, como em (A,B,) ) são geralmente opcionais. Não vi uma razão forte para não tornar o ponto opcional.

Acho que seria um bom momento para apenas fazer isso em Nightly e deixar o ecossistema decidir qual é o melhor para eles. Podemos ter lints para reforçar o estilo preferido, tornando-o personalizável.

Então, antes de chegar ao Estável, revisamos o uso da comunidade e decidimos se devemos escolher um ou apenas deixar os dois.

Eu discordo totalmente da noção de que macros não seriam de primeira classe o suficiente para este recurso (eu não o consideraria um recurso central em primeiro lugar) e gostaria de realçar meu comentário anterior sobre await!() (não importa se prefixo ou pós-fixado) não ser uma "macro real", mas acho que no final muitas pessoas querem que esse recurso seja estabilizado o mais rápido possível, em vez de bloquear isso em macros pós-fixadas e prefixo esperar realmente não é um bom ajuste para Rust.

Em qualquer caso, bloquear este tópico por um dia só ajudou muito e vou cancelar a inscrição agora. Sinta-se à vontade para me mencionar se estiver respondendo diretamente a um dos meus pontos.

Meu argumento contra a sintaxe com . é que await não é um campo e, portanto, não deve se parecer com um campo. Mesmo com destaque de sintaxe e fonte em negrito, parecerá um campo de alguma forma . seguido por palavra está fortemente associado ao acesso ao campo.

Não há razão para usar a sintaxe com . para uma melhor integração IDE, já que seríamos capazes de capacitar qualquer sintaxe diferente com . completamento usando algo como o recurso de completamento postfix disponível no Intellij IDEA.

Precisamos perceber await como palavra-chave de fluxo de controle. Se decidirmos fortemente usar o postfix await , devemos considerar a variante sem . e proponho usar a seguinte formatação para enfatizar melhor os pontos de bloqueio com possível interrupção:

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

Mais exemplos

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

Com destaque de sintaxe

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


Isso também pode ser uma resposta ao ponto @ivandardi sobre formatação

Acho que todos esses exemplos de postfix await são mais contra do que a favor. Longas cadeias de espera parecem erradas e o encadeamento deve ser mantido para futuros combinadores.

Compare (com match como await para realce de sintaxe):

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

para

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

Observe a falta de espera no meio? Porque o encadeamento geralmente pode ser resolvido com um design de API melhor. Se send() retorna um futuro personalizado com métodos adicionais, digamos, para converter mensagens de status diferentes de 200 em erros de hardware, então não há necessidade de esperas extras. No momento, pelo menos em reqwest , parece funcionar no local e produzir um simples Result vez de um novo futuro, que é de onde vem o problema aqui.

O encadeamento de espera é um cheiro forte de código na minha opinião, e .await ou .await() confundirá tantos novos usuários que nem chega a ser engraçado. .await especialmente parece algo fora do Python ou PHP, onde acessos de membros variáveis ​​podem magicamente ter um comportamento especial.

await deve ser um óbvio à frente, como se dissesse: "antes de continuar, esperamos por este" não "e depois esperamos por este". O último é literalmente o combinador and_then .

Outra maneira potencial de pensar sobre isso é considerar async / await expressões e futuros como iteradores. Ambos são preguiçosos, podem ter muitos estados e podem ser encerrados com sintaxe ( for-in para iteradores, await para futuros). Não proporíamos uma extensão de sintaxe para iteradores para encadea-los como expressões regulares como estamos aqui, certo? Eles usam combinadores para encadeamento e operações assíncronas / futuros devem fazer o mesmo, terminando apenas uma vez. Funciona bem.

Por fim, e o mais importante, quero poder folhear verticalmente os primeiros recuos de uma função e ver claramente onde acontecem as operações assíncronas. Minha visão não é tão boa, e ter esperas no final da linha ou presa no meio tornaria as coisas muito mais difíceis que posso simplesmente ficar com o futuro inteiramente cru.

Todos, por favor, leiam esta postagem do blog antes de comentar.

Async / await não tem a ver com conveniência. Não é sobre como evitar combinadores.

Trata-se de habilitar coisas novas que não podem ser feitas com combinadores.

Portanto, quaisquer sugestões de "vamos usar combinadores" estão fora do assunto e devem ser discutidas em outro lugar.

Observe a falta de espera no meio? Porque o encadeamento geralmente pode ser resolvido com um design de API melhor.

Você não obterá uma cadeia bonita no exemplo assíncrono real. Exemplo real:

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

Os combinadores não ajudam aqui. O aninhamento pode ser removido, mas não é fácil (tentei duas vezes e falhei). Você tem toneladas de Either:A(Either:A(Either:A(...))) no final.

OTOH é facilmente resolvido com async / await .

@Pauan escreveu antes de eu terminar minha postagem. Que assim seja. Não mencione os combinadores, async/await não devem ser semelhantes. Tudo bem se isso acontecer, há coisas mais importantes a se considerar.


Votar negativamente em meu comentário não tornará os combinadores mais convenientes.

Lendo o comentário de atribuição de tipo , pensei em como isso ficaria combinado com await . Dada a reserva que algumas pessoas têm com .await ou .await() como campos de membros / métodos de acesso confusos, eu me pergunto se uma sintaxe geral de "atribuição" poderia ser uma opção, (ou seja, "dê-me o que await retorna ").

Usando yield para await para realce de sintaxe

let result = connection.fetch("url"):yield?.collect_async():yield?;

Combinado com a atribuição de tipo, por exemplo:

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

Vantagens: ainda parece bom (IMHO), sintaxe diferente de acesso de campo / método.
Desvantagens: conflitos potenciais com a atribuição de tipo (?), Atribuições múltiplas teoricamente possíveis:

foo():yield:yield

Edit: Ocorreu-me que usar 2 atribuições seria mais lógico no exemplo combinado:

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

@Pauan , seria inapropriado aplicar excessivamente aquela postagem do blog. Os combinadores podem ser aplicados em conjunto com a sintaxe await para evitar os problemas que ela descreve.

O problema principal sempre foi que um futuro gerável deve ser 'static , mas se você capturou variáveis ​​por referência nesses combinadores, acabou com um futuro que não era 'static . Mas esperar um futuro diferente de 'static não torna o fn assíncrono em que você não está 'static . Então você pode fazer coisas assim:

`` `ferrugem
// fatia: & [T]

deixe x = aguardar futuro.e_então (| i | se slice.contém (i) {async_fn (slice)});

Então, eu estava planejando não comentar nada, mas queria adicionar algumas notas de alguém que não escreve muito código assíncrono, mas ainda terá que lê-lo:

  • O prefixo await é muito mais fácil de notar. Isso é quando você está esperando (dentro de um async fn) ou fora (na expressão de algum corpo de macro).
  • Postfix await sendo uma palavra-chave pode se destacar bem em cadeias com pouca ou nenhuma outra palavra-chave. Mas assim que você tiver fechamentos ou similares com outras estruturas de controle, seu awaits se tornará muito menos perceptível e fácil de ignorar.
  • Acho .await um pseudo-campo muito estranho. Não é como qualquer outro campo em qualquer aspecto que seja importante para mim.
  • Não tenho certeza se confiar na detecção de IDE e no realce de sintaxe é uma boa ideia. Há valor na facilidade de leitura quando nenhum destaque de sintaxe está disponível. Além disso, com as discussões recentes sobre como complicar a gramática e os problemas que isso causará para o conjunto de ferramentas, pode não ser uma boa ideia confiar que IDEs / editores acertam as coisas.

Eu não compartilho do sentimento de que await!() como uma macro "não parece de primeira classe". Macros são cidadãos de primeira classe em Rust. Eles também não existem apenas para "inventar sintaxe no código do usuário". Muitas macros "embutidas" não são definidas pelo usuário e não estão lá por meras razões sintáticas. format!() existe para dar ao compilador o poder de verificar estaticamente o tipo de strings de formato. include_bytes!() também não é definido pelo usuário e não está lá por meras razões sintáticas, é uma macro porque precisa ser uma extensão de sintaxe, pois, novamente, faz coisas especiais dentro do compilador e não poderia razoavelmente ser escrito de qualquer outra maneira, sem adicioná-lo como um recurso principal da linguagem.

E este é o meu ponto: macros são uma maneira perfeita de introduzir e ocultar a mágica do compilador de uma forma uniforme e nada surpreendente. Await é um bom exemplo disso: ele precisa de tratamento especial, mas sua entrada é apenas uma expressão e, como tal, é um ótimo ajuste para realização usando uma macro.

Então, eu realmente não vejo necessidade de uma palavra-chave especial. Dito isso, se vai ser uma palavra-chave, então deve ser apenas isso, uma palavra-chave simples - eu realmente não entendo a motivação de disfarçá-la como um campo. Isso simplesmente parece errado, não é um acesso de campo, nem mesmo remotamente como um acesso de campo (ao contrário, digamos, os métodos getter / setter com sugar podem ser), portanto, tratá-lo como um é inconsistente com a forma como os campos funcionam hoje na linguagem.

@ H2CO3 await! macro é bom de todas as maneiras desejáveis, exceto para duas propriedades:

  1. Chaves extras são realmente irritantes quando você escreve mais um milhar de espera. A ferrugem removeu os colchetes de if/while/... blocos pelo (acredito) mesmo motivo. Pode parecer sem importância atm, mas é realmente o caso
  2. Assimetria com async sendo uma palavra-chave. Async deve fazer algo maior do que apenas permitir uma macro no corpo do método. Caso contrário, poderia ser apenas um atributo #[async] no topo da função. Eu entendo que esse atributo não permitirá que você o use em blocos etc, mas fornece alguma expectativa de encontrar a palavra-chave await . Essa expectativa pode ser causada pela experiência em outras línguas, quem sabe, mas acredito fortemente que existe. Pode não ser abordado, mas deve ser considerado.

Caso contrário, poderia ser apenas um atributo #[async] no topo da função.

Por muito tempo foi, e sinto muito ver isso ir embora. Estamos introduzindo outra palavra-chave, enquanto o atributo #[async] funcionou perfeitamente bem e agora com macros procedurais semelhantes a atributos estáveis, seria até consistente com o resto da linguagem tanto sintática quanto semanticamente, mesmo se ainda fosse conhecido (e tratado especialmente) pelo compilador.

@ H2CO3 qual é o propósito ou vantagem de ter await como uma macro da perspectiva do usuário ? Existe alguma macro embutida que altera o fluxo de controle dos programas nos bastidores?

@ I60R A vantagem é a uniformidade com o resto da linguagem e, portanto, não ter que se preocupar com mais uma palavra-chave, com toda a bagagem que ela traria consigo - por exemplo, precedência e agrupamento são óbvios em uma invocação de macro.

Não vejo por que a segunda parte é relevante - não pode haver uma macro embutida que faça algo novo? Na verdade, acho que praticamente toda macro embutida faz algo "estranho" que não pode (razoavelmente / eficientemente / etc.) Ser alcançado sem o suporte do compilador. await!() não seria um novo intruso nesse aspecto.

Eu já sugeri aguardar implícito e, mais recentemente, sugerir aguardar sem cadeia .

A comunidade parece fortemente a favor da espera explícita (adoraria que isso mudasse, mas ...). No entanto, a granularidade da clareza é o que cria um padrão (ou seja, precisar dela muitas vezes na mesma expressão parece criar um padrão).

[ Aviso : a seguinte sugestão será controversa, mas dê uma chance sincera]

Estou curioso para saber se a maioria da comunidade se comprometeria e permitiria “espera implícita parcial”. Talvez ler e esperar uma vez por cadeia de expressão seja suficientemente explícito?

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

Isso é algo semelhante à desestruturação de valor, que estou chamando de desestruturação de expressão.

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

Eu também ficaria feliz com outras implementações da ideia central, "ler, aguardar uma vez por cadeia de expressão é provavelmente suficientemente explícito". Porque reduz o clichê, mas também torna o encadeamento possível com uma sintaxe baseada em prefixo que é mais familiar para todos.

@yazaddaruvala Houve um aviso acima, então tome cuidado.

Estou curioso para saber se a maioria da comunidade se comprometeria e permitiria “espera implícita parcial”. Talvez ler e esperar uma vez por cadeia de expressão seja suficientemente explícito?

  1. Não acho que a maior parte da comunidade queira algo implícito parcial. A abordagem nativa do Rust é mais como "tudo explícito". Mesmo as projeções u8 -> u32 são feitas explicitamente.
  2. Não funciona bem para cenários mais complicados. O que o compilador deve fazer com Vec<Future> ?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

await ser executado para cada chamada aninhada? Ok, provavelmente podemos fazer o await funcionar para uma linha de código (portanto, mapFunc aninhados não serão aguardados), mas é muito fácil quebrar esse comportamento. Se await funcionar para uma linha de código, qualquer refatoração como "extrair valor" a quebra.

@yazaddaruvala

Eu entendi que você está correto em seu propósito é o prefixo esperar que tenha precedência sobre ? com a possibilidade adicional de usar esperar no lugar da palavra-chave let para alterar o contexto para polvilhar todos os futuros impl. <_ I = "7"> retorna com espera?

O que significa que essas três declarações abaixo são equivalentes:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

E essas duas afirmações abaixo são equivalentes:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

Não tenho certeza se a sintaxe é ideal, mas talvez ter uma palavra-chave que muda para um "contexto implícito de espera" torne os problemas da palavra-chave de prefixo menos problemáticos. E ao mesmo tempo permitindo o uso de um controle mais refinado sobre onde o await é usado quando necessário.

Em minha opinião, para prefixo de uma linha curta, aguardar tem o benefício de similaridade com muitas outras linguagens de programação e muitas linguagens humanas . Se no final for decidido que esperar deve ser apenas um sufixo, espero que às vezes acabe usando uma macro Await!() (ou qualquer forma semelhante que não entre em conflito com palavras-chave), quando sinto que estou a familiaridade profundamente arraigada com a estrutura das frases em inglês ajudará a reduzir a sobrecarga mental de ler / escrever código. Há um valor definido nas formas de sufixo para expressões mais encadeadas, mas minha visão explicitamente não baseada em fatos objetivos é que, com a complexidade humana do prefixo aguardar subsidiado pela linguagem falada, o benefício da simplicidade de ter apenas uma forma não supera claramente a clareza de código potencial de ter ambos. Supondo que você confie no programador para escolher o que for mais apropriado para o pedaço de código atual, pelo menos.

@Pzixel

Não acho que a maior parte da comunidade queira algo implícito parcial. A abordagem nativa do Rust é mais como "tudo explícito".

Eu não vejo da mesma forma. Eu vejo isso sempre sendo um compromisso entre explícito e implícito. Por exemplo:

  • Tipos de variáveis ​​são necessários no escopo da função

    • Tipos de variáveis ​​podem estar implícitos em um escopo local.

    • Contextos de encerramento são implícitos

    • Dado que já podemos raciocinar sobre as variáveis ​​locais.

  • São necessárias vidas úteis para ajudar o verificador de empréstimo.

    • Mas no âmbito local, eles podem ser inferidos.

    • No nível da função, os tempos de vida não precisam ser explícitos, quando todos têm o mesmo valor.

  • Tenho certeza de que há mais.

Usar await apenas uma vez por expressão / instrução, com todos os benefícios do encadeamento, é um meio-termo muito razoável entre boilerplate explícito e redutor.

Não funciona bem para cenários mais complicados. O que o compilador deve fazer com Vec?

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Eu diria que o compilador deve errar. Há uma enorme diferença de tipo que não pode ser inferida. Enquanto isso, o exemplo com Vec<impl Future> não é resolvido por nenhuma das sintaxes neste tópico.

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

Eu diria que Vec<impl Future> é um exemplo pobre, ou deveríamos manter cada sintaxe proposta no mesmo padrão. Enquanto isso, a sintaxe "parcial-implícita espera" que propus funciona melhor do que as propostas atuais para cenários mais complicados como await (a, b) = (client.get("a")?, client.get("b")?); usando postfix normal ou prefixo await let (a, b) = (client.get("a") await?, client.get("b") await?); resulta em operações de rede sequenciais, onde o parcial -versão implícita pode ser executada simultaneamente pelo compilador desugardando apropriadamente (como eu mostrei em meu post original).

Em 2011, quando o recurso async em C # ainda estava em teste, perguntei se await era um operador de prefixo e recebi uma resposta do PM da linguagem C #. Como há uma discussão sobre se Rust deve usar um operador de prefixo, achei que seria útil postar essa resposta aqui. De Por que 'await' é um operador de prefixo em vez de um operador de postfix? :

Como você pode imaginar, a sintaxe das expressões de await foi um grande ponto de discussão antes de decidirmos o que está por aí :-). No entanto, não consideramos muito os operadores postfix. Existe algo sobre o postfix (cf. calculadores HP e a linguagem de programação Forth) que torna as coisas mais simples em princípio e menos acessíveis ou legíveis na prática. Talvez seja apenas a forma como a notação matemática faz uma lavagem cerebral em nós como crianças ...

Definitivamente, descobrimos que o operador de prefixo (com seu sabor literalmente imperativo - "faça isso!") Era de longe o mais intuitivo. A maioria de nossos operadores unários já são prefixos, e await parece se adequar a isso. Sim, ele se afoga em expressões complexas, mas o mesmo acontece com a ordem de avaliação em geral, devido ao fato de que, por exemplo, a aplicação de função também não é pós-fixada. Você primeiro avalia os argumentos (que estão à direita) e depois chama a função (que está à esquerda). Sim, há uma diferença na sintaxe do aplicativo de função em C # vem com parênteses já embutidos, enquanto para await você geralmente pode removê-los.

O que vejo amplamente (e eu mesmo adotei) em torno de await é um estilo que usa muitas variáveis ​​temporárias. Eu tendo a preferir

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

ou algo assim. Em geral, eu normalmente evitaria ter mais de um await em todas as expressões, exceto as mais simples (ou seja, eles são todos argumentos para a mesma função ou operador; provavelmente, esses são bons) e evito expressões de await entre parênteses, preferindo usar locais extras para ambos os trabalhos .

Faz sentido?

Mads Torgersen, PM em linguagem C #


Minha experiência pessoal como desenvolvedor C # é que a sintaxe me leva a usar esse estilo de instruções múltiplas e que isso torna o código assíncrono muito mais difícil de ler e escrever do que o equivalente síncrono.

@yazaddaruvala

Eu não vejo da mesma forma. Eu vejo isso sempre sendo um compromisso entre explícito e implícito

Ainda é explícito. Posso criar um link de um bom artigo sobre o assunto .

Eu diria que o compilador deve errar. Há uma enorme diferença de tipo que não pode ser inferida. Enquanto isso, o exemplo com Vecnão é resolvido por nenhuma das sintaxes neste tópico.

Por que haveria um erro? Estou feliz por ter Vec<impl future> , que eu poderia então juntar. Você propõe limitações extras sem motivo.

Eu diria que o Vecé um exemplo pobre, ou devemos manter todas as sintaxes propostas no mesmo padrão.

Devemos examinar cada caso. Não podemos simplesmente ignorar casos extremos porque "meh, você sabe, ninguém escreve isso de qualquer maneira". O design da linguagem trata principalmente de casos extremos.

@chescock: Eu concordo com você, mas dito antes, Rust tem uma grande diferença do C # aqui: em C # você nunca escreve (await FooAsync()).Bar() . Mas em Rust você faz. E estou falando sobre ? . Em C #, você tem uma exceção implícita que é propagada por meio de chamadas assíncronas. Mas na ferrugem você tem que ser explícito e escrever ? no resultado da função depois que ele foi aguardado.

Claro, você pode pedir aos usuários que escrevam

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

mas é muito mais estranho que

let foo = await? bar();
let bar = await? foo.baz();

Parece muito melhor, mas requer a introdução de uma nova combinação de await e ? doint (await expr)? . Além disso, se tivermos alguns outros operadores, poderíamos escrever await?&@# e fazer todas as combinações funcionarem juntas ... Parece um pouco complicado.

Mas então podemos apenas colocar o await como um postfix e ele se encaixará naturalmente na linguagem atual:

let foo = bar() await?;
let bar = foo.baz() await?;

agora await? são dois tokens separados, mas eles funcionam juntos. Você pode ter await?&@# e não vai se preocupar em hackea-lo no próprio compilador.

Claro, parece um pouco mais estranho do que a forma de prefixo, mas, dito isso, é tudo sobre a diferença Rust / C #. Podemos usar a experiência C # para ter certeza de que precisamos de uma sintaxe de espera explícita que provavelmente terá uma palavra-chave separada, mas não devemos seguir cegamente o mesmo caminho e ignorar as diferenças Rust / C #.

Fui um proponente do prefixo await por um longo tempo e até convidei desenvolvedores de linguagem C # para o tópico (então temos informações mais recentes do que em 2011 😄), mas agora acho que o postfix await palavra-chave é a melhor abordagem.

Não se esqueça que há um noturno, então podemos refazê-lo mais tarde se encontrarmos uma maneira melhor.

Ocorreu-me que podemos obter o melhor dos dois mundos com uma combinação de:

  • um prefixo async palavra-chave
  • um recurso geral de macro Postfix

Que juntos permitiriam uma macro postfix .await!() ser implementada sem a mágica do compilador.

No momento, uma macro postfix .await!() exigiria mágica do compilador. No entanto, se uma variante de prefixo da palavra-chave await fosse estabilizada, isso não seria mais verdade: a macro pós-fixada .await!() poderia ser implementada trivialmente como uma macro que prefixava seu argumento (uma expressão) com a palavra-chave await e envolveu tudo entre colchetes.

Vantagens desta abordagem:

  • O prefixo await palavra-chave está disponível e acessível para usuários familiarizados com essa construção em outros idiomas.
  • A palavra-chave assíncrona de prefixo pode ser usada onde for desejável fazer com que a natureza assíncrona de uma expressão "se destaque".
  • Haveria uma variante pós-fixada .await!() , que poderia ser usada em situações de encadeamento, ou qualquer outra situação onde a sintaxe do prefixo seja estranha.
  • Não haveria necessidade de mágica de compilador (potencialmente inesperada) para a variante postfix (caso contrário, seria um problema para o campo postfix, método ou opções de macro).
  • Macros Postfix também estariam disponíveis para outras situações (como .or_else!(continue) ), que por coincidência precisam da sintaxe de macro Postfix por motivos semelhantes (caso contrário, eles exigem que a expressão anterior seja encapsulada de uma maneira estranha e ilegível).
  • A palavra-chave do prefixo await pode ser estabilizada de forma relativamente rápida (permitindo que o ecossistema se desenvolva) sem que tenhamos que esperar pela implementação de macros pós-fixadas. Mas, de médio a longo prazo, ainda deixaríamos em aberto a possibilidade de uma sintaxe pós-fixada para aguardar.

@nicoburns , @ H2CO3 ,

Não devemos implementar operadores de fluxo de controle como await!() e .or_else!(continue) como macros, porque da perspectiva do usuário não há razão significativa para fazer isso. De qualquer forma, em ambas as formas, os usuários teriam exatamente os mesmos recursos e deveriam aprender os dois; no entanto, se esses recursos fossem implementados como macros, os usuários também se preocupariam com o porquê de serem implementados dessa forma. É impossível não notar isso, porque haveria apenas uma diferença bizarra e artificial entre operadores regulares de primeira classe e operadores regulares implementados como macros (já que macros por si só são de primeira classe, mas coisas implementadas por elas não são).

É exatamente a mesma resposta que para . antes de await : não precisamos disso. As macros não podem imitar o fluxo de controle de primeira classe: elas têm formas diferentes, têm casos de uso diferentes, têm realces diferentes, funcionam de maneira diferente e são percebidas de maneira completamente diferente.

Para os recursos .await!() e .or_else!(continue) existe uma solução adequada e ergonômica com belas sintaxes: await palavra-chave e none -operador de coalescência. Devemos preferir implementá-los em vez de algo genérico, raramente usado e feio com aparência de macros pós-fixadas.

Não há razão para usar macros, já que não há algo complicado como format!() , não há algo raramente usado como include_bytes!() , não há um DSL personalizado, não há uma remoção de duplicação no código do usuário, há não é necessária uma sintaxe semelhante a vararg, não há necessidade de algo parecer diferente e não podemos usar essa forma apenas porque é possível.

Não devemos implementar operadores de fluxo de controle como macros porque, da perspectiva do usuário, não há nenhuma razão significativa para fazer isso.

try!() foi implementado como uma macro. Havia uma razão perfeitamente boa para isso.

Já descrevi qual seria o motivo para fazer await uma macro - não há necessidade de um novo elemento de linguagem se um recurso existente também puder atingir o objetivo.

não há algo complicado como format!()

Eu discordo: format!() não é sobre complicações, é sobre verificação em tempo de compilação. Se não fosse pela verificação em tempo de compilação, poderia ser uma função.

A propósito, eu não sugeri que deveria ser uma macro postfix. (Acho que não deveria.)

Para ambos os recursos existe uma solução adequada e ergonômica com belas sintaxes

Não devemos dar a cada chamada de função, expressão de controle de fluxo ou recurso de conveniência secundária sua própria sintaxe muito especial. Isso só resulta em uma linguagem inchada, cheia de sintaxe sem motivo, e é exatamente o oposto de "bela".

(Eu disse o que podia; não vou mais responder a este aspecto da pergunta.)

@nicoburns

Ocorreu-me que podemos obter o melhor dos dois mundos com uma combinação de:

  • um prefixo await palavra-chave
  • um recurso geral de macro Postfix

Isso implica fazer de await uma palavra-chave contextual em oposição à palavra-chave real que é atualmente. Eu não acho que devemos fazer isso.

Que juntos permitiriam uma macro postfix .await!() ser implementada sem a mágica do compilador.

Esta é uma solução mais complexa em termos de implementação do que foo await ou foo.await (especialmente o último). Ainda há "magia", tanto dela; você acabou de fazer uma manobra contábil .

Vantagens desta abordagem:

  • O prefixo await palavra-chave está disponível e acessível para usuários familiarizados com essa construção em outros idiomas.

Se vamos adicionar .await!() mais tarde, estamos apenas dando aos usuários mais para aprender (tanto await foo quanto foo.await!() ) e agora os usuários perguntarão "qual devo usar quando..". Isso parece usar mais do orçamento de complexidade do que foo await ou foo.await como as soluções individuais fariam.

  • Macros Postfix também estariam disponíveis para outras situações (como .or_else!(continue) ), que por coincidência precisam da sintaxe de macro Postfix por razões semelhantes (caso contrário, eles exigem que a expressão anterior seja encapsulada de uma forma estranha e ilegível).

Acredito que as macros postfix têm valor e devem ser adicionadas à linguagem. Isso não significa, entretanto, que eles precisam ser usados ​​para await ing; Como você mencionou, há .or_else!(continue) e muitos outros lugares onde macros postfix seriam úteis.

  • O prefixo await palavra-chave pode ser estabilizado relativamente rapidamente (permitindo que o ecossistema se desenvolva) sem que tenhamos que esperar a implementação de macros pós-fixadas. Mas, de médio a longo prazo, ainda deixaríamos em aberto a possibilidade de uma sintaxe pós-fixada para aguardar.

Não vejo valor em "deixar possibilidades abertas"; o valor do postfix para composição, experiência IDE, etc. é conhecido hoje. Não estou interessado em "estabilizar await foo hoje e espero que possamos chegar a um consenso sobre foo.await!() amanhã".


@ H2CO3

try!() foi implementado como uma macro. Havia uma razão perfeitamente boa para isso.

try!(..) foi descontinuado, o que significa que o consideramos impróprio para o idioma, em particular porque não era pós-fixado. Usá-lo como um argumento - que não seja para o que não devemos fazer - parece estranho. Além disso, try!(..) é definido sem qualquer suporte do compilador .

A propósito, eu não sugeri que deveria ser uma macro postfix. (Acho que não deveria.)

await não é "cada um ..." - em particular, não é um recurso de conveniência menor ; em vez disso, é um recurso importante da linguagem que está sendo adicionado.

@phaylon

Além disso, com as discussões recentes sobre como complicar a gramática e os problemas que isso causará para o conjunto de ferramentas, pode não ser uma boa ideia confiar que IDEs / editores acertam as coisas.

A sintaxe foo.await tem como objetivo reduzir os problemas de ferramentas, pois qualquer editor com qualquer aparência de bom suporte para Rust já entenderá a forma .ident . O que o editor precisa fazer é apenas adicionar await à lista de palavras-chave. Além disso, bons IDEs já possuem autocompletar de código baseado em . - então, parece mais simples estender RLS (ou equivalentes ...) para fornecer await como a primeira sugestão quando o usuário digitar . após my_future .

Quanto a complicar a gramática, .await tem menos probabilidade de complicar a gramática, uma vez que apoiar .await na sintaxe é essencialmente analisar .$ident e não errar em ident == keywords::Await.name() .

A sintaxe foo.await visa reduzir os problemas de ferramentas, pois qualquer editor com qualquer aparência de bom suporte para Rust já entenderá a forma .ident. O que o editor precisa fazer é apenas adicionar await à lista de palavras-chave. Além disso, bons IDEs já possuem o auto-completar de código baseado em. - então parece mais simples estender RLS (ou equivalentes ...) para fornecer await como a primeira sugestão quando o usuário digita. depois do meu_futuro.

Eu apenas acho que isso está em desacordo com a discussão de gramática. E o problema não é agora, mas daqui a 5, 10, 20 anos, depois de algumas edições. Mas, como espero que você tenha percebido, também mencionei que mesmo que uma palavra-chave seja destacada, os pontos de espera podem passar despercebidos se outras palavras-chave estiverem envolvidas.

Quanto a complicar a gramática, .await tem menos probabilidade de complicar a gramática, visto que o suporte a .await na sintaxe é essencialmente analisar. $ Ident e não gerar erros em ident == keywords :: Await.name ().

Acho que a honra pertence a await!(future) , pois já é totalmente suportada pela gramática.

@Centril try! eventualmente se tornou redundante porque o operador ? pode fazer estritamente mais. Não é "impróprio para o idioma". Você pode não gostar , o que eu aceito. Mas para mim é na verdade uma das melhores coisas que Rust inventou e foi um dos seus argumentos de venda. E eu sei que é implementado sem suporte do compilador - mas não consigo ver como isso é relevante ao discutir se ele executa ou não o fluxo de controle. Ele faz, independentemente.

esperar não é "cada um ..."

Mas outros mencionados aqui (por exemplo, or_else ) são, e meu ponto ainda se aplica aos principais recursos de qualquer maneira. Adicionar sintaxe apenas para adicionar sintaxe não é uma vantagem, então sempre que houver algo que já funcione em um caso mais geral, deve-se preferir em vez de inventar uma nova notação. (Eu sei que o outro argumento contra macros é que "elas não são pós-fixadas". Eu simplesmente não acho que os benefícios de await ser seu próprio operador pós-fixada são altos o suficiente para justificar o custo. Sobrevivemos a chamadas de funções aninhadas. ficará igualmente bem após ter escrito algumas macros aninhadas.)

Na quarta-feira, 23 de janeiro de 2019 às 09:59:36 +0000, Mazdak Farrokhzad escreveu:

  • Macros Postfix também estariam disponíveis para outras situações (como .or_else!(continue) ), que coincidentemente precisam da sintaxe de macro Postfix por razões semelhantes (caso contrário, eles exigem que a expressão anterior seja encapsulada de uma forma estranha e ilegível).

Acredito que as macros postfix têm valor e devem ser adicionadas à linguagem. Isso não significa, entretanto, que eles precisam ser usados ​​para await ing; Como você mencionou, há .or_else!(continue) e muitos outros lugares onde macros postfix seriam úteis.

A principal razão para usar .await!() é que se parecer com uma macro torna
é claro que pode afetar o fluxo de controle.

.await parece com um acesso de campo, .await() parece com uma função
chamada, e nem os acessos de campo nem as chamadas de função podem afetar o controle
fluxo. .await!() parece uma macro e as macros podem afetar o controle
fluxo.

@joshtriplett I não afeta o fluxo de controle no sentido padrão. Esse é o fato básico para justificar que o verificador de empréstimo deve funcionar em futuros conforme definido (e pin é o raciocínio como ). Do ponto de vista da execução de função local, a execução de await é como a maioria das outras chamadas de função. Você continua de onde parou e tem um valor de retorno na pilha.

Eu apenas acho que isso está em desacordo com a discussão de gramática. E o problema não é agora, mas daqui a 5, 10, 20 anos, depois de algumas edições.

Não tenho ideia do que você quer dizer com isso.

Acho que a honra pertence a await!(future) , pois já é totalmente suportada pela gramática.

Eu entendo, mas neste ponto, devido às inúmeras outras desvantagens dessa sintaxe, acho que ela está efetivamente descartada.

@Centril try! eventualmente se tornou redundante porque o operador ? pode fazer estritamente mais. Não é "impróprio para o idioma". Você pode não gostar disso, porém, eu aceito.

Está explicitamente obsoleto e, além disso, é um erro difícil escrever try!(expr) no Rust 2018. Decidimos coletivamente fazer isso e, portanto, foi considerado impróprio.

O principal motivo para usar .await!() é que a aparência de uma macro deixa claro que pode afetar o fluxo de controle.

.await parece com um acesso de campo, .await() parece com uma chamada de função e nem os acessos de campo nem as chamadas de função podem afetar o fluxo de controle.

Eu acredito que a distinção é algo que os usuários aprenderão com relativa facilidade, especialmente porque .await geralmente será seguido por ? (que é o fluxo de controle local da função). Quanto às chamadas de função, e como mencionado acima, acho que é justo dizer que os métodos iterativos são uma forma de fluxo de controle. Além disso, só porque uma macro pode afetar o fluxo de controle, não significa que o fará . Muitas macros não (por exemplo, dbg! , format! , ...). O entendimento de que .await!() ou .await afetará o fluxo de controle (embora em um sentido muito mais fraco do que ? , de acordo com a nota de await si.

@Centril

Isso envolve fazer a espera uma palavra-chave contextual em oposição à palavra-chave real que é atualmente. Eu não acho que devemos fazer isso.

Eek. Isso é um pouco doloroso. Talvez a macro pudesse ter um nome diferente, como .wait!() ou .awaited!() (o último é muito bom, pois deixa claro que se aplica à expressão anterior).

"O que, juntos, permitiria que uma macro postfix .await! () Fosse implementada sem a mágica do compilador."
Esta é uma solução mais complexa em termos de implementação do que foo await ou foo.await (especialmente o último). Ainda há "magia", tanto dela; você acabou de fazer uma manobra contábil.

Além disso, try! (..) é definido sem qualquer suporte do compilador.

E se tivéssemos uma palavra-chave de prefixo await e macros pós-fixadas, .await!() (talvez com um nome diferente) também poderia ser implementado sem o suporte do compilador, certo? Claro que a palavra-chave await si ainda implicaria uma quantidade significativa de mágica do compilador, mas isso seria simplesmente aplicado ao resultado de uma macro, e não é diferente do relacionamento de try!() com o return palavra-chave.

Se vamos adicionar .await! () Mais tarde, estamos apenas dando aos usuários mais para aprender (ambos await foo e foo.await! ()) E agora os usuários perguntarão "o que devo usar quando ..". Isso parece usar mais do orçamento de complexidade do que foo await ou foo.await como soluções únicas fariam.

Acho que a complexidade que isso adiciona ao usuário é mínima (complexidade de implementação pode ser outro assunto, mas se quisermos macros pós-fixadas de qualquer maneira ...). Ambas as formas são lidas intuitivamente, de forma que, ao ler qualquer uma delas, fique claro o que está acontecendo. Quanto a qual escolher ao escrever o código, os documentos poderiam simplesmente dizer algo como:

"Existem duas maneiras de esperar um Futuro em Ferrugem: await foo.bar(); e foo.bar().await!() . Qual usar é uma preferência estilística e não faz diferença para o fluxo de execução"

Não vejo valor em "deixar possibilidades abertas"; o valor do postfix para composição, experiência IDE, etc. é conhecido hoje.

Isso é verdade. Acho que, para mim, é mais uma questão de garantir que nossa implementação não exclua a possibilidade de termos uma linguagem mais unificada no futuro.

Não tenho ideia do que você quer dizer com isso.

Não importa muito. Mas, em geral, também prefiro quando a sintaxe é óbvia sem um bom destaque. Portanto, as coisas se destacam em git diff e em outros contextos semelhantes.

Em teoria, sim, e para programas triviais sim, mas na realidade os programadores
precisam saber onde estão seus pontos de suspensão.

Para um exemplo simples, você poderia segurar um RefCell em um ponto de suspensão e
o comportamento do seu programa será diferente do que se o RefCell fosse
liberado antes do ponto de suspensão. Em grandes programas, haverá
inúmeras sutilezas como essa, onde o fato de a função atual
está suspendendo é uma informação importante.

Na quarta-feira, 23 de janeiro de 2019 às 14h21 HeroicKatora [email protected]
escrevi:

@joshtriplett https://github.com/joshtriplett I não afeta o controle
fluxo no sentido padrão. Este é o fato básico para justificar que o
O verificador de empréstimo deve funcionar em futuros conforme definido. Do ponto de vista do
execução de função local, a execução de await é como qualquer outra chamada de função.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

Na quarta-feira, 23 de janeiro de 2019 às 02:26:07 PM -0800, Mazdak Farrokhzad escreveu:

O principal motivo para usar .await!() é que a aparência de uma macro deixa claro que pode afetar o fluxo de controle.

.await parece com um acesso de campo, .await() parece com uma chamada de função e nem os acessos de campo nem as chamadas de função podem afetar o fluxo de controle.

Acredito que a distinção é algo que os usuários aprenderão com relativa facilidade

Por que eles deveriam?

Acredito que a distinção seja algo que os usuários aprenderão com relativa facilidade, especialmente porque .await geralmente é seguido por? (que é o fluxo de controle local da função).

Freqüentemente, sim, mas nem sempre, e a sintaxe definitivamente deve funcionar para casos em que esse não é o caso.

Quanto às chamadas de função, e como mencionado acima, acho que é justo dizer que os métodos iterativos são uma forma de fluxo de controle.

Não da mesma forma que return ou ? (ou mesmo break ) são. Seria muito surpreendente se uma chamada de função pudesse retornar dois níveis acima da função que a chamou (uma das razões pelas quais Rust não tem exceções é precisamente porque isso é surpreendente), ou quebrar um loop na chamada função.

Além disso, só porque uma macro pode afetar o fluxo de controle, não significa que o fará. Muitas macros não (por exemplo, dbg !, format !, ...). O entendimento de que .await! () Ou .await afetará o fluxo de controle (embora em um sentido muito mais fraco do que?, De acordo com a nota de

Eu não gosto nada disso. Efetuar o fluxo de controle é um efeito colateral muito importante . Deve ter uma forma sintática diferente para construções que normalmente não podem fazer isso. As construções que podem fazer isso no Rust são: palavras-chave ( return , break , continue ), operadores ( ? ) e macros (avaliando para um das outras formas). Por que turvar as águas adicionando uma exceção?

@ejmahler Não vejo como segurar um RefCell é diferente para a comparação com uma chamada de função normal. Também pode ser que a função interna queira adquirir o mesmo RefCell. Mas ninguém parece ter um grande problema em não compreender imediatamente o gráfico de chamadas potencial completo. Da mesma forma, os problemas com o esgotamento da pilha não recebem nenhuma preocupação especial que você deva ter para anotar o espaço de pilha fornecido. Aqui a comparação seria favorável para co-rotinas! Devemos tornar as chamadas de função normais mais visíveis se não estiverem embutidas? Precisamos de chamadas de cauda explícitas para controlar esse problema cada vez mais difícil com as bibliotecas?

A resposta é, imho, que o maior risco ao usar o RefCell e esperar é a falta de familiaridade. Quando as outras questões acima podem ser abstraídas do hardware, acredito que nós, como programadores, também podemos aprender a não nos agarrar ao RefCell etc. em todos os pontos de rendimento, exceto onde for examinado.

Na quarta-feira, 23 de janeiro de 2019 às 10:30:10 +0000, Elliott Mahler escreveu:

Em teoria, sim, e para programas triviais sim, mas na realidade os programadores
precisam saber onde estão seus pontos de suspensão.

: +1:

@HeroicKatora

Uma diferença crítica é a quantidade de código que você precisa inspecionar, quantas
possibilidades que você tem que considerar. Os programadores já estão acostumados a
certificando-se de que nada que eles chamem também use um RefCell que eles verificaram
Fora. Mas com o await, em vez de inspecionar uma única pilha de chamadas, não temos
controle sobre o que é executado durante o await - literalmente qualquer linha do
programa pode ser executado. Minha intuição me diz que o programador médio é
muito menos equipados para lidar com essa explosão de possibilidades, pois
tem que lidar com isso com muito menos frequência.

Para outro exemplo de pontos de suspensão sendo informações críticas, no jogo
programação, normalmente escrevemos corrotinas que devem ser retomadas
uma vez por quadro, e mudamos o estado do jogo em cada currículo. Se negligenciarmos um
ponto de suspensão em uma dessas corrotinas, podemos deixar o jogo em um
estado para vários quadros.

Tudo isso pode ser tratado dizendo "seja mais inteligente e faça menos
erros ”, é claro, mas essa abordagem historicamente não funciona.

Na quarta-feira, 23 de janeiro de 2019 às 14h44 Josh Triplett [email protected]
escrevi:

Na quarta-feira, 23 de janeiro de 2019 às 10:30:10 +0000, Elliott Mahler escreveu:

Em teoria, sim, e para programas triviais sim, mas na realidade os programadores
precisam saber onde estão seus pontos de suspensão.

: +1:

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

Para outro exemplo de pontos de suspensão sendo informações críticas, na programação de jogos, normalmente escrevemos corrotinas que devem ser retomadas uma vez por quadro e mudamos o estado do jogo em cada retomada. Se ignorarmos um ponto de suspensão em uma dessas corrotinas, podemos deixar o jogo em um estado interrompido por vários quadros.

Se você soltar a chave em um SlotMap, você perderá memória. A linguagem em si não ajuda você com isso inerentemente. O que eu quero dizer é que seus exemplos de como await não está vagamente visível o suficiente (desde que seja uma palavra-chave, sua ocorrência será única) não parecem generalizar para problemas que não ocorreram para outros recursos. Eu acho que você deve avaliar esperar menos sobre quais características a linguagem oferece imediatamente e mais sobre como torna possível para você expressar suas próprias características. Avalie suas ferramentas e aplique-as. Sugerir uma solução para o estado do jogo e como isso é resolvido suficientemente bem: Escreva um invólucro que afirme no retorno que o estado do jogo de fato marcou a quantidade necessária, async permite que você peça emprestado seu próprio estado para essa causa. Banir outras esperas nesse código compartimentado.

A explosão de estado no RefCell não vem de você não verificar se ele não é usado em outro lugar, mas de outros pontos de código, não sabendo que você confiou naquele invariante e adicionando-o silenciosamente. Estes são endereçados da mesma forma, documentação, comentários, embalagem correta.

Segurar RefCell não é muito diferente de segurar Mutex ao fazer o bloqueio de io. Você pode obter um deadlock, que é pior e mais difícil de depurar do que um pânico. E ainda não anotamos operações de bloqueio com a palavra-chave block explícita. :).

Compartilhar RefCell entre funções assíncronas limitará bastante sua usabilidade geral ( !Send ), então não acho que será comum. E RefCell geralmente deve ser evitado e, quando usado, emprestado pelo menor tempo possível.

Portanto, não acho que arrastar RefCell acidentalmente sobre um ponto de rendimento seja um grande negócio.

Se ignorarmos um ponto de suspensão em uma dessas corrotinas, podemos deixar o jogo em um estado interrompido por vários quadros.

Corrotinas e funções assíncronas são uma coisa diferente. Não estamos tentando tornar yield implícito.

Para qualquer um a favor da sintaxe da macro em vez da sintaxe da palavra-chave aqui: Descreva o que é sobre await que significa que deveria ser uma macro e como isso é diferente de, digamos, while , que _poderia_ ter acabou de ser uma macro while! ( mesmo usando apenas macro_rules! ; nenhuma caixa especial).

Edit: Isso não está sendo retórico. Estou genuinamente interessado em tal coisa, da mesma forma que pensei que queria executar para aguardar primeiro (como C #), mas o RFC me convenceu do contrário.

Para qualquer um a favor da sintaxe da macro em vez da sintaxe da palavra-chave aqui: Descreva do que se trata await, o que significa que deveria ser uma macro e como isso é diferente de, digamos, while, que poderia ter sido há pouco! macro (mesmo usando apenas macro_rules !; nenhum caso especial em tudo).

Eu concordo principalmente com esse raciocínio, e quero usar uma palavra-chave para sintaxe de prefixo.

No entanto, conforme descrito acima (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), eu sou a favor de também ter uma macro postfix para a sintaxe postfix (talvez .awaited!() para evitar conflitos de nome com a palavra-chave do prefixo). Meu raciocínio é em parte que torna as coisas mais simples ter uma palavra-chave que só pode ser usada de uma maneira, e salvando outras variações para macros, mas sendo principalmente que, pelo que posso ver, ninguém foi capaz de sugerir um postfix sintaxe de palavra-chave que eu consideraria aceitável:

  • foo.bar().await e foo.bar().await() seriam tecnicamente palavras-chave, mas eles se parecem com um acesso de campo ou chamada de método, e não acho que tal construção de modificação de fluxo de controle deva ser escondida assim.
  • foo.bar() await é apenas confuso, especialmente com mais encadeamentos, como foo.bar() await.qux() . Nunca vi uma palavra-chave usada como um postfix assim (tenho certeza que existem, mas na verdade não consigo pensar em um único exemplo em qualquer idioma que conheço de palavras-chave que funcionem assim). E eu acho que parece muito confuso como se o await se aplicasse a qux() não a foo.bar() .
  • foo.bar()@await ou alguma outra pontuação pode funcionar. Mas não é particularmente agradável e parece bastante ad-hoc. Na verdade, não acho que haja problemas significativos com essa sintaxe (ao contrário das opções acima). Eu apenas sinto que quando começamos a adicionar sigilos, o equilíbrio começa a se inclinar para o abandono da sintaxe customizada, e para que ela seja uma macro.

Vou reiterar uma ideia anterior mais uma vez, embora ajustada com novos pensamentos e considerações, então tento permanecer em silêncio.

await como uma palavra-chave só é válida dentro de uma função async qualquer maneira, então devemos permitir o identificador await outro lugar. Se uma variável é nomeada await em um escopo externo, ela não pode ser acessada de dentro de um bloco async menos que seja usado como um identificador bruto. Ou, por exemplo, a macro await! .

Além disso, adaptar a palavra-chave como essa permitiria meu ponto principal: devemos permitir a combinação e correspondência de geradores e funções assíncronas, assim:

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

Eu acredito que isso é suficientemente transparente para usuários extremamente avançados que querem fazer coisas divertidas, mas permite que usuários novos e moderados usem apenas o necessário. 2 e 3, se usados ​​sem nenhuma instrução yield , são efetivamente idênticos.

Também acho que o prefixo await? é um ótimo atalho para adicionar ? ao resultado da operação assíncrona, mas isso não é estritamente necessário.

Se macros postfix se tornarem uma coisa oficial (o que eu espero que aconteçam), tanto await!(...) quanto .await!() podem coexistir, permitindo três maneiras equivalentes de fazer o Wait, para casos de uso e estilos específicos . Não acredito que isso adicionaria sobrecarga cognitiva, mas permitiria maior flexibilidade.

Idealmente, async fn e #[async] fn* poderiam até mesmo compartilhar o código de implementação para transformar o gerador subjacente em uma máquina de estado assíncrona.

Esses problemas deixaram claro que não existe um estilo preferido de verdade, então o melhor que posso esperar é fornecer níveis de complexidade limpos, flexíveis, legíveis e fáceis de abordar. Acho que o esquema acima é um bom compromisso para o futuro.

await como uma palavra-chave só é válida dentro de uma função async , portanto, devemos permitir que o identificador aguarde em outro lugar.

Não sei se isso é prático no Rust, dadas as macros higiênicas. Se eu chamar foo!(x.await) ou foo!(await { x }) quando quiser um expr , acho que não deveria ser ambíguo que a palavra-chave await é desejada - não await field ou a expressão literal await struct com abreviação de init de campo - mesmo em um método síncrono.

permitindo três maneiras equivalentes de fazer esperas

Por favor não. (Pelo menos em core . Obviamente, as pessoas podem criar macros em seus próprios códigos, se quiserem)

No momento, uma macro postfix .await! () Exigiria mágica do compilador. No entanto, se uma variante de prefixo da palavra-chave await fosse estabilizada, isso não seria mais verdade: a macro postfix .await! () Poderia ser implementada trivialmente como uma macro que prefixava seu argumento (uma expressão) com a palavra-chave await e envolvia tudo entre colchetes.

Notarei que isso é igualmente verdade - mas mais fácil! - na outra direção: se estabilizarmos a sintaxe da palavra-chave .await , as pessoas já podem fazer uma macro de prefixo awaited!() se não gostarem de postfix o suficiente.

Não gosto da ideia de adicionar várias variantes permitidas (como prefixo e pós-fixado), mas por um motivo um pouco diferente do que os usuários pedirão. Eu trabalho em uma empresa bastante grande e as brigas pelo estilo do código são reais. Eu gostaria de ter apenas uma forma óbvia e correta para usar. Afinal, o Zen do python pode estar certo com relação a esse ponto.

Não gosto da ideia de adicionar várias variantes permitidas (como prefixo e pós-fixado), mas por um motivo um pouco diferente do que os usuários pedirão. Eu trabalho em uma empresa bastante grande e as brigas pelo estilo do código são reais. Eu gostaria de ter apenas uma forma óbvia e correta para usar. Afinal, o Zen do python pode estar certo com relação a esse ponto.

Eu sei o que você quer dizer. No entanto, não há como impedir que os programadores definam suas próprias macros fazendo coisas como await!() . Estruturas semelhantes sempre serão possíveis. Então realmente HAVERÁ variantes diferentes de qualquer maneira.

Bem, não await!(...) pelo menos, pois isso seria um erro; mas se um usuário definir macro_rules! wait { ($e:expr) => { e.await }; } e usar isso como wait!(expr) então parecerá nitidamente unidiomático e provavelmente sairá de moda rapidamente. Isso diminui significativamente a probabilidade de variação no ecossistema e permite que os usuários aprendam menos estilos. Portanto, acho que o argumento de

@Centril Se alguém quiser fazer coisas ruins, dificilmente poderá ser impedido. E quanto a _await!() ou awai!() ?

ou, quando o identificador Unicode habilitado, algo como àwait!() .

...

@earthengine O objetivo é definir normas da comunidade (semelhante ao que fazemos com style lints e rustfmt ), não para evitar que várias pessoas façam coisas estranhas de propósito. Estamos lidando com probabilidades aqui, não garantias absolutas de não ver _await!() .

Vamos resumir e revisar os argumentos para cada sintaxe postfix:

  • __ expr await (palavra-chave do Postfix) __: A sintaxe da palavra-chave do Postfix usa a palavra-chave await que já reservamos. Await é uma transformação mágica, e usar uma palavra-chave ajuda a se destacar de forma adequada. Não se parece com um acesso de campo ou um método ou chamada de macro, e não teremos que explicar isso como uma exceção. Ele se encaixa bem com as regras de análise atuais e com o operador ? . O espaço no encadeamento de métodos não é, sem dúvida, atribuição de tipo generalizada , uma RFC que parece ser aceita de alguma forma. No lado negativo, IDEs podem precisar fazer mais mágica para fornecer um preenchimento automático para await . As pessoas podem achar o espaço no encadeamento de métodos muito incômodo (embora @withoutboats tenha argumentado que isso pode ser uma vantagem), especialmente se o código não estiver formatado de forma que await termine cada linha. Ele usa uma palavra-chave que talvez pudéssemos evitar se adotássemos outra abordagem.

  • __ expr.await (campo postfix) __: A sintaxe do campo Postfix aproveita o "poder do ponto" - parece natural no encadeamento e permite que os IDEs completem automaticamente await sem realizar outras operações (como como remover automaticamente o ponto ). É tão conciso quanto a palavra-chave Postfix. O ponto na frente de await pode tornar as instâncias dele mais fáceis de encontrar com grep. No lado negativo, parece um acesso de campo. Como um acesso de campo aparente, não há dica visual de que "isso faz alguma coisa".

  • __ expr.await() (método postfix) __: A sintaxe do método Postfix é semelhante ao campo Postfix. Por outro lado, a sintaxe de chamada () indica ao leitor: "isso faz alguma coisa". Visto localmente, quase faz sentido da mesma maneira que uma chamada a um método de bloqueio resultaria na execução em um programa multi-threaded. No lado negativo, é um pouco mais longo e barulhento, e disfarçar o comportamento mágico de await como um método pode ser confuso. Podemos dizer que await é um método no mesmo sentido limitado que call/cc em Scheme é uma função. Como método, precisamos considerar se Future::await(expr) deve funcionar de forma consistente com o UFCS .

  • __ expr.await!() (macro postfix) __: A sintaxe da macro Postfix aproveita de forma semelhante o "poder do ponto". A macro ! bang indica "isso pode fazer algo mágico." Por outro lado, isso é ainda mais barulhento e, embora as macros façam mágica, normalmente não fazem mágica ao código circundante como await faz. Também no lado negativo, assumindo que padronizamos uma sintaxe de macro postfix de propósito geral , pode haver problemas em continuar a tratar await como uma palavra-chave.

  • __ expr@ , expr# , expr~ e outros símbolos de um único caractere__: Usar um único caractere como fazemos com ? maximiza a concisão e talvez crie uma sintaxe pós-fixada parece mais natural. Como acontece com ? , podemos descobrir que apreciamos essa concisão se await começar a permear nosso código. No lado negativo, até e a menos que a dor de ter nosso código repleto de await se torne um problema, é difícil ver um consenso se formando em torno das compensações inerentes à adoção de tal sintaxe.

Eu queria fazer uma postagem para agradecer a @traviscross por essas postagens de resumo! Eles foram consistentemente bem escritos e bem interligados. É muito apreciado. :coração:

Eu tenho uma ideia de que adicionando "operadores de pipe" como F #, os usuários podem usar prefixo ou pós-fixo (com diferença de sintaxe explícita).

// 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 Excelente resumo. Também houve alguma discussão sobre a combinação de sigilo e palavra-chave, por exemplo, fut@await , então irei apenas adicionar isso aqui para as pessoas que vêm a este tópico.

Eu alistei os prós e os contras dessa sintaxe aqui . @earthengine diz que outros sigilos além de @ são possíveis, como ~ . @BenoitZugmeyer favorece @await , e pergunta se macros postfix ala expr!await seria uma boa ideia. @dpc argumenta que @await é muito ad-hoc e não se integra bem com o que já temos, também, que Rust já é pesado em sigilos; @cenwangumass concorda que é muito ad-hoc. @newpavlov diz que a parte await parece redundante, especialmente se não adicionarmos outras palavras-chave semelhantes no futuro. @nicoburns diz que a sintaxe pode funcionar e que não há muitos problemas com ela, mas que é uma solução muito ad-hoc.

@traviscross ótimo resumo!

Meus 0,02 $ na ordem do pior para o melhor na minha opinião:

  • 3 é definitivamente um no-go porque nem chega perto de uma chamada de método.
  • 2 não é um campo, é muito confuso, especialmente para iniciantes. ter await na lista de completamento não ajuda muito. Depois de escrever muitos deles, basta escrever como fn ou extern . O preenchimento adicional é ainda pior do que nada, porque em vez de métodos / campos úteis, serei sugerido por uma palavra-chave.
  • 4 Macro é algo que se encaixa aqui, mas não me agrada. Quero dizer assimetria com async sendo a palavra-chave, como mencionei acima
  • 5 Sigil pode ser muito conciso e difícil de detectar, mas é uma entidade separada e pode ser tratada assim. Não se parece com nada, portanto, não produz confusão para os usuários
  • 1 Melhor abordagem IMHO, é apenas um sigilo tão fácil de detectar e que já é uma palavra-chave reservada. A separação do espaço, conforme mencionado acima, é uma vantagem, não uma falha. É normal existir quando nenhuma formatação é feita, mas com rustfmt é ainda menos significativo.

Como alguém que está esperando ansiosamente por este momento, aqui estão meus $ 0,02 também.

Na maior parte, concordo com @Pzixel , exceto nos dois últimos pontos, no sentido de que eu os try!() / ? par. Eu ainda vi pessoas argumentarem a favor de try!() vez de ? para fins de clareza, e acho que ter alguma agência sobre a aparência de seu código neste caso específico é mais um pró do que um contra , mesmo à custa de ter duas sintaxes diferentes.

Em particular, uma palavra-chave postfix await é ótima se você escrever seu código assíncrono como uma sequência explícita de etapas, por exemplo

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

Por outro lado, você pode preferir fazer algo mais complicado, no estilo de encadeamento de método Rust-y típico, por exemplo

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

Acho que, neste caso, já está bastante claro a partir do contexto que estamos lidando com um futuro, e a verbosidade do teclado await é redundante. Comparar:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

Outra coisa que gosto na sintaxe pós-fixada de símbolo único é que ela deixa claro que o símbolo pertence à expressão , enquanto (para mim, pessoalmente) o await independente parece um pouco ... perdido ? :)

Acho que o que estou tentando dizer é que await fica melhor em instruções , enquanto um postfix de símbolo único fica melhor em expressões .

Esses são apenas meus pensamentos, de qualquer maneira.

Visto que esta já é a mãe de todos os tópicos de remoção de bicicletas, eu queria adicionar outro sigilo que não foi mencionado até agora AFAICT: -> .

A idéia é que ele espelhe -> da declaração do tipo de retorno da função que segue, que - sendo a função async - é o tipo de retorno esperado.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

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

Acima, o que você obtém de send()-> é Result<Response, HttpError> , exatamente como está escrito na declaração da função.

Estes são meus $ 0,02 depois de ler a maior parte da discussão acima e meditar sobre as opções propostas por alguns dias. Não há razão para dar peso à minha opinião - sou apenas uma pessoa aleatória na internet. Provavelmente não comentarei muito mais, pois sou cauteloso em não adicionar mais ruído à discussão.


Eu gosto de um sigilo postfix. Há precedência para sigilos pós-fixados e acho que seria consistente com o resto da linguagem. Eu não tenho nenhuma preferência particular sobre qual sigilo particular é usado - nenhum é atraente, mas acho que vai desaparecer com a familiaridade. Um sigilo apresenta o mínimo de ruído em comparação com outras opções de pós-fixação.

Não me importo com a ideia de substituir . (ao encadear) por -> para esperar. Ainda é conciso e sem ambigüidades, mas prefiro algo que possa ser usado nos mesmos contextos que ? .

Não gosto muito das outras opções de postfix que foram apresentadas. await não é um campo ou método e, portanto, parece totalmente inconsistente com o resto da linguagem para uma construção de fluxo de controle ser apresentada como tal. Não há macros ou palavras-chave pós-fixadas no idioma, o que também parece inconsistente.

Se eu fosse novo na linguagem, sem conhecê-la totalmente, então assumiria que .await ou .await() não eram recursos especiais da linguagem e eram campos ou métodos em um tipo - como é o caso com todos os outros campos e métodos no idioma que o usuário verá. Depois de ganhar mais experiência, se .await!() for escolhido, o usuário pode se esforçar para aprender como definir suas próprias macros pós-fixadas (assim como aprenderam a definir suas próprias macros de prefixo) - não podem. Se aquele usuário viu um sigilo, ele pode precisar procurar a documentação (assim como ? ), mas ele não o confundiria com qualquer outra coisa e perderia tempo tentando encontrar a definição de .await() ou documentação para o campo .await .

Eu gosto de um prefixo await { .. } . Essa abordagem tem precedência clara, mas tem problemas (que já foram discutidos em detalhes). Apesar disso, acho que seria benéfico para quem prefere usar combinadores. Eu não gostaria que essa fosse a única opção implementada, já que não é ergonômico com encadeamento de métodos, mas acho que complementaria muito bem um sigilo pós-fixo.

Não gosto de outras opções de prefixo, elas não parecem consistentes com outras construções de fluxo de controle na linguagem. Da mesma forma que um método postfix, uma função de prefixo é inconsistente, não há nenhuma outra função global embutida na linguagem usada para o fluxo de controle. Também não há nenhuma macro usada para controlar o fluxo (com exceção de try!(..) mas isso está obsoleto porque temos uma solução melhor - um sigilo pós-fixado).


Quase todas as minhas preferências se resumem ao que parece natural e consistente (para mim). Qualquer que seja a solução escolhida, deve ser dado tempo para experimentação antes da estabilização - a experiência prática será um juiz muito melhor de qual opção é melhor do que a especulação.

Também vale a pena considerar que pode haver uma maioria silenciosa que poderia ter opiniões totalmente diferentes - aqueles que participam dessas discussões (incluindo eu) não são necessariamente representantes de todos os usuários do Rust (isso não é para ser pequeno de qualquer forma ofensiva).

tl; dr sigilos Postfix são uma forma natural de expressar o await (devido à precedência) e é uma abordagem concisa e consistente. Eu adicionaria um prefixo await { .. } e um postfix @ (que pode ser usado nos contextos como ? ). É mais importante para mim que Rust permaneça internamente consistente.

@SamuelMoriarty

Acho que neste caso já está bastante claro a partir do contexto que estamos lidando com um futuro, e a verbosidade do teclado de await é redundante. Comparar:

Desculpe, mas nem percebi ~ à primeira vista. Reli todo o comentário e minha segunda leitura foi mais bem-sucedida. Explica muito porque acho que o sigilo é pior. É apenas uma opinião, mas acabou de ser confirmada mais uma vez.

Outro ponto contra sigilos é que Rust se torna J. Por exemplo:

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

?@? significa "função que pode retornar um erro ou um futuro, que deve ser aguardada, com erro, se houver, propagado para um chamador"

Eu gostaria mais de ter

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

@rolandsteiner

Uma vez que esta já é a mãe de todos os tópicos de remoção de bicicletas, eu queria adicionar outro sigilo que não foi mencionado até agora AFAICT: ->.

Tornar a gramática dependente do contexto não torna as coisas melhores, apenas piores. Isso leva a erros piores e tempos de compilação mais lentos.

Temos um significado distinto para -> , não tem nada a ver com async/await .

Eu prefiro o prefixo await { .. } e, se possível, um pós-fixado ! sigilo.
Um ponto de exclamação enfatizaria sutilmente a preguiça dos futuros. Eles não serão executados até receberem um comando com um ponto de exclamação.

O exemplo acima ficaria assim:

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

Desculpe se minha opinião não é relevante, pois tenho pouca experiência assíncrona e nenhuma experiência com o ecossistema de função assíncrona do Rust.

No entanto, olhando para .send()?!?.json()?!?; e outras combinações como essa, eu entendo os motivos básicos pelos quais a proposta baseada em sigilo parece errada para mim.

Primeiro, parece-me que os sigilos encadeados rapidamente se tornam ilegíveis, onde quer que seja ?!? ou ?~? ou ?->? . Esta será mais uma coisa em que os iniciantes tropeçarão, tentando adivinhar se é um operador ou vários. As informações estão muito compactadas.

Em segundo lugar, em geral, parece-me que os pontos de espera são menos comuns do que os pontos de propagação de erro e mais significativos. Os pontos de espera são significativos o suficiente para eu merecer ser um estágio na cadeia por conta própria, não uma "transformação menor" ligada a outro estágio (e especialmente não "apenas uma das transformações menores"). Eu provavelmente ficaria bem mesmo com a forma de palavra-chave de prefixo (que quase força um await a quebrar a cadeia), mas em geral isso parece muito restritivo. Minha opção ideal provavelmente seria uma macro do tipo método .await!() com a possibilidade futura de expandir o sistema de macro para permitir macros do usuário do tipo método.

A linha de base mínima para estabilização, em minha opinião, é o operador de prefixo await my_future . Tudo o mais pode seguir.

expr....await refletiria o contexto para algo acontecendo até aguardar e consistente com os operadores rustlang. Além disso, async await é um padrão paralelo ..await não pode ser expresso como método ou propriedade semelhante

Eu tenho minha inclinação para await!(foo) , mas como outros apontaram que fazer isso nos forçaria a cancelar a reserva e, assim, impedir seu uso futuro como um operador até 2021, irei com await { foo } como minha preferência. Quanto ao postfix await, não tenho opinião em particular.

Eu sei que pode não ser o melhor lugar para falar sobre esperar implícito, mas algo como um esperar implícito explícito funcionaria? Portanto, primeiro temos o prefixo para aguardar o pouso:

await { future }?

E depois adicionamos algo semelhante a

let result = implicit await { client.get("https://my_api").send()?.json()?; }

ou

let result = auto await { client.get("https://my_api").send()?.json()?; }

Ao escolher o modo implícito, tudo entre {} é automaticamente aguardado.

Isso unificou a sintaxe await e equilibraria a necessidade do prefixo esperar, encadear e ser o mais explícito possível.

Decidi rg --type csharp '[^ ]await' pesquisar exemplos de lugares onde o prefixo era inferior ao ideal. É possível que nem todos sejam perfeitos, mas são códigos reais que passaram por revisão de código. (Exemplos ligeiramente limpos para remover certas coisas do modelo de domínio.)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Usando FluentAssertions como uma maneira mais agradável de fazer as coisas do que o MSTest normal assert_eq! Assert.Equal .

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Essa coisa geral de "olha, eu realmente só precisava de uma coisa fora disso" é um monte delas.

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Outro "Eu só precisava de uma propriedade". (À parte: "cara, fico feliz que Rust não precise de CancellationToken s.)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

O mesmo .collect() um que as pessoas mencionaram em Rust.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

Eu estava pensando em talvez gostar da palavra-chave postfix, com rustfmt newlines depois dela (e depois de ? , se presente), mas são pensamentos assim que me fazem pensar que newline não é bom em geral.

else if (!await container.ExistsAsync())

Um dos raros em que o prefixo era realmente útil.

var response = (HttpWebResponse)await request.GetResponseAsync();

Houve alguns casts, embora, é claro, os casts sejam outro lugar onde Rust é pós-fixado, mas C # é prefixo.

using (var response = await this.httpClient.SendAsync(requestMsg))

Este prefixo vs postfix não importa, mas acho que é outra diferença interessante: como C # não tem Drop , um monte de coisas acabam _precisando_ ir em variáveis ​​e não encadeadas.

alguns dos exemplos de fixadas :

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

(obrigado a @ Nemo157 pelas correções)

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

Para mim, depois de ler isso, @ sigilo está fora da mesa, pois é apenas invisível, especialmente na frente de ? .

Eu não vi ninguém neste tópico discutir a variante de espera para Stream. Eu sei que está fora do escopo, mas deveríamos estar pensando nisso?

Seria uma pena se tomássemos uma decisão que acabou sendo um bloqueador para o await no Streams.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

Você não pode usar await dentro de um encerramento como este, seria necessário haver um método de extensão adicional em Option que leva um encerramento assíncrono e retorna um Future si (em no momento em que acredito que a assinatura do método de extensão é impossível de especificar, mas espero que possamos encontrar uma maneira de tornar os fechamentos assíncronos utilizáveis ​​em algum ponto) .

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(ou uma tradução mais direta do código usando if let vez de um combinador)

@ Nemo157 Yeal, mas provavelmente podemos fazer isso sem funções extras:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Mas a abordagem if let parece ser mais natural para mim.

Para mim, depois de ler isso, @ sigil está fora da mesa, pois é apenas invisível, especialmente na frente de?.

Não se esqueça dos esquemas alternativos de realce de código; para mim, esse código se parece com este:
1

Pessoalmente, não acho que "invisibilidade" aqui seja um problema maior do que ? autônomo.

E um esquema de cores inteligente pode torná-lo ainda mais perceptível (por exemplo, usando uma cor diferente da de ? ).

@newpavlov você não pode escolher o esquema de cores em ferramentas externas, por exemplo, nas guias de revisão do gitlab / github.

Dito isso, não é uma boa prática confiar apenas no realce. Outros podem ter outras preferências.

Olá, sou um novo aprendiz do Rust vindo do C ++. Só quero comentar que não seria algo como

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

ou

id = id.or_else_async(async || { 
    self.storage_coordinator.get_default_widget_async() await?.identity 
}) await?;

ser basicamente incompreensível? O await é empurrado para o final da linha, enquanto nosso foco está principalmente concentrado no início (que é como quando você usa um mecanismo de pesquisa).
Algo como

id =  await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

ou

id = auto await {
    id.or_else_async(async || { self.storage_coordinator.get_default_widget_async()?.identity })
}?;

sugerido anteriormente parece muito melhor para mim.

Concordo. As primeiras palavras são a primeira coisa que alguém olha durante a varredura de código, resultados de pesquisa, parágrafos de texto, etc. Isso imediatamente coloca qualquer tipo de postfix em desvantagem.

Para ? colocação, estou convencido de que await? foo é suficientemente distinto e fácil de aprender.

Se estabilizarmos isso agora e depois de um ou dois anos de uso decidirmos que realmente queremos algo melhor para o encadeamento, podemos considerar macros postfix como um recurso geral.

Eu gostaria de propor uma variação na ideia de ter ambos pré e pós- fixados aguardando por
Podemos ter 2 coisas:

  • um prefixo await palavra-chave (por exemplo, o sabor com ligação mais forte do que ? , mas isso é menos importante)
  • um novo método em std::Future , por exemplo fn awaited(self) -> Self::Output { await self } . Seu nome também pode ser block_on , ou blocking , ou algo melhor que outra pessoa possa inventar.

Isso permitiria o uso de prefixo "simples" e também permitiria o encadeamento, evitando ter que esperar uma palavra-chave contextual.

Tecnicamente, o segundo ponto de marcador também pode ser realizado com uma macro postfix, caso em que escreveríamos .awaited!() .

Isso permitiria um código semelhante a este:

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

Outras questões à parte, o ponto principal desta proposta é ter await como o bloco de construção básico do mecanismo, assim como match , e então ter combinadores para nos poupar de ter que digitar muitos palavras-chave e chaves de todos os tipos. Acho que isso implica que eles podem ser ensinados da mesma maneira: primeiro o simples await , depois, para evitar muitos parênteses, pode-se usar .awaited() e retirar a corrente.


Alternativamente, uma versão mais mágica poderia abandonar inteiramente a palavra-chave await e confiar em um método mágico .awaited() em std :: Future, que não pode ser implementado por outros que escrevem seus próprios futuros, mas eu acho isso seria bastante contra-intuitivo e muito especial.

um novo método em std::Future , por exemplo fn awaited(self) -> Self::Output { await self }

Tenho certeza de que isso é impossível (sem tornar a função mágica), porque para esperar dentro dela, ela precisaria ser async fn awaited(self) -> Self::Output { await self } , que por sua vez ainda precisaria ser await ed. E se estamos pensando em tornar a função mágica, pode muito bem ser apenas a palavra-chave, IMO.

Já existe Future::wait (embora aparentemente o 0.3 ainda não o tenha?), Que executa um futuro bloqueando o thread.

O problema é que _o objetivo de await é _não_ bloquear o thread_. Se a sintaxe a aguardar for de prefixo e quisermos uma opção pós-fixada, ela deve ser uma macro pós-fixada e não um método.

E se você vai dizer para usar um método mágico, basta chamá-lo de .await() , que já foi discutido várias vezes no tópico, tanto como um método de palavra-chave quanto como um extern "rust-await-magic" "real mágico "fn.

EDIT: scottmcm ninja'd me e o GitHub não me informou (porque móvel?), Mas ainda vou deixar isso.

@scottmcm Você também seria capaz de pesquisar a contagem de frequência em que await parecia OK vs subótimo? Acho que uma pesquisa para contagem de frequência de prefixo vs pós-fixo pode ajudar a responder a várias perguntas.

  1. Tenho a impressão de que até agora o melhor caso de uso do postfix await foi
client.get("https://my_api").send() await?.json() await?

como muitos posts usar como exemplo. Talvez eu tenha esquecido alguma coisa, mas há outros casos? Não seria bom extrair essa linha em uma função se ela aparecesse com frequência em todo o código-base?

  1. Como discuti anteriormente, se algumas pessoas escreverem
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

o que eles fazem é ocultar exatamente os await visualmente, em vez de torná-los explícitos para que cada ponto de rendimento possa ser visto claramente .

  1. A palavra-chave Postfix exigiria a invenção de algo que não existe em outras línguas tradicionais. Se não oferecer um resultado significativamente melhor, não vale a pena inventar.

A espera é empurrada para o final da linha, enquanto nosso foco está principalmente concentrado no início (que é como quando você usa um mecanismo de pesquisa).

Obviamente, não posso refutar que as pessoas digitalizam o conteúdo da web usando um padrão em forma de F , @ dowchris97.

Não é certo, porém, que eles usam o mesmo padrão para o código. Por exemplo, este outro na página parece que pode corresponder melhor à forma como as pessoas procuram ? e, portanto, pode procurar await :

O padrão pontilhado consiste em pular grandes pedaços de texto e escanear como se procurasse algo específico, como um link, dígitos, uma palavra específica ou um conjunto de palavras com uma forma distinta (como um endereço ou assinatura).

Para comparação, vamos pegar o mesmo exemplo se ele retornou Result vez de impl Future :

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

Acho que é bem claro que um padrão de leitura em forma de F para um código como esse não ajuda a encontrar ? s, nem ajuda se ele está dividido em várias linhas, como

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

Acho que uma descrição análoga à sua poderia ser usada para argumentar que a posição pós-fixada está "escondendo exatamente o ? visualmente em vez de torná-lo explícito para que cada ponto de retorno possa ser visto claramente", que concordo que as preocupações originais sobre ? , mas parece não ter sido um problema na prática.

Portanto, no geral, acho que colocá-lo onde as pessoas já foram treinadas para digitalizar ? é a melhor maneira de garantir que as pessoas vejam. Eu definitivamente prefiro que eles não tenham que usar duas varreduras diferentes simultaneamente.

@scottmcm

Não é certo, porém, que eles usam o mesmo padrão para o código. Por exemplo, este outro na página parece que pode corresponder melhor à forma como as pessoas procuram ? e, portanto, pode procurar await :

O padrão pontilhado consiste em pular grandes pedaços de texto e escanear como se procurasse algo específico, como um link, dígitos, uma palavra específica ou um conjunto de palavras com uma forma distinta (como um endereço ou assinatura).

Certamente também não há evidências de que o uso de padrões pontilhados ajude a compreender o código melhor / mais rápido. Especialmente quando os seres humanos usam mais comumente o padrão de forma F, que mencionarei mais tarde.

Pegue esta linha como exemplo, quando você começar a usar o padrão pontilhado, suponha que você nunca tenha lido isso antes.

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Para mim, comecei quando vi async , depois vou primeiro await? , depois self.storage_coordinator.get_default_widget_async() , depois .identity , e finalmente percebi toda a linha é assíncrono. Eu diria que esta definitivamente não é a experiência de leitura de que gosto. Um dos motivos é que nosso sistema de linguagem escrita não tem esse tipo de salto intercalado para a frente e para trás . Quando você pula, ele interrompe a construção do modelo mental do que esta linha está fazendo.

Para efeito de comparação, o que esta linha está fazendo, como você sabe disso?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

Assim que alcancei await? , imediatamente tive um aviso de que isso é assíncrono. Então li let widget = await? , novamente, sem qualquer dificuldade, eu sabia que era assíncrono, algo estava acontecendo. Sinto que sigo o padrão da forma F. De acordo com https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/ , a forma F é o padrão mais comumente usado. Então, vamos projetar um sistema que se adapte à natureza humana ou inventar algum sistema que precise de educação especial e trabalhe contra nossa natureza ? Eu prefiro o primeiro. A diferença seria ainda mais forte quando as linhas assíncronas e normais fossem intercaladas assim

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

Devo procurar await? entre as linhas, sabe?

let id = id.try_or_else (|| Ok (self.storage_coordinator.try_get_default_widget () ?. identidade)) ?;

Por isso, não acho que a comparação seja boa. Primeiro, ? não muda tanto o modelo mental. Em segundo lugar, o sucesso de ? reside no fato de que não exige que você salte para a frente e para trás entre as palavras. A sensação que tenho quando leio ? em .try_get_default_widget()? é, "ok, você conseguiu um bom resultado com isso". É isso aí. Não preciso voltar atrás para ler outra coisa para entender esta linha.

Portanto, minha conclusão geral é que o postfix pode fornecer alguma conveniência limitada ao escrever código. No entanto, pode causar problemas maiores para a leitura de código, que eu defendo ser mais frequente do que escrever.

Como isso ficaria realmente com um estilo proposto rustfmt e destaque de sintaxe ( while -> async , match -> await ):

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

Não sei quanto a você, mas vejo match es instantaneamente.

(Ei @ CAD97 ,

@ dowchris97

Se você vai argumentar que ? não muda o modelo mental, o que há de errado com meu argumento de que await não deve mudar seu modelo mental do código? (É por isso que eu preferi opções de espera implícitas anteriormente, embora eu esteja convencido de que não é o ajuste certo para Rust.)

Especificamente, você leu async no _início_ do cabeçalho da função. Se a função for tão grande que não cabe em uma tela, é quase certo que é muito grande e você tem outros problemas de legibilidade, maiores do que encontrar os await pontos.

Uma vez que você é async , você precisa manter esse contexto. Mas nesse ponto tudo o que aguarda é, é uma transformação que transforma uma computação adiada Future no resultado Output estacionando o trem de execução atual.

Não deveria importar que isso signifique uma transformação de máquina de estado complicada. Esse é um detalhe de implementação. A única diferença dos threads do SO e do bloqueio é que outro código pode ser executado no thread atual enquanto você aguarda a computação adiada, portanto Sync não está protegendo você tanto. (Se houver alguma coisa, eu li isso como um requisito para async fn ser Send + Sync vez de inferido e com permissão para thread-inseguro.)

Em um estilo muito mais orientado a funções, seus widgets e resultados seriam semelhantes (sim, eu sei que isso não está usando uma Mônada e pureza real etc. me perdoe):

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

Mas porque essa é a ordem inversa do pipeline de processo, a multidão funcional inventou o operador "pipeline", |> :

    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;

E em Rust, esse operador de pipelining é . , que fornece o escopo do que pode ser pipeline e pesquisa direcionada ao tipo por meio da aplicação do método:

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

Quando você pensa em . como pipelining de dados da mesma maneira que |> , as cadeias mais longas geralmente vistas em Rust começam a fazer mais sentido e quando bem formatadas (como no exemplo da Centril) você as usa não perca a legibilidade porque você só tem um pipeline vertical de transformações nos dados.

await não diz "ei, isso é assíncrono". async sim. await é apenas como você estaciona e aguarda o cálculo adiado, e faz todo o sentido disponibilizá-lo para o operador de pipelining de Rust.

(Ei @Centril, você se esqueceu de tornar isso um async fn (ou while fn ), o que dilui um pouco meu ponto 😛)

se poderíamos ou não redefinir a invocação de macro

m!(item1, item2)

é o mesmo que

item1.m!(item2)

então podemos usar o estilo await prefix e postfix

await!(future)

e

future.await!()

@ CAD97

await não diz "ei, isso é assíncrono". async sim. await é apenas como você estaciona e aguarda o cálculo adiado, e faz todo o sentido disponibilizá-lo para o operador de pipelining de Rust.
Sim, acho que entendi bem, mas não escrevi com rigor.

Eu também entendo seu ponto. Mas não convencido, ainda acho que colocar await na frente será tremendamente melhor.

Eu sei que |> é usado em outras línguas para significar outra coisa , mas parece muito bom e extremamente claro para mim em Ferrugem no lugar do prefixo 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)
}

O argumento sobre a ordem de leitura se aplicaria igualmente bem ao operador ? substituindo try!() . Afinal, "ei, isso pode render" é importante, mas "ei, isso pode retornar mais cedo" também é importante, se não mais. E, de fato, preocupações sobre visibilidade foram levantadas repetidamente na discussão sobre bicicletas sobre ? (incluindo este tópico interno e este problema do GitHub ). Mas a comunidade acabou concordando em aprová-lo e as pessoas se acostumaram com isso. Seria estranho agora acabar com os modificadores ? e await aparecendo em lados opostos de uma expressão, só porque, basicamente, a comunidade mudou de ideia sobre a importância da visibilidade.

O argumento sobre a ordem de leitura se aplicaria igualmente bem ao operador ? substituindo try!() . Afinal, "ei, isso pode render" é importante, mas "ei, isso pode retornar mais cedo" também é importante, se não mais. E, de fato, preocupações sobre visibilidade foram levantadas repetidamente na discussão sobre bicicletas sobre ? (incluindo este tópico interno e este problema do GitHub ). Mas a comunidade acabou concordando em aprová-lo e as pessoas se acostumaram com isso. Seria estranho agora acabar com os modificadores ? e await aparecendo em lados opostos de uma expressão, só porque, basicamente, a comunidade mudou de ideia sobre a importância da visibilidade.

Não é realmente sobre o que seu código está fazendo. É sobre seu modelo mental do que seu código está fazendo, se o modelo pode ser facilmente construído ou não, se há choques ou não, se é intuitivo ou não. Eles podem diferir muito de um para outro.

Não quero discutir isso mais. Eu acho que a comunidade está longe de se decidir por uma solução postfix, embora muitas pessoas aqui possam apoiá-la. Mas acho que pode haver uma solução:

Mozilla constrói Firefox certo? É tudo sobre UI / UX! Que tal uma pesquisa séria de HCI sobre este problema? Portanto, podemos realmente nos convencer usando dados, não conjecturas.

@ dowchris97 Houve (apenas) duas instâncias de comparação de código do mundo real neste segmento: Uma comparação de ~ 24k linhas com bancos de dados e reqwest e uma comparação que exemplifica por que C # não é uma comparação precisa para basear a sintaxe do Rust . Ambos descobriram que no código Rust do mundo real, esperar no postfix parece mais natural e não sofre dos problemas que outras linguagens sem a semântica de movimento de valor natural têm. A menos que alguém apareça com outro exemplo do mundo real de tamanho decente que tende a mostrar o oposto, estou bastante convencido de que a sintaxe de prefixo é uma necessidade imposta por outras linguagens a si mesmas porque carecem da semântica de valor clara do pipelining de Rust (onde ler código idiomático da esquerda para a direita quase sempre faz exatamente o que o modelo mental sugere).

Editar: apenas se não estiver claro o suficiente, nenhum dos C #, Python, C ++, Javascript tem métodos de membro que recebem self por valor em vez de referência. C ++ tem o mais próximo com referências rvalue, mas a ordem do destruidor ainda é confusa em comparação com Rust.

Acho que o argumento de que await é melhor como um prefixo não deriva de como você tem que mudar seu modelo mental do código, mas sim de como temos palavras-chave com prefixo, mas não temos palavras-chave com pós-fixação pós-correções, a ferrugem usa sigilos como é exemplificado por ? ). É por isso que await foo() parece mais fácil de ler do que foo() await e porque algumas pessoas querem @ no final de uma declaração e não gostam de ter await lá.

Por uma razão semelhante, .await parece estranho de usar: o operador ponto é usado para acessar campos e métodos de uma estrutura (que também é porque ele não pode ser visto como um operador de pipeline puro), então tendo .await é como dizer "o ponto é usado para acessar campos e métodos de uma estrutura ou para acessar await , que não é um campo ou uma função".

Pessoalmente, gostaria de ver o prefixo await ou um sigilo pós-fixado (não precisa ser @ ).

Ambos descobriram que no código Rust do mundo real, esperar no postfix parece mais natural

Essa é uma declaração controversa. O exemplo reqwest apresenta apenas uma versão da sintaxe postfix.

Em uma nota diferente, se esta discussão se resumir a um voto de quem gosta mais do que, por favor mencione no reddit para que as pessoas não reclamem como fizeram com impl Trait em argumentos de função.

@ eugene2k Para a discussão fundamental de se o postfix se encaixa no modelo mental dos programadores Rust, a maioria ou todas as sintaxes do postfix estão aproximadamente na mesma escala em comparação ao prefixo. Eu não acho que haja uma diferença significativa de legibilidade entre as variantes pós-fixadas como entre prefixo e pós-fixadas. Veja também minha comparação de baixo nível de precedência de operador, que conclui que sua semântica é igual na maioria dos usos, então é uma questão de qual operador transmite melhor o significado (atualmente prefiro uma sintaxe de chamada de função real, mas não têm uma forte preferência sobre os outros).

@ eugene2k Decisões em Rust nunca são feitas por votação. Rust não é uma democracia, é uma meritocracia.

As equipes core / lang examinam todos os vários argumentos e perspectivas e então decidem. Essa decisão é tomada por consenso (entre os membros da equipe), não por votação.

Embora as equipes do Rust levem em consideração os desejos gerais da comunidade, em última análise, eles decidem com base no que acham que é melhor para o Rust a longo prazo.

A melhor maneira de influenciar Rust é apresentar novas informações, ou fazer novos argumentos, ou mostrar novas perspectivas.

Repetir argumentos existentes ou dizer "eu também" (ou similar) não aumenta as chances de uma proposta ser aceita. As propostas nunca são aceitas com base na popularidade.

Isso também significa que os vários votos positivos / negativos neste tópico não importam para qual proposta é aceita.

(Não estou me referindo a você especificamente, estou explicando como as coisas funcionam para o bem de todos neste tópico.)

@Pauan Já foi dito que as equipes core / lang olham para várias propostas e então decidem. Mas as decisões de "o que é mais fácil de ler" são decisões pessoais. Nenhum argumento lógico apresentado ao tomador de decisão mudará sua visão pessoal do que ele prefere melhor. Além disso, argumentos como 'é assim que as pessoas lêem os resultados da pesquisa' e 'um estudo foi conduzido por tais e tais pesquisas mostrou que as pessoas preferem isso e aquilo' são facilmente contestados (o que não é uma coisa ruim). O que pode mudar a mente do tomador de decisão é ver e não gostar dos resultados da aplicação de sua decisão em um contexto no qual ele não pensou. Então, quando todos esses contextos foram observados e os tomadores de decisão na equipe gostam de uma abordagem, enquanto a maioria dos outros usuários, que não estão na equipe, gostam de outra abordagem, qual deve ser a decisão final?

Então, quando todos esses contextos foram observados e os tomadores de decisão na equipe gostam de uma abordagem, enquanto a maioria dos outros usuários, que não estão na equipe, gostam de outra abordagem, qual deve ser a decisão final?

A decisão é sempre feita pela equipe. Período. É assim que as regras foram projetadas intencionalmente.

E os recursos são frequentemente implementados por membros da equipe. E os membros da equipe também estabeleceram confiança na comunidade. Portanto, eles têm autoridade de jure e de fato.

Se a situação mudar (talvez com base no feedback) e os membros da equipe mudarem de ideia, eles podem mudar sua decisão. Mas mesmo assim, a decisão é sempre feita pelos membros da equipe.

Como você disse, muitas vezes as decisões envolvem alguma subjetividade e, portanto, é impossível agradar a todos, mas uma decisão deve ser tomada. Para chegar a uma decisão, o sistema que Rust usa é baseado no consenso dos membros da equipe.

Qualquer discussão sobre se Rust deve ser controlado de forma diferente está fora do tópico e deve ser discutida em outro lugar.

(PS: Eu não estou nas equipes principais ou de idioma, então não tenho autoridade nesta decisão, então tenho que adiá-los, assim como você)

@HeroicKatora

Não acho que haja uma diferença significativa de legibilidade entre as variantes pós-fixadas

Discordo. Acho que foo().await()?.bar().await()? é mais fácil de ler do que foo() await?.bar() await? ou mesmo foo()@?.bar()@? Apesar disso, acho que ter métodos que não são realmente métodos abre um precedente ruim.

Eu gostaria de propor outra ideia. Eu concordo que o prefixo await não é fácil de encadear com outras funções. Que tal esta sintaxe postfix: foo(){await}?.bar()?{await} ? Não pode ser confundido com chamadas de função e parece-me bastante fácil de ler em cadeia.

E mais uma proposta escrita por mim. Vamos considerar a seguinte sintaxe de chamada de método:

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

O que o torna único entre as outras propostas:

  • Os colchetes tornam a precedência e o escopo muito mais limpos.
  • A sintaxe é extensível o suficiente para permitir a remoção temporária de ligações em outros contextos também.

Acho que a extensibilidade é a vantagem mais forte aqui. Esta sintaxe permitiria implementar recursos de linguagem que em sua forma comum não são possíveis no Rust devido à alta relação complexidade / utilidade. A lista de possíveis construções de linguagem é fornecida abaixo:

  1. Adiamento de todos os operadores de prefixo (incluindo await - é como deveria funcionar):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Funcionalidade do operador de pipeline:
// 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. Substituindo o retorno da função:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Funcionalidade de murchar:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. Divisão de corrente:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
  1. Macros Postfix:
let x = long().method().[dbg!(it)].chain();

Acho que a introdução de um novo tipo de sintaxe (campos mágicos, macros postfix, colchetes) tem um impacto maior na linguagem do que esse recurso sozinho e deve exigir um RFC.

Existe algum repositório que já usa fortemente await ? Eu me ofereceria para reescrever um trecho maior em cada estilo proposto, para que possamos ter uma ideia melhor de como eles se parecem e quão compreensível é o código.

Eu reescrevo em delimitadores obrigatórios:

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

É quase idêntico a await!() . Então, ficou lindo! Tendo usado await!() por um ou dois anos, por que você de repente inventaria o postfix await que não aparece em nenhum lugar na história da linguagem de programação.

Hã. A sintaxe expr.[await it.foo()] @ I60R com a palavra-chave contextual it é muito legal. Eu não esperava gostar de nenhuma nova proposta de sintaxe extravagante, mas isso é realmente muito bom, é um uso inteligente do espaço de sintaxe (porque IIRC .[ não é uma sintaxe válida em qualquer lugar) e resolveria muito mais problemas do que apenas esperar.

Concordou que definitivamente exigiria um RFC, e pode não ser a melhor opção. Mas eu acho que é outro ponto do lado de se contentar com uma sintaxe de prefixo para await por enquanto, com o conhecimento de que há uma série de opções para resolver o problema de "operadores de prefixo são difíceis de encadear" de uma forma mais geral que beneficia mais do que apenas async / espera no futuro.

E mais uma proposta escrita por mim. Vamos considerar a seguinte sintaxe de chamada de método:

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

O que o torna único entre as outras propostas:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

Acho que a extensibilidade é a vantagem mais forte aqui. Essa sintaxe permitiria implementar recursos de linguagem que em sua forma comum não são possíveis no Rust devido à alta relação complexidade / utilidade. A lista de possíveis construções de linguagem é fornecida abaixo:

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

Verdadeiramente incompreensível.

@tajimaha Olhando para o seu exemplo, acho que await {} pode realmente ser muito melhor do que await!() se formos com delimitadores obrigatórios, porque evita o problema de "muitos colchetes" que pode causar legibilidade problemas com a sintaxe await!() .

Comparar:
`` `c #
esperar {foo.bar (url, false, qux.clone ())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps você pode obter o destaque de sintaxe de async e await para exemplos simples, definindo o idioma para c #.)

@nicoburns Você pode usar qualquer um de () , {} ou [] com a macro.

@sgrif Esse é um bom ponto. E como esse é o caso, eu sugeriria que "palavra-chave de prefixo com delimitadores obrigatórios" faz muito pouco sentido como uma opção. Como ele quebra a consistência com outras macros basicamente sem benefício.

(FWIW, eu ainda acho que "prefixo sem delimitadores" com uma solução geral como macros postfix ou a sugestão de @ I60R para postfix faz mais sentido. Mas a opção "apenas ficar com a macro existente" está crescendo em mim ...)

Eu sugeriria que "palavra-chave de prefixo com delimitadores obrigatórios" faz muito pouco sentido como opção. Como ele quebra a consistência com outras macros basicamente sem benefício.

Por que isso faz pouco sentido e por que uma palavra-chave que não tem a mesma sintaxe de uma macro é um problema?

@tajimaha

Eu sugeriria que "palavra-chave de prefixo com delimitadores obrigatórios" faz muito pouco sentido como opção. Como ele quebra a consistência com outras macros basicamente sem benefício.

Por que isso faz pouco sentido e por que uma palavra-chave que não tem a mesma sintaxe de uma macro é um problema?

Bem, se await fosse uma macro, isso teria a vantagem de não adicionar nenhuma sintaxe extra à linguagem. Reduzindo assim a complexidade da linguagem. No entanto, há um bom argumento para usar uma palavra-chave: return , break , continue e outras construções de modificação de fluxo de controle também são palavras-chave. Mas todos eles funcionam sem delimitadores, portanto, para serem consistentes com essas construções, await também precisaria funcionar sem delimitadores.

Se você tiver aguardado com delimitadores obrigatórios, terá:

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

Isso é potencialmente confuso, pois agora você precisa se lembrar de 3 formas de sintaxe em vez de 2. E, visto que palavras-chave com delimitadores obrigatórios não oferecem nenhum benefício sobre a sintaxe de macro, acho que seria preferível apenas seguir o padrão sintaxe macro se desejarmos impor delimitadores (o que não estou absolutamente convencido de que devamos).

Uma pergunta para quem está usando muito async / await no Rust hoje: com que frequência você está aguardando funções / métodos vs variáveis ​​/ campo?

Contexto:

Eu sei que em C # é comum fazer coisas que se resumem a este padrão:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

Dessa forma, ambos funcionam em paralelo. (Alguns dirão que Task.WhenAll deve ser usado aqui, mas a diferença de desempenho é minúscula e torna o código mais confuso, pois requer a passagem de índices de array.)

Mas é meu entendimento que, em Rust, isso não funcionará realmente em paralelo, uma vez que poll para fooTask não seria chamado até que bar obtivesse seu valor, e _precisa_ usar um combinador, talvez

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

Portanto, estou curioso para saber se alguém regularmente acaba tendo o futuro em uma variável ou campo que você precisa aguardar, ou se está quase sempre aguardando expressões de chamada. Porque se for o último, há uma pequena variante de formatação da palavra-chave postfix que não discutimos realmente: palavra-chave postfix _no-space_.

Eu não pensei muito sobre se isso é bom, mas seria possível escrever código apenas

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

(Na verdade, não quero ter uma discussão rustfmt, como já disse, mas lembro-me de que uma das razões para não gostar da palavra-chave pós-fixada _ era_ o espaço separando os pedaços visuais)

Se continuarmos com isso, podemos também usar a sintaxe .await para alavancar
o poder do ponto, não?

Uma pergunta para quem está usando muito async / await no Rust hoje: com que frequência você está aguardando funções / métodos vs variáveis ​​/ campo?

Mas é meu entendimento que, no Rust, isso não funcionará realmente em paralelo [...]

Corrigir. Da mesma base de código de antes, aqui está este exemplo:

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

Se eu fosse reescrever o encerramento com um encerramento 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())?
};

Se eu mudasse para a sintaxe .await (e encadeasse):

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

Existe algum repositório que já usa muito o Wait? Eu me ofereceria para reescrever um pedaço maior em cada estilo proposto

@gralpli Infelizmente, nada que eu possa abrir o código usa pesadamente await! . Definitivamente, ele se presta mais ao código do aplicativo no momento (especialmente por ser tão instável).

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

Essas linhas estão mostrando exatamente como o código é bagunçado pelo uso excessivo de postfix e encadeamento .

Vamos ver a versão do prefixo:

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

As duas versões usam 7 linhas, mas a segunda é mais limpa. Existem também duas lições para usar delimitadores obrigatórios:

  1. O await { future }? não parece barulhento se future parte for longa. Veja let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. Quando a linha é curta, usar await(future) pode ser melhor. Veja Ok(await(req.json())?)

IMO, ao alternar entre as duas variantes, a legibilidade deste código é muito melhor do que antes.

O primeiro exemplo está formatado incorretamente. Não acho que o rustfmt formata como
este. Você poderia executar o rustfmt nele e publicá-lo novamente aqui?

@ivandardi @mehcode Você poderia fazer isso? Não sei como posso formatar a sintaxe .await . Acabei de copiar o código. Obrigado!

Eu gostaria de acrescentar que este exemplo mostra:

  1. O código de produção não será apenas cadeias simples e agradáveis ​​como:
client.get("https://my_api").send().await?.json().await?
  1. As pessoas podem abusar ou abusar do encadeamento.
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?;

Aqui, o fechamento assíncrono lida com cada ID, não tem nada a ver com o controle de nível superior futures_unordered . Colocá-los juntos reduz significativamente sua capacidade de entendê-lo.

Tudo _foi_ executado em rustfmt da minha postagem (com algumas pequenas alterações para torná-lo compilado). Ainda não está decidido onde .await? é colocado e atualmente coloco-o no final da linha que está sendo aguardada.


Agora eu concordo que tudo parece muito horrível. Este é um código escrito em um prazo e as coisas tendem a parecer horríveis quando você tem uma sala cheia de cozinheiros trabalhando para tirar algo.

Quero ressaltar (do seu ponto) que o prefixo abusivo pode parecer muito pior (na minha opinião, é claro):

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

Agora vamos nos divertir e torná-lo muito mais agradável usando retrospectiva e alguns novos adaptadores no futuro v0.3

Prefixo com precedência "óbvia" (e açúcar)
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();
Prefixo com precedência "útil"
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()?;
Prefixo com delimitadores obrigatórios
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()
}?;
Campo Postfix
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect().await?;
Palavra-chave Postfix
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect() await?;

Nit menor aqui. Não há TryStreamExt::and_then , provavelmente deveria haver. Parece um RP fácil para qualquer pessoa com tempo que deseja contribuir.


  • Quero, mais uma vez, expressar minha forte aversão por await? Em longas cadeias, perco completamente a noção de ? que aprendi a procurar no final das expressões para significar que essa expressão é falível e pode _exitir a função_.

  • Além disso, quero expressar minha crescente aversão por await .... ? (precedência útil), pois considero o que aconteceria se tivéssemos um fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

Quero ressaltar (do seu ponto) que o prefixo abusivo pode parecer muito pior (na minha opinião, é claro):

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

Isso não é realmente uma preocupação porque em python javascript, é mais provável que as pessoas escrevam em linhas separadas. Na verdade, não vi await (await f) em python.

Prefixo com delimitadores obrigatórios

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

Isso também parece estar de volta ao uso de combinadores. Embora o objetivo da introdução de async / await seja reduzir seu uso quando apropriado.

Bem, esse é o objetivo de ter o postfix esperando. Como se trata de Rust, é menos provável que as pessoas escrevam em linhas separadas, já que Rust incentiva o encadeamento. E para isso, a sintaxe postfix é essencialmente obrigatória para que o fluxo de instruções siga o mesmo fluxo de leitura de linha. Se não tivermos sintaxe postfix, então haverá muito código com temporários que também estão encadeados, enquanto se tivéssemos postfix await isso poderia ser reduzido a uma única cadeia.

@ivandardi @mehcode Copiando do Rust RFC em async / await:

Depois de ganhar experiência e feedback do usuário com o ecossistema baseado no futuro, descobrimos alguns desafios ergonômicos. Usar o estado que precisa ser compartilhado entre pontos de espera era extremamente não ergonômico - exigindo arcos ou encadeamento de junção - e embora os combinadores fossem mais ergonômicos do que escrever manualmente um futuro, eles ainda costumavam levar a conjuntos confusos de retornos de chamada aninhados e encadeados.

... use com um açúcar sintático que se tornou comum em muitas linguagens com IO assíncrono - as palavras-chave async e await.

Da perspectiva do usuário, ele pode usar async / await como se fosse um código síncrono e só precisa anotar suas funções e chamadas.

Portanto, o objetivo de introduzir o async await é reduzir o encadeamento e criar o código assíncrono como se eles estivessem sincronizados . O encadeamento é mencionado apenas duas vezes neste RFC, "exigindo arcos ou encadeamento de junção" e "ainda costuma levar a conjuntos confusos de callbacks aninhados e encadeados". Não parece muito positivo para mim.

Argumentar pelo encadeamento e, portanto, pela palavra-chave postfix, possivelmente precisaria de uma grande reescrita deste RFC.

@tajimaha Você entendeu mal o RFC. Ele está falando especificamente sobre os combinadores do Futuro ( map , and_then , etc.), não está falando sobre encadeamento em geral (por exemplo, métodos que retornam impl Future ).

Eu acho que é seguro dizer que async métodos serão bastante comuns, então o encadeamento é realmente muito importante.

Além disso, você entendeu mal o processo: o RFC não precisa ser reescrito. O RFC é um ponto de partida, mas não é uma especificação. Nenhum dos RFCs é definido em pedra (nem deveriam ser!)

O processo RFC é fluido, não é tão rígido quanto você está sugerindo. Nada no RFC nos impede de discutir o postfix await .

Além disso, quaisquer alterações serão colocadas na RFC de estabilização , não na RFC original (que já foi aceita e, portanto, não será alterada).

Argumentar pelo encadeamento e, portanto, pela palavra-chave postfix, possivelmente precisaria de uma grande reescrita deste RFC.

Estou brincando aqui.

Uma coisa provavelmente é verdade, ao escrever o RFC. As pessoas queriam especificamente uma nova ferramenta "que pudesse usar async / await como se fosse um código síncrono". A tradição que segue a sintaxe cumpriria melhor essa promessa.
E aqui você vê, não são combinadores futuros,

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

No entanto, "eles ainda costumavam levar a conjuntos confusos de retornos de chamada aninhados e encadeados".

As pessoas queriam especificamente uma nova ferramenta "que pudesse usar async / await como se fosse um código síncrono". A tradição que segue a sintaxe cumpriria melhor essa promessa.

Não vejo como isso é verdade: tanto o prefixo quanto o pós-fixado await atendem a esse desejo.

Na verdade, o postfix await provavelmente atende melhor a esse desejo, porque é muito natural com cadeias de métodos (que são muito comuns no código Rust síncrono!)

Usar o prefixo await incentiva fortemente muitas variáveis ​​temporárias, que geralmente não são o estilo idiomático do Rust.

No entanto, "eles ainda costumavam levar a conjuntos confusos de retornos de chamada aninhados e encadeados".

Vejo exatamente um encerramento, e não é um retorno de chamada, ele está apenas chamando map em um Iterator (nada a ver com Futuros!)

Por favor, não tente torcer as palavras do RFC para justificar o prefixo await .

Usar o RFC para justificar o prefixo await é muito estranho, porque o próprio RFC diz que a sintaxe não é final e será decidida posteriormente. A hora dessa decisão é agora.

A decisão será tomada com base nos méritos das várias propostas, o RFC original é completamente irrelevante (exceto como uma referência histórica útil).

Observe, o exemplo mais recente de @ mehcode está usando principalmente combinadores _stream_ (e o único combinador futuro poderia ser substituído por um bloco assíncrono). Isso é equivalente a usar combinadores de iteradores em código síncrono, portanto, pode ser usado em algumas situações quando são mais apropriados do que loops.

Isso é offtopic, mas a maior parte da conversa aqui está sendo mantida por cerca de uma dúzia de comentaristas. Dos 383 comentários até o momento em que analisei esta edição, havia apenas 88 pôsteres exclusivos. Em um esforço para evitar esgotar / sobrecarregar qualquer pessoa que tivesse que ler esses comentários, eu recomendaria ser o mais minucioso possível em seus comentários e certificar-se de que não seja uma reiteração de um ponto anterior.


Histograma dos comentários

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

Observe, o exemplo mais recente de @ mehcode está usando principalmente combinadores _stream_ (e o único combinador futuro poderia ser substituído por um bloco assíncrono). Isso é equivalente a usar combinadores de iteradores em código síncrono, portanto, pode ser usado em algumas situações quando são mais apropriados do que loops.

O mesmo pode ser argumentado aqui que eu posso / devo usar o prefixo await onde eles são mais apropriados do que o encadeamento.

@Pauan Aparentemente, não são apenas palavras muitos temporários, como reclamam os defensores do postfix (pelo menos neste caso). Além disso, suponha que seu código tenha uma cadeia de um liner com duas esperas, como posso depurar a primeira? (esta é uma pergunta verdadeira e não sei).
Em segundo lugar, a comunidade da ferrugem está se tornando maior, pessoas de origens diversas (como eu, eu uso python / c / java mais) não concordarão que cadeias de métodos são as melhores maneiras de fazer as coisas. Espero que, ao tomar uma decisão, não seja (não deveria ser) apenas baseado no ponto de vista dos primeiros usuários.

@tajimaha A maior mudança de clareza de pós-correção para prefixo parece estar usando um encerramento local para remover alguns argumentos de função aninhados. Isso não parece ser um prefixo exclusivo para mim, eles são bastante ortogonais. Você pode fazer o mesmo para o postfix, e acho que é ainda mais claro. Eu concordo que isso pode ser um uso indevido de encadeamento para algumas bases de código, mas não vejo como esse uso indevido é exclusivo ou conectado ao postfix de uma forma importante.

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

Mas, no pós-fixado, a let -binding e a Ok do resultado podem ser removidas juntas as últimas ? para fornecer diretamente o resultado e então o bloco de código também é desnecessário, dependendo no gosto pessoal. Isso não funciona bem no prefixo devido a duas esperas na mesma instrução.

Eu não entendo o sentimento regularmente declarado de que as ligações são unidiomáticas no código Rust. Eles são muito frequentes e comuns em exemplos de código, especialmente em torno do tratamento de resultados. Raramente vejo mais de 2 ? no código com o qual lido.

Além disso, o que idiomatic é muda ao longo da vida de uma linguagem, então eu tomaria muito cuidado ao usá-lo como um argumento.

Não sei se algo como isso foi sugerido antes, mas um prefixo await palavra-chave pode se aplicar a uma expressão inteira? Tomando o exemplo que foi mencionado antes:

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

onde get , send e json são assíncronos.

Para mim (tendo pouca experiência assíncrona em outras linguagens de programação), o postfix expr await parece natural: “Faça isso, _então_ aguarde o resultado.”

Houve algumas preocupações de que os exemplos a seguir pareciam estranhos:

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

No entanto, eu diria que isso deve ser dividido em várias linhas:

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

Isso é muito mais claro e tem a vantagem adicional de que await é fácil de detectar, se estiver sempre no final da linha.

Em um IDE, essa sintaxe não tem o "poder do ponto", mas ainda é melhor do que a versão do prefixo: quando você digita o ponto e percebe que precisa await , você só precisa excluir o ponto e digite " await ". Ou seja, se o IDE não oferece preenchimento automático para palavras-chave.

A sintaxe de ponto expr.await é confusa porque nenhuma outra palavra-chave de fluxo de controle usa um ponto.

Acho que o problema é que embora tenhamos encadeamento, que às vezes pode ser bonito, não devemos ir ao extremo dizendo que tudo deve ser feito em encadeamento. Devemos fornecer ferramentas também para programação em estilo C ou Python. Embora o Python quase não tenha nenhum componente de encadeamento, seu código é frequentemente elogiado por ser legível. Os programadores de Python também não reclamam que temos muitas variáveis ​​temporárias.

Que tal um postfix then ?

Eu não entendo o sentimento regularmente declarado de que as ligações são unidiomáticas no código Rust. Eles são muito frequentes e comuns em exemplos de código, especialmente em torno do tratamento de resultados. Raramente vejo mais de 2 ? no código com o qual lido.

Além disso, o que idiomatic é muda ao longo da vida de uma linguagem, então eu tomaria muito cuidado ao usá-lo como um argumento.

Isso me inspirou a pesquisar o código Rust atual onde há dois ou mais ? em uma linha (pode ser que alguém possa pesquisar o uso de várias linhas). Eu pesquisei xi-editor, alacritty, ripgrep, morcego, xray, fd, firecracker, yew, Rocket, exa, iron, parity-ethereum, tikv. Esses são projetos do Rust com a maioria das estrelas.

O que descobri é que aproximadamente apenas cerca de 40 linhas de 585562 linhas totais usam dois ou mais ? em uma linha. Isso é 0,006% .

Também quero destacar que estudar os padrões de uso de código existentes não revelará a experiência do usuário ao escrever um novo código.

Suponha que você receba um trabalho para interagir com uma nova API ou seja novo no uso de solicitações. É provável que você escreva

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

em apenas um tiro ? Se você é novo na API, duvido que queira ter certeza de construir a solicitação corretamente, ver o status de retorno, verificar sua suposição sobre o que esta API retorna ou apenas brincar com a API desta forma:

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

Uma API de rede não se parece em nada com os dados da memória, você não sabe o que está lá. Isso é bastante natural para a prototipagem. E, quando você está prototipando, você se preocupa se tudo dá certo, NÃO muitas variáveis ​​temporárias . Você pode dizer que podemos usar sintaxe postfix como:

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

Mas, se você já tem:

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

Tudo que você precisa fazer é provavelmente envolvê-lo em uma função e seu trabalho está feito, o encadeamento nem mesmo surge neste processo.

O que descobri é que aproximadamente apenas cerca de 40 linhas de um total de 585562 linhas usam duas ou mais? em uma linha.

Eu gostaria de sugerir que esta não é uma medida útil. O que realmente importa é mais do que um operador de fluxo de controle _por expressão_. De acordo com o estilo típico (rustfmt), eles quase sempre terminam em linhas diferentes no arquivo, embora pertençam à mesma expressão e, portanto, seriam encadeados na quantidade que o postfix (teoricamente) importa para await .

não devemos ir ao extremo dizendo que tudo deve ser feito no encadeamento

Alguém disse que tudo _deve_ ser feito com encadeamento? Tudo o que vi até agora é que deve ser _ergonômico_ encadear nos casos em que faz sentido, o mesmo que acontece no código síncrono.

apenas cerca de 40 linhas de 585562 linhas totais usam dois ou mais? em uma linha.

Não tenho certeza se isso é relevante para prefixo vs postfix. Notarei que _nenhum_ dos meus exemplos em C # de querer postfix incluiu vários await s em uma linha, nem mesmo vários await s em uma instrução. E o exemplo de @Centril de layout de postfix em potencial também não colocou vários await s em uma linha.

Uma comparação melhor pode ser com coisas encadeadas de ? , como estes exemplos do compilador:

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

Edit: Parece que você me venceu desta vez , @ CAD97 : ligeiramente_smiling_face:

Isso é chocantemente semelhante ao código de promessa de javascipt com muitos then s. Eu não chamaria isso de síncrono. É quase certo que o encadeamento tenha um await e finja ser síncrono.

Prefixo com delimitadores obrigatórios

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 Bom ponto. Eu sabia que há limitações para o que estou medindo:

(pode ser que alguém possa pesquisar o uso de várias linhas)

Estou fazendo isso porque @skade mencionou similaridade entre await e ? , então fiz uma análise rápida. Estou dando uma ideia que não estou fazendo uma pesquisa séria. Acho que uma análise mais detalhada seria difícil de fazer olhando para o código, certo? Pode ser necessário analisar e identificar expressões, com as quais não estou familiarizado. Espero que alguém possa fazer essa análise.

Alguém disse que tudo deve ser feito com encadeamento? Tudo o que vi até agora é que deve ser ergonômico encadear nos casos em que faz sentido, o mesmo que acontece no código síncrono.

O que estou dizendo é que, se apenas o postfix for adicionado, não seria ergonômico quando o estilo C / Python parece bom (claro que sim). Eu também abordo o encadeamento pode não ser necessário quando você protótipo .

Tenho a sensação de que a direção deste segmento está muito focada no encadeamento excessivo e em como tornar o código de espera o mais conciso possível.

Em vez disso, quero encorajar a todos a observar como o código assíncrono difere das variantes síncronas e como isso influenciará o uso e a utilização de recursos. Menos de 5 comentários nos 400 neste tópico mencionam essas diferenças. Se você não está ciente dessas diferenças, pegue a versão noturna atual e tente escrever um pedaço decente de código assíncrono. Incluindo tentar obter uma versão idiomática (não combinadora) assíncrona / esperar da parte da compilação de código que é discutida nos últimos 20 posts.

Fará a diferença se as coisas existem puramente entre pontos de rendimento / espera, se as referências são persistentes entre pontos de espera e, muitas vezes, alguns requisitos peculiares em torno de futuros tornam não possível escrever um código tão conciso quanto imaginado neste segmento. Por exemplo, não podemos colocar funções assíncronas arbitrárias em combinadores arbitrários, porque eles podem não funcionar com tipos !Unpin . Se criarmos futuros a partir de blocos assíncronos, eles podem não ser diretamente compatíveis com combinadores como join! ou select! , porque eles precisam de tipos fixados e fundidos, portanto, chamadas adicionais para pin_mut! e .fuse() pode ser exigido dentro.

Além disso, para trabalhar com blocos assíncronos, os novos utilitários baseados em macro funcionam join! e select! funcionam muito melhor do que as antigas variantes do combinador. E estes são da maneira excessiva que é frequentemente fornecida como exemplos

Eu não sei como postfix await pode funcionar com .unwrap() neste exemplo de tokio mais simples

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

Se o prefixo for adotado, ele se tornará

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

Mas se o postfix for adotado,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

Existe alguma explicação intuitiva que possamos dar aos usuários? Está em conflito com as regras existentes. await é um campo? ou await é uma associação que tem um método chamado unwrap() ? QUE PENA! Desdobramos muito quando iniciamos um projeto. Violação de várias regras de design no The Zen of Python.

Casos especiais não são especiais o suficiente para quebrar as regras.
Se a implementação for difícil de explicar, é uma má ideia.
Diante da ambigüidade, recuse a tentação de adivinhar.

Existe alguma explicação intuitiva que possamos dar aos usuários? Está em conflito com as regras existentes. await é um campo? ou await é uma ligação que tem um método chamado unwrap() ? QUE PENA! Desdobramos muito quando iniciamos um projeto. Violação de várias regras de design no The Zen of Python.

Eu diria que, embora haja muitos documentos em docs.rs chamando unwrap , unwrap deve ser substituído por ? em muitos casos do mundo real. Pelo menos, esta é a minha prática.

O que descobri é que aproximadamente apenas cerca de 40 linhas de um total de 585562 linhas usam duas ou mais? em uma linha.

Eu gostaria de sugerir que esta não é uma medida útil. O que realmente importa é mais do que um operador de fluxo de controle _por expressão_. De acordo com o estilo típico (rustfmt), eles quase sempre terminam em linhas diferentes no arquivo, embora pertençam à mesma expressão e, portanto, seriam encadeados na quantidade que o postfix (teoricamente) importa para await .

Eu nego que possa haver limite para esta abordagem.

Pesquisei novamente por xi-editor, alacritty, ripgrep, morcego, xray, fd, foguete, teixo, Foguete, exa, ferro, paridade-ethereum, tikv. Esses são projetos do Rust com a maioria das estrelas. Desta vez, procurei o padrão:

xxx
  .f1()
  .f2()
  .f3()
  ...

e se há vários operadores de fluxo de controle nessas expressões.

Identifiquei APENAS 15 das 7066 cadeias com vários operadores de controle de fluxo. Isso é 0,2% . Essas linhas abrangem 167 de 585562 linhas de código. Isso é 0,03% .

@cenwangumass Obrigado por

Uma consideração é que, uma vez que Rust tem religação variável com let, pode ser um argumento convincente para o prefixo await , em que se usado consistentemente, você teria uma linha de código separada para cada await espera -ponto. A vantagem é dupla: os rastreamentos de pilha, já que o número da linha dá maior contexto de onde o problema ocorreu, e a facilidade do ponto de interrupção de depuração, já que é comum desejar definir um ponto de interrupção em cada ponto de espera separado para inspecionar variáveis, pode superar a brevidade de uma única linha de código.

Pessoalmente, estou dividido entre o estilo de prefixo e o sigilo pós-fixo, embora depois de ler https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 Eu provavelmente sou a favor de um sigilo pós-fixo.

Estilo de prefixo renderizado:

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

Estilo Postfix renderizado:

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

Sigilo postfix renderizado @:

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

Sigilo postfix renderizado nº:

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

Não vi pessoas suficientes falando sobre await por Stream s. Embora esteja fora do escopo, tomar a decisão em torno de await com um pouco de previsão de Stream pode valer o esforço.

Provavelmente precisaremos de algo como estes:
Sintaxe de prefixo

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Sintaxe Postfix

for response in stream await {
    let response = response?;
    ...
}
for response in stream.await!() {
    let response = response?;
    ...
}

// Or a specialized variant of `await` and `?`
//     Note (Not Obvious): The `?` actually applies to each response of `await`
for response in stream await? {
    ...
}
for response in stream.await!()? {
    ...
}

Possivelmente sintaxe mais ergonômica / consistente

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

Eu só quero ter certeza de que esses exemplos sejam discutidos pelo menos um pouco, porque embora o postfix seja bom para encadear Future , à primeira vista ele parece o menos intuitivo para Stream . Não tenho certeza de qual é a solução certa aqui. Talvez postfix await para Future e um estilo diferente de await para Stream ? Mas, para sintaxes diferentes, precisaríamos garantir que a análise não seja ambígua.

Esses são apenas meus pensamentos iniciais, pode valer a pena dar ao Stream usecase um pouco mais de reflexão de uma perspectiva pós-fixada. Alguém tem alguma ideia sobre como o postfix pode funcionar bem com Stream , ou se deveríamos ter duas sintaxes?

A sintaxe para iteração de fluxo não é algo que acontecerá por muito tempo, eu imagino. É bastante fácil usar um loop while let e fazê-lo manualmente:

Campo Postfix
while let Some(value) = stream.try_next().await? {
}
Prefixo com Precedência "Consistente" e Açúcar
while let Some(value) = await? stream.try_next() {
}
Prefixo com delimitadores obrigatórios
while let Some(value) = await { stream.try_next() }? {
}

Seria a maneira atual de fazer as coisas (mais await ).


Notarei que este exemplo faz com que "prefixo com delimitadores" pareça especialmente ruim para mim. Embora await(...)? possa parecer um pouco melhor, se fôssemos fazer "prefixo com delimitadores", esperaria que permitíssemos apenas _um_ tipo de delimitador (como try { ... } ).

Tangente rápida, mas não seria "esperar com delimitadores" apenas ... prefixo normal esperar? await espera uma expressão, portanto, await expr e await { expr } são essencialmente iguais. Não faria sentido apenas aguardar com delimitadores e não tê-los sem também, especialmente porque qualquer expressão pode ser circundada por {} e continuar sendo a mesma expressão.

A questão sobre streams me levou a pensar que, para Rust, poderia ser bastante natural ter a espera disponível não apenas como um operador em expressões, mas também como um modificador em padrões:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

que então pode trabalhar naturalmente com for

for async x in my_stream { ... }

@ivandardi

O problema que "prefix aguarda com delimitadores obrigatórios" resolve é a questão de precedência em torno de ? . Com delimitadores _ obrigatórios_, não há questão de precedência.

Veja try { .... } para sintaxe _similar_ (estável). Try tem delimitadores obrigatórios pelo mesmo motivo - como ele interage com ? - já que ? dentro das chaves é muito diferente do que fora.

@yazaddaruvala Eu não acho que deveria haver uma maneira específica assíncrona de lidar com um loop for com resultados mais agradáveis, que deveria vir de algum recurso genérico que também lida com Iterator<Item = Result<...>> .

// postfix syntax
for response in stream await {
    ...
}

Isso implica que stream: impl Future<Output = Iterator> e você está esperando que o iterador esteja disponível, não cada elemento. Não vejo muita chance de algo melhor do que async for item in stream (sem um recurso mais geral como as menções de


@ivandardi a diferença é a precedência, uma vez que você começa a encadear no final dos delimitadores, aqui estão dois exemplos que analisam de forma diferente com "prefixo de precedência óbvio", "prefixo de precedência útil" (pelo menos meu entendimento de qual é a precedência "útil") e "prefixo de delimitadores obrigatórios".

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

que analisam (com delimitadores suficientes para que não sejam ambíguos para todas as três variantes)

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

Para minha mente, @scottmcm 's exemplos de C # no https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 olhar muito bem com os delimitadores movidos do (await foo).bar posição para a posição await(foo).bar :

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Et cetera. Esse layout é familiar para chamadas de função normais, familiar para fluxo de controle baseado em palavras-chave existente e familiar para outras linguagens (incluindo C #). Isso evita quebrar o orçamento de estranheza e não parece causar problemas para o encadeamento pós- await .

Embora admitidamente tenha o mesmo problema de try! para encadeamento multi- await , isso não parece ser tão importante quanto era para Result ? Eu preferiria limitar a estranheza da sintaxe aqui do que acomodar um padrão relativamente incomum e indiscutivelmente ilegível.

e familiarizado com outras linguagens (incluindo C #)

Não é assim que a precedência funciona em C # (ou Javascript ou Python)

await(response.Content.ReadAsStringAsync()).Should().Be(text);

é equivalente a

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

(o operador ponto tem precedência mais alta do que await , então vincula mais fortemente, mesmo se você tentar fazer await parecer uma chamada de função).

Eu sei, essa não é a afirmação que eu estava fazendo. Apenas que a ordem permaneça a mesma, então a única coisa que as pessoas precisam adicionar são parênteses, e que (separadamente) a sintaxe de chamada de função existente tem a precedência correta.

Este layout é [...] familiar ao fluxo de controle existente com base em palavras-chave

Discordo. Na verdade, _consideramos_ o uso desse layout no fluxo de controle existente com base em palavras-chave:

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

E rustfmt muda isso para return (4); , _adicionando em_ um espaço.

Isso realmente não afeta o que estou tentando fazer e parece que estou perdendo a cabeça. return (4) , return(4) , nada mais é do que uma questão de estilo.

Eu li a edição inteira e comecei a sentir que {} colchetes e prefixo estão bem, mas então ele desapareceu

Vamos ver:

let foo = await some_future();
let bar = await {other_future()}?.bar

Parece bom, não é? Mas e se quisermos encadear mais await em uma cadeia?

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

IMHO parece muito pior do que

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

No entanto, dito isso, não acredito em encadeamento assíncrono, como escrito na postagem de . Aqui está meu exemplo de uso de async/await em um hiper servidor, por favor, mostre onde o encadeamento pode ser útil usando este exemplo concreto. Estou muito tentado, sem brincadeira.


Sobre os exemplos anteriores, a maioria deles está "corrompida" de alguma forma por combinadores. Você quase nunca precisa deles desde que você espere. Por exemplo

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

é apenas

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

se os futuros podem retornar um erro é tão simples como:

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 ,

Eu criei o segmento de pré-RFC para a sintaxe val.[await future] em https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await -chaining / 9304

As questões de procedimento de https://internals.rust-lang.org

Acho que temos dois campos nesta discussão: pessoas que gostariam de enfatizar os pontos de rendimento versus pessoas que gostariam de não enfatizá-los.

Assíncrono em Rust, por volta de 2018 contém a seguinte sinopse:

A notação Async / await é uma maneira de fazer a programação assíncrona se parecer mais com a programação síncrona.

Pegando o exemplo de tokio simples acima (alterando unwrap() para ? ):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

e aplicar sigilo postfix leva a

let response = client.get(uri).timeout(Duration::from_secs(10))!?

que se assemelha muito à programação síncrona e não tem await intercalados de notação de prefixo e pós-fixação.

Eu usei ! como sigilo postfix neste exemplo, embora essa sugestão tenha me dado alguns votos negativos em um comentário anterior. Fiz isso porque o ponto de exclamação tem um significado inerente para mim neste contexto que tanto @ (que meu cérebro lê como "em") e # não têm. Mas isso é simplesmente uma questão de gosto e não é o meu ponto.

Eu simplesmente prefiro qualquer sigilo pós-fixo de caractere único a todas as outras alternativas, precisamente porque é muito discreto, tornando mais fácil entender o que o código que você está lendo está realmente fazendo versus se é ou não assíncrono, o que Eu consideraria um detalhe de implementação. Eu não diria que não é importante, mas argumentaria que é muito menos importante para o fluxo do código do que o retorno antecipado no caso de ? .

Colocando de outra forma: você se preocupa principalmente com os pontos de rendimento ao escrever código assíncrono, não tanto ao lê-lo. Como o código é lido com mais freqüência do que escrito, uma sintaxe discreta para await seria útil.

Gostaria de lembrar a experiência da equipe C # (os destaques são meus):

É também por isso que não escolhemos nenhuma forma 'implícita' para 'esperar'. Na prática, era algo sobre o qual as pessoas queriam pensar com muita clareza e que queriam primeiro e centralizado em seu código, para que pudessem prestar atenção nele. Curiosamente, mesmo anos depois, essa tendência se manteve. isto é, às vezes, muitos anos depois, lamentamos que algo seja excessivamente prolixo . Alguns recursos são bons assim no início, mas uma vez que as pessoas se sintam confortáveis ​​com eles, são mais adequados para algo mais simples. Esse não foi o caso com 'esperar'. As pessoas ainda parecem realmente gostar da natureza pesada dessa palavra-chave

Personagens sigilosos são quase invisíveis sem o realce adequado e são menos amigáveis ​​para os recém-chegados (como eu provavelmente disse antes).


Mas falando sobre sigilos, @ provavelmente não confundirá quem não fala inglês (por exemplo, eu) porque não lemos como at . Temos um nome completamente diferente para ele, que não dispara automaticamente quando você lê o código, então você apenas o entende como um hieróglifo completo, com seu próprio significado.

@huxi Tenho a sensação de que os sigilos já foram descartados. Consulte a postagem da Centril novamente para ver as razões pelas quais devemos usar a palavra await .


Além disso, para todos que não gostam de postfix await, e usam o argumento pragmático de "bem, não há muito código da vida real que possa ser encadeado, portanto, postfix await não deve ser adicionado", aqui está uma pequena anedota (que provavelmente irei massacrar devido à má memória):

Na Segunda Guerra Mundial, uma das nações em conflito estava perdendo muitos aviões lá fora. Eles tinham que reforçar seus aviões de alguma forma. Portanto, a maneira óbvia de saber onde eles devem se concentrar é olhar para os aviões que voltaram e ver onde as balas atingem mais. Acontece que, em um avião, em média 70% dos furos estavam nas asas, 10% na área do motor e 20% em outras áreas do avião. Então, com essas estatísticas, faria sentido reforçar as asas, certo? Errado! A razão para isso é que você está apenas olhando para os aviões que voltaram. E nesses aviões, parece que o dano da bala nas asas não é tão ruim. No entanto, todos os aviões que voltaram sofreram apenas pequenos danos na área dos motores, o que pode levar à conclusão de que grandes danos na área dos motores são fatais. Portanto, a área do motor deve ser reforçada.

Meu ponto com esta anedota é: talvez não haja muitos exemplos de código da vida real que possam tirar vantagem do postfix await porque não há postfix await em outras linguagens. Portanto, todos estão acostumados a escrever código de espera de prefixo e estão muito acostumados com isso, mas nunca saberemos se as pessoas começariam o encadeamento de espera mais se tivéssemos o postfix de espera.

Portanto, acho que o melhor curso de ação seria aproveitar a flexibilidade que as compilações noturnas nos fornecem e escolher uma sintaxe de prefixo e uma sintaxe de pós-fixação para adicionar à linguagem. Eu voto para o prefixo "Precedência Óbvia" esperar, e o postfix .await esperar. Essas não são as opções finais de sintaxe, mas precisamos escolher uma para começar, e acho que essas duas opções de sintaxe proporcionariam uma experiência mais recente em comparação com outras opções de sintaxe. Depois de implementá-los durante a noite, podemos obter estatísticas de uso e opiniões sobre como trabalhar com código real usando ambas as opções e podemos continuar a discussão de sintaxe, desta vez apoiada em um argumento pragmático melhor.

@ivandardi A anedota é bastante pesada e IMHO não se encaixa: é um conto motivador no início de uma jornada, para lembrar as pessoas de procurarem o não-óbvio e cobrir todos os ângulos, _não_ deve ser usado contra a oposição em uma discussão. Foi adequado para Rust 2018, onde foi levantado e não vinculado a um problema específico. Para usar isso contra o outro lado em um debate é indelicado, você precisa afirmar a posição da pessoa que vê mais ou tem mais visão para fazer isso funcionar. Não acho que é isso que você quer. Além disso, permanecendo na imagem, talvez o postfix não esteja em nenhum dos idiomas porque o postfix nunca chegou em casa;).

As pessoas realmente mediram e olharam: nós temos um operador acorrentado em Rust ( ? ) que raramente é usado para encadeamento. https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

Também foi bem abordado que esta é _apenas_ uma medição do estado atual, como @cenwangumass colocou muito bem aqui: https://github.com/rust-lang/rust/issues/57640#issuecomment -457962730. Portanto, não é como se as pessoas estivessem usando isso como números finais.

Eu quero uma história onde o encadeamento se torne um estilo dominante se o postfix for realmente o caminho a percorrer. Não estou convencido disso, entretanto. Eu também mencionei acima que o exemplo dominante usado por aqui ( reqwest ) requer apenas 2 awaits porque a API escolheu assim e uma API em cadeia conveniente sem a necessidade de 2 awaits pode ser facilmente construída hoje.

Embora eu aprecie a necessidade de uma fase de pesquisa, gostaria de salientar que a espera já está gravemente atrasada, qualquer fase de pesquisa tornará isso pior. Também não temos como coletar estatísticas em muitas bases de código, teríamos que montar um conjunto de falantes. Eu adoraria mais pesquisas de usuários aqui, mas isso leva tempo, configuração que ainda não existe e pessoas para executá-la.

@Pzixel por causa de deixar a religação, acho que você poderia escrever

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

Como

`` `
deixe foo = esperar algum_futuro ();
deixar bar = esperar {outro_futuro ()} ?. bar_async ();
deixar bar = esperar {bar} ?;

@Pzixel Você tem alguma fonte para essa citação de experiência da equipe C #? Porque o único que consegui encontrar foi este comentário . Isso não é uma acusação ou algo assim. Eu gostaria apenas de ler o texto completo.

Meu cérebro traduz @ para "at" por causa de seu uso em endereços de e-mail. Esse símbolo é chamado de "Klammeraffe" na minha língua nativa, que se traduz aproximadamente como "macaco-agarrador". Na verdade, eu aprecio que meu cérebro tenha se contentado com "em".

Meus 2 centavos, como um usuário Rust relativamente novo (com experiência em C ++, mas isso não ajuda muito).

Alguns de vocês mencionaram recém-chegados, aqui está o que eu acho:

  • em primeiro lugar, uma macro await!( ... ) parece indispensável para mim, já que é muito simples de usar e não pode ser mal interpretada. Parece semelhante a println!() , panic!() , ... e foi isso que aconteceu com try! afinal.
  • Os delimitadores obrigatórios também são simples e inequívocos.
  • uma versão pós-fixada, seja campo, função ou macro, não seria difícil de ler ou escrever IMHO, uma vez que os novatos diriam apenas "ok, é assim que você faz". Este argumento também é válido para a palavra-chave postfix, que é "incomum", mas "por que não".
  • em relação à notação de prefixo com precedência útil, acho que ficaria confuso. O fato de await ficar mais firme vai impressionar algumas pessoas e eu acho que alguns usuários irão preferir colocar muitos parênteses para deixar isso claro (encadeamento ou não).
  • precedência óbvia sem açúcar é simples de entender e ensinar. Então, para introduzir o açúcar em torno dele, basta chamar await? uma palavra-chave útil. Útil porque elimina a necessidade de parênteses pesados:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids? espera ... unwraps the future, so you have to use ? to unwrap the Result // but there is some sugar if you want, thanks to the await? `Operador
    deixe a resposta = esperar? http :: get ("https://www.rust-lang.org/");
    // mas você não deve encadear, porque esta sintaxe não leva a um código encadeado legível
- 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")
};

Como ensinar isso:

  • ( await!() macro possível)
  • prefixo recomendado quando nenhum encadeamento acontece, com açúcar (veja acima)
  • Postfix recomendado com encadeamento
  • é possível misturá-los, mas não recomendado
  • possível usar prefixo ao encadear com combinadores

Por experiência própria, eu diria que o prefixo esperar não é um problema para encadeamento.
O encadeamento acrescenta muito no código Javascript com prefixo await e combinadores como f.then(x => ...) sem perder qualquer legibilidade na minha opinião e eles não parecem sentir qualquer necessidade de trocar combinadores por postfix await.

Façam:

let ret = response.await!().json().await!().to_string();

é o mesmo que:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

Eu realmente não vejo os benefícios do postfix await acima das cadeias combinatórias.
Acho mais fácil entender o que acontece no segundo exemplo.

Não vejo nenhum problema de legibilidade, encadeamento ou precedência no código a seguir:

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

Eu seria a favor desse prefixo esperar porque:

  • da familiaridade com outras línguas.
  • do alinhamento com as demais palavras-chave (retornar, quebrar, continuar, render, etc ...).
  • ainda parece Rust (com combinadores como fazemos com iteradores).

Async / await será uma grande adição à linguagem e acho que mais pessoas usarão mais coisas relacionadas ao futuro após essa adição e sua estabilização.
Alguns podem até descobrir a programação assíncrona pela primeira vez com o Rust.
Portanto, pode ser benéfico manter a complexidade da linguagem baixa, enquanto os conceitos por trás do assíncrono já podem ser difíceis de aprender.

E eu não acho que enviar prefixo e postfix await seja uma boa ideia, mas esta é apenas uma opinião pessoal.

@huxi Sim, já https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Meu cérebro traduz @ para "at" devido ao seu uso em endereços de e-mail. Esse símbolo é chamado de "Klammeraffe" na minha língua nativa, que se traduz aproximadamente como "macaco-agarrador". Na verdade, eu aprecio que meu cérebro tenha se contentado com "em".

Tenho uma história parecida: na minha língua é "cachorro", mas não afeta a leitura de emails.
É bom ver sua experiência.

@llambda a questão era sobre encadeamento. Claro que você pode apenas introduzir variáveis ​​extras. Mas de qualquer maneira, este await { foo }? vez de await? foo ou foo await? parece lobisomem.

@totorigolo
Bela postagem. Mas não acho que sua segunda sugestão seja um bom caminho a seguir. Quando você introduz duas maneiras de fazer algo, você apenas produz confusão e problemas, por exemplo, você precisa da opção rustfmt ou seu código se torna uma bagunça.

@Hirevo async/await supostamente removem a necessidade em combinadores. Não vamos voltar a "mas você pode fazer isso apenas com combinadores". Seu código é o mesmo que

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

Então, vamos remover async/await completamente?

Dito isto, os combinadores são menos expressivos, menos convenientes e às vezes você simplesmente não consegue expressar o que await poderia, por exemplo, empréstimo entre pontos de espera (para que Pin foi projetado).

Não vejo nenhum problema de legibilidade, encadeamento ou precedência no código a seguir

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

Eu faço:

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

Algo estranho está acontecendo? Sem problemas:

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

É mais complicado fazê-lo funcionar com combinadores. Sem mencionar que suas funções ? em then / map não funcionarão como esperado e seu código não funcionará sem into_future() e alguns outros coisas estranhas que você não precisa no fluxo de async/await .

Concordo que minha proposição não é a ideal, pois introduz muita sintaxe legal usando a palavra-chave async . Mas acredito que as regras são fáceis de entender e que satisfazem todos os nossos casos de uso.

Mas, novamente, isso significaria muitas variações permitidas:

  • await!(...) , para consistência com try!() e macros bem conhecidas como println!()
  • await , await(...) , await { ... } , ie. sintaxe de prefixo sem açúcar
  • await? , await?() , await? {} , ie. sintaxe de prefixo com açúcar
  • ... await , ou seja, sintaxe pós-fixada (mais ... await? , mas que não é açúcar)
  • todas as combinações deles, mas que não são incentivadas (veja meu post anterior )

Mas, na prática, esperamos apenas ver:

  • prefixo sem Result s: await , ou await { ... } para esclarecer com expressões longas
  • prefixo com Result s: await? , ou await? {} para esclarecer com expressões longas
  • postfix ao encadear: ... await , ... await?

+1 para enviar, aguarde! () Macro para estável. Amanhã, se possível 😄

Há muita especulação sobre como esse padrão _será_ usado e preocupações com a ergonomia nesses casos. Agradeço essas preocupações, mas não vejo um motivo convincente para que essa alteração não seja iterativa. Isso permitirá a geração de métricas de uso real que podem informar posteriormente a otimização (assumindo que seja necessário).

Se a adoção do Rust assíncrono é um esforço maior para grandes caixas / projetos do que qualquer esforço de refatoração _opcional_ posterior para mover para uma sintaxe mais concisa / expressiva, então eu defendo fortemente que permitamos que esse esforço de adoção comece agora mesmo. A incerteza contínua está causando dor.

@Pzixel
(Primeiro, cometi um erro ao chamar a função fetch_user, vou corrigi-lo neste post)

Não estou dizendo que async / await seja inútil e que devemos removê-lo para combinadores.
Await permite associar um valor de um futuro a uma variável no mesmo escopo depois de resolvido, o que é realmente útil e nem mesmo possível com combinadores sozinhos.
Remover o await não era meu ponto.
Eu acabei de dizer que combinadores podem funcionar muito bem com await e que o problema de encadeamento pode ser resolvido apenas aguardando a expressão inteira construída por combinadores (removendo a necessidade de await (await fetch("test")).json() ou await { await { fetch("test") }.json() } ).

No meu exemplo de código, ? se comporta como pretendido por curto-circuito, fazendo com que o fechamento retorne um Err(...) , não a função inteira (essa parte é controlada pelo await? em toda a cadeia).

Você efetivamente reescreveu meu exemplo de código, mas removeu a parte de encadeamento dele (fazendo ligações).
Por exemplo, para a parte de depuração, o seguinte tem exatamente o mesmo comportamento com apenas o dbg necessário! e nada mais (nem mesmo parênteses extras):

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

Não sei como fazer o mesmo sem combinadores e sem ligações adicionais ou parênteses.
Algumas pessoas fazem o encadeamento para evitar preencher o escopo com variáveis ​​temporárias.
Então, eu só queria dizer que os combinadores podem ser úteis e não devem ser ignorados ao se tomar uma decisão sobre uma sintaxe específica a respeito de sua capacidade de encadeamento.

E, por último, por curiosidade, por que o código não funcionaria sem .into_future() , eles já não são futuros (não sou especialista nisso, mas espero que já sejam futuros)?

Eu gostaria de destacar um problema significativo com fut await : ele mexe seriamente com a forma como as pessoas lêem o código. Existe um certo valor em expressões de programação serem semelhantes a frases usadas em uma linguagem natural, esta é uma das razões pelas quais temos construções como for value in collection {..} , porque na maioria das linguagens escrevemos a + b ("a mais b") em vez de a b + , e escrever / ler "espera algo" é muito mais natural para o inglês (e outros idiomas SVO ) do que "algo espera". Imagine que, em vez de ? , teríamos usado um postfix try palavra-chave: let val = foo() try; .

fut.await!() e fut.await() não têm esse problema porque se parecem com chamadas de bloqueio familiares (mas "macro" enfatiza adicionalmente a "mágica" associada), então eles serão percebidos de forma diferente de um espaço separado palavra-chave. O sigilo também será percebido de forma diferente, de forma mais abstrata, sem paralelos diretos com as frases da linguagem natural, e por isso acho irrelevante como as pessoas lêem os sigilos propostos.

Concluindo: se for decidido manter a palavra-chave, acredito fortemente que devemos escolher apenas variantes de prefixo.

@rpjohnst Não tenho certeza de qual é o seu ponto, então. actual_fun(a + b)? e break (a + b)? importam para mim por causa da precedência diferente, então eu não sei o que await(a + b)? deveria ser.

Tenho acompanhado essa discussão de longe e tenho algumas perguntas e comentários.

Meu primeiro comentário é que acredito que a macro .await!() postfix satisfaz todos os objetivos principais do Centril, com exceção do primeiro ponto:

" await deve permanecer uma palavra-chave para habilitar o design da linguagem futura."

Que outros usos da palavra-chave await vemos no futuro?


Edit : Eu não entendi exatamente como await funções. Tirei minhas afirmações incorretas e atualizei os exemplos.

Meu segundo comentário é que a palavra-chave await em Rust faz coisas completamente diferentes de await em outro idioma e isso tem o potencial de causar confusão e condições de corrida inesperadas.

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;
}

Eu acredito que um prefixo async faz um trabalho muito mais claro, indicando que os valores assíncronos são partes de uma máquina de estado construída por compilador:

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;
}

É intuitivo que as funções async permitem que você use valores async . Isso também cria a intuição de que há um mecanismo assíncrono funcionando em segundo plano e como ele pode estar funcionando.

Esta sintaxe tem algumas questões não resolvidas e problemas claros de composição, mas deixa claro onde o trabalho assíncrono está acontecendo e serve como um complemento de estilo síncrono para o mais combinável e encadeado .await!() .

Tive dificuldade em perceber o postfix await no final de algumas expressões em exemplos de estilo procedural anteriores, então aqui está como ficaria com esta sintaxe proposta:

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

(Há também o argumento a ser feito para uma macro .dbg!() facilmente combinável e encadeada, mas isso é para um fórum diferente.)

Na terça-feira, 29 de janeiro de 2019 às 23h31min32s-0800, Sphericon escreveu:

Tenho acompanhado essa discussão de longe e tenho algumas perguntas e comentários.

Meu primeiro comentário é que acredito que a macro .await!() postfix satisfaz todos os objetivos principais do Centril, com exceção do primeiro ponto:

" await deve permanecer uma palavra-chave para habilitar o design da linguagem futura."

Como um dos principais proponentes da sintaxe .await!() , eu
absolutamente acho que await deve permanecer uma palavra-chave. Dado que vamos
precisa de .await!() para ser construído no compilador de qualquer maneira, parece
trivial.

@Hirevo

Você efetivamente reescreveu meu exemplo de código, mas removeu a parte de encadeamento dele (fazendo ligações).

Eu não entendo essa abordagem de "encadeamento por encadeamento". O encadeamento é bom quando é bom, por exemplo, não cria variáveis ​​temporárias inúteis. Você diz "você removeu o encadeamento", mas pode ver que isso não fornece nenhum valor aqui.

Por exemplo, para a parte de depuração, o seguinte tem exatamente o mesmo comportamento com apenas o dbg necessário! e nada mais (nem mesmo parênteses extras)

Não, você fez isso

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

Não é a mesma coisa.

Eu acabei de dizer que os combinadores podem funcionar muito bem com o await e que o problema de encadeamento pode ser resolvido apenas aguardando a expressão inteira construída pelos combinadores (removendo a necessidade de await (await fetch ("teste")). Json () ou await {await {fetch ("test")} .json ()}).

É apenas um problema para o prefixo await , ele não existe para outras formas dele.

Não sei como fazer o mesmo sem combinadores e sem ligações adicionais ou parênteses.

Por que não criar essas ligações? Bindings são sempre melhores para o leitor, por exemplo, e se você tiver um depurador que poderia imprimir um valor de binding, mas que não pudesse avaliar uma expressão.

Finalmente, você não removeu realmente nenhuma ligação. Você acabou de usar a sintaxe labmda |user| onde eu usei let user = ... . Não economizou nada, mas agora este código é mais difícil de ler, mais difícil de depurar e não tem acesso ao pai (por exemplo, você tem que envolver os erros externamente na cadeia de métodos em vez de fazê-lo chamar a si mesmo, provavelmente).


Resumindo: o encadeamento não fornece valor por si só. Pode ser útil em alguns cenários, mas não é um deles. E como escrevo código assíncrono / aguardo há mais de seis anos, acredito que você não queira escrever código assíncrono encadeado nunca . Não porque você não pudesse, mas porque é inconveniente e uma abordagem vinculativa é sempre mais agradável de ler e, freqüentemente, de escrever. Não quer dizer que você não precisa de combinadores. Se você tem async/await , não precisa dessas centenas de métodos em futures / streams , você só precisa de dois: join e select . Todo o resto pode ser feito por meio de iteradores / ligações / ..., ou seja, ferramentas de linguagem comum, que não fazem com que você aprenda mais uma infraestrutura.

A comunidade await como palavra-chave ou sigilo, portanto, imho suas ideias sobre async exigem outro RFC.

Meu segundo comentário é que a palavra-chave await em Rust faz coisas completamente diferentes de await em outro idioma e isso tem o potencial de causar confusão e condições de corrida inesperadas.

Você pode ser mais detalhado? Não vi nenhuma diferença entre JS / C # await's, exceto futuros baseados em enquetes, mas realmente não tem nada a ver com async/await .

Com relação à sintaxe de prefixo await? proposta em vários lugares aqui:
`` `C #
deixe foo = esperar? 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, o prefixo await? parece uma sintaxe que é muito especial para mim.

) * editado.

Como isso ficaria com futuros de futuros? Ou seja, seria arbitrariamente extensível:

let foo = await?? double_trouble();

IOW, await? parece uma sintaxe muito especial para mim.

@rolandsteiner por "futuros de futuros", você quer dizer impl Future<Output = Result<Result<_, _>, _>> (um await + dois ? implica em "desembrulhar" um único futuro e dois resultados para mim, sem esperar futuros aninhados )

await? _is_ um caso especial, mas é um caso especial que provavelmente se aplicará a mais de 90% dos usos de await . Todo o propósito dos futuros é uma forma de esperar nas operações assíncronas de _IO_, IO é falível, então 90% + de async fn provavelmente retornará io::Result<_> (ou algum outro tipo de erro que inclua uma variante IO ) Funções que retornam Result<Result<_, _>, _> são muito raras atualmente, então eu não esperaria que exigissem sintaxe especial.

@ Nemo157 Você tem razão, é claro: Resultado dos Resultados. Atualizei meu comentário.

Hoje nós escrevemos

  1. await!(future?) para future: Result<Future<Output=T>,E>
  2. await!(future)? para future: Future<Output=Result<T,E>>

E se escrevermos await future? , temos que descobrir qual deles significa.

Mas será que o caso 1 sempre pode se transformar no caso 2? No caso 1, a expressão produz um futuro ou um erro. Mas o erro pode ser adiado e movido no futuro. Portanto, podemos apenas lidar com o caso 2 e fazer uma conversão automática acontecendo aqui.

Do ponto de vista do programador, Result<Future<Output=T>,E> garante o retorno antecipado para o caso de erro, mas exceto que os dois têm o mesmo sementic. Eu posso imaginar que o compilador pode funcionar e evitar a chamada adicional de poll se o caso de erro estiver imediato.

Portanto, a proposta é:

await exp? pode ser interpretado como await (exp?) se exp for Result<Future<Output=T>,E> , e interpretado como (await exp)? se exp for Future<Output=Result<T,E>> . Em ambos os casos, ele retornará antecipadamente com erro e retornará ao resultado verdadeiro se estiver funcionando bem.

Para casos mais complicados, podemos aplicar algo como o método automático de desreferência do receptor:

> Ao interpretar await exp???? , primeiro verificamos exp e se é Result , try isso e continuamos quando o resultado ainda é Result até ficar sem ? ou ter algo que não seja Result . Então tem que ser um futuro e nós await nele e aplicar o resto ? s.

Eu era um defensor de palavras-chave / sigilos do Postfix e ainda sou. No entanto, só quero mostrar que a precedência do prefixo pode não ser um grande problema na prática e tem soluções alternativas.

Eu sei que os membros da equipe Rust não gostam de coisas implícitas, mas, em tal caso, há muito pouca diferença entre os potenciais semenciais e temos uma boa maneira de garantir que fazemos a coisa certa.

await? é um caso especial, mas é um caso especial que provavelmente se aplicará a mais de 90% dos usos de await . Todo o objetivo dos futuros é uma forma de esperar nas operações assíncronas de IO, IO é falível, então 90% + de async fn provavelmente retornará io::Result<_> (ou algum outro tipo de erro que inclua uma variante IO ) Funções que retornam Result<Result<_, _>, _> são muito raras atualmente, então eu não esperaria que elas exigissem sintaxe especial.

Casos especiais são ruins para compor, expandir ou aprender e, eventualmente, se transformam em bagagem. Não é um bom meio-termo fazer exceções às regras da linguagem para um único caso de uso teórico de usabilidade.

Seria possível implementar Future para Result<T, E> where T: Future ? Dessa forma, você poderia apenas await result_of_future sem precisar desembrulhar com ? . E isso, é claro, retornaria um Resultado, então você o chamaria de await result_of_future , o que significaria (await result_of_future)? . Dessa forma, não precisaríamos da sintaxe await? e a sintaxe do prefixo seria um pouco mais consistente. Avise-me se houver algo de errado com isso.

Argumentos adicionais para await com delimitadores obrigatórios incluem (pessoalmente, não tenho certeza de qual sintaxe eu gosto mais no geral):

  • Nenhum caso especial do operador ? , nenhum await? ou await??
  • Congruente com os operadores de controle de fluxo existentes, como loop , while e for , que também exigem delimitadores obrigatórios
  • Sente-se mais em casa com construções Rust existentes semelhantes
  • Eliminar maiúsculas e minúsculas especiais ajuda a evitar problemas ao escrever macros
  • Não usa sigilos ou postfix, evitando gastos do orçamento de estranheza

Exemplo:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

No entanto, depois de brincar com isso em um editor, parece ainda complicado. Acho que prefiro await e await? sem delimitadores, como com break e return .

Seria possível implementar Futuro para Resultadoonde T: Futuro?

Você desejaria o inverso. O mais comum a aguardar é um futuro em que seu tipo de saída é um resultado.

Há então o argumento explícito contra não ocultar ou absorver ? apenas em espera. E se você quiser coincidir com o resultado, etc.

Se você tiver um Result<Future<Result<T, E2>>, E1> , aguardando ele retornaria um Result<Result<T, E2>, E1> .

Se você tiver um Future<Result<T, E1>> , aguardar por ele simplesmente retornará o Result<T, E1> .

Não há como ocultar ou absorver ? na espera, e você pode fazer o que for necessário com o Resultado depois.

Oh. Devo ter entendido mal você então. Não vejo como isso ajude, pois ainda precisamos combinar ? com espera 99% do tempo.


Oh. A sintaxe await? supostamente implica (await future)? que seria o caso comum.

Exatamente. Então, nós apenas tornaríamos o vínculo await mais firme em await expr? , e se essa expressão for um Result<Future<Result<T, E2>>, E1> então seria avaliado como algo do tipo Result<T, E2> . Isso significaria que não há nenhum caso especial para aguardar em tipos de resultado. Ele apenas segue as implementações normais do traço.

@ivandardi e quanto a Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Note, essa forma de futuro se foi. Existe um único tipo associado agora, Saída (que provavelmente será um Resultado).


@ivandardi Ok. Eu vejo agora. A única coisa que você teria contra você é que a precedência é algo estranho que você teria que aprender lá, pois é um desvio, mas o mesmo acontece com await , suponho.

Embora um Resultado que retorna um futuro seja tão raro que eu não encontrei um caso além de algo no núcleo do tokio que foi removido, então não acho que precisamos de impls sugar / trait para ajudar nesse caso.

@ivandardi e quanto a Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Bem, eu presumiria que isso não é possível, visto que o traço Future tem apenas um tipo associado Output .


@mehcode Bem, ele responde a algumas das preocupações que foram levantadas anteriormente, eu diria. Também ajuda a decidir sobre a sintaxe do prefixo, porque haveria apenas uma sintaxe de espera de prefixo em vez das opções "Precedência óbvia" e "Precedência útil".

Bem, eu suporia que isso não seja possível, visto que o traço Futuro tem apenas um tipo associado de Saída.

Por que não?

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 Veja https://doc.rust-lang.org/std/future/trait.Future.html

Você está falando sobre a velha característica que estava na caixa futures .

Honestamente, não acho que ter uma palavra-chave na posição de prefixo como deveria ser:

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

tem qualquer vantagem forte sobre ter um sigilo na mesma posição:

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

Acho que só parece inconsistente com outros operadores de prefixo, adiciona espaços em branco redundantes antes da expressão e desloca o código para o lado direito a uma distância perceptível.


Podemos tentar usar sigilo com sintaxe de ponto estendida ( Pré-RFC ), que resolve problemas com escopos profundamente aninhados:

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

bem como adiciona a possibilidade de métodos em cadeia:

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

E, obviamente, vamos substituir * por @ que faz mais sentido aqui:

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

O que eu gosto aqui é que @ reflete em await que é colocado no LHS da declaração de função, enquanto ? reflete em Result<User> que é colocado no RHS de declaração de função. Isso torna @ extremamente consistente com ? .


Alguma opinião sobre isso?

Alguma opinião sobre isso?

Qualquer aparelho extra é desnecessário

@mehcode Sim, não percebi que agora falta o tipo Error futuros. Mas a questão ainda é válida: você pode ter uma função, que provavelmente retorne um recurso, que, quando concluído, pode retornar resultado ou erro.

Alguma opinião sobre isso?

Sim! De acordo com o comentário de Centril , sigilos não são muito agradáveis. Eu só começaria a considerar sigilos se alguém pudesse criar uma regex que identificasse todos os pontos de espera.

Quanto à sua proposta de sintaxe de ponto estendida, você teria que explicá-la com muito mais profundidade, fornecendo a semântica e desavenhando cada uso dela. No momento, não consigo entender o significado dos trechos de código que você postou com essa sintaxe.


Mas a questão ainda é válida: você pode ter uma função, que provavelmente retorne um recurso, que, quando concluído, pode retornar resultado ou erro.

Então você teria um Future<Item=Result<T, E>> , certo? Bem, nesse caso, você apenas ... espera o futuro e lida com o resultado 😐

Suponha que foo seja do tipo Future<Item=Result<T, E>> . Então await foo será um Result<T, E> e você pode usar o seguinte para lidar com os erros:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

Então você teria um futuro>, certo? Bem, nesse caso, você apenas ... espera o futuro e lida com o resultado 😐

Não, quero dizer Result<Future<Item=Result<T, E>>, OtherE>

Com a variante postfix, você apenas faz

let bar = foo()? await?.bar();

@melodicstream Eu atualizei minha postagem e adicionei um link para o pré-RFC para esse recurso

Nossa ... Acabei de reler todos os comentários pela terceira vez ...

O único sentimento consistente é minha antipatia pela notação pós-fixada .await em todas as suas variações, porque eu esperava seriamente que await fizesse parte de Future com essa sintaxe. O "poder do ponto" pode funcionar para um IDE, mas não funciona para mim. Eu certamente poderia me adaptar a ela se fosse a sintaxe estabilizada, mas duvido que algum dia ela realmente pareça "certa" para mim.

Eu escolheria a notação de prefixo await super básica sem quaisquer colchetes obrigatórios, principalmente por causa do princípio KISS e porque fazê-lo semelhante à maioria das outras linguagens vale muito.

Desaçúcar await? future para (await future)? seria bom e apreciado, mas qualquer coisa além disso parece cada vez mais como uma solução sem um problema para mim. A religação let simples melhorou a legibilidade do código na maioria dos exemplos e eu, pessoalmente, provavelmente seguiria esse caminho ao escrever o código, mesmo se o encadeamento fácil fosse uma opção.

Com isso dito, estou muito feliz por não estar em posição de decidir sobre isso.

Agora vou deixar esse assunto de lado, em vez de adicionar mais ruído e aguardar (trocadilho intencional) o veredicto final.

... pelo menos vou honestamente tentar fazer isso ...

@Pzixel Supondo que foo seja do tipo Result<Future<Item=Result<T, E1>>, E2> , então await foo seria do tipo Result<Result<T, E1>, E2> e então você pode simplesmente lidar com esse Resultado de acordo.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream não, não vai. Você não pode esperar Result, você pode esperar Future. So you have to do foo ()? to unwrap Futuro from Resultado , then do esperar to get a result, then again ? `Para desembrulhar o resultado do futuro.

No modo postfix, será foo? await? , no prefixo ... Não tenho certeza.

Portanto, seus exemplos simplesmente não funcionam, especialmente o último, porque deveria ser

(await foo.unwrap()).unwrap()

No entanto, @huxi pode estar certo de que estamos resolvendo o problema que provavelmente não existe. A melhor maneira de descobrir é permitir macro postfix e ver a base de código real após a adoção básica de async/await .

@Pzixel É por isso que fiz a proposta de implementar Future em todos os tipos de Result<Future<Item=T>, E> . Fazer isso permitiria o que estou dizendo.

Embora esteja OK com await foo?? para Result<Future<Output=Result<T, E1>>, E2> , NÃO estou feliz com await foo.unwrap().unwrap() . No meu primeiro modelo cerebral, isso tem que ser

(await foo.unwrap()).unwrap()

Caso contrário, ficarei muito confuso. A razão é que ? é um operador geral e unwrap é um método. O compilador pode fazer algo especial para operadores como . , mas se for um método normal, assumirei que está sempre relacionado à expressão mais próxima apenas em seu lado esquerdo.

A sintaxe pós-fixada, foo.unwrap() await.unwrap() , também é válida para mim, pois sei que await é apenas uma palavra-chave, não um objeto, portanto, deve fazer parte da expressão antes de unwrap() .

A macro de estilo postfix resolve muito desses problemas muito bem, mas apenas a questão de se queremos manter a familiaridade com as linguagens existentes e mantê-lo prefixado. Eu votaria no estilo postfix.

Estou certo de que o código a seguir não é igual:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

e

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

O primeiro entrará em pânico ao tentar criar um futuro, enquanto o segundo entrará em pânico na primeira votação. Em caso afirmativo, devemos fazer algo com ele? Pode ser contornado como

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

Mas é muito menos conveniente.

@Pzixel : esta é uma decisão que foi tomada. async fn são totalmente preguiçosos e não funcionam até serem pesquisados ​​e, portanto, capturam todos os tempos de vida de entrada para todo o tempo de vida futuro. A solução alternativa é uma função de agrupamento que retorna impl Future sendo explícito e usando um bloco async (ou fn) para o cálculo adiado. Isso é _requisito_ para empilhar Future combinadores sem alocação entre cada um, pois após a primeira votação eles não podem ser movidos.

Esta é uma decisão tomada e porque os sistemas de espera implícitos não funcionam (bem) para o Rust.

Desenvolvedor C # aqui, e vou defender o estilo de prefixo, então prepare-se para votar negativamente.

A sintaxe do Postfix com um ponto ( read().await ou read().await() ) é muito enganosa e sugere um acesso de campo ou invocação de método, e await não é nenhuma dessas coisas; é um recurso de linguagem. Não sou um Rustáceo experiente de forma alguma, mas não conheço nenhum outro .something caso que seja efetivamente uma palavra-chave que será reescrita pelo compilador. O mais próximo que consigo pensar é o sufixo ? .

Tendo assíncrono / aguardado em C # por alguns anos, a sintaxe padrão de prefixo com um espaço ( await foo() ) não me causou nenhum problema durante todo esse tempo. Nos casos em que um await aninhado é necessário, não é oneroso usar parênteses, que é a sintaxe padrão para todas as precedências de operador explícitas e, portanto, fácil de entender, por exemplo.

`` `c #
var list = (espera 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()

Isso parece estranho.

Acho que a consistência com outras línguas também é algo que deve ser considerado. No momento, uma palavra-chave await antes da expressão assíncrona é a sintaxe para C #, VB.NET, JavaScript e Python, e é a sintaxe proposta para C ++ também. São cinco dos sete principais idiomas do mundo; C e Java ainda não têm async / await, mas ficaria surpreso se eles fossem de outra maneira.

A menos que haja realmente boas razões para ir uma maneira diferente, ou, dito de outra forma, se tanto prefixo e postfix pesar o mesmo em termos de vantagens e compromissos, eu sugiro que há muito a ser dito para o extra-oficialmente " padrão "sintaxe.

@markrendle Também sou dev C #, escrevendo async/await desde 2013. E gosto de prefixo também. Mas Rust é muito diferente.

Você provavelmente sabe que seu (await Foo()).ToList() é um caso raro. Na maioria dos casos, você apenas escreve await Foo() e, se precisar de outro tipo, provavelmente deseja corrigir a assinatura Foo .

Mas a ferrugem é outra coisa. Temos ? aqui e temos que propagar os erros. Em C # async / await quebra exceções automaticamente, lança-as entre pontos de await e assim por diante. Você não tem ferrugem. Considere que cada vez que você escreve await em C #, você deve escrever

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

É muito tedioso ter todos esses aparelhos.

OTOH com postfix que você tem

var response = client.GetAsync("www.google.com") await.HandleException();
var json =  response.ReadAsStreamAsync() await.HandleException();
var somethingElse = DoMoreAsyncStuff(json) await.HandleException();
...

ou em termos de ferrugem

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

E, imaine, teremos outro transformador, além de ? e await , vamos chamá-lo de @ . Se inventarmos uma sintaxe extra para await? , então teremos que ter await@ , @? , await@? , ... para torná-lo consistente.


Eu amo o prefixo await também, mas não tenho certeza se Rust está bem com ele. Ter regras extras adicionadas ao idioma pode ser ok, mas é um fardo que provavelmente não vale a pena facilitar a leitura.

Releia todos os comentários, incluindo o rastreamento do tópico do problema (passei quase 3 horas nisso). E meu resumo é: o postfix @ parece ser a melhor solução, e infelizmente é a menos apreciada.

Há muitas opiniões (tendenciosas) sobre isso, e abaixo estão meus comentários sobre elas:


async-await não estaria familiarizado com o postfix @

Na verdade, seria apenas meio estranho porque a mesma sintaxe async fn seria fornecida e a mesma funcionalidade de await ainda estaria disponível.

Rust tem muitos sigilos e não devemos introduzir um outro sigilo

Mas os sigilos estão bem até que sejam legíveis e consistentes com outra sintaxe. E a introdução de um novo sigilo não significa estritamente que tornará as coisas de alguma forma piores. Na perspectiva de consistência, o postfix @ é uma sintaxe muito melhor do que qualquer prefixo / postfix await .

Os trechos como ?!@# me lembram Perl

Na realidade, raramente veríamos qualquer outra combinação além de @? que parece bastante simples e ergonômica. Tipos de retorno como impl Try<impl Future<T>> , impl Future<impl Try<impl Try<T>>> e até impl Future<T> , ocorreriam no máximo em 10% dos usos, como poderíamos ver em exemplos do mundo real ITT. Além disso, o conglomerado de sigilos em 90% desses 10% indicaria um cheiro de código quando você deve consertar sua API ou introduzir uma ligação temporária para esclarecê-la.

Postfix @ é tão difícil de notar!

E isso é realmente uma grande vantagem no contexto de async-await . Esta sintaxe introduz a capacidade de expressar código assíncrono como se fosse síncrono, portanto, await mais intrusivo apenas interfere em seu propósito original. Os exemplos acima mostram claramente que a ocorrência de pontos de rendimento pode ser densa o suficiente para inchar o código quando são muito explícitos. Claro que devemos tê-los à vista, entretanto @ sigilo seria perfeitamente adequado para isso, assim como await sem inchar o código.

Esperar o futuro é uma operação cara e devemos dizer explicitamente que

Realmente faz sentido, porém não acho que devamos dizer isso com tanta frequência e tão alto. Acho que poderia haver analogia com a sintaxe de mutação: mut é exigido explicitamente apenas uma vez e, além disso, podemos usar a vinculação mutável implicitamente. Outra analogia pode ser fornecido com a sintaxe inseguro: explícita unsafe é necessária apenas uma vez e ainda podemos excluir a referência ponteiros crus, chamar funções inseguras, etc. E ainda uma outra analogia poderia ser fornecido com o operador de bolha: explícita impl Try return ou o próximo bloco try são necessários apenas uma vez e, posteriormente, podemos usar o operador ? com confiança. Assim, da mesma forma, async explícito anota operações possivelmente duráveis ​​apenas uma vez e, além disso, podemos aplicar o operador @ que realmente faria isso.

É estranho porque eu leio de forma diferente de await , e não consigo digitar facilmente

Se você leu & como "ref" e ! como "não", então não acho que como você leu @ atualmente é um forte argumento para não usar isso símbolo. Também não importa o quão rápido você digita, porque a leitura do código é sempre uma prioridade mais importante. Em ambos os casos, a adoção de @ é muito simples.

É ambíguo porque @ já é usado na correspondência de padrões

Não acho que isso seja um problema porque ! já é usado em macros, & já é usado em empréstimos e em && operador, | é usado nos fechamentos, nos padrões e no operador || , + / - pode ser aplicado na posição do prefixo e . pode ser usado em contextos mais diferentes. Não há nada de surpreendente e nada de errado em reutilizar os mesmos símbolos em diferentes construções de sintaxe.

Seria difícil fazer grep

Se você quiser verificar se algumas fontes contêm padrões assíncronos, rg async seria uma solução muito mais simples e direta. Se você realmente quiser encontrar todos os pontos de rendimento dessa maneira, poderá escrever algo como rg "@\s*[;.?|%^&*-+)}\]]" que não é simples, mas também não é algo estonteante. Considerando a freqüência com que será necessário, IMO esta regex é absolutamente normal.

@ não é amigável para iniciantes e é difícil pesquisar no Google

Acho que é realmente mais fácil de entender para iniciantes porque funciona de forma consistente com o operador ? . Ao contrário, o prefixo / pós-fixado await não seria consistente com outra sintaxe do Rust, além de introduzir dor de cabeça com associatividade / formatação / significado. Eu também não acho que await acrescentaria algum valor autodocumentável porque uma única palavra-chave não pode descrever todo o conceito subjacente e os iniciantes iriam aprender com o livro de qualquer maneira. E Rust nunca foi uma linguagem que fornece dicas de texto simples para cada uma das possíveis construções de sintaxe. Se alguém esquecer o que significa o símbolo @ (eu realmente duvido que seja um problema porque também há muitas associações para lembrá-lo corretamente) - então, pesquisá-lo no Google também deve ser bem-sucedido, porque atualmente rust @ symbol retorna todas as informações relevantes sobre @ na correspondência de padrões.

Já reservamos a palavra-chave await , então devemos usá-la de qualquer maneira

Por quê? É normal ter uma palavra-chave reservada não usada quando se constata que a implementação do recurso final é melhor sem ela. Além disso, nenhuma promessa foi feita de que exatamente await palavra-chave seria introduzida. E esse identificador não é muito comum no código do mundo real, então não perderíamos nada quando ele permaneceria reservado até a próxima edição.

? foi um erro e @ seria um segundo erro

Provavelmente você pensa mais lexicalmente e por isso prefere trabalhar com palavras em vez de símbolos. E por causa disso você não vê nenhum valor na sintaxe mais fina. Mas tenha em mente que mais pessoas pensam mais visualmente e, para elas, palavras intrusivas são mais difíceis de trabalhar. Provavelmente, um bom compromisso aqui seria fornecer await!() macro ao lado, como try!() foi fornecido juntamente com ? , portanto, você seria capaz de usá-lo em vez de @ se você estiver realmente desconfortável com @ .

^^^^^^^ você releu meu comentário também?

Eu tive uma grande revelação ao ler o comentário de @ I60R que me fez a favor do sigilo postfix, e eu não gostei de usar um sigilo até agora (ao invés disso, prefiro alguma forma de postfix aguardar).

Nossa operação de espera não é await . Pelo menos não da maneira que await é usado em C # e JavaScript, as duas maiores fontes de familiaridade async / await .

Nessas linguagens, a semântica de iniciar um Task (C #) / Promise (JS) é que a tarefa seja imediatamente colocada na fila de tarefas para ser executada. Em C #, este é um contexto multi-threaded, em JS é um único thread.

Mas em qualquer uma dessas linguagens, await foo semanticamente significa "estacione esta tarefa e aguarde a conclusão da tarefa foo , e enquanto isso os recursos computacionais sendo usados ​​por esta tarefa podem ser usados ​​por outros tarefas ".

É por isso que você obtém paralelismo com await s separados após construir vários Task / Promise s (mesmo em executores de thread único): a semântica de await aren Não "execute esta tarefa", mas sim "execute alguma tarefa" até terminar, aguardando e podermos continuar.

Isso é totalmente separado do início preguiçoso ou ansioso-até-o-primeiro-aguarde de tarefas quando criadas, embora fortemente relacionado. Estou falando sobre enfileirar o resto da tarefa após a conclusão do trabalho síncrono.

Já temos confusão ao longo dessa linha em usuários de iteradores preguiçosos e futuros atuais. Suponho que a sintaxe await não está nos ajudando em nada aqui.

Nosso await!(foo) portanto, não é await foo no modo "tradicional" C # ou JS. Então, qual linguagem está mais relacionada à nossa semântica? Eu agora acredito firmemente que é ( @matklad corrija-me se eu entendi errado) suspend fun Kotlin. Ou, como foi referido nas discussões adjacentes a Rust, "assíncrono explícito, espera implícita".

Breve revisão: em Kotlin, você só pode chamar suspend fun de dentro de um contexto suspend . Isso executa imediatamente a função chamada até a conclusão, suspendendo o contexto empilhado conforme exigido pela função filha. Se quiser executar o filho suspend fun em paralelo, crie o equivalente a fechamentos assíncronos e os combine com um combinador suspend fun para executar vários fechamentos suspend em paralelo. (É um pouco mais complicado do que isso.) Se você deseja executar um filho suspend fun em segundo plano, você cria um tipo de tarefa que coloca essa tarefa no executor e expõe um .await() suspend fun método para juntar sua tarefa de volta com a tarefa em segundo plano.

Esta arquitetura, embora descrita com verbos diferentes do Future de Rust, soa muito familiar.

Eu já expliquei por que esperar implícito não funciona para Rust e mantenha essa posição. Precisamos que async fn() -> T e fn() -> Future<T> sejam equivalentes em uso para que o padrão "faça algum trabalho inicial" seja possível (para fazer vidas funcionarem em casos complicados) com futuros preguiçosos. Precisamos de futuros preguiçosos para que possamos empilhar futuros sem uma camada de indireção entre cada um para fixação.

Mas agora estou convencido de que a sintaxe "explícita, mas silenciosa" de foo@ (ou algum outro sigilo) faz sentido. Por mais que eu deteste dizer a palavra aqui, @ , neste caso, é uma operação monádica em async assim como ? é a operação monádica em try .

Há alguma opinião que espalhar ? em seu código é apenas para fazer o compilador parar de reclamar quando você retorna um Result . E, de certa forma, você está fazendo isso. Mas é para preservar nosso ideal de clareza de código local. Preciso saber se essa operação é descompactada por meio da mônada ou tomada literalmente localmente.

(Se eu errar os detalhes da mônada, tenho certeza de que alguém vai me corrigir, mas acredito que meu ponto de vista continua válido.)

Se @ se comportar da mesma maneira, acho que isso é uma coisa boa. Não se trata de acelerar um certo número de tarefas, em seguida, await ing seu acabamento, é sobre a construção de uma máquina de estado que outra pessoa precisa bombear para a conclusão, seja construindo-a em sua própria máquina de estado ou colocá-lo em um contexto de execução.

TL; DR: se você tirar alguma coisa desta postagem do mini blog, que seja: await!() Rust não é await como em outros idiomas. Os resultados são _semilar_, mas a semântica de como as tarefas são tratadas é muito diferente. Com isso, nos beneficiamos de não usar a sintaxe comum, pois não temos o comportamento comum.

(Foi mencionado em uma discussão anterior uma linguagem que mudou de futuros preguiçosos para ansiosos. Acredito que isso seja porque await foo sugere que foo já está acontecendo, e estamos apenas aguardando que produza um resultado.)

EDITAR: se você quiser discutir isso de uma forma mais sincronizada, execute ping me @ CAD no Discord de desenvolvimento de ferrugem (link é 24h) ou internos ou usuários (@ CAD97). Eu adoraria colocar minha posição contra algum escrutínio direto.

EDIT 2: Desculpe GitHub @ cad pelo ping acidental; Tentei escapar do @ e não tive a intenção de puxar você para isso.

@ I60R

Os trechos como ?!@# me lembram Perl

Na realidade, raramente veríamos qualquer outra combinação além de @? que IMO parece bastante simples e

Você não pode saber até que apareça. Se tivermos outro transformador de código - yield sigil, por exemplo - seria um script Perl real.

Postfix @ é tão difícil de notar!

E isso é realmente uma grande vantagem no contexto de async-await .

A dificuldade em perceber await pontos não pode ser uma vantagem.

Esperar o futuro é uma operação cara e devemos dizer explicitamente que

Realmente faz sentido, porém não acho que devamos dizer isso com tanta frequência e tão alto.

Releia meus links sobre a experiência com C #, por favor. É realmente esse importante dizer que este alto e isso muitas vezes.


Outros pontos são bastante válidos, mas aqueles que respondi são lacunas que não podem ser corrigidas.

@ CAD97 Como um desenvolvedor C # que pode dizer como async é diferente no Rust .. Bem, não é tão diferente como você descreve. IMHO suas implicações são baseadas em uma premissa falsa

Nosso await!(foo) portanto, não é await foo no modo "tradicional" C # ou JS.

É exatamente a mesma coisa na mente de dev C #. Claro, você deve se lembrar que os futuros não serão executados até que sejam pesquisados, mas na maioria dos casos isso não importa. Na maioria dos casos await significa apenas "Eu quero obter o valor não empacotado do F futuro na variável x ". Ter o futuro não rodando até a votação é na verdade um alívio depois do mundo C #, então as pessoas ficarão felizes. Além disso, C # tem conceitos semelhantes com Iterators / generatos, que também são preguiçosos, então o pessoal do C # está bastante familiarizado com a coisa toda.

Resumindo, pegar await trabalhando um pouco diferente é mais fácil do que ter um sigilo funcionando exatamente como await em seu idioma% younameit%, mas não exatamente. As pessoas não ficarão confusas com a diferença, uma vez que não é tanto o que você pensa que elas podem pensar. Situações em que a execução ansiosa / preguiçosa importa são realmente raras, portanto, não deve ser a preocupação principal do design.

@Pzixel É exatamente a mesma coisa na mente de dev C #. Claro, você deve se lembrar que os futuros não serão executados até que sejam pesquisados, mas na maioria dos casos isso não importa.

Situações em que a execução ansiosa / preguiçosa importa são _realmente_ raras, portanto, não devem ser a principal preocupação de design.

Infelizmente isso não é verdade: o padrão de "gerar algumas promessas e depois await elas" é muito comum e desejável , mas não funciona com o Rust .

Essa é uma diferença muito grande, que até pegou alguns membros do Rust de surpresa!

async / await no Rust realmente está mais perto de um sistema monádico, basicamente não tem nada em comum com JavaScript / C #:

  • A implementação é completamente diferente (construir uma máquina de estado e depois executá-la em uma Tarefa, o que é consistente com mônadas).

  • A API é completamente diferente (pull vs push).

  • O comportamento padrão de ser preguiçoso é completamente diferente (o que é consistente com mônadas).

  • Só pode haver um consumidor, em vez de muitos (o que é consistente com as mônadas).

  • Separar erros e usar ? para tratá-los é completamente diferente (o que é consistente com os transformadores monad).

  • O modelo de memória é completamente diferente, o que tem um grande impacto na implementação do Rust Futures e também em como os usuários realmente usam o Futures.

A única coisa em comum é que todos os sistemas são projetados para tornar o código assíncrono mais fácil. É isso aí. Isso realmente não tem muito em comum.

Muitas muitas pessoas mencionaram C #. Nós sabemos.

Sabemos o que C # fez, sabemos o que JavaScript fez. Sabemos por que essas línguas tomaram essas decisões.

Membros oficiais da equipe C # falaram conosco e explicaram em detalhes por que tomaram essas decisões com async / await.

Ninguém está ignorando essas linguagens, ou pontos de dados, ou casos de uso. Eles estão sendo levados em consideração.

Mas Rust realmente é diferente de C # ou JavaScript (ou qualquer outra linguagem). Portanto, não podemos simplesmente copiar cegamente o que outras línguas fazem.

Mas em qualquer uma dessas linguagens, esperar foo significa semanticamente "estacionar esta tarefa e aguardar a conclusão da tarefa foo

É exatamente o mesmo em Rust. A semântica das funções assíncronas é a mesma. Se você chamar um fn assíncrono (que cria um Future), ele ainda não iniciará a execução. Isso é realmente diferente do mundo JS e C #.

Esperar por isso conduzirá o futuro à conclusão, que é igual a qualquer outro lugar.

Infelizmente isso não é verdade: o padrão de "gerar algumas promessas e depois await elas" é muito comum e desejável , mas não funciona com o Rust .

Você poderia dar mais detalhes? Eu li a postagem, mas não encontrei o que há de errado com

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

Claro, eles não dispararão até a linha 2, mas na maioria dos casos é um comportamento desejável e ainda estou convencido de que essa é uma grande diferença que não permite mais usar a palavra-chave await . O próprio nome do tópico sugere que a palavra await faz sentido aqui.

Ninguém está ignorando essas linguagens, ou pontos de dados, ou casos de uso. Eles estão sendo levados em consideração.

Não vou ser chato de repetir os mesmos argumentos indefinidamente. Acho que entreguei o que deveria, então não vou mais fazer isso.


PS

  • A API é completamente diferente (pull vs push).

Faz diferença ao implementar apenas seu próprio futuro. Mas em 99 (100?)% Dos casos, você apenas usa combinadores de futuros que escondem essa diferença.

  • O modelo de memória é completamente diferente, o que tem um grande impacto na implementação do Rust Futures e também em como os usuários realmente usam o Futures.

Você poderia ser mais detalhadamente? Na verdade, eu já brinquei com o código assíncrono ao escrever um servidor web simples no Hyper e não notei nenhuma diferença. Coloque async aqui, await ali, pronto .

O argumento de que rust's async / await tem uma implementação diferente de outras linguagens e então devemos fazer algo diferente não é muito convincente. C's

for (int i = 0; i < n; i++)

e Python's

for i in range(n)

definitivamente não compartilham o mesmo mecanismo subjacente, mas por que o Python escolhe usar for ? Deve usar i in range(n) @ ou i in range(n) --->> ou qualquer outro para mostrar esta diferença importante !

A questão aqui é se a diferença é importante para o trabalho diário do usuário!

A vida diária de um usuário de ferrugem típico é:

  1. Interagindo com algumas APIs HTTP
  2. Escreva algum código de rede

e ele realmente se preocupa com

  1. Ele pode terminar o trabalho de forma rápida e ergonômica.
  2. Rust é excelente para seu trabalho.
  3. Ele tem controle de baixo nível, se quiser

NOT rust tem um milhão de detalhes de implementação que são diferentes de C #, JS . É trabalho de um designer de linguagem esconder diferenças irrelevantes, apenas expondo as úteis para os usuários .

Além disso, ninguém está reclamando de await!() por razões como "Não sei que é uma máquina de estado", "Não sei que é baseado em pull".

Os usuários não se importam com essas diferenças.

Esperar por isso conduzirá o futuro à conclusão, que é igual a qualquer outro lugar.

Bem, não é bem assim? Se eu apenas escrever let result = await!(future) em um async fn e chamar essa função, nada acontecerá até que ela seja colocada em um executor e pesquisada por ele.

@ ben0x539 sim, eu reli ... Mas atualmente não tenho nada a acrescentar.

@ CAD97 fico feliz em ver que meu post foi inspirador. Posso responder sobre o Kotlin: por padrão, ele funciona da mesma maneira que outras linguagens, embora você possa obter um comportamento preguiçoso semelhante ao do Rust se quiser (por exemplo, val lazy = async(start=LAZY) { deferred_operation() }; ). E sobre semânticas semelhantes: IMO, a semântica mais próxima é fornecida na programação reativa, uma vez que os observáveis reativos são frios (preguiçosos) por padrão (por exemplo, val lazy = Observable.fromCallable { deferred_operation() }; ). Assiná-los agenda o trabalho real da mesma forma que await!() no Rust, mas também há uma grande diferença de que a assinatura por padrão é uma operação sem bloqueio e os resultados calculados de forma assíncrona quase sempre são tratados em encerramentos separadamente do fluxo de controle atual. E há uma grande diferença em como o cancelamento funciona. Então, eu acho que o comportamento de Rust async é único, e eu apoio totalmente seu argumento de que await é confuso e sintaxe diferente é apenas uma vantagem aqui!

@Pzixel Tenho certeza de que não apareceria nenhum novo sigilo. Implementar um sigilo yield como sigilo não faz sentido porque é uma construção de fluxo de controle como if / match / loop / return . Obviamente, todas as construções de fluxo de controle devem usar palavras-chave porque elas descrevem a lógica de negócios e a lógica de negócios está sempre em prioridade. Ao contrário, @ e também ? são construções de tratamento de exceções e a lógica de tratamento de exceções é menos importante, portanto, deve ser sutil. Eu não encontrei nenhum argumento forte de que ter uma grande palavra-chave await seja uma coisa boa (considerando a quantidade de texto, é possível que eu não os veja) porque todos eles apelam à autoridade ou experiência (isso, entretanto, não significa que eu rejeite a autoridade ou experiência de alguém - isso simplesmente não pode ser aplicado aqui de forma plena).

@tajimaha o exemplo do Python que você forneceu, na verdade, contém mais "detalhes de implementação". Podemos considerar for como async , (int i = 0; i < n; i++) como await e i in range(n) como @ e aqui é óbvio que Python introduziu uma nova palavra-chave - in (em Java é realmente sigilo - : ) e adicionalmente introduziu uma nova sintaxe de intervalo. Ele poderia reutilizar algo mais familiar como (i=0, n=0; i<n; i++) vez de introduzir muitos detalhes de implementação. Mas, da maneira atual, o impacto na experiência do usuário é apenas positivo, a sintaxe é mais simples e os usuários realmente se preocupam com ela.

@Pzixel Tenho certeza de que não apareceria nenhum novo sigilo. Implementar um yield como sigilo não faz nenhum sentido porque é uma construção de fluxo de controle como if / match / loop / return . Obviamente, todas as construções de fluxo de controle devem usar palavras-chave porque elas descrevem a lógica de negócios e a lógica de negócios está sempre em prioridade. Ao contrário, @ e também ? são construções de tratamento de exceções e a lógica de tratamento de exceções é menos importante, portanto, deve ser sutil.

await não é uma "construção de tratamento de exceções".
Com base em premissas inválidas, suas implicações também são falsas.

o tratamento de exceções também é um fluxo de controle, mas ninguém acha que ? é uma coisa ruim.

Eu não encontrei nenhum argumento forte de que ter uma grande palavra-chave await seja uma coisa boa (considerando a quantidade de texto, é possível que eu não os encontre) porque todos eles apelam à autoridade ou experiência (isso, entretanto, não significa que eu rejeite a autoridade ou experiência de alguém - ela simplesmente não pode ser aplicada _aqui_ de maneira plena).

Porque o Rust não tem experiência própria, então ele só pode ver o que outras linguagens pensam sobre ele, outras equipes experimentam e assim por diante. Você está tão confiante no sigilo, mas não tenho certeza se você tentou realmente usá-lo.

Eu não quero fazer um argumento sobre a sintaxe no Rust, mas eu vi muitos argumentos pós-fixados vs prefixos e talvez possamos ter o melhor dos dois mundos. E alguém já tentou propor isso em C ++. A proposta de C ++ e Coroutine TS foi mencionada algumas vezes aqui, mas na minha opinião uma proposta alternativa chamada Core Coroutines merece mais atenção.

Autores de Core Coroutines propõem substituir co_await por um novo token semelhante ao do operador.
Com o que eles chamam de sintaxe de operador de desdobramento , é possível usar notações de prefixo e pós-fixadas (sem ambigüidade):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

Achei que poderia ser interessante, ou pelo menos tornará a discussão mais completa.

Mas Rust realmente é diferente de C # ou JavaScript (ou qualquer outra linguagem). Portanto, não podemos simplesmente copiar cegamente o que outras línguas fazem.

Por favor, não use a "diferença" como desculpa para sua inclinação pessoal para uma sintaxe não ortodoxa.

Quantas diferenças existem? Pegue qualquer linguagem popular, Java, C, Python, JavaScript, C #, PHP, Ruby, Go, Swift, etc., estática, dinâmica, compilada, interpretada. Eles são muito diferentes no conjunto de recursos, mas ainda têm muito em comum em sua sintaxe. Houve um momento em que você sentiu que alguma dessas linguagens era como 'brainf * ck'?

Acho que devemos nos concentrar em fornecer recursos diferentes, mas úteis, não uma sintaxe estranha.

Depois de ler o tópico, também acho que o debate está praticamente encerrado quando alguém invalida a necessidade urgente de encadeamento, alguém invalida a alegação de que as pessoas estão incomodadas com muitas variáveis ​​temporárias.

IMO, você perdeu o debate sobre as necessidades da sintaxe do postfix. Você só pode recorrer à "diferença". É outra tentativa desesperada.

@tensorduruk Tenho a impressão de que suas palavras estão sendo hostis demais para os outros usuários. Por favor, tente colocá-los em cheque.


E honestamente, se há muitas pessoas que são contra a sintaxe pós-fixada, então devemos decidir sobre uma sintaxe de prefixo por enquanto, esperar para ver como o código com a sintaxe de prefixo é escrito e, em seguida, fazer uma análise para ver quanto do código foi escrito poderia se beneficiar de ter o postfix aguardando. Dessa forma, nós agradamos a todos que não se sentem confortáveis ​​com uma mudança inovadora como o postfix await, ao mesmo tempo que obtemos um bom argumento pragmático de se o postfix espera ou não.

O pior cenário com isso é se fizermos tudo isso e chegarmos a uma sintaxe de espera de postfix que é de alguma forma completamente melhor do que espera de prefixo. Isso levaria a muita rotatividade de código se as pessoas se importassem em mudar para a melhor forma de esperar.

E eu acho que toda essa discussão de sintaxe realmente se resume ao encadeamento. Se o encadeamento não fosse uma coisa, então postfix await estaria completamente fora da janela e seria muito mais fácil decidir apenas por prefix await. No entanto, o encadeamento é muito importante no Rust e, portanto, abre a discussão para os seguintes tópicos:

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?

Ou algo assim. Se alguém puder propor um padrão de decisão melhor com pontos melhores do que eu, faça-o! XD

Portanto, em primeiro lugar, em vez de até mesmo discutir a sintaxe, devemos decidir se queremos pós-fixos, prefixos ou ambos, pós-fixos e prefixos, e por que queremos a escolha que fazemos. Assim que tivermos isso resolvido, podemos passar a problemas menores relacionados à escolha da sintaxe.

@tensorduruk

IMO, você perdeu o debate sobre as necessidades da sintaxe do postfix. Você só pode recorrer à "diferença". É outra tentativa desesperada.

Sério, por que não usar apenas suas línguas faladas em vez da hostilidade desnecessária aqui?
Usando sua lógica, a ferrugem deve ter classes porque outras linguagens convencionais as têm. Rust não deveria ter emprestado porque outra linguagem não tem.

Não lidar com o encadeamento seria uma oportunidade perdida. i, e Eu não gostaria de criar ligações para uma variável temporária, se o encadeamento puder torná-la anônima.

No entanto, acho que devemos apenas nos ater à palavra-chave macro atual para await como nightly. Tenha macros postfix como um recurso noturno e deixe as pessoas brincar com isso. Sim, haverá churns assim que tivermos resolvido, mas isso pode ser tratado pelo rustfix.

@tensorduruk

IMO, você perdeu o debate sobre as necessidades da sintaxe do postfix. Você só pode recorrer à "diferença". É outra tentativa desesperada.

Sério, por que não usar apenas suas línguas faladas em vez da hostilidade desnecessária aqui?
Usando sua lógica, a ferrugem deve ter classes porque outras linguagens convencionais as têm. Rust não deveria ter emprestado porque outra linguagem não tem.

Não lidar com o encadeamento seria uma oportunidade perdida. i, e Eu não gostaria de criar ligações para uma variável temporária, se o encadeamento puder torná-la anônima.

No entanto, acho que devemos apenas nos ater à palavra-chave macro atual para await como nightly. Tenha macros postfix como um recurso noturno e deixe as pessoas brincar com isso. Sim, haverá churns assim que tivermos resolvido, mas isso pode ser tratado pelo rustfix.

por favor, leia atentamente. Eu disse que deveríamos fornecer recursos úteis, não uma sintaxe estranha. estrutura? emprestar? características!

por favor, também resolva problemas que realmente existem. as pessoas mostraram, com exemplos ou estatísticas, que ligações temporárias podem não ser um problema. Vocês, que apóiam f await f.await já tentaram convencer o outro lado usando evidências?

também me negar é inútil. para que o postfix seja aceito, você precisa discutir onde o postfix é útil (provavelmente não repita o argumento do encadeamento, isso é um beco sem saída; provavelmente um problema frequente que irrita as pessoas, com alguma evidência, não um problema de brinquedo).

podemos obter a sintaxe como C # ou JS? a maioria dos desenvolvedores usa em todo o mundo, eu não gosto de usar nova sintaxe ou Inconsistent, Rust também é difícil para novas pessoas aprenderem.

O seguinte seria um apêndice da minha postagem sobre o uso do postfix @ de await


Devemos usar experiência de muitas outras línguas em vez

E nós realmente o usamos. Mas isso não significa que devemos repetir completamente a mesma experiência. Além disso, ao argumentar desta forma contra @ você provavelmente não alcançaria nenhum resultado útil porque apelar para a experiência anterior não é convincente :

Os relatos genéticos de um problema podem ser verdadeiros e podem ajudar a iluminar os motivos pelos quais o problema assumiu sua forma atual, mas não são conclusivos na determinação de seus méritos.

Devíamos usar await porque muitas pessoas adoram

Essas pessoas podem amar await por muitos outros motivos, não apenas porque é await . Essas razões podem não existir em Rust também. E argumentar desta forma contra @ provavelmente não traria nenhum ponto novo à discussão porque apelar ao público não é convincente :

O argumentum ad populum pode ser um argumento válido na lógica indutiva; por exemplo, uma pesquisa com uma população considerável pode descobrir que 100% preferem determinada marca de produto a outra. Um argumento convincente (forte) pode ser feito de que a próxima pessoa a ser considerada também muito provavelmente preferirá aquela marca (mas nem sempre 100%, uma vez que pode haver exceções), e a pesquisa é uma evidência válida dessa afirmação. No entanto, não é adequado como argumento para o raciocínio dedutivo como prova, por exemplo, dizer que a pesquisa prova que a marca preferida é superior à concorrência em sua composição ou que todos preferem aquela marca à outra.

@Pzixel

Mas a ferrugem é outra coisa. Temos ? aqui e temos que propagar os erros. Em C # async / await quebra exceções automaticamente, lança-as entre pontos de await e assim por diante. Você não tem ferrugem. Considere que cada vez que você escreve await em C #, você deve escrever

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

É muito tedioso ter todos esses aparelhos.

Em C # você escreve isto:

try
{
  var response = await client.GetAsync("www.google.com");
  var json =  await response.ReadAsStreamAsync();
  var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
  // handle exception
}

Com relação ao argumento do encadeamento de erros de propagação, por exemplo, foo().await? , há alguma razão pela qual o ? não pode ser adicionado ao operador await no prefixo?

let response = await? getProfile();

Outra coisa que acabou de me ocorrer: e se você quiser match em um Future<Result<...>> ? Qual destes é mais fácil de ler?

// Prefix
let userId = match await response {
  Ok(u) => u.id,
  _ => -1
};
// Postfix
let userId = match response {
  Ok(u) => u.id,
  _ => -1
} await;

Além disso, uma expressão async match seria uma coisa? Por exemplo, você gostaria que o corpo de uma expressão de correspondência fosse assíncrono? Nesse caso, haveria uma diferença entre match await response e await match response . Como match e await são ambos operadores efetivamente unários e match já é prefixo, seria mais fácil distinguir se await também fossem prefixo. Com um prefixo e um postfix, torna-se difícil especificar se você está aguardando a correspondência ou a resposta.

let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await; // Are we awaiting match or response here?

Se você tiver que esperar as duas coisas, estará olhando para algo como

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

Embora eu ache que pode ser

// Postfix - ... this is weirder and uglier
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;

(O design da linguagem de programação é difícil.)

Independentemente disso, vou reiterar que Rust tem precedência para operadores unários serem prefixos com match , e await é um operador unário.

C# // Postfix - ... this is weirder and uglier let userId = match response await { ... } await;

A beleza está nos olhos de quem vê.

Independentemente disso, vou reiterar que Rust tem precedência para operadores unários serem prefixos com match , e await é um operador unário.

? por outro lado é unário, mas pós-fixo.

Independentemente disso, sinto que a discussão agora está circulando pelo ralo. Sem trazer novos pontos de discussão, não há como repetir as mesmas posições repetidamente.

FWIW, estou feliz em obter suporte para await seja qual for a sintaxe - embora eu tenha minhas próprias preferências, não acho que nenhuma das sugestões realistas seja terrível demais para usar.

@markrendle, não tenho certeza do que você está respondendo

Em C # você escreve isto:

Eu sei como escrevo em C #. Eu disse "imagine como é que não tínhamos exceções". Porque Rust não.

Com relação ao argumento do encadeamento de erros de propagação, por exemplo, foo().await? , há alguma razão pela qual o ? não pôde ser adicionado ao operador await no prefixo?

Já foi falado duas ou três vezes, por favor, leia o tópico. Resumindo: é uma construção artificial, que não funcionará bem se tivermos algo adicional a ? . await? como sufixo simplesmente funciona, quando await? como prefixo requer suporte adicional no compilador. E ainda requer chaves para o encadeamento (o que eu pessoalmente não gosto aqui, mas as pessoas sempre mencionam isso como algo importante), quando o postfix não espera.

Outra coisa que acabou de me ocorrer: e se você quiser match em um Future<Result<...>> ? Qual destes é mais fácil de ler?

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

Como outro usuário de C #, direi que suas sintaxes de prefixo: new , await e conversões no estilo C foram as que mais atrapalharam minha intuição. Eu apoio fortemente a opção do operador postfix.

No entanto, qualquer sintaxe será melhor do que encadear futuros explicitamente, mesmo uma pseudo-macro. Eu aceitarei qualquer resolução.

@orthoxerox Você levantou um ponto muito bom. Em meu trabalho diário, escrevo principalmente Java e desprezo o novo operador a um nível em que todas as minhas classes que precisam de instanciação explícita (uma ocorrência surpreendentemente rara quando você usa padrões de construtor e injeção de dependência) têm um método de fábrica estático apenas para que eu possa ocultar o operador .

@Pzixel

@markrendle, não tenho certeza do que você está respondendo

Em C # você escreve isto:

Eu sei como escrevo em C #. Eu disse "imagine como é que não tínhamos exceções". Porque Rust não.

Suponho que isso seja provavelmente uma barreira do idioma, porque não foi isso que você disse, mas aceito que pode ter sido o que você quis dizer.

De qualquer forma, como @rolandsteiner disse, o importante é que tenhamos alguma forma de async / await, então estou feliz em aguardar a decisão do time principal, e todos os fãs do postfix podem esperar a decisão do time principal. 😛 ❤️ ☮️

@yasammez Venha para C #. Na v8.0, usamos apenas new () sem o nome do tipo :)

Vou apenas lançar algumas idéias para o operador Postfix.

foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator

Não estou dizendo se o operador postfix é o caminho a seguir ou não, mas pessoalmente acho @ um dos mais barulhentos e estranhos de todas as opções possíveis. ~ do comentário de @phaux parece muito mais elegante e menos "ocupado". Além disso, se não estou perdendo nada, não o usamos para nada no Rust ainda.

Fui proposto ~ antes de @phaux, embora eu não queira reivindicar a patente; P

Eu propus isso porque é como um eco falando:

Hi~~~~~
Where r u~~~~~

Hay~~~~~
I am in another mountain top~~~~~

~ às vezes é usado após uma frase para indicar o fim, o que é adequado para esperar!

Eu não posso dizer se este tópico atingiu o ponto mais ridículo ou se estamos no caminho certo.

Acho que ~ não é fácil de responder em alguns teclados, especialmente em alguns teclados mecânicos pequenos e delicados.

Poderia ser:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};

Poderíamos introduzir um trígrafo como ... para usuários em layouts de teclado onde ~ é inconveniente.

No início, eu estava fortemente inclinado a ter uma sintaxe de prefixo exigida por delimitador como await(future) ou await{future} já que é tão inequívoca e fácil de analisar visualmente. No entanto, eu entendo as propostas de outros de que um Futuro Rust não é como a maioria dos outros Futuros de outras linguagens, uma vez que não coloca uma tarefa em um executor imediatamente, mas em vez disso é mais uma estrutura de fluxo de controle que transforma o contexto no que é homomórfico a uma cadeia de chamadas de mônadas essencialmente.

Isso me faz pensar que é um tanto lamentável que agora haja uma confusão na tentativa de compará-lo com outras línguas a esse respeito. O análogo mais próximo realmente é a notação mônada do em Haskell ou a compreensão for em Scala (que são os únicos com os quais estou familiarizado de início). De repente, agradeço a consideração de propor uma sintaxe única, mas temo que a existência do operador ? tenha encorajado e desencorajado o uso de outros sigilos com ele. Qualquer outro operador baseado em sigilo próximo a ? faz com que pareça barulhento e confuso, como future@? , mas o precedente estabelecido por ter um operador de sigilo pós-fixado significa que outro não é tão ridículo.

Estou, portanto, convencido do mérito do operador de sigilo pós-fixo. A desvantagem disso é que o sigilo que eu preferia é o tipo nunca. Eu teria preferido ! já que acho que future!? me faria rir sempre que eu o escrevesse, e faz mais sentido visual para mim ver. Suponho que $ seria o próximo, pois é visualmente discernível future$? . Ver ~ ainda me lembra dos primeiros dias da Rust, quando ~ era o operador de prefixo para alocação de heap. É tudo muito pessoal, portanto, não invejo os responsáveis ​​pelas decisões finais. Se eu fosse eles, provavelmente optaria pela escolha inofensiva do operador de prefixo com os delimitadores necessários.

No entanto, eu entendo as propostas de outros de que um Futuro Rust não é como a maioria dos outros Futuros de outras linguagens, uma vez que não coloca uma tarefa em um executor imediatamente, mas em vez disso é mais uma estrutura de fluxo de controle que transforma o contexto no que é homomórfico a uma cadeia de chamadas de mônadas essencialmente.

Eu tendo a discordar. O comportamento que você mencionou não é uma propriedade de await , mas da função ou escopo async circundante. Não é await que atrasa a execução do código anterior, mas o escopo que contém o referido código.

Provavelmente, o problema com o símbolo @ aparência estranha é que usá-lo nesse contexto nunca foi esperado antes, portanto, na maioria das fontes ele é fornecido de uma forma desconfortável para nós.

Então, fornecer um glifo melhor e algumas ligaduras para fontes de programação populares (ou pelo menos para Fira Code da Mozilla) pode melhorar um pouco a situação.

Em todos os outros casos, para mim não parece que @ seja tão estranho para causar problemas reais ao escrever ou manter código.


Por exemplo, o código a seguir usa um símbolo diferente de @ - :


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

Expanda para comparação como fica com ANSI @ normal


// 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

No início, eu estava fortemente inclinado a ter uma sintaxe de prefixo exigida por delimitador como await(future) ou await{future} já que é tão inequívoca e fácil de analisar visualmente.

Então você provavelmente quer if { cond } , while { cond } e match { expr } inequívocos ...

No entanto, eu entendo as propostas de outros de que um Futuro Rust não é como a maioria dos outros Futuros de outras línguas

Não é verdade. Na verdade, é como a maioria dos outros futuros de outras línguas. A diferença entre "executar até a primeira espera quando gerado" e "executar até a primeira espera quando pesquisada" não é tão grande. Eu sei porque trabalhei com os dois. Se você realmente pensa "quando essa diferença entra em jogo", encontrará apenas um caso de canto. por exemplo, você obterá um erro ao criar o futuro na primeira enquete em vez de quando for criado.

Eles podem ser diferentes internamente, mas são exatamente os mesmos da perspectiva do usuário, então não faz sentido para mim fazer uma distinção aqui.

@Pzixel

A diferença entre "executar até a primeira espera quando gerado" e "executar até a primeira espera quando pesquisada" não é tão grande.

Essa não é a diferença da qual estamos falando, no entanto. Não estamos falando de preguiçoso vs ansioso para esperar primeiro.

O que estamos falando é que await junta-se ao esperado Promise (JS) / Task (C #) no executor em outras linguagens, que já foi colocado no executor na construção (então já estava rodando em "background"), mas no Rust, Future s são máquinas em estado inerte até await! -driven.

Promise / Task é um identificador para uma operação assíncrona em execução. Future é um cálculo assíncrono diferido. Pessoas, incluindo nomes notáveis ​​em Rust, cometeram esse erro antes, e exemplos foram vinculados antes no meio desses mais de 500 comentários.

Pessoalmente, acho que essa incompatibilidade de semântica é grande o suficiente para neutralizar a familiaridade de await . Nossos Future s cumprem a mesma meta de Promise / Task , mas por meio de um mecanismo diferente.


Curiosamente, para mim, quando aprendi async / await em JavaScript pela primeira vez, async era "apenas" algo que escrevi para obter o superpoder await . E a maneira como fui ensinado a obter paralelismo foi a = fa(); b = fb(); /* later */ await [a, b]; (ou seja lá o que for, já faz muito tempo que não preciso escrever JS). Minha posição é que a visão de async outras pessoas se alinha comigo, em que a semântica de Rust não diverge em async (dá a você await superpotência), mas em Future construção e await! .


Neste ponto, acredito que a discussão sobre as diferenças na semântica de async / Future / await Rust já terminou e nenhuma informação nova está sendo apresentada. A menos que você tenha uma nova posição e / ou visão para trazer, provavelmente seria melhor para o tópico se deixarmos essa discussão aqui. (Eu ficaria feliz em levá-lo para Internals e / ou Discord.)

@ CAD97 sim, vejo sua posição, mas acho que a discórdia não é tão grande.

Você me pegou, eu te peguei. Portanto, deixe a discussão fluir.

@ CAD97

Pessoas, incluindo nomes notáveis ​​em Rust, cometeram esse erro antes, e exemplos foram vinculados antes no meio desses mais de 500 comentários.

Se mesmo pessoas intimamente familiarizadas com Rust cometem esse erro, é realmente um erro?

Então, tivemos várias discussões sobre async-await no Rust All Hands. No decorrer dessas discussões, algumas coisas ficaram claras:

Primeiro, não há consenso (ainda) na equipe lang sobre a sintaxe de await . Existem claramente muitas possibilidades e argumentos fortes a favor de todos eles. Passamos muito tempo explorando alternativas e produzimos muitas idas e vindas interessantes. Uma próxima etapa imediata para essa discussão, eu acho, é converter essas notas (junto com outros comentários deste tópico) em uma espécie de comentário resumido que expõe o caso para cada variante, e então continuar a partir daí. Estou trabalhando com @withoutboats e @cramertj nisso.

Afastando-se da questão de sintaxe, outra coisa que planejamos fazer é uma triagem geral do status da implementação. Existem várias limitações atuais (por exemplo, a implementação requer TLS sob o capô atualmente para encadear informações sobre o waker). Eles podem ou não ser bloqueadores da estabilização , mas, independentemente, são problemas que precisam ser resolvidos em última instância, e isso vai exigir algum esforço concentrado (em parte da equipe do compilador). Outra próxima etapa é conduzir essa triagem e gerar um relatório. Espero que façamos essa triagem na próxima semana e teremos uma atualização em seguida.

Nesse ínterim, irei prosseguir e bloquear este tópico até termos a oportunidade de produzir os relatórios acima . Eu sinto que o tópico já cumpriu seu propósito de explorar o possível espaço de design com alguma profundidade e comentários adicionais não serão particularmente úteis neste estágio. Assim que tivermos os relatórios mencionados em mãos, também apresentaremos os próximos passos para chegar a uma decisão final.

(Para expandir um pouco o parágrafo final, estou bastante interessado em explorar maneiras alternativas de explorar o espaço do design além de longos tópicos de discussão. Este é um tópico muito maior do que posso abordar neste comentário, então não entrarei em detalhes , mas basta dizer por agora que estou bastante interessado em tentar encontrar maneiras melhores de resolver este - e no futuro! - debates de sintaxe.)

Relatório de status de espera assíncrona:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

Em relação ao meu comentário anterior, ele contém os resultados da triagem e algumas reflexões sobre a sintaxe (mas ainda não uma redação completa da sintaxe).

Marcando este problema como bloqueio para estabilização de espera assíncrona, pelo menos por enquanto.

Há muito tempo, Niko prometeu que escreveríamos um resumo da discussão na equipe de linguagem e na comunidade sobre a sintaxe final para o operador await. Nossas desculpas pela longa espera. Uma descrição do status da discussão está no link abaixo. Antes disso, porém, deixe-me também atualizar sobre onde a discussão está agora e para onde iremos a partir daqui.

Breve resumo de onde async-await está agora

Primeiro, esperamos estabilizar async-await na versão 1.37 , que se ramifica em 4 de julho de 2019. Já que não queremos estabilizar a macro await! , temos que resolver a questão de sintaxe antes disso. Observe que esta estabilização não representa o fim da estrada - mas o começo. Resta trabalho de recurso a ser feito (por exemplo, fn assíncrono em traits) e também trabalho de implementação (otimização contínua, correção de bugs e similares). Ainda assim, estabilizar async / await será um marco importante!

No que diz respeito à sintaxe, o plano de resolução é o seguinte:

  • Para começar, estamos publicando um artigo sobre o debate sobre a sintaxe até agora - por favor, dê uma olhada.
  • Queremos ser compatíveis com futuras extensões óbvias para a sintaxe: streams de processamento com for loops em particular (como o loop for await do JavaScript). É por isso que venho trabalhando em uma série de posts sobre esse assunto ( primeiro post aqui e mais no futuro).
  • Na próxima reunião da equipe lang em 2 de maio, planejamos discutir a interação com os loops for e também estabelecer um plano para chegar a uma decisão final sobre a sintaxe a tempo de estabilizar async / await em 1.37. Publicaremos uma atualização após a reunião neste tópico interno .

A redação

O writeup é um documento em papel da caixa de depósito, disponível aqui . Como você verá, é bastante longo e expõe muitos dos argumentos para frente e para trás. Agradeceríamos comentários sobre isso; em vez de reabrir este problema (que já tem mais de 500 comentários), criei um tópico interno para esse propósito .

Como eu disse antes, planejamos chegar a uma decisão final em um futuro próximo. Também sentimos que a discussão atingiu em grande parte um estado estável: esperamos que as próximas semanas sejam o "período de comentário final" para essa discussão de sintaxe. Após a reunião, esperamos ter um cronograma mais detalhado para compartilhar sobre como essa decisão será tomada.

A sintaxe Async / await é provavelmente o recurso mais esperado que o Rust ganhou desde 1.0, e a sintaxe para await em particular foi uma das decisões sobre a qual recebemos mais feedback. Obrigado a todos que participaram dessas discussões nos últimos meses! Esta é uma escolha sobre a qual muitas pessoas têm sentimentos fortemente divergentes; Queremos assegurar a todos que seu feedback está sendo ouvido e que a decisão final será alcançada após muita reflexão e cuidadosa deliberação.

Esta página foi útil?
0 / 5 - 0 avaliações