Rust: Résoudre la syntaxe `wait`

Créé le 15 janv. 2019  ·  512Commentaires  ·  Source: rust-lang/rust

Avant de commenter dans ce fil, veuillez vérifier https://github.com/rust-lang/rust/issues/50547 et essayez de vérifier que vous ne dupliquez pas les arguments qui y ont déjà été présentés.


Notes des bergers:

Si vous êtes nouveau dans ce fil, pensez à partir de https://github.com/rust-lang/rust/issues/57640#issuecomment -456617889, qui a été suivi de trois excellents commentaires de synthèse, dont le dernier était https: //github.com/rust-lang/rust/issues/57640#issuecomment -457101180. (Merci, @traviscross!)

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

Commentaire le plus utile

J'ai pensé qu'il pourrait être utile d'écrire comment les autres langages gèrent une construction d'attente.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Pirater

$result = await task;

Dard

var result = await task;

Avec tout cela, rappelons-nous que les expressions Rust peuvent résulter en plusieurs méthodes chaînées. La plupart des langues ont tendance à ne pas faire cela.

Tous les 512 commentaires

J'ai pensé qu'il pourrait être utile d'écrire comment les autres langages gèrent une construction d'attente.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Pirater

$result = await task;

Dard

var result = await task;

Avec tout cela, rappelons-nous que les expressions Rust peuvent résulter en plusieurs méthodes chaînées. La plupart des langues ont tendance à ne pas faire cela.

Avec tout cela, rappelons-nous que les expressions Rust peuvent résulter en plusieurs méthodes chaînées. La plupart des langues ont tendance à ne pas faire cela.

Je dirais que les langages qui prennent en charge les méthodes d'extension ont tendance à en avoir. Ceux-ci incluraient Rust, Kotlin, C # (par exemple, méthode-syntaxe LINQ et divers constructeurs) et F #, bien que ce dernier utilise fortement l'opérateur de tube pour le même effet.

Purement anecdotique de ma part, mais je lance régulièrement une douzaine d'expressions enchaînées de méthodes dans le code Rust dans la nature et cela se lit et fonctionne bien. Je n'ai pas vécu cela ailleurs.

Je voudrais voir que ce problème a été référencé dans le premier post du # 50547 (à côté de la case à cocher "Syntaxe finale pour await").

Kotlin

val result = task.await()

La syntaxe de Kotlin est:

val result = doTask()

Le await est juste un suspendable function , pas une chose de première classe.

Merci de l'avoir mentionné. Kotlin se sent plus implicite parce que les futurs sont impatients par défaut. Cependant, il est toujours courant dans un bloc différé d'utiliser cette méthode pour attendre d'autres blocs différés. Je l'ai certainement fait plusieurs fois.

@cramertj Puisqu'il y a 276 commentaires dans https://github.com/rust-lang/rust/issues/50547 , pourriez-vous résumer les arguments qui y sont présentés pour qu'il soit plus facile de ne pas les répéter ici? (Peut-être les ajouter à l'OP ici?)

Kotlin se sent plus implicite parce que les futurs sont impatients par défaut. Cependant, il est toujours courant dans un bloc différé d'utiliser cette méthode pour attendre d'autres blocs différés. Je l'ai certainement fait plusieurs fois.

peut-être devriez-vous ajouter les deux cas d'utilisation avec un peu de contexte / description.

Qu'en est-il également des autres langages utilisant des attentes implicites, comme go-lang?

Une des raisons d'être en faveur d'une syntaxe post-correction est que du point de vue des appelants, une attente se comporte un peu comme un appel de fonction: vous abandonnez le contrôle de flux et lorsque vous le récupérez, un résultat attend sur votre pile. Dans tous les cas, je préférerais une syntaxe qui embrasse le comportement de type fonction en contenant une paranthèse de fonction. Et il y a de bonnes raisons de vouloir séparer la construction des coroutines à partir de leur première exécution afin que ce comportement soit cohérent entre les blocs de synchronisation et asynchrones.

Mais alors que le style de coroutine implicite a été débattu, et que je suis du côté de l'explicitation, l' appel d'une coroutine ne pourrait-il pas être assez explicite? Cela fonctionne probablement mieux lorsque la coroutine n'est pas directement utilisée lors de sa construction (ou pourrait fonctionner avec des flux). En substance, contrairement à un appel normal, nous nous attendons à ce qu'une coroutine prenne plus de temps que nécessaire dans un ordre d'évaluation plus détendu. Et .await!() est plus ou moins une tentative de différenciation entre les appels normaux et les appels coroutine.

Donc, après avoir, espérons-le, fourni une vision quelque peu nouvelle des raisons pour lesquelles la post-correction pourrait être préférée, une modeste proposition de syntaxe:

  • future(?)
  • ou future(await) qui vient avec ses propres compromis bien sûr mais semble être accepté comme moins déroutant, voir en bas de l'article.

Adaptation d'un exemple assez populaire d'un autre thread (en supposant que logger.log également une coroutine, pour montrer à quoi ressemble un appel immédiat):

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
}

Et avec l'alternative:

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
}

Pour éviter le code illisible et pour faciliter l'analyse, n'autorisez que des espaces après le point d'interrogation, pas entre celui-ci et le paran ouvert. Donc future(? ) est bon alors que future( ?) ne le serait pas. Ce problème ne se pose pas dans le cas de future(await) où tous les jetons actuels peuvent être utilisés comme précédemment.

L'interaction avec d'autres opérateurs de post-correction (tels que l'actuel ? -try) est également similaire aux appels de fonction:

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

Quelques raisons d'aimer ça:

  • Comparé à .await!() il ne fait pas allusion à un membre qui pourrait avoir d'autres utilisations.
  • Il suit la priorité naturelle des appels, comme le chaînage et l'utilisation de ? . Cela réduit le nombre de classes de priorité et facilite l'apprentissage. Et les appels de fonction ont toujours été quelque peu spéciaux dans le langage (même s'ils ont un trait), de sorte qu'il n'y a aucune attente d'un code utilisateur capable de définir son propre my_await!() qui a une syntaxe et un effet très similaires.
  • Cela pourrait se généraliser aux générateurs et aux flux, ainsi qu'aux générateurs qui s'attendent à ce que plus d'arguments soient fournis lors de la reprise. En substance, cela se comporte comme un FnOnce tandis que Streams se comporterait comme un FnMut . Des arguments supplémentaires peuvent également être acceptés facilement.
  • Pour ceux qui ont déjà utilisé le Futures actuel, cela montre comment un ? avec Poll aurait dû fonctionner tout au long (séquence de stabilisation malheureuse ici). En tant qu'étape d'apprentissage, cela est également cohérent avec l'attente d'un opérateur basé sur ? pour détourner le flux de contrôle. (await) d'un autre côté ne satisferait pas cela mais après tout la fonction s'attendra toujours à reprendre au point de divergence.
  • Il utilise une syntaxe de type fonction, bien que cet argument ne soit valable que si vous êtes d'accord avec moi: smile:

Et des raisons de ne pas aimer ça:

  • ? semble être un argument mais il n'est même pas appliqué à une expression. Je crois que cela pourrait être résolu par l'enseignement, car le gage auquel il semble s'appliquer est l'appel de fonction lui-même, qui est la notion quelque peu correcte. Cela signifie également positivement que la syntaxe est sans ambiguïté, j'espère.
  • Un mélange plus (et différent) de paranthesis et de ? peut être difficile à analyser. Surtout quand vous avez un futur retournant le résultat d'un autre futur: construct_future()(?)?(?)? . Mais vous pourriez faire le même argument pour pouvoir obtenir un résultat d'un objet fn , conduisant à une expression telle que celle-ci étant autorisée: foobar()?()?()? . Puisque néanmoins je n'ai jamais vu cela utilisé ni plainte, la division en déclarations séparées dans de tels cas semble être assez rarement requise. Ce problème n'existe pas non plus pour construct_future()(await)?(await)? -
  • future(?) est mon meilleur coup à une syntaxe laconique et encore un peu concise. Pourtant, son raisonnement est fondé sur les détails d'implémentation dans les coroutines (retour temporaire et envoi à la reprise), ce qui pourrait le rendre inadapté à une abstraction. future(await) serait une alternative qui pourrait encore être expliquée après que await ait été internalisé en tant que mot-clé, mais la position de l'argument est un peu difficile à avaler pour moi. Cela pourrait être bien, et il est certainement plus lisible lorsque la coroutine renvoie un résultat.
  • Interférence avec d'autres propositions d'appel de fonction?
  • Le tien? Vous n'avez pas besoin de l'aimer, c'était juste un gaspillage de ne pas au moins proposer cette syntaxe de post-correction laconique.

future(?)

Il n'y a rien de spécial à propos de Result : les contrats à terme peuvent renvoyer n'importe quel type de Rust. Il se trouve que certains Futures rapportent Result

Alors, comment cela fonctionnerait-il pour les contrats à terme qui ne rapportent pas Result ?

Il semble que ce que je voulais dire n'était pas clair. future(?) est ce qui a été précédemment décrit comme future.await!() ou similaire. Le branchement sur un futur qui renvoie également un résultat serait future(?)? (deux façons différentes d'abandonner le flux de contrôle tôt). Cela rend le sondage futur (?) et le test des résultats ? orthogonaux. Edit: a ajouté un exemple supplémentaire pour cela.

Le branchement sur un futur qui renvoie également un résultat serait future(?)?

Merci de clarifier. Dans ce cas, je n'en suis certainement pas fan.

Cela signifie que l'appel d'une fonction qui renvoie un Future<Output = Result<_, _>> serait écrit comme foo()(?)?

Il est très syntaxique et utilise ? à deux fins complètement différentes.

Si c'est précisément le conseil à l'opérateur ? qui est lourd, on pourrait bien sûr le remplacer par le mot clé nouvellement réservé. J'avais seulement initialement considéré que cela ressemblait trop à un argument réel de type déroutant, mais le compromis pourrait fonctionner en termes d'aider à analyser mentalement l'énoncé. Ainsi, la même instruction pour impl Future<Output = Result<_,_>> deviendrait:

  • foo()(await)?

Le meilleur argument pour lequel ? est approprié est que le mécanisme interne utilisé est quelque peu similaire (sinon nous ne pourrions pas utiliser Poll dans les bibliothèques actuelles) mais cela peut manquer le point d'être une bonne abstraction.

C'est très syntaxique

je pensais que c'était tout l'intérêt des attentes explicites?

il utilise ? à deux fins complètement différentes.

oui, donc la syntaxe foo()(await) serait beaucoup plus agréable.

cette syntaxe est comme appeler une fonction qui renvoie une fermeture puis appeler cette fermeture dans JS.

Ma lecture de "syntax-heavy" était plus proche de "sigil-heavy", voir une séquence de ()(?)? est assez choquant. Cela a été soulevé dans le message d'origine:

Un mélange plus (et différent) de paranthesis et de ? peut être difficile à analyser. Surtout quand vous avez un futur retournant le résultat d'un autre futur: construct_future()(?)?(?)?

Mais vous pourriez faire le même argument pour pouvoir obtenir un résultat d'un objet fn , conduisant à une expression telle que celle-ci étant autorisée: foobar()?()?()? . Puisque néanmoins je n'ai jamais vu cela utilisé ni plainte, la division en déclarations séparées dans de tels cas semble être assez rarement requise.

Je pense que la réfutation ici est: combien de fois avez-vous vu -> impl Fn dans la nature (sans parler de -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _> )? Combien de fois pensez-vous voir -> impl Future<Output = Result<_, _>> dans une base de code asynchrone? Le fait de devoir nommer une valeur de retour rare impl Fn pour rendre le code plus facile à lire est très différent de devoir nommer une fraction significative des valeurs de retour temporaires impl Future .

Le fait de devoir nommer une valeur de retour impl Fn rare pour rendre le code plus facile à lire est très différent de devoir nommer une fraction significative des valeurs de retour impl futures temporaires.

Je ne vois pas comment ce choix de syntaxe a une influence sur le nombre de fois où vous devez nommer explicitement votre type de résultat. Je ne pense pas que cela n'influence pas l'inférence de type différemment de await? future .

Cependant, vous avez tous fait de très bons points ici et plus je crée d'exemples (j'ai édité le message original pour toujours contenir les deux versions de syntaxe), plus je penche vers future(await) moi-même. Il n'est pas déraisonnable de taper et conserve toute la clarté de la syntaxe des appels de fonction que cela était censé évoquer.

Combien de fois vous attendez-vous à voir -> impl Future> dans une base de code asynchrone?

Je m'attends à voir le type équivalent de celui-ci (un fn asynchrone qui renvoie un résultat) tout le temps , probablement même la majorité de tous les fns asynchrones, car si ce que vous attendez est même un IO, vous lancerez presque certainement Erreurs d'E / S vers le haut.


Lien vers mon article précédent sur le problème du suivi et ajout de quelques réflexions supplémentaires.

Je pense qu'il y a très peu de chances qu'une syntaxe n'incluant pas la chaîne await caractères

  • Le préfixe attend avec des délimiteurs obligatoires. Ici, c'est aussi une décision sur les délimiteurs (accolades ou parenthèses ou accepter les deux; tous ont leurs propres avantages et inconvénients). Autrement dit, await(future) ou await { future } . Cela résout complètement les problèmes de précédence, mais est syntaxiquement bruyant et les deux options de délimiteur présentent des sources possibles de confusion.
  • Le préfixe attend avec la priorité "utile" concernant ? . (Autrement dit, qui attendent se lie plus étroitement que?). Cela peut surprendre certains utilisateurs qui lisent du code, mais je pense que les fonctions qui renvoient des résultats futurs seront extrêmement plus courantes que les fonctions qui renvoient des résultats futurs.
  • Le préfixe attend avec la priorité "évidente" concernant ? . (Autrement dit, cela? Lie plus serré que d'attendre). Sucre de syntaxe supplémentaire await? pour une attente et? opérateur. Je pense que ce sucre de syntaxe est nécessaire pour rendre cet ordre de priorité viable, sinon tout le monde écrira (await future)? tout le temps, ce qui est une pire variante de la première option que j'ai énumérée.
  • Postfix attend avec l'espace de syntaxe wait. Cela résout le problème de priorité en ayant un ordre visuel clair entre les deux opérateurs. Je me sens mal à l'aise avec cette solution à bien des égards.

Mon propre classement parmi ces choix change chaque fois que j'examine la question. À partir de ce moment, je pense que l'utilisation de la préséance évidente avec le sucre semble être le meilleur équilibre entre ergonomie, familiarité et compréhension. Mais dans le passé, j'ai privilégié l'une des deux autres syntaxes de préfixe.

Pour des raisons de discussion, je vais donner à ces quatre options ces noms:

Nom | Avenir | Avenir du résultat | Résultat du futur
--- | --- | --- | ---
Délimiteurs obligatoires | await(future) ou await { future } | await(future)? ou await { future }? | await(future?) ou await { future? }
Priorité utile | await future | await future? | await (future?)
Préséance évidente avec sucre | await future | await? future ou (await future)? | await future?
Mot-clé Postfix | future await | future await? | future? await

(J'ai spécifiquement utilisé "postfix keyword" pour distinguer cette option des autres syntaxes postfix comme "postfix macro".)

L'une des lacunes de la `` bénédiction '' await future? dans la préséance utile mais aussi d'autres qui ne fonctionnent pas comme post-correction serait que les modèles habituels de conversion manuelle d'expressions avec ? peuvent ne plus s'appliquer, ou exiger que Future réplique explicitement les méthodes Result d'une manière compatible. Je trouve cela surprenant. S'ils sont répliqués, il devient soudainement aussi déroutant de savoir lesquels des combinateurs travaillent sur un futur retourné et lesquels sont impatients. En d'autres termes, il serait aussi difficile de décider ce que fait réellement un combinateur que dans le cas de l'attente implicite. (Edit: en fait, voir deux commentaires ci-dessous où j'ai une perspective plus technique ce que je veux dire par un remplacement surprenant de ? )

Un exemple où nous pouvons récupérer d'un cas d'erreur:

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

Ce problème ne se produit pas pour les opérateurs de post-correction réels:

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

Ce sont des expressions différentes, l'opérateur point a une priorité plus élevée que l'opérateur "priorité évidente" await , donc l'équivalent est:

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

Cela a exactement la même ambiguïté quant or_recover savoir si or_recover si pour une raison quelconque vous avez besoin de savoir si cette partie spécifique est asynchrone).

Cela a exactement la même ambiguïté de savoir si or_recover est asynchrone ou non.

Pas exactement pareil. unwrap_or_else doit produire une coroutine car elle est attendue, donc l'ambiguïté est de savoir si get_result est une coroutine (donc un combinateur est construit) ou un Result<impl Future, _> (et Ok contient déjà une coroutine, et Err construit une). Les deux n'ont pas les mêmes préoccupations de pouvoir identifier d'un coup d'œil le gain d'efficacité en déplaçant un point de séquence await vers un join , ce qui est l'une des principales préoccupations de attendre implicite. La raison en est que dans tous les cas, ce calcul intermédiaire doit être sync et doit avoir été appliqué au type avant await et doit avoir abouti à la coroutine attendue. Il y a une autre préoccupation plus grande ici:

Ce sont des expressions différentes, l'opérateur point a une priorité plus élevée que l'opérateur d'attente "priorité évidente", donc l'équivalent est

Cela fait partie de la confusion, le remplacement de ? par une opération de récupération a fondamentalement changé la position de await . Dans le contexte de la syntaxe ? , étant donné une expression partielle expr de type T , j'attends la sémantique suivante d'une transformation (en supposant que T::unwrap_or_else existe) :

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

Cependant, sous 'Priorité utile' et await expr? ( await expr donne T ) nous obtenons à la place

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

alors que dans une préséance évidente, cette transformation ne s'applique plus du tout sans paranthèse supplémentaire, mais au moins l'intuition fonctionne toujours pour «Résultat du futur».

Et qu'en est-il du cas encore plus intéressant où vous attendez à deux points différents dans une séquence de combinateur? Avec n'importe quelle syntaxe de préfixe, je pense que cela nécessite des paranthèses. Le reste du langage Rust essaie longtemps d'éviter cela pour faire fonctionner les «expressions évaluées de gauche à droite», un exemple de ceci est la magie d'auto-ref.

Exemple pour montrer que cela empire pour les chaînes plus longues avec plusieurs points d'attente / essai / combinaison.

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

Ce que j'aimerais voir évité, c'est de créer l'analogue Rust de l'analyse de type C où vous sautez entre
côtés gauche et droit de l'expression pour les combinateurs 'pointer' et 'array'.

Entrée de table dans le style de @withoutboats :

| Nom | Avenir | Avenir du résultat | Résultat du futur |
| - | - | - | - |
| Délimiteurs obligatoires | await(future) | await(future)? | await(future?) |
| Priorité utile | await future | await future? | await (future?) |
| Préséance évidente | await future | await? future | await future? |
| Appel Postfix | future(await) | future(await)? | future?(await) |

| Nom | Enchaîné |
| - | - |
| Délimiteurs obligatoires | await(await(foo())?.bar())? |
| Priorité utile | await(await foo()?).bar()? |
| Préséance évidente | await? (await? foo()).bar() |
| Appel Postfix | foo()(await)?.bar()(await) |

Je suis fortement en faveur d'un postfix wait pour diverses raisons mais je n'aime pas la variante montrée par @withoutboats , principalement il semble pour les mêmes raisons. Par exemple. foo await.method() est déroutant.

Regardons d'abord un tableau similaire mais en ajoutant quelques variantes de postfix supplémentaires:

| Nom | Avenir | Avenir du résultat | Résultat du futur |
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| Délimiteurs obligatoires | await { future } | await { future }? | await { future? } |
| Priorité utile | await future | await future? | await (future?) |
| Préséance évidente | await future | await? future | await future? |
| Mot-clé Postfix | future await | future await? | future? await |
| Champ Postfix | future.await | future.await? | future?.await |
| Méthode Postfix | future.await() | future.await()? | future?.await() |

Regardons maintenant une expression future chaînée:

| Nom | Futures chaînées des résultats |
| ---------------------- | -------------------------- ----------- |
| Délimiteurs obligatoires | await { await { foo() }?.bar() }? |
| Priorité utile | await (await foo()?).bar()? |
| Préséance évidente | await? (await? foo()).bar() |
| Mot-clé Postfix | foo() await?.bar() await? |
| Champ Postfix | foo().await?.bar().await? |
| Méthode Postfix | foo().await()?.bar().await()? |

Et maintenant, pour un exemple dans le monde réel, à partir de reqwests , où vous voudrez peut-être attendre un futur enchaîné de résultats (en utilisant mon formulaire d'attente préféré).

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

En fait, je pense que chaque séparateur a l'air bien pour la syntaxe postfix, par exemple:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
Mais je n'ai pas une forte opinion sur celui à utiliser.

La macro de postfix (ie future.await!() ) pourrait-elle toujours être une option? C'est clair, concis et sans ambiguïté:

| Avenir | Avenir du résultat | Résultat du futur |
| --- | --- | --- |
| future.await! () | future.await! ()? | futur? .await! () |

De plus, la macro postfix nécessite moins d'efforts pour être implémentée, et est facile à comprendre et à utiliser.

De plus, la macro postfix nécessite moins d'efforts pour être implémentée, et est facile à comprendre et à utiliser.

De plus, il utilise simplement une fonctionnalité lang commune (ou du moins cela ressemblerait à une macro de suffixe normale).

Une macro postfix serait bien car elle combine la concision et la chaînabilité de postfix avec les propriétés non magiques et la présence évidente de macros, et s'intégrerait bien avec des macros d'utilisateurs tiers, telles que .await_debug!() , .await_log!(WARN) ou .await_trace!()

Une macro postfix serait bien car elle combine [...] les propriétés non magiques [...] des macros

@novacrazy le problème avec cet argument est que toute macro await! _ serait magique, elle effectue une opération qui n'est pas possible dans le code écrit par l'utilisateur (actuellement, l'implémentation basée sur le générateur sous-jacent est quelque peu exposée, mais je crois comprendre que avant la stabilisation, cela sera complètement caché (et l'interaction avec lui pour le moment nécessite de toute façon d'utiliser des fonctionnalités nocturnes internes de rustc )).

@ Nemo157 Hmm. Je ne savais pas que c'était censé être si opaque.

Est-il trop tard pour reconsidérer l'utilisation d'une macro procédurale comme #[async] pour faire la transformation de la fonction "asynchrone" en fonction du générateur, plutôt qu'un mot clé magique? Il s'agit de trois caractères supplémentaires à taper, et pourraient être marqués dans la documentation de la même manière que #[must_use] ou #[repr(C)] .

Je n'aime vraiment pas l'idée de cacher autant de couches d'abstraction qui contrôlent directement le flux d'exécution. Cela semble contraire à ce qu'est Rust. L'utilisateur doit être en mesure de suivre complètement le code et de comprendre comment tout fonctionne et où va l'exécution. Ils devraient être encouragés à pirater les choses et à tromper les systèmes, et s'ils utilisent Rust sûr, cela devrait être sûr. Cela n'améliore rien si nous perdons le contrôle de bas niveau, et je peux aussi bien m'en tenir à des contrats à terme bruts.

Je crois fermement que Rust, le langage (et non std / core ), devrait fournir des abstractions et une syntaxe uniquement si elles sont impossibles (ou très peu pratiques) à faire par les utilisateurs ou std . Toute cette chose asynchrone est devenue incontrôlable à cet égard. Avons-nous vraiment besoin de quelque chose de plus que l'API pin et les générateurs dans rustc ?

@novacrazy Je suis généralement d'accord avec le sentiment mais pas avec la conclusion.

devrait fournir des abstractions et une syntaxe uniquement si elles sont impossibles (ou très peu pratiques) à faire par les utilisateurs ou std.

Quelle est la raison d'avoir des boucles for dans le langage alors qu'elles pourraient aussi être une macro qui se transforme en loop avec des pauses. Quelle est la raison de || closure quand il pourrait s'agir d'un trait dédié et de constructeurs d'objets. Pourquoi avons-nous introduit ? alors que nous avions déjà try!() . La raison pour laquelle je ne suis pas d’accord avec ces questions et vos conclusions est la cohérence. L'intérêt de ces abstractions n'est pas seulement le comportement qu'elles encapsulent mais aussi l'accessibilité de celui-ci. for -replacement se décompose en mutabilité, chemin de code principal et lisibilité. || -replacement se décompose en verbosité de la déclaration - similaire à Futures actuellement. try!() se décompose dans l'ordre attendu des expressions et de la composabilité.

Considérez que async n'est pas seulement le décorateur d'une fonction, mais qu'il existe d'autres idées pour fournir des modèles supplémentaires par aync-blocks et async || . Puisqu'elle s'applique à différents éléments de langage, la facilité d'utilisation d'une macro semble sous-optimale. Ne même pas penser à la mise en œuvre si elle doit être visible par l'utilisateur alors.

L'utilisateur doit être en mesure de suivre complètement le code et de comprendre comment tout fonctionne et où va l'exécution. Ils devraient être encouragés à pirater les choses et à tromper les systèmes, et s'ils utilisent Rust sûr, cela devrait être sûr.

Je ne pense pas que cet argument s'applique parce que l'implémentation de coroutines utilisant entièrement std api reposerait probablement fortement sur unsafe . Et puis il y a l'argument inverse , car alors qu'il est faisable et vous ne serez pas arrêté même s'il y a moyen syntaxique et sémantique dans la langue de le faire-tout changement va être fortement au risque de rupture des hypothèses faites en unsafe -code. Je soutiens que Rust ne devrait pas donner l'impression qu'il essaie d'offrir une interface standard à l'implémentation de bits qu'il n'a pas l'intention de stabiliser bientôt, y compris les composants internes de Coroutines. Un analogue à cela serait extern "rust-call" qui sert de magie actuelle pour indiquer clairement que les appels de fonction n'ont pas une telle garantie. Nous pourrions vouloir ne jamais avoir à return , même si le sort des coroutines empilées n'a pas encore été décidé. Nous pourrions vouloir accrocher une optimisation plus profondément dans le compilateur.

A part: En parlant de quelle idée, en théorie, pas aussi sérieuse, pourrait-on attendre la coroutine comme un hypothétique extern "await-call" fn () -> T ? Si tel est le cas, cela permettrait en prélude un

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

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

alias. future.await() dans les éléments documentés d'un espace utilisateur. Ou d'ailleurs, une autre syntaxe d'opérateur pourrait également être possible.

@HeroicKatora

Pourquoi avons-nous introduit ? alors que nous avions déjà try!()

Pour être honnête, j'étais également contre cela, même si cela s'est développé sur moi. Ce serait plus acceptable si Try était un jour stabilisé, mais c'est un autre sujet.

Le problème avec les exemples de «sucre» que vous donnez est qu'ils sont du sucre très, très mince. Même impl MyStruct est plus ou moins de sucre pour impl <anonymous trait> for MyStruct . Ce sont des sucres de qualité de vie qui n'ajoutent aucun frais généraux.

En revanche, les générateurs et les fonctions asynchrones ajoutent une surcharge pas tout à fait insignifiante et une surcharge mentale importante. Les générateurs en particulier sont très difficiles à implémenter en tant qu'utilisateur et pourraient être utilisés plus efficacement et plus facilement dans le cadre du langage lui-même, tandis que l'async pourrait être implémenté relativement facilement.

Le point sur les blocs ou fermetures asynchrones est intéressant cependant, et je concède qu'un mot-clé y serait plus utile, mais je m'oppose toujours à l'incapacité d'accéder aux éléments de niveau inférieur si nécessaire.

Idéalement, il serait merveilleux de prendre en charge le mot clé async et une macro d'attribut / procédurale #[async] , la première permettant un accès de bas niveau au générateur généré (sans jeu de mots). Pendant ce temps, yield devrait être interdit dans les blocs ou les fonctions utilisant async comme mot-clé. Je suis sûr qu'ils pourraient même partager le code de mise en œuvre.

Comme pour await , si les deux sont possibles, nous pourrions faire quelque chose de similaire, et limiter le mot-clé await à async fonctions / blocs de mots-clés, et utiliser une sorte de await!() macro dans les fonctions #[async] .

Pseudocode:

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

Le meilleur des deux mondes.

Cela ressemble à une progression plus naturelle de la complexité à présenter à l'utilisateur, et peut être utilisé plus efficacement pour plus d'applications.

N'oubliez pas que ce problème concerne spécifiquement la discussion de la syntaxe de await . D'autres conversations sur la façon dont les fonctions et les blocs async sont implémentés sont hors de portée, sauf dans le but de rappeler aux gens que await! n'est pas quelque chose que vous pouvez ou ne pourrez jamais écrire dans Rust's langue de surface.

Je voudrais pondérer spécifiquement les avantages et les inconvénients de toutes les propositions de syntaxe post-correction. Si l'une des syntaxes se démarque avec un petit nombre d'inconvénients, nous devrions peut-être y aller. Dans le cas contraire, il serait préférable de prendre en charge une syntaxe délimitée par un préfixe de syntaxe qui est compatible avec une post-correction encore à déterminer si le besoin s'en fait sentir. Comme Postfix semble résonner comme étant le plus concis pour quelques membres, il semble pratique de les évaluer fortement avant de passer à d'autres.

La comparaison sera syntax , example (le reqwest de @mehcode ressemble à un benchmark du monde réel utilisable à cet égard), puis un tableau de ( concerns , et facultatif resolution , par exemple s'il est convenu que cela pourrait se résumer à l'enseignement). N'hésitez pas à ajouter de la syntaxe et / ou des préoccupations, je les éditerai dans ce post collectif. Si je comprends bien, toute syntaxe qui n'implique pas await semblera très probablement étrangère aux nouveaux arrivants et aux utilisateurs expérimentés, mais toutes celles actuellement répertoriées l'incluent.

Exemple dans une syntaxe de préfixe pour référence seulement, ne faites pas de bikeshed cette partie s'il vous plaît:

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

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

    • | Préoccupation | Résolution |

      | - | - |

      | Le chaînage sans ? peut être déroutant ou interdit | |

  • Champ Postfix foo().await?

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

    • | Préoccupation | Résolution |

      | - | - |

      | Ressemble à un champ | |

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

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

    • | Préoccupation | Résolution |

      | - | - |

      | Ressemble à une méthode ou à un trait | Il peut être documenté comme ops:: trait? |

      | Pas un appel de fonction | |

  • Appel Postfix foo()(await)?

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

    • | Préoccupation | Résolution |

      | - | - |

      | Peut être confondu avec l'argument réel | mot-clé + mise en évidence + pas de chevauchement |

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

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

    • | Préoccupation | Résolution |

      | - | - |

      | Ne sera pas réellement une macro… | |

      | … Ou, await n'est plus un mot clé | |

Une réflexion supplémentaire sur le post-correctif vs le préfixe du point de vue de l'incorporation éventuelle de générateurs: compte tenu des valeurs, yield et await occupent deux types d'instructions opposées. Le premier donne une valeur de votre fonction vers l'extérieur, le second accepte une valeur.

Sidenote: Eh bien, Python a des générateurs interactifsyield peut renvoyer une valeur. Symétriquement, les appels à un tel générateur ou à un flux nécessitent des arguments supplémentaires dans un paramètre de type fortement. N'essayons pas de généraliser trop loin, et nous verrons que l'argument est probablement transféré dans les deux cas.

Ensuite, je soutiens qu'il n'est pas naturel que ces déclarations se ressemblent. Ici, comme pour les expressions d'affectation, nous devrions peut-être nous écarter d'une norme définie par d'autres langages lorsque cette norme est moins cohérente et moins concise pour Rust. Comme exprimé ailleurs, tant que nous incluons await et que des similitudes avec d'autres expressions avec le même ordre d'argumentation existent, il ne devrait y avoir aucun obstacle majeur à la transition même à partir d'un autre modèle.

Depuis implicite semble hors de la table.

En utilisant async / await dans d'autres langages et en regardant les options ici, je n'ai jamais trouvé qu'il était syntaxiquement agréable d'enchaîner les futurs.

Une variante non chaînable est-elle sur la table?

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

J'aime un peu ça, car vous pourriez faire valoir que cela fait partie du modèle.

Le problème avec l'attente d'une reliure est que l'histoire d'erreur est loin d'être agréable.

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

Nous devons garder à l'esprit que presque tous les futurs disponibles dans la communauté à attendre sont faillibles et doivent être combinés avec ? .

Si nous avons vraiment besoin du sucre de syntaxe:

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

Il faudrait ajouter à la fois await et await? en tant que mots clés ou nous l'étendons également à let , c'est- let? result = 1.divide(0); dire

Compte tenu de la fréquence à laquelle le chaînage est utilisé dans le code Rust, je suis tout à fait d'accord qu'il est important que l'attente chaînée soit aussi claire que possible pour le lecteur. Dans le cas de la variante postfix de await:

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

Cela se comporte généralement de la même manière que je m'attends à ce que le code Rust se comporte. J'ai un problème avec le fait que await() dans ce contexte ressemble à un appel de fonction, mais a un comportement magique (sans fonction) dans le contexte de l'expression.

La version macro de postfix rendrait cela plus clair. Les gens sont habitués aux points d'exclamation en rouille signifiant "il y a de la magie ici" et j'ai certainement une préférence pour cette version pour cette raison.

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

Cela dit, cela vaut la peine de considérer que nous avons déjà try!(expr) dans la langue et que c'était notre précurseur pour ? . Ajouter une macro await!(expr) maintenant serait tout à fait cohérent avec la façon dont try!(expr) et ? ont été introduits dans le langage.

Avec la version await!(expr) de await, nous avons la possibilité soit de migrer vers une macro postfixe plus tard , soit d'ajouter un nouvel opérateur de style ? sorte que le chaînage devienne facile. Un exemple similaire à ? mais pour await:

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

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

Je pense que nous devrions utiliser await!(expr) ou await!{expr} pour l'instant car c'est à la fois très raisonnable et pragmatique. Nous pouvons alors prévoir de migrer vers une version postfixe de await (ie .await! ou .await!() ) plus tard si / une fois les macros postfix deviennent une chose. (Ou éventuellement en passant par l'ajout d'un opérateur de style ? supplémentaire ... après beaucoup de bikeshedding sur le sujet: P)

FYI, la syntaxe de Scala n'est pas Await.result car c'est un appel bloquant. Les Futures de Scala sont des monades et utilisent donc des appels de méthode normaux ou la compréhension de la monade for :

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

À la suite de cette horrible notation, une bibliothèque appelée scala-async été créée avec la syntaxe à laquelle je suis le plus favorable, qui est la suivante:

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
}

Cela reflète fortement ce à quoi je voudrais que le code rust ressemble, avec l'utilisation de délimiteurs obligatoires, et, en tant que tel, je voudrais être d'accord avec les autres pour rester avec la syntaxe actuelle de await!() . Early Rust était lourd de symboles et a été éloigné pour une bonne raison, je suppose. L'utilisation du sucre syntaxique sous la forme d'un opérateur postfix (ou autre) est, comme toujours, rétrocompatible, et la clarté de await!(future) est sans ambiguïté. Cela reflète également la progression que nous avons eue avec try! , comme mentionné précédemment.

L'avantage de le conserver sous forme de macro est qu'il est plus immédiatement évident en un coup d'œil qu'il s'agit d'une fonctionnalité de langage plutôt que d'un appel de fonction normal. Sans l'ajout de ! , la mise en évidence de la syntaxe de l'éditeur / visionneuse serait le meilleur moyen de pouvoir repérer les appels, et je pense que se fier à ces implémentations est un choix plus faible.

Mes deux cents (pas un contributeur régulier, fwiw) Je suis le plus partisan de copier le modèle de try! . Cela a été fait une fois auparavant, cela a bien fonctionné et après qu'il est devenu très populaire, il y avait suffisamment d'utilisateurs pour envisager un opérateur postfix.

Donc mon vote serait: stabiliser avec await!(...) et punt sur un opérateur postfix pour un joli chaînage basé sur un sondage des développeurs Rust. Await est un mot-clé, mais le ! indique que c'est quelque chose de "magique" pour moi et les parenthèses le maintiennent sans ambiguïté.

Aussi une comparaison:

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

Mon troisième centime est que j'aime @ (pour «attendre») et # (pour représenter le multi-threading / concurrence).

J'aime aussi le suffixe @ ! Je pense que ce n'est en fait pas une mauvaise option, même s'il semble que ce n'est pas viable.

  • _ @ for await_ est un mnémonique agréable et facile à retenir
  • ? et @ seraient très similaires, donc apprendre @ après avoir appris ? ne devrait pas être un tel saut
  • Il facilite le balayage d'une chaîne d'expressions de gauche à droite, sans avoir à parcourir en avant pour trouver un délimiteur de fermeture afin de comprendre une expression

Je suis très favorable à la syntaxe await? foo , et je pense qu'elle est similaire à une syntaxe vue en mathématiques, où par exemple. sin² x peut être utilisé pour signifier (sin x) ². Cela semble un peu gênant au début, mais je pense que c'est très facile de s'y habituer.

Comme dit ci-dessus, je suis favorable à l'ajout de await!() tant que macro, tout comme try!() , pour l'instant et finalement à décider comment le postfixer. Si nous pouvons garder à l'esprit le support d'un rustfix qui convertit automatiquement les appels await!() vers le postfix, attend que ce soit encore à décider, encore mieux.

L'option de mot-clé postfix est clairement gagnante pour moi.

  • Il n'y a pas de problème de priorité / ordre, mais l'ordre pourrait toujours être rendu explicite avec des parenthèses. Mais surtout pas besoin d'imbrication excessive (argument similaire pour préférer le suffixe «?» Au remplacement de «try ()!»).

  • Cela a l'air bien avec le chaînage multiligne (voir le commentaire précédent de @earthengine), et encore une fois, il n'y a aucune confusion concernant la commande ou ce qui est attendu. Et pas d'imbrication / de parenthèses supplémentaires pour les expressions avec de multiples utilisations de await:

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;
  • Elle se prête à une simple macro await! () (Voir le commentaire précédent de @novacrazy):
macro_rules! await {
    ($e:expr) => {{$e await}}
}
  • Même sur une seule ligne, nue (sans le '?'), Le postfix attend le chaînage des mots clés ne me dérange pas car il se lit de gauche à droite et nous attendons le retour d'une valeur sur laquelle la méthode suivante opère alors (même si je voudrais juste préférez le code rustfmt sur plusieurs lignes). L'espace rompt la ligne et est suffisant comme indicateur visuel / signal que l'attente se produit:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Pour suggérer un autre candidat que je n'ai pas encore vu mettre en avant (peut-être parce qu'il ne serait pas analysable), qu'en est-il d'un amusant opérateur postfix à double point '..'? Cela me rappelle que nous attendons quelque chose (le résultat!) ...

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

J'aime aussi le suffixe @ ! Je pense que ce n'est en fait pas une mauvaise option, même s'il semble que ce n'est pas viable.

  • _ @ for await_ est un mnémonique agréable et facile à retenir
  • ? et @ seraient très similaires, donc apprendre @ après avoir appris ? ne devrait pas être un tel saut
  • Il facilite le balayage d'une chaîne d'expressions de gauche à droite, sans avoir à parcourir en avant pour trouver un délimiteur de fermeture afin de comprendre une expression

Je ne suis pas fan de l'utilisation de @ pour await. Il est difficile de taper sur un clavier à disposition fin / balayage car je dois appuyer sur alt-gr avec mon pouce droit, puis appuyer sur la touche 2 sur la ligne de numéro. De plus, @ a une signification bien établie (at), donc je ne vois pas pourquoi nous devrions en confondre la signification.

Je préfère de loin taper simplement await , c'est plus rapide car il ne nécessite aucune acrobatie au clavier.

Voici ma propre évaluation, très subjective. J'ai également ajouté future@await , ce qui me semble intéressant.

| syntaxe | notes |
| --- | --- |
| await { f } | fort:

  • très simple
  • parallèles for , loop , async etc.
faible:
  • très verbeux (5 lettres, 2 accolades, 3 espaces facultatifs, mais probablement peluchés)
  • le chaînage entraîne de nombreuses accolades imbriquées ( await { await { foo() }?.bar() }? )
|
| await f | fort:
  • parallèles await syntaxe de Python, JS, C # et Dart
  • simple, court
  • la priorité utile et la priorité évidente se comportent bien avec ? ( await fut? contre await? fut )
faible:
  • ambiguë: la préséance utile ou évidente doit être apprise
  • le chaînage est également très lourd ( await (await foo()?).bar()? vs await? (await? foo()).bar() )
|
| fut.await
fut.await()
fut.await!() | fort:
  • permet un chaînage très facile
  • court
  • belle complétion de code
faible:
  • tromper les utilisateurs en leur faisant croire que c'est un champ / fonction / macro défini quelque part. Edit: Je suis d'accord avec @jplatte que await!() est le moins magique
|
| fut(await) | fort:
  • permet un chaînage très facile
  • court
faible:
  • tromper les utilisateurs en leur faisant croire qu'il existe une variable await définie quelque part et que les futurs peuvent être appelés comme une fonction
|
| f await | fort:
  • permet un chaînage très facile
  • court
faible:
  • ne correspond à rien dans la syntaxe de Rust, pas évident
  • mon cerveau regroupe les client.get("https://my_api").send() await.unwrap().json() await.unwrap() en client.get("https://my_api").send() , await.unwrap().json() et await.unwrap() (groupés par abord, puis . ) ce qui n'est pas correct
  • pour Haskellers: ressemble à du curry mais ne l'est pas
|
| f@ | fort:
  • permet un chaînage très facile
  • très court
faible:
  • semble un peu maladroit (du moins au début)
  • consomme @ qui pourrait être mieux adapté à autre chose
  • peut être facile à ignorer, en particulier dans les grandes expressions
  • utilise @ d'une manière différente de toutes les autres langues
|
| f@await | fort:
  • permet un chaînage très facile
  • court
  • belle complétion de code
  • await n'a pas besoin de devenir un mot clé
  • compatible vers l'avant: permet d'ajouter de nouveaux opérateurs de suffixe sous la forme @operator . Par exemple, le ? aurait pu être fait comme @try .
  • mon cerveau regroupe les client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() dans les bons groupes (groupés par . abord, puis @ )
faible:
  • utilise @ d'une manière différente de toutes les autres langues
  • pourrait inciter à ajouter trop d'opérateurs postfix inutiles
|

Mes scores:

  • familiarity (fam): à quel point cette syntaxe est-elle proche des syntaxes connues (Rust et autres, comme Python, JS, C #)
  • évidence (obv): Si vous deviez lire ceci dans le code de quelqu'un d'autre pour la première fois, seriez-vous capable de deviner la signification, la préséance, etc.?
  • verbosité (vrb): combien de caractères il faut pour écrire
  • visibilité (vis): à quel point il est facile de repérer (plutôt que d'oublier) dans le code
  • chaining (cha): Comme il est facile de l'enchaîner avec . et autres await
  • grouping (grp): si mon cerveau regroupe le code dans les bons morceaux
  • compatibilité ascendante (fwd): si cela permet d'être ajusté plus tard de manière insécable

| syntaxe | 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 |

Il me semble que nous devrions refléter la syntaxe try!() dans la première coupe et obtenir une utilisation réelle de l'utilisation de await!(expr) avant d'introduire une autre syntaxe.

Cependant, si / quand nous construisons une syntaxe alternative.

Je pense que @ air moche, "at" pour "async" ne me semble pas aussi intuitif, et le symbole est déjà utilisé pour la correspondance de motifs.

async préfixe ? (ce qui sera souvent).

Postfix .await!() enchaîne bien, semble assez immédiatement évident dans sa signification, inclut le ! pour me dire qu'il va faire de la magie, et est moins roman syntaxiquement, donc de la "prochaine coupe" approche I personnellement favoriserait celui-ci. Cela dit, pour moi, il reste à voir à quel point cela améliorerait le code réel par rapport à la première coupe await! (expr) .

Je préfère l'opérateur de préfixe pour les cas simples:
let result = await task;
Cela semble beaucoup plus naturel en tenant compte du fait que nous n'écrivons pas le type de résultat, donc attendre aide mentalement lors de la lecture de gauche à droite pour comprendre que le résultat est une tâche avec l'attente.
Imaginez-le comme ceci:
let result = somehowkindoflongtask await;
tant que vous n'atteignez pas la fin de la tâche, vous ne vous rendez pas compte que le type qu'il renvoie doit être attendu. Gardez à l'esprit aussi (bien que cela soit sujet à changement et non directement lié à l'avenir du langage) que les IDE en tant qu'Intellij insèrent le type (sans aucune personnalisation, si cela est même possible) entre le nom et les égaux.
Imaginez-le comme ceci:
6voler6ykj

Cela ne veut pas dire que mon opinion est à cent pour cent en faveur du préfixe. Je préfère fortement la version postfix de future lorsque les résultats sont impliqués, car cela semble beaucoup plus naturel. Sans aucun contexte, je peux facilement dire lequel de ces mots signifie quoi:
future await?
future? await
Regardez plutôt celui-ci, lequel de ces deux est vrai, du point de vue d'un débutant:
await future? === await (future?)
await future? === (await future)?

Je suis en faveur du mot-clé préfixe: await future .

C'est celui utilisé par la plupart des langages de programmation qui ont async / await, et est donc immédiatement familier aux personnes qui connaissent l'un d'entre eux.

En ce qui concerne la priorité de await future? , quel est le cas courant?

  • Une fonction retournant un Result<Future> qui doit être attendu.
  • Un futur à attendre qui renvoie un Result : Future<Result> .

Je pense que le deuxième cas est beaucoup plus courant lorsqu'il s'agit de scénarios typiques, car les opérations d'E / S peuvent échouer. Par conséquent:

await future? <=> (await future)?

Dans le premier cas moins courant, il est acceptable d'avoir des parenthèses: await (future?) . Cela pourrait même être une bonne utilisation de la macro try! si elle n'était pas obsolète: await try!(future) . De cette façon, l'opérateur await et le point d'interrogation ne sont pas des côtés différents du futur.

Pourquoi ne pas prendre await comme premier paramètre de fonction async ?

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

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

let output = future
    .run(await);

Ici future.run(await) est une alternative à await future .
Il peut s'agir simplement d'une fonction async régulière qui prend le futur et exécute simplement la macro await!() dessus.

C ++ (TR de concurrence)

auto result = co_await task;

C'est dans le Coroutines TS, pas la concurrence.

Une autre option pourrait être d'utiliser le mot clé become au lieu 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 pourrait cependant être un mot-clé pour le TCO garanti

Merci @EyeOfPython pour cet [aperçu]. C'est particulièrement utile pour les personnes qui viennent tout juste de rejoindre le hangar à vélos.

Personnellement, j'espère que nous resterons à l'écart de f await , simplement parce que la syntaxe est très peu rustique et que cela semble un peu spécial et magique. Cela deviendrait l'une de ces choses sur lesquelles les utilisateurs novices de Rust seront souvent confus et je pense que cela n'ajoute pas beaucoup de clarté, même aux vétérans de Rust, pour en valoir la peine.

@novacrazy le problème avec cet argument est que toute macro await! _ serait magique, elle effectue une opération qui n'est pas possible dans le code écrit par l'utilisateur

@ Nemo157 Je suis d'accord qu'une macro await! serait magique, mais je dirais que ce n'est pas un problème. Il y a déjà plusieurs macros de ce type dans std , par exemple compile_error! et je n'ai vu personne s'en plaindre. Je pense qu'il est normal d'utiliser une macro en ne comprenant que ce qu'elle fait, pas comment elle le fait.

Je suis d'accord avec les commentateurs précédents que postfix serait le plus ergonomique, mais je préférerais commencer par prefix-macro await!(expr) et potentiellement passer à postfix-macro une fois que c'est une chose plutôt que d'avoir expr.await (champ intégré au compilateur magique) ou expr.await() (méthode intégrée au compilateur magique). Les deux introduiraient une syntaxe complètement nouvelle uniquement pour cette fonctionnalité, et l'OMI qui rendrait le langage incohérent.

@EyeOfPython Voulez- vous ajouter future(await) à vos listes et à votre table? Tous les points positifs de votre évaluation de future.await() semblent se transférer sans la faiblesse

Puisque certains ont soutenu que malgré la coloration syntaxique, foo.await ressemble trop à un accès à un champ, nous pourrions changer le jeton . en # et écrire à la place foo#await . Par exemple:

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

Pour illustrer comment GitHub rendrait cela avec la coloration syntaxique, remplaçons await par match puisqu'ils sont de longueur égale:

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

Cela semble à la fois clair et ergonomique.

La justification de # plutôt qu'un autre jeton n'est pas spécifique, mais le jeton est assez visible, ce qui aide.

Donc, autre concept: si le futur était référence :

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;

Ce serait vraiment moche et incohérent. Laissez au moins lever l'ambiguïté de cette syntaxe:

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;

Mieux, mais toujours moche (et pire encore, cela rend la coloration syntaxique de github). Cependant, pour gérer cela, introduisons la possibilité de retarder l'opérateur de préfixe :

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;

C'est exactement ce que je veux! Non seulement pour await ( $ ) mais aussi pour deref ( * ) et negate ( ! ).

Quelques mises en garde dans cette syntaxe:

  1. Priorité des opérateurs: pour moi c'est évident mais pour les autres utilisateurs?
  2. Interopérabilité des macros: le symbole $ causerait-il des problèmes ici?
  3. Incohérence de l'expression: l'opérateur de préfixe principal s'applique à l'expression entière tandis que l'opérateur de préfixe retardé s'applique uniquement à l'expression délimitée . ( await_chain fn le démontrent), est-ce déroutant?
  4. Analyse et implémentation: cette syntaxe est-elle valable du tout?

@Centril
IMO # est trop proche de la syntaxe littérale brute r#"A String with "quotes""#

IMO # est trop proche de la syntaxe littérale brute r#"A String with "quotes""#

Il semble assez clair d'après le contexte quelle est la différence dans ce cas.

@Centril
C'est le cas, mais la syntaxe est également vraiment étrangère au style que Rust utilise à mon humble avis. Il ne ressemble à aucune syntaxe existante avec une fonction similaire.

@Laaas Pas plus que ? lors de son introduction. Je serais ravi de choisir .await ; mais d'autres semblent mécontents de cela, alors j'essaie de trouver autre chose qui fonctionne (c'est-à-dire qui est clair, ergonomique, suffisamment facile à taper, chaînable, a une bonne priorité) et foo#await semble satisfaire tout cela.

? a cependant un tas d'autres bons points, tandis que #await semble plutôt arbitraire. Dans tous les cas, si vous voulez quelque chose comme .await , pourquoi pas .await! ?

@Centril C'était la raison principale derrière future(await) , pour éviter l'accès au champ sans avoir à ajouter d'opérateurs supplémentaires ou de syntaxe étrangère.

tandis que #await semble plutôt arbitraire.

# est arbitraire, oui - est-ce une réussite ou une rupture? Lorsqu'une nouvelle syntaxe est inventée à un moment donné, elle doit être arbitraire parce que quelqu'un a pensé qu'elle avait l'air bien ou avait du sens.

pourquoi pas .await! ?

C'est tout aussi arbitraire, mais je n'y suis pas opposé.

@Centril C'était la raison principale derrière future(await) , pour éviter l'accès au champ sans avoir à ajouter d'opérateurs supplémentaires ou de syntaxe étrangère.

Au lieu de cela, il ressemble à une application de fonction où vous passez await à future ; cela me semble plus déroutant que "l'accès au champ".

Au lieu de cela, il ressemble à une application de fonction où vous passez en attente vers le futur; cela me semble plus déroutant que "l'accès au champ".

Pour moi, cela faisait partie de la concision. Await est dans de nombreux aspects comme un appel de fonction (l'appelé s'exécute entre vous et votre résultat), et le passage d'un mot-clé à cette fonction a clairement indiqué qu'il s'agissait d'un autre type d'appel. Mais je vois comment il pourrait être déroutant que le mot-clé soit similaire à tout autre argument. (Moins la coloration syntaxique et les messages d'erreur du compilateur essayant de renforcer ce message. Futures ne peut pas être appelé autrement que d'être attendu et vice versa).

@Centril

Être arbitraire n'est pas une chose négative , mais avoir une ressemblance avec une syntaxe existante est une chose positive. .await! n'est pas aussi arbitraire, car ce n'est pas une syntaxe entièrement nouvelle; après tout, c'est juste une macro de post-correction nommée await .

Pour clarifier / ajouter au point sur la nouvelle syntaxe spécifiquement pour await , ce que je veux dire par là est l'introduction d'une nouvelle syntaxe qui diffère de la syntaxe existante. Il y a beaucoup de mots-clés préfixes, dont certains ont été ajoutés après Rust 1.0 (par exemple union ), mais AFAIK pas un seul mot-clé suffixe (peu importe s'il est séparé par un espace, un point ou autre chose) . Je ne peux pas non plus penser à une autre langue qui a des mots-clés postfixes.

À mon humble avis, la seule syntaxe postfix qui n'augmente pas considérablement l'étrangeté de Rust est la macro postfix, car elle reflète les appels de méthode mais peut clairement être identifiée comme une macro par quiconque a déjà vu une macro Rust.

J'ai aussi aimé la macro standard await!() . C'est juste clair et simple.
Je préfère qu'un accès de type champ (ou tout autre suffixe) coexiste en tant que awaited .

  • Await sent que vous attendez ce qui vient ensuite / juste. Attendu, à ce qui est venu avant / gauche.
  • Cohérent à la méthode cloned() sur les itérateurs.

Et j'ai aussi aimé le @? comme du sucre semblable à ? .
C'est personnel, mais je préfère en fait la combinaison &? , car @ tendance à être trop grand et à dépasser la ligne, ce qui est très distrayant. & est également grand (bon) mais ne sous-estime pas (également bon). Malheureusement, & déjà un sens, bien qu'il soit toujours suivi d'un ? .. Je suppose?

Par exemple.

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

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

Pour moi, le @ ressemble à un personnage gonflé, distrait le flux de lecture. D'un autre côté, &? est agréable, ne distrait pas (ma) lecture et a un bel espace supérieur entre le & et le ? .

Personnellement, je pense qu'une simple macro await! devrait être utilisée. Si les macros post-correctives entrent dans le langage, alors la macro pourrait simplement être développée, non?

@Laaas Autant que j'aimerais qu'il y en ait, il n'y a pas encore de macros postfix dans Rust, ce sont donc une nouvelle syntaxe. Notez également que foo.await!.bar et foo.await!().bar ne sont pas la même syntaxe de surface. Dans ce dernier cas, il y aurait un suffixe réel et une macro intégrée (ce qui implique de renoncer à await comme mot clé).

@jplatte

Il y a beaucoup de mots-clés préfixes, dont certains ont été ajoutés après Rust 1.0 (par exemple union ),

union n'est pas un opérateur d'expression unaire, il n'est donc pas pertinent dans cette comparaison. Il y a exactement 3 opérateurs de préfixes unaires stables dans Rust ( return , break et continue ) et tous sont tapés à ! .

Hmm ... Avec @ ma proposition précédente est légèrement meilleure:

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 C'est mon point, puisque nous n'avons pas encore de macros de post-correction, cela devrait simplement être await!() . Aussi je voulais dire .await!() FYI. Je ne pense pas que await doive être un mot-clé, bien que vous puissiez le réserver si vous le trouvez problématique.

union n'est pas un opérateur d'expression unaire, il n'est donc pas pertinent dans cette comparaison. Il y a exactement 3 opérateurs de préfixes unaires stables dans Rust ( return , break et continue ) et tous sont tapés à ! .

Cela signifie toujours que la variante préfixe-mot-clé s'inscrit dans le groupe des opérateurs d'expression unaires, même si elle est typée différemment. La variante du mot-clé postfix est une divergence beaucoup plus grande par rapport à la syntaxe existante, trop grande par rapport à l'importance de await dans le langage à mon avis.

Concernant la macro de préfixe et la possibilité de passer à la post-correction: await doit rester un mot-clé pour que cela fonctionne. La macro serait alors un élément de langage spécial où il est permis d'utiliser un mot-clé comme nom sans r# supplémentaire, et r#await!() pourrait mais ne devrait probablement pas invoquer la même macro. À part cela, cela semble être la solution la plus pragmatique pour le rendre disponible.

Autrement dit, conservez await comme mot-clé, mais résolvez await! en une macro lang-item.

@HeroicKatora pourquoi doit-il rester un mot-clé pour que cela fonctionne?

@Laaas Parce que si nous voulons garder la possibilité de transition ouverte, nous devons rester ouverts pour la future syntaxe post-correction qui l'utilise comme mot-clé. Par conséquent, nous devons conserver la réservation de mot-clé afin de ne pas avoir besoin d'une pause d'édition pour la transition.

@Centril

Notez également que foo.await! .Bar et foo.await! (). Bar ne sont pas la même syntaxe de surface. Dans ce dernier cas, il y aurait un suffixe réel et une macro intégrée (ce qui implique de renoncer à wait comme mot clé).

Cela ne pourrait-il pas être résolu en faisant du mot-clé await combiné avec ! résoudre une macro interne (qui ne peut pas être définie par des moyens normaux)? Ensuite, il reste un mot clé mais se résout en une macro dans une syntaxe de macro autrement.

@HeroicKatora Pourquoi await in x.await!() serait-il un mot-clé réservé?

Ce ne serait pas le cas, mais si nous ne résolvons pas la post-correction, ce n'est pas nécessairement la solution à laquelle nous arriverons dans les discussions ultérieures. Si c'était la meilleure possibilité unique convenue, alors nous devrions adopter cette syntaxe de post-correction exacte en premier lieu.

C'est une autre fois que nous rencontrons quelque chose qui fonctionne beaucoup mieux en tant qu'opérateur postfix. Le grand exemple de ceci est try! que nous avons finalement donné son propre symbole ? . Cependant je pense que ce n'est pas la dernière fois où un opérateur postfix est plus optimal et on ne peut pas donner à tout son caractère spécial. Donc je pense que nous ne devrions au moins pas commencer par @ . Ce serait beaucoup mieux si nous avions un moyen de faire ce genre de choses. C'est pourquoi je supporte le style de macro postfix .await!() .

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

Mais pour que cela ait un sens, des macros postfixes elles-mêmes devraient être introduites. Par conséquent, je pense qu'il serait préférable de commencer avec une syntaxe de macro await!(foo) normale. Nous pourrions plus tard étendre cela à foo.await!() ou même foo@ si nous pensons vraiment que c'est assez important pour justifier son propre symbole.
Le fait que cette macro ait besoin d'un peu de magie n'est pas nouveau pour std et pour moi ce n'est pas un gros problème.
Comme le dit @jplatte :

@ Nemo157 Je suis d'accord qu'une macro await! serait magique, mais je dirais que ce n'est pas un problème. Il y a déjà plusieurs macros de ce type dans std , par exemple compile_error! et je n'ai vu personne s'en plaindre. Je pense qu'il est normal d'utiliser une macro en ne comprenant que ce qu'elle fait, pas comment elle le fait.

Un problème récurrent que je vois abordé ici concerne le chaînage et l'utilisation
attendre dans les expressions d'enchaînement. Peut-être qu'il pourrait y avoir une autre solution?

Si nous ne voulons pas utiliser await pour le chaînage et n'utiliser que wait pour
affectations, nous pourrions avoir quelque chose comme simplement remplacer let par await:
await foo = future

Ensuite, pour le chaînage, nous pourrions imaginer une sorte d'opération comme await res = fut1 -> fut2 ou await res = fut1 >>= fut2 .

Le seul cas manquant est l'attente et le retour du résultat, un raccourci
pour await res = fut; res .
Cela pourrait être facilement fait avec un simple await fut

Je ne pense pas avoir vu une autre proposition comme celle-ci (du moins pour le
chaînage), donc en laissant tomber ça ici car je pense que ce serait bien d'utiliser.

@HeroicKatora J'ai ajouté fut(await) à la liste et l'ai classée selon mon opinion.

Si quelqu'un a l'impression que mon score n'est pas correct, dites-le moi!

Au niveau de la syntaxe, je pense que .await!() est propre et permet le chaînage sans ajouter de bizarreries à la syntaxe.

Cependant, si jamais nous obtenons de "vraies" macros postfixes, ce sera un peu bizarre, car vraisemblablement .r#await!() pourrait être masqué, alors que .await!() ne le pourrait pas.

Je suis assez fortement contre l'option "mot-clé postfix" (séparé par un espace comme: foo() await?.bar() await? ), car je trouve que le await est joint à la partie suivante de l'expression, et non la partie sur laquelle il opère. Je préférerais à peu près n'importe quel symbole autre que les espaces ici, et je préférerais même la syntaxe de préfixe à celle-ci malgré ses inconvénients avec les longues chaînes.

Je pense que la "préséance évidente" (avec le await? sucre) est clairement la meilleure option de préfixe, car les délimiteurs obligatoires sont une douleur pour le cas très courant d'attendre une seule instruction, et "précédence utile" n'est pas du tout intuitif, et donc déroutant.

Au niveau de la syntaxe, je pense que .await!() est propre et permet le chaînage sans ajouter de bizarreries à la syntaxe.

Cependant, si jamais nous obtenons de "vraies" macros postfixes, ce sera un peu bizarre, car vraisemblablement .r#await!() pourrait être masqué, alors que .await!() ne le pourrait pas.

Si nous obtenons des macros postfixes et que nous utilisons .await!() nous pouvons annuler la réservation de await comme mot clé et en faire une macro suffixe. L'implémentation de cette macro exigerait encore un peu de magie de courage mais ce serait une vraie macro tout comme compiler_error! est aujourd'hui.

@EyeOfPython Pourriez-vous peut-être expliquer en détail quels changements vous envisagez dans forward-compatibility ? Je ne sais pas de quelle manière fut@await serait supérieur à fut.await!() et await { future } inférieur à await!(future) . La colonne verbosity semble également un peu étrange, certaines expressions sont courtes mais ont une cote inférieure, considère-t-elle les déclarations enchaînées, etc. Tout le reste semble équilibré et ressemble à l'évaluation approfondie la mieux condensée à ce jour.

Après avoir lu cette discussion, il semble que beaucoup de gens souhaitent ajouter une macro await!() normale et découvrir la version postfix plus tard. Cela suppose que nous voulons réellement une version postfixe, ce que je considérerai comme vrai pour le reste de ce commentaire.

Je voudrais donc simplement sonder l'opinion de tout le monde ici: Devrions-nous tous être d'accord sur la syntaxe await!(future) POUR MAINTENANT? Les avantages sont qu'il n'y a aucune ambiguïté d'analyse avec cette syntaxe, et c'est aussi une macro, donc pas de changement de la syntaxe du langage pour prendre en charge ce changement. Les inconvénients sont que cela aura l'air moche pour le chaînage, mais cela n'a pas d'importance car cette syntaxe peut être facilement remplacée par une version postfix automatiquement.

Une fois que nous avons réglé cela et que nous l'avons implémenté dans le langage, nous pouvons continuer la discussion sur la syntaxe d'attente de postfix avec des expériences, des idées et éventuellement d'autres fonctionnalités du compilateur, espérons-le plus matures.

@HeroicKatora Je l'ai noté plus haut car il pourrait libérer await pour être utilisé naturellement comme identifiant ordinaire et cela permettrait d'ajouter d'autres opérateurs de suffixe, alors que je pense que fut.await!() serait mieux si await été réservé. Cependant, je ne suis pas sûr que cela soit raisonnable, il semble également tout à fait valide que fut.await!() pourrait être plus élevé.

Pour await { future } vs await!(future) , ce dernier garde l'option ouverte pour changer à presque toutes les autres options, alors que le premier n'autorise vraiment que l'une des variantes await future ( comme indiqué dans l'entrée de blog @withoutboats ). Donc je pense que ça devrait certainement être fwd(await { future }) < fwd(await!(future)) .

En ce qui concerne la verbosité, vous avez raison, après avoir divisé les cas en sous-groupes, je n'ai pas réévalué la verbosité, qui devrait être la plus objective de toutes.

Je l'éditerai pour prendre en compte vos commentaires, merci!

Stabiliser await!(future) est à peu près la pire option possible que je puisse imaginer:

  1. Cela signifie que nous devons annuler la réservation de await ce qui signifie en outre que la conception future du langage devient plus difficile.
  2. Il prend sciemment le même chemin qu'avec try!(result) que nous avons déconseillé (et qui nécessite d'écrire r#try!(result) dans Rust 2018).

    • Si nous savons que await!(future) est une mauvaise syntaxe que nous voulons finalement déconseiller, cela crée volontairement une dette technique.

    • De plus, try!(..) est défini dans Rust alors que await!(future) ne peut pas l'être et serait plutôt la magie du compilateur.

    • La dépréciation de try!(..) n'a pas été facile et a eu un impact social. Revoir cette épreuve ne semble pas attrayant.

  3. Cela utiliserait une macro syntaxe pour une partie centrale et importante du langage; cela ne semble clairement pas de première classe.
  4. await!(future) est bruyant ; contrairement à await future vous devez écrire !( ... ) .
  5. Les API Rust, et en particulier la bibliothèque standard, sont centrées sur la syntaxe d'appel de méthode. Par exemple, il est courant de chaîner des méthodes lorsqu'il s'agit de Iterator s. Similaire à await { future } et await future , la syntaxe await!(future) rendra le chaînage de méthode difficile et induira des liaisons temporaires let . C'est mauvais pour l'ergonomie et à mon avis pour la lisibilité.

@Centril J'aimerais être d'accord mais il y a quelques questions ouvertes. Êtes-vous sûr (e) de 1. ? Si nous la rendons «magique», ne pourrions-nous pas la rendre encore plus magique en permettant à cela de faire référence à une macro sans la laisser tomber comme mot-clé?

J'ai rejoint Rust trop tard pour évaluer la perspective sociale de 2. . Répéter une erreur ne semble pas attrayant, mais comme certains codes n'ont toujours pas été convertis à partir de try! il devrait être évident que c'était une solution. Cela soulève la question de savoir si nous avons l'intention d'avoir async/await comme incitation à migrer vers l'édition 2018 ou plutôt d'être patient et de ne pas répéter cela.

Deux autres fonctionnalités centrales (à mon humble avis) comme 3. : vec![] et format! / println! . Le premier parce qu'il n'y a pas d'afaik de construction en boîte stable, le second en raison de la construction de la chaîne de format et de l'absence d'expressions typées de manière dépendante. Je pense que ces comparaisons mettent aussi en partie 4. dans une autre perspective.

Je m'oppose à toute syntaxe qui ne se lit pas un peu comme l'anglais. IE, "wait x" lit quelque chose comme l'anglais. "x # !! @! &" ne le fait pas. "x.await" se lit de manière alléchante comme l'anglais, mais ce ne sera pas le cas lorsque x est une ligne non triviale, comme un appel de fonction membre avec des noms longs, ou un tas de méthodes d'itération chaînées, etc.

Plus précisément, je supporte le "mot-clé x", où le mot-clé est probablement await . Je viens d'utiliser à la fois les coroutines C ++ TS et les coroutines c # unit, qui utilisent toutes deux une syntaxe très similaire à celle-ci. Et après des années à les utiliser dans le code de production, je pense qu'il est absolument essentiel de savoir où se trouvent vos limites d'élasticité . Lorsque vous parcourez la ligne de retrait de votre fonction, vous pouvez sélectionner chaque co_await / yield return dans une fonction de 200 lignes en quelques secondes, sans charge cognitive.

Il n'en est pas de même pour l'opérateur point avec await après, ou pour une autre syntaxe postfixe "pile de symboles".

Je crois que await est une opération fondamentale de flux de contrôle. Il devrait avoir le même niveau de respect que 'if , while , match et return . Imaginez si l'un d'entre eux était des opérateurs postfixes - lire du code Rust serait un cauchemar. Comme avec mon argument pour await, vous pouvez parcourir la ligne de retrait de n'importe quelle fonction Rust et sélectionner immédiatement tout le flux de contrôle. Il y a des exceptions, mais ce sont des exceptions, et ce n'est pas quelque chose que nous devrions rechercher.

Je suis d'accord avec @ejmahler. Nous ne devons pas oublier l'autre aspect du développement - la révision du code. Le fichier avec le code source est beaucoup plus souvent lu puis écrit, donc je pense qu'il devrait être plus facile à lire et à comprendre qu'à écrire. Trouver les points cédants est vraiment important lors de la révision du code. Et personnellement, je voterais pour Useful precedence .
Je crois que ceci:

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

est plus facile à comprendre plutôt que ceci:

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

@HeroicKatora

@Centril J'aimerais être d'accord mais il y a quelques questions ouvertes. Êtes-vous sûr (e) de 1. ? Si nous la rendons «magique», ne pourrions-nous pas la rendre encore plus magique en permettant à cela de faire référence à une macro sans la laisser tomber comme mot-clé?

Techniquement? Peut-être. Cependant, il devrait y avoir une forte justification pour des cas particuliers et dans ce cas, avoir de la magie sur la magie ne semble pas justifié.

Cela soulève la question de savoir si nous avons l'intention d'avoir async/await comme incitation à migrer vers l'édition 2018 ou plutôt d'être patient et de ne pas répéter cela.

Je ne sais pas si nous avons déjà dit que nous voulions un nouveau système de modules, async / await et try { .. } pour être des incitations; mais quelle que soit notre intention, ils le sont, et je pense que c'est une bonne chose. Nous voulons que les gens commencent à utiliser de nouvelles fonctionnalités de langage pour écrire des bibliothèques meilleures et plus idiomatiques.

Deux autres fonctionnalités centrales (à mon humble avis) comme 3. : vec![] et format! / println! . Le premier beaucoup parce qu'il n'y a pas d'afaik de construction en boîte stable,

Le premier existe, et est écrit vec![1, 2, 3, ..] , pour imiter des expressions littérales de tableau, par exemple [1, 2, 3, ..] .

@ejmahler

"x.await" se lit de manière alléchante comme l'anglais, mais ce ne sera pas le cas lorsque x est une ligne non triviale, comme un appel de fonction membre avec des noms longs, ou un tas de méthodes d'itération chaînées, etc.

Quel est le problème avec un tas de méthodes d'itération chaînées? C'est Rust distinctement idiomatique.
L'outil rustfmt formatera également les chaînes de méthodes sur différentes lignes afin que vous obteniez (à nouveau en utilisant match pour afficher la coloration syntaxique):

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

Si vous lisez .await comme "alors attendez", cela se lit parfaitement, du moins pour moi.

Et après des années à les utiliser dans le code de production, je pense qu'il est absolument essentiel de savoir où se trouvent vos limites d'élasticité .

Je ne vois pas comment le suffixe await annule cela, en particulier dans le formatage rustfmt ci-dessus. De plus, vous pouvez écrire:

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

si vous en avez envie.

dans une fonction de 200 lignes en quelques secondes, sans charge cognitive.

Sans trop en savoir sur la fonction particulière, il me semble que 200 LOC viole probablement le principe de responsabilité unique et en fait trop. La solution est de lui faire moins et de le diviser. En fait, je pense que c'est la chose la plus importante pour la maintenabilité et la lisibilité.

Je crois que await est une opération fondamentale de flux de contrôle.

Il en va de même pour ? . En fait, await et ? sont tous deux des opérations de flux de contrôle efficaces qui disent "extraire la valeur hors de son contexte". En d'autres termes, dans le contexte local, vous pouvez imaginer ces opérateurs ayant le type await : impl Future<Output = T> -> T et ? : impl Try<Ok = T> -> T .

Il y a des exceptions, mais ce sont des exceptions, et ce n'est pas quelque chose que nous devrions rechercher.

Et l'exception ici est ? ?

@andreytkachenko

Je suis d'accord avec @ejmahler. Nous ne devons pas oublier l'autre aspect du développement - la révision du code. Le fichier avec le code source est beaucoup plus souvent lu puis écrit, donc je pense qu'il devrait être plus facile à lire et à comprendre qu'à écrire.

Le désaccord porte sur ce qui serait le mieux pour la lisibilité et l'ergonomie.

est plus facile à comprendre plutôt que ceci:

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

Ce n'est pas ainsi qu'il serait formaté; l'exécuter via rustfmt vous permet d'obtenir:

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

@ejmahler @andreytkachenko Je suis d'accord avec @Centril ici, le plus grand changement (certains diront peut-être une amélioration, je ne le ferais pas) que vous gagnez de la syntaxe du préfixe est que les utilisateurs sont incités à diviser leurs déclarations sur plusieurs lignes car tout le reste est illisible. Ce n'est pas Rust-y et les règles de formatage habituelles compensent cela dans la syntaxe post-correction. Je considère également que le point de rendement est plus obscur dans la syntaxe du préfixe car await n'est pas réellement placé au point de code où vous cédez, mais plutôt opposé à celui-ci.

Si vous allez dans cette direction, expérimentons pour le préciser, plutôt avec await en remplacement de let dans l'esprit de l' idée de

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

Mais pour aucun de ceux-ci, je vois suffisamment d'avantages pour expliquer leur perte de composabilité et leurs complications dans la grammaire.

Hm ... est-il souhaitable d'avoir à la fois un préfixe et un suffixe en attente? Ou juste la forme de suffixe?

Le chaînage des appels de méthode est idiomatique, en particulier quand on parle de
itérateurs et, dans une bien moindre mesure, des options, mais à part cela, la rouille est
un langage impératif d'abord et un langage fonctionnel ensuite.

Je ne dis pas qu'un suffixe est littéralement incompréhensible, je fais un
argument de charge cognitive qui cache une opération de flux de contrôle de niveau supérieur,
ayant la même importance que le `` retour '', augmente la charge cognitive lorsque
comparé au fait de le mettre aussi près du début de la ligne - et je suis
en faisant cet argument basé sur des années d'expérience de production.

Le sam 19 janvier 2019 à 11 h 59, Mazdak Farrokhzad [email protected]
a écrit:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril J'aimerais être d'accord mais il y a
quelques questions ouvertes. Etes-vous sûr de 1.? Si nous le faisons, la `` magie '' pourrait
nous ne le rendons pas encore plus magique en permettant à cela de faire référence à une macro sans
le laisser tomber comme mot clé?

Techniquement? Peut-être. Cependant, il devrait y avoir une forte justification pour
cas particuliers et dans ce cas avoir de la magie sur la magie ne semble pas
justifié.

Cela soulève la question de savoir si nous avons l'intention d'avoir async / wait comme
une incitation à migrer vers l'édition 2018 ou plutôt être patient et non
répétez ceci.

Je ne sais pas si nous avons déjà dit que nous voulions un nouveau système de modules, async /
attendez, et essayez {..} d'être des incitations; mais quelle que soit notre intention
ils le sont, et je pense que c'est une bonne chose. Nous voulons que les gens finissent par
commencer à utiliser de nouvelles fonctionnalités de langage pour écrire mieux et plus idiomatique
bibliothèques.

Deux autres fonctionnalités centrales (à mon humble avis) ressemblant beaucoup à 3.: vec! [] Et format! /
println !. Le premier beaucoup car il n'y a pas d'écurie en boîte
construction afaik,

Le premier existe, et s'écrit vec! [1, 2, 3, ..], pour imiter un tableau
expressions littérales, par exemple [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" se lit comme l'anglais, mais ce ne sera pas le cas lorsque x est un
ligne non triviale, comme un appel de fonction membre avec des noms longs, ou un tas
des méthodes d'itération chaînée, etc.

Quel est le problème avec un tas de méthodes d'itération chaînées? C'est distinctement
rouille idiomatique.
L'outil rustfmt formatera également les chaînes de méthodes sur différentes lignes afin que vous
get (à nouveau en utilisant match pour afficher la coloration syntaxique):

laissez toto = alpha (). match? // ou alpha() match? , alpha()#match? , alpha().match!()?
.bêta
.some_other_stuff (). match?
.even_more_stuff (). match
.stuff_and_stuff ();

Si vous lisez .await comme "alors attendez", il se lit parfaitement, du moins pour moi.

Et après des années à les utiliser dans le code de production, je pense que savoiroù se trouvent vos limites de rendement, en un coup d'œil, est absolument critique .

Je ne vois pas comment postfix wait annule cela, en particulier dans le rustfmt
mise en forme ci-dessus. De plus, vous pouvez écrire:

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

si vous en avez envie.

dans une fonction de 200 lignes en quelques secondes, sans charge cognitive.

Sans trop en savoir sur la fonction particulière, il me semble
que 200 LOC viole probablement le principe de responsabilité unique et
trop. La solution est de lui faire moins et de le diviser. En fait, je
pense que c'est la chose la plus importante pour la maintenabilité et la lisibilité.

Je crois qu'attendre est une opération fondamentale de contrôle de flux.

Alors?. En fait, attendez et? sont tous deux des opérations de flux de contrôle efficaces
qui disent «extraire la valeur hors de son contexte». En d'autres termes, dans le local
context, vous pouvez imaginer ces opérateurs ayant le type await: impl
Avenir-> T et? : impl Essayez-> T.

Il y a des exceptions, mais ce sont des exceptions, et pas quelque chose que nous devrions
lutter pour.

Et l'exception ici est? ?

@andreytkachenko https://github.com/andreytkachenko

Je suis d'accord avec @ejmahler https://github.com/ejmahler . Nous ne devrions pas
oubliez l'autre côté du développement - révision du code. Le fichier avec le code source est
beaucoup plus souvent lu qu'écrit, donc je pense qu'il devrait être plus facile de
lire et comprendre puis écrire.

Le désaccord porte sur ce qui serait le mieux pour la lisibilité et
ergonomie.

est plus facile à comprendre plutôt que ceci:

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

Ce n'est pas ainsi qu'il serait formaté; le formatage idiomatique rustfmt
est:

laissez le corps: MyResponse = client
.get ("https: // mon_api")
.envoyer()
.rencontre?
.into_json ()
.rencontre?;

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@ejmahler Nous ne sommes pas d'accord re. "cache"; les mêmes arguments ont été .

L'opérateur ? est très court, et a été critiqué dans le passé pour "cacher" un retour. La plupart du temps, le code est lu et non écrit. Le renommer en ?! le rendra deux fois plus long et donc plus difficile à ignorer.

Néanmoins, nous avons finalement stabilisé ? et depuis, je pense que la prophétie ne s'est pas concrétisée.
Pour moi, le suffixe await suit l'ordre de lecture naturel (au moins pour les locuteurs de langue de gauche à droite). En particulier, il suit l'ordre des flux de données.

Sans parler de la coloration syntaxique: tout ce qui est lié à l'attente peut être mis en évidence avec une couleur vive, afin de pouvoir être trouvé en un coup d'œil. Donc, même si nous avions un symbole au lieu du mot await réel, il serait toujours très lisible et trouvable dans le code syntaxique mis en évidence. Cela dit, je préfère toujours l'utilisation du mot await juste pour des raisons de grepping - il est plus facile de grep le code pour tout ce qui est attendu si nous n'utilisons que le mot await au lieu d'un symbole comme @ ou #, dont la signification dépend de la grammaire.

vous tous, ce n'est pas sorcier

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

postfix ... est extrêmement lisible, difficile à rater en un coup d'œil et super intuitif puisque vous le lisez naturellement comme le code qui se termine en attendant que le résultat du futur soit disponible. aucune manigance de précédence / macro n'est nécessaire et aucun bruit de ligne supplémentaire de sigils inconnus, puisque tout le monde a déjà vu des ellipses.

(excuses à @solson)

@ ben0x539 Cela signifie-t-il que je peux accéder à un membre de mon résultat comme future()....start ? Ou attendre un résultat de plage comme si range()..... ? Et comment vous voulez dire exactement no precedence/macro shenanigans necessary and puisque les points .. suspension actuels

Oui le ? L'opérateur existe. J'ai déjà reconnu qu'il y avait
exceptions. Mais c'est une exception. La grande majorité du flux de contrôle dans tout
Le programme Rust se produit via des mots-clés préfixes.

Le sam 19 janvier 2019 à 13 h 51 Benjamin Herr [email protected]
a écrit:

vous tous, ce n'est pas sorcier

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

postfix ... est extrêmement lisible, difficile à manquer en un coup d'œil et super
intuitif puisque vous le lisez naturellement comme le code qui suit
en attendant que le résultat du futur devienne disponible. non
précédence / macro manigances nécessaires et aucun bruit de ligne supplémentaire de
des signes inconnus, puisque tout le monde a déjà vu des ellipses.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora Cela a l'air un peu artificiel mais oui, bien sûr. Je voulais dire que puisque c'est une opération postfix, comme les autres solutions postfix suggérées, cela évite le besoin d'une priorité contre-intuitive pour await x? , et ce n'est pas une macro.

@ejmahler

Oui le ? L'opérateur existe. J'ai déjà reconnu qu'il y avait des exceptions. Mais c'est une exception.

Il existe deux formes d'expression qui correspondent à keyword expr , à savoir return expr et break expr . Le premier est plus courant que le second. La forme continue 'label ne compte pas vraiment puisque, bien qu'elle soit une expression, elle n'est pas de la forme keyword expr . Vous avez donc maintenant 2 formes d'expression unaire de mot-clé avec préfixe entier et 1 forme d'expression unaire avec suffixe. Avant même de prendre en compte que ? et await sont plus similaires que await et return , j'appellerais à peine return/break expr une règle pour que ? soit une exception contre.

La grande majorité du flux de contrôle dans tout programme Rust se produit via des mots-clés de préfixe.

Comme mentionné ci-dessus, break expr n'est pas si courant ( break; est plus typique et return; sont plus typiques et ce ne sont pas des formes d'expression unaires). Ce qui reste est tôt return expr; s et il ne me semble pas du tout clair que cela soit beaucoup plus courant que match , ? , juste imbriqués if let boucles else s et for . Une fois que try { .. } stabilise, je m'attendrais à ce que ? soit utilisé encore plus.

@ ben0x539 Je pense que nous devrions réserver ... pour les génériques variadiques, une fois que nous sommes prêts à les avoir

J'aime beaucoup l'idée d'innover avec la syntaxe postfix ici. Cela a beaucoup plus de sens avec le flux et je me souviens à quel point le code s'est amélioré lorsque nous sommes passés du préfixe try! au suffixe ? . Je pense qu'il y a beaucoup de gens qui ont fait l'expérience dans la communauté Rust des améliorations apportées au code.

Si nous n'aimons pas l'idée de .await je suis sûr qu'une certaine créativité peut être faite pour trouver un opérateur de suffixe réel. Un exemple pourrait simplement être d'utiliser ++ ou @ pour await.

:( Je ne veux plus attendre.

Tout le monde est à l'aise avec la syntaxe de macro, la plupart des personnes dans ce fil qui commencent par d'autres opinions semblent finir par privilégier la syntaxe de macro.

Bien sûr, ce sera une "macro magique", mais les utilisateurs se soucient rarement de ce à quoi ressemble l'expansion de la macro et pour ceux qui le font, il est assez facile d'expliquer la nuance dans la documentation.

La syntaxe de macro régulière est un peu comme la tarte aux pommes, c'est la deuxième option préférée de tout le monde, mais par conséquent l'option préférée de la famille [0]. Surtout, comme avec try! nous pouvons toujours le changer plus tard. Mais surtout, plus tôt nous pourrons tous être d'accord, plus vite nous pourrons tous commencer à l'utiliser et être productifs!

[0] (Référencé dès la première minute) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

match, if, if let, while, while let et for sont tous des flux de contrôle omniprésents
qui utilisent des préfixes. Faire semblant de faire une pause et de continuer est le seul flux de contrôle
Les mots-clés induisent en erreur.

Le sam 19 janvier 2019 à 15:37 Yazad Daruvala [email protected]
a écrit:

:( Je ne veux plus attendre.

Tout le monde est à l'aise avec la syntaxe des macros, la plupart des utilisateurs de ce fil
commencer par d'autres opinions semblent finir par privilégier la macro syntaxe.

Bien sûr, ce sera une "macro magique", mais les utilisateurs se soucient rarement de la nature de la macro
l'expansion ressemble et pour ceux qui le font, il est assez facile d'expliquer
nuance dans les documents.

La syntaxe de macro régulière est un peu comme la tarte aux pommes, c'est la deuxième
option préférée mais par conséquent l'option préférée de la famille [0].
Surtout, comme avec try! nous pouvons toujours le changer plus tard. Mais la plupart
surtout, plus tôt nous pourrons tous être d'accord, plus tôt nous pourrons tous commencer
en l'utilisant réellement et soyez productif!

[0] (référencé dans la première minute)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mitsuhiko Je suis d'accord! Postfix semble plus rustique en raison du chaînage. Je pense que la syntaxe fut@await j'ai proposée est une autre option intéressante qui ne semble pas avoir autant d'inconvénients que les autres propositions. Je ne sais pas si c'est trop loin et une version plus terre-à-terre serait préférable.

@ejmahler

match, if, if let, while, while let et for sont tous des flux de contrôle omniprésents qui utilisent des préfixes. Prétendre pause et continuer sont les seuls mots-clés du flux de contrôle est une erreur frustrante.

Ce n'est pas du tout trompeur. La grammaire pertinente pour ces constructions est à peu près:

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

Ici, les formulaires sont if/while expr block , for pat in expr block et match expr { pat0 => expr0, .., patn => exprn } . Il y a un mot-clé qui précède ce qui suit sous toutes ces formes. Je suppose que c'est ce que vous entendez par "utilise des préfixes". Cependant, ce sont toutes des formes de bloc et non des opérateurs de préfixe unaire. La comparaison avec await expr est donc trompeuse car il n'y a ni cohérence ni règle à proprement parler. Si vous recherchez la cohérence avec les formulaires en bloc, comparez cela avec await block , et non await expr .

@mitsuhiko Je suis d'accord! Postfix semble plus rustique en raison du chaînage.

La rouille est dualiste. Il prend en charge les approches impératives et fonctionnelles. Et je pense que c'est bien, car dans différents cas, chacun d'eux peut être plus adapté.

Je ne sais pas. On dirait que ce serait génial d'avoir les deux:

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

Ayant utilisé Scala pendant un certain temps, j'aimais aussi beaucoup de choses fonctionnant comme ça. Surtout match et if seraient bien d'avoir des positions postfixes dans Rust.

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

Résumé à ce jour

Matrices d'options:

Matrice récapitulative des options (en utilisant @ comme sigil, mais peut être n'importe quoi):

| Nom | Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
| --- | --- | --- | --- |
| PREFIX | - | - | - |
| Macro de mot-clé | await!(fut) | await!(fut)? | await!(fut?) |
| Fonction de mot-clé | await(fut) | await(fut)? | await(fut?) |
| Préséance utile | await fut | await fut? | await (fut?) |
| Préséance évidente | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn avec mot-clé | fut(await) | fut(await)? | fut?(await) |
| Champ de mot-clé | fut.await | fut.await? | fut?.await |
| Méthode de mot-clé | fut.await() | fut.await()? | fut?.await() |
| Macro de mot-clé Postfix | fut.await!() | fut.await!()? | fut?.await!() |
| Mot-clé d'espace | fut await | fut await? | fut? await |
| Mot-clé Sigil | fut@await | fut@await? | fut?@await |
| Sigil | fut@ | fut@? | fut?@ |

Le sigil de "Sigil Keyword" _peut_ être # , car alors vous ne pourriez pas le faire avec un futur appelé r . ... car le sigil n'aurait pas à changer de tokenisation comme mon premier souci .

Plus d'utilisation réelle (PM moi d'autres cas d'utilisation _real_ avec plusieurs await sur urlo et je les ajouterai):

| Nom | (reqwest) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json | > ? |
| --- | --- |
| PREFIX | - |
| Macro de mot-clé | await!(client.get("url").send())?.json()? |
| Fonction de mot-clé | await(client.get("url").send())?.json()? |
| Préséance utile | (await client.get("url").send()?).json()? |
| Préséance évidente | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn avec mot-clé | client.get("url").send()(await)?.json()? |
| Champ de mot-clé | client.get("url").send().await?.json()? |
| Méthode de mot-clé | client.get("url").send().await()?.json()? |
| Macro de mot-clé Postfix | client.get("url").send().await!()?.json()? |
| Mot-clé d'espace | client.get("url").send() await?.json()? |
| Mot-clé Sigil | client.get("url").send()@await?.json()? |
| Sigil | client.get("url").send()@?.json()? |

NOTE DE MODIFICATION: On m'a fait remarquer qu'il peut être judicieux pour Response::json de renvoyer également un Future , où send attend l'IO sortant et json (ou autre interprétation du résultat) attend l'IO entrant. Je vais cependant laisser cet exemple tel quel, car je pense qu'il est significatif de montrer que le problème de chaînage s'applique même avec un seul point d'attente d'E / S dans l'expression.

Il semble y avoir un consensus approximatif sur le fait que parmi les options de préfixe, la priorité évidente (avec le sucre await? ) est la plus souhaitable. Cependant, de nombreuses personnes se sont prononcées en faveur d'une solution postfix, afin de faciliter le chaînage comme ci-dessus. Bien que le choix du préfixe fasse l'objet d'un consensus approximatif, il ne semble pas y avoir de consensus sur la meilleure solution postfix. Toutes les options proposées conduisent à une confusion facile (atténuée par la mise en évidence des mots clés):

  • Fn With Keyword => appel d'un fn avec un argument appelé await
  • Champ de mot-clé => accès au champ
  • Keyword Method => appel de méthode
  • Macro (préfixe ou suffixe) => await un mot-clé ou non?
  • Mot-clé d'espace => rompt le regroupement en une seule ligne (mieux sur plusieurs lignes?)
  • Sigil => ajoute de nouveaux sigils à une langue déjà perçue comme lourde de sigils

Autres suggestions plus drastiques:

  • Autoriser à la fois le préfixe (priorité évidente) et le suffixe "champ" (cela pourrait être appliqué à plus de mots clés comme match , if , etc. à l'avenir pour en faire un modèle généralisé, mais ce n'est pas nécessaire addendum à ce débat) [[référence] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await en modèles pour résoudre les futurs (pas de chaînage du tout) [[reference] (https://github.com/rust-lang/rust/issues/57640)]
  • Utilisez un opérateur de préfixe mais autorisez le retarder [[reference] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

La stabilisation avec une macro de mot-clé await!(fut) est bien sûr compatible pour l'avenir avec pratiquement tout ce qui précède, bien que cela nécessite de faire en sorte que la macro utilise un mot-clé au lieu d'un identifiant régulier.

Si quelqu'un a un exemple d'exemple la plupart du temps réel qui utilise deux await dans une chaîne, j'aimerais le voir; personne n'en a partagé un jusqu'à présent. Cependant, le suffixe await est également utile même si vous n'avez pas besoin de await plus d'une fois dans une chaîne, comme le montre l'exemple reqwest.

Si j'ai manqué quelque chose de notable au-dessus de ce commentaire récapitulatif, envoyez-moi un message sur urlo et j'essaierai de l'ajouter. (Bien que j'exige qu'il ajoute les commentaires de quelqu'un d'autre pour éviter le favoritisme de la voix forte.)

Personnellement, j'ai toujours été en faveur du préfixe mot-clé avec la priorité évidente. Je pense toujours que la stabilisation avec une macro de mot-clé await!(fut) serait utile pour collecter des informations du monde réel sur l'endroit où l'attente se produit dans les cas d'utilisation du monde réel, et nous permettrait toujours d'ajouter un préfixe non macro ou une option de postfix plus tard .

Cependant, en rédigeant le résumé ci-dessus, j'ai commencé à aimer "Keyword Field". "Espace Mot clé" est agréable lorsqu'il est divisé sur plusieurs lignes:

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

mais sur une ligne, cela fait une pause maladroite qui regroupe mal l'expression: client.get("url").send() await?.json()? . Cependant, avec le champ de mot-clé, il semble bon dans les deux formes: client.get("url").send().await?.json()?

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

bien que je suppose qu'une "méthode de mot-clé" coulerait mieux, car c'est une action. Nous _pouvions_ même en faire une "vraie" méthode sur Future si nous voulions:

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

( extern "rust-await" impliquerait bien sûr toute la magie nécessaire pour réellement faire l'attente et ce ne serait pas réellement un vrai fn, il serait principalement là parce que la syntaxe ressemble à une méthode, si un mot-clé méthode est utilisée.)

Autoriser à la fois le préfixe (priorité évidente) et le suffixe "champ" ...

Si une syntaxe postfixe est sélectionnée (peu importe si elle est ensemble ou à la place du préfixe), ce serait certainement un argument pour une discussion future: nous avons maintenant des mots-clés qui fonctionnent à la fois en notation préfixe et postfixe, précisément parce que parfois on est préférable à l'autre, alors peut-être pourrions-nous simplement autoriser les deux là où cela a du sens, et augmenter la flexibilité de la syntaxe, tout en unifiant les règles. C'est peut-être une mauvaise idée, peut-être qu'elle sera rejetée, mais c'est certainement une discussion à avoir dans le futur, si la notation postix est utilisée pour await .

Je pense qu'il y a très peu de chances qu'une syntaxe qui n'inclut pas la chaîne de caractères await soit acceptée pour cette syntaxe.

: +1:


Une pensée aléatoire que j'ai eue après avoir vu un tas d'exemples ici ( comme ceux de @mehcode ): L'une des plaintes dont je me souviens à propos de .await est que c'est trop difficile à voir †, mais étant donné que les choses attendues sont typiquement faillible, le fait qu'il soit souvent .await? permet d'attirer l'attention de toute façon.

† Si vous utilisez quelque chose qui ne met pas en évidence les mots clés


@ejmahler

Je m'oppose à toute syntaxe qui ne se lit pas un peu comme l'anglais

Quelque chose comme request.get().await lit aussi bien que quelque chose comme body.lines().collect() . Dans "un tas de méthodes d'itération enchaînées", je pense que _prefix_ se lit en fait pire, car vous devez vous rappeler qu'ils ont dit "attendez" au début, et ne savez jamais quand vous entendez quelque chose si ça va être ce que vous attendre, un peu comme une phrase de chemin de jardin .

Et après des années à les utiliser dans le code de production, je pense qu'il est absolument essentiel de savoir où se trouvent vos limites d'élasticité. Lorsque vous parcourez la ligne d'indentation de votre fonction, vous pouvez sélectionner chaque retour co_await / yield dans une fonction de 200 lignes en quelques secondes, sans charge cognitive.

Cela implique qu'il n'y en a jamais dans une expression, ce qui est une restriction que je ne prendrais absolument pas en charge, étant donné la nature orientée expression de Rust. Et au moins avec await C #, il est absolument plausible d'avoir CallSomething(argument, await whatever.Foo() .

Étant donné que async _will_ apparaîtra au milieu des expressions, je ne comprends pas pourquoi il est plus facile de voir dans le préfixe que dans le suffixe.

Il devrait recevoir le même niveau de respect que «if, while, match et return. Imaginez si l'un d'entre eux était des opérateurs postfixes - lire du code Rust serait un cauchemar.

return (et continue et break ) et while sont notables comme étant _complètement_ inutiles à enchaîner, car ils renvoient toujours ! et () . Et bien que pour une raison quelconque vous ayez omis for , nous avons vu du code écrit très bien avec .for_each() sans effets négatifs, en particulier en rayon .

Nous devons probablement faire la paix avec le fait que async/await va être une fonctionnalité majeure du langage. Il apparaîtra dans toutes sortes de codes. Il envahira l'écosystème - à certains endroits, il sera aussi courant que ? . Les gens devront l'apprendre.

Par conséquent, nous pourrions vouloir nous concentrer consciemment sur ce que ressentira le choix de syntaxe après l'avoir utilisé pendant une longue période plutôt que sur ce qu'il ressentira au début.

Nous devons également comprendre qu'en ce qui concerne les constructions de flux de contrôle, await est un autre type d'animal. Des constructions comme return , break , continue , et même yield peuvent être comprises intuitivement en termes de jmp . Quand nous les voyons, nos yeux rebondissent sur l'écran parce que le flux de contrôle qui nous tient à cœur se déplace ailleurs. Cependant, alors que await affecte le flux de contrôle de la machine, il ne déplace pas le flux de contrôle qui est important à nos yeux et à notre compréhension intuitive du code.

Nous ne sommes pas tentés d'enchaîner les appels inconditionnels à return ou break car cela n'aurait aucun sens. Pour des raisons similaires, nous ne sommes pas tentés de modifier nos règles de préséance pour les adapter. Ces opérateurs ont une faible priorité. Ils prennent tout à droite et le renvoient quelque part, mettant fin à l'exécution dans cette fonction ou ce bloc. L'opérateur await , cependant, veut être chaîné. C'est une partie intégrante d'une expression, pas la fin.

Après avoir examiné la discussion et les exemples de ce fil, il me reste le sentiment rongeant que nous vivrions pour regretter des règles de préséance surprenantes.

Le candidat cheval de traque semble aller avec await!(expr) pour le moment et espérer que quelque chose de mieux sera résolu plus tard. Avant de lire les remarques de @Centril , j'aurais probablement soutenu cela dans l'intérêt de sortir cette fonctionnalité importante avec presque n'importe quelle syntaxe. Cependant, ses arguments me convainquent que ce ne serait que de la répression. Nous savons que le chaînage des appels de méthode est important dans Rust. Cela a conduit à l'adoption du mot-clé ? , qui est très populaire et très réussi. Utiliser une syntaxe dont nous savons qu'elle nous décevra, c'est en effet simplement ajouter de la dette technique.

Au début de ce fil, @withoutboats a indiqué que seules quatre options existantes semblent viables. Parmi ceux-ci, seule la syntaxe postfix expr await est susceptible de nous rendre heureux à long terme. Cette syntaxe ne crée pas d'étranges surprises de précédence. Cela ne nous oblige pas à créer une version préfixe de l'opérateur ? . Cela fonctionne bien avec le chaînage de méthodes et ne rompt pas le flux de contrôle de gauche à droite. Notre opérateur ? réussi sert de précédent pour un opérateur suffixe, et await ressemble plus à ? en pratique qu'à return , break ou yield . Alors qu'un opérateur non-symbole postfix peut être nouveau dans Rust, l'utilisation de async/await sera suffisamment répandue pour le rendre rapidement familier.

Bien que toutes les options pour une syntaxe postfixe semblent réalisables, expr await présente certains avantages. Cette syntaxe indique clairement que await est un mot-clé, ce qui permet de souligner le flux de contrôle magique. Par rapport à expr.await , expr.await() expr.await! , expr.await!() , etc., cela évite d'avoir à expliquer que cela ressemble à un champ / méthode / macro, mais vraiment n'est pas dans ce cas particulier. Nous nous habituerions tous au séparateur d'espace ici.

Épeler await comme @ ou utiliser un autre symbole qui ne pose pas de problèmes d'analyse est attrayant. C'est certainement un opérateur suffisamment important pour le justifier. Mais si, à la fin, il faut l'épeler await , ce sera très bien. Tant qu'il est en position postfix.

Comme quelqu'un l'a mentionné de vrais exemples ... Je maintiens une base de code de rouille de 23 858 lignes (selon tokei) qui est très fortement asynchrone et utilise des contrats à terme 0,1 await (hautement expérimentaux, je sais). Allons-y (expurgé) spelunking (notez que tout a été exécuté via 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)
}

Maintenant, transformons cela en la variante de préfixe la plus populaire, priorité évidente avec le sucre. Pour des raisons évidentes, cela n'a pas été exécuté via rustfmt, alors nous vous prions de nous excuser s'il existe une meilleure façon de l'écrire.

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

Enfin, transformons ceci en ma variante préférée de postfix, "champ 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)
}

Après cet exercice, je trouve un certain nombre de choses.

  • Je suis maintenant fermement contre await? foo . Il se lit bien pour les expressions simples mais le ? se sent perdu sur les expressions complexes. Si nous devons faire un préfixe, je préfère avoir une priorité "utile".

  • L'utilisation de la notation postfix me conduit à joindre des instructions et à réduire les liaisons let inutiles.

  • L'utilisation de la notation postfix _field_ me conduit à préférer fortement que .await? apparaisse dans la ligne de l'objet qu'il attend plutôt que sur sa propre ligne dans le jargon rustfmt.

J'apprécie la notation postfixe ellipses "..." ci-dessus, à la fois pour sa concision et symboliquement en langue anglaise représentant une pause en prévision de quelque chose d'autre. (Comme comment fonctionne le comportement asynchrone!), Il s'enchaîne également bien.

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

Je doute que d'autres options soient aussi concises et visuellement significatives.

UNE

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

Doit-il même être considéré comme une bonne forme d'avoir de longues chaînes d'attente?

Pourquoi ne pas simplement utiliser des combinateurs Future normaux?

En fait, certaines expressions ne se traduisent pas bien en chaînes d'attente si vous souhaitez avoir des comportements de sauvegarde en cas d'échec.

Personnellement, je pense ceci:

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

value.unwrap()

se lit beaucoup plus naturellement que ceci:

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

Après tout, nous avons toujours la pleine puissance des contrats à terme à coût zéro entre nos mains.

Considérez cela.

@EyeOfPython Je voudrais @ in future@await . Nous pouvons écrire future~await , où le ~ est travaillé comme un demi-trait d'union, et fonctionnerait pour tous les opérateurs de suffixe possibles.

Le trait d'union - était déjà utilisé comme opérateur moins et opérateur négatif. Ce n'est plus bon. Mais ~ était utilisé pour indiquer des objets de tas dans Rust, et sinon, il n'était guère utilisé dans aucun langage de programmation. Cela donnerait moins de confusion aux personnes d'autres langues.

@earthengine Bonne idée, mais peut-être simplement utiliser future~ ce qui signifie attendre un futur, où le ~ est travaillé comme le mot-clé await (comme le symbole ?

Avenir | Avenir du résultat | Résultat du futur
- | - | -
futur ~ | futur ~? | futur? ~

Également des futurs enchaînés comme:

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

J'ai regardé comment Go implémente la programmation asynchrone et j'ai trouvé quelque chose d'intéressant. L'alternative la plus proche aux futurs en Go sont les canaux. Et au lieu de await ou d'une autre syntaxe criarde pour attendre les valeurs, les canaux Go fournissent juste l'opérateur <- à cette fin. Pour moi, cela a l'air assez propre et simple. Auparavant, j'ai souvent vu comment les gens louaient Go pour sa syntaxe simple et ses bonnes installations asynchrones, c'est donc une bonne idée d'apprendre quelque chose de son expérience.

Malheureusement, nous ne pouvions pas avoir exactement la même syntaxe car il y a beaucoup plus d'accolades dans le code source de Rust que dans Go, principalement à cause des génériques. Cela rend l'opérateur <- vraiment subtil et pas agréable à travailler. Un autre inconvénient est qu'il pourrait être considéré comme opposé à -> dans la signature de la fonction et il n'y a aucune raison de le considérer comme ça. Et encore un autre inconvénient est que <- sigil était destiné à être implémenté en tant que placement new , de sorte que les gens pourraient mal l'interpréter.

Donc, après quelques expériences avec la syntaxe, je me suis arrêté à <-- sigil:

let output = <-- future;

Dans le contexte async <-- est assez simple, bien que moins de <- . Mais à la place, il offre un gros avantage sur <- ainsi que sur le préfixe await - il joue bien avec l'indentation.

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

Pour moi, cet opérateur semble encore plus unique et plus facile à repérer que await (qui ressemble plus à n'importe quel autre mot-clé ou variable dans le contexte du code). Malheureusement, sur Github, cela semble plus mince que dans mon éditeur de code, mais je pense que ce n'est pas fatal et nous pouvons vivre avec cela. Même si quelqu'un se sent mal à l'aise, une coloration syntaxique différente ou une meilleure police (en particulier avec les ligatures) résoudra tous les problèmes.

Une autre raison d'aimer cette syntaxe est qu'elle pourrait conceptuellement être exprimée comme "quelque chose qui n'est pas encore là". La direction de droite à gauche de la flèche est opposée à la direction dans laquelle nous lisons le texte, ce qui nous permet de le décrire comme "chose qui vient du futur". La forme longue de l'opérateur <-- suggère également que "une opération durable commence". Un crochet angulaire et deux traits d'union dirigés à partir de future pourraient symboliser "l'interrogation continue". Et nous sommes toujours capables de le lire comme "attendre" comme avant.
Pas une sorte d'illumination, mais cela pourrait être amusant.


La chose la plus importante dans cette proposition est que l'enchaînement de méthodes ergonomiques serait également possible. L'idée d' opérateur de préfixe retardé que j'ai proposé précédemment convient bien ici. De cette façon, nous aurions le meilleur des deux mondes de la syntaxe du préfixe et du suffixe await . J'espère vraiment qu'il y aurait aussi des extras utiles que je voulais personnellement à de nombreuses reprises auparavant: le déréférencement retardé et la syntaxe de négation retardée .

À travers, je ne suis pas sûr que le mot retardé soit approprié ici, peut-être devrions-nous le nommer différemment.

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

La priorité des opérateurs semble assez évidente: de gauche à droite.

J'espère que cette syntaxe réduira le besoin d'écrire des fonctions is_not_* dont le but est uniquement de nier et de renvoyer une propriété bool . Et les expressions booléennes / de déréférencement dans certains cas seront plus propres lors de leur utilisation.


Enfin, je l'ai appliqué sur des exemples du monde réel publiés par @mehcode et j'aime la façon dont <-- met correctement l'accent sur la fonction async l'intérieur des chaînes d'appels de méthodes. Au contraire, le suffixe await ressemble simplement à un accès au champ normal ou à un appel de fonction (selon la syntaxe) et il est presque impossible de les distinguer sans coloration syntaxique ou mise en forme spéciale.

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

Après tout: c'est la syntaxe que je souhaite utiliser.

@novacrazy

Doit-il même être considéré comme une bonne forme d'avoir de longues chaînes d'attente? Pourquoi ne pas simplement utiliser des combinateurs Future réguliers?

Je ne sais pas si je ne vous ai pas mal compris mais ceux-ci ne sont pas exclusifs, vous pouvez toujours utiliser des combinateurs

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

Mais pour que cela fonctionne, la clause and_then doit être tapée à FnOnce(T) -> impl Future<_> , pas seulement à FnOnce(T) -> U . Faire des combinateurs enchaînés sur le futur et le résultat ne fonctionne que proprement sans paranthèse dans postfix:

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

Dans cet article, je vais me concentrer sur la question de la priorité des

  • Appel de méthode ( future.await() )
  • Expression de champ ( future.await )
  • Appel de fonction ( future(await) )

Les différences de fonctionnalité sont plutôt faibles mais existantes. Notez que j'accepterais tout cela, il s'agit principalement d'un réglage fin. Pour tous les montrer, nous avons besoin de certains types. Veuillez ne pas faire de commentaires sur le caractère artificiel de l'exemple, c'est la version la plus compressée qui montre toutes les différences à la fois.

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)
  • Appel de méthode: foo.member.await()(42)
    Se lie le plus fortement, donc pas de paranthèse du tout
  • Membre: (foo.member.await)(42)
    Nécessite une paranthèse autour du résultat attendu quand il s'agit d'un appelable, cela est cohérent avec le fait d'avoir un appelable en tant que membre, sinon confusion avec l'appel à la fonction membre. Cela suggère également que l'on pourrait déstructurer avec des motifs: let … { await: value } = foo.member; value(42) quelque sorte?
  • Appel de fonction: (foo.member)(await)(42)
    Nécessite une paranthèse pour la déstructuration (nous déplaçons le membre) car il se comporte comme un appel de fonction.

Tous se ressemblent quand nous ne détruisons pas une structure d'entrée par le déplacement d'un membre, ni n'appelons le résultat comme un appelable puisque ces trois classes de précédence se succèdent directement. Comment voulons-nous que les futurs se comportent?

Le meilleur parallèle à l'appel de méthode devrait être juste un autre appel de méthode prenant self .

Le meilleur parallèle au membre est une structure qui n'a que le membre implicite await et est donc déstructurée en partant de cela, et ce mouvement attend implicitement le futur. Celui-ci semble le moins évident.

Le parallèle à la fonction est l'appel est le comportement des fermetures. Je préférerais cette solution comme l'ajout le plus propre au corpus de langage (car dans la syntaxe correspond le mieux les possibilités du type), mais il y a des points positifs pour l'appel de méthode et .await n'est jamais plus long que les autres.

@HeroicKatora Nous pourrions .await dans libsyntax pour autoriser foo.await(42) mais ce serait incohérent / ad hoc. Cependant, (foo.await)(42) semble utilisable car s'il existe des fermetures à terme, elles ne sont probablement pas si courantes. Ainsi, si nous optimisons pour le cas courant, ne pas avoir à ajouter () à .await emporte probablement sur l'équilibre.

@Centril Je suis d'accord, la cohérence est importante. Quand je vois quelques comportements similaires, j'aimerais en déduire d'autres par synthèse. Mais ici .await semble gênant, surtout avec l'exemple ci-dessus montrant clairement qu'il est parallèle à la déstructuration implicite (à moins que vous ne puissiez trouver une syntaxe différente où ces effets se produisent?). Quand je vois la déstructuration, je me demande instantanément si je peux utiliser cela avec des liaisons let, etc. Ceci, cependant, ne serait pas possible car nous déstructurerions le type d'origine qui n'a pas de tel membre ou surtout quand le type est juste impl Future<Output=F> (Sidenote généralement non pertinente: faire ce travail nous ramènerait à un préfixe alternatif await _ = à la place de let _ = , curieusement¹).

Cela ne nous interdit pas d'utiliser la syntaxe en tant que telle, je pense que je pourrais m'occuper de l'apprendre et si elle s'avère être la dernière, je l'utiliserai avec vigueur, mais cela semble être une faiblesse évidente.


¹ Cela pourrait être cohérent tout en autorisant ? en permettant ? derrière les noms dans un motif

  • await value? = failing_future();

pour correspondre à la partie Ok d'un Result . Cela semble intéressant à explorer également dans d'autres contextes, mais plutôt hors sujet. Cela conduirait également à faire correspondre la syntaxe du préfixe et du suffixe pour await en même temps.

Cela ne nous interdit pas d'utiliser la syntaxe en tant que telle, je pense que je pourrais m'occuper de l'apprendre et si elle s'avère être la dernière, je l'utiliserai avec vigueur, mais cela semble être une faiblesse évidente.

Je pense que chaque solution aura un inconvénient dans une certaine dimension ou dans un cas précis. cohérence, ergonomie, chaînabilité, lisibilité, ... Cela en fait une question de degré, d'importance des cas, d'adéquation au code et aux API typiques de Rust, etc.

Dans le cas d'un utilisateur écrivant foo.await(42) ...

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

    foo.closure(42);
}

... nous fournissons déjà de bons diagnostics:

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

Ajuster cela pour l'adapter à foo.await(42) semble tout à fait réalisable. En fait, pour autant que je sache, nous savons que l'utilisateur a l'intention de (foo.await)(42) quand foo.await(42) est écrit, donc cela peut être cargo fix ed dans un MachineApplicable manière. En effet, si nous stabilisons foo.await mais n'autorisons pas foo.await(42) je crois que nous pouvons même changer la priorité plus tard si nous en avons besoin puisque foo.await(42) ne sera pas légal au début.

D'autres emboîtements fonctionneraient (par exemple, le futur du résultat de la fermeture - non pas que ce soit courant):
`` rouille
struct HasClosure fn _foo () -> Résultat <(), ()> {
laissez toto: HasClosure <_ i = "27"> = HasClosure {fermeture: Ok (| x | {})};

foo.closure?(42);

Ok(())

}

Par exemple, l'avenir du résultat de la fermeture - non pas que ce sera commun

L'opérateur de suffixe supplémentaire ? rend cela sans ambiguïté sans modifications de syntaxe - dans aucun des exemples post-correction. Aucun ajustement nécessaire . Les problèmes sont seulement que .member explicitement un champ et que la déstructuration des mouvements doit s'appliquer en premier. Et je ne veux vraiment pas dire que ce serait difficile à écrire. Je veux surtout dire que cela semble incompatible avec d'autres utilisations de .member qui, par exemple, peuvent être transformées en correspondance. Le message original pesait les points positifs et négatifs à cet égard.

Edit: Ajuster pour ajuster future.await(42) a le risque supplémentaire, probablement involontaire, de rendre ceci a) incompatible avec les fermetures où ce n'est pas le cas en raison des méthodes du même nom que le membre étant autorisées; b) inhiber les développements futurs où nous aimerions donner des arguments à await . Mais, comme vous l'avez mentionné précédemment, ajuster pour Future renvoyer une fermeture ne devrait pas être le problème le plus pressant.

@novacrazy Pourquoi ne pas utiliser simplement des combinateurs Future réguliers?

Je ne suis pas sûr de votre expérience avec Futures 0.3, mais on s'attend généralement à ce que les combinateurs ne soient pas beaucoup utilisés et que l'utilisation principale / idiomatique sera async / await.

Async / await présente plusieurs avantages par rapport aux combinateurs, par exemple, il prend en charge l'emprunt à travers les points de rendement.

Les combinateurs existaient bien avant async / await, mais async / await a quand même été inventé, et pour une bonne raison!

Async / await est là pour rester, ce qui signifie qu'il doit être ergonomique (y compris avec les chaînes de méthodes).

Bien sûr, les gens sont libres d'utiliser les combinateurs s'ils le souhaitent, mais ils ne devraient pas être nécessaires pour obtenir une bonne ergonomie.

Comme @cramertj l'a dit, essayons de garder la discussion concentrée sur async / await, et non sur les alternatives à async / await.

En fait, certaines expressions ne se traduisent pas bien en chaînes d'attente si vous souhaitez avoir des comportements de sauvegarde en cas d'échec.

Votre exemple peut être considérablement simplifié:

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

Cela indique clairement quelles parties gèrent l'erreur et quelles parties sont sur le chemin normal normal.

C'est l'un des avantages de async / await: il fonctionne bien avec d'autres parties du langage, y compris les boucles, les branches, match , ? , try , etc.

En fait, à part await , c'est le même code que vous écririez si vous n'utilisiez pas Futures.

Une autre façon de l'écrire, si vous préférez utiliser le combinateur 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()

Et le meilleur de tous est de déplacer le code normal dans une fonction distincte, ce qui rend le code de gestion des erreurs encore plus clair:

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

(Ceci est une réponse au commentaire de

Pour être clair, vous n'êtes pas obligé de mettre entre parenthèses, je l'ai mentionné parce que certaines personnes ont dit que c'était trop difficile à lire sans les parenthèses. Ainsi, les parenthèses sont un choix stylistique

Toutes les syntaxes bénéficient de parenthèses dans certaines situations, aucune des syntaxes n'est parfaite, il s'agit de savoir pour quelles situations nous voulons optimiser.

De plus, après avoir relu votre commentaire, vous pensiez peut-être que je préconisais le préfixe await ? Je ne l'étais pas, mon exemple utilisait le suffixe await . Dans l'ensemble, j'aime postfix await , bien que j'aime aussi certaines des autres syntaxes.

Je commence à être à l'aise avec fut.await , je pense que la réaction initiale des gens sera "Attendez, c'est comme ça que vous attendez? Bizarre." mais plus tard, ils l'adoreraient pour sa commodité. Bien sûr, il en va de même pour @await , qui se démarque beaucoup plus que .await .

Avec cette syntaxe, nous pouvons omettre certains des let dans l'exemple:

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

Cela rend également plus clair ce qui est déballé avec ? , pour await some_op()? , il n'est pas évident de savoir si some_op() est déballé ou le résultat attendu.

@Pauan

Je n'essaye pas de détourner l'attention du sujet ici, j'essaie de souligner qu'il n'existe pas dans une bulle. Nous devons considérer comment les choses fonctionnent ensemble .

Même si la syntaxe idéale était choisie, je voudrais toujours utiliser des futurs et des combinateurs personnalisés dans certaines situations. L'idée que ceux-ci pourraient être légèrement obsolètes me fait remettre en question toute la direction de Rust.

Les exemples que vous donnez semblent toujours terribles par rapport aux combinateurs, et avec la surcharge du générateur, ils seront probablement un peu plus lents et produiront plus de code machine.

En attendant, tout ce bikeshedding préfixe / suffixe sigil / mot-clé est merveilleux, mais peut-être devrions-nous être pragmatiques et choisir l'option la plus simple qui est la plus familière aux utilisateurs venant sur Rust. Ie: mot-clé préfixe

Cette année va passer plus vite qu'on ne le pense. Même janvier est presque terminé. S'il s'avère que les utilisateurs ne sont pas satisfaits d'un mot-clé de préfixe, il peut être modifié dans une édition 2019/2020. Nous pouvons même faire une blague «avec le recul c'est 2020».

@novacrazy

Le consensus général que j'ai vu est que le plus tôt que nous voudrions une troisième édition est 2022. Nous ne voulons certainement pas planifier une autre édition; l'édition 2018 était géniale mais pas sans ses coûts. (Et l'un des points de l'édition 2018 est de rendre async / wait possible, imaginez reprendre cela et dire "non, vous devez passer à l'édition 2020 maintenant!")

En tout cas, je ne pense pas qu'une transition de mot-clé préfixe -> mot-clé postfixe soit possible dans une édition, même si cela serait souhaitable. La règle concernant les éditions est qu'il doit y avoir un moyen d'écrire du code idiomatique dans l'édition X de sorte qu'il compile sans avertissement et fonctionne de la même manière dans l'édition X + 1.

C'est la même raison pour laquelle nous préférons ne pas stabiliser avec une macro de mots clés si nous pouvons générer un consensus sur une solution différente; stabiliser délibérément une solution que nous savons n'est pas souhaitable est en soi problématique.

Je pense que nous avons montré qu'une solution postfix est plus optimale, même pour les expressions avec un seul point d'attente. Mais je doute que l'une des solutions postfix proposées soit évidemment meilleure que toutes les autres.

Juste mes deux cents (je ne suis personne, mais je suis la discussion depuis assez longtemps). Ma solution préférée serait la version postfixe @await . Peut-être pourriez-vous envisager un postfix !await , comme une nouvelle syntaxe de macro postfix?

Exemple:

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

Après quelques itérations de langage, être capable d'implémenter nos propres macros postfix serait génial.

... toutes les nouvelles propositions de sygils

Rust est déjà lourd de syntaxe / sygil, nous avons le mot clé await réservé, stuff@await (ou tout autre sygil) semble bizarre / moche (subjectif, je sais), et c'est juste une syntaxe ad-hoc qui ne s'intègre à rien d'autre dans la langue qui est un gros drapeau rouge.

Je regarde comment Go met en œuvre
... <- ... proposition

@ I60R : Go a une terrible syntaxe pleine de solutions ad hoc, et est totalement impératif, contrairement à Rust. Cette proposition est encore une fois lourde de syntaxe / syntaxe et totalement ad hoc juste pour cette fonctionnalité particulière.

@ I60R : Go a une syntaxe terrible

Évitons de dénigrer d'autres langues ici. "X a une syntaxe terrible" ne conduit pas à l'illumination et au consensus.

En tant qu'utilisateur Python / JavaScript / Rust et étudiant en informatique, je préfère personnellement que le préfixe await + f.await() soit à la fois dans le langage.

  1. Python et JavaScript ont le préfixe await . Je m'attendrais à voir apparaître await au début. Si je dois lire en profondeur pour me rendre compte que c'est du code asynchrone, je me sens très mal à l'aise. Avec la capacité WASM de Rust, il pourrait attirer de nombreux développeurs JS. Je pense que la familiarité et le confort sont vraiment importants, étant donné que Rust a déjà beaucoup d'autres nouveaux concepts.

  2. Postfix await semble pratique dans les paramètres de chaînage. Cependant, je n'aime pas les solutions comme .await , @await , f await parce qu'elles ressemblent à une solution ad hoc à la syntaxe await alors qu'il est logique de penser .await() comme appel d'une méthode sur le future .

Rust est déjà un départ de javascript et await n'a rien à voir avec l'appel de fonction (c'est-à-dire que la fonctionnalité ne peut pas être émulée via une fonction) en utilisant des fonctions pour désigner wait rend déroutant les premiers temporisateurs introduits dans async-await. Par conséquent, je pense que la syntaxe devrait être différente.

Je me suis convaincu que .await() est probablement beaucoup plus souhaitable que .await , bien que le reste de cet article couvre un peu cette position.

La raison en est que await!(fut) doit _consumer fut par valeur_. Faire ressembler à un accès à un champ est _bad_, car cela n'a pas la connotation de se déplacer comme le fait un mot-clé de préfixe, ou le potentiel de se déplacer comme une macro ou un appel de méthode.

Fait intéressant, la syntaxe de la méthode par mot-clé la fait presque ressembler à une conception d' attente implicite . Malheureusement, "Explicit Async, Implicit Await" n'est pas possible pour Rust (donc _veuillez_ ne pas le relancer sur ce fil) car nous voulons que async fn() -> T soit utilisé de la même manière que fn() -> Future<T> , plutôt que d'activer un comportement «d'attente implicite».

Le fait qu'une syntaxe .await() _ ressemble_ à un système d'attente implicite (comme Kotlin utilise) pourrait être un détracteur, et donnerait presque l'impression d'un "Async implicite, Attente implicite" en raison de la magie autour du non- vraiment-une-méthode-call .await() syntaxe. Pourriez-vous utiliser cela comme await(fut) avec UFCS? Serait-ce Future::await(fut) pour l'UFCS? Toute syntaxe qui ressemble à une autre dimension du langage pose des problèmes à moins qu'elle ne puisse être quelque peu unifiée avec elle au moins syntaxiquement, même si ce n'est pas fonctionnellement.

Je reste sceptique si les avantages de toute solution de postfix _individuelle_ l'emportent sur les inconvénients de cette même solution, bien que le concept d'une solution de postfix soit plus souhaitable que celui d'un préfixe en général.

Je suis un peu surpris que ce fil soit plein de suggestions qui semblent être faites parce qu'elles sont possibles - et non parce qu'elles semblent apporter des avantages significatifs par rapport aux suggestions initiales.
Pouvons-nous arrêter de parler de $ , # , @ , ! , ~ etc, sans évoquer un argument significatif ce qui ne va pas avec await , qui est bien compris et a fait ses preuves dans divers autres langages de programmation?

Je pense que le post de https://github.com/rust-lang/rust/issues/57640#issuecomment -455361619 a déjà répertorié toutes les bonnes options.

De ceux:

  • Délimiteurs obligatoires Les délimiteurs semblent bien, au moins il est évident quelle est la priorité et nous pouvons lire d'autres codes sans plus de clarté. Et taper deux parenthèses n'est pas si mal. Le seul inconvénient est peut-être que cela ressemble à un appel de fonction, même s'il s'agit d'une opération de flux de contrôle différente.
  • Une priorité utile pourrait être l'option préférable. Cela semble être la voie empruntée par la plupart des autres langues, c'est donc familier et éprouvé.
  • Personnellement, je pense que le mot-clé postfix avec espace blanc semble étrange avec plusieurs attentes dans une seule déclaration:
    client.get("url").send() await?.json()? . Cet espace entre les deux ne semble pas à sa place. Avec les parenthèses, cela aurait un peu plus de sens pour moi: (client.get("url").send() await)?.json()?
    Mais je trouve le flux de contrôle encore plus difficile à suivre qu'avec la variante de préfixe.
  • Je n'aime pas le champ postfix. await effectue une opération très complexe - Rust n'a pas de propriétés calculables et l'accès aux champs est par ailleurs une opération très simple. Cela semble donc donner une fausse impression sur la complexité de cette opération.
  • La méthode Postfix pourrait être correcte. Cela pourrait encourager certaines personnes à écrire de très longues déclarations avec plusieurs attentes, ce qui pourrait masquer davantage les points de rendement. Cela donne également à nouveau l'apparence d'un appel de méthode, même si c'est quelque chose de différent.

Pour ces raisons, je préfère la "préséance utile" suivie de "délimiteurs obligatoires"

Go a une terrible syntaxe pleine de solutions ad hoc, et est totalement impératif, contrairement à Rust. Cette proposition est encore une fois lourde de syntaxe / syntaxe et totalement ad hoc juste pour cette fonctionnalité particulière.

@dpc , si vous lisez complètement la proposition <-- , vous verrez que cette syntaxe n'est inspirée que par Go, mais est assez différente et utilisable dans un contexte impératif et de chaînage de fonctions. Je ne vois pas non plus en quoi la syntaxe await n'est pas une solution ad hoc, pour moi c'est beaucoup plus spécifique et beaucoup plus maladroit que <-- . C'est similaire d'avoir deref reference / reference.deref / etc au lieu de *reference , ou d'avoir try result / result.try / etc au lieu de result? . Je ne vois aucun avantage à utiliser le mot-clé await autre que la familiarité avec JS / Python / etc qui de toute façon devrait être moins important que d'avoir une syntaxe cohérente et composable. Et je ne vois aucun inconvénient à avoir <-- sigil autre que ce n'est pas un await qui de toute façon n'est pas aussi simple qu'un anglais simple et les utilisateurs devraient comprendre ce qu'il fait en premier.

Edit: cela pourrait également être une bonne réponse au post @ Matthias247 , car il fournit des arguments contre await et propose une alternative possible non affectée par les mêmes problèmes


C'est vraiment intéressant pour moi, à travers, de lire la critique contre la syntaxe <-- , sans arguments qui font appel à des raisons historiques et préjugées.

Mentionnons les spécificités réelles de la priorité:

Le diagramme de préséance tel qu'il se présente aujourd'hui :

Opérateur / Expression | Associativité
- | -
Chemins |
Appels de méthode |
Expressions de champ | de gauche à droite
Appels de fonctions, indexation de tableaux |
? |
Unaire - * ! & &mut |
as | de gauche à droite
* / % | de gauche à droite
+ - | de gauche à droite
<< >> | de gauche à droite
& | de gauche à droite
^ | de gauche à droite
\| | de gauche à droite
== != < > <= >= | Exiger des parenthèses
&& | de gauche à droite
\|\| | de gauche à droite
.. ..= | Exiger des parenthèses
= += -= *= /= %= &= \|= ^= <<= >>= | de droite à gauche
return break fermetures |

La précédence utile place await avant ? afin qu'il se lie plus étroitement que ? . Un ? dans la chaîne lie donc un `await à tout ce qui le précède.

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

Oui, avec une priorité utile, cela "fonctionne juste". Savez-vous ce que cela fait en un coup d'œil? Est-ce que c'est du mauvais style (probablement)? Si tel est le cas, rustfmt peut-il résoudre ce problème automatiquement?

Une priorité évidente place await _quelque part_ en dessous de ? . Je ne sais pas exactement où, bien que ces détails n'aient probablement pas trop d'importance.

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

Savez-vous ce que cela fait en un coup d'œil? Rustfmt peut-il le mettre dans un style utile qui ne gaspille pas automatiquement l'espace vertical et horizontal?


Où se situeraient les mots-clés avec suffixe? Probablement une méthode de mot-clé avec des appels de méthode et un champ de mot-clé avec des expressions de champ, mais je ne sais pas comment les autres doivent se lier. Quelles options conduisent aux configurations les moins possibles où le await reçoit un "argument" surprenant?

Pour cette comparaison, je soupçonne que les "délimiteurs obligatoires" (ce que j'ai appelé la fonction mot-clé dans le résumé ) l'emportent facilement, car cela équivaudrait à un appel de fonction normal.

@ CAD97 Pour être clair, rappelez-vous que .json() est aussi un avenir (au moins en reqwests ).

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

Plus je joue avec la conversion d'expressions de rouille complexes (même celles qui n'ont besoin que d'une attente, cependant, notez que dans ma future base de code de 20000+, presque chaque expression asynchrone est une attente, directement suivie d'une autre attente), plus je n'aime pas préfixe pour Rust.

C'est _all_ à cause de l'opérateur ? . Aucun autre langage n'a un opérateur de flux de contrôle postfixe _et_ await qui sont essentiellement toujours appariés dans du code réel.


Ma préférence reste le champ postfix . En tant qu'opérateur de contrôle postfix, je pense qu'il a besoin du regroupement visuel serré que future.await fournit sur future await . Et comparé à .await()? , je préfère à quoi .await? ressemble _weird_ donc ce sera _noté_ et les utilisateurs ne supposeront pas que c'est une fonction simple (et ne demanderont donc pas pourquoi UFCS ne fonctionne pas) .


Comme un autre point de données en faveur de postfix, lorsque celui-ci est stabilisé, un rustfix pour passer de await!(...) à ce que nous décidons serait très apprécié. Je ne vois pas comment quoi que ce soit d'autre que la syntaxe de postfix pourrait être traduit sans ambiguïté sans envelopper inutilement des éléments dans ( ... ) .

Je pense que nous devrions d'abord répondre à la question "voulons-nous encourager l'utilisation de await dans des contextes de chaînage?". Je crois que la réponse la plus répandue est "oui", donc cela devient un argument fort pour les variantes de suffixe. Alors que await!(..) sera le plus simple à ajouter, je pense que nous ne devrions pas répéter l'histoire try!(..) . Aussi, personnellement, je ne suis pas d'accord avec l'argument selon lequel "le chaînage cache une opération potentiellement coûteuse", nous avons déjà beaucoup de méthodes de chaînage qui peuvent être très lourdes, donc l'enchaînement n'implique pas de paresse.

Alors que le mot-clé préfixe await sera le plus familier pour les utilisateurs venant d'autres langues, je ne pense pas que nous devrions prendre notre décision en nous basant sur celui-ci et nous devrions plutôt nous concentrer sur le long terme, c'est-à-dire la convivialité, la commodité et la lisibilité. . @withoutboats a parlé de "budget de familiarité", mais je crois fermement que nous ne devrions pas introduire de solutions sous-optimales juste pour des raisons de familiarité.

Maintenant, nous ne voulons probablement pas deux façons de faire la même chose, nous ne devrions donc pas introduire à la fois des variantes de suffixe et de préfixe. Disons donc que nous avons réduit nos options aux variantes de postfix.

Commençons par fut await , je n'aime pas du tout cette variante, car elle dérangera sérieusement la façon dont les humains analysent le code et ce sera une source constante de confusion lors de la lecture du code. (N'oubliez pas que le code est principalement destiné à la lecture)

Suivant fut.await , fut.await() et fut.await!() . Je pense que la variante la plus cohérente et la moins déroutante sera la macro postfix. Je ne pense pas que cela vaille la peine d'introduire une nouvelle entité «fonction de mot-clé» ou «méthode de mot-clé» juste pour enregistrer quelques caractères.

Enfin, les variantes basées sur le sigil: fut@await et fut@ . Je n'aime pas la variante fut@await , si nous introduisons le sceau pourquoi s'embêter avec la partie await ? Avons-nous des plans pour de futures extensions fut@something ? Sinon, cela semble tout simplement redondant. Donc j'aime la variante fut@ , elle résout les problèmes de précédence, le code devient facile à comprendre, à écrire et à lire. Les problèmes de visibilité peuvent être résolus par la mise en évidence du code. Il ne sera pas difficile d'expliquer cette fonctionnalité par "@ for await". Bien sûr, le plus gros inconvénient est que nous paierons cette fonctionnalité avec le "budget sigil" très limité, mais compte tenu de l'importance de la fonctionnalité et de la fréquence à laquelle elle sera utilisée dans les bases de code asynchrones, je pense que cela vaudra la peine à long terme . Et bien sûr, nous pouvons établir certains parallèles avec ? . (Bien que nous devrons être préparés pour les blagues Perl des critiques de Rust)

En conclusion: à mon avis, si nous sommes prêts à alourdir le "budget sigil", nous devrions opter pour fut@ , et sinon avec fut.await!() .

En parlant de familiarité, je ne pense pas que nous devrions trop nous soucier de la familiarité avec JS / Python / C #, car Rust est dans un créneau différent et a déjà un aspect différent dans de nombreux domaines. Fournir une syntaxe similaire à ces langages est un objectif à court terme et à faible récompense. Personne ne sélectionnera Rust uniquement pour utiliser un mot-clé familier lorsque sous le capot, cela fonctionne complètement différent.

Mais la familiarité avec Go compte, car Rust est dans un créneau similaire et même par philosophie, il est plus proche de Go que d'autres langues. Et malgré toute la haine préjugée, l'un des points forts des deux est qu'ils ne copient pas aveuglément des fonctionnalités, mais implémentent plutôt des solutions qui ont vraiment raison.

IMO, dans ce sens, la syntaxe <-- est la plus forte ici

Avec tout cela, rappelons-nous que les expressions Rust peuvent résulter en plusieurs méthodes chaînées. La plupart des langues ont tendance à ne pas faire cela.

Je voudrais rappeler l'expérience de l'équipe de développement C #:

La principale considération par rapport à la syntaxe C # est la priorité des opérateurs wait foo?

C'est quelque chose que je pense pouvoir commenter. Nous avons beaucoup pensé à la préséance avec «wait» et nous avons essayé de nombreuses formes avant de définir la forme que nous voulions. L'une des choses fondamentales que nous avons trouvées était que pour nous, et les clients (internes et externes) qui voulaient utiliser cette fonctionnalité, il était rarement le cas que les gens voulaient vraiment `` enchaîner '' quoi que ce soit après leur appel asynchrone.

La tendance des gens à vouloir «continuer» avec «attendre» à l'intérieur d'une expr était rare. Nous voyons parfois des choses comme (await expr) .M (), mais celles-ci semblent moins courantes et moins souhaitables que le nombre de personnes qui font wait expr.M ().

et

C'est aussi pourquoi nous n'avons utilisé aucune forme «implicite» pour «attendre». Dans la pratique, c'était quelque chose à laquelle les gens voulaient réfléchir très clairement, et qu'ils voulaient au centre de leur code afin d'y prêter attention. Chose intéressante, même des années plus tard, cette tendance est restée. c'est-à-dire que parfois nous regrettons plusieurs années plus tard que quelque chose soit excessivement verbeux. Certaines fonctionnalités sont bonnes de cette manière dès le début, mais une fois que les gens sont à l'aise avec cela, elles sont mieux adaptées à quelque chose de plus terser. Cela n'a pas été le cas avec «attendre». Les gens semblent toujours aimer la nature lourde de ce mot-clé et la priorité que nous avons choisie.

Est un bon point contre sigil au lieu d'un mot (clé) dédié.

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

Vous devriez vraiment écouter les gars avec des millions d'utilisateurs.

Donc vous ne voulez rien enchaîner, vous voulez juste avoir plusieurs await , et mon expérience est la même. J'ai écrit du code async/await pendant plus de 6 ans, et je n'ai jamais voulu une telle fonctionnalité. La syntaxe de Postfix semble vraiment étrangère et est considérée comme résolvant une situation qui ne se produira probablement jamais. Async appel

La tendance des gens à vouloir «continuer» avec «attendre» à l'intérieur d'une expr était rare. Nous voyons parfois des choses comme (await expr) .M (), mais celles-ci semblent moins courantes et moins souhaitables que le nombre de personnes qui font wait expr.M ().

Cela ressemble à une analyse a posteriori. Peut-être qu'une des raisons pour lesquelles ils ne continuent pas est qu'il est extrêmement gênant de le faire dans la syntaxe du préfixe (comparable à ne pas vouloir plusieurs fois try! dans une instruction car cela reste lisible via l'opérateur ? ). Ce qui précède considère principalement (pour autant que je sache) la préséance, pas la position. Et je voudrais vous rappeler que C # n'est pas Rust, et que les membres de trait peuvent changer un peu le désir d'appeler des méthodes sur les résultats.

@ I60R ,

  1. Je pense que la familiarité est importante. Rust est une langue relativement nouvelle et les gens migreront à partir d'autres langues et si Rust semble familier, il leur sera plus facile de prendre la décision de choisir Rust.
  2. Je ne suis pas très amusant des méthodes de chaînage - il est beaucoup plus difficile de déboguer de longues chaînes et je pense que le chaînage ne fait que compliquer la lisibilité du code et ne peut être autorisé qu'en option supplémentaire (comme les macros .await!() ). La forme du préfixe obligera les développeurs à extraire le code dans des méthodes au lieu de le chaîner, comme:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

en quelque chose comme ça:

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

Cela ressemble à une analyse a posteriori. Peut-être qu'une des raisons pour lesquelles ils ne continuent pas est qu'il est extrêmement difficile de le faire dans la syntaxe de préfixe. Ce qui précède ne considère que la priorité, pas la position. Et je voudrais vous rappeler que C # n'est pas Rust, et que les membres de trait peuvent changer un peu le désir d'appeler des méthodes sur les résultats.

Non, il s'agit d'expérimentations internes en équipe C # lorsqu'elle avait à la fois des formes préfixe / suffixe / implicite. Et je parle de mon expérience qui n'est pas seulement une habitude où je ne peux pas voir les pros du formulaire postfix.

@mehcode Votre exemple ne me motive pas. reqwest décide consciemment de faire en sorte que le cycle initial de demande / réponse et le traitement ultérieur de la réponse (flux de corps) séparent les processus concurrents, ils devraient donc être attendus deux fois, comme le montre @andreytkachenko .

reqwest pourrait totalement exposer les API suivantes:

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

ou

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

(Ce dernier étant un sucre simple de plus de and_then ).

Je trouve troublant que beaucoup d'exemples de postfix ici utilisent cette chaîne comme exemple, car reqwest s et hyper s est la meilleure décision api de garder ces choses séparées.

Je crois honnêtement que la plupart des exemples de postfix attendent ici devraient être réécrits en utilisant des combinateurs (et du sucre si nécessaire) ou être conservés des opérations similaires et attendus plusieurs fois.

@andreytkachenko

La forme du préfixe obligera les développeurs à extraire le code dans des méthodes au lieu de le chaîner, comme:

Est-ce une bonne ou une mauvaise chose? Pour les cas où il y a N méthodes, chacune pouvant entraîner M suivis, le développeur est censé fournir des méthodes N * M ? Personnellement, j'aime les solutions composables, même si elles sont un peu plus longues.

en quelque chose comme ça:

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

@Pzixel

Je ne pense pas qu'en C # vous obtiendriez autant de code chaîné / fonctionnel que vous le feriez en Rust. Ou ai-je tort? Cela rend les expériences des développeurs / utilisateurs C # intéressantes, mais pas nécessairement applicables à Rust. Je souhaite que nous puissions contraster avec Ocaml ou Haskell.

Je pense que la cause fondamentale du désaccord est que certains d'entre nous apprécient le style impératif, et d'autres fonctionnels. Et c'est tout. Rust prend en charge les deux, et les deux côtés du débat veulent qu'async s'adapte bien à la manière dont ils écrivent généralement le code.

@dpc

Je ne pense pas qu'en C # vous obtiendriez autant de code chaîné / fonctionnel que vous le feriez en Rust. Ou ai-je tort? Cela rend les expériences des développeurs / utilisateurs C # intéressantes, mais pas nécessairement applicables à Rust. Je souhaite que nous puissions contraster avec Ocaml ou Haskell.

LINQ et le style fonctionnel sont très populaires en C #.

@dpc ,
si vous êtes sur la lisibilité - je pense que le code est explicite - comme meilleur, et mon expérience dit que si les noms des méthodes / fonctions sont bien auto-descriptifs, il n'est pas nécessaire de faire les suivis.

si vous avez des frais généraux - Le compilateur Rust est plutôt intelligent pour les incorporer (de toute façon, nous avons toujours #[inline] ).

@dpc

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

Mon sentiment est que cela répète fondamentalement le problème de l'API future: cela facilite le chaînage, mais le type de cette chaîne devient beaucoup plus difficile à pénétrer et à déboguer.

Ce même argument ne s'applique-t-il pas au? opérateur cependant? Cela permet plus de chaînage et rend ainsi le débogage plus difficile. Mais pourquoi avons-nous choisi? essayez! puis? Et pourquoi Rust préfère-t-il les API avec un modèle de générateur? Rust a donc déjà fait un tas de choix favorisant le chaînage dans les API. Et oui, cela peut rendre le débogage plus difficile, mais cela ne devrait-il pas se produire au niveau de la charpie - peut-être une nouvelle charpie pour clippy pour les chaînes trop grandes? Je dois encore voir une motivation suffisante quant à la façon dont l'attente est différente ici.

Est-ce une bonne ou une mauvaise chose? Pour les cas où il y a N méthodes, chacune pouvant entraîner M suivis, le développeur est censé fournir des méthodes N * M ? Personnellement, j'aime les solutions composables, même si elles sont un peu plus longues.

N et M ne sont pas nécessairement grands, et tous ne peuvent pas non plus être intéressants / utiles à extraire.

@andreytkachenko ,

  1. Je ne suis pas d'accord, la familiarité est surfaite ici. Lors de la migration à partir d'un autre langage, les premiers utilisateurs rechercheraient la possibilité de faire async-await programmation de style

  2. L'incapacité de déboguer de longues chaînes est une limitation des débogueurs et non du style de code. À propos de la lisibilité, je pense que cela dépend, et l'application d'un style impératif est ici inutilement restrictive. Si les appels de fonction async sont toujours des appels de fonction, il est surprenant de ne pas prendre en charge le chaînage. Et de toute façon, <-- est un opérateur de préfixe avec la possibilité de l'utiliser dans les chaînes de fonctions comme option supplémentaire, comme vous l'avez dit

@skade D'accord.

Tous les exemples avec de longues chaînes d'attente sont une odeur de code. Ils pourraient être résolus beaucoup plus élégamment avec des combinateurs ou des API mises à jour, plutôt que de créer ces machines à états génératrices de spaghetti sous le capot. La syntaxe abrégée extrême est une recette pour un débogage encore plus difficile que les futurs actuels.

Je suis toujours un fan des deux préfixe mot - clé await et macro await!(...) / .await!() , en combinaison avec async et #[async] générateur fonctionne , comme décrit dans mon commentaire ici .

Si tout se passe correctement, une seule macro pourrait probablement être créée pour await! pour gérer à la fois le préfixe et le suffixe, et le mot clé await ne serait qu'un mot clé à l'intérieur des fonctions async .

@skade ,
Pour moi, un gros inconvénient de l'utilisation de combinateurs sur future est qu'ils masqueront les appels de fonction async et qu'il serait impossible de les distinguer des fonctions régulières. Je veux voir tous les points suspensibles car il est très probable qu'ils soient utilisés à l'intérieur de chaînes de style constructeur.

@ I60R débogage de longues chaînes d'appels n'est absolument pas un problème de débogage, car le problème avec l'écriture de ces chaînes est d'obtenir une inférence de type correcte. Étant donné que beaucoup de ces méthodes prennent des paramètres génériques et distribuent des paramètres génériques, potentiellement liés à une fermeture, est un problème grave.

Je ne sais pas si rendre tous les points de suspension visibles est un objectif de la fonctionnalité. C'est aux exécutants de décider. Et c'est tout à fait possible avec toutes les versions proposées de la syntaxe await .

@novacrazy bon point, maintenant que vous le mentionnez, en tant qu'ancien javascripter, je n'utilise JAMAIS de chaînes d'attente, j'ai toujours utilisé ensuite des blocs

Je suppose que ça ressemblerait à quelque chose comme

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

dont je ne sais même pas si c'est possible, j'ai besoin de chercher sur l'avenir à Rust

@richardanaya c'est tout à fait possible (syntaxe différente).

Comme la boîte impl:

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

Nous pouvons entrer un nouveau mot-clé await et le trait Await avec comme ceci impl:

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

Et utilisez await comme méthode et comme mot-clé:

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

@richardanaya D'accord. J'ai été parmi les premiers développeurs à adopter async / await pour mes trucs webdev il y a plusieurs années, et le vrai pouvoir est venu de combiner async / await avec les Promises / Futures existantes. De plus, même votre exemple pourrait probablement être simplifié comme suit:

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

let value = result.unwrap();

Si vous avez imbriqué des contrats à terme ou des résultats de contrats à terme ou des contrats à terme de résultats, le combinateur .flatten() peut encore simplifier considérablement cela. Ce serait une mauvaise forme de déballer et d'attendre sur chacun d'eux manuellement.

@XX C'est redondant et invalide. await n'existe que dans les fonctions async , et ne peut fonctionner que sur les types qui implémentent Future / IntoFuture toute façon, il n'y a donc pas besoin d'un nouveau trait.

@novacrazy , @richardanaya

ils pourraient être résolus beaucoup plus élégamment avec des combinateurs ou des API mises à jour, plutôt que de créer ces machines à états spaghetti génératrices sous le capot. La syntaxe abrégée extrême est une recette pour un débogage encore plus difficile que les futurs actuels.

@novacrazy bon point, maintenant que vous le mentionnez, en tant qu'ancien javascripter, je n'utilise JAMAIS de chaînes d'attente, j'ai toujours utilisé ensuite des blocs

Les combinateurs ont des propriétés et des pouvoirs totalement différents dans Rusts async / wait, par exemple en ce qui concerne l'emprunt à travers les points de rendement. Vous ne pouvez pas écrire en toute sécurité des combinateurs qui ont les mêmes capacités que les blocs asynchrones. Laissons la discussion sur les combinateurs en dehors de ce fil, car elle n'est pas utile.

@ I60R

Pour moi, un gros inconvénient de l'utilisation de combinateurs à l'avenir est qu'ils masqueront les appels de fonctions asynchrones et qu'il serait impossible de les distinguer des fonctions régulières. Je veux voir tous les points suspensibles car il est très probable qu'ils soient utilisés à l'intérieur de chaînes de style constructeur.

Vous ne pouvez pas masquer les points de suspension. La seule chose que permettent les combinateurs est de créer d'autres futurs. À un moment donné, ceux-ci doivent être attendus.

Gardez à l'esprit que C # n'a pas le problème où la plupart du code utilisant un préfixe await le combinera avec l'opérateur postfix (déjà existant) ? . En particulier, les questions de préséance et la maladresse générale d'avoir deux «décorateurs» similaires apparaissent sur les côtés opposés d'une expression.

@ Matthias247 Si vous avez besoin d'emprunter des données, bien sûr, n'hésitez pas à utiliser plusieurs instructions await . Cependant, souvent, vous avez simplement besoin de déplacer des données, et les combinateurs sont parfaitement valables pour cela et ont le potentiel de compiler vers un code plus efficace. Parfois optimisé complètement.

Encore une fois, le vrai pouvoir est de combiner les choses ensemble. Il n'y a pas une seule bonne façon de faire les choses aussi compliquées que celle-ci. C'est en partie pourquoi je considère que tout ce bikeshedding syntaxique est exactement cela s'il ne va pas aider les nouveaux utilisateurs venant d'un autre langage et aider à créer un code maintenable, performant et lisible . 80% du temps, je doute que je toucherai même async / await quelle que soit la syntaxe, juste pour fournir les API les plus stables et les plus performantes en utilisant des futurs simples.

À cet égard, le mot-clé préfixe await et / ou la macro mixte await!(...) / .await!() sont les options les plus lisibles, les plus familières et les plus faciles à déboguer.

@andreytkachenko

J'ai vu votre message et je suis tout à fait d'accord, en fait, maintenant que je réfléchis à la «familiarité», je pense qu'il y a deux types de familiarité:

  1. Faire ressembler la syntaxe d’attente à des langages similaires qui utilisent beaucoup d’attente. Javascript est assez gros ici, et très pertinent pour nos capacités WASM en tant que communauté.

  2. Le deuxième type de familiarité est de rendre le code familier aux développeurs qui ne travaillent qu'avec du code synchrone. Je pense que le plus grand aspect de wait est de rendre le code asynchrone LOOK synchrone. C'est en fait une chose que javascript fait vraiment mal, c'est qu'il est normalisé de longues chaînes qui semblent totalement étrangères aux développeurs principalement synchrones.

Une chose que j'offrirais de mes jours javascript async, ce qui m'a été beaucoup plus utile en tant que développeur rétrospectivement était la possibilité de regrouper les promesses / futurs. Promise.all (p1 (), p2 ()) pour que je puisse facilement mettre en parallèle le travail. Le chaînage d'alors () n'était toujours qu'un écho de la promesse passée de Javascript, mais à peu près archaïque et inutile maintenant que j'y pense.

J'offrirais peut-être cette idée d'attendre. "Essayez de rendre les différences entre le code async et le code de synchronisation aussi minimes que possible"

@novacrazy La fonction async renvoie un type impl Future , non? Qu'est-ce qui nous empêche d'ajouter la méthode await au trait Future ? Comme ça:

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

@XX Si je comprends bien, les fonctions async de Rust sont transformées en machines à états à l'aide de générateurs. Cet article est une bonne explication. Donc await besoin d' une fonction async pour travailler, afin que le compilateur puisse les transformer correctement tous les deux. await ne peut pas fonctionner sans la partie async .

Future a un wait méthode, qui est similaire à ce que vous suggérez, mais bloque le thread courant.

@skade ,

Mais comment le style de code peut-il affecter l'inférence de type? Je ne vois pas de différence dans le même code qui est écrit dans un style enchaîné et impératif. Les types doivent être exactement les mêmes. Si le débogueur n'est pas capable de les comprendre, c'est certainement un problème dans le débogueur, pas dans le code.

@skade , @ Matthias247 , @XX

Avec les combinateurs, vous aurez exactement un point suspendu marqué au début de la chaîne de fonctions. Tous les autres seraient implicites à l'intérieur. C'est exactement le même problème qu'avec la prise de mut implicite, ce qui était personnellement pour moi l'un des plus gros points de confusion de Rust. Certaines API renverraient des futures tandis que d'autres renverraient des résultats de futures - je ne veux pas de cela. Les points de suspension devraient être explicites si possible et une syntaxe correctement composable encouragerait

@ I60R let liaisons sont des points de jonction pour l'inférence de type et aident à signaler les erreurs.

@novacrazy

Attendre a donc besoin d'une fonction asynchrone pour fonctionner

Cela peut-il être exprimé en type de retour? Par exemple, que le type de retour est impl Future + Async , au lieu de impl Future .

@skade J'ai toujours pensé que . dans la syntaxe des appels de méthode sert exactement le même objectif

@dpc

Je ne pense pas qu'en C # vous obtiendriez autant de code chaîné / fonctionnel que vous le feriez en Rust. Ou ai-je tort? Cela rend les expériences des développeurs / utilisateurs C # intéressantes, mais pas nécessairement applicables à Rust. Je souhaite que nous puissions contraster avec Ocaml ou Haskell.

vous obtenez autant que Rust. Regardez n'importe quel code LINQ et vous le verrez.

Le code asynchrone typique ressemble à ceci:

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, plus général

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

Vous n'enchaînez pas les appels, vous l'assignez à une variable et vous l'utilisez d'une manière ou d'une autre. C'est particulièrement vrai lorsque vous traitez avec un emprunteur.


Je peux convenir que vous pouvez enchaîner les futurs dans une seule situation. Lorsque vous souhaitez gérer une erreur pouvant survenir lors de l'appel. par exemple let a = await!(service_a)? . C'est la seule situation où l'alternative à postfix est meilleure. Je pouvais voir que cela profiterait ici, mais je ne pense pas que cela dépasse tous les inconvénients.

Une autre raison: qu'en est-il de impl Add for MyFuture { ... } et let a = await a + b; ?

Je voudrais rappeler la RFC d'assignation de type généralisée dans le contexte du mot-clé d'attente de postfix. Cela permettrait le code suivant:

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

Tout à fait comme le mot-clé d'attente de suffixe:

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

Avec les mêmes inconvénients lorsqu'il est mal formaté:

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

Je pense que si nous avalons la pilule du RFC Type Ascription, nous devrions avaler fut await pour plus de cohérence.

BTW, saviez-vous que c'est une syntaxe valide:

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

Pourtant, je n'ai vu aucune occurrence de cela dans le code réel.

Permettez-moi de m'écarter un peu. Je pense que nous devrions donner des macros postfixes, comme suggéré par @BenoitZugmeyer , une autre pensée. Celles-ci pourraient être faites soit comme expr!macro ou comme expr@macro , ou peut-être même expr.macro!() . Je pense que la première option serait préférable. Je ne suis pas sûr que ce soit une bonne idée, mais si nous voulons extraire un concept général et non une solution ad-hoc tout en attendant un postfix , nous devrions au moins penser aux macros postfix comme solution potentielle.

Gardez à l'esprit que même si nous devions faire de await une macro de suffixe, ce serait toujours magique (comme compile_error! ). Cependant, @jplatte et d'autres ont déjà établi que ce n'était pas un problème.

Comment je procéderais si nous suivions cette voie, j'établirais d'abord exactement comment les macros postfix fonctionneraient, puis n'autoriserais que await comme macro postfix magique, puis autoriserais plus tard les macros postfix définies.

Lexing

En ce qui concerne le lexing / l'analyse, cela pourrait être un problème. Si nous regardons expr!macro , le compilateur pourrait penser qu'il y a une macro appelée expr! et puis il y a quelques lettres invalides macro après cela. Cependant, expr!macro devrait être possible de lex par un lookahead de un, et quelque chose devient une macro de suffixe quand il y a un expr suivi d'un ! , directement suivi de identifier . Je ne suis pas un développeur de langage et je ne suis pas sûr que cela rende le lexing trop complexe. Je suppose simplement que les macros postfix peuvent prendre la forme de expr!macro .

Les macros postfix seraient-elles utiles?

Sur le dessus de ma tête, j'ai proposé ces autres cas d'utilisation pour les macros postfix. Je ne pense pas que tous puissent être implémentés par des macros personnalisées, donc je ne suis pas sûr que cette liste soit très utile.

  • stream!await_all : pour les streams en attente
  • option!or_continue : quand option vaut None, continuez la boucle
  • monad!bind : pour faire =<< sans le lier à un nom

stream!await_all

Cela nous permet non seulement d'attendre des futurs, mais aussi des flux.

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

serait équivalent à (dans un bloc async -stream-esque):

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

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

Cela nous permet de dérouler une option, et si c'est None , continuez la boucle.

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

serait équivalent à:

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

monad!bind

Celui-ci nous permettrait d'obtenir des monades d'une manière assez rustique (c'est-à-dire centrée sur l'expression). Rust n'a pas encore quelque chose comme des monades et je ne suis pas convaincu qu'elles soient ajoutées à Rust. Néanmoins, s'ils devaient être ajoutés, cette syntaxe pourrait être utile.

J'ai pris ce qui suit d' ici .

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

serait équivalent à:

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, plus intégré, avec moins de 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;
}

Évaluation

Je suis très partagé sur ce point. La partie mathématicienne de mon cerveau veut trouver une solution généralisée pour await , et les macros postfixes pourraient être un moyen de les réaliser. La partie pragmatique de mon cerveau pense: Pourquoi se donner la peine de faire un langage dont le macro système déjà personne ne comprend encore plus compliqué.

Cependant, à partir de ? et await , nous avons deux exemples d'opérateurs postfix super utiles. Et si nous en trouvons d'autres que nous voulons ajouter à l'avenir, peut-être similaires à ceux que j'ai mentionnés? Si nous les avons généralisés, nous pouvons les ajouter à Rust naturellement. Si ce n'est pas le cas, nous devrons proposer une autre syntaxe à chaque fois, ce qui gonflera probablement le langage plus que les macros postfix.

Qu'en pensez-vous?

@EyeOfPython

J'ai eu une idée très similaire, et je suis sûr que les macros postfix dans Rust sont une question de temps.
J'y pense à chaque fois lorsque je traite du slicing ndarray :

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

mais beaucoup mieux serait

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

et beaucoup de combinateurs peuvent disparaître, comme ceux avec _with ou _else postfixes:

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

@EyeOfPython @andreytkachenko Les macros postfixes ne sont actuellement pas une fonctionnalité de Rust et IMHO aurait besoin d'une phase complète de mise en œuvre RFC + FCP +.

Ce n'est pas une discussion RFC, mais une discussion d'une RFC acceptée qui doit être implémentée.

Pour cette raison, je ne pense pas qu'il soit pratique de les discuter ici ou de les proposer pour la syntaxe asynchrone. Cela retarderait encore massivement la fonctionnalité.

Pour freiner cette discussion déjà massive, je pense qu'il n'est pas utile de les discuter ici, ils peuvent être considérés comme en dehors de la question discutée.

Juste une réflexion: bien que ce fil soit spécifiquement pour await , je pense que nous aurons la même discussion pour les expressions yield tard, qui pourraient également être chaînées. Dans la mesure où je préférerais voir une syntaxe qui peut être généralisée, quelle que soit son apparence.

Mes raisons de ne pas utiliser de macros ici:

  1. Les macros sont fournies pour des éléments spécifiques au domaine et les utiliser de toutes les manières qui modifient le flux de contrôle des programmes ou émulent les fonctionnalités du langage de base est une exagération
  2. Les macros Postfix seraient immédiatement utilisées abusivement pour implémenter des opérateurs personnalisés ou d'autres fonctionnalités de langage ésotherique qui conduisent à un mauvais code
  3. Ils décourageraient le développement de fonctionnalités de langage appropriées:

    • stream.await_all est un cas d'utilisation parfait pour le combinateur

    • option.or_continue et le remplacement de _else combinateurs est un cas d'utilisation parfait pour l'opérateur de fusion nul

    • monad.bind est un cas d'utilisation parfait pour les chaînes if-let

    • ndarray slicing est un cas d'utilisation parfait pour les génériques const

  4. Ils mettraient en question l'opérateur déjà implémenté ?

@collinanderson

qui pourrait également être enchaîné

Pourquoi diable pensez-vous qu'ils pourraient être enchaînés? zéro occurrence dans le code réel, tout comme l'exemple println ci-dessus.

@Pzixel Il est probable que finalement Generator::resume prendra une valeur, et donc yield expr aura un type non- () .

@vlaff Je ne vois pas clairement voir un argument pour await devant être cohérent avec l'attribution de type. Ce sont des choses très différentes.

En outre, l'attribution de type est une autre de ces fonctionnalités qui sont essayées à plusieurs reprises, il n'y a aucune garantie que _celle_ le fera. Bien que je ne veuille pas m'opposer à cela, TA est une future RFC et il s'agit d'une discussion sur une fonctionnalité acceptée avec une syntaxe déjà proposée.

En lisant les nombreux commentaires, pour ajouter un résumé supplémentaire sur pourquoi attendre! (...) apparaît un chemin très idéal:

  1. il semble le plus familier au code existant car les macros sont familières
  2. il y a du travail existant qui utilise await! (...) https://github.com/alexcrichton/futures-await et pourrait aider à réduire la réécriture du code
  3. puisque les macros de postfix ne sont pas sur la table, peuvent ne jamais faire partie du langage et "attendre!" dans un contexte non standard ne semble pas être même une possibilité selon les RFC
  4. cela donne à la communauté plus de temps pour considérer la direction à long terme après une utilisation stable à grande échelle tout en fournissant quelque chose qui n'est pas totalement hors norme (par exemple. try! ())
  5. pourrait être utilisé comme un modèle similaire pour le rendement à venir! () jusqu'à ce que nous trouvions un chemin officiel qui pourrait satisfaire wait et yield
  6. le chaînage peut ne pas être aussi précieux qu'on l'espère et la clarté sera probablement même améliorée en ayant plusieurs attentes sur plusieurs lignes
  7. aucun changement ne devrait se produire pour les surligneurs de syntaxe IDE
  8. les personnes d'autres langues ne seront probablement pas ébranlées par une macro d'attente! (...) après avoir vu d'autres usages macro (moins de surcharge cognitive)
  9. c'est probablement le chemin d'effort le plus minimal de tous les chemins pour avancer vers la stabilisation

attendre! macro est déjà ici, la question est de chaîner.

Plus j'y pense, plus le suffixe me paraît bien. Par exemple:

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

Permet l'imbrication de tous les niveaux et la lecture du quire. Fonctionne parfaitement avec ? et les futurs opérateurs possibles. Cela semble un peu étranger pour les nouveaux arrivants, mais le profit de la grammaire peut le surpasser.

La raison principale est que await doit être un mot-clé séparé, pas un appel de fonction. C'est trop important pour qu'il ait sa propre mise en évidence et sa propre place dans le texte.

@Pzixel , @HeroicKatora , @skade

Voir par exemple les expressions Python yield et yield from : là, la fonction externe peut fournir une valeur qui deviendra le résultat de yield lorsque le générateur redémarrera. C'est ce que @valff voulait dire et n'a rien à voir avec les attributions de type. Par conséquent, yield aura également un type autre que ! ou () .

Depuis le point de la coroutine, yield et await suspendent et peuvent (éventuellement) renvoyer une valeur. Dans la mesure où ils ne sont que les deux faces d'une même pièce.

Et pour jeter une autre possibilité de syntaxe, _square-brackets-with-keyword_, en partie pour mettre en évidence ceci (en utilisant yield pour la coloration syntaxique):

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

"mot-clé postfix" me semble le plus logique. suffixe avec un caractère autre qu'un espace séparant l'expression future et await également un sens pour moi, mais pas '.' car c'est pour les méthodes et await n'est pas une méthode. Cependant, si vous voulez await comme mot-clé de préfixe, j'aime la suggestion @XX d'un await self pour ceux qui veulent enchaîner un tas de choses ensemble (bien que nous impossible de nommer la méthode await ; juste wait ferait bien que je pense). Personnellement, je finirais probablement par créer une attente par ligne et pas tellement une chaîne parce que je trouve les longues chaînes moins lisibles, donc le préfixe ou le postfixe fonctionneraient pour moi.

[modifier] J'ai oublié que wait existe déjà et bloque le futur, alors grattez cette pensée.

@roland Je fais spécifiquement référence à ceci, qui traite des attributions de type: https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@rolandsteiner donc vous écrivez

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

Quand je l'écrirais comme:

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

@skade Oh, tu voulais dire un commentaire différent de ce que je pensais, désolé. : stuck_out_tongue:

@Pzixel Il est probable que finalement Generator::resume prendra une valeur, et donc yield expr aura un type non- () .

Par conséquent, yield aura également un type autre que ! ou () .

@valff , @rolandsteiner Je trouve peu probable que yield renvoie la valeur de reprise, difficile à intégrer dans un langage typé statiquement sans rendre la syntaxe et / ou le trait du générateur ennuyeux à utiliser. Le prototype original avec des arguments de reprise utilisait le mot-clé gen arg pour faire référence à cet argument, quelque chose comme celui-ci est beaucoup plus susceptible de bien fonctionner IMO. De ce point de vue, yield retournera toujours () et ne devrait donc pas affecter beaucoup la discussion sur await .

Je pense que await devrait être un opérateur de préfixe, car async est (et je m'attends yield ce que

Cependant, je ne fais pas tourner tout le bikeshedding ici: pourquoi même envisager un mot-clé postfix là où aucun autre de Rust ne l'a? Cela rendrait la langue si bizarre.

Aussi, en enchaînant les futurs, je comprends pourquoi cela pourrait être intéressant. Mais l'enchaînement vous attend? Qu'est-ce que cela signifie même? Un avenir qui renvoie un nouvel avenir? Est-ce vraiment un idiome si commun que nous voulons que Rust ait cet idiome en tant que citoyen de première classe? Si cela nous tient vraiment à cœur, je pense que nous devrions:

  1. Optez pour la méthode (ie foo.await() ). Cela n'introduit pas de mot-clé postfix étrange et nous savons tous ce que cela signifie. Nous pouvons également enchaîner avec cela.
  2. Si nous voulons vraiment un mot-clé / loloperator, nous pourrons régler cette question plus tard.

Aussi, pour donner mon opinion personnelle, je déteste le await en position de mot-clé postfix.

Le préfixe await est utilisé dans d'autres langues pour une raison - il correspond au langage naturel. Vous ne dites pas "Je vais attendre votre arrivée". Bien qu'ayant écrit JavaScript, je suis peut-être un peu partial.

Je dirai aussi que je considère le await -chaining surfait. La valeur de async/await sur les futurs combinateurs est qu'il permet d'écrire du code asynchrone séquentiellement, en utilisant les constructions de langage standard telles que if , match , et cetera. Vous allez quand même vouloir rompre vos attentes.

N'inventons pas trop de syntaxe magique pour async/await , ce n'est qu'une petite partie du langage. La syntaxe de méthode proposée (ie foo.await() ) est IMO trop orthogonale aux appels de méthode normaux et se révèle trop magique. La machinerie proc-macro est en place, pourquoi ne pas la dissimuler derrière ça?

Que diriez-vous d'utiliser le mot-clé await par opposition à async dans la définition des fonctions? Par exemple:

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

Ce await fn ne peut être appelé que dans le contexte async :

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

Et désugaring à let n = (await foo(bar())); .
Ensuite, en plus du mot-clé await , le trait Future pourrait implémenter la méthode await pour utiliser la logique await en position de suffixe, par exemple:

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

Aussi, quelqu'un peut-il m'expliquer la relation avec les générateurs? Je suis surpris que async / await soient implémentés avant que les générateurs ne soient (même stabilisés).

Les générateurs async / await . Il n'y a pas de plans actuels pour les stabiliser, et ils nécessitent toujours un RFC non expérimental avant de pouvoir l'être. (Personnellement, j'adorerais les stabiliser, mais il semble probable qu'ils seront dans au moins un an ou deux, probablement pas RFC avant que async / await soit stable et il y a eu une certaine expérience avec lui).

@XX Je pense que vous async si vous utilisez await dans la définition de fonction. Tenons-nous en aux normes, n'est-ce pas?

@phaazon Pouvez-vous préciser plus en détail comment cela rompt la sémantique de async ?

La plupart des langages utilisent async pour introduire quelque chose qui va être asynchrone et await attend. Donc avoir await au lieu de async est un peu bizarre, pour moi.

@phaazon Non, pas à la place, mais en plus. Exemple complet:

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

s'il est valide, alors il sera possible d'implémenter la méthode await pour une utilisation dans une chaîne:

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

À mon avis, peu importe si postfix wait "se sent" trop magique et pas familier à la langue. Si nous adoptons la syntaxe .await() la méthode .await!() alors il s'agit juste d'expliquer que Future s ont une méthode .await() ou .await!() que vous pouvez également utiliser pour les attendre dans les cas qui ont du sens. Ce n'est pas si difficile à comprendre si vous passez 5 minutes à y réfléchir, même si vous ne l'avez jamais vu auparavant.

De plus, si nous utilisons le mot-clé prefix, qu'est-ce qui nous empêche d'écrire une caisse avec ce qui suit (pseudocode incomplet car je n'ai pas l'infrastructure pour traiter les détails en ce moment):

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

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

De cette façon, nous pouvons obtenir le suffixe attendre si nous le voulons. Je veux dire, même si cela n'est pas possible et que nous devons implémenter postfix comme par magie par le compilateur, nous pouvons toujours utiliser quelque chose comme l'exemple ci-dessus pour expliquer comment cela fonctionne principalement. Ce ne serait pas difficile à apprendre.

@ivandardi J'ai pensé à la même chose. Mais cette définition

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

ne dit pas que la fonction peut être appelée dans async -context uniquement.

@XX Ouais, comme je l'ai dit, ignorez le pseudocode cassé: PI n'a actuellement pas l'infrastructure pour rechercher comment la syntaxe et les traits corrects devraient être écrits ici: (Imaginez simplement que j'ai écrit ce qui le rend uniquement appelable en asynchrone contextes.

@XX , @ivandardi Par définition, await ne fonctionne que dans un contexte async . C'est donc illégal:

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

Ce doit être

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

Qui ne peut être appelé que dans un contexte async comme celui-ci:

await future.await()

Ce qui va à l'encontre de l'objectif général.

La seule façon de faire ce travail est de changer complètement async , ce qui est hors de la table.

@ivandardi Cette évolution de ma version:

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

`` rouille
wait fn wait (self) -> T {// Parce que async déjà pris
attendez-vous
}

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

@CryZe @XX Pourquoi me huez -vous? J'ai raison.

@XX Ce que vous essayez de faire est de modifier la signification de async et d'attendre complètement (par exemple, créez un contexte await qui est quelque peu différent d'un contexte async ). Je ne pense pas qu'il obtiendrait beaucoup de soutien de la part des développeurs de langage

@EyeOfPython Cette définition ne fonctionne pas comme prévu:

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

Plus probable:

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

Je vote contre parce que vous avez ignoré qu'il s'agit d'une pseudo syntaxe hypothétique. Essentiellement, leur argument est que Rust pourrait ajouter un trait spécial comme Drop et Copy que le langage comprend, ce qui ajoute la possibilité d'appeler .await () sur le type qui interagit ensuite avec le type comme le ferait un mot-clé await. De plus, le vote est de toute façon considéré comme non pertinent pour les RFC, car il s'agit de trouver une solution objective, et non basée sur des sentiments subjectifs tels que visualisés par des votes positifs / négatifs.

Je ne comprends pas ce que ce code devrait faire. Mais cela n'a rien à voir avec le fonctionnement actuel de l'attente asynchrone. Si cela vous tient à cœur, veuillez le garder en dehors de ce fil et rédigez un RFC dédié pour cela.

@CryZe Ce type est disponible. Il s'appelle Future. Et il a été convenu il y a longtemps que async / await est un mécanisme qui vise UNIQUEMENT à transformer le code async. Il ne s'agit pas d'une notation de liaison générale.

@ivandari votre postfix attend n'attend pas mais crée un nouvel avenir. C'est essentiellement une fonction d'identité. Vous ne pouvez pas attendre implicitement, car ce n'est pas une fonction normale.

@ Matthias247 Ce qu'ils essayaient de suggérer, c'était un moyen pour que postfix wait ait la syntaxe d'un appel de méthode. De cette façon, Rust n'a pas besoin d'introduire de nouveaux symboles arbitraires tels que #, @ ou ... comme nous l'avons fait avec le? opérateur, et ont toujours l'air assez naturel:

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

L'idée serait donc d'avoir en quelque sorte wait être un type spécial de méthode sur le trait Future que le compilateur voit de la même manière qu'un mot-clé await normal (que vous n'avez pas du tout besoin d'avoir dans ce cas) et de transformer l'async code dans un générateur à partir de là. Ensuite, vous avez le bon flux de contrôle logique de gauche à droite au lieu de gauche droite gauche avec await some_future()? et vous n'avez pas besoin d'introduire de nouveaux symboles étranges.

(Donc tl; dr: cela ressemble à un appel de méthode mais est en fait juste postfix wait)

Quelque chose que je n'ai pas vu mentionné explicitement en considération d'une syntaxe postfix est la prévalence des combinateurs d'erreur et d'options. C'est l'endroit où rust est le plus différent de tous les langages qui attendent - au lieu des retraits automatiques, nous avons .map_err()? .

En particulier des choses comme:

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

est moins lisible que (je me fiche de la syntaxe de postfix considérée):

let parsed = reqwest::get(..)
    .send() await
    .map_err(add_context)?
    .json() await
    .map_err(add_context)?;

_ même si_ ce formatage par défaut a plus de lignes que l'approche à variables multiples. Les expressions qui n'ont pas plusieurs attentes semblent devoir prendre moins d'espace vertical avec postfix-await.

Je n'écris pas de code asynchrone pour le moment, donc je parle par ignorance, désolé d'accumuler - si ce n'est pas un problème dans la pratique, alors c'est excellent. Je ne veux tout simplement pas que la gestion des erreurs appropriée soit rendue moins ergonomique juste pour être plus similaire à d'autres langages qui traitent les erreurs différemment.

Oui, c'est ce que je veux dire. Ce serait le meilleur des deux mondes, permettant aux gens de choisir d'utiliser le préfixe ou le suffixe en fonction de la situation.

@XX Si cela ne peut pas être exprimé dans le code utilisateur, alors nous devrons recourir au compilateur pour implémenter cette fonctionnalité. Mais en termes de compréhension du fonctionnement du suffixe, l'explication que j'ai publiée précédemment fonctionne toujours, même si ce n'est qu'une compréhension superficielle.

@CryZe , @XX ,

À première vue, il se détend, mais ne correspond absolument pas à la philosophie de Rust. Même la méthode sort sur les itérateurs ne retourne pas self et rompt la chaîne d'appels de méthode pour rendre l'allocation explicite. Mais vous vous attendez à ce que quelque chose de non moins impactant soit implémenté de manière complètement implicite, impossible à distinguer des appels de fonction normaux. Il n'y a aucune chance à l'OMI.

Oui, c'est ce que je veux dire. Ce serait le meilleur des deux mondes, permettant aux gens de choisir d'utiliser le préfixe ou le suffixe en fonction de la situation.

Pourquoi cela devrait-il être un objectif? Rust ne prend pas en charge cela pour tous les autres flux de contrôle (par exemple, break , continue , if , etc. Il n'y a pas de raison particulière, à part "cela pourrait paraître plus agréable à son avis particulier.

En général, je voudrais rappeler à tout le monde que await est assez spécial et n'est pas une invocation de méthode normale:

  • Les débogueurs peuvent se comporter de manière étrange lorsqu'ils passent au-dessus des attentes, car la pile peut se dérouler et se rétablir. Autant que je me souvienne, il a fallu beaucoup de temps pour que le débogage asynchrone fonctionne en C # et Javascript. Et ceux-ci ont des équipes rémunérées qui travaillent sur des débogueurs!
  • Les objets locaux qui ne traversent pas les points d'attente peuvent être stockés sur la vraie pile du système d'exploitation. Ceux qui ne le sont pas doivent être déplacés dans le futur généré, ce qui à la fin fera une différence dans la mémoire de tas requise (c'est là que le futur vivra).
  • Les emprunts sur les points d'attente sont la raison pour laquelle les futurs générés doivent être de !Unpin , et causent beaucoup d'inconvénients avec certains des combinateurs et mécanismes existants. Il pourrait être possible de générer des contrats à terme Unpin à partir async méthodes await sont invisibles, cela ne se produira jamais.
  • Il peut y avoir d'autres problèmes inattendus de vérification d'emprunt, qui ne sont pas couverts par l'état actuel de async / await.

@Pzixel @lnicola

Les exemples que vous donnez pour C # sont absolument impératifs, et rien de tel que les longues chaînes de style fonctionnel que nous voyons souvent dans Rust. Cette syntaxe LINQ n'est qu'un DSL et ne ressemble en rien à foo().bar().x().wih_boo(x).camboom().space_flight(); J'ai un peu cherché sur Google, et les exemples de code C # semblent 100% impératifs, tout comme la plupart des langages de programmation populaires. C'est pourquoi nous ne pouvons pas simplement prendre ce que la langue X a fait, car ce n'est tout simplement pas la même chose.

La notation de préfixe IMO s'intègre parfaitement dans le style impératif de codage. Mais Rust prend en charge les deux styles.

@skade

Je ne suis pas d'accord avec certains de vos commentaires (et d'autres) sur les problèmes de style fonctionnel. Pour être bref, admettons simplement qu'il y a des gens qui ont une forte préférence pour l'un ou l'autre.

@ Matthias247 Non, il y a une raison particulière autre que sa beauté. C'est parce que Rust encourage le chaînage. Et vous pourriez dire "oh, les autres mécanismes de contrôle de flux n'ont pas de syntaxe postfix, alors pourquoi attendre devrait-il être spécial?". Eh bien, c'est une mauvaise évaluation. Nous avons un flux de contrôle postfix. Ils sont simplement internes aux méthodes que nous appelons. Option::unwrap_or est comme faire une déclaration de correspondance de suffixe. Iterator::filter est comme faire une instruction if avec un suffixe. Ce n'est pas parce que le flux de contrôle ne fait pas partie de la syntaxe de chaînage elle-même que nous n'avons pas déjà de flux de contrôle postfix. De ce point de vue, l'ajout d'un suffixe await serait en fait cohérent avec ce que nous avons. Dans ce cas, nous pourrions même avoir quelque chose de plus similaire au fonctionnement de Iterator et au lieu d'avoir juste un suffixe brut en attente, nous pourrions avoir des combinateurs d'attente, comme Future::await_or . Quoi qu'il en soit, avoir postfix en attente n'est pas qu'une question d'apparence - c'est une question de fonctionnalité et de qualité de vie. Sinon, nous utiliserions toujours la macro try!() , non?

@dpc

Les exemples que vous donnez pour C # sont absolument impératifs, et rien de tel que les longues chaînes de style fonctionnel que nous voyons souvent dans Rust. Cette syntaxe LINQ n'est qu'un DSL et ne ressemble en rien à foo (). Bar (). X (). Wih_boo (x) .camboom (). Space_flight (); J'ai un peu cherché sur Google et les exemples de code C # semblent 100% impératifs, tout comme la plupart des langages de programmation populaires. C'est pourquoi nous ne pouvons pas simplement prendre ce que la langue X a fait, car ce n'est tout simplement pas la même chose.

Ce n'est pas vrai (vous pouvez vérifier n'importe quel framework, par exemple polly ), mais je ne discuterai pas à ce sujet. Je vois autant de code enchaîné dans les deux langues. Cependant, il y a en fait une chose qui rend tout différent. Et ça s'appelle Try trait. C # n'a rien de similaire, donc il ne suppose rien au-delà de await point. Si vous obtenez une exception, elle sera automatiquement encapsulée et déclenchée pour l'appelant.

Ce n'est pas le cas pour Rust, où vous devez utiliser manuellement l'opérateur ? .

Si vous devez avoir quelque chose au-delà du point await , vous devez soit créer un opérateur "combiné" await? , étendre les règles de langage, etc. syntaxe un peu étrangère, mais qui s'en soucie?).

Je vois donc actuellement postfix comme une solution plus viable. La seule suggestion que j'ai ici devrait être un mot-clé dédié séparé par des espaces, et non await() ou await!() .

Je pense que j'ai trouvé une bonne raison justifiant une méthode .await () normale. Si vous y réfléchissez, ce n'est pas différent de toute autre façon de bloquer. Vous appelez la méthode .await (), l'exécution du thread (éventuellement vert) est parquée et à un moment donné, le runtime en cours d'exécution reprend alors l'exécution du thread à un moment donné. Donc, à toutes fins utiles, que vous ayez un canal Mutex ou std comme ceci:

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

ou un futur comme ça:

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

n'est pas différent. Ils bloquent tous les deux l'exécution à leur recv () / await () respectif et ils enchaînent tous les deux de la même manière. La seule différence est que await () s'exécute probablement sur un exécuteur différent qui n'est pas le thread lourd du système d'exploitation (mais cela peut très bien être dans le cas d'un exécuteur à thread unique). Ainsi, le fait de décourager de fortes quantités de chaînage affecte les deux exactement de la même manière et devrait probablement conduire à une véritable peluche coupante.

Cependant, mon point ici est que de la personne qui écrit le point de vue de ce code, il n'y a pas beaucoup de différence entre .recv () ou .await (), les deux sont des méthodes qui bloquent l'exécution et retournent une fois le résultat disponible. . Donc, à toutes fins utiles, cela peut être à peu près une méthode normale et ne nécessite pas un mot-clé complet. Nous n'avons pas non plus de mot clé recv ou lock pour le canal Mutex et std.

Cependant rustc veut évidemment transformer tout le code en générateur. Mais est-ce réellement nécessaire sémantiquement du point de vue du langage? Je suis à peu près sûr que l'on pourrait écrire un compilateur alternatif à rustc qui implémente en attente via le blocage d'un thread OS réel (entrer dans un fn async aurait besoin de générer un thread, puis attendre bloquerait ce thread). Bien que ce soit une implémentation extrêmement naïve et lente, elle se comporterait sémantiquement exactement de la même manière. Donc, le fait que rustc réel se transforme en un générateur n'est pas nécessaire sémantiquement. Donc, je dirais en fait que rustc transformer l'appel à la méthode .await () en un générateur peut être vu comme un détail d'implémentation optimisant de rustc. De cette façon, vous pouvez justifier que .await () soit une méthode complète et non un mot-clé complet, mais aussi que rustc transforme le tout en générateur.

@CryZe contrairement à .recv() nous pouvons interrompre en toute sécurité await depuis n'importe quel autre endroit du programme, puis le code à côté de await ne sera pas exécuté. C'est une grande différence et c'est la raison la plus précieuse pour laquelle nous ne devrions pas faire await implicitement.

@ I60R Ce n'est pas moins explicite qu'attendre comme mot clé. Vous pouvez toujours le mettre en évidence de la même manière dans l'IDE si vous le souhaitez. Il déplace simplement le mot-clé à la position de suffixe, où je dirais qu'il est en fait moins facile à manquer (car il est exactement dans la position où l'exécution s'arrête).

Idée folle: attente implicite . Je suis passé à un fil de discussion séparé, car celui-ci est déjà trop occupé avec la discussion postfix vs préfixe.

@CryZe

Je pense que j'ai trouvé une bonne raison justifiant une méthode .await () normale. Si vous y réfléchissez, ce n'est pas différent de toute autre façon de bloquer.

Veuillez lire à nouveau https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515

@ Matthias247 Ce sont de très bons points, il semble que je les ai manqués.

Tout ce discours sur la transformation de await en une fonction, un membre ou autrement implicite est fondamentalement invalide. Ce n'est pas une action, c'est une transformation.

Le compilateur, à un niveau élevé, que ce soit via un mot-clé ou une macro, transforme les expressions d'attente en expressions de rendement de générateur spéciales, sur place, en tenant compte des emprunts et autres choses en cours de route.

Cela devrait être explicite.

Il doit être soit aussi apparent que possible en tant que mot-clé, soit générer évidemment du code avec une macro.

Les méthodes magiques, les traits, les membres, etc. sont trop faciles à mal interpréter et à mal comprendre, en particulier pour les nouveaux utilisateurs.

Le préfixe mot-clé await ou le préfixe macro await semblent être le moyen le plus acceptable de le faire, ce qui est logique car de nombreux autres langages le font de cette façon. Nous n'avons pas besoin d'être spéciaux pour réussir.

@novacrazy

Les méthodes magiques, les traits, les membres, etc. sont trop faciles à mal interpréter et à mal comprendre, en particulier pour les nouveaux utilisateurs.

Pouvez-vous développer? Plus précisément sur la variante .await!() , si possible.

Avoir un foo.await!() n'est pas difficile à lire, à mon avis, SURTOUT avec une coloration syntaxique appropriée, qui ne devrait pas être ignorée.

Vous en comprenez mal le sens? Ce que cela fait est essentiellement ce qui suit (ignorez les types mystakes):

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

AKA await this_foo et this_foo.await!() sont exactement égaux. Qu'est-ce qui est facile à mal comprendre à ce sujet?

Et sur le thème des nouveaux utilisateurs: de nouveaux utilisateurs vers quoi? Programmation en général, ou nouveaux utilisateurs de Rust en tant que langage mais avec une formation en langage de programmation? Parce que si c'est le premier, je doute qu'ils se lancent directement dans la programmation asynchrone. Et si c'est le dernier, alors il est plus facile d'expliquer la sémantique de postfix wait (comme expliqué ci-dessus) sans confusion.

Alternativement, si seul le préfixe Wait est ajouté, rien de ce que je sache n'interrompt la création d'un programme qui prend en entrée le code Rust de la forme

foo.bar().baz().quux().await!().melo().await!()

et le transforme en

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() est bien dans certains cas, et fonctionnerait probablement avec await!(...) comme:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

Cependant, les macros de méthode postfix n'existent pas pour le moment et peuvent ne jamais exister.

Si nous pensons que c'est une possibilité dans le futur, nous devrions aller avec la macro await!(...) pour l'instant et simplement ajouter le suffixe dans le futur quand cela sera implémenté.

L'idéal serait d'avoir les deux macros, mais s'il n'y a aucune intention d'implémenter des macros postfixes à l'avenir, le mot-clé préfixe await est probablement le meilleur pari.

@novacrazy Je peux être d'accord avec cela et c'est ma proposition originale. Nous devrions ajouter await!() pour l'instant et trouver un formulaire avec suffixe au fur et à mesure. Discutez aussi potentiellement de la possibilité de macros postfixes, et si nous pouvions ad-hoc un postfix .await!() dans le langage avant d'avoir le support complet des macros postfix dans le langage. Un peu comme ce qui s'est passé avec ? et le trait Try : il a d'abord été ajouté en tant que cas spécial et ensuite il a été étendu à un cas plus général. La seule chose à laquelle nous devons faire attention lorsque nous décidons est à quoi ressemblerait la syntaxe générale des macros postfixes, ce qui pourrait mériter une discussion séparée.

Le préfixe mot-clé await ou le préfixe macro await semblent être le moyen le plus acceptable de le faire, ce qui est logique car de nombreux autres langages le font de cette façon.

C'est évidemment possible, mais je ne me souviens que de deux arguments pour cela, et je ne les trouve pas convaincants:

  • C'est comme ça que les autres langues le font

    • C'est bien, mais nous faisons déjà les choses différemment, comme ne pas courir à moins que - poll ed au lieu de courir jusqu'au premier - await , car la rouille est fondamentalement différente Langue

  • Les gens aiment voir await au début de la ligne

    • Mais ce n'est pas toujours là, donc si c'est le seul endroit où l'on regarde on aura des problèmes

    • Il est tout aussi facile de voir les await s dans foo(aFuture.await, bFuture.await) que s'ils étaient préfixes

    • Dans rust, on scanne déjà la _end_ de la ligne pour ? si vous regardez le flux de contrôle

Ai-je oublié quelque chose?

Si le débat était "meh, ils sont tous à peu près pareils", je serais tout à fait d'accord avec "bien, si nous ne nous soucions pas vraiment, nous ferions aussi bien de faire ce que tout le monde fait". Mais je ne pense pas que ce soit là où nous en sommes.

@scottmcm Oui, "meh, ils sont tous à peu près

Nous devrions donc trouver un équilibre décent entre lisibilité, familiarité, maintenabilité et performances. Par conséquent, ma déclaration dans mon dernier commentaire est vraie, avec la macro await!() si nous avons l'intention d'ajouter des macros de méthode postfix ( .await!() ) à l'avenir, ou simplement un mot-clé de préfixe ennuyeux await autrement.

Je dis ennuyeux, car ennuyeux c'est bien. Nous voulons garder notre esprit loin de la syntaxe elle-même lors de l'écriture de code avec ceux-ci.

Si f.await() ne serait pas une bonne idée, alors je préfère la syntaxe du préfixe.

  1. En tant qu'utilisateur, j'espère que le langage que j'utilise n'a que quelques règles de syntaxe, et en utilisant ces règles, je peux en déduire de manière fiable ce qu'il pourrait faire. PAS d' exceptions ici et là. Dans Rust, async est au début, await au début ne sera pas une exception. Cependant, f await , le formulaire de mot-clé de publication serait. f.await ressemble à un accès aux champs, une exception . f.await!() macro postfix n'est jamais apparue dans le langage, et je ne sais pas dans quels autres cas elle serait bonne, une exception . Nous n'avons pas de réponse sur la manière dont ces syntaxes deviendraient des règles et non des exceptions ponctuelles .

  2. Lorsqu'une exception est faite, j'espère que cela aura un sens intuitif. Prenez ? comme exemple, ce qui peut être considéré comme une exception car peu fréquent dans d'autres langues. f()?.map() lit presque comme compute f () et ce résultat est-il bon? Le ? ici s'explique. Mais pour f await je demande pourquoi c'est postfix ?, pour f.await je demande est await un champ, f.await!() Je demande pourquoi la macro apparaît dans cette position? Ils ne me donnent pas un sens convaincant / intuitif, du moins à première vue.

  3. En prolongeant le premier point, Rust aimerait le plus être un langage système. Les principaux acteurs ici, C / C ++ / Go / Java sont tous quelque peu impératifs. Je crois aussi que la plupart des gars commencent leur carrière avec un langage impératif, C / Python / Java et non Haskell, etc. être très fonctionnel mais ne pas avoir un sentiment impératif familier.

  4. Je pense qu'il n'y a rien de mal à diviser une chaîne et à l'écrire en plusieurs lignes. Ce ne sera pas verbeux. C'est juste explicite .

En voyant comment la discussion se déplace continuellement d'une direction à l'autre (préfixe vs postfixe), j'ai développé une forte opinion selon laquelle il y a quelque chose qui ne va pas avec le mot clé await et nous devons nous en éloigner complètement. Au lieu de cela, je propose la syntaxe suivante qui, je pense, sera un très bon compromis parmi toutes les opinions publiées dans le fil actuel:

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

Lors de la première étape, nous l'implémenterions comme un opérateur de préfixe régulier sans aucune erreur de gestion de superstructures spécifiques:

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

Lors de la deuxième étape, nous implémenterons une syntaxe d'opérateur de préfixe différé qui permettrait également une gestion correcte des erreurs.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

C'est tout.


Voyons maintenant quelques exemples supplémentaires qui fournissent plus de contexte:


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

Il y a caché sous la version spoiler avec la coloration syntaxique activée


// 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, cette syntaxe est meilleure que les alternatives dans tous les aspects:

✓ Cohérence: semble très organique à l'intérieur du code Rust et ne nécessite pas de style de code de rupture
✓ Composabilité: s'intègre bien avec d'autres fonctionnalités de Rust comme le chaînage et la gestion des erreurs
✓ Simplicité: soit courte, descriptive, facile à comprendre et facile à utiliser
✓ Réutilisabilité: la syntaxe d'opérateur de préfixe différé serait également utile dans d'autres contextes
✓ Documentation: implique que les distributions côté appel circulent quelque part et attendent jusqu'à ce qu'elles reviennent
✓ Familiarité: fournit un modèle déjà familier mais le fait avec moins de compromis
✓ Lisibilité: se lit comme un anglais simple et ne déforme pas le sens des mots
✓ Visibilité: sa position rend très difficile d'être déguisé quelque part dans le code
✓ Accessibilité: pourrait être très facilement recherchée sur Google
✓ Bien testé: le golang est populaire de nos jours avec une syntaxe d'apparence similaire
✓ Surprises: il est difficile de mal comprendre et d'abuser de cette syntaxe
✓ Gratification: après une petite quantité d'apprentissage, chaque utilisateur sera satisfait du résultat


Edit: comme @ivandardi l'a souligné dans le commentaire ci-dessous, certaines choses doivent être clarifiées:

1. Oui, cette syntaxe est un peu difficile à analyser, mais en même temps, il est impossible d'inventer ici une syntaxe qui n'aurait aucun problème de lisibilité. go syntaxe await , et en position différée, l'OMI est plus lisible que postfix await par exemple:

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

où pas await semble ambigu et a une associativité différente de celle de tous les mots-clés existants, mais pourrait également devenir un bloqueur si nous décidons d'implémenter des blocs await dans le futur.

2. Il faudra créer un RFC, implémenter et stabiliser la syntaxe "opérateur de préfixe différé". Cette syntaxe ne serait pas spécifique au code asynchrone, mais son utilisation en asynchrone en serait la principale motivation.

3. Dans la syntaxe "opérateur de préfixe différé", le mot-clé est important car:

  • Le mot clé long augmenterait l'espace entre la variable et la méthode, ce qui est mauvais pour la lisibilité
  • Le mot clé long est un choix étrange pour l'opérateur de préfixe
  • Le mot clé long est inutilement détaillé lorsque nous voulons que le code asynchrone ressemble à une synchronisation

Quoi qu'il en soit, il est tout à fait possible de commencer à utiliser await au lieu de go , puis de le renommer dans l'édition 2022. À ce moment-là, il est très probable que différents mots clés ou opérateurs seront inventés. Et nous pouvons même décider qu'aucun changement de nom n'est requis du tout. J'ai trouvé que await était également lisible.

Il y a caché sous la version spoiler avec await utilisé au lieu 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()?;


Si vous êtes ici pour voter contre, assurez-vous:

  • que vous l'avez bien compris, car je ne suis peut-être pas le meilleur orateur pour expliquer de nouvelles choses
  • que votre opinion n'est pas biaisée, car il y a beaucoup de préjugés d'où provient cette syntaxe
  • que vous mettrez une raison valable ci-dessous, car les votes négatifs silencieux sont extrêmement toxiques
  • que votre raison ne concerne pas les sentiments immédiats, puisque j'ai essayé de concevoir une fonctionnalité à long terme ici

@ I60R

J'ai quelques reproches à ce sujet.

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

C'est un peu difficile à analyser. À première vue, vous penseriez que nous ferions une correspondance sur db.go , mais il y a quelque chose de plus à cela et vous dites "oooh, ok, load est une méthode de db". OMI cette syntaxe ne fonctionnera principalement pas car les méthodes doivent toujours être proches de l'objet auquel elles appartiennent, sans espace ni interruption de mot-clé entre elles.

Deuxièmement, votre proposition nécessite de définir ce qu'est la "syntaxe d'opérateur de préfixe différé" et éventuellement de la généraliser d'abord dans le langage. Sinon, ce sera quelque chose uniquement utilisé pour le code asynchrone, et nous revenons à la discussion sur "pourquoi pas les macros postfix?" aussi.

Troisièmement, je ne pense pas que le mot-clé compte du tout. Cependant, nous devrions privilégier await car le mot-clé a été réservé pour l'édition 2018, alors que le mot-clé go n'a pas et devrait faire l'objet d'une recherche crates.io plus approfondie avant d'être proposé.

En voyant comment la discussion se déplace continuellement d'une direction à une autre (préfixe vs postfix), j'ai développé une forte opinion selon laquelle il y a quelque chose qui ne va pas avec le mot-clé d'attente et que nous devons nous en éloigner complètement.

Nous pourrions avoir une attente (semi-) implicite et tout irait bien, mais ce sera difficile à vendre dans la communauté Rust, même en dépit du fait que tout l'intérêt des fonctions asynchrones est de cacher les détails de l'implémentation et de leur donner un aspect juste comme bloquer io one.

@dpc Je veux dire, la meilleure façon dont je peux penser que cela fonctionne comme un compromis entre l'attente semi-implicite et le désir d'avoir des sections de code explicitement attendues est quelque chose de similaire à unsafe .

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

Où tout ce qui se trouve à l'intérieur du bloc await serait automatiquement attendu. Cela résout le besoin d'attendre postfix pour le chaînage, puisque vous pouvez simplement attendre la chaîne entière à la place. Mais en même temps, je ne sais pas si cette forme serait souhaitable. Peut-être que quelqu'un d'autre peut trouver une très bonne raison contre cela.

@dpc Le but des fonctions asynchrones n'est pas de cacher les détails, c'est de les rendre plus accessibles et plus faciles à utiliser. Nous ne devons jamais cacher des opérations potentiellement coûteuses et tirer profit des futurs explicites lorsque cela est possible.

Personne ne confond les fonctions asynchrones avec les fonctions synchrones, dans aucune langue. Il s'agit simplement d'organiser des séquences d'opérations sous une forme plus lisible et maintenable, plutôt que d'écrire à la main une machine à états.

De plus, les attentes implicites comme le montre l'exemple de Future combinateurs Future et attendre dessus, ce serait très inefficace.

Explicite, familier, lisible, maintenable et performant: mot-clé préfixe await ou macro await!() , avec potentiellement une macro postfixe .await!() dans le futur.

@novacrazy Même si la syntaxe idéale était choisie, je voudrais toujours utiliser des futurs et des combinateurs personnalisés dans certaines situations. L'idée que ceux-ci pourraient être légèrement obsolètes me fait remettre en question toute la direction de Rust.

Bien sûr, vous pouvez continuer à utiliser des combinateurs, rien n'est obsolète.

C'est la même situation que ? : le nouveau code utilisera généralement ? (car c'est mieux que les combinateurs), mais les combinateurs Option / Résultat peuvent toujours être utilisés (et les plus funk comme or_else sont encore régulièrement utilisés).

En particulier, async / await peut complètement remplacer map et and_then , mais les combinateurs Future plus funk peuvent (et seront) toujours utilisés.

Les exemples que vous donnez paraissent toujours terribles par rapport aux combinateurs,

Je ne pense pas, je pense qu'ils sont beaucoup plus clairs, car ils utilisent les fonctionnalités standard de Rust.

La clarté concerne rarement le nombre de personnages, mais elle a tout à voir avec des concepts mentaux.

Avec le suffixe await c'est encore mieux:

some_op().await?
    .another_op().await?
    .final_op().await

Vous pouvez comparer cela à votre original:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

et avec la surcharge du générateur sera probablement un peu plus lent et produira plus de code machine.

Pourquoi prétendez-vous qu'il y aura des frais supplémentaires dus à async / await? Les générateurs et async / await sont spécialement conçus pour être sans coût et fortement optimisés.

En particulier, async / await se compile dans une machine à états fortement optimisée, tout comme les combinateurs Future.

plutôt que de créer ces machines d'état de spaghetti générateur sous le capot.

Les combinateurs du futur créent également une "machine à états spaghetti" sous le capot, ce pour quoi ils ont été conçus.

Ils ne sont pas fondamentalement différents de async / await.

[Les futurs combinateurs] ont le potentiel de compiler vers un code plus efficace.

Veuillez arrêter de diffuser de la désinformation. En particulier, async / await a la possibilité d'être plus rapide que les combinateurs Future.

S'il s'avère que les utilisateurs ne sont pas satisfaits d'un mot-clé de préfixe, il peut être modifié dans une édition 2019/2020.

Comme d'autres l'ont dit, les éditions ne sont pas une chose arbitraire que nous faisons juste quand nous en avons envie, elles sont spécifiquement conçues pour ne se produire que toutes les quelques années (au plus tôt).

Et comme @Centril l'a souligné, trouver une mauvaise syntaxe juste pour la remplacer plus tard est une façon très inefficace de faire les choses.

Le rythme des discussions est assez élevé en ce moment; donc dans un effort pour ralentir les choses et permettre aux gens de se rattraper, j'ai verrouillé le problème temporairement . Il sera déverrouillé dans un délai d'un jour. À ce moment-là, essayez d'être aussi constructif que possible pour aller de l'avant.

Débloquer le problème un jour plus tard ... Veuillez envisager de ne pas faire de remarques déjà faites et de garder les commentaires sur le sujet (par exemple, ce n'est pas le lieu de considérer l'attente implicite ou d'autres choses hors de portée ..).

Comme cela a été un long fil, soulignons certains des commentaires les plus intéressants qui y sont enfouis.

@mehcode nous a fourni un code réel complet avec await dans les positions de préfixe et de suffixe: https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

@Centril a soutenu de manière convaincante que stabiliser await!(expr) revient à ajouter sciemment de la dette technique: https://github.com/rust-lang/rust/issues/57640#issuecomment -455806584

@valff nous a rappelé que la syntaxe du mot-clé expr await postfix correspondrait le plus parfaitement à l' attribution de type généralisée : https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@quodlibetor a souligné que l'effet syntaxique des combinateurs d'erreur et d'option est unique à Rust et plaide en faveur de la syntaxe postfix: https://github.com/rust-lang/rust/issues/57640#issuecomment -456143523

En fin de compte, l'équipe lang va devoir passer un appel ici. Dans l'intérêt d'identifier un terrain d'entente qui pourrait conduire à un consensus, il serait peut-être utile de résumer les positions déclarées des membres de l'équipe lang sur la question clé de la syntaxe postfix:

  • @Centril a exprimé son soutien à la syntaxe postfix et a exploré un certain nombre de variations dans ce ticket. Centril ne veut particulièrement await!(expr) .

  • @cramertj a exprimé son soutien pour la syntaxe postfix et spécifiquement pour la syntaxe du mot clé expr await postfix.

  • @joshtriplett a exprimé son soutien à la syntaxe postfix et a suggéré que nous devrions également fournir une version de préfixe.

  • @scottmcm a exprimé son soutien à la syntaxe postfix.

  • @withoutboats ne veut pas stabiliser une syntaxe de macro. Tout en étant préoccupé par l'épuisement de notre "budget de non-familiarité", withoutboats considère que la syntaxe du mot-clé expr await postfix a "une vraie chance".

  • @aturon , @eddyb , @nikomatsakis et @pnkfelix n'ont encore exprimé aucune position concernant la syntaxe dans les numéros 57640 ou # 50547.

Donc, les choses dont nous avons besoin pour trouver une réponse oui / non:

  • Devrions-nous avoir une syntaxe de préfixe?
  • Devrions-nous avoir une syntaxe postfix?
  • Devrions-nous avoir une syntaxe de préfixe maintenant et comprendre la syntaxe de postfix plus tard?

Si nous choisissons la syntaxe de préfixe, il y a deux principaux prétendants que je pense que les gens acceptent le plus: Utile et Évident, comme on le voit dans ce commentaire . Les arguments contre await!() sont assez forts, donc je considère que cela est exclu. Nous devons donc déterminer si nous voulons une syntaxe utile ou évidente. À mon avis, nous devrions opter pour la syntaxe qui utilise le moins de parenthèses en général.

Et en ce qui concerne la syntaxe postfix, c'est plus compliqué. Il existe également de solides arguments pour le mot-clé postfix await avec espace. Mais cela dépend beaucoup de la façon dont le code est formaté. Si le code est mal formaté, le mot-clé suffixe await avec espace peut sembler vraiment mauvais. Donc, d'abord, nous devrions supposer que tout le code Rust sera formaté correctement avec rustfmt, et que rustfmt divisera toujours les attentes chaînées en différentes lignes, même si elles tiendraient sur une seule ligne. Si nous pouvons faire cela, alors cette syntaxe est très correcte, car elle résout le problème de la lisibilité et de la confusion avec les espaces au milieu d'une chaîne sur une seule ligne.

Et enfin, si nous utilisons à la fois le préfixe et le suffixe, nous devons comprendre et noter la sémantique des deux syntaxes pour voir comment elles interagissent l'une avec l'autre et comment l'une serait convertie en l'autre. Il devrait être possible de le faire, car choisir entre postfix ou prefix await ne devrait pas changer la façon dont le code est exécuté.

Pour clarifier mon processus de pensée, et comment j'aborde et évalue les propositions de syntaxe de surface. await ,
voici les objectifs que j'ai (sans aucun ordre d'importance):

  1. await devrait rester un mot-clé pour permettre la conception future du langage.
  2. La syntaxe devrait sembler de première classe.
  3. L'attente doit être chaînable pour bien composer avec ? et les méthodes en général puisqu'elles sont répandues dans Rust. Il ne faut pas être obligé de créer des liaisons temporaires let qui peuvent ne pas être des subdivisions significatives.
  4. Il devrait être facile de grep pour attendre des points.
  5. Il devrait être possible de voir les points d'attente en un coup d'œil.
  6. La priorité de la syntaxe doit être intuitive.
  7. La syntaxe doit bien s'adapter aux IDE et à la mémoire musculaire.
  8. La syntaxe doit être facile à apprendre.
  9. La syntaxe doit être ergonomique à écrire.

Après avoir défini (et probablement oublié ...) certains de mes objectifs, voici un résumé et mon évaluation de quelques propositions par rapport à ceux-ci:

  1. En conservant await comme mot-clé, il est difficile d'utiliser une syntaxe basée sur une macro, que ce soit await!(expr) ou expr.await!() . Pour utiliser une syntaxe de macro, await tant que macro devient soit codé en dur et non intégré à la résolution de nom (c'est- use core::await as foo; dire que await est entièrement abandonné en tant que mot clé.

  2. Je crois que les macros ont un sentiment distinctement non de première classe à leur sujet car elles sont destinées à inventer la syntaxe dans l'espace utilisateur. Bien que ce ne soit pas un point technique, l'utilisation de la macro syntaxe dans une telle construction centrale du langage donne une impression impolie. On peut soutenir que les syntaxes .await et .await() ne sont pas non plus les syntaxes de première classe; mais pas aussi de seconde classe que les macros le sentiraient.

  3. Le désir de faciliter le chaînage fait que toute syntaxe de préfixe fonctionne mal alors que les syntaxes postfixes composent naturellement avec des chaînes de méthodes et ? en particulier.

  4. La grepping est plus facile lorsque le mot-clé await est utilisé, qu'il s'agisse d'un suffixe, d'un préfixe, d'une macro, etc. Lorsqu'une syntaxe basée sur un sigil est utilisée, par exemple # , @ , ~ , le grepping devient plus difficile. La différence n'est pas grande, mais .await est légèrement plus facile à grep pour que await et await car l'un ou l'autre de ces éléments peut être inclus en tant que mots dans les commentaires alors qu'il est plus improbable pour .await .

  5. Les sigils sont probablement plus difficiles à repérer en un coup d'œil, tandis que await est plus facile à voir et surtout la syntaxe mise en évidence. Il a été suggéré que .await ou d'autres syntaxes postfixes sont plus difficiles à repérer en un coup d'œil ou que postfix await offre une mauvaise lisibilité. Cependant, à mon avis, @scottmcm note à juste titre que les futurs des résultats sont courants et que .await? aide à attirer davantage l'attention sur lui-même. De plus, Scott note que le préfixe await conduit à des phrases de chemin de jardin et que le préfixe attendre au milieu des expressions n'est pas plus lisible que postfix. Avec l'avènement de l'opérateur ? , les programmeurs Rust ont déjà besoin de scanner la fin des lignes pour rechercher le flux de contrôle .

  6. La précédence de .await et .await() est remarquable pour être complètement prévisible; ils fonctionnent comme leurs homologues d'accès sur le terrain et d'appel de méthode. Une macro de suffixe aurait vraisemblablement la même priorité qu'un appel de méthode. Pendant ce temps, le préfixe await a soit une priorité cohérente, prévisible et inutile par rapport à ? (c'est- await (expr?) dire (await expr)? dire ? ).

  7. En 2012, @nikomatsakis a noté que Simon Peyton Jones a noté une fois (p. 56) qu'il était envieux de la "puissance du point" et de la façon dont il fournit la magie de l'IDE par lequel vous pouvez restreindre la fonction ou le champ que vous voulez dire. Puisque la puissance du point existe dans de nombreux langages populaires (par exemple Java, C #, C ++, ..), cela a conduit à "atteindre le point" enraciné dans la mémoire musculaire. Pour illustrer la puissance de cette habitude, voici une capture d'écran, due à @scottmcm , de Visual Studio avec Re # er:
    Re#er intellisense

    C # n'a pas de postfix en attente. Néanmoins, cela est si utile qu'il est affiché dans une liste de saisie semi-automatique sans être une syntaxe valide. Cependant, il peut ne pas arriver à beaucoup, moi y compris, d'essayer .aw quand .await n'est pas la syntaxe de la surface. Considérez les avantages de l'expérience IDE si c'était le cas. Les syntaxes await expr et expr await n'offrent pas cet avantage.

  8. Les sigils offriraient probablement une faible familiarité. Le préfixe await a l'avantage d'être familiarisé avec C #, JS, etc. Cependant, ceux-ci ne séparent pas await et ? en opérations distinctes contrairement à Rust. Il n'est pas non plus exagéré de passer de await expr à expr.await - le changement n'est pas aussi radical que d'aller avec un sceau. De plus, en raison de la puissance des points susmentionnée, il est probable que .await sera appris en tapant expr. et en voyant await comme première option dans la fenêtre contextuelle d'auto-complétion.

  9. Les sigils sont faciles à écrire et offriraient une bonne ergonomie. Cependant, bien que .await soit plus long à taper, il est également livré avec des pouvoirs de points qui peuvent rendre l'écriture encore meilleure. Le fait de ne pas avoir à pénétrer dans des déclarations let facilite également une meilleure ergonomie. Le préfixe await , ou pire encore await!(..) , manque à la fois de pouvoirs de points et de capacités de chaînage. Quand on compare .await , .await() et .await!() , les premières offres sont les plus concises.

Comme aucune syntaxe n'est la meilleure pour atteindre tous les objectifs simultanément, un compromis doit être fait. Pour moi, la syntaxe avec le plus d'avantages et le moins d'inconvénients est .await . Notamment, il peut être enchaîné, préserve await comme mot-clé, est greppable, a des pouvoirs de points et est donc apprenable et ergonomique, a une priorité évidente et est enfin lisible (surtout avec un bon formatage et mise en évidence).

@Centril Juste pour clarifier quelques questions. Tout d'abord, je voudrais simplement confirmer que vous ne voulez que postfix attendre, non? Deuxièmement, comment aborder la dualité de .await étant un accès de champ? Serait-il correct d'avoir .await comme tel, ou devrait-on privilégier .await() avec les parenthèses de manière à impliquer qu'une sorte d'opération se produit là-bas? Et troisièmement, avec la syntaxe .await , est-ce que Wait serait le mot-clé ou simplement un identifiant "d'accès au champ"?

Tout d'abord, je voudrais simplement confirmer que vous ne voulez que postfix attendre, non?

Oui. Au moins maintenant.

Deuxièmement, comment aborder la dualité de .await étant un accès de champ?

C'est un léger inconvénient; il y a un avantage majeur dans la puissance du point. La distinction est quelque chose que je pense que les utilisateurs apprendront rapidement, d'autant plus qu'il s'agit d'un mot clé mis en évidence et que la construction sera utilisée fréquemment. De plus, comme Scott l'a noté, .await? sera le plus courant, ce qui devrait encore améliorer la situation. Il devrait également être facile d'ajouter une nouvelle entrée de mot-clé pour await dans rustdoc, comme nous l'avons fait pour fn .

Serait-il correct d'avoir .await comme tel, ou devrait-on privilégier .await() avec les parenthèses de manière à impliquer qu'une sorte d'opération se produit là-bas?

Je préfère .await sans queue () ; avoir () à la fin semble être du sel inutile dans la majorité des cas et, je suppose, rendrait les utilisateurs plus enclins à rechercher la méthode.

Et troisièmement, avec la syntaxe .await , est-ce que Wait serait le mot-clé ou juste un identifiant "field access"?

await resterait un mot-clé. Vraisemblablement, vous changeriez libsyntax de manière à ne pas commettre d'erreur lors de la rencontre de await après . , puis le représenteriez différemment dans AST ou lors de la réduction à HIR ... mais c'est surtout un détail d'implémentation .

Merci d'avoir clarifié tout cela! 👍

À ce stade, le problème clé semble être de savoir s'il faut adopter une syntaxe postfixe. Si c'est la direction, alors nous pouvons certainement nous limiter à laquelle. En lisant la salle, la plupart des partisans de la syntaxe postfix accepteraient toute syntaxe postfix raisonnable sur la syntaxe de préfixe.

Entrant dans ce fil, la syntaxe de postfix semblait être l'opprimé. Cependant, il a attiré le soutien clair de quatre membres de l'équipe lang et l'ouverture d'un cinquième (les quatre autres étant restés silencieux jusqu'à présent). De plus, il a attiré un soutien considérable de la part de la communauté plus large qui a commenté ici.

De plus, les partisans de la syntaxe postfix semblent être clairement convaincus que c'est la meilleure voie pour Rust. Il semble qu'un problème profond ou des réfutations convaincantes aux arguments avancés jusqu'à présent seraient nécessaires pour revenir sur ce soutien.

Compte tenu de cela, il semble que nous ayons besoin d'entendre @withoutboats , qui a sûrement suivi ce fil de près, et les quatre autres membres de l'équipe lang. Leurs opinions seront probablement le moteur de ce fil. S'ils ont des préoccupations, en discuter serait la priorité. S'ils sont convaincus du bien-fondé de la syntaxe postfix, alors nous pouvons passer à la recherche d'un consensus pour lequel.

Oups, je viens de remarquer que @Centril avait déjà commenté, alors je vais cacher mon message pour la propreté.

@ivandardi Compte tenu de l'objectif (1) du message, je crois comprendre que await serait toujours un mot-clé, donc ne serait jamais un accès au champ, de la même manière que loop {} n'est jamais une expression littérale struct. Et je m'attendrais à ce qu'il soit mis en évidence pour rendre le plus évident (comme https://github.com/rust-lang/rust-enhanced/issues/333 attend à faire dans Sublime).

Ma seule préoccupation est que faire en sorte que la syntaxe d'attente ressemble à un accès différent pourrait potentiellement causer de la confusion dans une base de code édition 2015 sans s'en rendre compte. (2015 est la valeur par défaut lorsqu'elle n'est pas spécifiée, ouvrant le projet de quelqu'un d'autre, etc.) Tant que l'édition 2015 a une erreur claire lorsqu'il n'y a pas await champ

Oh, et une chose que nous devrions également décider pendant que nous y sommes, c'est comment rustfmt finirait par le formater.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Utilise await - chèque
  2. Première classe - vérifier
  3. Chaînage - vérifier
  4. Grepping - vérifier
  5. Non sigil - chèque
  6. Priorité - ¹check
  7. Puissance de point - ²check
  8. Facile à apprendre - ³check
  9. Ergonomique - vérifier

¹Précédence: l'espace après await rend pas évident en position différée. Cependant, c'est exactement la même chose que sur le code suivant: client.get("https://my_api").await_send()?.await_json()? . Pour tous les anglophones, c'est encore plus naturel qu'avec toutes les autres propositions

² Puissance des points: il faudrait un support supplémentaire pour l'EDI pour déplacer await vers la gauche de l'appel de méthode après que . , ? ou ; soit tapé ensuite. Ne semble pas être trop difficile à mettre en œuvre

³Facile à apprendre: en position préfixe, il est déjà familier aux programmeurs. En position différée, ce serait évident après la stabilisation de la syntaxe


Mais le point le plus fort de cette syntaxe est qu'elle sera très cohérente:

  • Il ne peut pas être confondu avec l'accès à la propriété
  • Il a une priorité évidente avec ?
  • Il aura toujours le même formatage
  • Cela correspond au langage humain
  • Il n'est pas affecté par l'occurrence horizontale variable dans la chaîne d'appels de méthode *

* l'explication est cachée sous le spoiler


Avec la variante postfixe, il est difficile de prédire quelle fonction est async et où serait la prochaine occurrence de await sur l'axe horizontal:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

Avec une variante différée, ce serait presque toujours la même chose:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

N'y aurait-il pas un. entre l'attente et le prochain appel de méthode? Comme
get().await?.json() .

Le mercredi 23 janvier 2019, 05:06 I60R < [email protected] a écrit:

let val = attendre le futur;
let res = client.get ("https: // mon_api") .await send () ?. await json () ?;

  1. Les utilisations attendent - vérifier
  2. Première classe - vérifier
  3. Chaînage - vérifier
  4. Grepping - vérifier
  5. Non sigil - chèque
  6. Priorité - ¹check
  7. Puissance de point - ²check
  8. Facile à apprendre - ³check
  9. Ergonomique - vérifier

¹Précédence: l'espace ne le rend pas évident en position différée. Cependant c'est
exactement la même chose que sur le code suivant: client.get ("https: // my_api
") .await_send () ?. await_json () ?. Pour tous les anglophones, c'est encore plus
naturel qu'avec toutes les autres propositions

² Puissance de point: il faudrait un support supplémentaire pour que l'EDI passe à
la gauche de l'appel de méthode après. ou ? est tapé ensuite. Ne semble pas être trop
difficile à mettre en œuvre

³Facile à apprendre: en position préfixe, il est déjà familier aux programmeurs,
et en position différée, il serait évident après que la syntaxe soit

stabilisé

Mais le point le plus fort de cette syntaxe est qu'elle sera très
cohérent:

  • Il ne peut pas être confondu avec l'accès à la propriété
  • Il a une priorité évidente avec?
  • Il aura toujours le même formatage
  • Cela correspond au langage humain
  • Il n'est pas affecté par l'occurrence horizontale variable dans l'appel de méthode
    chaîne*

* l'explication est cachée sous le spoiler

Avec la variante postfix, il est difficile de prédire quelle fonction est asynchrone et
où serait la prochaine occurrence de wait sur l'axe horizontal:

let res = client.get ("https: // mon_api")

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

.short().await?;

Avec une variante différée, ce serait presque toujours la même chose:

let res = client.get ("https: // mon_api")

.await very_long_method_name(param, param, param)?

.await short()?;

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

nous devrions également décider pendant que nous y sommes, comment rustfmt finirait par le formater

C'est le domaine de https://github.com/rust-dev-tools/fmt-rfcs , donc je dirais que c'est hors sujet pour cette question. Je suis sûr qu'il y aura quelque chose qui sera compatible avec la correction et la priorité choisies.

@ I60R

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

À quoi cela ressemblerait-il pour les fonctions gratuites? Si je lis bien, c'est en fait un préfixe await .

Je pense que cela montre que nous ne devrions pas être aussi prompts à discréditer l'expérience de l'équipe du langage C #. Microsoft réfléchit beaucoup aux études d'utilisabilité, et nous prenons ici une décision basée sur une seule ligne de code et sur le principe que Rust est suffisamment spécial pour avoir une syntaxe différente de celle de tous les autres langages. Je ne suis pas d'accord avec cette prémisse - j'ai mentionné ci-dessus LINQ et les méthodes d'extension étant omniprésentes en C #.

Cependant, il peut ne pas arriver à beaucoup, moi y compris, d'essayer .aw quand .await n'est pas la syntaxe de la surface.

~ Je ne pense pas que cela arrivera à quelqu'un qui connaît les autres langues. ~ Cela mis à part, vous attendriez-vous à ce que la fenêtre contextuelle de complétion inclue à la fois les méthodes await et Future ?

[...] et nous prenons ici une décision basée sur une seule ligne de code et sur le principe que Rust est suffisamment spécial pour avoir une syntaxe différente de celle de tous les autres langages. Je ne suis pas d'accord avec cette prémisse [...]

@lnicola Je veux juste le signaler au cas où vous et d'autres l'auriez manqué (il est facile de le rater dans le déluge de commentaires): https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

Plusieurs exemples réels tirés du code de production réel.

Compte tenu du commentaire de @Centril ci - expr await et la syntaxe du champ expr.await postfix (et peut-être que la syntaxe de la méthode postfix n'est pas encore disponible).

Comparé au mot clé postfix, @Centril soutient que le champ postfix bénéficie de "la puissance du point" - que les IDE peuvent être mieux en mesure de le suggérer comme une autocomplétion - et que les instances de son utilisation peuvent être plus faciles à trouver avec grep car le point de tête fournit une clarification de l'utilisation du mot dans les commentaires.

Inversement, @cramertj a plaidé en faveur de la syntaxe des mots-clés postfix au motif qu'une telle syntaxe indique clairement qu'il ne s'agit pas d'un appel de méthode ou d'un accès à un champ. @withoutboats a soutenu que le mot-clé postfix est la plus familière des options de postfix.

En ce qui concerne "la puissance du point", nous devons considérer qu'un IDE pourrait toujours offrir await comme complétion après un point et ensuite simplement supprimer le point lorsque la complétion est sélectionnée. Un IDE suffisamment intelligent (par exemple avec RLS) pourrait même remarquer le futur et offrir await après un espace.

Le champ postfix et les options de méthode semblent offrir la plus grande surface pour les objections, en raison de l'ambiguïté, par rapport au mot clé postfix. Comme une partie de la prise en charge du champ / méthode postfix sur le mot clé postfix semble provenir d'un léger malaise avec la présence de l'espace dans le chaînage de méthodes, nous devrions examiner et nous engager avec l' observation de @valff selon laquelle le mot clé postfix ( foo.bar() await ) n'aura pas l'air plus surprenant que l'attribution de type généralisée ( foo.bar() : Result<Vec<_>, _> ). Pour les deux, le chaînage se poursuit après cet interlude séparé par des espaces.

@ivandardi , @lnicola ,

J'ai mis à jour mon commentaire récent avec un exemple manquant pour un appel de fonction gratuit. Si vous voulez plus d'exemples, vous pouvez vous référer au dernier spoiler dans mon commentaire précédent

Dans l'intérêt de débattre future.await? vs future await? ...

Quelque chose qui n'est pas trop discuté est le regroupement visuel ou la grepping d'une chaîne de méthodes.

consider ( match au lieu de await pour la coloration syntaxique)

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

par rapport à

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

Quand je scanne visuellement la première chaîne pour await j'identifie clairement et rapidement send().await? et je vois que nous attendons le résultat du send() .

Cependant, lorsque je scanne visuellement la deuxième chaîne pour await , je commence par voir await?.error_for_status() et je dois aller, non, sauvegarder, puis connecter send() await ensemble.


Oui, certains de ces mêmes arguments s'appliquent à l'attribution de type généralisée, mais ce n'est pas encore une fonctionnalité acceptée. Bien que j'aime l'attribution de type dans un sens, je pense que dans un contexte d'expression _general_ (vague, je sais), il devrait être exigé d'être enveloppé de parens. Tout cela est hors sujet pour cette discussion. Gardez également à l'esprit que le montant de await dans une base de code a un changement important en étant significativement plus élevé que le montant de l'attribution de type.

L'attribution de type généralisée lie également le type à l'expression attribuée via l'utilisation d'un opérateur distinct : . Tout comme tout autre opérateur binaire, sa lecture montre (visuellement) clairement que les deux expressions sont un seul arbre. Alors que future await n'a pas d'opérateur et pourrait facilement être confondu avec future_await par habitude de voir rarement deux noms non séparés par un opérateur sauf si le premier est un mot-clé (des exceptions s'appliquent, ce n'est pas dire que je préfère la syntaxe de préfixe, je ne le fais pas).

Comparez cela à l'anglais si vous voulez, où un trait d'union ( - ) est utilisé pour regrouper visuellement des mots qui autrement seraient facilement interprétés comme séparés.

Je pense que cela montre que nous ne devrions pas être aussi prompts à discréditer l'expérience de l'équipe du langage C #.

Je ne pense pas que «si vite» et «discréditer» soient justes ici. Je pense que la même chose se produit ici qui s'est produite dans le RFC async / wait lui-même: examiner attentivement pourquoi cela a été fait dans un sens, et déterminer si l'équilibre sort de la même manière pour nous. L'équipe C # est même mentionnée explicitement dans un:

Je pensais que l'idée de await était que vous vouliez réellement le résultat, pas l'avenir
alors peut-être que la syntaxe d'attente devrait être plus comme la syntaxe 'ref'

let future = task()
mais
let await result = task()

donc pour enchaîner vous devriez faire soit

task().chained_method(|future| { /* do something with future */ })

mais

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

Ils fonctionnent tous bien avec le chaînage, et tous ont des inconvénients qui ne sont pas de vrais champs / méthodes / macro.
Mais puisque await , en tant que mot-clé, est déjà une chose spéciale, nous n'avons pas besoin de le rendre plus spécial.
Nous devrions simplement sélectionner le plus simple, foo.await . () et !() sont redondants ici.

@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

Lorsque je scanne visuellement la première chaîne pour wait, j'identifie clairement et rapidement send (). et voyez que nous attendons le résultat de send ().

J'aime plus la version espacée, car il est beaucoup plus facile de voir où se construit l'avenir et où il est attendu. Il est beaucoup plus facile de voir ce qui se passe, à mon humble avis

image

Avec la séparation par points, cela ressemble beaucoup à un appel de méthode. La mise en évidence du code n'aidera pas beaucoup et elle n'est pas toujours disponible.

Enfin, je pense que le chaînage n'est pas le cas d'utilisation principal (sauf pour ? , mais foo await? est aussi clair que possible), et avec l'attente simple, cela devient

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

contre

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

Où ce dernier a l'air beaucoup plus maigre imo.

Donc, si nous avons réduit à future await et future.await , pouvons-nous simplement rendre le "point" magique facultatif? Pour que les gens puissent choisir celui qu'ils veulent et, surtout, arrêter les disputes et aller de l'avant!

Il n'est pas dangereux d'avoir une syntaxe facultative, Rust en a beaucoup d'exemples. le plus connu est le dernier sperator ( ; ou , ou autre, après le dernier élément, comme dans (A,B,) ) sont pour la plupart facultatifs. Je n'ai pas vu de raison valable pour laquelle nous ne pouvons pas rendre le point facultatif.

Je pense que ce serait le bon moment pour faire cela dans Nightly et laisser l'écosystème décider lequel est le meilleur pour eux. Nous pouvons avoir des lints pour appliquer le style préféré, le rend personnalisable.

Ensuite, avant d'atterrir chez Stable, nous examinons l'utilisation de la communauté et décidons si nous devons en choisir un ou simplement laisser les deux.

Je ne suis pas du tout d'accord avec l'idée que les macros ne se sentiraient pas assez de première classe pour cette fonctionnalité (je ne la considérerais pas comme une fonctionnalité essentielle en premier lieu) et je voudrais souligner à nouveau mon commentaire précédent sur await!() (peu importe si le préfixe ou le suffixe) n'est pas une "vraie macro", mais je suppose qu'à la fin, trop de gens veulent que cette fonctionnalité soit stabilisée dès que possible, plutôt que de bloquer cela sur les macros postfix et le préfixe wait n'est vraiment pas un bon ajustement pour Rust.

Dans tous les cas, verrouiller ce fil pendant une journée n'a beaucoup aidé et je vais me désabonner maintenant. N'hésitez pas à me mentionner si vous répondez directement à l'un de mes points.

Mon argument contre la syntaxe avec . est que await n'est pas un champ et donc il ne devrait pas ressembler à un champ. Même avec la coloration syntaxique et la police en gras, cela ressemblera à un champ en quelque sorte . suivi d'un mot est fortement associé à l'accès au champ.

Il n'y a aucune raison d'utiliser la syntaxe avec . pour une meilleure intégration IDE non plus, car nous serions en mesure d'autoriser n'importe quelle syntaxe différente avec l'achèvement . en utilisant quelque chose comme la fonction d'achèvement postfixe disponible dans Intellij IDEA.

Nous devons percevoir await comme un mot clé de flux de contrôle. Si nous décidons fortement d'opter pour postfix await , alors nous devrions envisager une variante sans . et je propose d'utiliser le formatage suivant pour mieux souligner les points de blocage avec une interruption possible:

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

Plus d'exemples

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

Avec coloration syntaxique

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


Cela pourrait également être une réponse au point @ivandardi sur le formatage

Je trouve que tous ces exemples pour postfix attendent d'être plus contre cela que pour cela. Les longues chaînes d'attente se sentent mal, et l'enchaînement devrait être réservé aux futurs combinateurs.

Comparez (avec la chose match as await pour la coloration syntaxique):

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

à

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

Remarquez le manque d'attente au milieu? Parce que le chaînage peut généralement être résolu par une meilleure conception d'API. Si send() retourne un futur personnalisé avec des méthodes supplémentaires, par exemple pour convertir des messages d'état non 200 en erreurs matérielles, alors il n'y a pas besoin d'attentes supplémentaires. À l'heure actuelle, en reqwest au moins, il semble fonctionner sur place et produire un simple Result plutôt qu'un nouvel avenir, d'où vient le problème.

Le chaînage attend est une forte odeur de code à mon avis, et .await ou .await() confondra tellement de nouveaux utilisateurs que ce n'est même pas drôle. .await ressemble particulièrement à quelque chose de Python ou PHP, où les accès aux membres variables peuvent par magie avoir un comportement spécial.

await devrait être une évidence, comme si on disait: "avant de continuer, nous attendons ce" non "et ensuite, nous attendons cela". Ce dernier est littéralement le combinateur and_then .

Une autre façon potentielle d'y penser est de considérer les expressions et les futurs async / await comme des itérateurs. Ils sont tous les deux paresseux, peuvent avoir de nombreux états et peuvent être terminés par la syntaxe ( for-in pour les itérateurs, await pour les futures). Nous ne proposerions pas une extension de syntaxe pour les itérateurs pour les enchaîner en tant qu'expressions régulières comme nous le sommes ici, non? Ils utilisent des combinateurs pour le chaînage, et les opérations / futures asynchrones devraient faire de même, ne se terminant qu'une seule fois. Cela fonctionne plutôt bien.

Enfin, et surtout, je veux pouvoir parcourir verticalement les premiers tirets d'une fonction et voir clairement où les opérations asynchrones se produisent. Ma vision n'est pas si belle, et avoir des attentes à la fin de la ligne ou coincé au milieu rendrait les choses tellement plus difficiles que je pourrais simplement rester avec des futurs bruts.

Tout le monde s'il vous plaît lire ce billet de blog avant de commenter.

Async / await n'est pas une question de commodité. Il ne s'agit

Il s'agit de permettre de nouvelles choses qui ne peuvent pas être faites avec des combinateurs.

Ainsi, toute suggestion de "utilisons plutôt des combinateurs" est hors sujet et devrait être discutée ailleurs.

Remarquez le manque d'attente au milieu? Parce que le chaînage peut généralement être résolu par une meilleure conception d'API.

Vous n'obtiendrez pas une jolie chaîne dans un vrai exemple asynchrone. Exemple réel:

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

Les combinateurs n'aident pas ici. L'imbrication peut être supprimée, mais ce n'est pas facile (j'ai essayé deux fois et j'ai échoué). Vous avez des tonnes de Either:A(Either:A(Either:A(...))) à la fin.

OTOH c'est facilement résolu avec async / await .

@Pauan l' a écrit avant que je termine mon message. Ainsi soit-il. Ne mentionnez pas les combinateurs, async/await ne devrait pas se ressembler. C'est bien si c'est le cas, il y a des choses plus importantes à considérer.


Le vote négatif de mon commentaire ne rendra pas les combinateurs plus pratiques.

En lisant le commentaire d'attribution de type , je me demandais à quoi cela ressemblerait combiné avec await . Compte tenu de la réservation que certaines personnes ont avec .await ou .await() comme champs de membre / accès aux méthodes déroutants, je me demande si une syntaxe générale d '"attribution" pourrait être une option, (c'est-à-dire, "donnez-moi ce que await renvoie ").

Utilisation de yield pour await pour la coloration syntaxique

let result = connection.fetch("url"):yield?.collect_async():yield?;

Combiné avec l'attribution de type, par exemple:

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

Avantages: toujours joli (IMHO), syntaxe différente de l'accès champ / méthode.
Inconvénients: conflits potentiels avec l'attribution de type (?), Plusieurs attributions théoriquement possibles:

foo():yield:yield

Edit: Il m'est venu à l'esprit que l'utilisation de 2 attributions serait plus logique dans l'exemple combiné:

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

@Pauan, il serait inapproprié de surappliquer ce billet de blog. Les combinateurs peuvent être appliqués en conjonction avec la syntaxe d'attente pour éviter les problèmes décrits.

Le problème central a toujours été qu'un futur engendrable devait être 'static , mais si vous capturiez des variables par référence dans ces combinateurs, vous vous retrouviez avec un futur qui n'était pas 'static . Mais attendre un futur non- 'static ne rend pas le fn asynchrone dans lequel vous êtes pas 'static . Vous pouvez donc faire des choses comme ceci:

`` rouille
// tranche: & [T]

let x = attendre le futur.et_alors (| i | si la tranche.contains (i) {async_fn (tranche)});

Donc, je prévoyais de ne pas commenter du tout, mais je voulais ajouter quelques notes de quelqu'un qui n'écrit pas beaucoup de code asynchrone, mais qui devra quand même le lire:

  • Le préfixe await est beaucoup plus facile à remarquer. C'est à la fois lorsque vous l'attendez (à l'intérieur d'un async fn) ou à l'extérieur (dans l'expression d'un corps de macro).
  • Postfix await étant un mot-clé peut bien se démarquer dans les chaînes avec peu ou pas d'autres mots-clés. Mais dès que vous avez des fermetures ou similaires avec d'autres structures de contrôle, votre awaits deviendra beaucoup moins visible et plus facile à oublier.
  • Je trouve que .await comme pseudo champ est très étrange. Ce n'est pas comme n'importe quel autre domaine qui m'importe.
  • Je ne sais pas si le recours à la détection IDE et à la coloration syntaxique est une bonne idée. Il est utile de faciliter la lisibilité lorsqu'aucune coloration syntaxique n'est disponible. De plus, avec les discussions récentes sur la complication de la grammaire et les problèmes que cela entraînera pour l'outillage, ce n'est peut-être pas une bonne idée de compter sur les IDE / éditeurs pour faire les choses correctement.

Je ne partage pas le sentiment que await!() tant que macro "ne se sent pas de première classe". Les macros sont un citoyen de première classe dans Rust. Ils ne sont pas non plus là uniquement pour «inventer la syntaxe dans le code utilisateur». De nombreuses macros "intégrées" ne sont format!() est là pour donner au compilateur la puissance de la vérification de type statique des chaînes de format. include_bytes!() n'est pas non plus défini par l'utilisateur et n'est pas là pour de simples raisons syntaxiques, c'est une macro car il doit s'agir d'une extension de syntaxe car, encore une fois, il fait des choses spéciales à l'intérieur du compilateur et il ne pourrait pas raisonnablement être écrit d'une autre manière sans l'ajouter en tant que fonctionnalité de langage de base.

Et c'est un peu mon argument: les macros sont un moyen idéal pour introduire et masquer la magie du compilateur d'une manière uniforme et sans surprise. Await est un bon exemple de ceci: il nécessite une manipulation spéciale, mais son entrée n'est qu'une expression, et en tant que tel, il convient parfaitement à la réalisation à l'aide d'une macro.

Donc, je ne vois pas vraiment la nécessité d'un mot-clé spécial. Cela dit, si cela doit être un mot-clé, alors ce devrait être juste cela, un simple mot-clé - je ne comprends pas vraiment la motivation de le déguiser en champ. Cela semble juste faux, ce n'est pas un accès au champ, ce n'est même pas à distance comme un accès au champ (contrairement, par exemple, aux méthodes getter / setter avec du sucre), donc le traiter comme un est incompatible avec la façon dont les champs fonctionnent aujourd'hui dans la langue.

@ H2CO3 await! macro est bon dans tous les cas, sauf pour deux propriétés:

  1. Les accolades supplémentaires sont vraiment ennuyeuses lorsque vous écrivez encore un millier d'attentes. Rust a supprimé les accolades des blocs if/while/... pour (je crois) la même raison. Cela peut sembler sans importance, mais c'est vraiment le cas
  2. Asymétrie avec async étant un mot-clé. Async devrait faire quelque chose de plus grand que d'autoriser une seule macro dans le corps de la méthode. Sinon, il pourrait simplement s'agir #[async] attribut mot-clé await . Cette attente peut être causée par l'expérience dans d'autres langues, qui sait, mais je crois fermement qu'elle existe. Il peut ne pas être résolu, mais il doit être pris en considération.

Sinon, il pourrait simplement s'agir #[async] attribut

Cela faisait longtemps, et je suis désolé de le voir disparaître. Nous introduisons un autre mot-clé alors que l'attribut #[async] fonctionnait parfaitement bien, et maintenant avec des macros procédurales de type attribut stables, il serait même cohérent avec le reste du langage à la fois syntaxiquement et sémantiquement, même s'il était toujours connu (et géré spécialement) par le compilateur.

@ H2CO3 quel est le but ou l'avantage d'avoir await comme macro du point de vue de l'utilisateur ? Existe-t-il des macros intégrées qui modifient le flux de contrôle des programmes sous le capot?

@ I60R L'avantage est l'uniformité avec le reste du langage, et donc de ne pas avoir à se soucier encore d'un autre mot-clé, avec tout le bagage qu'il apporterait avec lui-même - par exemple, la priorité et le regroupement sont évidents dans une invocation de macro.

Je ne vois pas pourquoi la deuxième partie est pertinente - ne peut-il pas y avoir une macro intégrée qui fait quelque chose de nouveau? En fait, je pense que pratiquement toutes les macros intégrées font quelque chose de "bizarre" qui ne peut pas (raisonnablement / efficacement / etc.) Être réalisé sans le support du compilateur. await!() ne serait pas un nouvel intrus à cet égard.

J'ai déjà suggéré une attente implicite , et plus récemment suggéré une attente non chaînable .

La communauté semble très fortement en faveur de l'attente explicite (j'aimerais que cela change, mais…). Cependant, la granularité de l'explicitation est ce qui crée le passe-partout (c'est-à-dire en avoir besoin plusieurs fois dans la même expression semble créer du passe-partout).

[ Attention : la suggestion suivante sera controversée, mais veuillez lui donner une chance sincère]

Je suis curieux de savoir si la plupart des membres de la communauté feraient des compromis et permettraient une «attente partielle-implicite»? Peut-être que lire attendre une fois par chaîne d'expression est assez explicite?

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

C'est quelque chose qui s'apparente à la déstructuration de la valeur, que j'appelle la déstructuration de l'expression.

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

Je serais également satisfait des autres implémentations de l'idée de base, "lire attendre une fois par chaîne d'expression est probablement assez explicite". Parce que cela réduit le passe-partout, mais aussi cela rend le chaînage possible avec une syntaxe basée sur le préfixe qui est la plus familière à tout le monde.

@yazaddaruvala Il y avait un avertissement ci-dessus, alors soyez prudent.

Je suis curieux de savoir si la plupart des membres de la communauté feraient des compromis et permettraient une «attente partielle-implicite»? Peut-être que lire attendre une fois par chaîne d'expression est assez explicite?

  1. Je ne pense pas que la plupart des communautés veulent quelque chose de partiel-implicite. L'approche native de Rust ressemble plus à "tout explicite". Même les lancers u8 -> u32 sont faits explicitement.
  2. Cela ne fonctionne pas bien pour des scénarios plus compliqués. Que doit faire le compilateur avec Vec<Future> ?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Doit-on effectuer await pour chaque appel imbriqué? Ok, nous pouvons probablement faire fonctionner wait pour une ligne de code (donc mapFunc imbriqués ne sera pas attendu), mais il est trop facile de briser ce comportement. Si wait fonctionne pour une ligne de code, tout refactoring comme "extraire la valeur" la rompt.

@yazaddaruvala

Dois-je comprendre que vous corrigez dans ce que vous proposez est le préfixe await qui a priorité sur ? avec la possibilité supplémentaire d'utiliser await à la place du mot-clé let pour changer le contexte pour saupoudrer tout impl Future <_ i = "7"> retourne avec wait?

Ce qui signifie que ces trois déclarations ci-dessous sont équivalentes:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

Et ces deux déclarations ci-dessous sont équivalentes:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

Je ne suis pas sûr que la syntaxe soit idéale, mais peut-être que le fait d'avoir un mot-clé qui se transforme en «attendre le contexte implicite» rend les problèmes du mot-clé de préfixe moins problématiques. Et en même temps, permettant d'utiliser un contrôle plus fin sur l'endroit où wait est utilisé en cas de besoin.

À mon avis, pour les courts métrages, le préfixe wait a l'avantage d'être similaire à de nombreux autres langages de programmation et de nombreux langages humains . Si à la fin il est décidé qu'attendre ne devrait être qu'un suffixe, je pense que je finirai parfois par utiliser une macro Await!() (ou toute autre forme similaire qui n'entre pas en conflit avec les mots-clés), quand je pense que cela s'appuie sur une connaissance approfondie de la structure des phrases en anglais aidera à réduire la surcharge mentale de lecture / écriture de code. Il y a une valeur certaine dans les formes de suffixe pour des expressions enchaînées plus longues, mais mon point de vue explicitement non basé sur des faits objectifs est que, avec la complexité humaine du préfixe en attente subventionné par la langue parlée, l'avantage de la simplicité de n'avoir qu'une seule forme ne l'emporte pas clairement sur la clarté potentielle du code d'avoir les deux. En supposant que vous faites confiance au programmeur pour choisir celui qui est le plus approprié pour le morceau de code actuel, au moins.

@Pzixel

Je ne pense pas que la plupart des communautés veulent quelque chose de partiel-implicite. L'approche native de Rust ressemble plus à "tout explicite".

Je ne vois pas les choses de la même manière. Je vois qu'il s'agit toujours d'un compromis entre explicite et implicite. Par exemple:

  • Les types de variables sont requis dans la portée de la fonction

    • Les types de variables peuvent être implicites dans une portée locale.

    • Les contextes de fermeture sont implicites

    • Étant donné que nous pouvons déjà raisonner sur les variables locales.

  • Des durées de vie sont nécessaires pour aider le vérificateur d'emprunt.

    • Mais dans une portée locale, ils peuvent être déduits.

    • Au niveau de la fonction, les durées de vie n'ont pas besoin d'être explicites, lorsqu'elles ont toutes la même valeur.

  • Je suis sûr qu'il y en a d'autres.

Utiliser await une seule fois par expression / instruction, avec tous les avantages du chaînage, est un compromis très raisonnable entre explicite et réducteur standard.

Cela ne fonctionne pas bien pour des scénarios plus compliqués. Que doit faire le compilateur avec Vec?

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Je dirais que le compilateur devrait faire une erreur. Il existe une énorme différence de type qui ne peut être déduite. Pendant ce temps, l'exemple avec Vec<impl Future> n'est résolu par aucune des syntaxes de ce thread.

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

Je dirais que le Vec<impl Future> est un mauvais exemple, ou nous devrions maintenir chaque syntaxe proposée au même standard. Pendant ce temps, la syntaxe "attente partielle-implicite" que j'ai proposée fonctionne mieux que les propositions actuelles pour des scénarios plus compliqués comme await (a, b) = (client.get("a")?, client.get("b")?); utilisant un suffixe normal ou un préfixe attendant let (a, b) = (client.get("a") await?, client.get("b") await?); résultats -La version implicite peut être exécutée simultanément par le compilateur désugardant de manière appropriée (comme je l'ai montré dans mon message d'origine).

En 2011, lorsque la fonctionnalité async en C # était encore en test, j'ai demandé que await était un opérateur de préfixe et j'ai obtenu une réponse du PM du langage C #. Puisqu'il y a une discussion sur la question de savoir si Rust devrait utiliser un opérateur de préfixe, j'ai pensé qu'il pourrait être utile de publier cette réponse ici. From Pourquoi 'await' est-il un opérateur préfixe au lieu d'un opérateur postfix? :

Comme vous pouvez l'imaginer, la syntaxe des expressions await a été un énorme point de discussion avant de nous décider sur ce qui est maintenant disponible :-). Cependant, nous n'avons pas beaucoup considéré les opérateurs postfix. Il y a juste quelque chose à propos de postfix (cf. les calculateurs HP et le langage de programmation Forth) qui rend les choses plus simples en principe et moins accessibles ou lisibles en pratique. C'est peut-être juste la façon dont la notation mathématique nous lave le cerveau en tant qu'enfants ...

Nous avons définitivement trouvé que l'opérateur de préfixe (avec sa saveur littéralement impérative - "fais ceci!") Était de loin le plus intuitif. La plupart de nos opérateurs unaires sont déjà préfixes, et await semble correspondre à cela. Oui, il se noie dans les expressions complexes, mais il en va de même pour l'ordre d'évaluation en général, en raison du fait que, par exemple, l'application de fonction n'est pas non plus un suffixe. Vous évaluez d'abord les arguments (qui sont sur la droite) puis appelez la fonction (qui est sur la gauche). Oui, il y a une différence dans cette syntaxe d'application de fonction en C # avec des parenthèses déjà intégrées, alors que pour await, vous pouvez généralement les supprimer.

Ce que je vois au sens large (et que j'ai moi-même adopté) autour de await, c'est un style qui utilise beaucoup de variables temporaires. J'ai tendance à préférer

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

ou quelque chose comme ça. En général, j'éviterais généralement d'avoir plus d'une attente en tout, sauf les expressions les plus simples (c'est-à-dire qu'ils sont tous des arguments pour la même fonction ou opérateur; ce sont probablement bien) et j'éviterais de mettre entre parenthèses les expressions d'attente, préférant utiliser des locals supplémentaires pour les deux emplois .

Ça a du sens?

Mads Torgersen, PM du langage C #


Mon expérience personnelle en tant que développeur C # est que la syntaxe me pousse à utiliser ce style d'instructions multiples, et que cela rend le code asynchrone beaucoup plus difficile à lire et à écrire que l'équivalent synchrone.

@yazaddaruvala

Je ne vois pas les choses de la même manière. Je vois que c'est toujours un compromis entre explicite et implicite

C'est toujours explicite. Je peux lier un bon article sur le sujet btw .

Je dirais que le compilateur devrait faire une erreur. Il existe une énorme différence de type qui ne peut être déduite. Pendant ce temps, l'exemple avec Vecn'est résolu par aucune des syntaxes de ce fil.

Pourquoi est-ce une erreur? Je suis content d'avoir Vec<impl future> , que je pourrais ensuite rejoindre. Vous proposez des limitations supplémentaires sans raison.

Je dirais que le Vecest un mauvais exemple, ou nous devrions maintenir chaque syntaxe proposée au même standard.

Nous devrions examiner chaque cas. Nous ne pouvons pas simplement ignorer les cas extrêmes parce que "moi, vous savez, personne n'écrit ça de toute façon". La conception du langage concerne principalement les cas de bord.

@chescock Je suis d'accord avec vous, mais étant dit avant, Rust a une très grande différence avec C # ici: en C #, vous n'écrivez jamais (await FooAsync()).Bar() . Mais dans Rust, c'est le cas. Et je parle de ? . En C #, vous avez une exception implicite qui se propage via des appels asynchrones. Mais dans rust, vous devez être explicite et écrire ? sur le résultat de la fonction après l'attente.

Bien sûr, vous pouvez demander aux utilisateurs d'écrire

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

mais c'est beaucoup plus étrange que

let foo = await? bar();
let bar = await? foo.baz();

Cela semble beaucoup mieux, mais cela nécessite l'introduction d'une nouvelle combinaison de await et ? doint (await expr)? . De plus, si nous avons d'autres opérateurs, nous pourrions écrire await?&@# et faire fonctionner toutes les combinaisons ... Cela semble un peu compliqué.

Mais alors nous pouvons simplement placer await comme un suffixe et il s'intégrera naturellement dans la langue actuelle:

let foo = bar() await?;
let bar = foo.baz() await?;

maintenant await? est deux jetons séparés, mais ils fonctionnent tout à fait. Vous pouvez avoir await?&@# et vous ne vous soucierez pas de le pirater dans le compilateur lui-même.

Bien sûr, cela semble un peu plus étrange que le préfixe, mais étant dit, tout dépend de la différence Rust / C #. Nous pouvons utiliser l'expérience C # pour être sûr que nous avons besoin d'une syntaxe d'attente explicite qui est susceptible d'avoir un mot-clé séparé, mais nous ne devons pas suivre aveuglément la même manière et ignorer les différences Rust / C #.

J'étais un partisan du préfixe await moi-même pendant longtemps et j'ai même invité des développeurs de langage C # dans le sujet (nous avons donc des informations plus récentes que celles de 2011 😄), mais maintenant je pense que postfix await mot clé

N'oubliez pas qu'il y a une soirée, donc nous pouvons la retravailler plus tard si nous trouvons un meilleur moyen.

Il m'est venu à l'esprit que nous pourrions peut-être tirer le meilleur parti des deux mondes avec une combinaison de:

  • un préfixe async mot-clé
  • une fonction de macro postfix générale

Ce qui, ensemble, permettrait à une macro de suffixe .await!() d'être implémentée sans la magie du compilateur.

À l'heure actuelle, une macro postfix .await!() nécessiterait la magie du compilateur. Cependant, si une variante de préfixe du mot-clé await était stabilisée, alors ce ne serait plus vrai: la macro postfix .await!() pourrait être implémentée de manière triviale comme une macro qui préfixe son argument (une expression) avec le mot-clé await et tout entre parenthèses.

Avantages de cette approche:

  • Le mot-clé préfixe await est disponible et accessible aux utilisateurs familiers avec cette construction à partir d'autres langages.
  • Le mot-clé prefix async peut être utilisé là où il est souhaitable de faire ressortir la nature asynchrone d'une expression.
  • Il y aurait une variante de suffixe .await!() , qui pourrait être utilisée dans des situations de chaînage, ou dans toute autre situation où la syntaxe du préfixe est maladroite.
  • Il n'y aurait pas besoin de magie du compilateur (potentiellement inattendue) pour la variante postfix (comme cela serait autrement un problème pour le champ, la méthode ou les options macro de postfix).
  • Les macros Postfix seraient également disponibles pour d'autres situations (comme .or_else!(continue) ), qui nécessitent par ailleurs la syntaxe de macro postfix pour des raisons similaires (elles nécessitent sinon que l'expression précédente soit encapsulée d'une manière maladroite et illisible).
  • Le mot-clé préfixe await pourrait être stabilisé relativement rapidement (permettant à l'écosystème de se développer) sans que nous ayons à attendre l'implémentation des macros postfixes. Mais à moyen et long terme, nous laisserions encore ouverte la possibilité d'une syntaxe postfixe pour l'attente.

@nicoburns , @ H2CO3 ,

Nous ne devrions pas implémenter des opérateurs de flux de contrôle comme await!() et .or_else!(continue) tant que macros, car du point de vue de l'utilisateur, il n'y a aucune raison valable de le faire. Quoi qu'il en soit, sous les deux formes, les utilisateurs auraient exactement les mêmes fonctionnalités et devraient les apprendre tous les deux, mais si ces fonctionnalités étaient implémentées sous forme de macros, les utilisateurs se demanderaient en outre pourquoi exactement elles sont implémentées de cette manière. Il est impossible de ne pas le remarquer, car il y aurait simplement une différence bizarre et artificielle entre les opérateurs réguliers de première classe et les opérateurs réguliers implémentés sous forme de macros (car en elles-mêmes, les macros sont de première classe, mais ce qu'elles implémentent ne le sont pas).

C'est exactement la même réponse que pour . avant await : nous n'en avons pas besoin. Les macros ne peuvent pas imiter un flux de contrôle de première classe: elles ont une forme différente, elles ont des cas d'utilisation différents, elles ont une mise en évidence différente, elles fonctionnent de manière différente et elles sont perçues complètement différemment.

Pour les fonctionnalités .await!() et .or_else!(continue) , il existe des solutions appropriées et ergonomiques avec de belles syntaxes: await mot-clé et none -coalescing opérateur. Nous devrions préférer les implémenter au lieu de quelque chose de générique, rarement utilisé et qui ressemble à des macros postfix.

Il n'y a aucune raison d'utiliser des macros, puisqu'il n'y a pas quelque chose de compliqué comme format!() , il n'y a pas quelque chose de rarement utilisé comme include_bytes!() , il n'y a pas de DSL personnalisé, il n'y a pas de suppression de la duplication dans le code utilisateur, il y a pas nécessaire une syntaxe de type vararg, il n'y a pas besoin de quelque chose qui ressemble différemment, et nous ne pouvons pas utiliser cette façon simplement parce que c'est possible.

Nous ne devrions pas implémenter les opérateurs de flux de contrôle sous forme de macros car du point de vue de l'utilisateur, il n'y a aucune raison valable de le faire.

try!() été implémenté en tant que macro. Il y avait une très bonne raison à cela.

J'ai déjà décrit la raison pour laquelle await serait une macro - il n'y a pas besoin d'un nouvel élément de langage si une fonctionnalité existante peut également atteindre l'objectif.

il n'y a pas quelque chose de compliqué comme format!()

Je ne suis pas d' format!() :

En passant, je n'ai pas suggéré que ce devrait être une macro postfix. (Je pense que ça ne devrait pas.)

Pour les deux fonctionnalités, il existe des solutions appropriées et ergonomiques avec de belles syntaxes

Nous ne devrions pas donner à chaque appel de fonction, expression de contrôle de flux ou fonctionnalité mineure de commodité sa propre syntaxe très spéciale. Il en résulte seulement un langage gonflé, plein de syntaxe sans raison, et c'est exactement le contraire de «beau».

(J'ai dit ce que je pouvais; je ne vais plus répondre à cet aspect de la question.)

@nicoburns

Il m'est venu à l'esprit que nous pourrions peut-être tirer le meilleur parti des deux mondes avec une combinaison de:

  • un préfixe await mot-clé
  • une fonction de macro postfix générale

Cela implique de faire de await un mot clé contextuel par opposition au mot clé réel qu'il est actuellement. Je ne pense pas que nous devrions faire ça.

Ce qui, ensemble, permettrait à une macro de postfix .await!() d'être implémentée sans la magie du compilateur.

Il s'agit d'une implémentation de solution plus complexe que foo await ou foo.await (en particulier cette dernière). Il y a toujours de la «magie», tout autant; vous venez de faire une manœuvre comptable .

Avantages de cette approche:

  • Le mot clé préfixe await est disponible et accessible aux utilisateurs familiers avec cette construction à partir d'autres langages.

Si nous ajoutons .await!() plus tard, nous donnons simplement aux utilisateurs plus à apprendre (à la fois await foo et foo.await!() ) et maintenant les utilisateurs demanderont "lequel dois-je utiliser quand..". Cela semble utiliser plus du budget de la complexité que foo await ou foo.await comme le feraient des solutions uniques.

  • Les macros Postfix seraient également disponibles pour d'autres situations (comme .or_else!(continue) ), qui nécessitent par ailleurs la syntaxe de macro postfix pour des raisons similaires (elles nécessitent sinon que l'expression précédente soit encapsulée d'une manière maladroite et illisible).

Je pense que les macros postfix ont de la valeur et devraient être ajoutées au langage. Cela ne signifie cependant pas qu'ils doivent être utilisés pour await ing; Comme vous l'avez mentionné, il y a .or_else!(continue) et de nombreux autres endroits où les macros postfix seraient utiles.

  • Le mot clé préfixe await pourrait être stabilisé relativement rapidement (permettant à l'écosystème de se développer) sans que nous ayons à attendre la mise en œuvre des macros postfixes. Mais à moyen et long terme, nous laisserions encore ouverte la possibilité d'une syntaxe postfixe pour l'attente.

Je ne vois aucune valeur à «laisser des possibilités ouvertes»; la valeur de postfix pour la composition, l'expérience IDE, etc. est connue aujourd'hui. Je ne suis pas intéressé par "stabiliser await foo aujourd'hui et j'espère que nous pourrons parvenir à un consensus sur foo.await!() demain".


@ H2CO3

try!() été implémenté en tant que macro. Il y avait une très bonne raison à cela.

try!(..) était obsolète, ce qui signifie que nous l'avons jugé impropre à la langue, en particulier parce qu'il n'était pas un suffixe. L'utiliser comme argument - autre que pour ce que nous ne devrions try!(..) est défini sans aucun support du compilateur .

En passant, je n'ai pas suggéré que ce devrait être une macro postfix. (Je pense que ça ne devrait pas.)

await n'est pas "chaque simple .." - en particulier, ce n'est pas une fonction de commodité mineure ; c'est plutôt une fonctionnalité de langage majeure qui est ajoutée.

@phaylon

De plus, avec les discussions récentes sur la complication de la grammaire et les problèmes que cela entraînera pour l'outillage, ce n'est peut-être pas une bonne idée de compter sur les IDE / éditeurs pour faire les choses correctement.

La syntaxe foo.await est destinée à réduire les problèmes d'outillage car tout éditeur avec un semblant de bon support pour Rust comprendra déjà le formulaire .ident . L'éditeur doit simplement ajouter await à la liste des mots-clés. De plus, les bons IDE ont déjà une complétion de code basée sur . - il semble donc plus simple d'étendre RLS (ou équivalents ...) pour fournir await comme première suggestion lorsque l'utilisateur tape . après my_future .

Pour ce qui est de compliquer la grammaire, .await est en fait le moins susceptible de compliquer la grammaire étant donné que prendre en charge .await dans la syntaxe consiste essentiellement à analyser .$ident et à ne pas se tromper sur ident == keywords::Await.name() .

La syntaxe foo.await est destinée à réduire les problèmes d'outillage car tout éditeur avec un semblant de bon support pour Rust comprendra déjà la forme .ident. Ce que l'éditeur doit faire, c'est simplement ajouter await à la liste des mots-clés. De plus, les bons IDE ont déjà la complétion de code basée sur. - il semble donc plus simple d'étendre RLS (ou équivalents ...) pour fournir wait comme première suggestion lorsque l'utilisateur tape. après mon_futur.

Je trouve simplement cela en contradiction avec la discussion de grammaire. Et le problème n'est pas seulement maintenant, mais dans 5, 10, 20 ans plus tard, après quelques éditions. Mais comme j'espère que vous l'avez remarqué, je mentionne également que même si un mot-clé est mis en surbrillance, les points d'attente peuvent passer inaperçus si d'autres mots-clés sont impliqués.

Quant à compliquer la grammaire, .await est en fait le moins susceptible de compliquer la grammaire étant donné que la prise en charge de .await dans la syntaxe consiste essentiellement à analyser. $ Ident et à ne pas se tromper sur ident == keywords :: Await.name ().

Je pense que l'honneur appartient à await!(future) , car il est déjà entièrement pris en charge par la grammaire.

@Centril try! finalement devenu redondant car l'opérateur ? peut faire strictement plus. Ce n'est pas «impropre à la langue». Cela ne vous plaira peut-être pas, ce que j'accepte. Mais pour moi, c'est en fait l'une des meilleures choses que Rust a inventées, et c'était l'un des arguments de vente. Et je sais qu'il est implémenté sans le support du compilateur - mais je ne vois pas en quoi cela est pertinent lorsque je discute si oui ou non il effectue un flux de contrôle. C'est vrai, peu importe.

attendre n'est pas "tout le monde .."

Mais d'autres mentionnés ici (par exemple or_else ) le sont, et mon point s'applique toujours aux fonctionnalités majeures de toute façon. Ajouter de la syntaxe juste pour ajouter de la syntaxe n'est pas un avantage, donc chaque fois qu'il y a quelque chose qui fonctionne déjà dans un cas plus général, il devrait être préféré à l'invention d'une nouvelle notation. (Je sais que l'autre argument contre les macros est qu '"elles ne sont pas postfixes". Je ne pense tout simplement pas que les avantages d'attendre d'être son propre opérateur postfix soient suffisamment élevés pour justifier le coût. Nous avons survécu aux appels de fonctions imbriquées. Nous sera tout aussi bien après avoir écrit quelques macros imbriquées.)

Le mercredi 23 janvier 2019 à 09:59:36 +0000, Mazdak Farrokhzad a écrit:

  • Les macros Postfix seraient également disponibles pour d'autres situations (comme .or_else!(continue) ), qui nécessitent par ailleurs la syntaxe de macro postfix pour des raisons similaires (elles nécessitent sinon que l'expression précédente soit enveloppée d'une manière maladroite et illisible).

Je pense que les macros postfix ont de la valeur et devraient être ajoutées au langage. Cela ne signifie cependant pas qu'ils doivent être utilisés pour await ing; Comme vous l'avez mentionné, il y a .or_else!(continue) et de nombreux autres endroits où les macros postfix seraient utiles.

La principale raison d'utiliser .await!() est que l'apparence d'une macro rend
il est clair que cela peut affecter le flux de contrôle.

.await ressemble à un accès de champ, .await() ressemble à une fonction
appel, et ni les accès aux champs ni les appels de fonction ne peuvent affecter le contrôle
couler. .await!() ressemble à une macro et les macros peuvent affecter le contrôle
couler.

@joshtriplett I n'affecte pas le flux de contrôle au sens standard. Ceci est le fait de base pour justifier que le vérificateur d'emprunt devrait fonctionner à terme tel que défini (et la broche est le raisonnement comment). Du point de vue de l'exécution de la fonction locale, l'exécution de await est comme la plupart des autres appels de fonction. Vous continuez là où vous vous êtes arrêté et avez une valeur de retour sur la pile.

Je trouve simplement cela en contradiction avec la discussion de grammaire. Et le problème n'est pas seulement maintenant, mais dans 5, 10, 20 ans plus tard, après quelques éditions.

Je n'ai aucune idée de ce que vous entendez par là.

Je pense que l'honneur appartient à await!(future) , car il est déjà entièrement pris en charge par la grammaire.

Je comprends, mais à ce stade, en raison des nombreux autres inconvénients de cette syntaxe, je pense qu'elle est effectivement exclue.

@Centril try! finalement devenu redondant car l'opérateur ? peut faire strictement plus. Ce n'est pas «impropre à la langue». Vous pourriez ne pas l'aimer, cependant, ce que j'accepte.

Il est explicitement obsolète et de plus une erreur difficile d'écrire try!(expr) sur Rust 2018. Nous avons décidé collectivement de le faire ainsi et il a donc été jugé inapte.

La principale raison d'utiliser .await!() est que ressembler à une macro indique clairement qu'elle peut affecter le flux de contrôle.

.await ressemble à un accès au champ, .await() ressemble à un appel de fonction, et ni les accès aux champs ni les appels de fonction ne peuvent affecter le flux de contrôle.

Je pense que la distinction est quelque chose que les utilisateurs apprendront relativement facilement, d'autant plus que .await sera généralement suivi de ? (qui est le flux de contrôle local de la fonction). En ce qui concerne les appels de fonction, et comme mentionné ci-dessus, je pense qu'il est juste de dire que les méthodes d'itération sont une forme de flux de contrôle. De plus, ce n'est pas parce qu'une macro peut affecter le flux de contrôle qu'elle le fera . De nombreuses macros ne le font pas (par exemple dbg! , format! , ...). La compréhension que .await!() ou .await affectera le flux de contrôle (bien que dans un sens beaucoup plus faible que ? , selon la note de @HeroicKatora ) découle du mot await lui-même.

@Centril

Cela implique de faire attendre un mot-clé contextuel par opposition au mot-clé réel qu'il est actuellement. Je ne pense pas que nous devrions faire ça.

Eek. C'est un peu douloureux. Peut-être que la macro pourrait avoir un nom différent comme .wait!() ou .awaited!() (ce dernier est assez agréable car il indique clairement que cela s'applique à l'expression précédente).

"Ce qui, ensemble, permettrait à une macro postfix .await! () D'être implémentée sans la magie du compilateur."
Il s'agit d'une implémentation de solution plus complexe que foo await ou foo.await (en particulier cette dernière). Il y a toujours de la «magie», tout autant; vous venez de faire une manœuvre comptable.

De plus, try! (..) est défini sans aucun support du compilateur.

Et si nous avions un préfixe await mot-clé et des macros de suffixe, alors .await!() (peut-être avec un nom différent) pourrait également être implémenté sans le support du compilateur, non? Bien sûr, le mot-clé await lui-même impliquerait toujours une quantité importante de magie du compilateur, mais cela serait simplement appliqué au résultat d'une macro, et n'est pas différent de la relation de try!() avec le return mot-clé.

Si nous allons ajouter .await! () Plus tard, nous donnons simplement aux utilisateurs plus à apprendre (les deux attendent foo et foo.await! ()) Et maintenant les utilisateurs demanderont "lequel dois-je utiliser quand ..". Cela semble utiliser plus du budget de complexité que foo await ou foo.await comme le feraient des solutions uniques.

Je pense que la complexité que cela ajoute à l'utilisateur est minime (la complexité de la mise en œuvre peut être une autre affaire, mais si nous voulons quand même des macros postfixes ...). Les deux formes se lisent intuitivement, de sorte que lors de la lecture de l'une ou de l'autre, ce qui se passe devrait être évident. Quant à savoir lequel choisir lors de l'écriture du code, la documentation pourrait simplement dire quelque chose comme:

"Il y a deux façons d'attendre un avenir dans Rust: await foo.bar(); et foo.bar().await!() . Laquelle utiliser est une préférence stylistique et ne fait aucune différence pour le déroulement de l'exécution"

Je ne vois aucune valeur à «laisser des possibilités ouvertes»; la valeur de postfix pour la composition, l'expérience IDE, etc. est connue aujourd'hui.

C'est vrai. Je suppose que dans mon esprit, il s'agit davantage de faire en sorte que notre mise en œuvre ne ferme pas la possibilité d'avoir un langage plus unifié à l'avenir.

Je n'ai aucune idée de ce que vous entendez par là.

Peu importe. Mais en général, je le préfère aussi lorsque la syntaxe est évidente sans une bonne mise en évidence. Ainsi, les choses se démarquent dans git diff et d'autres contextes similaires.

En théorie oui, et pour les programmes triviaux oui, mais en réalité les programmeurs
besoin de savoir où se trouvent leurs points de suspension.

Pour un exemple simple, vous pouvez maintenir une RefCell sur un point de suspension, et
le comportement de votre programme sera différent que si le RefCell était
libéré avant le point de suspension. Dans les grands programmes, il y aura
d'innombrables subtilités comme celle-ci, où le fait que la fonction actuelle
suspend est une information importante.

Le mercredi 23 janvier 2019 à 14:21, HeroicKatora [email protected]
a écrit:

@joshtriplett https://github.com/joshtriplett Je n'affecte pas le contrôle
flux au sens standard. C'est le fait fondamental pour justifier que le
Le vérificateur d'emprunt devrait fonctionner dans les contrats à terme tels que définis. Du point de vue de la
l'exécution d'une fonction locale, l'exécution de wait est comme tout autre appel de fonction.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

Le mercredi 23 janvier 2019 à 14:26:07 -0800, Mazdak Farrokhzad a écrit:

La principale raison d'utiliser .await!() est que ressembler à une macro indique clairement qu'elle peut affecter le flux de contrôle.

.await ressemble à un accès de champ, .await() ressemble à un appel de fonction, et ni les accès aux champs ni les appels de fonction ne peuvent affecter le flux de contrôle.

Je pense que la distinction est quelque chose que les utilisateurs apprendront relativement facilement

Pourquoi devraient-ils le faire?

Je pense que la distinction est quelque chose que les utilisateurs apprendront relativement facilement, d'autant plus que .await sera généralement suivi de? (qui est le flux de contrôle local de la fonction).

Souvent oui, mais pas toujours, et la syntaxe devrait certainement fonctionner dans les cas où ce n'est pas le cas.

En ce qui concerne les appels de fonction, et comme mentionné ci-dessus, je pense qu'il est juste de dire que les méthodes d'itération sont une forme de flux de contrôle.

Pas de la même manière que return ou ? (ou même break ). Il serait très surprenant qu'un appel de fonction puisse renvoyer deux niveaux de la fonction qui l'a appelé (l'une des raisons pour lesquelles Rust n'a pas d'exceptions est précisément parce que cela est surprenant), ou sortir d'une boucle dans l'appel fonction.

De plus, ce n'est pas parce qu'une macro peut affecter le flux de contrôle qu'elle le fera. De nombreuses macros ne le font pas (par exemple, dbg !, format !, ...). La compréhension que .await! () Ou .await affectera le flux de contrôle (bien que dans un sens beaucoup plus faible que?, Selon la note de wait ".

Je n'aime pas du tout ça. Effectuer le flux de contrôle est un effet secondaire très important . Il doit avoir une forme syntaxique différente de celle des constructions qui ne peuvent normalement pas faire cela. Les constructions qui peuvent faire cela dans Rust sont: les mots-clés ( return , break , continue ), les opérateurs ( ? ) et les macros (en évaluant à un des autres formes). Pourquoi brouiller les eaux en ajoutant une exception?

@ejmahler Je ne vois pas en quoi la tenue d'un RefCell est différente pour la comparaison avec un appel de fonction normal. Il se peut également que la fonction interne veuille acquérir la même RefCell. Mais personne ne semble avoir de problème majeur à ne pas saisir immédiatement le graphe d'appel potentiel complet. De même, les problèmes d'épuisement de la pile ne sont pas particulièrement préoccupés par la nécessité d'annoter l'espace de pile fourni. Ici la comparaison serait favorable pour les coroutines! Devrions-nous rendre les appels de fonction normaux plus visibles s'ils ne sont pas en ligne? Avons-nous besoin d'appels de queue explicites pour s'attaquer à ce problème de plus en plus difficile avec les bibliothèques?

La réponse est, à mon humble avis, que le plus grand risque lors de l'utilisation de RefCell et d'attente est la méconnaissance. Lorsque les autres problèmes ci-dessus peuvent être résumés du matériel, je pense qu'en tant que programmeurs, nous pouvons également apprendre à ne pas nous accrocher à RefCell, etc.

Le mercredi 23 janvier 2019 à 22:30:10 +0000, Elliott Mahler a écrit:

En théorie oui, et pour les programmes triviaux oui, mais en réalité les programmeurs
besoin de savoir où se trouvent leurs points de suspension.

: +1:

@HeroicKatora

Une différence critique est la quantité de code que vous devez inspecter, combien
possibilités dont vous devez tenir compte. Les programmeurs sont déjà habitués à
s'assurer que rien qu'ils appellent n'utilise également une RefCell qu'ils ont vérifiée
en dehors. Mais avec await, au lieu d'inspecter une seule pile d'appels, nous n'avons pas
contrôle sur ce qui est exécuté pendant l’attente - littéralement n’importe quelle ligne du
programme peut être exécuté. Mon intuition me dit que le programmeur moyen est
beaucoup moins équipés pour faire face à cette explosion de possibilités, car ils
doivent y faire face beaucoup moins fréquemment.

Pour un autre exemple de points de suspension étant des informations critiques, dans le jeu
programmation, nous écrivons généralement des coroutines destinées à être reprises
une fois par image, et nous changeons l'état du jeu à chaque reprise. Si nous oublions un
point de suspension dans l'une de ces coroutines, nous pouvons quitter le jeu dans un
état pour plusieurs cadres.

Tous ces problèmes peuvent être résolus en disant «soyez plus intelligent et gagnez moins
erreurs »bien sûr, mais cette approche ne fonctionne pas historiquement.

Le mercredi 23 janvier 2019 à 14:44 Josh Triplett [email protected]
a écrit:

Le mercredi 23 janvier 2019 à 22:30:10 +0000, Elliott Mahler a écrit:

En théorie oui, et pour les programmes triviaux oui, mais en réalité les programmeurs
besoin de savoir où se trouvent leurs points de suspension.

: +1:

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

Pour un autre exemple de points de suspension qui sont des informations critiques, dans la programmation de jeu, nous écrivons généralement des coroutines qui sont destinées à être reprises une fois par image, et nous changeons l'état du jeu à chaque reprise. Si nous oublions un point de suspension dans l'une de ces coroutines, nous pouvons laisser le jeu dans un état cassé pendant plusieurs images.

Si vous déposez la clé dans un SlotMap, vous perdez de la mémoire. La langue elle-même ne vous aide pas en soi. Ce que je veux dire, c'est que vos exemples de la façon dont await n'est vaguement pas assez visible (tant qu'il s'agit d'un mot clé, son occurrence sera unique) ne semblent pas se généraliser à des problèmes qui ne se sont pas produits pour d'autres fonctionnalités. Je pense que vous devriez évaluer attendre moins sur les fonctionnalités que le langage vous offre immédiatement et plus sur la façon dont il vous permet d'exprimer vos propres fonctionnalités. Évaluez vos outils puis appliquez-les. Suggérer une solution pour l'état du jeu, et comment cela est résolu assez bien: écrivez un wrapper qui affirme en retour que l'état du jeu a en fait coché le montant requis, async vous permet d'emprunter votre propre état pour cette cause. Interdire les autres attend dans ce code compartimenté.

L'explosion d'état dans RefCell ne vient pas de vous ne vérifiant pas qu'il n'est pas utilisé ailleurs, mais d'autres points de code ne sachant pas que vous vous êtes appuyé sur cet invariant et l'ajoute silencieusement. Ceux-ci sont traités de la même manière, documentation, commentaires, emballage correct.

Tenir RefCell n'est pas très différent de hodling Mutex en bloquant io. Vous pouvez obtenir une impasse, ce qui est pire et plus difficile à déboguer qu'une panique. Et pourtant, nous n'annotons pas les opérations de blocage avec le mot clé explicite block . :).

Le partage de RefCell entre des fonctions asynchrones limitera considérablement leur utilisation générale ( !Send ), donc je ne pense pas que ce sera courant. Et RefCell doit généralement être évité, et lorsqu'il est utilisé, emprunté pour le moins de temps possible.

Donc, je ne pense pas que hodling RefCell accidentellement au-dessus d'un seuil

Si nous oublions un point de suspension dans l'une de ces coroutines, nous pouvons laisser le jeu dans un état cassé pendant plusieurs images.

Les coroutines et les fonctions asynchrones sont une chose différente. Nous n'essayons pas de rendre yield implicite.

À quiconque est en faveur de la syntaxe de macro au lieu de la syntaxe de mot-clé ici: Veuillez décrire de quoi il s'agit await qui signifie que cela devrait être une macro et en quoi c'est différent de, disons, while , ce qui _pourrait_ avoir juste été une macro while! ( même en utilisant juste macro_rules! ; pas de casse spéciale du tout).

Edit: Ce n'est pas de la rhétorique. Je suis vraiment intéressé par une telle chose, de la même manière que je pensais que je voulais courir à la première attente (comme C #) mais la RFC m'a convaincu du contraire.

À quiconque est en faveur de la syntaxe de macro au lieu de la syntaxe de mot-clé ici: Veuillez décrire de quoi il s'agit, wait, cela signifie que cela devrait être une macro et en quoi c'est différent de, disons, while, qui aurait pu durer un moment! macro (même en utilisant uniquement des macro_rules !; pas de casse spéciale du tout).

Je suis généralement d'accord avec ce raisonnement, et je souhaite utiliser un mot-clé pour la syntaxe de préfixe.

Cependant, comme indiqué ci-dessus (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), je suis en faveur d'avoir également une macro postfix pour la syntaxe postfix (peut-être .awaited!() pour éviter les conflits de nom avec le mot-clé prefix). Mon raisonnement étant en partie que cela simplifie les choses d'avoir un mot-clé qui ne peut être utilisé que d'une seule manière, et d'enregistrer d'autres variantes pour les macros, mais étant principalement que pour autant que je sache, personne n'a été en mesure de trouver un suffixe syntaxe de mot-clé que je considérerais comme acceptable:

  • foo.bar().await et foo.bar().await() seraient techniquement des mots-clés, mais ils ressemblent à un accès de champ ou à un appel de méthode, et je ne pense pas qu'une telle construction de modification de flux de contrôle devrait être cachée comme ça.
  • foo.bar() await est juste déroutant, surtout avec un chaînage supplémentaire tel que foo.bar() await.qux() . Je n'ai jamais vu un mot-clé utilisé comme suffixe comme celui-là (je suis sûr qu'ils existent, mais je ne peux pas penser à un seul exemple dans aucune langue que je connaisse de mots-clés qui fonctionnent comme ça). Et je pense que cela est très déroutant, comme si l'attente s'appliquait au qux() non au foo.bar() .
  • foo.bar()@await ou une autre ponctuation pourrait fonctionner. Mais ce n'est pas particulièrement agréable et cela semble assez ponctuel. En fait, je ne pense pas qu'il y ait de problèmes significatifs avec cette syntaxe (contrairement aux options ci-dessus). J'ai juste l'impression qu'une fois que nous avons commencé à ajouter des sigils, la balance commence à pencher vers l'abandon de la syntaxe personnalisée et vers le fait que ce soit une macro.

Je vais réitérer une idée antérieure une fois de plus, bien que modifiée avec de nouvelles pensées et considérations, puis essayer de garder le silence.

await tant que mot clé n'est valide que dans une fonction async , nous devrions donc autoriser l'identifiant await ailleurs. Si une variable est nommée await dans une portée externe, elle ne peut pas être accédée depuis un bloc async sauf si elle est utilisée comme identifiant brut. Ou, par exemple, la macro await! .

De plus, adapter le mot-clé comme ça permettrait de faire mon point principal: nous devrions permettre le mélange et la mise en correspondance des générateurs et des fonctions asynchrones, comme ceci:

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

Je pense que cela est suffisamment transparent pour les utilisateurs extrêmement avancés qui souhaitent faire des choses géniales, mais permet aux utilisateurs nouveaux et modérés d'utiliser uniquement ce qui est nécessaire. 2 et 3, s'ils sont utilisés sans aucune instruction yield , sont en fait identiques.

Je pense aussi que le préfixe await? est un excellent raccourci pour ajouter ? au résultat de l'opération asynchrone, mais ce n'est pas strictement nécessaire.

Si les macros postfix deviennent une chose officielle (ce que j'espère en quelque sorte), les deux await!(...) et .await!() pourraient exister ensemble, permettant trois façons équivalentes de faire des waits, pour des cas d'utilisation et des styles spécifiques . Je ne pense pas que cela ajouterait des frais généraux cognitifs, mais permettrait une plus grande flexibilité.

Idéalement, async fn et #[async] fn* pourraient même partager le code d'implémentation pour transformer le générateur sous-jacent en une machine à états asynchrone.

Ces problèmes ont montré qu'il n'y a pas de véritable style préféré, donc le mieux que je puisse espérer est de fournir des niveaux de complexité propres, flexibles, lisibles et faciles à approcher. Je pense que le schéma ci-dessus est un bon compromis pour l’avenir.

await tant que mot clé n'est valide que dans une fonction async , nous devrions donc permettre à l'identifiant d'attendre ailleurs.

Je ne sais pas si c'est pratique dans Rust, étant donné les macros hygiéniques. Si j'appelle foo!(x.await) ou foo!(await { x }) quand il veut un expr , je pense qu'il devrait être sans ambiguïté que le mot clé await est recherché - pas le await ou l'expression littérale await struct avec un raccourci de champ init - même dans une méthode synchrone.

permettant trois façons de faire équivalentes attend

Je t'en prie, non. (Au moins en core . Évidemment, les gens peuvent créer des macros dans leur propre code. S'ils le souhaitent)

À l'heure actuelle, une macro postfix .await! () Nécessiterait la magie du compilateur. Cependant, si une variante de préfixe du mot-clé await était stabilisée, cela ne serait plus vrai: la macro postfix .await! () Pourrait être implémentée de manière triviale comme une macro qui préfixe son argument (une expression) avec le mot-clé await et encapsule tout entre parenthèses.

Je noterai que c'est tout aussi vrai - mais plus simple! - dans l'autre sens: si nous stabilisons la syntaxe du mot-clé .await , les gens peuvent déjà créer une macro de préfixe awaited!() s'ils n'aiment pas assez le suffixe.

Je n'aime pas l'idée d'ajouter plusieurs variantes autorisées (comme le préfixe et le suffixe) mais pour une raison légèrement différente de celle que les utilisateurs demanderont. Je travaille dans une entreprise assez grande et les disputes sur le style de code sont réelles. J'aimerais n'avoir qu'une seule forme évidente et correcte à utiliser. Après tout, le zen de python pourrait avoir raison sur ce point.

Je n'aime pas l'idée d'ajouter plusieurs variantes autorisées (comme le préfixe et le suffixe) mais pour une raison légèrement différente de celle que les utilisateurs demanderont. Je travaille dans une entreprise assez grande et les disputes sur le style de code sont réelles. J'aimerais n'avoir qu'une seule forme évidente et correcte à utiliser. Après tout, le zen de python pourrait avoir raison sur ce point.

Je vois ce que tu veux dire. Cependant, il n'y a aucun moyen d'empêcher les programmeurs de définir leurs propres macros en faisant des choses comme await!() . Les structures similaires seront toujours possibles. Il y aura donc vraiment différentes variantes de toute façon.

Eh bien, pas au moins await!(...) car ce serait une erreur; mais si un utilisateur définit macro_rules! wait { ($e:expr) => { e.await }; } et l'utilise ensuite comme wait!(expr) alors il semblera distinctement unidiomatique et deviendra probablement rapidement démodé. Cela réduit considérablement la probabilité de variation dans l'écosystème et permet aux utilisateurs d'apprendre moins de styles. Ainsi, je pense que le point de

@Centril Si quelqu'un veut faire de mauvaises choses, il est difficile de l'arrêter. Qu'en est-il de _await!() ou awai!() ?

ou, lorsque l'identifiant Unicode est activé, quelque chose comme àwait!() .

...

@earthengine Le but est de fixer des normes communautaires (similaires à ce que nous faisons avec les styles lints et rustfmt ), pas pour empêcher diverses personnes de faire des choses bizarres exprès. Nous traitons ici de probabilités , pas de garanties absolues de ne pas voir _await!() .

Résumons et passons en revue les arguments de chaque syntaxe de postfix:

  • __ expr await (mot-clé postfix) __: La syntaxe du mot-clé Postfix utilise le mot-clé await nous avons déjà réservé. Attendre est une transformation magique, et l'utilisation d'un mot-clé permet de se démarquer de manière appropriée. Cela ne ressemble pas à un accès à un champ, à une méthode ou à un appel de macro, et nous n'aurons pas à l'expliquer comme une exception. Il correspond bien aux règles d'analyse actuelles et à l'opérateur ? . L'espace dans le chaînage de méthodes n'est sans doute pas pire qu'avec Generalized Type Ascription , une RFC qui semble susceptible d'être acceptée sous une forme ou une autre. En revanche, les IDE devront peut-être faire plus de magie pour fournir une saisie semi-automatique pour await . Les gens peuvent trouver l'espace dans le chaînage de méthodes trop gênant (bien que @withoutboats ait soutenu que cela pourrait être un avantage), en particulier si le code n'est pas formaté de telle sorte que await termine chaque ligne. Il utilise un mot-clé que nous pourrions peut-être éviter d'utiliser si nous adoptions une autre approche.

  • __ expr.await (champ postfix) __: La syntaxe du champ Postfix exploite la "puissance du point" - cela semble naturel dans le chaînage et permet aux IDE de compléter automatiquement await sans effectuer d'autres opérations comme supprimant automatiquement le point ). C'est tout aussi concis que le mot-clé postfix. Le point devant await peut en faciliter la recherche d'instances avec grep. En revanche, cela ressemble à un accès sur le terrain. En tant qu'accès au champ apparent, il n'y a aucun indice visuel indiquant que «cela fait quelque chose».

  • __ expr.await() (méthode postfix) __: La syntaxe de la méthode Postfix est similaire au champ postfix. Du côté positif, la syntaxe d'appel () indique au lecteur, "cela fait quelque chose". Vu localement, cela a presque du sens de la même manière qu'un appel à une méthode de blocage donnerait une exécution dans un programme multi-thread. En revanche, c'est un peu plus long et plus bruyant, et déguiser le comportement magique de await en une méthode peut s'avérer déroutant. On peut dire que await est une méthode dans le même sens limité que call/cc dans Scheme est une fonction. En tant que méthode, nous aurions besoin de déterminer si Future::await(expr) devrait fonctionner de manière cohérente avec UFCS .

  • __ expr.await!() (macro postfix) __: La syntaxe des macros Postfix exploite de la même manière la "puissance du point". La macro ! bang indique "cela pourrait faire quelque chose de magique". En revanche, c'est encore plus bruyant, et bien que les macros fassent de la magie, elles ne font généralement pas de magie au code environnant comme le fait await . Également à la baisse, en supposant que nous normalisions une syntaxe de macro postfix à usage général , il peut y avoir des problèmes pour continuer à traiter await comme un mot clé.

  • __ expr@ , expr# , expr~ , et d'autres symboles à un seul caractère__: L'utilisation d'un seul caractère comme nous le faisons avec ? maximise la concision et peut-être rend la syntaxe postfixe semblent plus naturels. Comme pour ? , nous pouvons constater que nous apprécions cette concision si await commence à envahir notre code. En revanche, tant que la douleur d'avoir notre code jonché de await devient pas un problème, il est difficile de voir un consensus se former autour des compromis inhérents à l'adoption d'une telle syntaxe.

Je voulais prendre un post pour dire merci à @traviscross pour ces articles de synthèse! Ils ont toujours été bien écrits et bien réticulés. C'est très apprécié. :cœur:

J'ai une idée qu'en ajoutant des "opérateurs de pipe" comme F #, les utilisateurs peuvent utiliser soit le préfixe soit le suffixe (avec une différence de syntaxe explicite).

// 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 Excellent résumé. Il y a aussi eu une discussion autour de la combinaison du sigil et du mot-clé, par exemple fut@await , donc je vais juste ajouter ceci ici pour les personnes venant sur ce fil.

J'ai énuméré les avantages et les inconvénients de cette syntaxe ici . @earthengine indique que d'autres sigils que @ sont possibles, tels que ~ . @BenoitZugmeyer favorise @await , et demande si les macros de suffixe comme expr!await seraient une bonne idée. @dpc soutient que @await est trop ad hoc et ne s'intègre pas bien avec ce que nous avons déjà, aussi, que Rust est déjà lourd de signatures; @cenwangumass convient que c'est trop ad hoc. @newpavlov dit que la partie await semble redondante, surtout si nous n'ajoutons pas d'autres mots-clés similaires à l'avenir. @nicoburns dit que la syntaxe pourrait fonctionner et qu'il n'y a pas beaucoup de problèmes avec elle, mais que c'est une solution trop ad hoc.

@traviscross excellent résumé!

Mon 0,02 $ dans l'ordre du pire au meilleur à mon avis:

  • 3 est définitivement interdit car ce n'est même pas un appel de méthode.
  • 2 n'est pas un domaine, c'est très déroutant, surtout pour les nouveaux arrivants. avoir await sur la liste d'achèvement n'est pas vraiment utile. Après en avoir écrit des tonnes, il vous suffit de l'écrire sous la forme fn ou extern . La complétion supplémentaire est encore pire que rien car au lieu de méthodes / champs utiles, je serai suggéré par un mot-clé.
  • 4 La macro est quelque chose qui convient ici, mais cela ne me convient pas. Je veux dire l'asymétrie avec async étant un mot-clé, comme je l'ai mentionné ci-dessus
  • 5 Sigil est peut-être trop concis et plus difficile à repérer, mais c'est une entité séparée et peut être traitée ainsi. Ne ressemble à rien d'autre, ne produisant donc aucune confusion pour les utilisateurs
  • 1 Meilleure approche À mon humble avis, c'est juste un sceau facile à repérer et qui est déjà un mot-clé réservé. La séparation de l'espace, comme mentionné ci-dessus, est un avantage, pas un défaut. C'est bien d'exister quand aucun formatage n'est fait, mais avec rustfmt c'est encore moins significatif.

En tant que personne qui attend avec impatience que ce moment arrive, voici également mon 0,02 $.

Pour la plupart, je suis d'accord avec @Pzixel , sauf sur les deux derniers points, en ce sens que je les échangerais, ou peut-être même donnerais les deux en option, comme avec le try!() / ? paire. J'ai encore vu des gens plaider en faveur de try!() sur ? pour des raisons de clarté, et je pense qu'avoir une certaine agence sur l'apparence de votre code dans ce cas particulier est un avantage plutôt qu'un con , même au prix de deux syntaxes différentes.

En particulier, un mot-clé await postfix est idéal si vous écrivez votre code asynchrone comme une séquence explicite d'étapes, par exemple

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

D'un autre côté, vous préférerez peut-être faire quelque chose de plus compliqué à la place, dans le style de chaînage typique de la méthode Rust-y, par exemple

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

Je pense que dans ce cas, il est déjà assez clair du contexte que nous avons affaire à un avenir, et la verbosité du clavier await est redondante. Comparer:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

Une autre chose que j'aime à propos de la syntaxe postfix à symbole unique est qu'elle indique clairement que le symbole appartient à l'expression , alors que (pour moi, personnellement) le await autonome a l'air un peu ... perdu ? :)

J'imagine que ce que j'essaie de dire, c'est que await semble mieux dans les déclarations , alors qu'un suffixe à un seul symbole est meilleur dans les expressions .

Ce ne sont que mes pensées, de toute façon.

Comme c'est déjà la mère de tous les fils de perte de vélos, je voulais ajouter un autre sigil qui n'a pas été mentionné jusqu'à présent AFAICT: -> .

L'idée étant qu'elle reflète le -> de la déclaration de type de retour de la fonction qu'elle suit, qui - la fonction étant async - est le type de retour attendu.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

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

Dans ce qui précède, ce que vous obtenez de send()-> est un Result<Response, HttpError> , comme indiqué dans la déclaration de fonction.

Ce sont mes 0,02 $ après avoir lu la majeure partie de la discussion ci-dessus et réfléchi aux options proposées pendant quelques jours. Il n'y a aucune raison de donner du poids à mon opinion - je ne suis qu'une personne aléatoire sur Internet. Je ne commenterai probablement pas beaucoup plus loin car je suis prudent de ne pas ajouter plus de bruit à la discussion.


J'aime un sigil postfix. Il y a une priorité pour les sigils postfixes et je pense que cela serait cohérent avec le reste de la langue. Je n'ai pas de préférence particulière sur le sceau particulier utilisé - aucun n'est convaincant mais je pense que cela disparaîtra avec la familiarité. Un sigil introduit le moins de bruit par rapport aux autres options de postfix.

Cela ne me dérange pas de remplacer . (lors du chaînage) par -> pour attendre. C'est toujours concis et sans ambiguïté, mais je préférerais quelque chose qui peut être utilisé dans les mêmes contextes que ? .

Je n'aime pas du tout les autres options de postfix qui ont été présentées. await n'est ni un champ ni une méthode et il semble donc totalement incompatible avec le reste du langage pour une construction de flux de contrôle d'être présentée comme telle. Il n'y a pas de macros ou de mots-clés postfix dans la langue, ce qui semble également incohérent.

Si j'étais nouveau dans le langage, ne le connaissant pas complètement, alors je supposerais que .await ou .await() n'étaient pas des fonctionnalités de langage spéciales et étaient des champs ou des méthodes sur un type - comme c'est le cas avec tous les autres champs et méthodes dans la langue que l'utilisateur verra. Une fois acquis un peu plus d'expérience, si .await!() était choisi, l'utilisateur pourrait essayer d'apprendre à définir ses propres macros de suffixe (tout comme il a appris à définir ses propres macros de préfixe) - il ne le peut pas. Si cet utilisateur voyait un sceau, il pourrait avoir besoin de rechercher de la documentation (tout comme ? ), mais il ne le confondrait avec rien d'autre et perdrait du temps à essayer de trouver la définition de .await() ou documentation pour le champ .await .

J'aime un préfixe await { .. } . Cette approche a une priorité claire mais présente des problèmes (qui ont déjà été discutés en détail). Malgré cela, je pense que ce serait bénéfique pour ceux qui préfèrent utiliser des combinateurs. Je n'aimerais pas que ce soit la seule option implémentée, car ce n'est pas ergonomique avec le chaînage de méthodes, mais je pense que cela complimenterait bien un sigil postfix.

Je n'aime pas les autres options de préfixe, elles ne me semblent pas aussi cohérentes avec les autres constructions de flux de contrôle dans le langage. De la même manière qu'une méthode postfixe, une fonction de préfixe est incohérente, il n'y a aucune autre fonction globale intégrée au langage utilisé pour le flux de contrôle. Il n'y a pas non plus de macros utilisées pour le flux de contrôle (à l'exception de try!(..) mais c'est obsolète car nous avons une meilleure solution - un sigil postfix).


Presque toutes mes préférences se résument à ce qui semble naturel et cohérent (pour moi). Quelle que soit la solution choisie, il faut laisser du temps à l'expérimentation avant la stabilisation - l'expérience pratique sera un bien meilleur juge de la meilleure option que la spéculation.

Il vaut également la peine de considérer qu'il peut y avoir une majorité silencieuse qui pourrait avoir des opinions entièrement différentes - ceux qui s'engagent dans ces discussions (y compris moi-même) ne sont pas nécessairement représentatifs de tous les utilisateurs de Rust (ce n'est pas censé être offensant).

tl; dr Les sigils Postfix sont un moyen naturel d'exprimer l'attente (en raison de la priorité) et constituent une approche concise et cohérente. J'ajouterais un préfixe await { .. } et un suffixe @ (qui peut être utilisé dans les contextes comme ? ). Il est très important pour moi que Rust reste cohérent en interne.

@SamuelMoriarty

Je pense que dans ce cas, il est déjà assez clair du contexte que nous avons affaire à un avenir, et la verbosité du clavier d'attente est redondante. Comparer:

Désolé, mais je n'ai même pas repéré ~ en un coup d'œil. J'ai relu tout le commentaire et ma deuxième lecture a été plus réussie. Cela explique pourquoi je pense que le sceau est pire. Ce n'est qu'une opinion, mais elle vient d'être confirmée une fois de plus.

Un autre point contre les sigils est que Rust devient J.Par exemple:

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

?@? signifie "fonction qui peut renvoyer une erreur ou un futur, qui devrait être attendu, avec une erreur, le cas échéant, propagée à un appelant"

J'aimerais plus avoir

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

@rolandsteiner

Comme c'est déjà la mère de tous les fils de perte de vélo, je voulais ajouter un autre sigil qui n'a pas été mentionné jusqu'à présent AFAICT: ->.

Faire dépendre la grammaire du contexte n'améliore pas les choses, mais seulement pires. Cela entraîne de pires erreurs et des temps de compilation plus lents.

Nous avons une signification distincte pour -> , cela n'a rien à voir avec async/await .

Je préférerais le préfixe await { .. } et, si possible, un suffixe ! sigil.
Un point d'exclamation soulignerait subtilement la paresse des futurs. Ils ne s'exécuteront qu'après avoir reçu une commande avec un point d'exclamation.

L'exemple ci-dessus ressemblerait alors à ceci:

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

Désolé si mon avis n'est pas pertinent car j'ai peu d'expérience asynchrone et aucune expérience avec l'écosystème de la fonction asynchrone de Rust.

Cependant, en regardant .send()?!?.json()?!?; et d'autres combinaisons de ce genre, je comprends les raisons fondamentales pour lesquelles la proposition basée sur le sceau me semble incorrecte.

Premièrement, j'ai l'impression que les sigils enchaînés deviennent rapidement illisibles, où qu'ils soient ?!? ou ?~? ou ?->? . Ce sera une autre chose sur laquelle les débutants tomberont, en devinant s'il s'agit d'un opérateur ou de plusieurs. Les informations sont trop serrées.

Deuxièmement, en général, j'ai le sentiment que les points d'attente sont à la fois moins courants que les points de propagation d'erreur et plus significatifs. Les points d'attente sont suffisamment importants pour que je mérite d'être une étape de la chaîne à elle seule, pas une "transformation mineure" attachée à une autre étape (et surtout pas "juste une des transformations mineures"). Je serais probablement bien même avec la forme de mot-clé de préfixe uniquement (ce qui oblige presque une attente à rompre la chaîne), mais en général, cela semble trop restrictif. Mon option idéale serait probablement une macro de type méthode .await!() avec la possibilité future d'étendre le système de macros pour permettre des macros utilisateur de type méthode.

La base de référence minimale pour la stabilisation, à mon avis, est l'opérateur de préfixe await my_future . Tout le reste peut suivre.

expr....await refléterait le contexte de quelque chose qui se passe jusqu'à ce que Wait et cohérent avec les opérateurs rustlang. Async await est également un modèle parallèle..await ne peut pas être exprimé en tant que méthode ou propriété similaire

J'ai mon penchant pour await!(foo) , mais comme d'autres ont souligné que cela nous obligerait à ne pas réserver attendre et empêcherait ainsi son utilisation future en tant qu'opérateur jusqu'en 2021, j'irai avec await { foo } comme ma préférence. Quant aux postfix attendent, je n'ai pas d'avis particulier.

Je sais que ce n'est peut-être pas le meilleur endroit pour parler de l'attente implicite, mais est-ce que quelque chose comme une attente implicite explicite fonctionnerait? Donc, nous avons d'abord le préfixe d'attente d'atterrissage:

await { future }?

Et plus tard, nous ajoutons quelque chose de similaire à

let result = implicit await { client.get("https://my_api").send()?.json()?; }

ou

let result = auto await { client.get("https://my_api").send()?.json()?; }

Lors du choix du mode implicite, tout ce qui est entre {} est automatiquement attendu.

Cela a unifié la syntaxe await et équilibrerait le besoin de préfixe d'attente, d'enchaînement et d'être aussi explicite que possible.

J'ai décidé de rg --type csharp '[^ ]await' pour enquêter sur des exemples d'endroits où le préfixe était sous-optimal. Il est possible que tous ces éléments ne soient pas parfaits, mais ce sont de vrais codes qui ont subi une révision de code. (Exemples légèrement nettoyés pour supprimer certains éléments du modèle de domaine.)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Utiliser FluentAssertions comme un moyen plus agréable de faire les choses que le MSTest normal assert_eq! Assert.Equal .

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Cette chose générale de «regardez, je n'avais vraiment besoin que d'une chose» est un tas d'entre eux.

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Un autre "je n'avais besoin que d'une propriété". (A part: "mec je suis content que Rust n'ait pas besoin de CancellationToken s.)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

Le même .collect() que les gens ont mentionné dans Rust.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

J'avais pensé à peut-être aimer le mot-clé postfix, avec des nouvelles lignes rustfmt après (et après un ? , si présent), mais c'est comme ça qui me fait penser que la nouvelle ligne n'est pas bonne en général.

else if (!await container.ExistsAsync())

Un des rares où le préfixe était réellement utile.

var response = (HttpWebResponse)await request.GetResponseAsync();

Il y a eu quelques casts, bien que bien sûr les casts soient un autre endroit où Rust est postfix mais C # est préfixe.

using (var response = await this.httpClient.SendAsync(requestMsg))

Ce préfixe vs postfix n'a pas d'importance, mais je pense que c'est une autre différence intéressante: parce que C # n'a pas Drop , un tas de choses finissent par _nécessaire_ d'aller dans des variables et non enchaînées.

quelques - uns des exemples de » @scottmcm rustified dans les différentes variantes de postfix:

// keyword
response.content.read_as_string()) await?.should().be(text);
// field
response.content.read_as_string()).await?.should().be(text);
// function
response.content.read_as_string()).await()?.should().be(text);
// macro
response.content.read_as_string()).await!()?.should().be(text);
// sigil
response.content.read_as_string())@?.should().be(text);
// sigil + keyword
response.content.read_as_string())@await?.should().be(text);
// keyword
let previous = branch.list_history(timestamp_utc, None, 1) await?.history_entries.single_or_default();
// field
let previous = branch.list_history(timestamp_utc, None, 1).await?.history_entries.single_or_default();
// function
let previous = branch.list_history(timestamp_utc, None, 1).await()?.history_entries.single_or_default();
// macro
let previous = branch.list_history(timestamp_utc, None, 1).await!()?.history_entries.single_or_default();
// sigil
let previous = branch.list_history(timestamp_utc, None, 1)@?.history_entries.single_or_default();
// sigil + keyword
let previous = branch.list_history(timestamp_utc, None, 1)@await?.history_entries.single_or_default();
// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;
// field
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await?.identity).await?;
// function
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await()?.identity).await()?;
// macro
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await!()?.identity).await!()?;
// sigil
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@?.identity)@?;
// sigil + keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@await?.identity)@await?;

(merci à @ Nemo157 pour les corrections)

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

Pour moi, après avoir lu ceci, @ sigil est hors de la table, car il est juste invisible, surtout devant ? .

Je n'ai vu personne dans ce fil discuter de la variante d'attente pour Stream. Je sais que c'est hors de portée, mais devrions-nous y penser?

Ce serait dommage si nous prenions une décision qui s'avérait être un bloqueur pour attendre sur Streams.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

Vous ne pouvez pas utiliser await intérieur d'une fermeture comme celle-ci, il faudrait une méthode d'extension supplémentaire sur Option qui prend une fermeture asynchrone et renvoie un Future lui - même

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(ou une traduction plus directe du code en utilisant if let au lieu d'un combinateur)

@ Nemo157 Yeal, mais nous pouvons probablement le faire sans fonctions supplémentaires:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Mais l'approche if let me semble en effet plus naturelle.

Pour moi, après avoir lu ceci, @ sigil n'est plus sur la table, car il est simplement invisible, surtout devant?.

N'oubliez pas les schémas alternatifs de mise en évidence du code, pour moi, ce code ressemble à ceci:
1

Personnellement, je ne pense pas que l '"invisibilité" soit un problème plus important que pour les ? autonomes.

Et un jeu de couleurs intelligent peut le rendre encore plus visible (par exemple en utilisant une couleur différente de celle de ? ).

@newpavlov vous ne pouvez pas choisir le jeu de couleurs dans les outils externes, par exemple dans les onglets de révision gitlab / github.

Cela dit, ce n'est pas une bonne pratique de se fier uniquement à la mise en évidence. D'autres peuvent avoir d'autres préférences.

Salut les gars, je suis un nouvel apprenant Rust venant de C ++. Je veux juste faire un commentaire qui ne serait pas quelque chose comme

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

être fondamentalement incompréhensible? Le await est poussé vers la toute fin de la ligne tandis que notre concentration est principalement concentrée au début (ce qui est comme lorsque vous utilisez un moteur de recherche).
Quelque chose comme

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

suggéré plus tôt me semble beaucoup mieux.

Je suis d'accord. Les premiers mots sont la première chose que tout le monde regarde en scannant le code, les résultats de recherche, les paragraphes de texte, etc. Cela désavantage immédiatement tout type de suffixe.

Pour le placement ? , je suis convaincu que await? foo est suffisamment distinct et facile à apprendre.

Si nous stabilisons cela maintenant et après un an d'utilisation de deux, nous décidons que nous voulons vraiment quelque chose de mieux pour le chaînage, nous pouvons considérer les macros postfix comme une fonctionnalité générale.

Je voudrais proposer une variante de l'idée d'avoir à la fois un pré et un postfixe par @nicoburns .
On pourrait avoir 2 choses:

  • un préfixe await mot-clé (par exemple, la saveur avec une liaison plus forte que ? , mais c'est moins important)
  • une nouvelle méthode sur std::Future , par exemple fn awaited(self) -> Self::Output { await self } . Son nom pourrait aussi être block_on , ou blocking , ou quelque chose de mieux que quelqu'un d'autre trouvera.

Cela permettrait à la fois l'utilisation du préfixe "simple", mais aussi le chaînage, tout en évitant d'avoir à faire attendre un mot-clé contextuel.

Techniquement, le deuxième point peut également être accompli avec une macro postfix, auquel cas nous écririons .awaited!() .

Cela permettrait un code qui ressemble à ceci:

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

Mis à part d'autres problèmes, le point principal de cette proposition est d'avoir await comme bloc de construction de base du mécanisme, tout comme match , puis d'avoir des combinateurs pour nous éviter d'avoir à taper beaucoup de mots-clés et accolades de toutes sortes. Je pense que cela implique qu'ils peuvent être enseignés de la même manière: d'abord le simple await , puis pour éviter trop de parenthèses, on peut utiliser .awaited() et enchaîner.


Alternativement, une version plus magique pourrait complètement supprimer le mot-clé await et s'appuyer sur une méthode magique .awaited() sur std :: Future, qui ne peut pas être implémentée par d'autres qui écrivent leur propre futur, mais je pense ce serait assez contre-intuitif et un cas trop particulier.

une nouvelle méthode sur std::Future , par exemple fn awaited(self) -> Self::Output { await self }

Je suis à peu près sûr que c'est impossible (sans rendre la fonction magique), car pour attendre à l'intérieur, il faudrait que ce soit async fn awaited(self) -> Self::Output { await self } , qui lui-même devrait encore être await ed. Et si nous envisageons de rendre la fonction magique, cela pourrait tout aussi bien être le mot-clé, IMO.

Il existe déjà Future::wait (bien qu'apparemment 0.3 ne l'ait pas encore?), Qui exécute un futur bloquant le thread.

Le problème est que _le point entier de await est de _pas_ bloquer le thread_. Si la syntaxe à attendre est un préfixe et que nous voulons une option postfix, ce doit être une macro postfix et non une méthode.

Et si vous voulez dire utiliser une méthode magique, appelez-la simplement .await() , ce qui a déjà été discuté plusieurs fois dans le fil de discussion, à la fois comme méthode par mot-clé et comme méthode magique extern "rust-await-magic" "réel "fn.

EDIT: scottmcm ninja'd moi et GitHub ne m'ont pas informé (parce que mobile?), Je vais quand même laisser ça.

@scottmcm Pourriez-vous également examiner le nombre de fréquences où await semble correct par rapport à sous-optimal? Je pense qu'une enquête sur le nombre de fréquences préfixe vs postfixe pourrait aider à répondre à plusieurs questions.

  1. J'ai l'impression que jusqu'à présent, le meilleur cas d'utilisation de postfix await a été
client.get("https://my_api").send() await?.json() await?

autant de messages utilisent comme exemple. J'ai peut-être manqué quelque chose, mais y a-t-il d'autres cas? Ne serait-il pas bon d'extraire cette ligne dans une fonction si elle apparaît fréquemment dans toute la base de code?

  1. Comme je l'ai mentionné précédemment, si certaines personnes écrivent
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

ce qu'ils font est de cacher exactement visuellement le await plutôt que de le rendre explicite afin que chaque point de rendement soit clairement visible .

  1. Le mot clé Postfix nécessiterait d'inventer quelque chose qui n'existe pas dans les autres langages traditionnels. S'il n'offre pas un résultat nettement meilleur, l'inventer ne vaudrait pas la peine.

L'attente est poussée vers la toute fin de la ligne tandis que notre concentration est principalement concentrée au début (ce qui est comme lorsque vous utilisez un moteur de recherche).

Je ne peux évidemment pas réfuter le fait que les gens analysent le contenu Web en utilisant un modèle en forme de F , @ dowchris97.

Ce n'est pas une évidence, cependant, qu'ils utilisent le même modèle pour le code. Par exemple, cet autre sur la page semble mieux correspondre à la façon dont les gens recherchent ? , et pourrait donc chercher await :

Le motif tacheté consiste à sauter de gros morceaux de texte et à scanner comme si vous recherchiez quelque chose de spécifique, comme un lien, des chiffres, un mot particulier ou un ensemble de mots avec une forme distinctive (comme une adresse ou une signature).

À titre de comparaison, prenons le même exemple s'il a renvoyé Result au lieu de impl Future :

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

Je pense que c'est assez clair qu'un modèle de lecture en forme de F pour un code comme celui-là n'aide pas à trouver les ? s, ni ne l'aide s'il est divisé en plusieurs lignes comme

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

Je pense qu’une description analogue à la vôtre pourrait être utilisée pour affirmer que la position du suffixe "masque exactement visuellement le ? plutôt que de le rendre explicite pour que le point de retour puisse être clairement vu", ce qui, je suis d’accord, était l'original concerne ? , mais cela ne semble pas avoir été un problème dans la pratique.

Donc, dans l'ensemble, je pense que le mettre là où les gens ont déjà été formés pour scanner pour ? est le meilleur moyen de s'assurer que les gens le voient. Je préférerais certainement qu'ils n'aient pas à utiliser deux scans différents simultanément.

@scottmcm

Ce n'est pas une évidence, cependant, qu'ils utilisent le même modèle pour le code. Par exemple, cet autre sur la page semble mieux correspondre à la façon dont les gens recherchent ? , et pourrait donc chercher await :

Le motif tacheté consiste à sauter de gros morceaux de texte et à scanner comme si vous recherchiez quelque chose de spécifique, comme un lien, des chiffres, un mot particulier ou un ensemble de mots avec une forme distinctive (comme une adresse ou une signature).

Il n'y a certainement pas non plus de preuves que l'utilisation d'un motif repéré aide à comprendre le code mieux / plus rapidement. Surtout lorsque les êtres humains utilisent le plus souvent le motif en forme de F, que je mentionnerai plus tard.

Prenez cette ligne comme exemple, quand commencez-vous à utiliser un motif tacheté, supposons que vous ne l’ayez jamais lu auparavant.

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Pour moi, j'ai commencé quand j'ai vu async , puis je vais d'abord await? , puis self.storage_coordinator.get_default_widget_async() , puis .identity , puis j'ai finalement réalisé toute la ligne est asynchrone. Je dirais que ce n'est certainement pas l'expérience de lecture que j'aime. Une des raisons est que notre système de langage écrit n'a pas ce genre de sauts en avant et en arrière entrelacés . Lorsque vous sautez, cela interrompt la construction du modèle mental de ce que fait cette ligne.

À titre de comparaison, que fait cette ligne, comment le savez-vous?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

Dès que j'atteins await? , j'ai immédiatement eu un avertissement que c'était asynchrone. J'ai alors lu let widget = await? , encore une fois, sans aucune difficulté, je savais que c'était asynchrone, il se passait quelque chose. Je sens que je suis le modèle de forme F. Selon https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/ , la forme F est le modèle le plus couramment utilisé. Alors, allons-nous concevoir un système qui correspond à la nature humaine ou inventer un système qui nécessite une éducation spéciale et qui va à l'encontre de notre nature ? Je préfère le premier. La différence serait encore plus forte lorsque les lignes asynchrones et normales sont entrelacées comme ceci

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

Suis-je censé chercher await? travers les lignes?

let id = id.try_or_else (|| Ok (self.storage_coordinator.try_get_default_widget () ?. identity)) ?;

Pour cela, je ne pense pas que la comparaison soit bonne. Premièrement, ? ne change pas tellement le modèle mental. Deuxièmement, le succès de ? réside dans le fait qu'il ne vous oblige pas à sauter en avant et en arrière entre les mots. Le sentiment que j'ai en lisant dans ? en .try_get_default_widget()? est: "ok, vous en obtenez un bon résultat". C'est ça. Je n'ai pas besoin de revenir en arrière pour lire autre chose pour comprendre cette ligne.

Donc, ma conclusion générale est que postfix peut fournir une commodité limitée lors de l'écriture de code. Cependant, cela peut causer de plus gros problèmes pour la lecture du code, ce qui, selon moi, est plus fréquent que l'écriture.

À quoi cela ressemblerait-il avec une mise en évidence du style et de la syntaxe rustfmt ( while -> async , match -> await ):

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

Je ne sais pas pour vous, mais je repère instantanément les match es.

(Hey @ CAD97 , je l'ai réparé!)

@ dowchris97

Si vous prétendez que ? ne change pas le modèle mental, qu'est-ce qui ne va pas avec mon argument selon lequel await ne devrait pas changer votre modèle mental du code? (C'est pourquoi j'ai déjà privilégié les options d'attente implicites, même si j'ai été convaincu que ce n'est pas la bonne solution pour Rust.)

Plus précisément, vous lisez async au début de l'en-tête de la fonction. Si la fonction est si grande qu'elle ne tient pas sur un écran, elle est certainement trop grande et vous avez d'autres problèmes de lisibilité plus importants que de trouver les points await .

Une fois que vous êtes async , vous devez tenir ce contexte. Mais à ce stade, tout attendre est une transformation qui transforme un calcul différé Future en le résultat Output en stationnant le train d'exécution actuel.

Peu importe que cela signifie une transformation complexe de la machine à états. C'est un détail de mise en œuvre. La seule différence avec les threads du système d'exploitation et le blocage est que d'autres codes peuvent s'exécuter sur le thread actuel pendant que vous attendez le calcul différé, donc Sync ne vous protège pas autant. (Si quoi que ce soit, je lirais cela comme une exigence pour que async fn soit Send + Sync au lieu d'être inféré et autorisé à être thread-unsafe.)

Dans un style beaucoup plus axé sur les fonctions, vos widgets et vos résultats ressembleraient à (oui, je sais que cela n'utilise pas une Monad et une vraie pureté, etc. pardonnez-moi):

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

Mais comme c'est l'ordre inverse du pipeline de processus, la foule fonctionnelle a inventé l'opérateur "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;

Et dans Rust, cet opérateur de pipelining est . , qui fournit la portée de ce qui peut être pipeliné et une recherche dirigée par type via l'application de méthode:

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

Lorsque vous pensez à . comme des données de pipeline de la même manière que |> , les chaînes plus longues souvent vues dans Rust commencent à avoir plus de sens, et lorsqu'elles sont bien formatées (comme dans l'exemple de Centril) ne manquez pas la lisibilité car vous avez juste un pipeline vertical de transformations sur les données.

await ne vous dit pas "hé c'est asynchrone". async fait. await est la façon dont vous vous garez et attendez le calcul différé, et il est tout à fait logique de le rendre disponible à l'opérateur de pipelining de Rust.

(Hey @Centril vous avez oublié d'en faire un async fn (ou while fn ), ce qui dilue un peu mon point 😛)

si nous pouvons ou non redéfinir l'invocation de macro

m!(item1, item2)

est le même que

item1.m!(item2)

afin que nous puissions utiliser le style wait à la fois préfixe et suffixe

await!(future)

et

future.await!()

@ CAD97

await ne vous dit pas "hé c'est asynchrone". async fait. await est juste la façon dont vous vous garez et attendez le calcul différé, et il est tout à fait logique de le rendre disponible à l'opérateur de pipelining de Rust.
Oui, je suppose que je comprends bien mais je n'ai pas écrit rigoureusement.

Je comprends également votre point. Mais pas convaincu, je pense toujours que mettre await à l'avant sera bien meilleur.

Je sais que |> est utilisé dans d'autres langages pour signifier autre chose , mais cela me semble plutôt bien et extrêmement clair dans Rust à la place du préfixe 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)
}

L'argument concernant l'ordre de lecture s'appliquerait également à l'opérateur ? remplaçant try!() . Après tout, "hé cela pourrait céder" est important, mais "hé cela pourrait revenir tôt" est également important, sinon plus. Et en effet, des préoccupations concernant la visibilité ont été soulevées à plusieurs reprises dans la discussion sur le bikeshed sur ? (y compris ce fil de discussion interne et ce problème GitHub ). Mais la communauté a finalement décidé de l'approuver et les gens s'y sont habitués. Il serait étrange de se retrouver maintenant avec les modificateurs ? et await apparaissant sur les côtés opposés d'une expression, simplement parce que, fondamentalement, la communauté a changé d'avis sur l'importance de la visibilité.

L'argument concernant l'ordre de lecture s'appliquerait également à l'opérateur ? remplaçant try!() . Après tout, "hé cela pourrait céder" est important, mais "hé cela pourrait revenir tôt" est également important, sinon plus. Et en effet, des préoccupations concernant la visibilité ont été soulevées à plusieurs reprises dans la discussion sur le bikeshed sur ? (y compris ce fil de discussion interne et ce problème GitHub ). Mais la communauté a finalement décidé de l'approuver et les gens s'y sont habitués. Il serait étrange de se retrouver maintenant avec les modificateurs ? et await apparaissant sur les côtés opposés d'une expression, simplement parce que, fondamentalement, la communauté a changé d'avis sur l'importance de la visibilité.

Ce n'est vraiment pas ce que fait votre code. Il s'agit de votre modèle mental de ce que fait votre code, de savoir si le modèle peut être facilement construit ou non, s'il y a des chocs ou non, qu'il soit intuitif ou non. Ceux-ci peuvent différer largement de l'un à l'autre.

Je ne veux plus en débattre. Je pense que la communauté est loin de s'installer sur une solution postfixe bien que de nombreuses personnes ici puissent la soutenir. Mais je pense qu'il pourrait y avoir une solution:

Mozilla construit Firefox, non? Tout est question d'UI / UX! Qu'en est-il d'une recherche HCI sérieuse sur ce problème? On peut donc vraiment se convaincre en utilisant des données et non des conjectures.

@ dowchris97 Il y a eu (seulement) deux exemples de comparaison de code du monde réel dans ce fil: une comparaison d'environ 24 000 lignes avec des bases de données et reqwest et une comparaison qui illustre pourquoi C # n'est pas une comparaison précise avec la syntaxe de base de Rust . Les deux s'avèrent que dans le code Rust du monde réel, attendre dans postfix semble plus naturel et ne souffre pas des problèmes que rencontrent d'autres langages sans sémantique de déplacement de valeur naturelle. À moins que quelqu'un ne se présente avec un autre exemple du monde réel de taille décente qui tend à montrer le contraire, je suis assez convaincu que la syntaxe de préfixe est une nécessité imposée par d'autres langages à eux-mêmes parce qu'ils n'ont pas la sémantique de valeur claire du pipeline de Rust (où lire du code idiomatique de gauche à droite fait presque toujours exactement ce que le modèle mental suggère).

Edit: Juste si ce n'est pas assez clair, aucun des C #, Python, C ++, Javascript n'a de méthodes membres qui prennent self par valeur au lieu de référence. C ++ a le plus proche avec les références rvalue mais l'ordre du destructeur est toujours déroutant par rapport à Rust.

Je pense que l'argument selon lequel await est meilleur en tant que préfixe ne découle pas de la façon dont vous devez changer votre modèle mental du code, mais plutôt de la façon dont nous avons des mots-clés préfixes, mais pas de mots-clés postfixes, rust utilise des sigils comme illustré par ? ). C'est pourquoi await foo() semble plus facile à lire que foo() await et pourquoi certaines personnes veulent @ à la fin d'une déclaration et n'aiment pas avoir await là-bas.

Pour une raison similaire, .await semble étrange à utiliser: l'opérateur point est utilisé pour accéder aux champs et aux méthodes d'une structure (c'est aussi pourquoi il ne peut pas être considéré comme un opérateur de pipeline pur), donc avoir .await revient à dire "le point est utilisé pour accéder aux champs et aux méthodes d'une structure ou pour accéder à await , qui n'est ni un champ ni une fonction".

Personnellement, j'aimerais voir soit le préfixe await soit un sceau de suffixe (il n'est pas nécessaire que ce soit @ ).

Les deux s'avèrent que dans le code Rust Realworld, attendre dans postfix semble plus naturel

C'est une déclaration controversée. L'exemple reqwest ne présente qu'une seule version de la syntaxe postfix.

Sur une note différente, si cette discussion se résume à un vote de qui aime quoi de plus, veuillez le mentionner sur reddit afin que les gens ne se plaignent pas comme ils l'ont fait avec impl Trait en arguments de fonction.

@ eugene2k Pour la discussion fondamentale sur l'adéquation de postfix au modèle mental des programmeurs Rust, la plupart ou toutes les syntaxes postfix sont à peu près à la même échelle que le préfixe. Je ne pense pas qu'il y ait autant de différence de lisibilité significative entre les variantes postfix qu'entre prefix et postfix. Voir aussi ma comparaison de bas niveau de la priorité des opérateurs qui conclut que leur sémantique est égale dans la plupart des utilisations, donc c'est vraiment une question de savoir quel opérateur transmet le mieux le sens (je préfère actuellement une syntaxe d'appel de fonction réelle, mais ne ont une forte préférence sur les autres).

@ eugene2k Les décisions à Rust ne sont jamais prises par vote. Rust n'est pas une démocratie, c'est une méritocratie.

Les équipes core / lang examinent tous les différents arguments et perspectives, puis décident. Cette décision est prise par consensus (parmi les membres de l'équipe) et non par vote.

Bien que les équipes de Rust prennent absolument en compte les désirs généraux de la communauté, elles décident finalement en fonction de ce qu'elles pensent être le mieux pour Rust à long terme.

La meilleure façon d'influencer Rust est de présenter de nouvelles informations, ou de faire de nouveaux arguments, ou de montrer de nouvelles perspectives.

Répéter les arguments existants ou dire «moi aussi» (ou similaire) n'augmente pas les chances qu'une proposition soit acceptée. Les propositions ne sont jamais acceptées en fonction de leur popularité.

Cela signifie également que les divers votes positifs / négatifs de ce fil n'ont pas d'importance pour la proposition acceptée.

(Je ne fais pas référence à vous spécifiquement, j'explique comment les choses fonctionnent pour le bien de tout le monde dans ce fil.)

@Pauan Il a déjà été dit que les équipes core / lang examinent diverses propositions puis décident. Mais les décisions de «ce qui est plus facile à lire» sont des décisions personnelles. Aucun argument logique présenté au décideur ne changera sa vision personnelle de ce qu'il préfère. De plus, les arguments comme «c'est ainsi que les gens lisent les résultats de recherche» et «une étude a été menée par telle ou telle recherche ont montré que les gens préfèrent ceci et cela» sont facilement contestés (ce qui n'est pas une mauvaise chose). Ce qui peut changer l'esprit du décideur, c'est de voir et de ne pas aimer les résultats de l'application de sa décision dans un contexte auquel il n'a pas pensé. Ainsi, lorsque tous ces contextes ont été examinés et que les décideurs de l'équipe aiment une approche, tandis que la plupart des autres utilisateurs, qui ne font pas partie de l'équipe, aiment une autre approche, quelle devrait être la décision finale?

Ainsi, lorsque tous ces contextes ont été examinés et que les décideurs de l'équipe aiment une approche, tandis que la plupart des autres utilisateurs, qui ne font pas partie de l'équipe, aiment une autre approche, quelle devrait être la décision finale?

La décision est toujours prise par l'équipe. Période. C'est ainsi que les règles ont été conçues intentionnellement.

Et les fonctionnalités sont souvent mises en

Si la situation change (peut-être en fonction des commentaires) et que les membres de l'équipe changent d'avis, ils peuvent alors changer leur décision. Mais même dans ce cas, la décision est toujours prise par les membres de l'équipe.

Comme vous le dites, les décisions impliquent souvent une certaine subjectivité, et il est donc impossible de plaire à tout le monde, mais une décision doit être prise. Afin de parvenir à une décision, le système utilisé par Rust est basé sur le consensus des membres de l'équipe.

Toute discussion sur la question de savoir si Rust devrait être gouverné différemment est hors sujet et devrait être discutée ailleurs.

(PS je ne fais pas partie des équipes core ou lang, donc je n'ai aucune autorité dans cette décision, donc je dois m'en remettre à eux, tout comme vous)

@HeroicKatora

Je ne pense pas qu'il y ait autant de différence de lisibilité significative entre les variantes postfix

Je ne suis pas d'accord. Je trouve que foo().await()?.bar().await()? est plus facile à lire que foo() await?.bar() await? ou même foo()@?.bar()@? Malgré cela, j'ai l'impression que des méthodes qui ne sont pas vraiment des méthodes créent un mauvais précédent.

Je voudrais proposer une autre idée. Je suis d'accord que le préfixe Wait n'est pas facile à enchaîner avec d'autres fonctions. Que diriez-vous de cette syntaxe de postfix: foo(){await}?.bar()?{await} ? Il ne peut pas être confondu avec les appels de fonction et me semble assez facile à lire dans une chaîne.

Et encore une autre proposition écrite par moi. Considérons la syntaxe d'appel de méthode suivante:

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

Ce qui le rend unique parmi les autres propositions:

  • Les crochets rendent la priorité et la portée beaucoup plus propres.
  • La syntaxe est suffisamment extensible pour permettre également la suppression des liaisons temporaires dans d'autres contextes.

Je pense que l'extensibilité est ici le plus gros avantage. Cette syntaxe permettrait d'implémenter des fonctionnalités de langage qui, dans leur forme courante, ne sont pas possibles dans Rust en raison du rapport complexité / utilité élevé. La liste des constructions de langage possibles est fournie ci-dessous:

  1. Report de tous les opérateurs de préfixe (y compris await - c'est comme ça que ça doit fonctionner):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Fonctionnalité de l'opérateur 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. Remplacement du retour de fonction:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Fonctionnalité de Wither:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. Division de la chaîne:
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();

Je pense que l'introduction d'un nouveau type de syntaxe (champs magiques, macros postfixes, crochets) a un impact plus important sur le langage que cette fonctionnalité seule, et devrait nécessiter une RFC.

Existe-t-il un référentiel qui utilise déjà fortement await ? Je proposerais de réécrire un plus gros morceau dans chaque style proposé, afin que nous puissions avoir une meilleure idée de leur apparence et de la compréhension du code.

Je réécris dans les délimiteurs obligatoires:

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

C'est presque identique à await!() . Alors, c'est magnifique! Après avoir utilisé await!() pendant un an ou deux, pourquoi inventeriez-vous soudainement un postfix wait qui n'apparaît nulle part dans l'histoire du langage de programmation.

Huh. La syntaxe expr.[await it.foo()] @ I60R avec le mot clé contextuel it est assez soignée. Je ne m'attendais pas à aimer les nouvelles propositions de syntaxe, mais c'est vraiment très bien, c'est une utilisation intelligente de l'espace de syntaxe (car IIRC .[ n'est actuellement pas une syntaxe valide nulle part), et résoudrait beaucoup plus problèmes que d'attendre.

A convenu que cela nécessiterait certainement une RFC, et que cela pourrait ne pas être la meilleure option. Mais je pense que c'est un autre point du côté du règlement d'une syntaxe de préfixe pour await pour le moment, sachant qu'il existe un certain nombre d'options pour résoudre le problème des "opérateurs de préfixe sont difficiles à enchaîner" d'une manière plus générale qui profite plus que simplement async / wait à l'avenir.

Et encore une autre proposition écrite par moi. Considérons la syntaxe d'appel de méthode suivante:

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

Ce qui le rend unique parmi les autres propositions:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

Je pense que l'extensibilité est ici le plus gros avantage. Cette syntaxe permettrait d'implémenter des fonctionnalités de langage qui, dans leur forme courante, ne sont pas possibles dans Rust en raison du rapport complexité / utilité élevé. La liste des constructions de langage possibles est fournie ci-dessous:

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

Vraiment incompréhensible.

@tajimaha En regardant votre exemple, je pense que await {} pourrait en fait être bien meilleur que await!() si nous utilisons des délimiteurs obligatoires, car cela évite le problème de "trop ​​de crochets" qui peut entraîner la lisibilité problèmes avec la syntaxe await!() .

Comparer:
`` `c #
attendre {foo.bar (url, false, qux.clone ())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps Vous pouvez obtenir une coloration syntaxique de async et await pour des exemples simples en définissant le langage sur c #.)

@nicoburns Vous pouvez utiliser l'un des éléments suivants: () , {} ou [] avec la macro.

@sgrif C'est un bon point. Et comme c'est le cas, je suggérerais que l'option "mot-clé préfixe avec des délimiteurs obligatoires" n'a pas de sens. Comme il rompt la cohérence avec d'autres macros pour pratiquement aucun avantage.

(FWIW, je pense toujours que "préfixe sans délimiteurs" avec une solution générale comme les macros postfix ou la suggestion de @ I60R pour postfix a plus de sens. Mais l'option "juste coller avec la macro existante" se développe sur moi ...)

Je suggérerais que l'option "mot-clé préfixe avec délimiteurs obligatoires" n'a aucun sens. Comme il rompt la cohérence avec d'autres macros pour pratiquement aucun avantage.

Pourquoi cela n'a-t-il pas de sens et pourquoi un mot clé n'ayant pas la même syntaxe qu'une macro est un problème?

@tajimaha

Je suggérerais que l'option "mot-clé préfixe avec délimiteurs obligatoires" n'a aucun sens. Comme il rompt la cohérence avec d'autres macros pour pratiquement aucun avantage.

Pourquoi cela n'a-t-il pas de sens et pourquoi un mot clé n'ayant pas la même syntaxe qu'une macro est un problème?

Eh bien, si await était une macro, cela aurait l'avantage de ne pas ajouter de syntaxe supplémentaire au langage. Réduisant ainsi la complexité de la langue. Cependant, il existe un bon argument pour utiliser un mot-clé: return , break , continue , et d'autres constructions modifiant le flux de contrôle sont également des mots-clés. Mais tous fonctionnent sans délimiteurs, donc pour être cohérent avec ces constructions, await devrait également fonctionner sans délimiteurs.

Si vous avez des délimiteurs en attente avec obligatoire, vous avez:

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

Ceci est potentiellement déroutant car vous devez maintenant vous souvenir de 3 formes de syntaxe au lieu de 2. Et étant donné que les mots-clés avec délimiteurs obligatoires n'offrent aucun avantage par rapport à la syntaxe des macros, je pense qu'il serait préférable de s'en tenir à la norme syntaxe de macro si nous souhaitons appliquer des délimiteurs (ce que je ne suis pas du tout convaincu que nous devrions).

Une question pour ceux qui utilisent beaucoup async / await dans Rust aujourd'hui: à quelle fréquence attendez-vous des fonctions / méthodes vs variables / champ?

Le contexte:

Je sais qu'en C #, il est courant de faire des choses qui se résument à ce modèle:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

De cette façon, les deux fonctionnent en parallèle. (Certains diront que Task.WhenAll devrait être utilisé ici, mais la différence de performance est minime, et cela rend le code plus compliqué car il nécessite de passer par des index de tableau.)

Mais je crois comprendre que, dans Rust, cela ne fonctionnera pas du tout en parallèle, puisque poll pour fooTask ne sera pas appelé tant que bar n'aura pas sa valeur, et il _nécessite_ d'utiliser un combinateur, peut-être

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

Donc, étant donné cela, je suis curieux de savoir si l'on finit régulièrement par avoir l'avenir dans une variable ou un champ que vous devez attendre, ou si l'on attend presque toujours des expressions d'appel. Parce que si c'est le dernier, il existe une petite variante de mise en forme du mot-clé postfix dont nous n'avons pas vraiment discuté: le mot-clé postfix _no-space_.

Je n'ai pas beaucoup réfléchi à savoir si c'était bon, mais il serait possible d'écrire du code aussi simplement

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

(Je ne veux pas vraiment avoir une discussion rustfmt, comme je l'ai dit, mais je me souviens que l'une des raisons pour ne pas aimer le mot-clé postfix _ était_ l'espace interrompant le découpage visuel.)

Si nous acceptons cela, autant utiliser la syntaxe .await pour tirer parti
la puissance du point, non?

Une question pour ceux qui utilisent beaucoup async / await dans Rust aujourd'hui: à quelle fréquence attendez-vous des fonctions / méthodes vs variables / champ?

Mais je crois comprendre que, à Rust, cela ne fonctionnera pas du tout en parallèle [...]

Correct. À partir de la même base de code qu'avant, voici cet exemple:

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

Si je devais réécrire la fermeture avec une fermeture 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())?
};

Si je devais passer à la syntaxe .await (et l'enchaîner):

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

Y a-t-il un référentiel qui est déjà fortement utilisé? Je proposerais de réécrire un plus gros morceau dans chaque style proposé

@gralpli Malheureusement, rien de ce que je peux open source n'utilise massivement await! . Il se prête définitivement plus au code d'application pour le moment (surtout en étant si instable).

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

Ces lignes montrent exactement comment le code est perturbé par une utilisation excessive de postfix et du chaînage .

Voyons la version du préfixe:

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

Les deux versions utilisent toutes les deux 7 lignes mais IMO la seconde est plus propre. Il existe également deux points à retenir pour l'utilisation de délimiteurs obligatoires:

  1. La await { future }? n'a pas l'air bruyant si la partie future est longue. Voir let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. Lorsque la ligne est courte, utiliser await(future) pourrait être mieux. Voir Ok(await(req.json())?)

IMO, en basculant entre les deux variantes, la lisibilité de ce code est bien meilleure qu'avant.

Le premier exemple est mal formaté. Je ne pense pas que rustfmt le formate comme
cette. Pourriez-vous s'il vous plaît exécuter rustfmt dessus et le poster à nouveau ici?

@ivandardi @mehcode Pourriez-vous faire ça? Je ne sais pas comment je peux formater la syntaxe .await . Je viens de copier le code. Merci!

Je voudrais ajouter que cet exemple montre:

  1. Le code de production ne sera pas seulement des chaînes simples et agréables comme:
client.get("https://my_api").send().await?.json().await?
  1. Les gens peuvent abuser ou abuser du chaînage.
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?;

Ici, la fermeture asynchrone gère chaque ID, cela n'a rien à voir avec un contrôle de niveau supérieur futures_unordered . Les assembler réduit considérablement votre capacité à le comprendre.

Tout _ était_ parcouru par rustfmt de mon message (avec quelques modifications mineures pour le faire compiler). L'endroit où le .await? est placé n'est pas encore décidé et actuellement je le place à la fin de la ligne attendue.


Maintenant, je suis d'accord que tout cela a l'air assez horrible. C'est du code écrit sur une date limite et les choses sont vouées à avoir l'air affreuses lorsque vous avez une salle de cuisiniers qui tourne pour sortir quelque chose.

Je tiens à souligner (à partir de votre point) que l'abus du préfixe peut sembler bien pire (à mon avis bien sûr):

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

Maintenant, amusons-nous et rendons les choses beaucoup plus agréables en utilisant le recul et quelques nouveaux adaptateurs dans Futures v0.3

Préfixe avec préséance "évidente" (et sucre)
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();
Préfixe avec priorité "utile"
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()?;
Préfixe avec délimiteurs obligatoires
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()
}?;
Champ 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?;
Mot-clé 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?;

Petite nit ici. Il n'y a pas de TryStreamExt::and_then , il devrait probablement y en avoir. Cela ressemble à un PR facile pour quiconque a du temps et veut contribuer.


  • Je veux, encore une fois, exprimer ma forte aversion pour await? Dans les longues chaînes, je perds complètement la trace des ? que j'ai appris à rechercher à la fin des expressions pour signifier que cette expression est faillible et peut _fermer la fonction_.

  • Je veux en outre exprimer mon aversion croissante pour await .... ? (priorité utile) en considérant ce qui se passerait si nous avions un fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

Je tiens à souligner (à partir de votre point) que l'abus du préfixe peut sembler bien pire (à mon avis bien sûr):

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

Ce n'est pas vraiment un problème car en javascript python, les gens sont plus susceptibles de les écrire dans des lignes séparées. Je n'ai pas réellement vu await (await f) en python.

Préfixe avec délimiteurs obligatoires

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

Cela semble également être de retour à l'utilisation de combinateurs. Tandis que le but d'introduire async / await est de réduire son utilisation le cas échéant.

Eh bien, c'est tout l'intérêt d'avoir postfix en attente. Puisqu'il s'agit de Rust, les gens sont moins susceptibles de les écrire sur des lignes séparées, car Rust encourage le chaînage. Et pour cela, la syntaxe postfix est essentiellement obligatoire pour que le flux d'instructions suive le même flux de lecture de ligne. Si nous n'avons pas de syntaxe postfix, alors il y aura beaucoup de code avec des temporaires qui sont également chaînés, alors que si nous avions postfix wait, tout pourrait être réduit à une seule chaîne.

@ivandardi @mehcode Copie de Rust RFC sur async / await:

Après avoir acquis de l'expérience et des retours utilisateurs avec l'écosystème futuriste, nous avons découvert certains défis ergonomiques. L'utilisation d'un état qui doit être partagé entre les points d'attente était extrêmement peu ergonomique - nécessitant des arcs ou un chaînage de jointures - et bien que les combinateurs soient souvent plus ergonomiques que l'écriture manuelle d'un futur, ils conduisaient encore souvent à des ensembles désordonnés de rappels imbriqués et chaînés.

... utiliser avec un sucre syntaxique qui est devenu courant dans de nombreux langages avec async IO - les mots-clés async et await.

Du point de vue de l'utilisateur, ils peuvent utiliser async / await comme s'il s'agissait de code synchrone, et n'ont qu'à annoter leurs fonctions et leurs appels.

Donc, tout l'intérêt de l'introduction de l'attente asynchrone est de réduire le chaînage et de faire du code asynchrone comme s'il était synchronisé . Le chaînage n'est mentionné que deux fois dans cette RFC, "nécessitant soit des arcs, soit un chaînage de jointure" et "encore souvent conduit à des ensembles désordonnés de rappels imbriqués et chaînés". Cela ne me semble pas trop positif.

Argumenter pour le chaînage et donc le mot-clé suffixe nécessiterait probablement une réécriture majeure de cette RFC.

@tajimaha Vous map , and_then , etc.), il ne parle pas de chaînage en général (par exemple les méthodes qui retournent impl Future ).

Je pense qu'il est prudent de dire que les méthodes async seront assez courantes, donc le chaînage est en effet assez important.

De plus, vous comprenez mal le processus: le RFC n'a pas besoin d'être réécrit. La RFC est un point de départ, mais ce n'est pas une spécification. Aucun des RFC n'est gravé dans la pierre (et ils ne devraient pas l'être!)

Le processus RFC est fluide, il n'est pas aussi rigide que vous le laissez entendre. Rien dans la RFC ne nous empêche de discuter de postfix await .

De plus, tous les changements seront mis dans la RFC de stabilisation , pas dans la RFC d'origine (qui a déjà été acceptée et ne sera donc pas modifiée).

Argumenter pour le chaînage et donc le mot-clé suffixe nécessiterait peut-être une réécriture majeure de cette RFC.

Je plaisante ici.

Une chose est probablement vraie lors de l'écriture de la RFC. Les gens voulaient spécifiquement un nouvel outil "qui peut utiliser async / await comme s'il s'agissait de code synchrone". La tradition suivant la syntaxe répondrait mieux à cette promesse.
Et là vous voyez, ce ne sont pas les futurs combinateurs,

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

Pourtant, «ils conduisaient encore souvent à des ensembles désordonnés de rappels imbriqués et chaînés».

Les gens voulaient spécifiquement un nouvel outil "qui peut utiliser async / await comme s'il s'agissait de code synchrone". La tradition suivant la syntaxe répondrait mieux à cette promesse.

Je ne vois pas comment c'est vrai: le préfixe et le suffixe await répondent à ce désir.

En fait, postfix await répond probablement mieux à ce désir, car c'est très naturel avec les chaînes de méthodes (qui sont très courantes dans le code Rust synchrone!)

L'utilisation du préfixe await encourage fortement de nombreuses variables temporaires, ce qui n'est souvent pas le style Rust idiomatique.

Pourtant, «ils conduisaient encore souvent à des ensembles désordonnés de rappels imbriqués et chaînés».

Je vois exactement une fermeture, et ce n'est pas un rappel, c'est juste appeler map sur un Iterator (rien à voir du tout avec Futures!)

Veuillez ne pas essayer de contourner les mots de la RFC pour justifier le préfixe await .

Utiliser la RFC pour justifier le préfixe await est très étrange, car la RFC elle-même dit que la syntaxe n'est pas définitive et sera décidée plus tard. Le moment est venu de prendre cette décision.

La décision sera prise sur la base des mérites des différentes propositions, le RFC original est complètement hors de propos (sauf comme référence historique utile).

Notez que le dernier exemple de @ mehcode utilise principalement des combinateurs _stream_ (et le futur combinateur pourrait être remplacé de manière triviale par un bloc async). C'est l'équivalent de l'utilisation de combinateurs d'itérateurs dans du code synchrone, donc peut être utilisé dans certaines situations où ils sont plus appropriés que les boucles.

C'est hors sujet, mais la plupart de la conversation ici est menée par une douzaine de commentateurs. Sur les 383 commentaires au moment où j'ai gratté ce numéro, il n'y avait que 88 affiches uniques. Dans un effort pour éviter d'épuiser / surcharger quiconque devrait aller lire ces commentaires, je recommanderais d'être aussi minutieux que possible dans vos commentaires et de veiller à ce qu'il ne s'agisse pas d'une réitération d'un point précédent.


Histogramme des commentaires

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

Notez que le dernier exemple de @ mehcode utilise principalement des combinateurs _stream_ (et le futur combinateur pourrait être remplacé de manière triviale par un bloc async). C'est l'équivalent de l'utilisation de combinateurs d'itérateurs dans du code synchrone, donc peut être utilisé dans certaines situations où ils sont plus appropriés que les boucles.

La même chose peut être soutenue ici que je peux / devrais utiliser le préfixe await là où il est plus approprié que le chaînage.

@Pauan Apparemment, ce ne sont pas que des mots tordus. Je montre un problème de code réel, écrit par un supporteur de syntaxe postfix. Et comme je l'ai dit, le code de style préfixe illustre mieux votre intention alors qu'il n'a pas nécessairement beaucoup de temporaires comme les partisans de postfix se plaignent (du moins dans ce cas). aussi, supposons que votre code ait une chaîne à une seule ligne avec deux attentes, comment puis-je déboguer la première? (c'est une vraie question et je ne sais pas).
Deuxièmement, la communauté rust est de plus en plus grande, les gens de divers horizons (comme moi, j'utilise le plus python / c / java) ne seront pas tous d'accord sur le fait que les chaînes de méthodes sont les meilleures façons de faire les choses. J'espère que lors de la prise de décision, ce n'est pas (ne devrait pas être) uniquement basé sur le point de vue des premiers adoptants.

@tajimaha Le plus grand changement de clarté du post-correctif au préfixe semble être l'utilisation d'une fermeture locale pour supprimer certains arguments de fonction imbriqués. Cela ne me semble pas unique au préfixe, ceux-ci sont assez orthogonaux. Vous pouvez faire la même chose pour postfix, et je pense que c'est encore plus clair. Je conviens que c'est peut-être une mauvaise utilisation du chaînage pour certaines bases de code, mais je ne vois pas en quoi cette mauvaise utilisation est unique ou liée à postfix de manière majeure.

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

Mais, dans postfix, la liaison let et la Ok sur le résultat peuvent être supprimées ensemble le dernier ? pour fournir directement le résultat, puis le bloc de code est également inutile selon par goût personnel. Cela ne fonctionne pas très bien dans le préfixe du tout en raison de deux attentes dans la même instruction.

Je ne comprends pas le sentiment régulièrement exprimé selon lequel les liaisons sont unidiomatiques dans le code Rust. Ils sont assez fréquents et courants dans les exemples de code, en particulier autour de la gestion des résultats. Je vois rarement plus de 2 ? dans le code que je traite.

De plus, ce qu'est idiomatic change au cours de la vie d'une langue, je prendrais donc grand soin de l'utiliser comme argument.

Je ne sais pas si quelque chose comme ça a déjà été suggéré, mais un préfixe await mot-clé pourrait-il s'appliquer à une expression entière? Prenons l'exemple qui a été évoqué auparavant:

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

get , send et json sont asynchrones.

Pour moi (ayant peu d'expérience asynchrone dans d'autres langages de programmation), le suffixe expr await semble naturel: "Faites ceci, _puis_ attendez le résultat."

Certains craignaient que les exemples suivants semblent étranges:

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

Cependant, je dirais que cela devrait être divisé en plusieurs lignes:

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

Ceci est beaucoup plus clair et présente l'avantage supplémentaire que le await est facile à repérer, s'il est toujours à la fin de la ligne.

Dans un IDE, cette syntaxe n'a pas la "puissance du point", mais est toujours meilleure que la version du préfixe: Lorsque vous tapez le point et que vous remarquez ensuite que vous devez await , il vous suffit de supprimer le point et tapez " await ". Autrement dit, si l'EDI n'offre pas de saisie semi-automatique pour les mots clés.

La syntaxe du point expr.await est déroutante car aucun autre mot-clé de flux de contrôle n'utilise un point.

Je pense que le problème est que bien que nous ayons un chaînage, ce qui peut parfois être joli, il ne faut pas aller à l'extrême en disant que tout doit être fait en chaînage. Nous devrions également fournir des outils pour la programmation de style C ou Python. Bien que Python n'ait presque pas de composant de chaînage, son code est souvent loué pour être lisible. Les programmeurs Python ne se plaignent pas non plus d'avoir trop de variables temporaires.

Que diriez-vous d'un suffixe then ?

Je ne comprends pas le sentiment régulièrement exprimé selon lequel les liaisons sont unidiomatiques dans le code Rust. Ils sont assez fréquents et courants dans les exemples de code, en particulier autour de la gestion des résultats. Je vois rarement plus de 2 ? dans le code que je traite.

De plus, ce qu'est idiomatic change au cours de la vie d'une langue, je prendrais donc grand soin de l'utiliser comme argument.

Cela m'a inspiré à examiner le code Rust actuel où il y a deux ? ou plus sur une ligne (peut-être que quelqu'un peut examiner l'utilisation multiligne). J'ai sondé xi-éditeur, alacritty, ripgrep, chauve-souris, xray, fd, pétard, if, Rocket, exa, fer, parité-ethereum, tikv. Ce sont des projets Rust avec la plupart des stars.

Ce que je trouve, c'est qu'environ 40 lignes seulement sur un total de ? sur une ligne. C'est 0,006% .

Je tiens également à souligner que l' étude des modèles d'utilisation de code existants ne révélera pas l'expérience utilisateur de l'écriture de nouveau code.

Supposons que l'on vous donne un travail pour interagir avec une nouvelle API ou que vous débutez dans l'utilisation des requêtes. Est-ce que tu vas probablement écrire

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

en un seul coup ? Si vous êtes nouveau dans l'API, je doute que vous souhaitiez vous assurer que vous construisez la demande correctement, voyez l'état de retour, vérifiez votre hypothèse sur ce que cette API renvoie ou jouez simplement avec l'API comme ceci:

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

Une API réseau n'a rien à voir avec les données mémoire, vous ne savez pas ce qu'il y a. C'est assez naturel pour le prototypage. Et, lorsque vous effectuez un prototypage, vous vous inquiétez de ce que tout va bien PAS trop de variables temporaires . Vous pouvez dire que nous pouvons utiliser la syntaxe postfix comme:

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

Mais, si vous avez déjà ceci:

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

Tout ce que vous avez à faire est probablement de l'envelopper dans une fonction et votre travail est terminé, le chaînage n'émerge même pas dans ce processus.

Ce que je trouve, c'est qu'environ 40 lignes seulement sur 585562 lignes au total utilisent deux ou plus? en une seule ligne.

Je voudrais suggérer que ce n'est pas une mesure utile. Ce qui compte vraiment, ce sont plusieurs opérateurs de flux de contrôle _ par expression_. Selon le style typique (rustfmt), ils finiront presque toujours sur des lignes différentes dans le fichier bien qu'ils appartiennent à la même expression, et seraient donc enchaînés dans le montant que postfix (théoriquement) compte pour await .

il ne faut pas aller à l'extrême en disant que tout doit être fait en chaînage

Quelqu'un a-t-il dit que tout devrait être fait par chaînage? Tout ce que j'ai vu jusqu'à présent, c'est qu'il devrait être _ergonomique_ d'enchaîner dans les cas où cela a du sens, comme cela se produit dans le code synchrone.

seulement environ 40 lignes sur 585562 lignes au total utilisent deux ou plus? en une seule ligne.

Je ne suis pas sûr que ce soit pertinent pour le préfixe par rapport au suffixe. Je noterai que _aucun_ de mes exemples C # de recherche de suffixe n'a inclus plusieurs await s dans une ligne, ni même plusieurs await s dans une instruction. Et l'exemple de @Centril de disposition potentielle de postfix ne mettait pas non plus plusieurs await s sur une ligne.

Une meilleure comparaison pourrait être des choses enchaînées ? , comme ces exemples du compilateur:

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: On dirait que vous m'avez battu cette fois , @ CAD97 : légèrement_smiling_face:

Ceci est étonnamment similaire au code de promesse de javascipt avec beaucoup de then s. Je n'appellerais pas cela synchrone. Il est presque certain que le chaînage a un await et fait semblant d'être synchrone.

Préfixe avec délimiteurs obligatoires

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 Bon point. Je savais qu'il y a des limites à ce que je mesure:

(peut-être que quelqu'un peut examiner l'utilisation de plusieurs lignes)

Je fais cela parce que @skade a mentionné la similitude entre await et ? , alors j'ai fait une analyse rapide. Je donne une idée sans faire de recherche sérieuse. Je pense qu'une analyse plus fine serait difficile à faire en regardant le code, non? Il peut avoir besoin d'analyser et d'identifier des expressions, avec lesquelles je ne suis pas familier. J'espère que quelqu'un pourra faire cette analyse.

Quelqu'un a-t-il dit que tout devait être fait par chaînage? Tout ce que j'ai vu jusqu'à présent, c'est qu'il devrait être ergonomique d'enchaîner dans les cas où cela a du sens, comme cela se produit dans le code synchrone.

Ce que je dis, c'est que si seulement postfix est ajouté, ce ne serait pas ergonomique quand le style C / Python semble bon (bien sûr imo). Je traite également du chaînage peut ne pas être nécessaire lorsque vous effectuez un prototype .

J'ai le sentiment que la direction de ce fil est beaucoup trop axée sur le chaînage excessif et sur la façon de rendre le code d'attente aussi concis que possible.

Je veux encourager tout le monde à regarder en quoi le code asynchrone est différent des variantes synchrones, et comment cela influencera l'utilisation et l'utilisation des ressources. Moins de 5 commentaires sur les 400 de ce fil mentionnent ces différences. Si vous n'êtes pas conscient de ces différences, récupérez la version nocturne actuelle et essayez d'écrire un morceau décent de code asynchrone. Y compris essayer d'obtenir une version asynchrone / d'attente idiomatique (non combinateur) du morceau de code de compilation dont il est question dans les 20 derniers articles.

Cela fera une différence si les choses existent entre purement entre les points de rendement / attente, si les références sont persistantes à travers les points d'attente, et souvent certaines exigences particulières autour des futurs ne permettent pas d'écrire du code aussi concis que celui imaginé dans ce fil. Par exemple, nous ne pouvons pas placer des fonctions asynchrones arbitraires dans des combinateurs arbitraires, car ceux-ci pourraient ne pas fonctionner avec les types !Unpin . Si nous créons des futures à partir de blocs asynchrones, ils pourraient ne pas être directement compatibles avec des combinateurs comme join! ou select! , car ils ont besoin de types épinglés et fusionnés, donc des appels supplémentaires à pin_mut! et .fuse() peut être requis dans.

De plus, pour travailler avec des blocs asynchrones, les nouveaux utilitaires basés sur les macros fonctionnent join! et select! fonctionnent bien mieux que les anciennes variantes du combinateur. Et ce sont de la manière excessive qui est souvent fournie ici à titre d'exemples

Je ne sais pas comment postfix await peut fonctionner avec .unwrap() dans cet exemple de tokio le plus simple

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

Si le préfixe est adopté, il deviendra

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

Mais si postfix est adopté,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

Y a-t-il une explication intuitive que nous pouvons donner aux utilisateurs? Cela entre en conflit avec les règles existantes. await est un champ? ou await est une liaison qui a une méthode appelée unwrap() ? DOMMAGE! Nous déballons beaucoup lorsque nous démarrons un projet. Violation de plusieurs règles de conception dans The Zen of Python.

Les cas spéciaux ne sont pas assez spéciaux pour enfreindre les règles.
Si l'implémentation est difficile à expliquer, c'est une mauvaise idée.
Face à l'ambiguïté, refusez la tentation de deviner.

Y a-t-il une explication intuitive que nous pouvons donner aux utilisateurs? Cela entre en conflit avec les règles existantes. await est un champ? ou await est une liaison qui a une méthode appelée unwrap() ? DOMMAGE! Nous déballons beaucoup lorsque nous démarrons un projet. Violation de plusieurs règles de conception dans The Zen of Python.

Je dirais, bien qu'il y ait trop de documents dans docs.rs qui appellent unwrap , unwrap devrait être remplacé par ? dans de nombreux cas réels. Au bail, c'est ma pratique.

Ce que je trouve, c'est qu'environ 40 lignes seulement sur 585562 lignes au total utilisent deux ou plus? en une seule ligne.

Je voudrais suggérer que ce n'est pas une mesure utile. Ce qui compte vraiment, ce sont plusieurs opérateurs de flux de contrôle _ par expression_. Selon le style typique (rustfmt), ils finiront presque toujours sur des lignes différentes dans le fichier bien qu'ils appartiennent à la même expression, et seraient donc enchaînés dans le montant que postfix (théoriquement) compte pour await .

Je rejette qu'il puisse y avoir des limites à cette approche.

J'ai sondé à nouveau pour xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, if, Rocket, exa, iron, parity-ethereum, tikv. Ce sont des projets Rust avec la plupart des stars. Cette fois, j'ai cherché un motif:

xxx
  .f1()
  .f2()
  .f3()
  ...

et s'il existe plusieurs opérateurs de flux de contrôle dans ces expressions.

J'ai identifié SEULEMENT 15 chaînes sur 7066 ayant plusieurs opérateurs de flux de contrôle. C'est 0,2% . Ces lignes couvrent 167 sur 585562 lignes de code. C'est 0,03% .

@cenwangumass Merci d'avoir pris le temps et de quantifier. :cœur:

Une considération est que puisque Rust a une reliure variable avec let, il peut faire un argument convaincant pour le préfixe await , en ce que s'il est utilisé de manière cohérente, vous auriez une ligne de code distincte pour chaque await -point. L'avantage étant double: les traces de pile, car le numéro de ligne donne un plus grand contexte quant à l'endroit où le problème s'est produit, et la facilité de débogage des points d'arrêt, car il est courant de vouloir définir un point d'arrêt sur chaque point d'attente séparé pour inspecter les variables, peut l'emporter sur la brièveté de une seule ligne de code.

Personnellement, je suis déchiré entre le style préfixe et le sigil postfixe après avoir lu https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 Je suis probablement principalement en faveur d'un sigil postfix.

Style de préfixe rendu:

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

Style de postfix rendu:

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

Sigil postfixe rendu @:

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

Rendu du sigil postfixe #:

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

Je n'ai pas vu assez de gens parler de await pour Stream s. Bien que cela soit hors de portée, prendre la décision autour de await avec un peu de prévoyance à partir de Stream pourrait en valoir la peine.

Nous aurons probablement besoin de quelque chose comme ceci:
Syntaxe du préfixe

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Syntaxe de 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!()? {
    ...
}

Syntaxe probablement la plus ergonomique / cohérente

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

Je veux juste m'assurer que ces exemples sont discutés au moins un peu, car si postfix est bien pour enchaîner Future , à première vue, il semble le moins intuitif pour Stream . Je ne sais pas quelle est la bonne solution ici. Peut-être postfixe await pour Future et un style différent de await pour Stream ? Mais pour différentes syntaxes, nous aurions besoin de nous assurer que l'analyse n'est pas ambiguë.

Ce ne sont que mes premières réflexions, cela vaut peut-être la peine Stream réfléchir davantage à la Stream , ou si nous devrions avoir deux syntaxes?

La syntaxe pour l'itération de flux n'est pas quelque chose qui va se produire pendant un certain temps, j'imagine. Il est assez facile d'utiliser une boucle while let et de le faire manuellement:

Champ Postfix
while let Some(value) = stream.try_next().await? {
}
Préfixe avec préséance "cohérente" et sucre
while let Some(value) = await? stream.try_next() {
}
Préfixe avec délimiteurs obligatoires
while let Some(value) = await { stream.try_next() }? {
}

Ce serait la façon actuelle de faire les choses (plus await ).


Je noterai que cet exemple fait que "préfixe avec délimiteurs" me paraît particulièrement mauvais. Alors que await(...)? peut sembler un peu plus agréable, si nous devions faire "préfixe avec des délimiteurs", j'espère que nous n'autoriserons qu'un seul type de délimiteur (comme try { ... } ).

Tangente rapide, mais "attendre avec des délimiteurs" ne serait-il pas juste ... un préfixe normal attend? await attend une expression, donc avoir await expr et await { expr } sont essentiellement les mêmes. Cela n'aurait pas de sens de n'avoir attendu qu'avec des délimiteurs et de ne pas l'avoir sans trop, d'autant plus que toute expression peut être entourée de {} et continuer à être la même expression.

La question sur les flux m'a conduit à penser que pour Rust, il pourrait être tout à fait naturel de disposer d'une attente non seulement en tant qu'opérateur dans les expressions, mais aussi en tant que modificateur dans les modèles:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

qui peut alors naturellement fonctionner avec for

for async x in my_stream { ... }

@ivandardi

Le problème que «préfixe attendre avec des délimiteurs obligatoires» résout est la question de priorité autour de ? . Avec les délimiteurs obligatoires, il n'y a pas de question de priorité.

Voir try { .... } pour la syntaxe _similaire_ (stable). Try a des délimiteurs obligatoires pour la même raison - comment il interagit avec ? - car un ? à l'intérieur des accolades est très différent de celui à l'extérieur.

@yazaddaruvala Je ne pense pas qu'il devrait y avoir un moyen spécifique asynchrone de gérer une boucle for avec des résultats plus agréables, cela devrait simplement provenir d'une fonctionnalité générique qui traite également de Iterator<Item = Result<...>> .

// postfix syntax
for response in stream await {
    ...
}

Cela implique que stream: impl Future<Output = Iterator> et que vous attendez que l'itérateur soit disponible, pas chaque élément. Je ne vois pas beaucoup de chance pour quelque chose de mieux que async for item in stream (sans une fonctionnalité plus générale comme les mentions @tanriol ).


@ivandardi la différence est la priorité une fois que vous commencez à enchaîner à la fin des délimiteurs, voici deux exemples qui analysent différemment avec "préfixe de priorité évidente", "préfixe de précédence utile" (du moins ce que je comprends de la priorité "utile") et "préfixe des délimiteurs obligatoires".

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

à analyser (avec suffisamment de délimiteurs pour que ceux-ci soient sans ambiguïté pour les trois 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()?;

À mon avis, les exemples de @scottmcm de C # dans https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 semblent très bien avec les délimiteurs déplacés de la position (await foo).bar à la position await(foo).bar :

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Etc. Cette mise en page est familière des appels de fonction normaux, familière du flux de contrôle basé sur des mots clés existant et familière des autres langages (y compris C #). Cela évite de casser le budget d'étrangeté et ne semble pas poser de problèmes pour le chaînage post- await .

Bien que cela pose le même problème que try! pour le chaînage multi- await , cela ne semble pas être aussi important que pour Result ? Je préférerais de loin limiter l'étrangeté de la syntaxe ici plutôt que d'accommoder un modèle relativement rare et sans doute illisible.

et familier avec d'autres langages (y compris C #)

Ce n'est pas ainsi que la priorité fonctionne en C # (ou Javascript, ou Python)

await(response.Content.ReadAsStringAsync()).Should().Be(text);

est équivalent à

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

(l'opérateur point a une priorité plus élevée que await , donc se lie plus étroitement même si vous essayez de faire ressembler await à un appel de fonction).

Je sais, ce n'est pas ce que je prétendais. Simplement que l'ordre reste le même, donc la seule chose que les gens doivent ajouter est des parenthèses, et que (séparément) la syntaxe d'appel de fonction existante a la bonne priorité.

Cette disposition est [...] familière du flux de contrôle existant basé sur des mots-clés

Je ne suis pas d'accord. En fait, nous nous opposons à l'utilisation de cette disposition dans le flux de contrôle existant basé sur des mots-clés:

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

Et rustfmt change cela en return (4); , _ajout dans_ un espace.

Cela n'affecte pas vraiment le point que j'essaie de faire valoir et donne l'impression de fendre les cheveux. return (4) , return(4) , ce n'est rien de plus qu'un problème de style.

J'ai lu l'intégralité du numéro et j'ai commencé à penser que les accolades et le préfixe {} allaient bien, mais ils ont ensuite disparu

Voyons voir:

let foo = await some_future();
let bar = await {other_future()}?.bar

Ça a l'air sympa, n'est-ce pas? Mais que faire si nous voulons enchaîner plus de await dans une chaîne?

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

IMHO ça a l'air bien pire que

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

Cependant, étant dit, je ne crois pas au chaînage asynchrone, comme écrit dans le post de exemple d'utilisation de async/await dans un hyper serveur, veuillez montrer où le chaînage pourrait être utile en utilisant cet exemple concret. Je suis vraiment tenté, pas de blagues.


A propos des exemples précédents, la plupart d'entre eux sont "corrompus" d'une certaine manière par des combinateurs. Vous n'en avez presque jamais besoin depuis que vous attendez. Par exemple

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

est juste

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

si les futures peuvent renvoyer une erreur, c'est aussi simple que:

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 ,

J'ai créé un thread pré-RFC pour la syntaxe val.[await future] sur https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await -chaining / 9304

Les questions de procédure https://internals.rust-lang.org

Je pense que nous avons deux camps dans cette discussion: les personnes qui voudraient mettre l'accent sur les points de rendement par rapport aux personnes qui aimeraient les réduire.

Async dans Rust, vers 2018, contient le texte de présentation suivant:

La notation asynchrone / attente est une façon de rendre la programmation asynchrone plus proche de la programmation synchrone.

Prenant l' exemple de unwrap() en ? ):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

et l'application du sigil postfixe conduit à

let response = client.get(uri).timeout(Duration::from_secs(10))!?

qui ressemble étroitement à la programmation synchrone et manque de distractions entrecoupées de await de notation à la fois préfixe et suffixe.

J'ai utilisé ! comme sigil de suffixe dans cet exemple, même si cette suggestion m'a valu des votes négatifs dans un commentaire précédent. Je l'ai fait parce que le point d'exclamation a une signification inhérente pour moi dans ce contexte qui manque à la fois @ (que mon cerveau lit comme "at") et # . Mais c'est simplement une question de goût et non mon argument.

Je préférerais simplement tout sigil postfix à un seul caractère à toutes les autres alternatives précisément parce qu'il est très discret, ce qui facilite la prise en main de ce que le code que vous lisez fait réellement par rapport au fait qu'il soit ou non asynchrone, ce qui Je considérerais un détail de mise en œuvre. Je ne dirais pas que ce n'est pas du tout important, mais je dirais que c'est beaucoup moins important pour le flux du code que le retour anticipé dans le cas de ? .

Pour le dire autrement: vous vous souciez principalement des points de rendement lors de l'écriture de code asynchrone, pas tellement lors de sa lecture. Puisque le code est lu plus souvent qu'écrit, une syntaxe discrète pour await serait utile.

Je voudrais rappeler l'expérience de l'équipe C # (l'accent est mis sur moi):

C'est aussi pourquoi nous n'avons utilisé aucune forme «implicite» pour «attendre». Dans la pratique, c'était quelque chose à laquelle les gens voulaient réfléchir très clairement, et qu'ils voulaient au centre de leur code afin d'y prêter attention. Chose intéressante, même des années plus tard, cette tendance est restée. c'est-à-dire que parfois nous regrettons plusieurs années plus tard que quelque chose soit excessivement verbeux . Certaines fonctionnalités sont bonnes de cette manière dès le début, mais une fois que les gens sont à l'aise avec cela, elles sont mieux adaptées à quelque chose de plus terser. Cela n'a pas été le cas avec «attendre». Les gens semblent toujours aimer la nature lourde de ce mot-clé

Les caractères Sigil sont presque invisibles sans mise en évidence appropriée et sont moins conviviaux pour les nouveaux arrivants (comme je l'ai probablement déjà dit).


Mais en parlant de sigils, @ ne confondra probablement pas les non-anglophones (par exemple moi) parce que nous ne le lisons pas comme at . Nous avons un tout autre nom qui ne se déclenche pas automatiquement lorsque vous lisez le code, vous le comprenez donc comme un hiéroglyphe entier, avec sa propre signification.

@huxi J'ai le sentiment que les sigils sont déjà assez exclus. Consultez à nouveau l' article de await .


Aussi, pour tous ceux qui n'aiment pas postfix attendent autant, et utilisent l'argument pragmatique de "bien, il n'y a pas beaucoup de code réel qui pourrait être enchaîné, donc donc postfix await ne devrait pas être ajouté", voici une petite anecdote (que je vais probablement massacrer à cause d'une mauvaise mémoire):

À l'époque de la Seconde Guerre mondiale, l'une des nations combattantes perdait beaucoup d'avions là-bas. Ils ont dû renforcer leurs avions d'une manière ou d'une autre. Donc, le moyen le plus évident de savoir sur quoi ils doivent se concentrer est de regarder les avions qui sont revenus et de voir où les balles ont le plus touché. Il s'avère que dans un avion, en moyenne 70% des trous étaient dans les ailes, 10% dans la zone du moteur et 20% dans d'autres zones de l'avion. Donc, avec ces statistiques, il serait logique de renforcer les ailes, n'est-ce pas? Faux! La raison en est que vous ne regardez que les avions qui sont revenus. Et dans ces avions, il semble que les dommages causés par les balles sur les ailes ne soient pas si graves. Cependant, tous les avions qui sont revenus n'ont subi que des dommages mineurs à la zone du moteur, ce qui peut conduire à la conclusion que des dommages majeurs à la zone du moteur sont mortels. La zone du moteur doit donc être renforcée à la place.

Mon point avec cette anecdote est: peut-être qu'il n'y a pas beaucoup d'exemples de code réels qui peuvent tirer parti de l'attente de postfix car il n'y a pas d'attente de postfix dans d'autres langues. Donc, tout le monde a l'habitude d'écrire du code d'attente de préfixe et y est très habitué, mais on ne peut jamais savoir si les gens commenceraient à enchaîner les attend plus si nous avions postfix en attente.

Je pense donc que le meilleur plan d'action serait de tirer parti de la flexibilité que les constructions nocturnes nous offrent et de choisir une syntaxe de préfixe et une syntaxe de suffixe à ajouter au langage. Je vote pour le préfixe "Obvious Precedence", et le suffixe .await attend. Ce ne sont pas les choix de syntaxe finaux, mais nous devons en choisir un pour commencer, et je pense que ces deux choix de syntaxe fourniraient une expérience plus récente par rapport aux autres choix de syntaxe. Une fois que nous les avons implémentés tous les soirs, nous pouvons obtenir des statistiques d'utilisation et des opinions sur le travail avec du code réel en utilisant les deux options et nous pouvons ensuite continuer la discussion sur la syntaxe, cette fois soutenue par un meilleur argument pragmatique.

@ivandardi L'anecdote est assez lourde et IMHO ne convient pas: c'est une histoire motivante au début d'un voyage, pour rappeler aux gens de rechercher le non-évident et de couvrir tous les angles, ce n'est pas un conte à utiliser contre l'opposition dans une discussion. C'était approprié pour Rust 2018, où il a été soulevé et non lié à un problème spécifique. L'utiliser contre un autre camp dans un débat est impoli, vous devez affirmer la position de la personne qui voit plus ou a plus de vision pour que cela fonctionne. Je ne pense pas que ce soit ce que tu veux. De plus, en restant dans l'image, peut-être que postfix n'est dans aucune des langues car postfix n'est jamais rentré chez lui;).

Les gens ont en fait mesuré et regardé: nous avons un opérateur chaînable dans Rust ( ? ) qui est rarement utilisé pour le chaînage. https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

Il a également été bien expliqué qu'il ne s'agit que d'une mesure de l'état actuel, comme @cenwangumass l' a joliment https://github.com/rust-lang/rust/issues/57640#issuecomment -457962730. Ce n'est donc pas comme si les gens utilisaient cela comme des chiffres définitifs.

Je veux cependant une histoire où le chaînage devient un style à moitié dominant si postfix est vraiment la voie à suivre. Je n'en suis cependant pas convaincu. J'ai également mentionné ci-dessus que l'exemple dominant utilisé ici ( reqwest ) ne nécessite que 2 attentes car l'API l'a choisi et une API de chaîne pratique sans avoir besoin de 2 attentes peut facilement être construite aujourd'hui.

Bien que j'apprécie le besoin d'une phase de recherche, je tiens à souligner que l'attente est déjà sérieusement retardée, toute phase de recherche aggravera la situation. Nous n'avons également aucun moyen de collecter des statistiques sur de nombreuses bases de code, nous devrons assembler nous-mêmes un ensemble parlant. J'adorerais plus de recherches sur les utilisateurs ici, mais cela prend du temps, une configuration qui n'existe pas encore et des gens pour l'exécuter.

@Pzixel à cause de la reliure, je pense que vous pourriez écrire

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

comme

''
let foo = attendre some_future ();
let bar = attendre {other_future ()} ?. bar_async ();
let bar = attendre {bar} ?;

@Pzixel Avez-vous une source pour cette citation d'expérience de l'équipe C #? Parce que le seul que j'ai pu trouver était ce commentaire . Ce n'est pas une accusation ou quelque chose du genre. Je voudrais juste lire le texte intégral.

Mon cerveau traduit @ en "at" en raison de son utilisation dans les adresses e-mail. Ce symbole est appelé "Klammeraffe" dans ma langue maternelle qui se traduit en gros par "singe accroché". J'apprécie en fait que mon cerveau se soit contenté de "at" à la place.

Mes 2 cents, en tant qu'utilisateur Rust relativement nouveau (avec un fond C ++, mais cela n'aide pas vraiment).

Certains d'entre vous ont mentionné les nouveaux arrivants, voici ce que je pense:

  • tout d'abord, une macro await!( ... ) me semble indispensable, car elle est extrêmement simple à utiliser et ne peut être mal comprise. Cela ressemble à println!() , panic!() , ... et c'est ce qui s'est passé pour try! après tout.
  • Les délimiteurs obligatoires sont également simples et sans ambiguïté.
  • une version postfixe, que ce soit un champ, une fonction ou une macro, ne serait pas difficile à lire ou à écrire à mon humble avis, car les nouveaux venus diraient simplement "ok, c'est comme ça que vous faites". Cet argument vaut également pour le mot clé postfix, c'est "inhabituel" mais "pourquoi pas".
  • concernant la notation de préfixe avec une priorité utile, je pense que cela semblerait déroutant. Le fait que await lie plus étroitement va épater certains esprits et je pense que certains utilisateurs préféreront simplement mettre beaucoup de parenthèses pour le rendre clair (chaînage ou non).
  • la préséance évidente sans sucre est simple à comprendre et à enseigner. Ensuite, pour introduire le sucre autour de lui, il suffit d'appeler await? un mot-clé utile. Utile car cela supprime le besoin de parenthèses encombrantes:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids? attendre ... unwraps the future, so you have to use ? to unwrap the Result // but there is some sugar if you want, thanks to the attendent? `Opérateur
    laissez la réponse = attendre? http :: get ("https://www.rust-lang.org/");
    // mais vous ne devriez pas enchaîner, car cette syntaxe ne conduit pas à un code chaîné lisible
- 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")
};

Comment enseigner cela:

  • ( await!() macro possible)
  • préfixe recommandé en l'absence de chaînage, avec du sucre (voir ci-dessus)
  • postfix recommandé avec chaînage
  • il est possible de les mélanger, mais pas recommandé
  • possibilité d'utiliser le préfixe lors du chaînage avec des combinateurs

D'après mon expérience personnelle, je dirais que le préfixe d'attente n'est pas un problème pour le chaînage.
Le chaînage ajoute beaucoup de code Javascript avec le préfixe await et des combinateurs comme f.then(x => ...) sans perdre de lisibilité à mon avis et ils ne semblent pas ressentir le besoin d'échanger des combinateurs pour postfix wait.

Faire:

let ret = response.await!().json().await!().to_string();

est le même que:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

Je ne vois pas vraiment les avantages de postfix qui attendent au-dessus des chaînes de combinateur.
Je trouve plus facile de comprendre ce qui se passe dans le deuxième exemple.

Je ne vois aucun problème de lisibilité, de chaînage ou de priorité dans le code suivant:

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

Je serais en faveur de ce préfixe attendre car:

  • de la familiarité avec d’autres langues.
  • de l'alignement avec les autres mots-clés (return, break, continue, yield, etc ...).
  • cela ressemble toujours à Rust (avec des combinateurs comme nous le faisons avec des itérateurs).

Async / await sera un excellent ajout au langage et je pense que plus de gens vont utiliser plus de choses liées au futur après cet ajout et sa stabilisation.
Certains pourraient même découvrir la programmation asynchrone pour la première fois avec Rust.
Il peut donc être avantageux de limiter la complexité du langage alors que les concepts derrière l'async peuvent déjà être difficiles à apprendre.

Et je ne pense pas que l'envoi à la fois du préfixe et du suffixe en attente soit une bonne idée, mais ce n'est qu'une opinion personnelle.

@huxi Oui, je l'ai déjà lié ci-dessus, mais bien sûr, je peux le refaire: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Mon cerveau traduit @ en "at" en raison de son utilisation dans les adresses e-mail. Ce symbole est appelé "Klammeraffe" dans ma langue maternelle qui se traduit en gros par "singe accroché". J'apprécie en fait que mon cerveau se soit contenté de "at" à la place.

J'ai une histoire similaire: dans ma langue, c'est "chien", mais cela n'affecte pas la lecture des e-mails.
Ravi de voir votre expérience cependant.

@llambda la question portait sur le chaînage. Bien sûr, vous pouvez simplement introduire des variables supplémentaires. Mais quoi qu'il en await { foo }? ce await? foo ou foo await? air nul.

@totorigolo
Belle publication. Mais je ne pense pas que votre deuxième suggestion soit une bonne solution. Lorsque vous introduisez deux façons de faire quelque chose, vous ne produisez que de la confusion et des problèmes, par exemple vous avez besoin de l'option rustfmt ou votre code devient un désordre.

@Hirevo async/await sont censés supprimer le besoin dans les combinateurs. Ne revenons pas à "mais vous pouvez le faire uniquement avec des combinateurs". Votre code est le même que

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

Supprimons donc async/await ensemble?

Cela dit, les combinateurs sont moins expressifs, moins pratiques, et parfois vous ne pouvez tout simplement pas exprimer ce que await pourrait, par exemple emprunter entre des points d'attente (pour quoi Pin été conçu).

Je ne vois aucun problème de lisibilité, de chaînage ou de priorité dans le code suivant

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

Je fais:

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

Quelque chose de bizarre se passe? Aucun problème:

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

Il est plus difficile de le faire fonctionner avec des combinateurs. Sans oublier que vos fonctions ? in then / map fonctionneront pas comme prévu, et votre code ne fonctionnera pas sans into_future() et quelques autres choses étranges dont vous n'avez pas besoin dans async/await flow.

Je suis d'accord que ma proposition n'est pas optimale en ce qu'elle introduit beaucoup de syntaxe légale en utilisant le mot-clé async . Mais je pense que les règles sont faciles à comprendre et qu'elles satisfont tous nos cas d'utilisation.

Mais encore une fois, cela signifierait beaucoup de variations autorisées:

  • await!(...) , par cohérence avec try!() et des macros bien connues comme println!()
  • await , await(...) , await { ... } , soit. syntaxe de préfixe sans sucre
  • await? , await?() , await? {} , ie. syntaxe de préfixe avec sucre
  • ... await , soit. syntaxe postfix (plus ... await? , mais qui n'est pas du sucre)
  • toutes les combinaisons de celles-ci, mais qui ne sont pas encouragées (voir mon article précédent )

Mais en pratique, nous nous attendons à voir seulement:

  • préfixe sans Result s: await , ou await { ... } pour clarifier avec des expressions longues
  • préfixe avec Result s: await? , ou await? {} pour clarifier avec des expressions longues
  • suffixe lors du chaînage: ... await , ... await?

+1 pour expédier attendez! () Macro à stable. Demain, si possible 😄

Il y a beaucoup de spéculations sur la façon dont ce modèle _will_ sera utilisé et des préoccupations concernant l'ergonomie dans de tels cas. J'apprécie ces préoccupations, mais je ne vois pas de raison impérieuse pour laquelle cela ne pourrait pas être un changement itératif. Cela permettra de générer des métriques d'utilisation réelles qui pourront ultérieurement informer l'optimisation (en supposant que cela soit nécessaire).

Si l'adoption d'Async Rust est un effort plus important pour les caisses / projets majeurs que tout autre effort de refactorisation _ optionnel_ ultérieur pour passer à une syntaxe plus concise / expressive, alors je recommande vivement de permettre à cet effort d'adoption de commencer maintenant. L'incertitude persistante cause de la douleur.

@Pzixel
(Tout d'abord, j'ai fait une erreur en appelant la fonction fetch_user, je la corrigerai dans ce post)

Je ne dis pas que async / await est inutile et que nous devrions le supprimer pour les combinateurs.
Await permet de lier une valeur d'un futur à une variable dans la même portée après sa résolution, ce qui est vraiment utile et même pas possible avec des combinateurs seuls.
Supprimer wait n'était tout simplement pas mon point.
Je viens de dire que les combinateurs peuvent très bien jouer avec await, et que le problème de chaînage peut être résolu en n'attendant que l'expression entière construite par les combinateurs (supprimant le besoin de await (await fetch("test")).json() ou await { await { fetch("test") }.json() } ).

Dans mon exemple de code, le ? se comporte comme prévu en court-circuitant, ce qui fait que la fermeture renvoie un Err(...) , pas la fonction entière (cette partie est gérée par le await? sur toute la chaîne).

Vous avez effectivement réécrit mon exemple de code mais vous en avez supprimé la partie chaînage (en faisant des liaisons).
Par exemple, pour la partie débogage, ce qui suit a exactement le même comportement avec juste le dbg nécessaire! et rien de plus (pas même de parenthèses supplémentaires):

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

Je ne sais pas comment faire la même chose sans combinateurs et sans liaisons ou parenthèses supplémentaires.
Certaines personnes font du chaînage pour éviter de remplir la portée avec des variables temporaires.
Je voulais juste dire que les combinateurs peuvent être utiles et ne doivent pas être ignorés lors de la prise de décision sur une syntaxe particulière concernant sa capacité de chaînage.

Et, enfin, par curiosité, pourquoi le code ne fonctionnerait pas sans .into_future() , ne sont-ils pas déjà des futurs (je ne suis pas un expert en la matière, mais je m'attendrais à ce qu'ils soient déjà des futurs)?

Je voudrais souligner un problème important avec fut await : cela perturbe sérieusement la façon dont les gens lisent le code. Il y a une certaine valeur dans les expressions de programmation qui sont similaires aux phrases utilisées dans un langage naturel, c'est l'une des raisons pour lesquelles nous avons des constructions comme for value in collection {..} , pourquoi dans la plupart des langages nous écrivons a + b ("a plus b") au lieu de a b + , et écrire / lire "attendre quelque chose" est beaucoup plus naturel pour l'anglais (et d'autres langues SVO ) que "quelque chose attend". Imaginez simplement qu'au lieu de ? nous aurions utilisé un suffixe try mot-clé: let val = foo() try; .

fut.await!() et fut.await() n'ont pas ce problème car ils ressemblent à des appels de blocage familiers (mais "macro" met également l'accent sur la "magie" associée), donc ils seront perçus différemment d'un espace séparé mot-clé. Sigil sera également perçu différemment, d'une manière plus abstraite, sans aucun parallèle direct avec des phrases en langage naturel, et pour cette raison je pense que la façon dont les gens lisent les sigils proposés n'a pas d'importance.

En conclusion: s'il est décidé de s'en tenir au mot-clé, je crois fermement que nous devrions choisir parmi les variantes de préfixe uniquement.

@rpjohnst Je ne suis donc pas sûr de votre point de vue. actual_fun(a + b)? et break (a + b)? comptent pour moi à cause de la priorité différente, donc je ne sais pas ce que await(a + b)? est censé être.

J'ai suivi cette discussion de loin et j'ai quelques questions et commentaires.

Mon premier commentaire est que je pense que la macro .await!() postfix satisfait tous les objectifs principaux de Centril à l'exception du premier point:

" await devrait rester un mot-clé pour permettre la conception future du langage."

Quelles autres utilisations du mot-clé await voyons-nous à l'avenir?


Edit : J'ai mal compris comment fonctionne exactement await . J'ai frappé mes déclarations incorrectes et mis à jour les exemples.

Mon deuxième commentaire est que le mot-clé await dans Rust fait des choses complètement différentes de await dans une autre langue, ce qui peut causer de la confusion et des conditions de course inattendues.

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

Je crois qu'un préfixe async fait un travail beaucoup plus clair indiquant que les valeurs asynchrones sont des morceaux d'une machine à états construite par un compilateur:

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

Il est intuitif que les fonctions async vous permettent d'utiliser les valeurs async . Cela renforce également l'intuition qu'il y a des machines asynchrones à l'œuvre en arrière-plan et comment cela pourrait fonctionner.

Cette syntaxe a des questions non résolues et des problèmes de composabilité clairs, mais elle indique clairement où se déroule le travail asynchrone et elle sert de complément de style synchrone au .await!() plus composable et chaînable.

J'ai eu du mal à remarquer le suffixe await à la fin de certaines expressions dans les exemples précédents de style procédural, alors voici à quoi cela ressemblerait avec cette syntaxe proposée:

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

(Il y a aussi l'argument à faire pour une macro .dbg!() facilement composable et chaînable, mais c'est pour un forum différent.)

Le mar 29 janvier 2019 à 23:31:32 -0800, Sphericon a écrit:

J'ai suivi cette discussion de loin et j'ai quelques questions et commentaires.

Mon premier commentaire est que je pense que la macro .await!() postfix satisfait tous les objectifs principaux de Centril à l'exception du premier point:

" await devrait rester un mot-clé pour permettre la conception future du langage."

En tant que l'un des principaux promoteurs de la syntaxe .await!() , je
pense absolument que await doit rester un mot-clé. Étant donné que nous
besoin de .await!() pour être intégré au compilateur de toute façon, cela semble
banal.

@Hirevo

Vous avez effectivement réécrit mon exemple de code mais vous en avez supprimé la partie chaînage (en faisant des liaisons).

Je n'obtiens pas cette approche «chaînage pour chaînage». Le chaînage est bon quand il est bon, par exemple il ne crée pas de variables temporaires inutiles. Vous dites "vous avez supprimé le chaînage", mais vous pouvez voir que cela n'apporte aucune valeur ici.

Par exemple, pour la partie débogage, ce qui suit a exactement le même comportement avec juste le dbg nécessaire! et rien de plus (pas même des parenthèses supplémentaires)

Non, tu as fait ça

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

Ce n'est pas la même chose.

Je viens de dire que les combinateurs peuvent très bien jouer avec await, et que le problème de chaînage peut être résolu en n'attendant que l'expression entière construite par les combinateurs (supprimant le besoin de wait (await fetch ("test")). Json () ou await {wait {fetch ("test")} .json ()}).

C'est seulement un problème pour le préfixe await , il n'existe pas pour les autres formes de celui-ci.

Je ne sais pas comment faire la même chose sans combinateurs et sans liaisons ou parenthèses supplémentaires.

Pourquoi ne pas créer ces liaisons? Les liaisons sont toujours meilleures pour le lecteur, par exemple, et si vous avez un débogueur qui pourrait imprimer une valeur de liaison, mais qui ne pourrait pas évaluer une expression.

Enfin, vous n'avez supprimé aucune liaison. Vous venez d'utiliser la syntaxe labmda |user| où j'ai fait let user = ... . Cela ne vous a rien sauvé, mais maintenant ce code est plus difficile à lire, plus difficile à déboguer, et il n'a pas accès au parent (par exemple, vous devez envelopper les erreurs en externe dans la chaîne de méthodes au lieu de le faire s'appeler lui-même, Probablement).


En un mot: le chaînage ne fournit pas de valeur en soi. Cela peut être utile dans certains scénarios, mais ce n'est pas l'un d'entre eux. Et depuis que j'écris code async / Attendent depuis plus de six ans, je ne crois que vous ne voulez jamais. Non pas parce que vous ne pouvez pas, mais parce que c'est peu pratique et que l'approche de liaison est toujours plus agréable à lire et souvent à écrire. Pour ne pas dire que vous n'avez pas du tout besoin de combinateurs. Si vous avez async/await , vous n'avez pas besoin de ces centaines de méthodes sur futures / streams , vous n'avez besoin que de deux: join et select . Tout le reste peut être fait via des itérateurs / bindings / ..., c'est-à-dire des outils de langage commun, qui ne vous obligent pas à apprendre encore une autre infrastructure.

La communauté await comme mot-clé ou comme sigil, alors à mon avis, vos idées sur async nécessitent un autre RFC.

Mon deuxième commentaire est que le mot-clé await dans Rust fait des choses complètement différentes de wait dans une autre langue, ce qui peut causer de la confusion et des conditions de course inattendues.

Pouvez-vous être plus détaillé? Je n'ai vu aucune différence entre JS / C # await, sauf que les futures sont basées sur des sondages, mais cela n'a vraiment rien à voir avec async/await .

Concernant la syntaxe du préfixe await? proposée à plusieurs endroits ici:
`` C #
laissez toto = attendre? 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, le préfixe await? ressemble à une syntaxe trop spéciale pour moi.

) * édité.

À quoi cela ressemblerait-il avec les futurs des futurs? Autrement dit, serait-il arbitrairement extensible:

let foo = await?? double_trouble();

IOW, await? ressemble à une syntaxe trop spéciale pour moi.

@rolandsteiner par "futures of futures" voulez-vous dire impl Future<Output = Result<Result<_, _>, _>> (un await + deux ? implique "déballer" un seul futur et deux résultats pour moi, sans attendre les futurs imbriqués ).

await? _est_ un cas spécial, mais c'est un cas spécial qui s'appliquera probablement à plus de 90% des utilisations de await . Le point entier des contrats à terme est un moyen d'attendre les opérations _IO_ asynchrones, IO est faillible, donc 90% + de async fn renverra probablement io::Result<_> (ou un autre type d'erreur qui inclut une variante d'E / S ). Les fonctions qui renvoient Result<Result<_, _>, _> sont assez rares actuellement, donc je ne m'attendrais pas à ce qu'elles nécessitent une syntaxe de cas spécial.

@ Nemo157 Vous avez bien sûr raison: Résultat des résultats. Mis à jour mon commentaire.

Aujourd'hui, nous écrivons

  1. await!(future?) pour future: Result<Future<Output=T>,E>
  2. await!(future)? pour future: Future<Output=Result<T,E>>

Et si nous écrivons await future? nous devons déterminer lequel cela signifie.

Mais est-ce que le cas 1 peut toujours se transformer en cas 2? Dans le cas 1, l'expression produit soit un futur, soit une erreur. Mais l'erreur peut être retardée et déplacée dans le futur. Nous pouvons donc simplement gérer le cas 2 et effectuer une conversion automatique ici.

Du point de vue du programmeur, Result<Future<Output=T>,E> garantit un retour anticipé pour le cas d'erreur, mais sauf que les deux ont le même sementic. Je peux imaginer que le compilateur peut travailler et éviter l'appel supplémentaire poll si le cas d'erreur est immédiat.

La proposition est donc:

await exp? peut être interprété comme await (exp?) si exp est Result<Future<Output=T>,E> , et interprété comme (await exp)? si exp est Future<Output=Result<T,E>> . Dans les deux cas, il retournera tôt par erreur et se résoudra au vrai résultat s'il fonctionne correctement.

Pour les cas plus compliqués, nous pouvons appliquer quelque chose comme la déréférence du récepteur de méthode automatique:

> Lors de l'interpolation de await exp???? nous vérifions d'abord exp et si est Result , try it et continuez lorsque le résultat est encore un Result jusqu'à épuisement de ? ou avoir quelque chose qui n'est pas Result . Ensuite, il faut que ce soit un avenir et nous await dessus et appliquer le reste ? s.

J'étais un partisan de mot-clé / sigil postfix et je le suis toujours. Cependant, je veux juste montrer que la priorité des préfixes peut ne pas être un gros problème en pratique et avoir des solutions de contournement.

Je sais que les membres de l'équipe Rust n'aiment pas les choses implicites, mais dans un tel cas, il y a juste trop peu de différence entre les sementics potentiels et nous avons un bon moyen de nous assurer de faire la bonne chose.

await? est un cas spécial, mais c'est un cas spécial qui s'appliquera probablement à plus de 90% des utilisations de await . Le point entier des contrats à terme est un moyen d'attendre les opérations d'E / S asynchrones, les E / S sont faillibles, donc 90% + de async fn renverra probablement io::Result<_> (ou un autre type d'erreur qui inclut une variante d'E / S ). Les fonctions qui renvoient Result<Result<_, _>, _> sont assez rares actuellement, donc je ne m'attendrais pas à ce qu'elles nécessitent une syntaxe de cas spécial.

Les cas spéciaux sont mauvais à composer, à développer ou à apprendre, et éventuellement à se transformer en bagages. Ce n'est pas un bon compromis de faire des exceptions aux règles du langage pour un seul cas d'utilisation théorique d'utilisation.

Serait-il possible d'implémenter Future pour Result<T, E> where T: Future ? De cette façon, vous pouvez simplement await result_of_future sans avoir à le déballer avec ? . Et bien sûr, cela renverrait un résultat, donc vous l'appeleriez await result_of_future , ce qui signifierait (await result_of_future)? . De cette façon, nous n'aurions pas besoin de la syntaxe await? et la syntaxe du préfixe serait un peu plus cohérente. Faites-moi savoir s'il y a quelque chose de mal à cela.

Les arguments supplémentaires pour await avec des délimiteurs obligatoires incluent (personnellement, je ne suis pas sûr de la syntaxe que je préfère dans l'ensemble):

  • Pas de casse spéciale de l'opérateur ? , pas de await? ou await??
  • Conforme aux opérateurs de flux de contrôle existants tels que loop , while et for , qui nécessitent également des délimiteurs obligatoires
  • Se sent le plus à l'aise avec les constructions Rust similaires existantes
  • L'élimination du boîtier spécial permet d'éviter les problèmes lors de l'écriture de macros
  • N'utilise pas de sceau ou de suffixe, évitant de dépenser du budget d'étrangeté

Exemple:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

Cependant, après avoir joué avec cela dans un éditeur, cela semble encore encombrant. Je pense que je préférerais avoir await et await? sans délimiteurs, comme avec break et return .

Serait-il possible de mettre en œuvre Future for Resultoù T: Future?

Vous voudriez l'inverse. L'attendable le plus courant est un Future où son type de sortie est un résultat.

Il y a alors l'argument explicite agaisnt cachant ou absorbant autrement le ? dans juste await. Et si vous voulez faire correspondre le résultat, etc.

Si vous avez un Result<Future<Result<T, E2>>, E1> , l'attendre renverrait un Result<Result<T, E2>, E1> .

Si vous avez un Future<Result<T, E1>> , l'attendre retournerait simplement le Result<T, E1> .

Il n'y a pas de cacher ou d'absorber le ? dans l'attente, et vous pouvez faire tout ce qui est nécessaire avec le résultat par la suite.

Oh. J'ai dû mal vous comprendre alors. Je ne vois pas en quoi cela aide car nous devons encore combiner le ? avec wait 99% du temps.


Oh. La syntaxe await? est supposée impliquer (await future)? ce qui serait le cas courant.

Exactement. Donc, nous ferions simplement la liaison await plus serrée dans await expr? , et si cette expression est un Result<Future<Result<T, E2>>, E1> alors elle serait évaluée à quelque chose du type Result<T, E2> . Cela signifierait qu'il n'y a pas de boîtier spécial pour attendre les types de résultat. Il suit simplement les implémentations normales des traits.

@ivandardi qu'en est-il de Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Remarque, cette forme d'avenir a disparu. Il existe maintenant un seul type associé, Output (qui sera probablement un résultat).


@ivandardi D'accord. Je le vois maintenant. La seule chose que vous auriez contre vous, c'est que la priorité est quelque chose d'étrange que vous devriez apprendre là-bas car c'est une déviation, mais il en va de même pour la plupart de tout avec await je suppose.

Bien qu'un résultat qui renvoie un futur soit si rare, je n'ai pas trouvé de cas en dehors de quelque chose dans le noyau de tokio qui a été supprimé, donc je ne pense pas que nous ayons besoin d'implications de sucre / trait pour aider dans ce cas.

@ivandardi qu'en est-il de Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Eh bien, je suppose que ce n'est pas possible, vu que le trait Future n'a qu'un type associé Output .


@mehcode Eh bien, cela répond à certaines des préoccupations qui ont été soulevées précédemment, je dirais. Cela aide également à décider de la syntaxe du préfixe, car il n'y aurait qu'un seul préfixe en attente de syntaxe au lieu des choix «Préséance évidente» et «Précédence utile».

Eh bien, je suppose que ce n'est pas possible, vu que le trait Future n'a qu'un type associé à la sortie.

pourquoi pas?

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 Voir https://doc.rust-lang.org/std/future/trait.Future.html

Vous parlez de l'ancien trait qui se trouvait dans la caisse futures .

Honnêtement, je ne pense pas qu'avoir un mot-clé en position de préfixe comme il était censé être:

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

a un avantage certain par rapport au fait d'avoir un sceau dans la même position:

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

Je pense que cela ne semble pas cohérent avec les autres opérateurs de préfixe, ajoute un espace blanc redondant avant l'expression et décale le code vers le côté droit à une distance notable.


Nous pouvons essayer d'utiliser sigil avec une syntaxe de point étendue ( pré-RFC ) qui résout les problèmes avec des portées profondément imbriquées:

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

ainsi que la possibilité d'ajouter des méthodes en chaîne:

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

Et évidemment, remplaçons * par @ ce qui a plus de sens ici:

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

Ce que j'aime ici, c'est que @ reflète à await qui est placé sur LHS de la déclaration de fonction, tandis que ? reflète à Result<User> qui est placé sur le RHS de la déclaration de fonction. Cela rend @ extrêmement cohérent avec ? .


Des pensées à ce sujet?

Des pensées à ce sujet?

Tout appareil dentaire supplémentaire est tout simplement interdit

@mehcode Oui, je ne savais pas que les contrats à terme manquaient maintenant de type Error . Mais le point est toujours valable: vous pouvez avoir une fonction, qui renvoie probablement une fonctionnalité, qui, une fois terminée, peut renvoyer un résultat ou une erreur.

Des pensées à ce sujet?

Oui! Selon le commentaire de Centril , les sigils ne sont pas très reconnaissables. Je ne commencerais à considérer les sigils que si l'on pouvait créer un regex qui identifie tous les points d'attente.

En ce qui concerne votre proposition de syntaxe de point étendue, vous devrez l'expliquer beaucoup plus en profondeur, en fournissant la sémantique et la désuétude de chaque utilisation. Pour le moment, je ne comprends pas la signification des extraits de code que vous avez publiés avec cette syntaxe.


Mais le point est toujours valable: vous pouvez avoir une fonction, qui renvoie probablement une fonctionnalité, qui, une fois terminée, peut renvoyer un résultat ou une erreur.

Vous auriez donc un Future<Item=Result<T, E>> , non? Eh bien, dans ce cas, vous ... attendez simplement l'avenir et faites face au résultat 😐

Comme dans, supposons que foo soit de type Future<Item=Result<T, E>> . Alors await foo sera un Result<T, E> et vous pouvez utiliser ce qui suit pour traiter les erreurs:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

Alors tu aurais un avenir>, non? Eh bien, dans ce cas, vous ... attendez simplement l'avenir et faites face au résultat 😐

Non, je veux dire Result<Future<Item=Result<T, E>>, OtherE>

Avec la variante postfix, vous faites juste

let bar = foo()? await?.bar();

@melodicstream J'ai mis à jour mon message et ajouté un lien vers la pré-RFC pour cette fonctionnalité

Oh mon Dieu ... je viens de relire tous les commentaires pour la troisième fois ...

Le seul sentiment cohérent est mon aversion pour la notation postfixe .await dans toutes ses variantes car je m'attendrais sérieusement à ce que await fasse partie de Future avec cette syntaxe. La "puissance du point" peut fonctionner pour un IDE mais ne fonctionne pas du tout pour moi. Je pourrais certainement m'y adapter si c'est la syntaxe stabilisée, mais je doute que cela me sente jamais vraiment "juste".

J'irais avec la notation de préfixe super basique await sans crochets obligatoires, principalement à cause du principe KISS et parce que le faire de la même manière que la plupart des autres langages vaut beaucoup.

Déssucrer await? future à (await future)? serait bien et apprécié, mais tout ce qui va au-delà me semble de plus en plus être une solution sans problème. Un simple rebinding let amélioré la lisibilité du code dans la plupart des exemples et moi, personnellement, j'emprunterais probablement cette voie en écrivant du code même si le chaînage facile était une option.

Cela étant dit, je suis très heureux de ne pas être en mesure de prendre une décision à ce sujet.

Je vais maintenant laisser ce problème seul au lieu d'ajouter plus de bruit et d'attendre (jeu de mots) le verdict final.

... au moins j'essaierai honnêtement de faire ça ...

@Pzixel En supposant que foo est de type Result<Future<Item=Result<T, E1>>, E2> , alors await foo serait de type Result<Result<T, E1>, E2> et vous pouvez simplement traiter ce résultat en conséquence.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream non, ce ne sera pas le cas. Vous ne pouvez pas attendre Result, vous pouvez attendre Future. So you have to do foo ()? to unwrap Future from Résultat , then do await to get a result, then again ? `Pour dérouler le résultat du futur.

En mode postfixe, ce sera foo? await? , en préfixe ... Je ne suis pas sûr.

Donc, vos exemples ne fonctionnent tout simplement pas, en particulier le dernier, car il devrait être

(await foo.unwrap()).unwrap()

Cependant, @huxi a peut-être raison, nous résolvons le problème qui n'existe probablement pas. La meilleure façon de le comprendre autorise la macro postfix et voir la base de code réelle après l'adoption de base de async/await .

@Pzixel C'est pourquoi j'ai fait la proposition d'implémenter Future sur tous les types de genre Result<Future<Item=T>, E> . Faire cela permettrait ce que je dis.

Bien que je sois d'accord avec await foo?? pour Result<Future<Output=Result<T, E1>>, E2> , je ne suis PAS satisfait de await foo.unwrap().unwrap() . Dans mon premier modèle de cerveau, cela doit être

(await foo.unwrap()).unwrap()

Sinon, je serai vraiment déroutant. La raison en est que ? est un opérateur général et unwrap est une méthode. Le compilateur peut faire quelque chose de spécial pour les opérateurs comme . , mais s'il s'agit d'une méthode normale, je suppose qu'elle est toujours liée à l'expression la plus proche dans sa partie gauche, uniquement.

La syntaxe postfixe, foo.unwrap() await.unwrap() , me convient également, car je sais que await est juste un mot-clé, pas un objet, il doit donc faire partie de l'expression avant unwrap() .

La macro de style postfix résout bien la plupart de ces problèmes, mais juste la question de savoir si nous voulons conserver la familiarité avec les langages existants et les garder préfixés. Je voterais pour le style postfix.

Ai-je raison de dire que le code suivant n'est pas égal:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

et

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

Le premier panique en essayant de créer un avenir, tandis que le second panique au premier sondage. Si oui, devrions-nous en faire quelque chose? Cela pourrait être contourné comme

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

Mais c'est beaucoup moins pratique.

@Pzixel : c'est une décision qui a été prise. async fn sont totalement paresseux et ne s'exécutent pas du tout avant d'être interrogés, et capturent ainsi toutes les durées de vie des entrées pour toute la durée de vie future. La solution de contournement est une fonction d'encapsulation retournant impl Future étant explicite, et utilisant un bloc async (ou fn) pour le calcul différé. Ceci est nécessaire pour empiler les combinateurs Future sans allocation entre chacun d'eux, car après le premier sondage, ils ne peuvent pas être déplacés.

C'est une décision prise et pourquoi les systèmes d'attente implicites ne fonctionnent pas (bien) pour Rust.

Développeur C # ici, et je vais plaider pour le style de préfixe, alors préparez-vous à voter contre.

La syntaxe de Postfix avec un point ( read().await ou read().await() ) est très trompeuse et suggère un accès au champ ou une invocation de méthode, et await n'est pas l'une de ces choses; c'est une caractéristique du langage. Je ne suis en aucun cas un Rustacean chevronné, mais je ne connais aucun autre cas .something qui soit effectivement un mot-clé qui sera réécrit par le compilateur. Le plus proche auquel je puisse penser est le suffixe ? .

Ayant eu async / await en C # depuis quelques années maintenant, la syntaxe standard du préfixe avec un espace ( await foo() ) ne m'a pas posé de problèmes pendant tout ce temps. Dans les cas où un await imbriqué est requis, il n'est pas onéreux d'utiliser des parens, qui est la syntaxe standard pour toute priorité d'opérateur explicite et donc facile à grok, par exemple

`` `c #
var list = (attend 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()

Cela semble bizarre.

Je pense que la cohérence avec les autres langues est également un élément à prendre en compte. À l'heure actuelle, un mot-clé await avant l'expression asynchrone est la syntaxe pour C #, VB.NET, JavaScript et Python, et c'est également la syntaxe proposée pour C ++. Cela représente cinq des sept langues les plus populaires au monde; C et Java n'ont pas encore async / await, mais je serais surpris s'ils allaient autrement.

À moins qu'il n'y ait vraiment de bonnes raisons d'aller d'une manière différente, ou pour le dire autrement, si le préfixe et le suffixe pèsent tous les deux de la même façon en termes d'avantages et de compromis, je suggérerais qu'il y a beaucoup à dire pour le non-officiel " syntaxe standard ".

@markrendle Je suis aussi C # dev, j'écris async/await depuis 2013. Et j'aime aussi le préfixe. Mais Rust diffère beaucoup.

Vous savez probablement que votre (await Foo()).ToList() est un cas rare. Dans la plupart des cas, vous écrivez simplement await Foo() , et si vous avez besoin d'un autre type, vous voudrez probablement corriger la signature Foo .

Mais la rouille est une autre chose. Nous avons ? ici et nous devons propager des erreurs. En C # async / await encapsule automatiquement les exceptions, les jette à travers les points d'attente et ainsi de suite. Vous ne l'avez pas rouillé. Considérez chaque fois que vous écrivez await en C #, vous devez écrire

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

C'est très fastidieux d'avoir toutes ces broches.

OTOH avec postfix vous avez

var response = client.GetAsync("www.google.com") await.HandleException();
var json =  response.ReadAsStreamAsync() await.HandleException();
var somethingElse = DoMoreAsyncStuff(json) await.HandleException();
...

ou en termes de rouille

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

Et imaine nous aurons un autre transformateur, en plus de ? et await , appelons-le @ . Si nous inventons une syntaxe supplémentaire pour await? , alors nous devrons avoir await@ , @? , await@? , ... pour la rendre cohérente.


J'adore le préfixe await aussi, mais je ne suis pas sûr que Rust soit d'accord avec ça. L'ajout de règles supplémentaires dans la langue peut être acceptable, mais c'est un fardeau qui ne vaut probablement pas la peine d'être plus lisible.

Relisez tous les commentaires, y compris le fil de suivi des problèmes (passé près de 3 heures à ce sujet). Et mon résumé est: le suffixe @ semble être la meilleure solution, et malheureusement c'est la plus sous-estimée.

Il y a beaucoup d'opinions (biaisées) à ce sujet, et voici mes commentaires pour eux:


async-await ne serait pas familier avec le suffixe @

En fait, ce ne serait qu'à moitié inconnu car la même syntaxe async fn serait fournie, et les mêmes fonctionnalités qu'avec await seraient toujours disponibles.

La rouille a beaucoup de sigils et nous ne devrions pas introduire un autre sigil

Mais les sigils sont bons jusqu'à ce qu'ils soient lisibles et cohérents avec une autre syntaxe. Et l'introduction d'un nouveau sceau ne signifie pas strictement que cela aggravera les choses. Dans la perspective de la cohérence, le suffixe @ est une syntaxe bien meilleure que n'importe quel préfixe / suffixe await .

Les extraits comme ?!@# me rappellent Perl

En réalité, nous verrions rarement une autre combinaison que @? dont l'OMI semble assez simple et ergonomique. Les types de retour comme impl Try<impl Future<T>> , impl Future<impl Try<impl Try<T>>> , et même impl Future<T> , se produiraient au plus dans 10% des utilisations, comme nous avons pu le voir sur un exemple réel du monde ITT. De plus, une conglomération de sigil dans 90% de ces 10% indiquerait une odeur de code lorsque vous devez soit corriger votre API, soit introduire une liaison temporaire à la place pour le clarifier.

Postfix @ est si difficile à remarquer!

Et c'est en fait un gros avantage dans le contexte async-await . Cette syntaxe introduit la capacité d'exprimer du code asynchrone comme s'il était synchrone, donc plus intrusif await n'interfère qu'avec son objectif initial. Les exemples ci-dessus montrent clairement que l'occurrence des points de rendement peut être suffisamment dense pour gonfler le code lorsqu'ils sont trop explicites. Bien sûr, nous devons les avoir à vue, mais @ sigil conviendrait parfaitement pour cela ainsi que await sans gonfler le code.

Attendre l'avenir est une opération coûteuse et nous devrions dire explicitement que

Cela a vraiment du sens, mais je ne pense pas que nous devrions le dire si souvent et si fort. Je pense qu'il pourrait y avoir une analogie avec la syntaxe de mutation: mut n'est requis explicitement qu'une seule fois et de plus, nous pouvons utiliser la liaison mutable implicitement. Une autre analogie pourrait être fournie avec une syntaxe non sécurisée: explicite unsafe n'est requis qu'une seule fois et nous pouvons ensuite déréférencer des pointeurs bruts, appeler des fonctions non sécurisées, etc. Et encore une autre analogie pourrait être fournie avec l'opérateur bulle: explicite impl Try return ou prochain bloc try sont requis qu'une seule fois et nous pouvons utiliser l'opérateur ? toute confiance. Ainsi, de la même manière explicite async annote les opérations éventuellement durables une seule fois, et en outre, nous pouvons appliquer l'opérateur @ qui le ferait réellement.

C'est bizarre parce que je le lis différemment de await , et je ne peux pas le taper facilement

Si vous lisez & comme "ref" et ! comme "non", alors je ne pense pas que la façon dont vous lisez @ actuellement un argument fort pour ne pas l'utiliser symbole. La vitesse à laquelle vous le tapez n'a pas non plus d'importance, car la lecture du code est toujours une priorité plus importante. Dans les deux cas, adopter à @ est très simple.

C'est ambigu car @ est déjà utilisé dans la correspondance de motifs

Je ne pense pas que ce soit un problème car ! est déjà utilisé dans les macros, & est déjà utilisé pour l'emprunt et dans l'opérateur && , | est utilisé dans les fermetures, dans les modèles et dans l'opérateur || , + / - pourrait également être appliqué en position de préfixe, et . pourrait être utilisé pour le moment contextes plus différents. Il n'y a rien de surprenant ni de mal à réutiliser les mêmes symboles dans différentes constructions de syntaxe.

Ce serait difficile de grep

Si vous voulez vérifier si certaines sources contiennent des modèles asynchrones, rg async serait une solution beaucoup plus simple et directe. Si vous voulez vraiment trouver tous les points de rendement de cette manière, vous pourrez alors écrire quelque chose comme rg "@\s*[;.?|%^&*-+)}\]]" ce qui n'est pas simple, mais ce n'est pas non plus quelque chose d'époustouflant. Compte tenu de la fréquence à laquelle il sera nécessaire, IMO cette regex est tout à fait correcte.

@ n'est pas adapté aux débutants et il est difficile de rechercher sur Google

Je pense que c'est en fait plus facile à comprendre pour les débutants car cela fonctionne de manière cohérente avec l'opérateur ? . Au contraire, le préfixe / suffixe await ne serait pas cohérent avec une autre syntaxe Rust introduisant en plus des maux de tête avec l'associativité / la mise en forme / la signification. Je ne pense pas non plus que await ajouterait une valeur d'auto-documentation parce qu'un seul mot-clé ne peut pas décrire tout le concept sous-jacent et que les débutants l'apprendraient de toute façon. Et Rust n'a jamais été un langage qui vous donne des conseils en texte brut pour chacune des constructions syntaxiques possibles. Si quelqu'un oublie ce que signifie le symbole @ (je doute vraiment que ce soit un problème car il y a aussi beaucoup d'associations pour s'en souvenir correctement) - alors googler pour cela devrait également réussir car actuellement rust @ symbol renvoie toutes les informations pertinentes sur @ dans la correspondance de modèle.

Nous avons déjà réservé le mot-clé await , nous devrions donc l'utiliser de toute façon

Pourquoi? Il est normal d'avoir un mot-clé réservé inutilisé lorsqu'il s'avère que l'implémentation finale de la fonctionnalité est meilleure sans elle. De plus, aucune promesse n'a été donnée que le mot clé exactement await serait introduit. Et cet identifiant n'est pas très courant dans le code du monde réel, on ne perdrait donc rien s'il resterait réservé jusqu'à la prochaine édition.

? était une erreur et @ serait une deuxième erreur

Vous pensez probablement plus lexicalement, et à cause de cela, vous préférez travailler avec des mots plutôt qu'avec des symboles. Et à cause de cela, vous ne voyez aucune valeur dans une syntaxe plus fine. Mais gardez à l'esprit que plus de gens pensent plus visuellement, et pour eux, les mots intrusifs sont plus difficiles à travailler à la place. Probablement un bon compromis ici serait de fournir await!() macro try!() été fourni avec ? , vous pourrez donc l'utiliser au lieu de @ si vous n'êtes vraiment pas à l'aise avec @ .

^^^^^^^ Avez-vous aussi relu mon commentaire?

J'ai eu une grande révélation en lisant le commentaire de @ I60R qui m'a fait en faveur du sigil postfix, et je n'ai pas aimé utiliser un sigil jusqu'à présent (préférant plutôt une forme de postfix wait).

Notre opération d'attente n'est pas await . Du moins pas de la façon dont await est utilisé en C # et JavaScript, les deux plus grandes sources de familiarité async / await .

Dans ces langages, la sémantique du démarrage d'un Task (C #) / Promise (JS) est que la tâche est immédiatement placée dans la file d'attente des tâches pour être exécutée. En C #, il s'agit d'un contexte multi-thread, en JS, il s'agit d'un seul thread.

Mais dans l'un ou l'autre de ces langages, await foo signifie sémantiquement "parquer cette tâche et attendre l'achèvement de la tâche foo , et en attendant les ressources de calcul utilisées par cette tâche peuvent être utilisées par d'autres Tâches".

C'est pourquoi vous obtenez un parallélisme avec des await s séparés après avoir construit plusieurs Task / Promise s (même sur des exécuteurs à un seul thread): la sémantique de await aren 'pas "exécuter cette tâche" mais plutôt "exécuter une tâche" jusqu'à ce que nous ayons fini d'attendre et que nous puissions reprendre.

Ceci est entièrement distinct du démarrage paresseux ou impatient de commencer les tâches une fois créées, bien que fortement liées. Je prends soin de mettre le reste de la tâche en file d'attente une fois que le travail synchrone a été effectué.

Nous avons déjà une confusion dans ce sens chez les utilisateurs des itérateurs paresseux et des futurs actuels. Je suppose que la syntaxe await ne nous rend pas service ici.

Notre await!(foo) n'est donc pas await foo à la manière "traditionnelle" C # ou JS. Alors quel langage plus étroitement lié à notre sémantique? Je crois maintenant fermement que c'est ( @matklad corrigez-moi si je me trompe dans les détails) suspend fun Kotlin. Ou comme cela a été mentionné dans les discussions adjacentes à Rust, "asynchrone explicite, attente implicite".

Bref examen: dans Kotlin, vous ne pouvez appeler un suspend fun depuis un contexte suspend . Cela exécute immédiatement la fonction appelée jusqu'à son achèvement, suspendant le contexte empilé comme requis par la fonction enfant. Si vous voulez exécuter child suspend fun en parallèle, vous créez l'équivalent des fermetures asynchrones et vous les combinez avec un combinateur suspend fun pour exécuter plusieurs fermetures suspend en parallèle. (C'est un peu plus compliqué que cela.) Si vous souhaitez exécuter un enfant suspend fun en arrière-plan, vous créez un type de tâche qui place cette tâche sur l'exécuteur et expose un .await() suspend fun méthode pour rejoindre votre tâche avec la tâche d'arrière-plan.

Cette architecture, bien que décrite avec des verbes différents de ceux de Rust Future , semble très familière.

J'ai déjà expliqué pourquoi l'attente implicite ne fonctionne pas pour Rust et je maintiens cette position. Nous avons besoin de async fn() -> T et fn() -> Future<T> pour être équivalents dans l'utilisation afin que le modèle "faire un travail initial" soit possible (pour faire fonctionner les durées de vie dans des cas délicats) avec des futurs paresseux. Nous avons besoin de futurs paresseux afin de pouvoir empiler des futurs sans couche d'indirection entre chacun pour l'épinglage.

Mais je suis maintenant convaincu que la syntaxe "explicite mais silencieuse" de foo@ (ou d'un autre sigil) a du sens. Autant que je répugne à dire le mot ici, @ dans ce cas est une opération monadique en async tout comme ? est l'opération monadique en try .

Il y a une opinion selon laquelle saupoudrer ? dans votre code est juste pour que le compilateur arrête de se plaindre lorsque vous retournez un Result . Et dans un sens, vous faites cela. Mais c'est pour préserver notre idéal de clarté locale du code. J'ai besoin de savoir si cette opération est décompressée via la monade ou prise littéralement localement.

(Si je me trompe dans les détails de la monade, je suis sûr que quelqu'un me corrigera, mais je pense que mon argument est toujours valable.)

Si @ se comporte de la même manière, je suppose que c'est une bonne chose. Il ne s'agit pas d'accomplir un certain nombre de tâches, puis await construire une machine à états que quelqu'un d'autre doit pomper jusqu'à son achèvement, que ce soit en la construisant dans sa propre machine à états ou le mettre dans un contexte d'exécution.

TL; DR: si vous prenez une chose de ce mini article de blog, que ce soit ceci: await!() Rust n'est pas await comme c'est dans d'autres langues. Les résultats sont similaires, mais la sémantique sur la façon dont les tâches sont gérées est très différente. De cela, nous avons avantage à ne pas utiliser la syntaxe commune, car nous n'avons pas le comportement commun.

(Il a été mentionné dans une discussion précédente un langage qui est passé d'un futur paresseux à un futur impatient. Je crois que c'est parce que await foo suggère que foo est déjà en cours, et nous attendons simplement qu'il produise un résultat.)

EDIT: si vous voulez en discuter de manière plus synchrone, envoyez-moi un ping @ CAD sur le développement de rust Discord (le lien est 24h) ou des utilisateurs internes (@ CAD97). J'adorerais mettre ma position à l'épreuve d'un examen direct.

EDIT 2: Désolé GitHub @ cad pour le ping accidentel; J'ai essayé d'échapper au @ et je ne voulais pas vous entraîner là-dedans.

@ I60R

Les extraits comme ?!@# me rappellent Perl

En réalité, nous verrions rarement une autre combinaison que @? ce que l'OMI semble assez simple et

Vous ne pouvez pas le savoir jusqu'à ce qu'il apparaisse. Si nous avons encore un autre transformateur de code - yield sigil, par exemple - ce serait un vrai script Perl.

Postfix @ est si difficile à remarquer!

Et c'est en fait un gros avantage dans le contexte async-await .

La difficulté à remarquer des points await ne peut pas être un avantage.

Attendre l'avenir est une opération coûteuse et nous devrions dire explicitement que

Cela a vraiment du sens, mais je ne pense pas que nous devrions le dire si souvent et si fort.

Relisez mes liens sur l'expérience C #, s'il vous plaît. Il est en fait ce important de le dire haut et fort ce souvent.


D'autres points sont tout à fait valables, mais ceux auxquels j'ai répondu sont des lacunes irréparables.

@ CAD97 En tant que développeur C # qui peut dire en quoi async est différent dans Rust .. Eh bien, ce n'est pas si différent que vous le décrivez. IMHO vos implications sont basées sur une fausse prémisse

Notre await!(foo) n'est donc pas await foo à la manière "traditionnelle" C # ou JS.

C'est tout à fait la même chose dans l'esprit des développeurs C #. Bien sûr, vous devez vous rappeler que les contrats à terme ne seront pas exécutés avant d'être sondés, mais dans la plupart des cas cela n'a pas d'importance. Dans la plupart des cas, await signifie simplement "Je veux obtenir la valeur non emballée du futur F dans la variable x ". Avoir le futur qui ne tourne pas avant d'être interrogé est en fait un soulagement après le monde C #, donc les gens seront heureusement heureux. En plus de cela, C # a des concepts similaires avec les itérateurs / générateurs, qui sont également paresseux, donc la foule C # est assez familière avec tout cela.

En un mot, attraper await fonctionnant un peu différemment est plus facile que d'avoir un sigil fonctionnant comme await dans leur langage% younameit%, mais pas tout à fait. Les gens ne seront pas déroutés par la différence, car ce n'est pas tant l'affaire que vous pensez qu'ils pourraient le penser. Les situations dans lesquelles une exécution impatiente / paresseuse est vraiment importante sont

@Pzixel C'est à peu

Les situations dans lesquelles une exécution rapide / paresseuse est importante sont _really_ rares, donc cela ne devrait pas être la principale préoccupation de conception.

Malheureusement, ce n'est pas vrai: le modèle "engendrer des promesses et ensuite les await " est très courant et souhaitable , mais il ne fonctionne pas avec Rust .

C'est une très grande différence, qui a même surpris certains membres de Rust!

async / await dans Rust est vraiment plus proche d'un système monadique, il n'a fondamentalement rien de commun avec JavaScript / C #:

  • L'implémentation est complètement différente (construire une machine à états puis l'exécuter sur une tâche, ce qui est cohérent avec les monades).

  • L'API est complètement différente (pull vs push).

  • Le comportement par défaut d'être paresseux est complètement différent (ce qui est cohérent avec les monades).

  • Il ne peut y avoir qu'un seul consommateur, plutôt que plusieurs (ce qui est cohérent avec les monades).

  • Séparer les erreurs et utiliser ? pour les gérer est complètement différent (ce qui est cohérent avec les transformateurs monades).

  • Le modèle de mémoire est complètement différent, ce qui a un impact énorme à la fois sur la mise en œuvre de Rust Futures et sur la manière dont les utilisateurs utilisent réellement Futures.

Le seul point commun est que tous les systèmes sont conçus pour faciliter le code asynchrone. C'est ça. Ce n'est vraiment pas grand-chose en commun.

De nombreuses personnes ont mentionné C #. Nous savons.

Nous savons ce que C # a fait, nous savons ce que JavaScript a fait. Nous savons pourquoi ces langues ont pris ces décisions.

Les membres officiels de l'équipe de C # nous ont parlé et expliqué en détail pourquoi ils ont pris ces décisions avec async / await.

Personne n'ignore ces langages, ces points de données ou ces cas d'utilisation. Ils sont pris en compte.

Mais Rust est vraiment différent de C # ou JavaScript (ou de tout autre langage). Nous ne pouvons donc pas simplement copier aveuglément ce que font les autres langues.

Mais dans l'un ou l'autre de ces langages, attendre foo signifie sémantiquement "parquer cette tâche et attendre l'achèvement de la tâche foo

C'est exactement la même chose à Rust. La sémantique des fonctions asynchrones est la même. Si vous appelez un async fn (qui crée un Future), il ne démarre pas encore l'exécution. C'est en effet différent du monde JS et C #.

L'attendre conduira l'avenir à son terme, ce qui est le même que partout ailleurs.

Malheureusement, ce n'est pas vrai: le modèle "engendrer des promesses et ensuite les await " est très courant et souhaitable , mais il ne fonctionne pas avec Rust .

Pourriez-vous être plus en détail? J'ai lu l'article mais je n'ai pas trouvé ce qui ne va pas

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

Bien sûr, ils ne se déclencheront pas avant la ligne 2, mais dans la plupart des cas, c'est un comportement souhaitable, et je suis toujours convaincu que c'est une énorme différence qui ne permet plus d'utiliser le mot-clé await . Le nom du sujet lui-même indique que le mot await sens ici.

Personne n'ignore ces langages, ces points de données ou ces cas d'utilisation. Ils sont pris en compte.

Je ne serai pas ennuyeux de répéter les mêmes arguments encore et encore. Je pense que j'ai livré ce que je devais faire, donc je ne le ferai plus.


PS

  • L'API est complètement différente (pull vs push).

Cela fait la différence lors de la mise en œuvre de votre propre avenir uniquement. Mais dans 99 (100?)% Des cas, vous n'utilisez que des combinateurs de futures qui cachent cette différence.

  • Le modèle de mémoire est complètement différent, ce qui a un impact énorme à la fois sur la mise en œuvre de Rust Futures et sur la manière dont les utilisateurs utilisent réellement Futures.

Pourriez-vous être plus en détail? En fait, j'ai déjà joué avec du code asynchrone lors de l'écriture d'un simple serveur Web sur Hyper et je n'ai remarqué aucune différence. Mettez async ici, await là, c'est fait .

L'argument selon lequel async / await de rust a une implémentation différente des autres langages et que nous devons ensuite faire quelque chose de différent n'est pas convaincant. C's

for (int i = 0; i < n; i++)

et Python

for i in range(n)

ne partage certainement pas le même mécanisme sous-jacent, mais pourquoi Python choisit-il d'utiliser for ? Il devrait utiliser i in range(n) @ ou i in range(n) --->> ou autre pour montrer cette différence importante !

La question ici est de savoir si la différence est importante pour le travail quotidien de l'utilisateur!

La vie quotidienne d 'un utilisateur typique de rouille concerne:

  1. Interagir avec certaines API HTTP
  2. Écrivez du code réseau

et il se soucie vraiment de

  1. Il peut terminer le travail de manière rapide et ergonomique.
  2. Rust est performant pour son travail.
  3. Il a un contrôle de bas niveau s'il veut

NOT rust a un million de détails d'implémentation qui sont différents de C #, JS . C'est le travail d'un concepteur de langage de cacher les différences non pertinentes, en n'exposant que les plus utiles aux utilisateurs .

De plus, personne ne se plaint actuellement du await!() pour des raisons comme "Je ne sais pas que c'est une machine à états", "Je ne sais pas que c'est basé sur le pull".

Les utilisateurs ne se soucient pas et ne se soucient pas de ces différences.

L'attendre conduira l'avenir à son terme, ce qui est le même que partout ailleurs.

Eh bien, pas tout à fait, n'est-ce pas? Si j'écris simplement let result = await!(future) dans un async fn et que j'appelle cette fonction, rien ne se passera tant qu'il ne sera pas placé sur un exécuteur et interrogé par lui.

@ ben0x539 oui, je l'ai relu ... Mais pour le moment, je n'ai rien à ajouter.

@ CAD97 heureux de voir que mon message était inspirant. Je peux répondre à propos de Kotlin: par défaut, il fonctionne de la même manière que les autres langages, bien que vous puissiez obtenir un comportement paresseux de type Rust si vous le souhaitez (par exemple val lazy = async(start=LAZY) { deferred_operation() }; ). Et à propos de la sémantique similaire: IMO, la sémantique la plus proche est fournie dans la programmation réactive, puisque les observables réactives sont froides (paresseuses) par défaut (par exemple val lazy = Observable.fromCallable { deferred_operation() }; ). S'abonner à eux planifie le travail réel de la même manière que await!() dans Rust, mais il y a aussi une grande différence que l'abonnement par défaut est une opération non bloquante et que les résultats calculés de manière asynchrone sont presque toujours traités dans les fermetures séparément du flux de contrôle actuel. Et il y a une grande différence dans le fonctionnement de l'annulation. Donc, je pense que le comportement de Rust async est unique, et je soutiens pleinement votre argument selon lequel await est déroutant et qu'une syntaxe différente n'est qu'un avantage ici!

@Pzixel Je suis à peu près sûr qu'il n'y aurait pas de nouveau sceau. Implémenter un yield tant que sigil n'a aucun sens car c'est une construction de flux de contrôle comme if / match / loop / return . De toute évidence, toutes les constructions de flux de contrôle doivent utiliser des mots-clés car ils décrivent la logique métier et la logique métier est toujours prioritaire. Au contraire, @ ainsi que ? sont des constructions de gestion d'exceptions et la logique de gestion d'exceptions est moins importante, elle doit donc être subtile. Je n'ai trouvé aucun argument solide selon lequel avoir un gros mot-clé await est une bonne chose (compte tenu de la quantité de texte, il est possible que je les manque) parce que tous font appel à l'autorité ou à l'expérience (cela ne signifie cependant pas que je rejeter l'autorité ou l'expérience de quelqu'un - cela ne peut tout simplement pas être appliqué ici de manière complète).

@tajimaha l'exemple de Python que vous avez fourni contient en fait plus de "détails d'implémentation". Nous pouvons considérer for comme async , (int i = 0; i < n; i++) comme await , et i in range(n) comme @ et ici il est évident que Python a introduit un nouveau mot-clé - in (en Java, c'est en fait sigil - : ) et il a également introduit une nouvelle syntaxe de plage. Il pourrait réutiliser quelque chose de plus familier comme (i=0, n=0; i<n; i++) au lieu d'introduire beaucoup de détails d'implémentation. Mais à l'heure actuelle, l'impact sur l'expérience utilisateur n'est que positif, la syntaxe est plus simple et les utilisateurs s'en soucient vraiment.

@Pzixel Je suis à peu près sûr qu'il n'y aurait pas de nouveau sceau. Implémenter un yield comme sigil n'a aucun sens car c'est une construction de flux de contrôle comme if / match / loop / return . De toute évidence, toutes les constructions de flux de contrôle doivent utiliser des mots-clés car ils décrivent la logique métier et la logique métier est toujours prioritaire. Au contraire, @ ainsi que ? sont des constructions de gestion d'exceptions et la logique de gestion d'exceptions est moins importante, elle doit donc être subtile.

await n'est pas une "construction de gestion d'exception".
Sur la base d'une prémisse invalide, vos implications sont également fausses.

la gestion des exceptions est également un flux de contrôle, mais personne ne pense que ? est une mauvaise chose.

Je n'ai trouvé aucun argument solide selon lequel avoir un gros mot-clé await est une bonne chose (compte tenu de la quantité de texte, il est possible que je les manque) parce que tous font appel à l'autorité ou à l'expérience (cela ne signifie cependant pas que je rejeter l'autorité ou l'expérience de quelqu'un - cela ne peut tout simplement pas être appliqué ici de manière complète).

Parce que Rust n'a pas sa propre expérience, il ne pouvait donc voir que ce que les autres langages en pensent, l'expérience des autres équipes, etc. Vous êtes tellement confiant dans le sigil, mais je ne sais pas si vous avez même essayé de l'utiliser.

Je ne veux pas faire d'argument sur la syntaxe dans Rust, mais j'ai vu de nombreux arguments postfix vs prefix et peut-être que nous pouvons avoir le meilleur des deux mondes. Et quelqu'un d'autre a déjà essayé de proposer cela en C ++. La proposition C ++ et Coroutine TS a été mentionnée quelques fois ici, mais à mon avis, une proposition alternative nommée Core Coroutines mérite plus d'attention.

Les auteurs de Core Coroutines proposent de remplacer co_await par un nouveau jeton de type opérateur.
Avec, ce qu'ils appellent la syntaxe d'opérateur de dépliage , on peut utiliser à la fois la notation préfixe et postfixe (sans ambiguïté):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

J'ai pensé que cela pourrait être intéressant, ou du moins que cela rendrait la discussion plus complète.

Mais Rust est vraiment différent de C # ou JavaScript (ou de tout autre langage). Nous ne pouvons donc pas simplement copier aveuglément ce que font les autres langues.

Veuillez ne pas utiliser la "différence" comme excuse pour votre inclination personnelle vers une syntaxe peu orthodoxe.

Combien de différences y a-t-il? Prenez n'importe quel langage populaire, Java, C, Python, JavaScript, C #, PHP, Ruby, Go, Swift, etc., statique, dynamique, compilé, interprété. Ils sont très différents dans l'ensemble de fonctionnalités, mais ils ont encore beaucoup en commun pour leur syntaxe. Y a-t-il un moment où vous sentez que l'un de ces langages ressemble à «brainf * ck»?

Je pense que nous devrions nous concentrer sur la fourniture de fonctionnalités différentes mais utiles, pas sur une syntaxe étrange.

Après avoir lu le fil, je pense aussi que le débat est largement terminé quand quelqu'un invalide le besoin urgent de chaîner, quelqu'un invalide l'affirmation selon laquelle les gens sont agacés par trop de variables temporaires.

OMI, vous avez perdu le débat sur les besoins de la syntaxe postfix. Vous ne pouvez recourir qu'à la «différence». C'est un autre essai désespéré.

@tensorduruk J'ai le sentiment que vos mots sont un peu trop hostiles aux autres utilisateurs. Veuillez essayer de les vérifier.


Et honnêtement, s'il y a beaucoup de gens qui sont contre la syntaxe postfix, alors nous devrions simplement décider d'une syntaxe de préfixe pour le moment, attendre de voir comment le code avec la syntaxe de préfixe est écrit, puis faire une analyse pour voir combien de code est écrit pourrait bénéficier de l'attente de postfix. De cette façon, nous apaisons tous ceux qui ne sont pas à l'aise avec un changement innovant tel que postfix wait, tout en obtenant un bon argument pragmatique quant à savoir s'il faut ou non attendre postfix.

Le pire des cas dans ce cas est si nous faisons tout cela et trouvons une syntaxe d'attente de suffixe qui est en quelque sorte complètement meilleure que le préfixe d'attente. Cela entraînerait beaucoup de désabonnement de code si les gens étaient dérangés de passer à la meilleure forme d'attente.

Et je pense que toute cette discussion sur la syntaxe se résume vraiment à l'enchaînement. Si le chaînage n'était pas une chose, alors postfix await serait complètement hors de la fenêtre et il serait beaucoup plus facile de se contenter du préfixe await. Cependant, le chaînage est très important dans Rust et ouvre donc la discussion aux sujets suivants:

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 quelque chose comme ça. Si quelqu'un peut trouver un meilleur modèle de décision avec de meilleurs points que moi, faites-le! XD

Donc, tout d'abord, au lieu même de discuter de la syntaxe, nous devrions décider si nous voulons un suffixe, un préfixe ou à la fois un suffixe et un préfixe, et pourquoi nous voulons le choix que nous proposons. Une fois cela réglé, nous pouvons aborder des problèmes plus petits concernant le choix de la syntaxe.

@tensorduruk

OMI, vous avez perdu le débat sur les besoins de la syntaxe postfix. Vous ne pouvez recourir qu'à la «différence». C'est un autre essai désespéré.

Sérieusement, pourquoi ne pas simplement utiliser vos dites langues au lieu de l'hostilité déplacée ici?
En utilisant votre logique, rust devrait avoir des classes parce que d'autres langages conventionnels l'ont. Rust ne devrait pas avoir d'emprunt parce que les autres langues ne l'ont pas.

Ne pas gérer le chaînage serait une occasion manquée. i, e Je ne voudrais pas créer des liaisons pour une variable temporaire, si le chaînage peut l'anonymiser.

Cependant, je pense que nous devrions nous en tenir au mot-clé macro actuel pour attendre comme tous les soirs. Avoir des macros postfixes comme fonctionnalité nocturne et laisser les gens jouer avec. Oui, il y aura des taux de désabonnement une fois que nous serons installés, mais cela peut être géré par rustfix.

@tensorduruk

OMI, vous avez perdu le débat sur les besoins de la syntaxe postfix. Vous ne pouvez recourir qu'à la «différence». C'est un autre essai désespéré.

Sérieusement, pourquoi ne pas simplement utiliser vos dites langues au lieu de l'hostilité déplacée ici?
En utilisant votre logique, rust devrait avoir des classes parce que d'autres langages conventionnels l'ont. Rust ne devrait pas avoir d'emprunt parce que les autres langues ne l'ont pas.

Ne pas gérer le chaînage serait une occasion manquée. i, e Je ne voudrais pas créer des liaisons pour une variable temporaire, si le chaînage peut l'anonymiser.

Cependant, je pense que nous devrions nous en tenir au mot-clé macro actuel pour attendre comme tous les soirs. Avoir des macros postfixes comme fonctionnalité nocturne et laisser les gens jouer avec. Oui, il y aura des taux de désabonnement une fois que nous serons installés, mais cela peut être géré par rustfix.

Lisez attentivement s'il vous plaît. J'ai dit que nous devrions fournir des fonctionnalités utiles et non une syntaxe étrange. struct? emprunter? fonctionnalités!

veuillez également résoudre les problèmes qui existent vraiment. les gens ont montré, avec des exemples ou des statistiques, que les liaisons temporaires n'étaient peut-être pas un problème. Est-ce que vous soutenez f await f.await essayez de convaincre l'autre partie en utilisant des preuves?

moi aussi voter contre moi est inutile. pour que postfix soit accepté, vous devez argumenter où postfix est utile (ne répétez probablement pas l'argument de chaînage, c'est une impasse; probablement un problème fréquent qui ennuie les gens, avec des preuves, pas un problème de jouet).

pouvons-nous obtenir la syntaxe comme C # ou JS? la plupart des développeurs l'utilisent dans le monde, je n'aime pas que rust utilise une nouvelle syntaxe ou incohérente, Rust est également difficile à apprendre pour les nouvelles personnes.

Ce qui suit serait une annexe à mon article sur l'utilisation de postfix @ lieu de await


Nous devrions utiliser l'expérience de nombreuses autres langues à la place

Et nous l'utilisons réellement. Mais cela ne signifie pas que nous devons répéter complètement la même expérience. De plus, lorsque vous vous disputez de cette manière contre @ vous n'obtiendrez probablement aucun résultat utile car faire appel à l'expérience précédente n'est pas convaincant :

Les récits génétiques d'un problème peuvent être vrais et ils peuvent aider à éclairer les raisons pour lesquelles le problème a pris sa forme actuelle, mais ils ne sont pas concluants pour déterminer ses mérites.

Nous devrions utiliser await car beaucoup de gens l'adorent

Ces personnes peuvent aimer await pour de nombreuses autres raisons, pas seulement parce que c'est await . Ces raisons peuvent également ne pas exister dans Rust. Et argumenter de cette manière contre @ n'apporterait probablement pas de nouveaux points en discussion, car faire appel au public n'est pas convaincant :

L'argumentum ad populum peut être un argument valable en logique inductive; par exemple, un sondage auprès d'une population importante peut révéler que 100% préfèrent une certaine marque de produit à une autre. Un argument convaincant (fort) peut alors être avancé selon lequel la prochaine personne à prendre en considération préférera très probablement cette marque (mais pas toujours à 100% car il pourrait y avoir des exceptions), et le sondage est une preuve valide de cette affirmation. Cependant, il n'est pas approprié comme argument pour un raisonnement déductif comme preuve, par exemple de dire que le sondage prouve que la marque préférée est supérieure à la concurrence dans sa composition ou que tout le monde préfère cette marque à l'autre.

@Pzixel

Mais la rouille est une autre chose. Nous avons ? ici et nous devons propager des erreurs. En C # async / await encapsule automatiquement les exceptions, les jette à travers les points d'attente et ainsi de suite. Vous ne l'avez pas rouillé. Considérez chaque fois que vous écrivez await en C #, vous devez écrire

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

C'est très fastidieux d'avoir toutes ces broches.

En C #, vous écrivez ceci:

try
{
  var response = await client.GetAsync("www.google.com");
  var json =  await response.ReadAsStreamAsync();
  var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
  // handle exception
}

En ce qui concerne l'argument propagate-error-chaining, par exemple foo().await? , y a-t-il une raison pour laquelle le ? n'a pas pu être ajouté à l'opérateur await dans le préfixe?

let response = await? getProfile();

Une autre chose qui m'est venue à l'esprit: que faire si vous voulez match sur un Future<Result<...>> ? Lequel de ceux-ci est plus facile à lire?

// Prefix
let userId = match await response {
  Ok(u) => u.id,
  _ => -1
};
// Postfix
let userId = match response {
  Ok(u) => u.id,
  _ => -1
} await;

De plus, une expression async match serait-elle une chose? Comme dans, voudriez-vous que le corps d'une expression de correspondance soit asynchrone? Si tel est le cas, il y aurait une différence entre match await response et await match response . Parce que match et await sont tous deux des opérateurs unaires et que match est déjà un préfixe, il serait plus facile de distinguer si await étaient également un préfixe. Avec un préfixe et un suffixe, il devient difficile de spécifier si vous attendez la correspondance ou la réponse.

let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await; // Are we awaiting match or response here?

Si vous devez attendre les deux choses, vous regarderiez quelque chose comme

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

Bien que je suppose que cela pourrait être

// Postfix - ... this is weirder and uglier
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;

(La conception du langage de programmation est difficile.)

Quoi qu'il en soit, je vais répéter que Rust a la priorité pour les opérateurs unaires préfixés avec match , et await est un opérateur unaire.

C# // Postfix - ... this is weirder and uglier let userId = match response await { ... } await;

La beauté est dans l'oeil de celui qui regarde.

Quoi qu'il en soit, je vais répéter que Rust a la priorité pour les opérateurs unaires préfixés avec match , et await est un opérateur unaire.

? par contre est unaire mais suffixe.

Quoi qu'il en soit, j'ai l'impression que la discussion tourne maintenant autour du drain. Sans évoquer de nouveaux points de discussion, il ne sert à rien de réitérer les mêmes positions encore et encore.

FWIW, je suis heureux d'obtenir le support await quelle que soit la syntaxe - même si j'ai mes propres préférences, je ne pense pas qu'aucune des suggestions réalistes ne soit trop terrible à utiliser.

@markrendle Je ne sais pas trop sur quoi vous répondez

En C #, vous écrivez ceci:

Je sais comment j'écris en C #. J'ai dit "imaginez à quoi cela pourrait-il ressembler si nous n'avons pas d'exceptions". Parce que Rust ne le fait pas.

En ce qui concerne l'argument propagate-error-chaining, par exemple foo().await? , y a-t-il une raison pour laquelle le ? n'a pas pu être ajouté à l'opérateur await dans le préfixe?

Il a déjà été discuté deux ou trois fois, s'il vous plaît, lisez le sujet. En un mot: c'est une construction artificielle, qui ne fonctionnera pas bien si nous avons quelque chose de plus à ? . await? comme suffixe fonctionne juste, quand await? comme préfixe nécessite un support supplémentaire dans le compilateur. Et cela nécessite toujours des accolades pour le chaînage (ce que je n'aime pas ici, mais les gens le mentionnent toujours comme une chose importante), alors que postfix wait ne le fait pas.

Une autre chose qui m'est venue à l'esprit: que faire si vous voulez match sur un Future<Result<...>> ? Lequel de ceux-ci est plus facile à lire?

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

En tant qu'autre utilisateur de C #, je dirai que ses syntaxes de préfixes: new , await et les casts de style C ont le plus déclenché mon intuition. Je soutiens fortement l'option d'opérateur postfix.

Cependant, toute syntaxe sera meilleure que l'enchaînement explicite des futurs, même une pseudo-macro. Je serai heureux de toute résolution.

@orthoxerox Vous soulevez un très bon point. Dans mon travail quotidien, j'écris principalement Java et je méprise le nouvel opérateur à un niveau tel que toutes mes classes qui ont besoin d'une instanciation explicite (une occurrence étonnamment rare lorsque vous utilisez des modèles de générateur et une injection de dépendances) ont une méthode de fabrique statique juste pour que je puisse cacher l'opérateur. .

@Pzixel

@markrendle Je ne sais pas trop sur quoi vous répondez

En C #, vous écrivez ceci:

Je sais comment j'écris en C #. J'ai dit "imaginez à quoi cela pourrait-il ressembler si nous n'avons pas d'exceptions". Parce que Rust ne le fait pas.

Je suppose que c'est probablement une barrière linguistique, car ce n'est pas du tout ce que vous avez dit, mais j'accepterai que c'est peut-être ce que vous vouliez dire.

Quoi qu'il en soit, comme l' a dit

@yasammez Venez en C #. Dans la v8.0, nous utilisons simplement new () sans le nom du type :)

Je vais juste lancer quelques idées pour l'opérateur postfix.

foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator

Je ne dis pas si l'opérateur postfix est la voie à suivre ou non, mais personnellement, je trouve @ un des plus bruyants et étranges parmi tous les choix possibles. ~ du commentaire @phaux semble beaucoup plus élégant et moins "occupé". De plus, si je ne manque rien, nous ne l'utilisons pas encore pour quoi que ce soit dans Rust.

On m'a proposé ~ avant @phaux mais je ne veux pas revendiquer de brevet; P

J'ai proposé ceci parce que c'est comme un écho parlant:

Hi~~~~~
Where r u~~~~~

Hay~~~~~
I am in another mountain top~~~~~

~ est parfois utilisé après une phrase pour indiquer la fin, ce qui est apte à attendre!

Je ne peux pas dire si ce fil a atteint le sommet du ridicule ou sommes-nous sur quelque chose ici.

Je pense que ~ il n'est pas facile de répondre sur certains claviers, en particulier certains claviers mécaniques petits et délicats.

Pourrait être:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};

Nous pourrions introduire un trigraphe comme ... pour les utilisateurs sur des dispositions de clavier où ~ n'est pas pratique.

Au début, j'étais fortement d' await(future) avoir une syntaxe de préfixe obligatoire comme await{future} car elle est si claire et facile à analyser visuellement. Cependant, je comprends les propositions d'autres personnes selon lesquelles un futur de rouille n'est pas comme la plupart des autres Futures d'autres langues, car il ne confie pas immédiatement une tâche à un exécuteur, mais plutôt une structure de flux de contrôle qui transforme le contexte en homomorphique à une chaîne d'appels monade essentiellement.

Cela me fait penser qu'il est quelque peu malheureux qu'il y ait maintenant une confusion en essayant de le comparer à d'autres langues à cet égard. L'analogue le plus proche est vraiment la notation monade do en Haskell ou la compréhension for en Scala (qui sont les seules avec lesquelles je suis familier du haut de ma tête). Du coup, j'apprécie la possibilité de proposer une syntaxe unique, mais j'ai peur que l'existence de l'opérateur ? n'ait à la fois encouragé et découragé l'utilisation d'autres sigils avec lui. Tout autre opérateur basé sur le sigil à côté de ? rend bruyant et déroutant, comme future@? , mais le précédent établi en ayant un opérateur sigil postfixe signifie qu'un autre n'est pas si ridicule.

J'ai donc été convaincu du mérite de l'opérateur sigil postfix. L'inconvénient de cela, c'est que le sceau que j'aurais préféré est pris par le type jamais. J'aurais préféré ! car je pense que future!? me ferait rire chaque fois que je l'écrirais, et cela me semble le plus visuel. Je suppose que $ serait le suivant, puis comme il est visuellement discernable future$? . Voir ~ me rappelle encore les débuts de Rust lorsque ~ était l'opérateur de préfixe pour l'allocation de tas. Tout cela est très personnel, donc je n'envie pas les décideurs finaux. Si j'étais eux, je choisirais probablement simplement le choix inoffensif de l'opérateur de préfixe avec les délimiteurs requis.

Cependant, je comprends les propositions d'autres personnes selon lesquelles un futur de rouille n'est pas comme la plupart des autres Futures d'autres langues, car il ne confie pas immédiatement une tâche à un exécuteur, mais plutôt une structure de flux de contrôle qui transforme le contexte en homomorphique à une chaîne d'appels monade essentiellement.

J'ai tendance à ne pas être d'accord. Le comportement que vous mentionnez n'est pas une propriété de await , mais de la fonction ou de la portée async environnante. Ce n'est pas await qui retarde l'exécution du code qui le précède, mais la portée qui contient ledit code.

Probablement le problème avec le symbole @ aspect étrange est que son utilisation dans ce contexte n'était jamais prévue auparavant, donc dans la plupart des polices, il est fourni sous une forme si inconfortable pour nous.

Ensuite, fournir un meilleur glyphe et des ligatures pour les polices de programmation populaires (ou du moins pour Fira Code de Mozilla) pourrait améliorer un peu la situation.

Dans tous les autres cas, pour moi, il ne semble pas que @ soit si étrange de causer de réels problèmes lors de l'écriture ou de la maintenance du code.


Par exemple, le code suivant utilise @ symbole différent 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)
}

Développez pour comparer à quoi il ressemble avec 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

Au début, j'étais fortement d' await(future) avoir une syntaxe de préfixe obligatoire comme await{future} car elle est si claire et facile à analyser visuellement.

Alors vous voulez probablement if { cond } , while { cond } et match { expr } ...

Cependant, je comprends les propositions des autres selon lesquelles un futur de rouille n'est pas comme la plupart des autres Futures d'autres langues.

Ce n'est pas vrai. C'est en fait comme la plupart des autres futurs des autres langues. La différence entre "exécuter jusqu'à la première attente lors de l'apparition" et "exécuter jusqu'à la première attente lors de l'interrogation" n'est pas si grande. Je sais parce que j'ai travaillé avec les deux. Si vous pensez réellement "quand cette différence vient à jouer", vous ne trouverez que le cas d'angle. par exemple, vous obtiendrez une erreur lors de la création du futur lors du premier sondage au lieu de sa création.

Ils pourraient être différents en interne, mais ils sont tout à fait les mêmes du point de vue de l'utilisateur, donc cela n'a pas de sens pour moi de faire une distinction ici.

@Pzixel

La différence entre "exécuter jusqu'à la première attente lors de l'apparition" et "exécuter jusqu'à la première attente lors de l'interrogation" n'est pas si grande.

Ce n'est cependant pas la différence dont nous parlons. Nous ne parlons pas de paresseux vs impatients de commencer à attendre.

Ce dont nous parlons, c'est que await rejoint l'attendu Promise (JS) / Task (C #) sur l'exécuteur dans d'autres langues, qui a déjà été mis sur l'exécuteur à la construction (donc était déjà en cours d'exécution en "arrière-plan"), mais dans Rust, Future s sont des machines à états inertes jusqu'à ce que await! -driven.

Promise / Task est un handle vers une opération asynchrone en cours d'exécution. Future est un calcul asynchrone différé. Des gens, y compris des noms notables de Rust, ont déjà commis cette erreur et des exemples ont déjà été liés au milieu de ces 500 commentaires.

Personnellement, je pense que cette discordance de sémantique est assez grande pour contrer la familiarité de await . Nos Future accomplissent le même objectif que Promise / Task , mais avec un mécanisme différent.


De façon anecdotique, pour moi, quand j'ai appris pour la première fois async / await en JavaScript, async était "juste" quelque chose que j'ai écrit pour obtenir le await superpuissance. Et la façon dont on m'a appris à obtenir le parallélisme était de a = fa(); b = fb(); /* later */ await [a, b]; (ou quoi que ce soit, ça fait un âge que je n'ai pas eu à écrire JS). Mon opinion est que l'opinion des autres sur async s'aligne avec moi, car la sémantique de Rust ne correspond pas sur async (vous donnez await superpuissance), mais sur Future construction et await! .


À ce stade, je crois que la discussion sur les différences dans la sémantique async / Future / await a suivi son cours, et aucune nouvelle information n'est présentée. À moins que vous n'ayez une nouvelle position et / ou une nouvelle perspective à apporter, il serait probablement préférable pour le fil de laisser cette discussion ici. (Je serais heureux de l'apporter à Internals et / ou Discord.)

@ CAD97 oui, je vois votre position, mais je pense que la différence n'est pas si grande.

Vous m'avez, je vous ai. Alors laissez la discussion suivre son cours.

@ CAD97

Des gens, y compris des noms notables de Rust, ont déjà commis cette erreur et des exemples ont déjà été liés au milieu de ces 500 commentaires.

Si même des personnes intimement familières avec Rust font cette erreur, est-ce vraiment une erreur?

Ainsi, nous avons eu un certain nombre de discussions sur l'async-wait au Rust All Hands. Au cours de ces discussions, certaines choses sont devenues claires:

Premièrement, il n'y a pas (encore) de consensus dans l'équipe lang sur la syntaxe d'attente . Il existe clairement de nombreuses possibilités et des arguments solides en faveur de toutes. Nous avons passé beaucoup de temps à explorer des alternatives et avons produit beaucoup d'allers-retours intéressants. Une prochaine étape immédiate pour cette discussion, je pense, est de convertir ces notes (avec d'autres commentaires de ce fil) en une sorte de commentaire de synthèse qui présente le cas de chaque variante, puis de continuer à partir de là. Je travaille avec @withoutboats et @cramertj là- dessus.

En prenant du recul par rapport à la question de syntaxe, une autre chose que nous prévoyons de faire est un triage global de l'état de l'implémentation. Il existe un certain nombre de limitations actuelles (par exemple, l'implémentation nécessite actuellement TLS sous le capot pour transmettre des informations sur le waker). Celles-ci peuvent ou non être des obstacles à la stabilisation , mais peu importe ce sont des problèmes qui doivent finalement être résolus, et cela nécessitera un effort concerté (en partie de la part de l'équipe du compilateur). Une autre étape suivante consiste alors à effectuer ce triage et à générer un rapport. Je m'attends à ce que nous fassions ce triage la semaine prochaine, et nous aurons alors une mise à jour.

En attendant, je vais continuer et verrouiller ce fil jusqu'à ce que nous ayons eu la chance de produire les rapports ci-dessus . J'ai l'impression que le fil a déjà atteint son objectif d'explorer de manière approfondie l'espace de conception possible et que d'autres commentaires ne seront pas particulièrement utiles à ce stade. Une fois que nous aurons les rapports susmentionnés en main, nous présenterons également les prochaines étapes pour parvenir à une décision finale.

(Pour développer un peu le dernier paragraphe, je suis assez intéressé par l'exploration d'autres moyens d'explorer l'espace de conception au-delà des longs fils de discussion. C'est un sujet beaucoup plus vaste que je ne peux aborder dans ce commentaire, donc je n'entrerai pas dans les détails , mais il suffit de dire pour l'instant que je suis tout à fait intéressé à essayer de trouver de meilleures façons de résoudre ce - et à venir! - débats sur la syntaxe.)

Rapport d'état en attente asynchrone:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

Par rapport à mon commentaire précédent, cela contient les résultats du triage et quelques réflexions sur la syntaxe (mais pas encore une description complète de la syntaxe).

Marquer ce problème comme bloquant pour la stabilisation asynchrone, du moins pour le moment.

Il y a quelque temps, Niko a promis d'écrire un résumé de la discussion au sein de l'équipe de langue et de la communauté sur la syntaxe finale de l'opérateur await. Nos excuses pour la longue attente. Un compte rendu de l'état de la discussion est lié ci-dessous. Avant cela, permettez-moi également de vous donner une mise à jour sur l’état actuel de la discussion et sur la direction que nous prendrons à partir de maintenant.

Bref résumé de la situation actuelle de l'async-await

Tout d'abord, nous espérons stabiliser async-await dans la version 1.37 , qui se branche le 4 juillet 2019. Puisque nous ne voulons pas stabiliser la macro await! , nous devons résoudre la question de syntaxe avant cela. Notez que cette stabilisation ne représente pas la fin de la route, mais plutôt le début. Il reste du travail de fonctionnalité à faire (par exemple, async fn dans les traits) et aussi du travail impl (optimisation continue, correction de bogues, etc.) Pourtant, la stabilisation de l'async / await sera une étape majeure!

En ce qui concerne la syntaxe, le plan de résolution est le suivant:

  • Pour commencer, nous publions un article sur le débat sur la syntaxe jusqu'à présent - veuillez jeter un œil.
  • Nous voulons être compatibles avec les futures extensions évidentes de la syntaxe: traiter des flux avec des boucles for en particulier (comme la boucle for await JavaScript). C'est pourquoi j'ai travaillé sur une série d'articles sur ce problème ( premier article ici et d'autres à venir).
  • Lors de la prochaine réunion lang-team du 2 mai, nous prévoyons de discuter de l'interaction avec les boucles for et également d'établir un plan pour parvenir à une décision finale sur la syntaxe à temps pour stabiliser async / await dans la version 1.37. Nous publierons une mise à jour après la réunion sur ce fil de discussion interne .

L'écriture

La rédaction est un document papier Dropbox, disponible ici . Comme vous le verrez, il est assez long et présente de nombreux arguments dans les deux sens. Nous apprécierions vos commentaires à ce sujet; plutôt que de rouvrir ce numéro (qui contient déjà plus de 500 commentaires), j'ai créé un fil de discussion interne à cet effet .

Comme je l'ai déjà dit, nous prévoyons de prendre une décision finale dans un proche avenir. Nous pensons également que la discussion a largement atteint un état stable: attendez-vous à ce que les prochaines semaines soient la «période de commentaires finale» pour cette discussion sur la syntaxe. Après la réunion, nous espérons avoir un calendrier plus détaillé à partager sur la manière dont cette décision sera prise.

La syntaxe Async / await est probablement la fonctionnalité la plus attendue de Rust depuis la version 1.0, et la syntaxe pour await en particulier a été l'une des décisions sur lesquelles nous avons reçu le plus de commentaires. Merci à tous ceux qui ont participé à ces discussions au cours des derniers mois! C'est un choix sur lequel de nombreuses personnes ont des sentiments très divergents; nous voulons assurer à tous que vos commentaires sont entendus et que la décision finale sera prise après de longues délibérations réfléchies et minutieuses.

Cette page vous a été utile?
0 / 5 - 0 notes