Rust: Problème de suivi pour async/wait (RFC 2394)

Créé le 8 mai 2018  ·  308Commentaires  ·  Source: rust-lang/rust

Il s'agit du problème de suivi de la RFC 2394 (rust-lang/rfcs#2394), qui ajoute une syntaxe asynchrone et en attente au langage.

Je dirigerai le travail de mise en œuvre de ce RFC, mais j'apprécierais le mentorat car j'ai relativement peu d'expérience de travail dans rustc.

FAIRE:

Questions non résolues :

A-async-await A-generators AsyncAwait-Triaged B-RFC-approved C-tracking-issue T-lang

Commentaire le plus utile

À propos de la syntaxe : j'aimerais vraiment avoir await comme mot-clé simple. Par exemple, regardons une préoccupation du blog :

Nous ne savons pas exactement quelle syntaxe nous voulons pour le mot-clé wait. Si quelque chose est un futur d'un résultat - comme tout futur d'IO susceptible d'être - vous voulez pouvoir l'attendre et ensuite lui appliquer l'opérateur ? . Mais l'ordre de priorité pour activer cela peut sembler surprenant - await io_future? serait await premier et ? second, bien que ? soit lexicalement plus étroitement lié que wait.

Je suis d'accord ici, mais les accolades sont mauvaises. Je pense qu'il est plus facile de se rappeler que ? a une priorité inférieure à await et de terminer par :

let foo = await future?

C'est plus facile à lire, c'est plus facile à refactoriser. Je crois que c'est la meilleure approche.

let foo = await!(future)?

Permet de mieux comprendre un ordre dans lequel les opérations sont exécutées, mais imo c'est moins lisible.

Je crois qu'une fois que vous obtenez ce await foo? exécute d'abord await alors vous n'avez aucun problème avec cela. C'est probablement lexicalement plus lié, mais await est à gauche et ? à droite. Il est donc toujours assez logique await commencer par Result après.


Si des désaccords existent, veuillez les exprimer afin que nous puissions en discuter. Je ne comprends pas ce que signifie le downvote silencieux. Nous souhaitons tous du bien à la Rouille.

Tous les 308 commentaires

La discussion ici semble s'être arrêtée, donc la lier ici dans le cadre de la question de syntaxe await : https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

L'implémentation est bloquée sur #50307.

À propos de la syntaxe : j'aimerais vraiment avoir await comme mot-clé simple. Par exemple, regardons une préoccupation du blog :

Nous ne savons pas exactement quelle syntaxe nous voulons pour le mot-clé wait. Si quelque chose est un futur d'un résultat - comme tout futur d'IO susceptible d'être - vous voulez pouvoir l'attendre et ensuite lui appliquer l'opérateur ? . Mais l'ordre de priorité pour activer cela peut sembler surprenant - await io_future? serait await premier et ? second, bien que ? soit lexicalement plus étroitement lié que wait.

Je suis d'accord ici, mais les accolades sont mauvaises. Je pense qu'il est plus facile de se rappeler que ? a une priorité inférieure à await et de terminer par :

let foo = await future?

C'est plus facile à lire, c'est plus facile à refactoriser. Je crois que c'est la meilleure approche.

let foo = await!(future)?

Permet de mieux comprendre un ordre dans lequel les opérations sont exécutées, mais imo c'est moins lisible.

Je crois qu'une fois que vous obtenez ce await foo? exécute d'abord await alors vous n'avez aucun problème avec cela. C'est probablement lexicalement plus lié, mais await est à gauche et ? à droite. Il est donc toujours assez logique await commencer par Result après.


Si des désaccords existent, veuillez les exprimer afin que nous puissions en discuter. Je ne comprends pas ce que signifie le downvote silencieux. Nous souhaitons tous du bien à la Rouille.

J'ai des opinions mitigées sur le fait que await soit un mot-clé, @Pzixel. Bien qu'il ait certainement un attrait esthétique et qu'il soit peut-être plus cohérent, étant donné que async est un mot-clé, "le gonflement des mots-clés" dans n'importe quelle langue est une réelle préoccupation. Cela dit, avoir async sans await -t-il un sens, du point de vue des fonctionnalités ? Si c'est le cas, nous pouvons peut-être le laisser tel quel. Sinon, je pencherais pour faire de await un mot-clé.

Je pense qu'il est plus facile de se rappeler que ? a une priorité inférieure à await et de terminer par

Il est peut-être possible d'apprendre cela et de l'intérioriser, mais il y a une forte intuition que les choses qui se touchent sont plus étroitement liées que les choses qui sont séparées par des espaces, donc je pense que cela serait toujours mal lu à première vue dans la pratique.

Cela n'aide pas non plus dans tous les cas, par exemple une fonction qui renvoie un Result<impl Future, _> :

let foo = await (foo()?)?;

La préoccupation ici n'est pas simplement « pouvez-vous comprendre la priorité d'une seule attente+ ? », mais aussi « à quoi ressemble l'enchaînement de plusieurs attentes ? » Ainsi, même si nous choisissions simplement une priorité, nous aurions toujours le problème de await (await (await first()?).second()?).third()? .

Un résumé des options pour la syntaxe await , certaines du RFC et le reste du fil RFC :

  • Exiger des délimiteurs de quelque sorte : await { future }? ou await(future)? (c'est bruyant).
  • Choisissez simplement une priorité, de sorte que await future? ou (await future)? fasse ce qui est attendu (les deux semblent surprenants).
  • Combinez les deux opérateurs en quelque chose comme await? future (c'est inhabituel).
  • Faites await suffixe d'une manière ou d'une autre, comme dans future await? ou future.await? (c'est sans précédent).
  • Utilisez un nouveau sceau comme ? , comme dans future@? (il s'agit du "bruit de ligne").
  • N'utilisez aucune syntaxe, ce qui rend wait implicite (cela rend les points de suspension plus difficiles à voir). Pour que cela fonctionne, il faut aussi expliciter l'acte de construire un avenir. C'est le sujet du fil interne que j'ai lié ci-dessus .

Cela dit, avoir async sans await -t-il un sens, du point de vue des fonctionnalités ?

@alexreg C'est le cas. Kotlin fonctionne de cette façon, par exemple. C'est l'option "attente implicite".

@rpjohnst Intéressant. Eh bien, je suis généralement pour laisser async et await comme fonctionnalités explicites du langage, car je pense que c'est plus dans l'esprit de Rust, mais je ne suis pas un expert en programmation asynchrone. ..

@alexreg async/ @rpjohnst a très bien classé toutes les possibilités. Je préfère la deuxième option, je suis d'accord sur d'autres considérations (bruyant/inhabituel/...). Je travaille avec du code async/wait depuis 5 ans ou quelque chose du genre, il est vraiment important d'avoir un tel indicateur de mots-clés.

@rpjohnst

Ainsi, même si nous choisissions simplement une priorité, nous aurions toujours le problème de wait (await (await first()?).second()?).third()?.

Dans ma pratique, vous n'écrivez jamais deux await sur une seule ligne. Dans de très rares cas, lorsque vous en avez besoin, réécrivez-le simplement sous la forme then et n'utilisez pas du tout wait. Vous pouvez voir vous-même qu'il est beaucoup plus difficile à lire que

let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;

Je pense donc que ce n'est pas grave si le langage décourage d'écrire du code de cette manière afin de rendre le cas principal plus simple et meilleur.

hero away future await? semble intéressant bien que peu familier, mais je ne vois aucun contre-argument logique contre cela.

Dans ma pratique, vous n'écrivez jamais deux await sur une seule ligne.

Mais est-ce parce que c'est une mauvaise idée quelle que soit la syntaxe, ou simplement parce que la syntaxe await existante de C# la rend moche ? Les gens ont avancé des arguments similaires autour de try!() (le précurseur de ? ).

Les versions postfix et implicite sont bien moins moches :

first().await?.second().await?.third().await?
first()?.second()?.third()?

Mais est-ce parce que c'est une mauvaise idée quelle que soit la syntaxe, ou simplement parce que la syntaxe d'attente existante de C# la rend moche ?

Je pense que c'est une mauvaise idée quelle que soit la syntaxe car avoir une ligne par opération async est déjà assez complexe à comprendre et difficile à déboguer. Les avoir enchaînés dans une seule déclaration semble être encore pire.

Par exemple, regardons le vrai code (j'ai pris un morceau de mon projet):

[Fact]
public async Task Should_UpdateTrackableStatus()
{
    var web3 = TestHelper.GetWeb3();
    var factory = await SeasonFactory.DeployAsync(web3);
    var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
    var request = await season.GetOrCreateRequestAsync("123");

    var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
    var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");

    await request.UpdateStatusAsync(trackableStatus);
    await request.UpdateStatusAsync(nonTrackableStatus);

    var statuses = await request.GetStatusesAsync();

    Assert.Single(statuses);
    Assert.Equal(trackableStatus, statuses.Single());
}

Cela montre qu'en pratique, cela ne vaut pas la peine d'enchaîner des await même si la syntaxe le permet, car cela deviendrait complètement illisible await crois que ce n'est pas la seule raison pour laquelle c'est mauvais.

Les versions postfix et implicite sont bien moins moches

La possibilité de distinguer le début de la tâche et la tâche en attente est vraiment importante. Par exemple, j'écris souvent du code comme celui-ci (encore une fois, un extrait du projet) :

public async Task<StatusUpdate[]> GetStatusesAsync()
{
    int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
    var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
    var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
    {
        var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
        return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
    });

    return await Task.WhenAll(tasks);
}

Ici, nous créons N demandes asynchrones, puis nous les attendons. Nous n'attendons pas à chaque itération de boucle, mais nous créons d'abord un tableau de requêtes asynchrones, puis nous les attendons toutes en même temps.

Je ne connais pas Kotlin, alors peut-être qu'ils résolvent cela d'une manière ou d'une autre. Mais je ne vois pas comment vous pouvez l'exprimer si "exécuter" et "attendre" la tâche est la même.


Je pense donc que la version implicite est un non-sens dans des langages encore plus implicites comme C#.
Dans Rust avec ses règles qui ne vous permettent même pas de convertir implicitement u8 en i32 ce serait beaucoup plus déroutant.

@Pzixel Oui, la deuxième option semble être l'une des plus préférables. J'ai aussi utilisé async/await en C#, mais pas beaucoup, car je n'ai pas programmé principalement en C# depuis quelques années. Quant à la priorité, await (future?) est plus naturel pour moi.

@rpjohnst J'aime un peu l'idée d'un opérateur suffixe, mais je m'inquiète également de la lisibilité et des hypothèses que les gens feront - cela pourrait facilement devenir confus pour un membre d'un struct nommé await .

La possibilité de distinguer le début de la tâche et la tâche en attente est vraiment importante.

Pour ce que ça vaut, la version implicite fait ça. Il a été discuté à mort à la fois dans le fil RFC et dans le fil interne, donc je n'entrerai pas dans beaucoup de détails ici, mais l'idée de base est seulement que cela déplace l'explicitation de la tâche en attente à la construction de la tâche - ce n'est pas le cas. n'introduit aucune nouvelle implicite.

Votre exemple ressemblerait à ceci :

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

C'est ce que j'entendais par « pour que cela fonctionne, il faut aussi expliciter l'acte de construire un avenir ». C'est très similaire au travail avec des threads dans le code de synchronisation - l'appel d'une fonction attend toujours qu'elle se termine avant de reprendre l'appelant, et il existe des outils distincts pour introduire la concurrence. Par exemple, les fermetures et thread::spawn / join correspondent aux blocs asynchrones et join_all / select /etc.

Pour ce que ça vaut, la version implicite fait ça. Il a été discuté à mort à la fois dans le fil RFC et dans le fil interne, donc je n'entrerai pas dans beaucoup de détails ici, mais l'idée de base est seulement que cela déplace l'explicitation de la tâche en attente à la construction de la tâche - ce n'est pas le cas. n'introduit aucune nouvelle implicite.

Je crois que oui. Je ne vois pas ici quel serait le flux dans cette fonction, où se trouvent les points où l'exécution s'interrompt jusqu'à ce que l'attente soit terminée. Je ne vois que le bloc async qui dit "bonjour, quelque part ici il y a des fonctions asynchrones, essayez de savoir lesquelles, vous serez surpris !".

Autre point : la rouille a tendance à être un langage où l'on peut tout exprimer, proche du bare metal et ainsi de suite. J'aimerais fournir un code assez artificiel, mais je pense que cela illustre l'idée :

var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
   var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
   if (firstFinishedTask != b) // our task is timeouted
      throw new Exception(); // doing something
   // more logic here
}
else
{
   // more logic here
}

Rust a toujours tendance à fournir un contrôle total sur ce qui se passe. await vous permet de spécifier les points où le processus de continuation. Il vous permet également de unwrap une valeur dans le futur. Si vous autorisez la conversion implicite côté utilisation, cela a plusieurs implications :

  1. Tout d'abord, vous devez écrire du code sale pour simplement émuler ce comportement.
  2. Maintenant, RLS et IDE doivent s'attendre à ce que notre valeur soit soit Future<T> soit attendue T elle-même. Ce n'est pas un problème avec les mots-clés - il existe, alors le résultat est T , sinon c'est Future<T>
  3. Cela rend le code plus difficile à comprendre. Dans votre exemple, je ne vois pas pourquoi cela interrompt l'exécution à la ligne get_status_updates , mais pas sur get_status_update . Ils sont assez similaires les uns aux autres. Donc, soit ça ne fonctionne pas comme le code d'origine, soit c'est tellement compliqué que je ne peux pas le voir même quand je suis assez familier avec le sujet. Les deux alternatives ne font pas de cette option une faveur.

Je ne vois pas ici quel serait le flux dans cette fonction, où se trouvent les points où l'exécution s'interrompt jusqu'à ce que l'attente soit terminée.

Oui, c'est ce que je voulais dire par "cela rend les points de suspension plus difficiles à voir". Si vous lisez le fil de discussion interne lié, j'ai expliqué pourquoi ce n'est pas un si gros problème. Vous n'avez pas besoin d'écrire de nouveau code, vous mettez simplement les annotations à un endroit différent (blocs async au lieu de await ed expressions). Les IDE n'ont aucun problème à dire quel est le type (c'est toujours T pour les appels de fonction et Future<Output=T> pour les blocs async ).

Je noterai également que votre compréhension est probablement erronée quelle que soit la syntaxe. Rust async fonctions ne fonctionnent b.Status != TaskStatus.RanToCompletion chèque passera toujours. Cela a également été discuté à mort dans le fil RFC, si vous êtes intéressé par pourquoi cela fonctionne de cette façon.

Dans votre exemple, je ne vois pas pourquoi cela interrompt l'exécution à la ligne get_status_updates , mais pas sur get_status_update . Ils sont assez similaires les uns aux autres.

Il fait l' exécution d' interruption dans les deux endroits. La clé est que les blocs async ne s'exécutent pas tant qu'ils ne sont pas attendus, car cela est vrai pour tous les futurs dans Rust, comme je l'ai décrit ci-dessus. Dans mon exemple, get_statuses appelle (et attend donc) get_status_updates , puis dans la boucle il construit (mais n'attend pas) count futures, puis il appelle (et attend donc ) join_all , auquel cas ces contrats à terme appellent (et attendent donc) get_status_update .

La seule différence avec votre exemple, c'est quand exactement les futurs commencent à courir - dans le vôtre, c'est pendant la boucle ; dans le mien, c'est pendant join_all . Mais c'est une partie fondamentale du fonctionnement des futures Rust, rien à voir avec la syntaxe implicite ou même avec async / await du tout.

Je noterai également que votre compréhension est probablement erronée quelle que soit la syntaxe. Les fonctions asynchrones de Rust n'exécutent aucun code tant qu'elles ne sont pas attendues d'une manière ou d'une autre, donc votre vérification b.Status != TaskStatus.RanToCompletion réussira toujours.

Oui, les tâches C# sont exécutées de manière synchrone jusqu'au premier point de suspension. Merci d'avoir fait remarquer cela.
Cependant, cela n'a pas vraiment d'importance car je devrais toujours pouvoir exécuter une tâche en arrière-plan tout en exécutant le reste de la méthode, puis vérifier si la tâche en arrière-plan est terminée. Par exemple, cela pourrait être

var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the same

J'ai votre idée des blocs async et comme je vois, ce sont la même bête, mais avec plus d'inconvénients. Dans la proposition originale, chaque tâche asynchrone est associée à await . Avec des blocs async , chaque tâche serait associée à async bloc let a = foo() ou let b = await foo() et je sais que cette tâche est juste construite ou construite et attendue. Si je vois let a = foo() avec des blocs async , je dois regarder s'il y a des async ci-dessus, si je vous comprends bien, car dans ce cas

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Nous attendons toutes les tâches à la fois pendant que nous sommes ici

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Isn't "just a construction" anymore
        task.push({
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }
    tasks 
}

Nous les exécutons un par un.

Je ne peux donc pas dire quel est le comportement exact de cette partie :

let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))

Sans avoir plus de contexte.

Et les choses deviennent plus étranges avec les blocs imbriqués. Sans parler des questions sur l'outillage, etc.

le comportement du site d'appel devient dépendant du contexte

C'est déjà vrai avec le code de synchronisation normal et les fermetures. Par example:

// Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
    let data = do_something_synchronous();
    StatusUpdate { data }
});

vs

// Execute a block, immediately running `do_something_synchronous()`:
task.push({
    let data = do_something_synchronous();
    StatusUpdate { data }
});

Une autre chose que vous devez noter à partir de la proposition d'attente implicite complète est que vous ne pouvez pas appeler des async fn s à partir async contextes non some_function(arg1, arg2, etc) exécute toujours le corps de some_function jusqu'à la fin avant que l'appelant ne continue, que some_function soit ou non async . Ainsi, l'entrée dans un contexte async est toujours marquée explicitement, et la syntaxe d'appel de fonction est en fait plus cohérente.

Concernant la syntaxe d'attente : qu'en est-il d'une macro avec une syntaxe de méthode ? Je ne trouve pas de RFC réel pour permettre cela, mais j'ai trouvé quelques discussions ( 1 , 2 ) sur reddit, donc l'idée n'est pas sans précédent. Cela permettrait à await de fonctionner en position suffixe sans en faire un mot-clé / en introduisant une nouvelle syntaxe pour cette fonctionnalité uniquement.

// Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()?

Il existe une bibliothèque du monde Scala qui simplifie les compositions de monades : http://monadless.io

Peut-être que certaines idées sont intéressantes pour Rust.

citation de la doc :

La plupart des langages courants prennent en charge la programmation asynchrone utilisant l'idiome async/await ou l'implémentent (par exemple, F#, C#/VB, Javascript, Python, Swift). Bien qu'utile, async/await est généralement lié à une monade particulière qui représente les calculs asynchrones (Task, Future, etc.).

Cette bibliothèque implémente une solution similaire à async/await mais généralisée à tout type de monade. Cette généralisation est un facteur majeur étant donné que certaines bases de code utilisent d'autres monades comme Task en plus de Future pour les calculs asynchrones.

Étant donné une monade M , la généralisation utilise le concept d'élévation des valeurs régulières vers une monade ( T => M[T] ) et la suppression des valeurs d'une instance de monade ( M[T] => T ). > Exemple d'utilisation :

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Notez que lift correspond à async et unlift à wait.

C'est déjà vrai avec le code de synchronisation normal et les fermetures. Par example:

Je vois plusieurs différences ici :

  1. Le contexte lambda est inévitable, mais ce n'est pas pour await . Avec await nous n'avons pas de contexte, avec async nous devons en avoir un. Le premier gagne, car il offre les mêmes fonctionnalités, mais nécessite d'en savoir moins sur le code.
  2. Les lambdas ont tendance à être courts, plusieurs lignes au maximum pour que nous puissions voir tout le corps à la fois, et simples. async peuvent être assez grosses (aussi grosses que des fonctions normales) et compliquées.
  3. Les lambdas sont rarement imbriqués (sauf pour les appels then , mais c'est await est proposé), les blocs async sont imbriqués fréquemment.

Une autre chose que vous devez noter dans la proposition d'attente implicite complète est que vous ne pouvez pas appeler async fns à partir de contextes non asynchrones.

Hum, je ne l'avais pas remarqué. Cela ne sonne pas bien, car dans ma pratique, vous souhaitez souvent exécuter async à partir d'un contexte non asynchrone. En C#, async n'est qu'un mot-clé qui permet au compilateur de réécrire le corps de la fonction, il n'affecte en rien l'interface de la fonction, donc async Task<Foo> et Task<Foo> sont complètement interchangeables, et il découple l'implémentation et l'API.

Parfois, vous souhaiterez peut-être bloquer async tâche main . Vous devez bloquer (sinon vous retournez au système d'exploitation et le programme se termine) mais vous devez exécuter une requête HTTP asynchrone. Je ne sais pas quelle solution pourrait être ici, à part le piratage de main pour lui permettre d'être asynchrone aussi bien que nous le faisons avec le type de retour principal Result , si vous ne pouvez pas l'appeler depuis un main non asynchrone .

Une autre considération en faveur du await actuel est son fonctionnement dans d'autres langages populaires (comme l'a noté @fdietze ). Cela facilite la migration depuis d'autres langages tels que C#/TypeScript/JS/Python et constitue donc une meilleure approche en termes de recrutement de nouvelles personnes.

je vois plusieurs différences ici

Vous devez également réaliser que le RFC principal a déjà des blocs async , avec la même sémantique que la version implicite, donc.

Cela ne sonne pas bien, car dans ma pratique, vous souhaitez souvent exécuter async à partir d'un contexte non asynchrone.

Ce n'est pas un problème. Vous pouvez toujours utiliser async blocs non async contexte ( ce qui est bien parce qu'ils évaluent juste un F: Future comme toujours), et vous pouvez toujours spawn ou un bloc à terme en utilisant exactement la même API qu'avant.

Vous ne pouvez tout simplement pas appeler des async fn s, mais enveloppez-les plutôt dans un bloc async comme vous le faites quel que soit le contexte dans lequel vous vous trouvez, si vous voulez un F: Future hors de lui.

async est juste un mot-clé qui permet au compilateur de réécrire le corps de la fonction, cela n'affecte en aucune façon l'interface de la fonction

Oui, c'est une différence légitime entre les propositions. Il a également été couvert dans le fil interne. On peut soutenir qu'avoir des interfaces différentes pour les deux est utile car cela vous montre que la version async fn n'exécutera aucun code dans le cadre de la construction, tandis que la version -> impl Future peut par exemple lancer une requête avant de vous donner un F: Future . Cela rend également les async fn plus cohérents avec les fn normaux, en ce sens qu'appeler quelque chose déclaré comme -> T vous donnera toujours un T , peu importe si c'est async .

(Vous devriez également noter que dans Rust, il y a encore un sacré saut entre async fn et la version Future -returning, comme décrit dans le RFC. La version async fn ne mentionne pas Future n'importe où dans sa signature ; et la version manuelle nécessite impl Trait , ce qui entraîne quelques problèmes liés à la durée de vie. C'est, en fait, une partie de la motivation pour async fn pour commencer.)

Il facilite la migration depuis d'autres langages tels que C#/TypeScript/JS/Python

Ceci n'est un avantage que pour la syntaxe littérale await future , qui est assez problématique en elle-même dans Rust. Tout ce avec quoi nous pourrions nous retrouver a également une incompatibilité avec ces langages, tandis qu'implicite await a au moins a) des similitudes avec Kotlin et b) des similitudes avec du code synchrone basé sur des threads.

Oui, c'est une différence légitime entre les propositions. Il a également été couvert dans le fil interne. On peut soutenir qu'avoir des interfaces différentes pour les deux est utile

Je dirais que _avoir des interfaces différentes pour les deux a certains avantages_, car le fait que l'API dépende des détails de l'implémentation ne me semble pas bien. Par exemple, vous rédigez un contrat qui délègue simplement un appel au futur interne

fn foo(&self) -> Future<T> {
   self.myService.foo()
}

Et puis vous voulez juste ajouter de la journalisation

async fn foo(&self) -> T {
   let result = await self.myService.foo();
   self.logger.log("foo executed with result {}.", result);
   result
}

Et cela devient un changement décisif. Ouah ?

Ceci n'est un avantage que pour la syntaxe future littérale, qui est assez problématique en soi dans Rust. Tout ce avec quoi nous pourrions nous retrouver a également une incompatibilité avec ces langages, tandis qu'implicite await a au moins a) des similitudes avec Kotlin et b) des similitudes avec du code synchrone basé sur des threads.

C'est un avantage pour toute syntaxe await , await foo / foo await / foo@ / foo.await /... même chose, la seule différence est que vous le placez avant/après ou avez un sceau au lieu d'un mot-clé.

Vous devez également noter que dans Rust, il y a encore un grand pas entre async fn et la version future, comme décrit dans la RFC

Je le sais et ça m'inquiète beaucoup.

Et cela devient un changement décisif.

Vous pouvez contourner cela en retournant un bloc async . Sous la proposition implicite d'attente, votre exemple ressemble à ceci :

fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
    async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}

Et avec la journalisation :

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        result
    }
}

Le plus gros problème avec cette distinction survient lors de la transition de l'écosystème des futures implémentations manuelles et combinateurs (le seul moyen aujourd'hui) à async/wait. Mais même dans ce cas, la proposition vous permet de conserver l'ancienne interface et d'en fournir une nouvelle asynchrone à côté. C# est plein de ce modèle, par exemple.

Eh bien, cela semble raisonnable.

Cependant, je crois qu'une telle implicite (nous ne voyons pas si foo() ici est une fonction async ou de synchronisation) conduit aux mêmes problèmes que ceux qui se sont posés dans des protocoles tels que COM + et était une raison pour laquelle WCF a été implémenté tel qu'il était . Les gens avaient des problèmes lorsque les demandes distantes asynchrones ressemblaient à de simples appels de méthodes.

Ce code a l'air parfaitement bien, sauf que je ne peux pas voir si une demande est asynchrone ou synchronisée. Je crois que c'est une information importante. Par example:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        let bars: Vec<Bar> = Vec::new();
        for i in 0..100 {
           bars.push(self.my_other_service.bar(i, result));
        }
        result
    }
}

Il est crucial de savoir si bar est une fonction synchrone ou asynchrone. Je vois souvent await dans la boucle comme un marqueur que ce code doit être modifié pour obtenir de meilleurs résultats tout au long de la charge et des performances. Il s'agit d'un code que j'ai examiné hier (le code est sous-optimal, mais c'est l'une des itérations d'examen):

image

Comme vous pouvez le voir, j'ai facilement repéré que nous avons une boucle en attente ici et j'ai demandé à la changer. Lorsque le changement a été validé, nous avons obtenu une accélération de chargement de page 3x. Sans await je pourrais facilement oublier cette mauvaise conduite.

J'avoue que je n'ai pas utilisé Kotlin, mais la dernière fois que j'ai regardé ce langage, il me semblait qu'il s'agissait principalement d'une variante de Java avec moins de syntaxe, au point qu'il était facile de traduire mécaniquement l'un à l'autre. Je peux aussi imaginer pourquoi il serait apprécié dans le monde de Java (qui a tendance à être un peu lourd en syntaxe), et je suis conscient qu'il a récemment gagné en popularité, en particulier parce qu'il n'est pas Java (la situation Oracle contre Google ).

Cependant, si nous décidons de prendre en compte la popularité et la familiarité, nous pourrions vouloir jeter un œil à ce que fait JavaScript, qui est également explicite await .

Cela dit, await été introduit dans les langages grand public par C#, qui est peut-être un langage où la convivialité était considérée comme de la plus haute importance . En C#, les appels asynchrones sont indiqués non seulement par le mot-clé await , mais également par le suffixe Async des appels de méthode. L'autre fonctionnalité de langage qui partage le plus avec await , yield return est également visible dans le code.

Pourquoi donc? Mon point de vue est que les générateurs et les appels asynchrones sont des constructions trop puissantes pour les laisser passer inaperçus dans le code. Il existe une hiérarchie d'opérateurs de flux de contrôle :

  • exécution séquentielle d'instructions (implicite)
  • appels de fonction/méthode (assez apparent, à comparer avec par exemple Pascal où il n'y a pas de différence au niveau du site d'appel entre une fonction nulle et une variable)
  • goto (d'accord, ce n'est pas une hiérarchie stricte)
  • générateurs ( yield return tendance à se démarquer)
  • await + Async suffixe

Remarquez comment ils passent également du moins au plus verbeux, selon leur expressivité ou leur puissance.

Bien sûr, d'autres langues ont adopté des approches différentes. Les continuations de schéma (comme dans call/cc , qui n'est pas trop différent de await ) ou les macros n'ont pas de syntaxe pour montrer ce que vous appelez. Pour les macros, Rust a choisi de les rendre faciles à voir.

Je dirais donc qu'avoir moins de syntaxe n'est pas souhaitable en soi (il existe des langages comme APL ou Perl pour cela), et que la syntaxe n'a pas à être simplement passe-partout, et a un rôle important dans la lisibilité.

Il y a aussi un argument parallèle (désolé, je ne me souviens pas de la source, mais cela pourrait provenir de quelqu'un de l'équipe linguistique) selon lequel les gens sont plus à l'aise avec la syntaxe bruyante pour les nouvelles fonctionnalités lorsqu'elles sont nouvelles, mais sont alors d'accord avec un moins verbeux une fois qu'ils finissent par être couramment utilisés.


Quant à la question de await!(foo)? vs await foo? , je suis dans l'ancien camp. Vous pouvez intérioriser à peu près n'importe quelle syntaxe, mais nous sommes trop habitués à nous inspirer de l'espacement et de la proximité. Avec await foo? il y a une grande chance que l'on se remette en question sur la priorité des deux opérateurs, tandis que les accolades indiquent clairement ce qui se passe. Sauver trois personnages n'en vaut pas la peine. Et en ce qui concerne la pratique de l'enchaînement de await! s, bien que cela puisse être un idiome populaire dans certaines langues, je pense qu'il a trop d'inconvénients comme une mauvaise lisibilité et une mauvaise interaction avec les débogueurs pour qu'il vaut la peine d'être optimisé.

Sauver trois personnages n'en vaut pas la peine.

D'après mon expérience anecdotique, les caractères supplémentaires (par exemple les noms plus longs) ne sont pas vraiment un problème, mais les jetons supplémentaires peuvent être vraiment ennuyeux. En termes d'analogie avec le processeur, un nom long est un code en ligne droite avec une bonne localité - je peux simplement le taper à partir de la mémoire musculaire - tandis que le même nombre de caractères lorsqu'il implique plusieurs jetons (par exemple, la ponctuation) est ramifié et plein d'erreurs de cache.

(Je suis tout à fait d'accord pour dire que await foo? ne serait pas évident et que nous devrions l'éviter, et qu'il serait de loin préférable d'avoir à taper plus de jetons ; mon observation est seulement que tous les personnages ne sont pas créés égaux.)


@rpjohnst Je pense que votre proposition alternative pourrait avoir une réception légèrement meilleure si elle était présentée comme "explicite async" plutôt que "implicite en attente" :-)

Il est crucial de savoir si bar est une fonction synchrone ou asynchrone.

Je ne suis pas sûr que ce soit vraiment différent de savoir si une fonction est bon marché ou chère, ou si elle fait des E/S ou non, ou si elle touche un état global ou non. (Cela s'applique également à la hiérarchie de @lnicola - si les appels asynchrones s'exécutent jusqu'à la fin, tout comme les appels de synchronisation, ils ne sont vraiment pas différents en termes de puissance !)

Par exemple, le fait que l'appel était dans une boucle est tout aussi, sinon plus, important que le fait qu'il était asynchrone. Et dans Rust, où la parallélisation est tellement plus facile à réaliser, vous pourriez tout aussi bien suggérer que les boucles synchrones d' apparence coûteuse soient remplacées par des itérateurs Rayon !

Donc, je ne pense pas qu'exiger await soit vraiment très important pour attraper ces optimisations. Les boucles sont déjà toujours de bons endroits pour rechercher une optimisation, et les async fn s sont déjà un bon indicateur que vous pouvez obtenir une simultanéité d'E/S bon marché. Si vous manquez ces opportunités, vous pouvez même écrire un message Clippy pour un "appel asynchrone dans une boucle" que vous exécutez occasionnellement. Ce serait formidable d'avoir un lint similaire pour le code synchrone également !

La motivation pour "async explicite" n'est pas simplement "moins de syntaxe", comme l'implique @lnicola . C'est pour rendre le comportement de la syntaxe d'appel de fonction plus cohérent, de sorte que foo() exécute toujours le corps de foo jusqu'à la fin. Dans le cadre de cette proposition, omettre une annotation ne vous donne que du code moins simultané, c'est ainsi que pratiquement tout le code se comporte déjà. Sous « explicit wait », omettre une annotation introduit une concurrence accidentelle, ou au moins un entrelacement accidentel, ce qui est problématique .

Je pense que votre proposition alternative pourrait avoir une meilleure réception si elle était présentée comme "explicite async" plutôt que "implicite en attente" :-)

Le fil est nommé "construction future explicite, attente implicite", mais il semble que ce dernier nom soit resté. :P

Je ne suis pas sûr que ce soit vraiment différent de savoir si une fonction est bon marché ou chère, ou si elle fait des E/S ou non, ou si elle touche un état global ou non. (Cela s'applique également à la hiérarchie de @lnicola - si les appels asynchrones s'exécutent jusqu'à la fin, tout comme les appels de synchronisation, ils ne sont vraiment pas différents en termes de puissance !)

Je pense que c'est aussi important que de savoir que la fonction change d'état, et nous avons déjà un mot-clé mut à la fois du côté de l'appel et du côté de l'appelant.

La motivation pour "async explicite" n'est pas simplement "moins de syntaxe", comme l'implique @lnicola . C'est pour rendre le comportement de la syntaxe d'appel de fonction plus cohérent, de sorte que foo() exécute toujours le corps de foo jusqu'à la fin.

D'un côté c'est une bonne considération. De l'autre, vous pouvez facilement séparer la création future et l'exécution future. Je veux dire, si foo vous renvoie une abstraction qui vous permet ensuite d'appeler run et d'obtenir un résultat, cela ne rend pas foo une poubelle inutile qui ne fait rien, cela fait un très chose utile : il construit un objet que vous pouvez appeler des méthodes plus tard. Cela ne le rend pas différent. La méthode foo nous appelons n'est qu'une boîte noire et nous voyons sa signature Future<Output=T> et elle renvoie en fait un futur. Donc, nous le await explicitement quand nous voulons le faire.

Le fil est nommé "construction future explicite, attente implicite", mais il semble que ce dernier nom soit resté. :P

Personnellement, je pense que la meilleure alternative est "explicite async explicitement wait" :)


PS

J'ai aussi eu une pensée ce soir : avez-vous essayé de communiquer avec C# LDM ? Par exemple, des gars comme @HaloFour , @gafter ou @CyrusNajmabadi . Ce peut être une très bonne idée de leur demander pourquoi ils ont pris la syntaxe qu'ils ont prise. Je proposerais de demander à des gars d'autres langues également, mais je ne les connais tout simplement pas :) Je suis sûr qu'ils ont eu plusieurs débats sur la syntaxe existante et ils pourraient déjà en discuter beaucoup et ils peuvent avoir des idées utiles.

Cela ne signifie pas que Rust doit avoir cette syntaxe comme C#, mais cela permet simplement de prendre une décision plus pondérée.

Personnellement, je pense que la meilleure alternative est "explicite async explicitement wait" :)

La proposition principale n'est cependant pas "asynchrone explicite", c'est pourquoi j'ai choisi le nom. C'est " async implicite ", car vous ne pouvez pas dire en un coup d'œil où l'asynchrone est introduite. Tout appel de fonction non annoté peut construire un futur sans l'attendre, même si Future n'apparaît nulle part dans sa signature.

Pour ce que ça vaut la peine, le fil de fonctionnement interne ne comprend une alternative « async explicite await explicite », parce que son avenir compatible avec une ou l' autre alternative principale. (Voir la dernière section du premier message.)

as-tu essayé de communiquer avec C# LDM ?

L'auteur de la RFC principale l'a fait. Le point principal qui en est ressorti, pour autant que je m'en souvienne, était la décision de ne pas inclure Future dans la signature de async fn s. En C#, vous pouvez remplacer Task par d'autres types pour avoir un certain contrôle sur la façon dont la fonction est pilotée. Mais dans Rust, nous n'avons pas (et n'aurons pas) un tel mécanisme - tous les futurs passeront par un seul trait, il n'est donc pas nécessaire d'écrire ce trait à chaque fois.

Nous avons également communiqué avec les concepteurs du langage Dart, et cela a été une grande partie de ma motivation pour rédiger la proposition "asynchrone explicite". Dart 1 a eu un problème parce que les fonctions ne s'exécutaient pourrait exécuter la fonction entière lorsqu'il est appelé, ce qui éviterait également cette confusion.

Nous avons également communiqué avec les concepteurs du langage Dart, et cela a été une grande partie de ma motivation pour rédiger la proposition "asynchrone explicite". Dart 1 a eu un problème parce que les fonctions ne s'exécutaient pas jusqu'à leur première attente lorsqu'elles étaient appelées (pas tout à fait la même chose que le fonctionnement de Rust, mais similaire), et cela a causé une telle confusion que dans Dart 2, elles ont changé pour que les fonctions s'exécutent à leur premier attendre lorsqu'il est appelé. Rust ne peut pas le faire pour d'autres raisons, mais il pourrait exécuter la fonction entière lorsqu'il est appelé, ce qui éviterait également cette confusion.

Super expérience, je n'étais pas au courant. Ravi d'entendre que vous avez fait un travail aussi énorme. Bravo

J'ai aussi eu une pensée ce soir : avez-vous essayé de communiquer avec C# LDM ? Par exemple, des gars comme @HaloFour , @gafter ou @CyrusNajmabadi . Ce peut être une très bonne idée de leur demander pourquoi ils ont pris la syntaxe qu'ils ont prise.

Je suis heureux de fournir toutes les informations qui vous intéressent. Cependant, et je ne fais que les parcourir. Serait-il possible de résumer les questions spécifiques que vous vous posez actuellement ?

Concernant la syntaxe de await (cela peut être complètement stupide, n'hésitez pas à me crier dessus ; je suis un noob de programmation asynchrone et je n'ai aucune idée de ce dont je parle):

Au lieu d'utiliser le mot "attendre", ne pouvons-nous pas introduire un symbole/opérateur, similaire à ? . Par exemple, cela pourrait être # ou @ ou quelque chose d'autre qui n'est actuellement pas utilisé.

Par exemple, s'il s'agissait d'un opérateur suffixe :

let stuff = func()#?;
let chain = blah1()?.blah2()#.blah3()#?;

Il est très concis et se lit naturellement de gauche à droite : attendez d'abord ( # ), puis gérez les erreurs ( ? ). Il n'a pas le problème du mot-clé wait de postfix, où .await ressemble à un membre de structure. # est clairement un opérateur.

Je ne sais pas si postfix est le bon endroit pour cela, mais c'était comme ça à cause de la priorité. Comme préfixe :

let stuff = #func()?;

Ou diable même :

let stuff = func#()?; // :-D :-D

Cela a-t-il déjà été discuté ?

(Je me rends compte que cela commence un peu à s'approcher de la syntaxe « mash clavier aléatoire de symboles » pour laquelle Perl est tristement célèbre ... :-D )

@rayvector https://github.com/rust-lang/rust/issues/50547#issuecomment -388108875 , 5ème alternative.

@CyrusNajmabadi merci d'être venu. La question principale est de savoir quelle option parmi celles répertoriées correspond le mieux au langage Rust actuel, ou peut-être existe-t-il une autre alternative? Ce sujet n'est pas très long, vous pouvez donc le faire défiler rapidement de haut en bas. La question principale : est-ce que Rust doit suivre la méthode C#/TS/... await ou peut-être devrait-il implémenter la sienne. La syntaxe actuelle est-elle une sorte d'"héritage" que vous aimeriez modifier d'une manière ou d'une autre ou elle convient le mieux à C# et c'est également la meilleure option pour les nouveaux langages ?

La principale considération par rapport à la syntaxe C# est la priorité de l'opérateur await foo? doit attendre en premier, puis évaluer l'opérateur ? ainsi que la différence qui, contrairement à C#, ne s'exécute pas dans le thread appelant avant le premier await , mais ne démarre pas du tout, de la même manière que l'extrait de code actuel n'exécute les contrôles de négativité tant que GetEnumerator n'est pas appelé pour la première fois :

IEnumerable<int> GetInts(int n)
{
   if (n < 0)
      throw new InvalidArgumentException(nameof(n));
   for (int i = 0; i <= n; i++)
      yield return i;
}

Plus détaillé dans mon premier commentaire et discussion ultérieure.

@Pzixel Oh, je suppose que j'ai raté celui-là lorsque je parcourais ce fil plus tôt ...

En tout cas, je n'ai pas vu beaucoup de discussions à ce sujet, à part cette brève mention.

Y a-t-il de bons arguments pour/contre ?

@rayvector J'ai argumenté un peu ici en faveur d'une syntaxe plus verbeuse. L'une des raisons est celle que vous mentionnez :

la syntaxe « mash clavier aléatoire de symboles » pour laquelle Perl est tristement célèbre

Pour clarifier, je ne pense pas que await!(f)? soit vraiment en lice pour la syntaxe finale, il a été choisi spécifiquement parce que c'est un moyen solide de ne pas s'engager dans un choix particulier. Voici des syntaxes (y compris l'opérateur ? ) qui, je pense, sont toujours "en cours" :

  • await f?
  • await? f
  • await { f }?
  • await(f)?
  • (await f)?
  • f.await?

Ou peut-être une combinaison de ceux-ci. Le fait est que plusieurs d'entre eux contiennent des accolades pour être plus clair sur la priorité et il y a beaucoup d'options ici - mais l'intention est que await sera un opérateur de mot-clé, pas une macro, dans la version finale ( sauf changement majeur comme ce que rpjohnst a proposé).

Je vote pour un opérateur d'attente postfix simple (par exemple ~ ) ou le mot-clé sans parenthèses et avec la priorité la plus élevée.

J'ai parcouru ce fil et je voudrais proposer ce qui suit :

  • await f? évalue d'abord l'opérateur ? , puis attend le futur résultant.
  • (await f)? attend d'abord le futur, puis évalue l'opérateur ? rapport au résultat (en raison de la priorité ordinaire de l'opérateur Rust)
  • await? f est disponible sous forme de sucre syntaxique pour `(wait f)?. Je pense que le "retour futur d'un résultat" sera un cas très courant, donc une syntaxe dédiée a beaucoup de sens.

Je suis d'accord avec les autres commentateurs pour dire que await devrait être explicite. C'est assez simple de faire cela en JavaScript, et j'apprécie vraiment l'explicitation et la lisibilité du code Rust, et j'ai l'impression que rendre async implicite ruinerait cela pour le code asynchrone.

Il m'est venu à l'esprit que "bloc asynchrone implicite" devrait être implémentable en tant que proc_macro, qui insère simplement un mot-clé await avant tout futur.

La question principale est de savoir quelle option parmi celles répertoriées correspond le mieux au langage Rust actuel tel qu'il est,

Demander à un concepteur C# ce qui correspond le mieux au langage de rouille est... intéressant :)

Je ne me sens pas qualifié pour prendre une telle décision. J'aime la rouille et m'en mêler. Mais ce n'est pas une langue que j'utilise jour après jour. Je ne l'ai pas non plus profondément ancré dans ma psyché. En tant que tel, je ne pense pas être qualifié pour faire des déclarations sur les choix appropriés pour cette langue ici. Vous voulez me poser des questions sur Go/TypeScript/C#/VB/C++. Bien sûr, je me sentirais beaucoup plus à l'aise. Mais la rouille est trop hors de mon domaine d'expertise pour me sentir à l'aise avec de telles pensées.

La principale considération contre la syntaxe C# est la priorité des opérateurs await foo?

C'est quelque chose que je sens que je peux commenter. Nous avons beaucoup pensé à la priorité avec « wait » et nous avons essayé de nombreuses formes avant de choisir la forme que nous voulions. L'une des principales choses 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 "chaîner" quoi que ce soit au-delà de leur appel asynchrone. En d'autres termes, les gens semblaient fortement attirés par le fait que « attendre » soit la partie la plus importante de toute expression complète, et donc qu'elle soit près du sommet. Remarque : par « expression complète », j'entends des choses comme l'expression que vous obtenez en haut d'une déclaration d'expression, ou l'expression à droite d'une affectation de niveau supérieur, ou l'expression que vous passez en tant qu'« argument » à quelque chose.

La tendance des gens à vouloir « continuer » avec « attendre » à l'intérieur d'une expression é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 faisant await expr.M() .

C'est aussi pourquoi nous n'avons choisi aucune forme « implicite » pour « attendre ». Dans la pratique, c'était quelque chose à quoi les gens voulaient penser très clairement, et qu'ils voulaient au premier plan dans leur code afin qu'ils puissent y prêter attention. Chose intéressante, même des années plus tard, cette tendance s'est maintenue. c'est-à-dire que parfois nous regrettons bien des années plus tard que quelque chose soit excessivement verbeux. Certaines fonctionnalités sont bonnes de cette manière au début, mais une fois que les gens sont à l'aise avec, elles conviennent mieux à quelque chose de plus rapide. 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.

Jusqu'à présent, nous avons été très satisfaits du choix de priorité pour notre public. Nous pourrions, à l'avenir, apporter quelques modifications ici. Mais dans l'ensemble, il n'y a pas de forte pression pour le faire.

--

ainsi que la différence qui, contrairement à l'exécution de C#, ne s'exécute pas dans le thread de l'appelant avant la première attente, mais ne démarre pas du tout, de la même manière que l'extrait de code actuel n'exécute pas de vérifications de négativité tant que GetEnumerator n'est pas appelé pour la première fois :

OMI, la façon dont nous avons fait les recenseurs était en quelque sorte une erreur et a conduit à un tas de confusion au fil des ans. Cela a été particulièrement mauvais en raison de la propension à écrire beaucoup de code comme ceci :

```c#
void SomeEnumerator (X arguments)
{
// Valider Args, faire un travail synchrone.
return SomeEnumeratorImpl(args);
}

void SomeEnumeratorImpl (X arguments)
{
// ...
rendement
// ...
}

People have to write this *all the time* because of the unexpected behavior that the iterator pattern has.  I think we were worried about expensive work happening initially.  However, in practice, that doesn't seem to happen, and people def think about the work as happening when the call happens, and the yields themselves happening when you actually finally start streaming the elements.

Linq (which is the poster child for this feature) needs to do this *everywhere*, this highly diminishing this choice.

For ```await``` i think things are *much* better.  We use 'async/await' a ton ourselves, and i don't think i've ever once said "man... i wish that it wasn't running the code synchronously up to the first 'await'".  It simply makes sense given what the feature is.  The feature is literally "run the code up to await points, then 'yield', then resume once the work you're yielding on completes".  it would be super weird to not have these semantics to me since it is precisely the 'awaits' that are dictating flow, so why would anything be different prior to hitting the first await.

Also... how do things then work if you have something like this:

```c#
async Task FooAsync()
{
    if (cond)
    {
        // only await in method
        await ...
    }
} 

Vous pouvez totalement appeler cette méthode et ne jamais cliquer sur attendre. si "l'exécution ne s'exécute pas dans le thread de l'appelant avant la première attente", que se passe-t-il réellement ici ?

attendre? f est disponible en tant que sucre syntaxique pour `(wait f)?. Je pense que le "retour futur d'un résultat" sera un cas très courant, donc une syntaxe dédiée a beaucoup de sens.

Cela résonne le plus en moi. Il permet à « attendre » d'être le concept le plus élevé, mais permet également une gestion simple des types de résultats.

Une chose que nous savons de C# est que l'intuition des gens autour de la priorité est liée aux espaces. Donc, si vous avez "attendre x?" alors on a immédiatement l'impression que await a moins de priorité que ? parce que le ? contigu à l'expression. Si ce qui précède était réellement analysé comme (await x)? cela surprendrait notre public.

L'analyser comme await (x?) semblerait le plus naturel à partir de la syntaxe, et répondrait au besoin d'obtenir un « résultat » d'une future/tâche en retour, et de vouloir « attendre » que si vous avez réellement reçu une valeur . Si cela a ensuite renvoyé un résultat lui-même, il semble approprié de le combiner avec le « attendre » pour signaler que cela se produit par la suite. ainsi await? x? chaque ? se lie étroitement à la partie du code à laquelle il se rapporte le plus naturellement. Le premier ? concerne le await (et plus précisément le résultat de celui-ci), et le second concerne le x .

si "l'exécution ne s'exécute pas dans le thread de l'appelant avant la première attente", que se passe-t-il réellement ici ?

Rien ne se passe tant que l'appelant n'attend pas la valeur de retour de FooAsync , auquel cas le corps de FooAsync s'exécute jusqu'à ce qu'un await soit renvoyé.

Cela fonctionne de cette façon car les Rust Future sont pilotés par sondage, alloués à la pile et inamovibles après le premier appel à poll . L'appelant doit avoir la possibilité de les mettre en place - sur le tas pour les Future niveau supérieur, ou bien par valeur à l'intérieur d'un parent Future , souvent sur le "stack frame" d'un appel async fn --avant l' exécution de

Cela signifie que nous sommes bloqués soit avec a) une sémantique de type générateur C#, où aucun code ne s'exécute à l'invocation, soit b) une sémantique de type coroutine Kotlin, où l'appel de la fonction l'attend également immédiatement et implicitement (avec une fermeture de type async { .. } blocs lorsque vous avez besoin d'une exécution simultanée).

Je suis en quelque sorte en faveur de ce dernier, car il évite le problème que vous mentionnez avec les générateurs C#, et évite également complètement la question de la priorité des opérateurs.

@CyrusNajmabadi Dans Rust, Future ne fonctionne généralement pas jusqu'à ce qu'il soit généré en tant que Task (c'est beaucoup plus similaire à F# Async ):

let bar = foo();

Dans ce cas foo() renvoie un Future , mais il ne probablement pas faire quoi que ce soit. Vous devez le générer manuellement (ce qui est également similaire à F# Async ):

tokio::run(bar);

Lorsqu'il est généré, il exécutera alors le Future . Étant donné que c'est le comportement par défaut de Future , il serait plus cohérent pour async / Attendent à Rust à ne pas exécuter de code jusqu'à ce qu'il soit donné naissance.

Évidemment, la situation est différente en C#, car en C#, lorsque vous appelez foo() il commence immédiatement à exécuter le Task , il est donc logique en C# d'exécuter le code jusqu'au premier await .

Aussi... comment les choses fonctionnent-elles alors si vous avez quelque chose comme ça [...] Vous pouvez totalement appeler cette méthode et ne jamais cliquer sur attendre. si "l'exécution ne s'exécute pas dans le thread de l'appelant avant la première attente", que se passe-t-il réellement ici ?

Si vous appelez FooAsync() cela ne fait rien, aucun code n'est exécuté. Ensuite, lorsque vous le générez, il exécutera le code de manière synchrone, le await ne s'exécutera jamais, et il renvoie donc immédiatement () (qui est la version de Rust de void )

En d'autres termes, ce n'est pas "l'exécution ne s'exécute pas dans le thread de l'appelant avant la première attente", c'est "l'exécution ne s'exécute pas tant qu'elle n'est pas explicitement générée (comme avec tokio::run )"

Rien ne se passe tant que l'appelant n'attend pas la valeur de retour de FooAsync, auquel point le corps de FooAsync s'exécute jusqu'à ce qu'un wait ou qu'il revienne.

Ick. Cela semble malheureux. Il arrive souvent que je ne puisse jamais attendre quelque chose (souvent à cause d'une annulation et d'une composition avec des tâches). En tant que développeur, j'apprécierais toujours d'obtenir des erreurs précoces (ce qui est l'une des raisons les plus courantes pour lesquelles les gens veulent que l'exécution se déroule jusqu'à l'attente).

Cela signifie que nous sommes bloqués soit avec a) une sémantique de type générateur C#, où aucun code ne s'exécute à l'appel, soit b) une sémantique de type coroutine Kotlin, où l'appel de la fonction l'attend également immédiatement et implicitement (avec une fermeture de type async { . . } pour les cas où vous avez besoin d'une exécution simultanée).

Compte tenu de ceux-ci, je préférerais de loin le premier au second. Juste ma préférence personnelle cependant. Si l'approche kotlin semble plus naturelle pour votre domaine, alors allez-y !

@CyrusNajmabadi Ick. Cela semble malheureux. Il arrive souvent que je ne puisse jamais attendre quelque chose (souvent à cause d'une annulation et d'une composition avec des tâches). En tant que développeur, j'apprécierais toujours d'obtenir des erreurs précoces (ce qui est l'une des raisons les plus courantes pour lesquelles les gens veulent que l'exécution se déroule jusqu'à l'attente).

Je ressens exactement le contraire. D'après mon expérience avec JavaScript, il est très courant d'oublier d'utiliser await . Dans ce cas, le Promise fonctionnera toujours, mais les erreurs seront avalées (ou d'autres choses étranges se produisent).

Avec le style Rust/Haskell/F#, soit le Future s'exécute (avec une gestion correcte des erreurs), soit il ne s'exécute pas du tout. Ensuite, vous remarquez qu'il ne fonctionne pas, alors vous enquêtez et réparez le problème. Je crois que cela se traduit par un code plus robuste.

@Pauan @rpjohnst Merci pour les explications. Ce sont des approches que nous avons également envisagées. Mais il s'est avéré que cela n'était pas vraiment souhaitable dans la pratique.

Dans les cas où vous ne vouliez pas qu'il « fasse quoi que ce soit. Vous devez le générer manuellement », nous avons trouvé plus simple de modéliser cela comme renvoyant quelque chose qui générait des tâches à la demande. c'est-à-dire quelque chose d'aussi simple que Func<Task> .

Je ressens exactement le contraire. D'après mon expérience avec JavaScript, il est très courant d'oublier d'utiliser wait.

C # fonctionne pour essayer de s'assurer que vous attendiez ou que vous utilisiez la tâche de manière judicieuse.

mais les erreurs seront avalées

C'est le contraire de ce que je dis. Je dis que je veux que le code s'exécute avec impatience afin que les erreurs soient des choses que je frappe immédiatement, même dans le cas où je ne parviendrais jamais à exécuter le code dans la tâche. C'est la même chose avec les itérateurs. Je préférerais de loin savoir que je le créais de manière incorrecte au moment où j'appelle la fonction plutôt que potentiellement beaucoup plus loin si/quand l'itérateur est diffusé.

Ensuite, vous remarquez qu'il ne fonctionne pas, alors vous enquêtez et réparez le problème.

Dans les scénarios dont je parle, "ne pas courir" est tout à fait raisonnable. Après tout, mon application peut décider à tout moment qu'elle n'a pas besoin d'exécuter la tâche. Ce n'est pas le bug que je décris. Le bogue que je décris est que je n'ai pas réussi la validation, et je veux en savoir plus à ce sujet au plus près du point où j'ai logiquement créé le travail par opposition au moment où le travail doit réellement s'exécuter. Étant donné qu'il s'agit de modèles pour décrire le traitement asynchrone, il est souvent probable qu'ils soient éloignés les uns des autres. Il est donc utile d'avoir les informations sur les problèmes le plus tôt possible.

Comme mentionné, ce n'est pas hypothétique non plus. Une chose similaire se produit avec les flux/itérateurs. Les gens les créent souvent, mais ne les réalisent que plus tard. Cela a été un fardeau supplémentaire pour les gens d'avoir à retracer ces choses jusqu'à leur source. C'est pourquoi tant d'API (y compris la BCL) doivent maintenant faire la séparation entre le travail synchrone/précoce et le travail réel différé/paresseux.

C'est le contraire de ce que je dis. Je dis que je veux que le code s'exécute avec impatience afin que les erreurs soient des choses que je frappe immédiatement, même dans le cas où je ne parviendrais jamais à exécuter le code dans la tâche.

Je peux comprendre le désir d'erreurs précoces, mais je suis confus : dans quelle situation « finiriez-vous par ne pas générer le Future » ?

La façon dont les Future fonctionnent dans Rust est que vous composez des Future ensemble de différentes manières (y compris async/wait, y compris les combinateurs parallèles, etc.), et en faisant cela, il crée un unique fusionné Future qui contient tous les sous- Future s. Et puis au niveau supérieur de votre programme ( main ), vous utilisez ensuite tokio::run (ou similaire) pour le générer.

Mis à part cet unique appel tokio::run dans main , vous ne générerez généralement pas de Future s manuellement, à la place vous les composez simplement. Et la composition gère naturellement le frai/la gestion des erreurs/l'annulation/etc. correctement.

Je veux aussi clarifier quelque chose. Quand je dis quelque chose comme :

Mais il s'est avéré que cela n'était pas vraiment souhaitable dans la pratique.

Je parle très spécifiquement de choses avec notre langue/plate-forme. Je ne peux que donner un aperçu des décisions qui ont du sens pour C#/.Net/CoreFx etc. sens différent.

Je peux comprendre le désir d'erreurs précoces, mais je suis confus : dans quelle situation « finiriez-vous par ne pas engendrer le futur » ?

Tout le temps :)

Considérez comment Roslyn (le compilateur C#/VB/base de code IDE) est lui-même écrit. Il est fortement asynchrone et interactif . c'est-à-dire que le cas d'utilisation principal est d'être utilisé de manière partagée avec de nombreux clients qui y accèdent. Les services Cliest sont courants et interagissent avec l'utilisateur sur une multitude de fonctionnalités, dont beaucoup décident qu'ils n'ont plus besoin d'effectuer le travail qu'ils pensaient à l'origine important, car l'utilisateur effectue un certain nombre d'actions. Par exemple, pendant que l'utilisateur tape, nous effectuons des tonnes de compositions et de manipulations de tâches, et nous pouvons finir par décider de ne même pas les exécuter car un autre événement est survenu quelques ms plus tard.

Par exemple, pendant que l'utilisateur tape, nous effectuons des tonnes de compositions et de manipulations de tâches, et nous pouvons finir par décider de ne même pas les exécuter car un autre événement est survenu quelques ms plus tard.

N'est-ce pas simplement géré par l'annulation, cependant?

Et la composition gère naturellement le frai/la gestion des erreurs/l'annulation/etc. correctement.

On dirait simplement que nous avons deux modèles très différents pour représenter les choses. C'est bien :) Mes explications sont à prendre dans le contexte du modèle que nous choisissons. Ils peuvent ne pas avoir de sens pour le modèle que vous choisissez.

On dirait simplement que nous avons deux modèles très différents pour représenter les choses. C'est bien :) Mes explications sont à prendre dans le contexte du modèle que nous choisissons. Ils peuvent ne pas avoir de sens pour le modèle que vous choisissez.

Absolument, j'essaie simplement de comprendre votre point de vue, et aussi d'expliquer notre point de vue. Merci d'avoir pris le temps d'expliquer les choses.

N'est-ce pas simplement géré par l'annulation, cependant?

L'annulation est un concept orthogonal à l'asynchronie (pour nous). Ils sont couramment utilisés ensemble. Mais aucun ne nécessite l'autre.

Vous pourriez avoir un système entièrement sans annulation, et il se peut simplement que vous ne parveniez jamais à exécuter le code qui « attend » les tâches que vous avez composées. c'est-à-dire que pour une raison logique, votre code peut simplement dire "je n'ai pas besoin d'attendre 't', je vais juste faire autre chose". Rien à propos des tâches (dans notre monde) ne dicte ou ne nécessite qu'on s'attende à ce que cette tâche soit attendue. Dans un tel système, je voudrais obtenir une validation précoce.

Remarque : ceci est similaire au problème de l'itérateur. Vous pouvez appeler quelqu'un pour obtenir les résultats que vous avez l' intention d'utiliser plus tard dans votre code. Cependant, pour un certain nombre de raisons, il se peut que vous n'ayez pas à utiliser les résultats. Mon désir personnel serait toujours d'obtenir les résultats de validation tôt, même si techniquement je n'aurais pas pu les obtenir et que mon programme réussisse.

Je pense qu'il y a des arguments raisonnables dans les deux sens. Mais mon point de vue est que l'approche synchrone a eu plus d'avantages que d'inconvénients. Bien sûr, si l'approche synchrone ne correspond littéralement pas à la façon dont votre implémentation réelle veut fonctionner, cela semble répondre à la question de savoir ce que vous devez faire :D

En d'autres termes, je ne pense pas que votre approche soit mauvaise ici. Et s'il a de gros avantages autour de ce modèle que vous pensez être bon pour Rust, alors allez-y :)

Vous pourriez avoir un système entièrement sans annulation, et il se peut simplement que vous ne parveniez jamais à exécuter le code qui « attend » les tâches que vous avez composées. c'est-à-dire que pour une raison logique, votre code peut simplement dire "je n'ai pas besoin d'attendre 't', je vais juste faire autre chose".

Personnellement, je pense que c'est mieux géré par la logique habituelle if/then/else :

async fn foo() {
    if some_condition {
        await!(bar());
    }
}

Mais comme vous le dites, c'est juste une perspective très différente de C#.

Personnellement, je pense que c'est mieux géré par la logique habituelle if/then/else :

Oui. ce serait bien si la vérification de la condition pouvait être effectuée au même moment où la tâche est créée (et des tonnes de cas sont comme ça). Mais dans notre monde, ce n'est généralement pas le cas que les choses soient si bien connectées comme ça. Après tout, nous voulons faire avec impatience un travail asynchrone en réponse aux utilisateurs (afin que les résultats soient prêts en cas de besoin), mais nous pouvons décider plus tard que nous ne nous en soucions plus.

Dans nos domaines, « l'attente » se produit au moment où la personne « a besoin de la valeur », qui est une autre détermination/composante/etc. de la décision sur « devrais-je commencer à travailler sur la valeur ? »

Dans un sens, ils sont très découplés, et c'est considéré comme une vertu. Le producteur et le consommateur peuvent avoir des politiques totalement différentes, mais peuvent communiquer efficacement sur le travail asynchrone effectué grâce à la belle abstraction de la « Tâche ».

Quoi qu'il en soit, je me retirerai de l'opinion sync/async. Il est clair que des modèles très différents sont en jeu ici. :)

En termes de priorité, j'ai donné quelques informations sur la façon dont C# pense les choses. J'espère que c'est utile. Faites-moi savoir si vous voulez plus d'informations là-bas.

@CyrusNajmabadi Oui, vos idées ont été très utiles. Personnellement, je suis d'accord avec vous pour dire que await? foo est la voie à suivre (bien que j'aime aussi la proposition "explicite async ").

BTW, si vous voulez l'un des meilleurs avis d'experts sur toutes les subtilités du modèle .net autour de la modélisation du travail async/synchrone, et tous les avantages/inconvénients de ce système, alors @stephentoub serait la personne à qui parler. Il serait environ 100 fois meilleur que moi pour expliquer les choses, clarifier les avantages et les inconvénients et être probablement capable d'approfondir les modèles des deux côtés. Il connaît intimement l'approche du .net ici (y compris les choix faits et les choix rejetés), et la façon dont il a dû évoluer depuis le début. Il est également douloureusement conscient des coûts de performance des approches adoptées par .net (ce qui est l'une des raisons pour lesquelles ValueTask existe maintenant), et j'imagine que ce serait quelque chose auquel vous pensez en premier lieu avec votre désir de zéro/faible -abstractions de coûts.

D'après mes souvenirs, des réflexions similaires sur ces scissions ont été intégrées à l'approche de .net au début, et je pense qu'il pouvait très bien parler des décisions finales qui ont été prises et de leur pertinence.

Je voterais toujours en faveur de await? future même si cela semble un peu inconnu. Y a-t-il de réels inconvénients à les composer ?

Voici une autre analyse approfondie des avantages et des inconvénients des asynchrones froids (F#) et chauds (C#, JS): http://tomasp.net/blog/async-csharp-differences.aspx

Il existe maintenant une nouvelle RFC pour les macros postfix qui permettrait d'expérimenter avec postfix await sans changement de syntaxe dédié : https://github.com/rust-lang/rfcs/pull/2442

await {} est mon préféré ici, rappelant unsafe {} plus il montre la priorité.

let value = await { future }?;

@seunlanlege
oui, c'est réminiscence, donc les gens ont une fausse supposition qu'ils peuvent écrire du code comme celui-ci

let value = await {
   let val1 = future1;
   future2(val1)
}

Mais ils ne peuvent pas.

@Pzixel
si je vous comprends bien, vous supposez que les gens supposeraient que les contrats à terme sont implicitement attendus à l'intérieur d'un bloc await {} ? Je ne suis pas d'accord avec ça. await {} n'attendrait que sur l'expression à laquelle le bloc évalue.

let value = await {
    let future = create_future();
    future
};

Et ce devrait être un modèle qui est découragé

simplifié

let value = await { create_future() };

Vous proposez un énoncé où plus d'une expression « devrait être découragée ». Vous n'y voyez rien de mal ?

Est-il avantageux de faire de await un motif (à part ref etc) ?
Quelque chose comme:

let await n = bar();

Je préfère appeler cela un modèle async que await , bien que je ne vois pas beaucoup d'avantages à en faire une syntaxe de modèle. Les syntaxes de modèle fonctionnent généralement en double par rapport à leurs homologues d'expression.

Selon la page actuelle de https://doc.rust-lang.org/nightly/std/task/index.html , le mod de tâche consiste à la fois en réexportations depuis libcore et en réexportations pour liballoc, ce qui rend le résultat un peu ... sous-optimal. J'espère que cela sera résolu d'une manière ou d'une autre avant que cela ne se stabilise.

J'ai regardé le code. Et j'ai quelques suggestions :

  • [x] Le UnsafePoll trait et Poll ENUM ont des noms très similaires, mais ils ne sont pas liés. Je suggère de renommer UnsafePoll , par exemple en UnsafeTask .
  • [x] Dans la caisse à terme, le code était divisé en différents sous-modules. Maintenant, la plupart des codes sont regroupés en task.rs ce qui rend la navigation plus difficile. Je suggère de le diviser à nouveau.
  • [x] TaskObj#from_poll_task() a un nom étrange. Je suggère de le nommer new() place
  • [x] TaskObj#poll_task pourrait simplement être poll() . Le champ appelé poll pourrait être appelé poll_fn ce qui suggérerait également qu'il s'agit d'un pointeur de fonction
  • Waker peut utiliser la même stratégie que TaskObj et mettre la vtable sur la pile. Juste une idée, je ne sais pas si on veut ça. Serait-ce plus rapide parce que c'est un peu moins d'indirection ?
  • [ ] dyn est maintenant stable en bêta. Le code devrait probablement utiliser dyn là où il s'applique

Je peux également fournir un PR pour ce genre de choses. @cramertj @aturon n'hésitez pas à me contacter via Discord pour discuter des détails.

que diriez-vous d'ajouter simplement une méthode await() pour tous les Future ?

    /// just like and_then method
    let x = f.and_then(....);
    let x = f.await();

    await f?     =>   f()?.await()
    await? f     =>   f().await()?

/// with chain invoke.
let x = first().await().second().await()?.third().await()?
let x = first().await()?.second().await()?.third().await()?
let x = first()?.await()?.second().await()?.third().await()?

@zengsai Le problème est que await ne fonctionne pas comme une méthode normale. En fait, réfléchissez à ce que la méthode await ferait lorsqu'elle n'était pas dans un bloc/une fonction async . Les méthodes ne savent pas dans quel contexte elles sont exécutées, cela ne peut donc pas provoquer d'erreur de compilation.

@xfix ce n'est pas vrai en général. Le compilateur peut faire ce qu'il veut et pourrait gérer l'appel de méthode spécialement dans ce cas. L'appel de style de méthode résout le problème de préférence mais il est inattendu (wait ne fonctionne pas de cette façon dans d'autres langages) et serait probablement un vilain hack dans le compilateur.

@elszben Que le compilateur puisse faire ce qu'il veut ne signifie pas qu'il doit faire ce qu'il veut.

future.await() ressemble à un appel de fonction normal, alors que ce n'est pas le cas. Si vous voulez suivre cette voie, la syntaxe future.await!() proposée quelque part ci-dessus permettrait la même sémantique, et marquerait clairement avec une macro « Quelque chose de bizarre se passe ici, je sais.

Edit : message supprimé

J'ai déplacé ce message dans le futur RFC. Lien

Quelqu'un a-t-il regardé l'interaction entre async fn et #[must_use] ?

Si vous avez un async fn , l'appeler directement n'exécute aucun code et renvoie un Future ; il semble que tous les async fn devraient avoir un #[must_use] inhérent sur le type "externe" impl Future , donc vous ne pouvez pas les appeler sans faire quelque chose avec le Future .

En plus de cela, si vous attachez vous-même un #[must_use] au async fn , il semble que cela devrait s'appliquer au retour de la fonction interne . Donc, si vous écrivez #[must_use] async fn foo() -> T { ... } , vous ne pouvez pas écrire await!(foo()) sans faire quelque chose avec le résultat de l'attente.

Quelqu'un a-t-il examiné l'interaction entre async fn et #[must_use] ?

Pour les autres personnes intéressées par cette discussion, voir https://github.com/rust-lang/rust/issues/51560.

Je pensais à la façon dont les fonctions asynchrones sont implémentées et j'ai réalisé que ces fonctions ne prennent pas en charge la récursivité, ni la récursivité mutuelle non plus.

pour la syntaxe d'attente, je suis personnellement vers les macros post-fix, pas d'approche d'attente implicite, pour son chaînage facile, et qu'elle peut aussi être utilisée un peu comme un appel de méthode

@ warlord500, vous await .

@Pzixel s'il vous plaît ne
Je sais que certains contributeurs pourraient ne pas vouloir autoriser le chaînage en attente, mais il y en a certains d'entre nous
développeurs qui le font. Je ne sais pas où vous avez même eu l'idée que j'ignorais
les opinions des développeurs, mon commentaire spécifiait uniquement l'opinion d'un membre de la communauté et mes raisons pour avoir cette opinion.

EDIT : si vous avez une divergence d'opinion, n'hésitez pas à partager ! Je suis curieux de savoir pourquoi tu dis
nous ne devrions pas autoriser le chaînage des attentes via une méthode telle que la syntaxe ?

@warlord500 parce que l'équipe MS a partagé son expérience avec des milliers de clients et des millions de développeurs. Je le sais moi-même parce que j'écris du code asynchrone/attente au jour le jour, et vous ne voulez jamais les enchaîner. Voici le devis exact, si vous le souhaitez :

Nous avons beaucoup pensé à la priorité avec « wait » et nous avons essayé de nombreuses formes avant de choisir la forme que nous voulions. L'une des principales choses 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 "chaîner" quoi que ce soit au-delà de leur appel asynchrone. En d'autres termes, les gens semblaient fortement attirés par le fait que « attendre » soit la partie la plus importante de toute expression complète, et donc qu'elle soit près du sommet. Remarque : par « expression complète », j'entends des choses comme l'expression que vous obtenez en haut d'une déclaration d'expression, ou l'expression à droite d'une affectation de niveau supérieur, ou l'expression que vous passez en tant qu'« argument » à quelque chose.

La tendance des gens à vouloir « continuer » avec « attendre » à l'intérieur d'une expression é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 faisant await expr.M() .

Je suis maintenant assez confus, si je vous comprends bien, nous ne devrions pas soutenir
attendre le style de chaîne facile post-fix parce qu'il n'est pas couramment utilisé? vous voyez attendre comme étant la partie la plus importante d'une expression.
Je présume seulement dans ce cas de m'assurer de bien vous comprendre.
Si je me trompe, n'hésitez pas à me corriger.

aussi, vous pouvez s'il vous plaît poster le lien vers l'endroit où vous avez obtenu le devis,
Je vous remercie.

mon contre aux deux points ci-dessus est simplement parce que vous n'utilisez pas quelque chose de commun, cela ne signifie pas nécessairement que le soutenir serait nocif pour le cas où cela rend le code plus propre.

attendent parfois inst la partie la plus importante d'une expression, si la future expression génératrice est
la partie la plus importante et que vous voudriez la mettre vers le haut, vous pouvez toujours le faire si nous autorisons un style de macro postfix en plus du style de macro normal

aussi, vous pouvez s'il vous plaît poster le lien vers l'endroit où vous avez obtenu le devis,
Je vous remercie.

Mais... mais tu as dit que tu as lu tout le fil...

Mais je n'ai aucun problème à le partager : https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886 . Je vous suggère de lire tous les articles de Cyrus, c'est vraiment l'expérience de tout l'écosystème C#/.Net, c'est une expérience inestimable qui peut être réutilisée par Rust.

parfois attendre inst la partie la plus importante d'une expression

La citation dit clairement le contraire 😄 Et vous savez, j'ai le même sentiment moi-même, en écrivant async/wait au quotidien.

Avez-vous une expérience avec async/wait ? Pouvez-vous le partager alors, s'il vous plaît?

Wow, je ne peux pas croire que j'ai raté ça. Merci d'avoir pris le temps de votre journée pour faire le lien.
Je n'ai aucune expérience, donc je suppose que dans le grand schéma, mon opinion n'a pas beaucoup d'importance

@Pzixel J'apprécie que vous async / await , mais soyez respectueux envers les autres contributeurs. Vous n'avez pas besoin de critiquer le niveau d'expérience des autres pour faire entendre vos propres points techniques.

Note du modérateur : @Pzixel Les attaques personnelles contre les membres de la communauté ne sont pas autorisées. Je l'ai supprimé de votre commentaire. Ne le refait pas. Si vous avez des questions sur notre politique de modération, veuillez nous contacter à l'

@crabtw Je n'ai critiqué personne. Je m'excuse pour tout inconvénient qui pourrait avoir lieu ici.

J'ai posé une question sur l'expirience une fois lorsque je voulais comprendre si une personne avait un réel besoin d'enchaîner les 'attendre' ou si c'était son extrapolation des caractéristiques d'aujourd'hui. Je ne voulais pas faire appel à l'autorité, c'est juste un tas d'informations utiles où je peux dire "vous devez l'essayer vous-même et réaliser cette vérité vous-même". Rien d'offensant ici.

Les attaques personnelles contre les membres de la communauté ne sont pas autorisées. Je l'ai supprimé de votre commentaire.

Aucune attaque personnelle. Comme je peux le voir, vous avez commenté ma référence sur les votes négatifs. Eh bien, c'était juste mon réacteur sur mon post downvote, rien de spécial. Comme elle a été supprimée, il est également raisonnable de supprimer cette référence (cela peut même être déroutant pour les autres lecteurs), alors merci de l'avoir supprimée.

Merci pour la référence. Je voulais mentionner que vous ne devriez prendre aucun de ce que je dis comme 'évangile' :) Rust et C# sont des langages différents avec des communautés, des paradigmes et des idiomes différents. Vous devez faire les meilleurs choix pour votre langue. J'espère que mes mots sont utiles et peuvent donner un aperçu. Mais soyez toujours ouvert à différentes façons de faire les choses.

J'espère que vous proposerez quelque chose d'incroyable pour Rust. Ensuite , nous pouvons voir ce que vous avez fait et volons adopter gracieusement pour C # :)

Pour autant que je sache, l'argument lié parle principalement de la priorité de await , et soutient en particulier qu'il est logique d'analyser await x.y() comme await (x.y()) plutôt que (await x).y() parce que l'utilisateur voudra et attendra plus souvent la première interprétation (et l'espacement suggère également cette interprétation). Et j'aurais tendance à être d'accord, bien que j'observe également que la syntaxe comme await!(x.y()) supprime l'ambiguïté.

Cependant, je ne pense pas que cela suggère une réponse particulière concernant la valeur du chaînage comme x.y().await!().z() .

Le commentaire cité est intéressant en partie parce qu'il y a une grande différence dans Rust, qui a été l'un des grands facteurs pour retarder notre détermination de la syntaxe d'attente finale : C# n'a pas d'opérateur ? , donc ils n'ont pas de code qui devrait être écrit (await expr)? . Ils décrivent (await expr).M() comme vraiment rare, et j'ai tendance à penser que ce serait également vrai dans Rust, mais la seule exception à cela, de mon point de vue, est ? , qui sera très courant parce que de nombreux futurs seront évalués en fonction des résultats ( tous ceux qui existent actuellement le font, par exemple).

@withoutboats oui, c'est vrai. Je voudrais citer encore une fois cette partie :

la seule exception à cela, de mon point de vue, est ?

S'il n'y a qu'une exception, il semble raisonnable de créer await? foo comme raccourci pour (await foo)? et d'avoir le meilleur des deux mondes.

Pour le moment, au moins, la syntaxe proposée de await!() permettra une utilisation sans ambiguïté de ? . Nous pouvons nous inquiéter d'une syntaxe plus courte pour la combinaison de await et ? si et quand nous décidons de changer la syntaxe de base pour await . (Et selon ce que nous changeons en , nous pourrions ne pas avoir de problème du tout.)

@joshtriplett ces accolades supplémentaires suppriment l'ambiguïté, mais elles sont vraiment très lourdes. Par exemple, rechercher dans mon projet actuel :

Matching lines: 139 Matching files: 10 Total files searched: 77

J'ai 139 attend dans 2743 sloc. Ce n'est peut-être pas grave, mais je pense que nous devrions considérer l'alternative sans bretelles comme une alternative plus propre et meilleure. Cela dit, ? est la seule exception, donc nous pourrions facilement utiliser await foo sans accolades, et introduire une syntaxe spéciale juste pour ce cas particulier. Ce n'est pas grave, mais cela pourrait sauver quelques accolades pour un projet LISP.

J'ai créé un article de blog expliquant pourquoi je pense que les fonctions asynchrones devraient utiliser l'approche du type de retour externe pour leur signature. Bonne lecture!

https://github.com/MajorBreakfast/rust-blog/blob/master/posts/2018-06-19-outer-return-type-approach.md

Je n'ai pas suivi toutes les discussions, alors n'hésitez pas à m'indiquer où cela aurait déjà été discuté si je l'avais manqué.

Voici une préoccupation supplémentaire concernant l'approche du type de retour interne : à quoi ressemblerait la syntaxe pour Stream s, lorsqu'elle sera spécifiée ? Je pense que async fn foo() -> impl Stream<Item = T> serait joli et cohérent avec async fn foo() -> impl Future<Output = T> , mais cela ne fonctionnerait pas avec l'approche du type de retour interne. Et je ne pense pas que nous voudrons introduire un mot-clé async_stream .

@Ekleog Stream devrait utiliser un mot-clé différent. Il ne peut pas utiliser async car impl Trait fonctionne dans l'autre sens. Il ne peut que garantir que certains traits sont mis en œuvre, mais les traits eux-mêmes doivent déjà être mis en œuvre sur le type concret sous-jacent.

L'approche du type de retour externe serait cependant utile si nous souhaitions un jour ajouter des fonctions de générateur asynchrone :

async_gen fn foo() -> impl AsyncGenerator<Yield = i32, Return = ()> { yield 1; ... }

Stream pourrait être implémenté pour tous les générateurs asynchrones avec Return = () . Cela rend cela possible :

async_gen fn foo() -> impl Stream<Item = i32> { yield 1;  ... }

Remarque : Les générateurs sont déjà activés tous les soirs, mais ils n'utilisent pas cette syntaxe. Ils ne sont pas non plus conscients de l'épinglage, contrairement à Stream dans les contrats à terme 0.3.

Edit : ce code utilisait auparavant un Generator . J'ai raté une différence entre Stream et Generator . Les flux sont asynchrones. Cela signifie qu'ils peuvent mais ne doivent pas donner de valeur. Ils peuvent soit répondre avec Poll::Ready ou Poll::Pending . D' Generator autre côté, un AsyncGenerator pour refléter cela.

Edit2 : @Ekleog L'implémentation actuelle des générateurs utilise une syntaxe sans marqueur et semble détecter qu'il devrait s'agir d'un générateur en recherchant un yield à l'intérieur du corps. Cela signifie que vous auriez raison de dire que async pourraient être réutilisés. Cependant, si cette approche est sensée, c'est une autre question. Mais je suppose que c'est pour un autre sujet ^^'

En effet, je pensais que async pourrait être réutilisé, ne serait-ce que parce que async ne serait, selon cette RFC, autorisé qu'avec des Future s, et donc pourrait détecter il génère un Stream en regardant le type de retour (qui doit être soit un Future soit un Stream ).

La raison pour laquelle je soulève cela maintenant est que si nous voulons avoir le même mot-clé async pour générer à la fois Future s et Stream s, alors je pense que le retour externe L'approche de type serait beaucoup plus propre, car elle serait explicite, et je ne pense pas que quiconque s'attendrait à ce qu'un async fn foo() -> i32 produise un flux de i32 (ce qui serait possible si le corps contenait un yield et l'approche de type de retour interne a été choisie).

Nous pourrions avoir un deuxième mot-clé pour les générateurs (par exemple gen fn ), puis créer des flux simplement en appliquant les deux (par exemple async gen fn ). Le type de retour externe n'a pas du tout besoin d'intervenir.

@rpjohnst Je l'ai

Nous ne voulons pas définir deux types associés. Un flux n'est toujours qu'un seul type, pas impl Iterator<Item=impl Future>> ou quelque chose comme ça.

@rpjohnst Je voulais dire les types associés Yield et Return de générateurs (async)

gen fn foo() -> impl Generator<Yield = i32, Return = ()> { ... }

C'était mon croquis d'origine, mais je pense que parler de générateurs prend trop d'avance, du moins pour le problème de suivi :

// generator
fn foo() -> T yields Y

// generator that implements Iterator
fn foo() yields Y

// async generator
async fn foo() -> T yields Y

// async generator that implements Stream
async fn foo() yields Y

Plus généralement, je pense que nous devrions avoir plus d'expérience avec la mise en œuvre avant de revoir les décisions prises dans le RFC. Nous tournons autour des mêmes arguments que nous avons déjà avancés, nous avons besoin d'une expérience avec la fonctionnalité telle que proposée par la RFC pour voir si une repondération est nécessaire.

J'aimerais être entièrement d'accord avec vous, mais je me demande simplement : si je lis correctement votre commentaire, la stabilisation de la syntaxe async/await attendra une syntaxe et une implémentation décentes pour les flux asynchrones, et d'acquérir de l'expérience avec les deux ? (car il ne serait pas possible de changer entre les types de retour externes et les types de retour internes une fois stabilisé)

Je pensais qu'async/wait était attendu pour Rust 2018 et n'espérais pas que les générateurs asynchrones soient prêts d'ici là, mais…?

(De plus, mon commentaire était uniquement destiné à être un argument supplémentaire pour le billet de blog de @MajorBreakfast , mais il semble avoir complètement effacé la discussion sur ce sujet… ce n'était pas du tout mon objectif, et je suppose que le débat devrait se recentrer sur cet article de blog ?)

Le cas d'utilisation étroit du mot-clé wait me confond toujours. (Esp. Future vs Stream vs Generator)

Un mot-clé de rendement ne serait-il pas suffisant pour tous les cas d'utilisation ? Un péché

{ let a = yield future; println(a) } -> Future

Ce qui maintient le type de retour explicite et donc un seul mot-clé est nécessaire pour toute la sémantique basée sur la "continuation" sans fusionner trop étroitement le mot-clé et la bibliothèque.

(Nous l'avons fait dans la langue d'argile d'ailleurs)

@aep await ne donne pas d'avenir au générateur-- il interrompt l'exécution du Future et rend le contrôle à l'appelant.

@cramertj eh bien, cela aurait pu faire exactement cela (retourner un futur qui contient la continuation après le mot-clé yield), ce qui est un cas d'utilisation beaucoup plus large.
mais je suppose que je suis un peu en retard à la fête pour cette discussion ? :)

@aep Le raisonnement pour un await Spécifiques mot - clé est pour composabilité avec un futur générateur spécifique yield mot - clé. Nous voulons prendre en charge les générateurs asynchrones, ce qui signifie deux "portées" de continuation indépendantes.

De plus, il ne peut pas retourner un futur qui contient la continuation, car les futurs Rust sont basés sur des sondages et non sur des rappels, au moins partiellement pour des raisons de gestion de la mémoire. Beaucoup plus facile pour poll de muter un seul objet que pour yield faire référence.

Je pense que async/wait ne devrait pas être un mot clé pour polluer la langue elle-même, car async n'est qu'une fonctionnalité et non interne de la langue.

@sackery Il fait partie des éléments internes du langage et ne peut pas être implémenté uniquement en tant que bibliothèque.

alors faites-le simplement comme mot-clé, tout comme nim, c# le fait !

Question : quelle devrait être la signature des fermetures async sans mouvement qui capturent des valeurs par référence mutable ? Actuellement, ils sont purement et simplement interdits. Il semble que nous voulions une sorte d'approche GAT qui permettrait à l'emprunt de la fermeture de durer jusqu'à ce que l'avenir soit mort, par exemple :

trait AsyncFnMut {
    type Output<'a>: Future;
    fn call(&'a mut self, args: ...) -> Self::Output<'a>;
}

@cramertj il y a un problème général ici avec le retour de références mutables à l'environnement capturé d'une fermeture. Peut-être que la solution n'a pas besoin d'être liée à async fn?

@sansbateaux c'est vrai, ça va être beaucoup plus courant dans des situations async que ça ne le serait probablement ailleurs.

Que diriez-vous de fn async au lieu de async fn ?
J'aime mieux let mut que mut let .

fn foo1() {
}
fn async foo2() {
}
pub fn foo3() {
}
pub fn async foo4() {
}

Une fois que vous avez recherché pub fn , vous pouvez toujours trouver toutes les fonctions publiques dans le code source.mais actuellement la syntaxe ne l'est pas.

fn foo1() {
}
async fn foo2() {
}
pub fn foo3() {
}
pub async fn foo4() {
}

Cette proposition n'est pas très importante, c'est une question de goût personnel.
Donc je respecte l'opinion à vous tous :)

Je pense que tous les modificateurs devraient passer avant fn. C'est clair et c'est comme ça que ça se passe dans d'autres langues. C'est juste du bon sens.

@Pzixel Je sais que les modificateurs d'accès doivent être fn car c'est important.
mais je pense que async est probablement pas.

@xmeta Je n'ai jamais vu cette idée proposée auparavant. Nous voulons probablement mettre async devant fn pour être cohérent, mais je pense qu'il est important de considérer toutes les options. Merci d'avoir posté!

// Status quo:
pub unsafe async fn foo() {} // #![feature(async_await, futures_api)]
pub const unsafe fn foo2() {} // #![feature(const_fn)]

@MajorBreakfast Merci pour votre réponse, j'ai pensé comme ça.

{ Public, Private } ⊇ Function  → put `pub` in front of `fn`
{ Public, Private } ⊇ Struct    → put `pub` in front of `struct`
{ Public, Private } ⊇ Trait     → put `pub` in front of `trait`
{ Public, Private } ⊇ Enum      → put `pub` in front of `enum`
Function ⊇ {Async, Sync}        → put `async` in back of `fn`
Variable ⊇ {Mutable, Imutable}  → put `mut` in back of `let`

@xmeta @MajorBreakfast

async fn est indivisible, il représente une fonction asynchrone。

async fn est un tout.

Vous recherchez pub fn ,cela signifie que vous recherchez une fonction de synchronisation publique.
De la même manière, vous recherchez pub async fn , cela signifie que vous recherchez une fonction asynchrone publique.

@ZhangHanDong

  • async fn définit une fonction normale qui renvoie un futur. Toutes les fonctions qui renvoient un futur sont considérées comme « asynchrones ». Les pointeurs de fonction de async fn s et d'autres fonctions qui renvoient un futur sont identiques°. Voici un exemple de terrain de jeu . Une recherche de "async fn" ne peut trouver que les fonctions qui utilisent la notation, elle ne trouvera pas toutes les fonctions asynchrones.
  • Une recherche de pub fn ne trouvera pas les fonctions unsafe ou const .

° Le type concret renvoyé par un async fn est bien entendu anonyme. Je veux dire qu'ils renvoient tous les deux un type qui implémente Future

@xmeta note que mut ne "va pas après let", ou plutôt, que mut ne modifie pas let . let prend un motif, c'est-à-dire

let PATTERN = EXPRESSION;

mut fait partie du PATTERN , pas du let lui-même. Par example:

// one is mutable one is not
let (mut a, b) = (1, 2);

@steveklabnik je comprends. Je voulais juste montrer l'association entre la structure hiérarchique et l'ordre des mots. Merci

Que pensent les gens du comportement souhaité de return et break intérieur des blocs async ? Actuellement, return renvoie du bloc asynchrone -- si nous autorisons return , c'est vraiment la seule option possible. Nous pourrions carrément bannir return et utiliser quelque chose comme 'label: async { .... break 'label x; } pour revenir d'un bloc asynchrone. Cela est également lié à la conversation sur l'opportunité d'utiliser le mot-clé break ou return pour la fonctionnalité de rupture de blocs (https://github.com/rust-lang/rust/issues/ 48594).

Je suis pour permettre return . Le principal souci de l'interdire est que cela pourrait être déroutant car il ne revient pas de la fonction en cours, mais du bloc asynchrone. Je doute cependant que ce soit déroutant. Les fermetures permettent déjà return et je n'ai jamais trouvé cela déroutant. Apprendre que return s'applique également aux blocs asynchrones est facile pour l'OMI et l'autoriser est très précieux pour l'OMI.

@cramertj return doit toujours quitter la fonction conteneur, jamais un bloc interne ; si cela n'a pas de sens pour que cela fonctionne, ce qui semble être le cas, alors return ne devrait pas fonctionner du tout.

Utiliser break pour cela semble malheureux, mais étant donné que nous avons malheureusement une valeur de rupture d'étiquette, c'est au moins cohérent avec cela.

Les mouvements et fermetures asynchrones sont-ils toujours prévus ? Ce qui suit est tiré du RFC :

// closure which is evaluated immediately
async move {
     // asynchronous portion of the function
}

et plus bas dans la page

async { /* body */ }

// is equivalent to

(async || { /* body */ })()

ce qui rend return aligné avec les fermetures, et semble assez facile à comprendre et à expliquer.

La rfc break-to-block prévoit-elle d'autoriser le saut d'une fermeture intérieure avec une étiquette ? Si ce n'est pas le cas (et je ne suggère pas que cela devrait l'autoriser), il serait très regrettable de refuser le comportement cohérent de returns , puis d'utiliser une alternative qui est également incompatible avec le rfc de break-to-blocks.

@memoryruins async || { ... return x; ... } devrait absolument fonctionner. Je dis que async { ... return x; ... } ne devrait pas, précisément parce que async n'est pas une fermeture. return a une signification très spécifique : "retour de la fonction contenante". Les fermetures sont une fonction. les blocs asynchrones ne le sont pas.

@memoryruins Les deux sont déjà implémentés.

@joshtriplett

les blocs asynchrones ne le sont pas.

Je suppose que je les considère toujours comme des fonctions dans le sens où elles sont un corps avec un contexte d'exécution défini séparément du bloc qui les contient, il est donc logique pour moi que return soit interne au async Bloc || et async .

@cramertj "syntaxique" est important, cependant.

Pensez-y de cette façon. Si vous avez quelque chose qui ne ressemble pas à une fonction (ou à une fermeture, et que vous avez l'habitude de reconnaître les fermetures comme des fonctions), et que vous voyez un return , où votre analyseur mental pense-t-il que cela va ?

Tout ce qui détourne return rend plus déroutant la lecture du code de quelqu'un d'autre. Les gens sont au moins habitués à l'idée que break renvoie à un bloc parent et ils devront lire le contexte pour savoir quel bloc. return a toujours été le plus gros marteau qui revient de toute la fonction.

Si elles ne sont pas traitées de la même manière que les fermetures évaluées immédiatement, je suis d'accord que le retour serait alors incohérent, en particulier sur le plan syntaxique. Si ? dans les blocs asynchrones a déjà été décidé (le RFC dit toujours qu'il était indécis), alors j'imagine qu'il serait aligné avec cela.

@joshtriplett, il me semble arbitraire de dire que vous pouvez reconnaître les fonctions et les fermetures (qui sont syntaxiquement très différentes) en tant que "portées de retour", mais les blocs asynchrones ne peuvent pas être reconnus de la même manière. Pourquoi deux formes syntaxiques distinctes sont-elles acceptables, mais pas trois ?

Il y a eu des discussions préalables sur ce sujet sur le RFC . Comme je l'ai dit ici, je suis en faveur des blocs asynchrones utilisant break _sans_ avoir à fournir une étiquette (il n'y a aucun moyen de sortir du bloc asynchrone vers une boucle externe pour ne pas perdre d'expressivité).

@withoutboats Une fermeture n'est qu'un autre type de fonction ; une fois que vous avez appris "une fermeture est une fonction", vous pouvez appliquer tout ce que vous savez sur les fonctions aux fermetures, y compris " return revient toujours de la fonction contenante".

@Nemo157 Même si vous avez sans étiquette break cibler le bloc async , vous devrez fournir un mécanisme (comme 'label: async ) pour revenir tôt d'une boucle à l'intérieur d'un bloc asynchrone .

@joshtriplett

Une fermeture n'est qu'un autre type de fonction ; une fois que vous avez appris "une fermeture est une fonction", vous pouvez appliquer tout ce que vous savez sur les fonctions aux fermetures, y compris "le retour revient toujours de la fonction contenant".

Je pense que les blocs async sont également une sorte de "fonction" - une fonction sans arguments qui peut être exécutée jusqu'à la fin de manière asynchrone. Il s'agit d'un cas particulier async fermetures

@cramertj oui , je supposais que tout point d'arrêt implicite peut également être étiqueté si nécessaire (comme je pense qu'ils le peuvent tous actuellement).

Tout ce qui rend le flux de contrôle plus difficile à suivre, et en particulier redéfinit ce que signifie return , met à rude épreuve la capacité à lire le code en douceur.

Dans le même ordre d'idées, le guide standard en C est "n'écrivez pas de macros qui reviennent du milieu de la macro". Ou, comme cas moins courant mais toujours problématique : si vous écrivez une macro qui ressemble à une boucle, break et continue devraient fonctionner à l'intérieur. J'ai vu des gens écrire des macros en boucle qui intègrent en fait deux boucles, donc break ne fonctionne pas comme prévu, et c'est extrêmement déroutant.

Je pense que les blocs asynchrones sont aussi une sorte de "fonction"

Je pense que c'est une perspective basée sur la connaissance des éléments internes de la mise en œuvre.

Je ne les vois pas du tout comme des fonctions.

Je ne les vois pas du tout comme des fonctions.

@joshtriplett

Je soupçonne que vous auriez avancé le même argument pour un langage avec des fermetures pour la première fois - que return ne devrait pas fonctionner dans la fermeture, mais dans la fonction de définition. Et en effet, il y a des langages qui prennent cette interprétation, comme Scala.

@cramertj je ne le ferais pas, non; pour les lambdas et/ou les fonctions définies dans une fonction, il semble tout à fait naturel qu'elles soient une fonction. (Ma première exposition à ceux-ci était en Python, FWIW, où les lambdas ne peuvent pas utiliser return et dans les fonctions imbriquées return renvoie de la fonction contenant le return .)

Je pense qu'une fois que l'on sait ce que fait un bloc asynchrone, il est intuitivement clair comment return doit se comporter. Une fois que vous savez que cela représente une exécution retardée, il est clair que return ne peut pas s'appliquer à la fonction. Il est clair que la fonction sera déjà revenue au moment où le bloc s'exécute. OMI apprendre cela ne devrait pas être un grand défi. Nous devrions au moins l'essayer et voir.

Cette RFC ne propose pas comment les constructions ? -operator et control-flow comme return , break et continue devraient fonctionner à l'intérieur des blocs asynchrones.

Serait-il préférable d'interdire tout opérateur de flux de contrôle ou de reporter les blocs jusqu'à ce qu'une RFC dédiée soit écrite ? Il y avait d'autres caractéristiques souhaitées mentionnées pour être discutées plus tard. En attendant, nous aurons des fonctions asynchrones, des fermetures et des await! :)

Je suis d'accord avec @memoryruins ici, je pense qu'il vaudrait la peine de créer un autre RFC pour discuter de ces spécificités plus en détail.

Que pensez-vous d'une fonction qui nous permet d'accéder au contexte depuis un fn asynchrone, peut-être appelé core::task::context() ? Il paniquerait simplement s'il était appelé de l'extérieur d'un fn asynchrone. Je pense que ce serait très pratique, par exemple pour accéder à l'exécuteur pour générer quelque chose.

@MajorBreakfast cette fonction s'appelle lazy

async fn foo() -> i32 {
    await!(lazy(|ctx| {
        // do something with ctx
        42
    }))
}

Pour quelque chose de plus spécifique comme le frai, il y aura probablement des fonctions d'assistance qui le rendront plus ergonomique

async fn foo() -> i32 {
    let some_task = lazy(|_| 5);
    let spawned_task = await!(spawn_with_handle(some_task));
    await!(spawned_task)
}

@Nemo157 En fait, c'est à spawn_with_handle que j'aimerais l'utiliser. Lors de la conversion du code en 0.3, j'ai remarqué que spawn_with_handle n'est en fait qu'un futur car il a besoin d'accéder au contexte ( voir code ). Ce que j'aimerais faire, c'est ajouter une méthode spawn_with_handle à ContextExt et faire de spawn_with_handle une fonction gratuite qui ne fonctionne qu'à l'intérieur des fonctions asynchrones :

fn poll(self: PinMut<Self>, cx: &mut Context) -> Poll<Self::Output> {
     let join_handle = ctx.spawn_with_handle(future);
     ...
}
async fn foo() {
   let join_handle = spawn_with_handle(future); // This would use this function internally
   await!(join_handle);
}

Cela supprimerait toutes les absurdités de double attente que nous avons actuellement.

À bien y penser, la méthode devrait s'appeler core::task::with_current_context() et fonctionner un peu différemment car il doit être impossible de stocker une référence.

Edit : Cette fonction existe déjà sous le nom get_task_cx . Il est actuellement dans libstd pour des raisons techniques. Je propose de rendre l'API publique une fois qu'elle pourra être mise dans libcore.

Je doute qu'il soit possible d'avoir une fonction qui puisse être appelée à partir d'une fonction non async qui pourrait vous donner le contexte d'une fonction parent async une fois qu'elle a été déplacée hors de TLS. À ce stade, le contexte sera probablement traité comme une variable locale cachée à l'intérieur de la fonction async , vous pourriez donc avoir une macro qui accède directement au contexte dans cette fonction, mais il n'y aurait aucun moyen d'avoir spawn_with_handle retire comme par magie le contexte de son appelant.

Donc, potentiellement quelque chose comme

fn spawn_with_handle(executor: &mut Executor, future: impl Future) { ... }

async fn foo() {
    let join_handle = spawn_with_handle(async_context!().executor(), future);
    await!(join_handle);
}

@ Nemo157 Je pense que vous avez raison: une fonction comme celle que je propose ne pourrait probablement pas fonctionner si elle n'est pas appelée directement depuis le fn asynchrone. Peut-être que le meilleur moyen est de faire de spawn_with_handle une macro qui utilise await interne (comme select! et join! ):

async fn foo() {
    let join_handle = spawn_with_handle!(future);
    await!(join_handle);
}

Cela a l'air sympa et peut être implémenté facilement via await!(lazy(|ctx| { ... })) à l'intérieur de la macro.

async_context!() est problématique car cela ne peut pas m'empêcher de stocker la référence de contexte entre les points d'attente.

async_context!() est problématique car cela ne peut pas m'empêcher de stocker la référence de contexte entre les points d'attente.

Selon la mise en œuvre, il peut. Si des arguments de générateur complets sont ressuscités, ils devraient être limités de sorte que vous ne puissiez de toute façon pas conserver une référence à travers le point de rendement, la valeur derrière l'argument aurait une durée de vie qui ne s'exécute que jusqu'au point de rendement. Async/wait hériterait simplement de cette limitation.

@ Nemo157 Vous voulez dire quelque chose comme ça ?

let my_arg = yield; // my_arg lives until next yield

@Pzixel Désolé de réveiller une vieille discussion _possiblement_, mais j'aimerais ajouter mes réflexions.

Oui, j'aime que la syntaxe await!() supprime l'ambiguïté en la combinant avec des choses comme ? , mais je suis également d'accord pour dire que cette syntaxe est ennuyeuse à taper mille fois dans un seul projet. Je pense aussi que c'est bruyant et qu'un code propre est important.

C'est pourquoi je me demande quel est le véritable argument contre un symbole suffixé (qui a déjà été mentionné plusieurs fois auparavant), tel que something_async()@ comparé à quelque chose avec await , peut-être parce que await est un mot-clé bien connu dans d'autres langues ? Le @ pourrait être amusant car il ressemble à un a de wait, mais il peut s'agir de n'importe quel symbole qui conviendrait parfaitement.

Je dirais qu'un tel choix de syntaxe serait logique car quelque chose de similaire s'est produit avec try!() qui est essentiellement devenu un suffixe ? (je sais que ce n'est pas exactement la même chose). Il est concis, facile à retenir et facile à taper.

Une autre chose géniale à propos d'une telle syntaxe est que le comportement est immédiatement clair lorsqu'il est combiné avec le symbole ? (du moins je pense que ce serait le cas). Jetez un œil à ce qui suit :

// Await, then unwrap a Result from the future
awaiting_a_result()@?;

// Unwrap a future from a result, then await
result_with_future()?@;

// The real crazy can make it as funky as they want
magic()?@@?@??@; 
// - I'm joking, of course

Cela n'a pas de problème car await future? fait là où il n'est pas clair à première vue ce qui se passera à moins que vous ne soyez au courant d'une telle situation. Et pourtant, son implémentation est cohérente avec ? .

Maintenant, il y a juste quelques petites choses auxquelles je peux penser qui contreraient cette idée :

  • c'est peut-être _trop_ concis et moins visible/verbeux contrairement à quelque chose avec await , ce qui rend _difficile_ de repérer les points de suspension dans une fonction.
  • c'est peut-être asymétrique avec le mot-clé async , où l'un est un mot-clé et l'autre un symbole. Bien que await!() souffre du même problème qui est un keywre par rapport à une macro.
  • choisir un symbole ajoute encore un autre élément syntaxique, et une chose à apprendre. Mais, en supposant que cela puisse devenir quelque chose de couramment utilisé, je ne pense pas que ce soit un problème.

@phaux a également mentionné l'utilisation du symbole ~ . Cependant, je pense que ce caractère est génial à taper sur un certain nombre de dispositions de clavier, je vous recommande donc de laisser tomber cette idée.

Quelles sont vos pensées les gars? Êtes-vous d'accord que c'est en quelque sorte similaire à la façon dont try!() _bevenue_ ? ? Préférez-vous await ou un symbole, et pourquoi ? Suis-je fou d'en discuter, ou peut-être que j'ai raté quelque chose ?

Désolé pour toute terminologie incorrecte que j'ai pu utiliser.

Le plus gros souci que j'ai avec la syntaxe basée sur les sigils est qu'elle peut facilement se transformer en soupe de glyphes, comme vous l'avez gentiment démontré. Quiconque connaît Perl (pré-6) comprendra où je veux en venir. Éviter le bruit de ligne est le meilleur moyen d'avancer.

Cela dit, peut-être que la meilleure façon de procéder est exactement comme avec try! ? C'est-à-dire, commencez avec une macro async!(foo) explicite, et si besoin est, ajoutez un sceau qui serait du sucre pour elle. Bien sûr, cela reporte le problème à plus tard, mais async!(foo) est largement suffisant pour une première itération d'async/await, avec l'avantage d'être relativement peu controversé. (et d'avoir le précédent de try! / ? devrait survenir d'un sceau)

@withoutboats Je n'ai pas lu l'intégralité de ce fil, mais est-ce que quelqu'un aide à la mise en œuvre ? Où est votre branche de développement ?

Et, concernant les considérations non résolues restantes, quelqu'un a-t-il demandé l'aide d'experts extérieurs à la communauté Rust ? Joe Duffy connaît et se soucie beaucoup de la concurrence et comprend assez bien les détails délicats , et il a donné un discours à RustConf , donc je soupçonne qu'il peut être disposé à une demande de conseils, si une telle demande est justifiée.

@BatmanAoD une implémentation initiale a été https://github.com/rust-lang/rust/pull/51580

Le fil RFC original avait des commentaires d'un certain nombre d'experts dans l'espace PLT, même en dehors de Rust :)

Je voudrais suggérer le symbole '$' pour attendre les Futures, parce que le temps c'est de l'argent, et je tiens à le rappeler au compilateur.

Je rigole. Je ne pense pas qu'avoir un symbole pour attendre soit une bonne idée. Rust consiste à être explicite et à permettre aux gens d'écrire du code de bas niveau dans un langage puissant qui ne vous permet pas de vous tirer une balle dans le pied. Un symbole est beaucoup plus vague qu'une macro await! , et permet aux gens de se tirer une balle dans le pied d'une manière différente en écrivant du code difficile à lire. Je dirais déjà que ? est un pas de trop.

Je suis également en désaccord avec l'existence d'un mot-clé async à utiliser sous la forme async fn . Cela implique une sorte de "biais" envers l'async. Pourquoi async mérite-t-il un mot-clé ? Le code asynchrone n'est pour nous qu'une autre abstraction qui n'est pas toujours nécessaire. Je pense qu'un attribut async agit plus comme une "extension" du dialecte de base Rust qui nous permet d'écrire du code plus puissant.

Je ne suis pas un architecte de langage, mais j'ai un peu d'expérience dans l'écriture de code asynchrone avec Promises en JavaScript, et je pense que la façon dont c'est fait là-bas rend le code asynchrone un plaisir à écrire.

@steveklabnik Ah, d'accord, merci. Pouvons-nous (/devrais-je) mettre à jour la description du problème ? Peut-être que l'élément « mise en œuvre initiale » devrait être divisé en « mise en œuvre sans support move » et « mise en œuvre complète » ?

La prochaine itération de mise en œuvre est-elle en cours d'élaboration dans une branche/une branche publique ? Ou cela ne peut-il même pas continuer jusqu'à ce que la RFC 2418 soit acceptée ?

Pourquoi le problème de syntaxe async/wait est-il discuté ici plutôt que dans une RFC ?

@c-edw Je pense que la question sur le mot-clé async est répondue par Quelle couleur est votre fonction ?

@parasyte Il m'a été suggéré que ce message est en fait un argument contre toute l'idée de fonctions asynchrones sans concurrence de style green-thread gérée automatiquement.

Je ne suis pas d'accord avec cette position, car les threads verts ne peuvent pas être implémentés de manière transparente sans un runtime (géré), et il y a de bonnes raisons pour que Rust prenne en charge le code asynchrone sans l'exiger.

Mais il semble que votre lecture du post soit que la sémantique async / await soit bonne, mais il y a une conclusion à tirer sur le mot-clé ? Cela vous ennuierait-il de développer cela ?

Là aussi, je partage ton point de vue. Je disais que le mot-clé async est nécessaire, et l'article expose le raisonnement qui le sous-tend. Les conclusions tirées par l'auteur sont différentes.

@parasyte Ah, d'accord. Je suis content d'avoir demandé - en raison de l'aversion de l'auteur pour les dichotomies rouge/bleu, je pensais que vous disiez le contraire !

J'aimerais clarifier davantage, car je pense que je n'ai pas tout à fait rendu justice.

La dichotomie est inévitable . Certains projets ont essayé de l'effacer en rendant chaque appel de fonction asynchrone, en imposant que les appels de fonction de synchronisation n'existent pas. Midori en est un exemple évident. Et d'autres projets ont tenté d'effacer la dichotomie en cachant les fonctions asynchrones derrière la façade des fonctions de synchronisation. gevent est un exemple de ce genre.

Les deux ont le même problème ; ils ont encore besoin de la dichotomie pour faire la distinction entre attendre qu'une tâche asynchrone se termine et démarrer une tâche de manière asynchrone !

  • Midori a introduit non seulement le mot-clé await , mais aussi un mot-clé async sur l'appel de fonction site .
  • gevent fournit gevent.spawn en plus de l'attente implicite des appels de fonction d'apparence normale.

C'est la seule raison pour laquelle j'ai évoqué l'article sur la couleur d'une fonction, car il répond à la question « Pourquoi async mérite-t-il un mot-clé ? »

Eh bien, même le code synchrone basé sur des threads peut distinguer "attendre qu'une tâche se termine" (rejoindre) et "démarrer une tâche" (spawn). Vous pouvez imaginer un langage où tout est async (en termes d'implémentation), mais il n'y a pas d'annotation sur await (car c'est le comportement par défaut), et le async Midori est plutôt une fermeture passée à un spawn API. Cela place le tout-async sur la même base syntaxique/fonction-couleur que tout-sync.

Donc, même si je suis d'accord que async mérite un mot-clé, il me semble que c'est plus parce que Rust se soucie du mécanisme de mise en œuvre à ce niveau et doit fournir les deux couleurs pour cette raison.

@rpjohnst Oui, j'ai lu vos propositions. C'est conceptuellement la même chose que de cacher les couleurs a la gevent. Ce que j'ai critiqué sur le forum rust dans le même fil; chaque appel de fonction semble synchrone, ce qui représente un danger particulier lorsqu'une fonction est à la fois synchrone et bloquante dans un pipeline asynchrone. Ce genre de bug est imprévisible et un véritable désastre à dépanner.

Je ne parle pas de ma proposition en particulier, je parle d'un langage où tout est asynchrone. Vous pouvez ainsi échapper à la dichotomie - ma proposition ne tente pas cela.

IIUC c'est exactement ce que Midori a tenté. À ce stade, les mots-clés contre les fermetures ne font que discuter de la sémantique.

Le jeu. 12 juillet 2018 à 15h01, Russell Johnston [email protected]
a écrit:

Vous avez utilisé la présence de mots-clés comme argument pour expliquer pourquoi la dichotomie
existe toujours à Midori. Si vous les supprimez, où est la dichotomie ? le
la syntaxe est identique au code all-sync, mais avec les capacités d'async
code.

Parce que lorsque vous appelez une fonction async sans attendre son résultat, elle
renvoie une promesse de manière synchrone. Ce qui peut être attendu plus tard. ??

_Wow, quelqu'un sait quelque chose sur Midori ? J'ai toujours pensé que c'était un projet fermé sur lequel presque aucune créature vivante ne travaillait. Ce serait intéressant si quelqu'un d'entre vous a écrit à ce sujet plus en détail._

/hors sujet

@Pzixel Aucune créature vivante n'y travaille encore , car le projet a été arrêté. Mais le blog de Joe Duffy contient de nombreux détails intéressants. Voir mes liens ci-dessus.

Nous avons déraillé ici, et j'ai l'impression de me répéter, mais cela fait partie de "la présence de mots-clés" - le mot-clé await . Si vous remplacez les mots-clés par des API comme spawn et join , vous pouvez être totalement asynchrone (comme Midori) mais sans aucune dichotomie (contrairement à Midori).

Ou en d'autres termes, comme je l'ai déjà dit, ce n'est pas fondamental - nous l'avons uniquement parce que nous voulons le choix.

@CyrusNajmabadi désolé de vous avoir encore voici quelques informations supplémentaires sur la prise de décision.

Si vous ne voulez plus être mentionné, dites-le-moi, s'il vous plaît. J'ai simplement pensé que vous pourriez être intéressé.

Depuis le canal discord #wg-net :

@cramertj
matière à réflexion : j'écris souvent Ok::<(), MyErrorType>(()) à la fin des blocs async { ... } . peut-être y a-t-il quelque chose que nous pouvons proposer pour rendre plus facile la contrainte du type d'erreur ?

@sansbateaux
[...] peut-être que nous voulons qu'il soit cohérent avec [ try ] ?

(Je me souviens d'une discussion relativement récente sur la façon dont les blocs try pourraient déclarer leur type de retour, mais je ne le trouve pas maintenant...)

teintes mentionnées :

async -> io::Result<()> {
    ...
}

async: io::Result<()> {
    ...
}

async as io::Result<()> {
    ...
}

Une chose que try peut faire et qui est moins ergonomique avec async est d'utiliser une liaison de variable ou une attribution de type, par exemple

let _: io::Result<()> = try { ... };
let _: impl Future<Output = io::Result<()>> = async { ... };

J'avais précédemment lancé l'idée d'autoriser une syntaxe de type fn pour le trait Future , par exemple Future -> io::Result<()> . Cela rendrait l'option de fourniture de type manuel un peu meilleure, bien qu'il y ait encore beaucoup de caractères :

let _: impl Future -> io::Result<()> = async {
}
async -> impl Future<Output = io::Result<()>> {
    ...
}

serait mon choix.

C'est similaire à la syntaxe de fermeture existante :

|x: i32| -> i32 { x + 1 };

Edit : Et finalement quand il est possible pour TryFuture d'implémenter Future :

async -> impl TryFuture<Ok = i32, Error = ()> {
    ...
}

Edit2 : Pour être précis, ce qui précède fonctionnerait avec les définitions de traits d'aujourd'hui. C'est juste qu'un type TryFuture n'est pas aussi utile aujourd'hui car il n'implémente actuellement pas Future

@MajorBreakfast Pourquoi -> impl Future<Output = io::Result<()>> plutôt que -> io::Result<()> ? Nous faisons déjà le return-type-desugaring pour async fn foo() -> io::Result<()> , donc IMO si nous utilisons une syntaxe basée sur -> il semble clair que nous voudrions le même sucre ici.

@cramertj Oui, cela devrait être cohérent. Mon message ci-dessus suppose un peu que je peux vous convaincre tous que l'approche du type de retour externe est supérieure 😁

Dans le cas où nous utiliserions async -> R { .. } nous devrions également utiliser try -> R { .. } ainsi qu'utiliser expr -> TheType en général pour l'attribution de type. En d'autres termes, la syntaxe d'attribution de type que nous utilisons doit être appliquée uniformément partout.

@Centril je suis d'accord. Il doit être utilisable partout. Je ne suis plus sûr que -> soit vraiment le bon choix. J'associe -> au fait d'être appelable. Et les blocs asynchrones ne le sont pas.

@MajorBreakfast Je suis fondamentalement d'accord; Je pense que nous devrions utiliser : pour l'attribution de type, donc async : Type { .. } , try : Type { .. } et expr : Type . Nous avons discuté des ambiguïtés potentielles sur Discord et je pense que nous avons trouvé une solution avec : qui a du sens...

Une autre question concerne Either enum. Nous avons déjà Either dans futures caisse de Either d' either caisse de

Parce que Futures semble être fusionné en standard (au moins des parties très basiques), pourrions-nous également y inclure Either ? Il est crucial de les avoir pour pouvoir retourner impl Future partir de la fonction.

Par exemple, j'écris souvent du code comme suit :

fn handler() -> impl Future<Item = (), Error = Bar> + Send {
    someFuture()
        .and_then(|x| {
            if condition(&x) {
                Either::A(anotherFuture(x))
            } else {
                Either::B(future::ok(()))
            }
        })
}

J'aimerais pouvoir l'écrire comme :

async fn handler() -> Result<(), Bar> {
    let x = await someFuture();
    if condition(&x) {
        await anotherFuture(x);
    }
}

Mais si je comprends bien, lorsque async est développé, cela nécessite que Either soit inséré ici, car soit nous entrons en condition, soit nous ne le faisons pas.

_Vous pouvez trouver le code réel ici si vous le souhaitez.

@Pzixel, vous n'aurez pas besoin de Either dans les fonctions async , tant que vous await les futurs, la transformation de code que async fait masquera ces deux types en interne et présentent un seul type de retour au compilateur.

@Pzixel Aussi, j'espère (personnellement) que Either ne sera pas introduit avec cette RFC, car cela introduirait une version restreinte de https://github.com/rust-lang/rfcs/issues /2414 (qui ne fonctionne qu'avec 2 types et uniquement avec des Future s), ajoutant ainsi probablement un cruft d'API si une solution générale est fusionnée - et comme @ Nemo157 l'a mentionné, il ne semble pas être une urgence de avoir Either ce moment :)

@Ekleog bien sûr, j'ai juste été frappé par cette idée que j'ai en fait des tonnes de either dans mon code asynchrone existant et j'aimerais vraiment m'en débarrasser. Ensuite, je me suis souvenu de ma confusion lorsque j'ai passé environ une demi-heure jusqu'à ce que je réalise qu'il ne compile pas parce que j'avais either crate dans les dépendances (les futures erreurs sont assez difficiles à comprendre donc cela a pris beaucoup de temps). C'est pourquoi j'ai écrit le commentaire, juste pour être sûr que ce problème est résolu d'une manière ou d'une autre.

Bien sûr, ce n'est pas lié à async/await seulement, c'est plus générique, donc ça mérite sa propre RFC. Je voulais seulement souligner que soit futures devrait connaître either ou vice versa (afin d'implémenter IntoFuture correctement).

@Pzixel Le Either exporté par la caisse à terme est une réexportation de la caisse either . La caisse 0.3 de futures ne peut pas implémenter Future pour Either cause des règles orphelines. Il est fort probable que nous allons également supprimer les impls Stream et Sink pour Either pour plus de cohérence et proposer une alternative à la place (discutée ici ). De plus, la caisse either pourrait alors implémenter Future , Stream et Sink elle-même, probablement sous un indicateur de fonctionnalité.

Cela dit, comme @Nemo157 l'a déjà mentionné, lorsque vous travaillez avec des contrats à terme, il est préférable d'utiliser simplement des fonctions asynchrones au lieu de Either .

Le truc async : Type { .. } est maintenant proposé dans https://github.com/rust-lang/rfcs/pull/2522.

Les fonctions async/await implémentant automatiquement Send déjà implémentées ?

Il semble que la fonction asynchrone suivante ne soit pas (encore ?) Send :

pub async fn __receive() -> ()
{
    let mut chan: futures::channel::mpsc::Receiver<Box<Send + 'static>> = None.unwrap();

    await!(chan.next());
}

Le lien vers le reproducteur complet (qui ne se compile pas sur le terrain de jeu faute de futures-0.3 , je suppose) est ici .

De plus, lors de l'enquête sur ce problème, je suis tombé sur https://github.com/rust-lang/rust/issues/53249, qui, je suppose, devrait être ajouté à la liste de suivi du premier message :)

Voici un terrain de jeu montrant que les fonctions async/await implémentant Send _devraient_ fonctionner. Décommenter la version Rc détecte correctement cette fonction comme non- Send . Je peux jeter un œil à votre exemple spécifique dans un instant (pas de compilateur Rust sur cette machine :slightly_frowning_face:) pour essayer de comprendre pourquoi cela ne fonctionne pas.

@Ekleog std::mpsc::Receiver n'est pas Sync , et le async fn vous avez écrit en contient une référence. Les références aux éléments !Sync sont !Send .

@cramertj Hmm… mais est-ce que je ne possède pas un mpsc::Receiver , qui devrait être Send si son type générique est Send ? (aussi, ce n'est pas un std::mpsc::Receiver mais un futures::channel::mpsc::Receiver , qui est aussi Sync si le type est Send , désolé de ne pas avoir remarqué le mpsc::Receiver alias était ambigu !)

@Nemo157 Merci ! J'ai ouvert https://github.com/rust-lang/rust/issues/53259 afin d'éviter trop de bruit sur ce problème :)

La question de savoir si et comment les blocs async autorisent ? et d'autres flux de contrôle pourraient justifier une certaine interaction avec les blocs try (par exemple try async { .. } pour autoriser ? sans confusion similaire à return ?).

Cela signifie que le mécanisme de spécification du type d' async bloc try bloc https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Frappez juste ce que je pensais au début être un problème de futures-rs , mais il s'avère qu'il s'agit peut-être d'un problème async/wait, alors le voici : https://github.com/rust-lang-nursery/ futures-rs/issues/1199#issuecomment -413089012

Comme discuté il y a quelques jours sur discord, await n'a pas encore été réservé en tant que mot-clé. Il est assez essentiel d'obtenir cette réservation (et de l'ajouter au mot-clé lint de l'édition 2018) avant la sortie de 2018. C'est une réservation un peu compliquée puisque nous voulons continuer à utiliser la syntaxe des macros pour le moment.

L'API Future/Task aura-t-elle un moyen de générer des futurs locaux ?
Je vois qu'il y a SpawnLocalObjError , mais il semble être inutilisé.

@panicbit Au groupe de travail, nous discutons actuellement de l'opportunité d'inclure la fonctionnalité de génération dans le contexte. https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError n'est pas entièrement inutilisé : LocalPool de la caisse à terme l'utilise. Vous avez cependant raison, rien dans libcore ne l'utilise)

@withoutboats J'ai remarqué que quelques-uns des liens dans la description du problème sont obsolètes. Plus précisément, https://github.com/rust-lang/rfcs/pull/2418 est fermé et https://github.com/rust-lang-nursery/futures-rs/issues/1199 a été déplacé vers https:// /github.com/rust-lang/rust/issues/53548

NB. Le nom de ce problème de suivi est async/wait mais il est également attribué à l'API de tâche ! L'API de tâche a actuellement une RFC de stabilisation en attente : https://github.com/rust-lang/rfcs/pull/2592

Une chance de rendre les mots-clés réutilisables pour des implémentations asynchrones alternatives ? actuellement, cela crée un avenir, mais c'est une sorte d'occasion manquée de rendre l'async basée sur le push plus utilisable.

@aep Il est possible de passer facilement d'un système push au système Future basé sur pull en utilisant oneshot::channel .

Par exemple, les promesses JavaScript sont basées sur le push, donc stdweb utilise oneshot::channel pour convertir les promesses JavaScript en Rust Futures . Il utilise également oneshot::channel pour d'autres API de rappel push, comme setTimeout .

En raison du modèle de mémoire de Rust, les Futures basés sur le push ont des coûts de performances supplémentaires par rapport au pull . Il est donc préférable de payer ce coût de performance uniquement lorsque cela est nécessaire (par exemple en utilisant oneshot::channel ), plutôt que d'avoir tout le système Future basé sur le push.

Cela dit, je ne fais pas partie des équipes core ou lang, donc rien de ce que je dis n'a d'autorité. C'est juste mon avis personnel.

c'est en fait l'inverse dans le code à ressources limitées. les modèles pull ont une pénalité parce que vous avez besoin de la ressource à l'intérieur de la chose qui est tirée plutôt que de fournir la prochaine valeur prête à travers une pile de fonctions d'attente. La conception de futures.rs est tout simplement trop chère pour tout ce qui se rapproche du matériel, comme les commutateurs réseau (mon cas d'utilisation) ou les moteurs de rendu de jeux.

Cependant, dans ce cas, tout ce dont nous avons besoin ici est de faire émettre asynchrone quelque chose comme le fait Generator. Comme je l'ai déjà dit, je pense que l'async et les générateurs sont en fait la même chose si vous l'abstraitez suffisamment au lieu de lier deux mots-clés à une seule bibliothèque.

Cependant, dans ce cas, tout ce dont nous avons besoin ici est de faire émettre asynchrone quelque chose comme le fait Generator.

async à ce stade est littéralement une enveloppe minimale autour d'un littéral générateur. J'ai du mal à voir comment les générateurs aident avec les E/S asynchrones basées sur le push, n'avez-vous pas plutôt besoin d'une transformation CPS pour ceux-ci ?

Pourriez-vous être plus précis sur ce que vous entendez par « vous avez besoin des ressources à l'intérieur de la chose qui est extraite ? » Je ne sais pas pourquoi vous en auriez besoin, ni en quoi « alimenter la prochaine valeur prête via une pile de fonctions en attente » est différent de poll() .

J'avais l'impression que les contrats à terme basés sur le push étaient plus chers (et donc plus difficiles à utiliser dans des environnements contraints). Permettre à des rappels arbitraires d'être attachés à un futur nécessite une certaine forme d'indirection, généralement une allocation de tas, donc au lieu d'allouer une fois avec le futur racine, vous allouez à chaque combinateur. Et l'annulation devient également plus complexe en raison de problèmes de sécurité des threads, donc vous ne la supportez pas ou vous avez besoin de tous les rappels pour utiliser les opérations atomiques pour éviter la course. Tout cela s'ajoute à un cadre beaucoup plus difficile à optimiser, pour autant que je sache.

n'avez-vous pas plutôt besoin d'une transformation CPS pour ceux-là ?

oui, la syntaxe actuelle du générateur ne fonctionne pas pour cela car elle n'a pas d'arguments pour la continuation, c'est pourquoi j'espérais que l'async apporterait des moyens de le faire.

vous avez besoin des ressources à l'intérieur de la chose qui est tirée ?

c'est ma terrible façon de dire que l'inversion de l'ordre asynchrone fonctionne deux fois a un coût. C'est-à-dire une fois du matériel aux futures et vice-versa en utilisant des canaux. Vous devez transporter tout un tas de choses qui n'ont aucun avantage dans le code quasi matériel.

Un exemple courant serait que vous ne pouvez pas simplement invoquer la future pile lorsque vous savez qu'un descripteur de fichier d'un socket est prêt, mais devez à la place implémenter toute la logique d'exécution pour mapper les événements du monde réel aux futurs, ce qui a un coût externe tel que le verrouillage, la taille du code et surtout la complexité du code.

Permettre à des rappels arbitraires d'être attachés à un futur nécessite une certaine forme d'indirection

oui, je comprends que les rappels coûtent cher dans certains environnements (pas dans le mien, où la vitesse d'exécution n'a pas d'importance, mais j'ai 1 Mo de mémoire totale, donc futures.rs ne tient même pas sur flash), cependant, vous n'avez pas du tout besoin de répartition dynamique lorsque vous avez quelque chose comme des continuations (que le concept de générateur actuel implémente à moitié).

Et l'annulation devient également plus complexe en raison de la sécurité des threads

Je pense que nous confondons les choses ici. Je ne préconise pas les rappels. Les continuations peuvent être des piles statiques très bien. Par exemple, ce que nous avons implémenté dans le langage Clay n'est qu'un modèle de générateur que vous pouvez utiliser pour pousser ou tirer. c'est à dire:

async fn add (a: u32) -> u32 {
    let b = await
    a + b
}

add(3).continue(2) == 5

Je suppose que je peux continuer à le faire avec une macro, mais j'ai l'impression que c'est une opportunité manquée ici de gaspiller un mot-clé de langue sur un concept spécifique.

pas dans le mien, où la vitesse d'exécution n'a pas d'importance, mais j'ai 1 Mo de mémoire totale, donc futures.rs ne tient même pas sur flash

Je suis à peu près sûr que les futurs actuels sont destinés à fonctionner dans des environnements à mémoire limitée. Qu'est-ce qui prend exactement autant de place ?

Edit : ce programme prend 295 Ko d'espace disque lors de la compilation --release sur mon macbook (basic hello world prend 273 Ko) :

use futures::{executor::LocalPool, future};

fn main() {
    let mut pool = LocalPool::new();
    let hello = pool.run_until(future::ready("Hello, world!"));
    println!("{}", hello);
}

pas dans le mien, où la vitesse d'exécution n'a pas d'importance, mais j'ai 1 Mo de mémoire totale, donc futures.rs ne tient même pas sur flash

Je suis à peu près sûr que les futurs actuels sont destinés à fonctionner dans des environnements à mémoire limitée. Qu'est-ce qui prend exactement autant de place ?

Et qu'entendez-vous par mémoire ? J'ai exécuté un code basé sur l'async/l'attente actuel sur des appareils avec 128 Ko de flash/16 Ko de RAM. Il y a certainement des problèmes d'utilisation de la mémoire avec async/await actuellement, mais ce sont principalement des problèmes d'implémentation et peuvent être améliorés en ajoutant quelques optimisations supplémentaires (par exemple https://github.com/rust-lang/rust/issues/52924).

Un exemple courant serait que vous ne pouvez pas simplement invoquer la future pile lorsque vous savez qu'un descripteur de fichier d'un socket est prêt, mais devez à la place implémenter toute la logique d'exécution pour mapper les événements du monde réel aux futurs, ce qui a un coût externe tel que le verrouillage, la taille du code et surtout la complexité du code.

Pourquoi? Cela ne ressemble toujours à rien de ce à quoi l'avenir vous oblige. Vous pouvez aussi facilement appeler poll que vous le feriez avec un mécanisme push.

Et qu'entendez-vous par mémoire ?

Je ne pense pas que ce soit pertinent. Toute cette discussion a détaillé l'invalidation d'un point que je n'avais même pas l'intention de faire. Je ne suis pas ici pour critiquer l'avenir au-delà de dire que boulonner sa conception dans le langage de base est une erreur.

Ce que je veux dire, c'est que le mot-clé async peut être rendu à l'épreuve du temps s'il est fait correctement. Des continuations, c'est ce que je veux, mais peut-être que d'autres personnes auront des idées encore meilleures.

Vous pouvez aussi facilement appeler poll qu'un mécanisme push.

Oui, cela aurait du sens si Future:poll avait des arguments d'appel. Il ne peut pas les avoir parce que le sondage doit être abstrait. Au lieu de cela, je propose d'émettre une continuation à partir du mot-clé async et d'implémenter Future pour toute continuation avec zéro argument.

C'est un changement simple et sans effort qui n'ajoute aucun coût aux futurs mais permet la réutilisation des mots-clés qui sont actuellement exclusivement pour une bibliothèque.

Mais les continations peuvent bien sûr aussi être implémentées avec un préprocesseur, c'est ce que nous allons faire. Malheureusement le desucre ne peut être qu'une fermeture, ce qui coûte plus cher qu'une véritable continuation.

@aep Comment permettrait-on de réutiliser les mots-clés ( async et await ) ?

@Centril, ma solution rapide naïve serait de réduire un asynchrone à un générateur et non à un avenir. Cela laissera le temps de rendre le générateur utile pour des continuations appropriées plutôt que d'être un backend exclusif pour les futures.

C'est comme un PR de 10 lignes peut-être. Mais je n'ai pas la patience de combattre une ruche d'abeilles dessus, donc je vais juste construire une pré-proc pour désucrer un mot-clé différent.

Je n'ai pas suivi les trucs async, donc désolé si cela a été discuté avant / ailleurs, mais quel est le plan (de mise en œuvre) pour prendre en charge async / wait dans no_std ?

AFAICT l'implémentation actuelle utilise TLS pour passer un Waker mais il n'y a pas de support TLS (ou thread) dans no_std / core . @alexcrichton m'a dit qu'il serait possible de se débarrasser du TLS si / quand Generator.resume prend en charge les arguments.

Le plan de blocage de la stabilisation d'async / wait sur le support no_std est-il mis en œuvre ? Ou sommes-nous sûrs que le support no_std peut être ajouté sans changer aucun des éléments qui seront stabilisés pour être livrés std async / wait sur stable ?

@japaric poll prend maintenant le contexte explicitement. AFAIK, TLS ne devrait plus être requis.

https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll

Edit : non pertinent pour l'async/wait, uniquement pour les futures.

[...] sommes-nous sûrs que le support no_std peut être ajouté sans changer aucun des éléments qui seront stabilisés pour expédier std async / wait sur stable ?

Je le crois. Les éléments pertinents sont les fonctions de std::future , elles sont toutes cachées derrière une fonction instable gen_future supplémentaire qui ne sera jamais stabilisée. La transformation async utilise set_task_waker pour stocker le waker dans TLS, puis await! utilise poll_with_tls_waker pour y accéder. Si les générateurs prennent en charge l'argument de reprise, alors la transformation async peut passer le réveil en tant qu'argument de reprise et await! peut le lire à partir de l'argument.

EDIT : même sans arguments de générateur, je pense que cela pourrait également être fait avec un code légèrement plus compliqué dans la transformation asynchrone. Personnellement, j'aimerais voir des arguments de générateur ajoutés pour d'autres cas d'utilisation, mais je suis à peu près certain que la suppression de l'exigence TLS avec/sans eux sera possible.

@japaric Même bateau. Même si quelqu'un faisait fonctionner les futures sur l'embarqué, c'est très risqué car tout est Tier3.

J'ai découvert un vilain hack qui nécessite beaucoup moins de travail que de réparer async : tisser dans un arcà travers une pile de générateurs.

  1. voir l'argument "Poll" https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 c'est un arc
  2. enregistrer quelque chose dans le sondage à la ligne 87
  3. rendement pour générer un point de continuation à la ligne 92
  4. appeler un générateur à partir d'un générateur pour créer une pile de niveau supérieur à la ligne 207
  5. enfin exécuter toute la pile en passant un runtime à la ligne 215

Idéalement, ils réduiraient simplement l'async à une pile de fermeture "pure" plutôt qu'à un futur afin que vous n'ayez besoin d'aucune hypothèse d'exécution et que vous puissiez ensuite insérer l'environnement impur comme argument à la racine.

J'étais à mi-chemin de la mise en œuvre

https://twitter.com/arvidep/status/1067383652206690307

mais un peu inutile d'aller jusqu'au bout si je suis le seul à le vouloir.

Et je ne pouvais pas m'empêcher de me demander si l'async/attente sans TLS sans arguments de générateur est possible, j'ai donc implémenté une paire de macros no_std basée sur le proc-macro async_block! / await! en utilisant uniquement des variables locales.

Cela nécessite certainement des garanties de sécurité beaucoup plus subtiles que la solution actuelle basée sur TLS ou une solution basée sur des arguments de générateur (du moins lorsque vous supposez simplement que les arguments de générateur sous-jacents sont sains), mais je suis à peu près sûr que c'est sain (tant que personne utilise le trou d'hygiène assez grand que je n'ai pas trouvé de moyen de contourner, ce ne serait pas un problème pour une implémentation dans le compilateur car il peut utiliser des identifiants gensym innommables pour communiquer entre la transformation async et la macro d'attente).

Je viens de réaliser qu'il n'est pas question de déplacer await! de std à core dans l'OP, peut-être que #56767 pourrait être ajouté à la liste des problèmes à résoudre avant la stabilisation à suivre cette.

@Nemo157 Comme await! ne devrait pas être stabilisé, ce n'est de toute façon pas un bloqueur.

@Centril Je ne sais pas qui vous a dit que await! ne devrait pas se stabiliser... :wink:

@cramertj Il voulait dire la version de la macro et non la version du mot-clé, je crois...

@crlf0710 qu'en est-il de la version implicite/explicite async-block ?

@crlf0710 Je l'ai fait aussi :)

@cramertj Ne voulons-nous pas supprimer la macro car il existe actuellement un vilain hack dans le compilateur qui rend possible l'existence de await et de await! ? Si nous stabilisons la macro, nous ne pourrons jamais la supprimer.

@stjepang Je ne me await! , à part une préférence générale pour les notations postfixes et une aversion pour l'ambiguïté et les symboles imprononçables/non compatibles avec Google. Pour autant que je sache, les suggestions actuelles (avec ? pour clarifier la priorité) sont :

  • await!(x)? (ce que nous avons aujourd'hui)
  • await x? ( await se lie plus étroitement que ? , toujours la notation de préfixe, a besoin de parenthèses pour enchaîner les méthodes)
  • await {x}? (comme ci-dessus, mais nécessite temporairement {} afin de lever l'ambiguïté)
  • await? x ( await se lie moins étroitement, toujours la notation de préfixe, a besoin de parenthèses pour enchaîner les méthodes)
  • x.await? (ressemble à un accès sur le terrain)
  • x# / x~ /etc. (quelque symbole)
  • x.await!()? (style postfix-macro, @withoutboats et je pense que d'autres ne sont peut-être pas des fans de postfix-macros parce qu'ils s'attendent à ce que . autorise la répartition basée sur le type, ce qui ne serait pas le cas pour les macros postfix )

Je pense que la meilleure route vers l'expédition est d'atterrir await!(x) , d'annuler le mot-clé await , et éventuellement de vendre un jour aux gens la gentillesse des macros postfix, nous permettant d'ajouter x.await!() . D'autres ont des avis différents ;)

Je suis ce problème très vaguement, mais voici mon avis :

Personnellement, j'aime la macro await! telle qu'elle est et telle qu'elle est décrite ici : https://blag.nemo157.com/2018/12/09inside-rusts-async-transform.html

Ce n'est pas une sorte de magie ou de nouvelle syntaxe, juste une macro ordinaire. Moins c'est plus, après tout.

Là encore, j'ai aussi préféré try! , car Try n'est toujours pas stabilisé. Cependant, await!(x)? est un compromis décent entre le sucre et les actions nommées évidentes, et je pense que cela fonctionne bien. En outre, elle pourrait potentiellement être remplacée par une autre macro dans une bibliothèque tierce pour gérer des fonctionnalités supplémentaires, telles que le traçage de débogage.

Pendant ce temps, async / yield est "juste" du sucre syntaxique pour les générateurs. Cela me rappelle l'époque où JavaScript obtenait une prise en charge asynchrone/attente et vous aviez des projets comme Babel et Regenerator qui transpilaient du code asynchrone pour utiliser des générateurs et des promesses/futurs pour les opérations asynchrones, essentiellement comme nous le faisons.

Gardez à l'esprit qu'à terme, nous voudrons que l'async et les générateurs soient des fonctionnalités distinctes, potentiellement même composables les unes avec les autres (produisant un Stream ). Laisser await! tant que macro qui descend juste à yield n'est pas une solution permanente.

Laissant attendre! car une macro qui s'abaisse pour céder n'est pas une solution permanente.

Il ne peut pas être visible en permanence par l'utilisateur qu'il descend à yield , mais il peut certainement continuer à être implémenté de cette façon. Même lorsque vous avez async + generators = Stream vous pouvez toujours utiliser par exemple yield Poll::Pending; vs yield Poll::Ready(next_value) .

Gardez à l'esprit que nous voudrons éventuellement que l'async et les générateurs soient des fonctionnalités distinctes

L'async et les générateurs ne sont-ils Future s par opposition à toute valeur normale. Un exécuteur devrait évaluer et attendre l'exécution de la fonction asynchrone. Plus quelques trucs supplémentaires à vie, je ne suis pas sûr.

En fait, j'ai écrit une fois

@cramertj Cela ne peut pas être implémenté de cette façon si les deux sont des "effets" distincts. Il y a une discussion à ce sujet ici : https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. Nous ne voulons pas yield Poll::Ready(next_value) , nous voulons yield next_value , et avoir await s ailleurs dans la même fonction.

@rpjohnst

Nous ne voulons pas générer Poll::Ready(next_value), nous voulons générer next_value et avons waits ailleurs dans la même fonction.

Oui, bien sûr, c'est ce à quoi cela ressemblerait à l'utilisateur, mais en termes de désucrage, vous n'avez qu'à envelopper yield s dans Poll::Ready et ajouter un Poll::Pending à les yield générés à partir de await! . Syntaxiquement, pour les utilisateurs finaux, ils apparaissent comme des fonctionnalités distinctes, mais ils peuvent toujours partager une implémentation dans le compilateur.

@cramertj Aussi celui-ci :

  • await? x

@novacrazy Oui, ce sont des fonctionnalités distinctes, mais elles devraient être composables ensemble.

Et effectivement en JavaScript ils sont composables :

https://thenewstack.io/whats-coming-up-in-javascript-2018-async-generators-better-regex/

"Les générateurs et itérateurs asynchrones sont ce que vous obtenez lorsque vous combinez une fonction asynchrone et un itérateur, c'est donc comme un générateur asynchrone dans lequel vous pouvez attendre ou une fonction asynchrone à partir de laquelle vous pouvez céder", a-t-il expliqué. Auparavant, ECMAScript vous permettait d'écrire une fonction dans laquelle vous pouviez céder ou attendre, mais pas les deux. « C'est vraiment pratique pour consommer des flux qui font de plus en plus partie de la plate-forme Web, en particulier avec l'objet Fetch exposant les flux. »

L'itérateur asynchrone est similaire au modèle Observable, mais plus flexible. « Un Observable est un modèle push ; une fois que vous y êtes abonné, vous recevez des événements et des notifications à toute vitesse, que vous soyez prêt ou non, vous devez donc mettre en œuvre des stratégies de mise en mémoire tampon ou d'échantillonnage pour gérer le bavardage », a expliqué Terlson. L'itérateur asynchrone est un modèle push-pull - vous demandez une valeur et elle vous est envoyée - qui fonctionne mieux pour des choses comme les primitives d'E/S réseau.

@Centril ok, ouvert #56974, est-ce assez correct pour être ajouté en tant que question non résolue au PO ?


Je ne veux vraiment pas entrer à nouveau dans le cycle de syntaxe await , mais je dois répondre à au moins un point :

Personnellement, j'aime la macro await! telle qu'elle est et telle qu'elle est décrite ici : https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Notez que j'ai également dit que je ne crois pas que la macro puisse rester une macro implémentée par la bibliothèque (en ignorant si elle continuera ou non à apparaître comme une macro pour les utilisateurs), pour en expliquer les raisons :

  1. Cacher l'implémentation sous-jacente, comme l'indique l'un des problèmes non résolus, vous pouvez actuellement créer un générateur en utilisant || await!() .
  2. La prise en charge des générateurs asynchrones, comme le mentionne @cramertj, nécessite de faire la différence entre les yield ajoutés par await et les autres yield écrits par l'utilisateur. Cela _pourrait_ être fait en tant qu'étape de pré-extension de macro, _si_ les utilisateurs n'ont jamais voulu yield intérieur des macros, mais il existe des constructions yield -in-macro très utiles comme yield_from! . Avec la contrainte que les yield s dans les macros doivent être pris en charge, cela nécessite que await! soit au moins une macro intégrée (sinon la syntaxe réelle).
  3. Prise en charge de async fn sur no_std . Je connais deux façons d'implémenter cela, les deux nécessitent le async fn -created- Future et await pour partager un identifiant dans lequel le waker est stocké. La seule façon dont je peut voir pour avoir un identifiant hygiéniquement sûr partagé entre ces deux endroits est si les deux sont implémentés dans le compilateur.

Je pense qu'il ya un peu de confusion ici-- il n'a jamais été l'intention que await! publiquement visiblement extensible à une enveloppe autour des appels à yield . Tout avenir pour la syntaxe de type macro await! reposera sur une implémentation similaire à celle de la compile_error! , assert! , format_args! etc. et serait capable de desucrer à un code différent selon le contexte.

Le seul élément important à comprendre ici est qu'il n'y a pas de différence sémantique significative entre les syntaxes proposées - ce ne sont que des syntaxes de surface.

J'écrirais une alternative pour résoudre la syntaxe await .

Tout d'abord, j'aime l'idée de mettre le await comme opérateur suffixe. Mais expression.await ressemble trop à un champ, comme déjà souligné.

Ma proposition est donc expression awaited . L'inconvénient ici est que awaited n'est pas encore conservé en tant que mot-clé, mais c'est plus naturel en anglais et pourtant il n'y a pas de telles expressions (je veux dire, des formes de grammaire comme expression [token] ) est valide dans Rust en ce moment, donc cela peut être justifié.

Ensuite, nous pouvons écrire expression? awaited pour attendre un Result<Future,_> , et expression awaited? pour attendre un Future<Item=Result<_,_>> .

@engineterre

Bien que je ne sois pas convaincu par le mot-clé awaited , je pense que vous êtes sur quelque chose.

L'idée clé ici est la suivante : yield et await sont comme return et ? .

return x renvoie la valeur x , tandis que x? déballe le résultat x , en retournant plus tôt si c'est Err .
yield x renvoie la valeur x , tandis que x awaited attend le futur x , revenant plus tôt s'il est Pending .

Il y a une belle symétrie. Peut-être que await devrait vraiment être un opérateur suffixe.

let x = x.do_something() await.do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Je ne suis pas fan d'une syntaxe postfixe attendue pour la raison exacte que @cramertj vient de montrer. Cela réduit la lisibilité globale, en particulier pour les expressions longues ou les expressions enchaînées. Cela ne donne aucun sens d'imbrication comme le ferait await! / await . Il n'a pas la simplicité de ? , et nous manquons de symboles à utiliser pour un opérateur suffixe...

Personnellement, je suis toujours en faveur de await! pour les raisons que j'ai décrites précédemment. Il se sent rouillé et sans fioritures.

Cela réduit la lisibilité globale, en particulier pour les expressions longues ou les expressions enchaînées.

Dans les normes Rustfmt, l'exemple doit être écrit

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;

Je peux à peine voir comment cela affecte la lisibilité.

J'aime aussi postfix wait. Je pense qu'utiliser un espace serait inhabituel et aurait tendance à briser le regroupement mental. Cependant, je pense que .await!() serait paire bien, avec ? montage avant ou après, et ! permettrait des interactions de flux de contrôle.

(Cela ne nécessite pas de mécanisme de macro postfix entièrement général ; le compilateur peut utiliser un cas spécial .await!() .)

Au début, j'ai vraiment détesté le suffixe await (sans . ou () ) car il a l'air assez étrange - les gens venant d'autres langues auront un bon petit rire à notre frais à coup sûr. C'est un coût que nous devrions prendre au sérieux. Cependant, x await n'est clairement pas un appel de fonction ou un accès au champ ( x.await / x.await() / await(x) ont tous ce problème) et il y a moins de funky problèmes de préséance. Cette syntaxe résoudrait clairement ? et la priorité d'accès à la méthode, par exemple foo await? et foo? await ont tous deux un ordre de priorité clair pour moi, tout comme foo await?.x et foo await?.y (ne niant pas qu'ils semblent étranges, arguant seulement que la priorité est claire).

je pense aussi que

stream.for_each(async |item| {
    ...
}) await;

se lit plus bien que

await!(stream.for_each(async |item| {
    ...
});

Dans l'ensemble, je serais en faveur de cela.

@joshtriplett RE .await!() nous devrions parler séparément - j'étais initialement en faveur de cela également, mais je ne pense pas que nous devrions atterrir si nous ne pouvons pas également obtenir des macros postfix en général, et je pense il y a beaucoup d'opposition permanente à leur égard (avec une assez bonne raison, bien que malheureuse), et j'aimerais vraiment que cela ne bloque pas la stabilisation de await .

Pourquoi pas les deux?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Je vois plus l'attrait de postfix maintenant, et je suis à la limite de l'aimer davantage dans certains scénarios. Surtout avec la triche ci-dessus, qui est si simple qu'elle n'a même pas besoin d'être fournie par Rust lui-même.

Donc, +1 pour suffixe.

Je pense que nous devrions également avoir une fonction de préfixe, en plus de la version postfix.

En ce qui concerne les spécificités de la syntaxe de suffixe, je n'essaie pas de dire que .await!() est la seule syntaxe de suffixe viable ; Je ne suis tout simplement pas un fan de postfix await avec un espace de début.

Cela semble passable (bien que toujours inhabituel) lorsque vous le formatez avec une instruction par ligne, mais beaucoup moins raisonnable lorsque vous formatez des instructions simples sur une seule ligne.

Pour ceux qui n'aiment pas les opérateurs de mots-clés postfix, nous pouvons définir un opérateur symbolique approprié pour await .

À l'heure actuelle, nous étions en quelque sorte à court d'opérateurs en caractères ASCII simples pour l'opérateur suffixe. Cependant que diriez-vous

let x = do_something()⌛.do_somthing_else()⌛;

Si nous avons vraiment besoin d'ASCII simple, j'ai trouvé (inspiré de la forme ci-dessus)

let x = do_something()><.do_somthing_else()><;

ou (forme simulée en position horizontale)

let x = do_something()>=<.do_somthing_else()>=<;

Une autre idée est de faire de la structure await un crochet.

let x = >do_something()<.>do_something_else()<;

Toutes ces solutions ASCII partagent le même problème de passage que <..> est déjà trop utilisé et nous avons des problèmes d'analyse avec < et > . Cependant, >< ou >=< pourraient être mieux pour cela car ils ne nécessitent aucun espace à l'intérieur de l'opérateur et aucun < ouvert dans la position actuelle.


Pour ceux qui n'aiment pas l'espace entre les deux mais OK pour les opérateurs de mots-clés postfix, que diriez-vous d'utiliser des tirets :

let x = do_something()-await.do_something_else()-await;

À propos d'avoir de nombreuses façons différentes d'écrire le même code, je ne l'aime pas personnellement. La principale raison pour laquelle il est beaucoup plus difficile pour les nouveaux arrivants d'avoir une bonne compréhension de la bonne manière ou de l'intérêt de l'avoir. La deuxième raison est que nous aurons de nombreux projets différents qui utiliseront une syntaxe différente et qu'il serait plus difficile de sauter de l'un à l'autre et de le lire (spécialement pour les nouveaux venus dans la rouille). Je pense qu'une syntaxe différente ne devrait être implémentée que s'il y a réellement une différence et cela donne certains avantages. Beaucoup de sucre dans le code ne font que rendre beaucoup plus difficile l'apprentissage et le travail avec la langue.

@goffrie Oui, je suis d'accord pour dire que nous ne devrions pas avoir beaucoup de façons différentes de faire la même chose. Cependant, je proposais simplement différentes alternatives, la communauté n'a qu'à en choisir une. Ce n'est donc pas vraiment un problème.

De plus, en termes de macro await! il n'y a aucun moyen d'empêcher l'utilisateur d'inventer ses propres macros pour le faire différemment, et Rust est destiné à permettre cela. Par conséquent, « avoir plusieurs façons différentes de faire la même chose » est inévitable.

Je pense que cette simple macro stupide que j'ai montrée démontre que quoi que nous fassions, les utilisateurs feront ce qu'ils veulent de toute façon. Un mot-clé, qu'il s'agisse d'un préfixe ou d'un suffixe, peut être transformé en une macro de préfixe de type fonction, ou vraisemblablement en une macro de type méthode de suffixe, chaque fois qu'elles existent. Même si nous choisissons des macros de type fonction ou méthode pour await , elles pourraient être inversées avec une autre macro. Cela n'a vraiment pas d'importance.

Par conséquent, nous devons nous concentrer sur la flexibilité et le formatage. Fournir une solution qui remplirait le plus facilement toutes ces possibilités.

De plus, bien que dans ce court laps de temps je me sois attaché à la syntaxe des mots clés postfix, await devrait refléter tout ce qui est décidé pour yield avec des générateurs, qui est probablement un mot clé préfixe. Pour les utilisateurs qui souhaitent une solution postfix, des macros de type méthode existeront probablement à terme.

Ma conclusion est qu'un mot-clé préfixe await est la meilleure syntaxe par défaut pour le moment, peut-être avec une caisse ordinaire fournissant aux utilisateurs une macro de type fonction await! , et à l'avenir une méthode de type postfix .await!() macro.

@novacrazy

De plus, bien que dans ce court laps de temps je me sois attaché à la syntaxe des mots-clés postfix, await devrait refléter tout ce qui est décidé pour yield avec des générateurs, qui est probablement un mot-clé préfixe.

L'expression yield 42 est de type ! , alors que foo.await est de type Tfoo: impl Future<Output = T> . @stjepang fait la bonne analogie avec ? et return ici. await n'est pas comme yield .

Pourquoi pas les deux?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Vous devrez nommer la macro autrement car await doit rester un vrai mot-clé.


Pour diverses raisons, je m'oppose au préfixe await et encore plus à la forme de bloc await { ... } .

Il y a d'abord les problèmes de priorité avec await expr? où la priorité cohérente est await (expr?) mais vous voulez (await expr)? . Comme solution aux problèmes de priorité, certains ont suggéré await? expr en plus de await expr . Cela implique await? comme unité et boîtier spécial ; cela semble injustifié, un gaspillage de notre budget de complexité et une indication que await expr a de sérieux problèmes.

Plus important encore, le code Rust, et en particulier la bibliothèque standard, est fortement centré sur la puissance de la syntaxe d'appel de point et de méthode. Lorsque await est un préfixe, cela encourage l'utilisateur à inventer des liaisons temporaires au lieu de simplement enchaîner des méthodes. C'est la raison pour laquelle ? est suffixe, et pour la même raison, await devrait également être suffixe.

Pire encore, ce serait await { ... } . Cette syntaxe, si elle était formatée de manière cohérente selon rustfmt , se transformerait en :

    let x = await { // by analogy with `loop`
        foo.bar.baz.other_thing()
    };

Cela ne serait pas ergonomique et gonflerait considérablement la longueur verticale des fonctions.


Au lieu de cela, je pense que l'attente, comme ? , devrait être suffixe car cela correspond à l'écosystème Rust qui est centré sur le chaînage de méthodes. Un certain nombre de syntaxes suffixes ont été mentionnées ; Je vais passer en revue certains d'entre eux :

  1. foo.await!() -- Ceci est la solution de macro postfix . Bien que je sois fortement en faveur des macros postfix, je suis d'accord avec @cramertj dans https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040 que nous ne devrions pas le faire à moins que nous nous engagions également à postfix macro en général. Je pense aussi que l'utilisation d'une macro postfix de cette manière donne un sentiment plutôt pas de première classe ; nous devrions imo éviter de faire en sorte qu'une construction de langage utilise la syntaxe de macro.

  2. foo await -- Ce n'est pas si mal, cela fonctionne vraiment comme un opérateur postfix ( expr op ) mais j'ai l'impression qu'il manque quelque chose avec ce formatage (c'est-à-dire qu'il semble "vide"); En revanche, expr? attache ? directement sur expr ; il n'y a pas de place ici. Cela rend ? visuellement attrayant.

  3. foo.await -- Cela a été critiqué pour ressembler à un accès au champ ; et c'est vrai. Nous devons cependant nous rappeler que await est un mot-clé et qu'il sera donc mis en évidence par la syntaxe en tant que tel. Si vous lisez le code Rust dans votre IDE ou de manière équivalente sur GitHub, await sera d'une couleur ou d'une audace différente de celle de foo . En utilisant un mot-clé différent, nous pouvons démontrer ceci :

    let x = foo.match?;
    

    Habituellement, les champs sont aussi des noms alors que await est un verbe.

    Bien qu'il y ait un facteur de ridicule initial à propos de foo.await , je pense qu'il devrait être considéré sérieusement comme une syntaxe visuellement attrayante tout en étant lisible.

    En prime, l'utilisation de .await vous donne la puissance du point et l' auto-complétion que le point a généralement dans les IDE (voir page 56). Par exemple, vous pouvez écrire foo. et si foo s'avère être un futur, await sera affiché comme premier choix. Cela facilite à la fois l'ergonomie et la productivité des développeurs, car atteindre le point est une chose que de nombreux développeurs ont formée à la mémoire musculaire.

    Dans toutes les syntaxes postfix possibles, malgré les critiques concernant l'apparence d'accès au champ, cela reste ma syntaxe préférée.

  4. foo# -- Ceci utilise le sigil # pour attendre le foo . Je pense que considérer un sceau est une bonne idée étant donné que ? est aussi un sceau et parce que cela rend l'attente légère. Combiné avec ? cela ressemblerait à foo#? -- ça a l'air OK. Cependant, # n'a pas de justification spécifique. Au contraire, c'est simplement un sceau qui est toujours disponible.

  5. foo@ -- Un autre sceau est @ . Lorsqu'il est combiné avec ? , nous obtenons foo@? . Une justification de ce sceau spécifique est qu'il ressemble à a -ish ( @wait ).

  6. foo! -- Enfin, il y a ! . Lorsqu'il est combiné avec ? , nous obtenons foo!? . Malheureusement, cela a un certain sentiment WTF. Cependant, ! semble forcer la valeur, ce qui correspond à « wait ». Il y a un inconvénient en ce que foo!() est déjà une invocation de macro légale, donc attendre et appeler une fonction devrait être écrit (foo)!() . Utiliser foo! comme syntaxe nous priverait également de la possibilité d'avoir des macros de mots-clés (par exemple foo! expr ).

Un autre sceau unique est foo~ . L'onde peut être comprise comme « l'écho » ou « prend du temps ». Pourtant, il n'est utilisé nulle part dans le langage Rust.

Tilde ~ était utilisé autrefois pour le type alloué de tas : https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

Les ? peuvent-ils être réutilisés ? Ou est-ce trop magique ? À quoi ressemblerait impl Try for T: Future ?

@parasyte Oui je me souviens. Mais c'était toujours parti depuis longtemps.

@jethrogb il n'y a aucun moyen que je puisse voir impl Try fonctionner directement, ? explicitement return s le résultat de Try de la fonction actuelle tandis que await besoin de yield .

Peut-être que ? pourrait être un cas spécial pour faire autre chose dans le contexte d'un générateur afin qu'il puisse soit yield soit return selon le type de l'expression à laquelle il est appliqué , mais je ne sais pas à quel point ce serait compréhensible. De plus, comment cela interagirait-il avec Future<Output=Result<...>> , devriez-vous let foo = bar()??; pour faire les deux "attendre" et ensuite obtenir la variante Ok du Result ( ou ? dans les générateurs serait-il basé sur un trait à trois états qui peut yield , return ou se résoudre en une valeur avec une seule application) ?

Cette dernière remarque entre parenthèses me fait penser que cela pourrait être réalisable, cliquez pour voir un croquis rapide
enum GenOp<T, U, E> { Break(T), Yield(U), Error(E) }

trait TryGen {
    type Ok;
    type Yield;
    type Error;

    fn into_result(self) -> GenOp<Self::Ok, Self::Yield, Self::Error>;
}
avec « foo ? » dans un générateur étendu à quelque chose comme (bien que cela ait un problème de propriété et doive également empiler le résultat de « foo »)
loop {
    match TryGen::into_result(foo) {
        GenOp::Break(val) => break val,
        GenOp::Yield(val) => yield val,
        GenOp::Return(val) => return Try::from_error(val.into()),
    }
}

Malheureusement, je ne vois pas comment gérer la variable de contexte waker dans un schéma comme celui-ci, peut-être si ? étaient spéciaux pour async au lieu de générateurs, mais si ça va être spécial -cased ici, ce serait bien s'il était utilisable pour d'autres cas d'utilisation de générateurs.

J'ai eu la même réflexion concernant la réutilisation de ? que @jethrogb.

@Nemo157

il n'y a aucun moyen que je puisse voir impl Try fonctionner directement, ? explicitement return s le résultat de Try de la fonction actuelle tandis que wait a besoin de yield .

Peut-être qu'il me manque des détails sur ? et le trait Try , mais où/pourquoi est-ce explicite ? Et un return dans une fermeture asynchrone n'est-il pas essentiellement le même que yield toute façon, juste une transition d'état différente ?

Peut-être que ? pourrait être un cas spécial pour faire autre chose dans le contexte d'un générateur afin qu'il puisse soit yield ou return selon le type de l'expression à laquelle il est appliqué , mais je ne sais pas à quel point ce serait compréhensible.

Je ne vois pas pourquoi cela devrait prêter à confusion. Si vous pensez à ? comme "continuer ou diverger", alors cela semble naturel, à mon humble avis. Certes, changer le trait Try pour utiliser des noms différents pour les types de retour associés serait utile.

De plus, comment cela interagirait-il avec Future<Output=Result<...>> , devriez-vous let foo = bar()?? ;

Si vous souhaitez attendre le résultat puis quitter plus tôt un résultat d'erreur, alors ce serait l'expression logique, oui. Je ne pense pas qu'un TryGen spécial à trois états serait nécessaire.

Malheureusement, je ne vois pas comment gérer la variable de contexte waker dans un schéma comme celui-ci, peut-être si ? étaient dans un cas spécial pour l'async au lieu des générateurs, mais s'il doit être dans un cas spécial ici, ce serait bien s'il était utilisable pour d'autres cas d'utilisation de générateurs.

Je ne comprends pas cette partie. Pourriez-vous détailler ?

@jethrogb @rolandsteiner Une structure pourrait implémenter à la fois Try et Future . Dans ce cas, lequel doit ? déballer ?

@jethrogb @rolandsteiner Une structure pourrait implémenter à la fois Try et Future. Dans ce cas, lequel ? déballer?

Non, cela ne pouvait pas à cause de la couverture impl Try for T: Future.

Pourquoi personne ne parle de la construction explicite et de la proposition

mais ce n'est que de l'ombrage de vélo, je pense que nous devrions nous contenter de la syntaxe macro simple await!(my_future) au moins pour le moment

mais ce n'est que de l'ombrage de vélo, je pense que nous devrions nous contenter de la syntaxe macro simple await!(my_future) au moins pour le moment

Non, ce n'est pas "juste" du vélo comme si c'était quelque chose de banal et d'insignifiant. Que await soit écrit en préfixe ou en suffixe a un impact fondamental sur la façon dont le code asynchrone est écrit en ce qui concerne. l'enchaînement des méthodes et comment il se sent composable. La stabilisation sur await!(future) implique également que await tant que mot-clé soit abandonné, ce qui rend impossible l'utilisation future de await tant que mot-clé. "Au moins pour l'instant" suggère que nous pouvons trouver une meilleure syntaxe plus tard et fait abstraction de la dette technique que cela implique. Je suis opposé à l'introduction sciemment de dette pour une syntaxe qui est censée être remplacée plus tard.

Stabiliser sur wait! (future) implique également que wait en tant que mot-clé soit abandonné, ce qui rend impossible l'utilisation future de wait en tant que mot-clé.

nous pourrions en faire un mot-clé à l'époque suivante, nécessitant la syntaxe d'identification brute pour la macro, tout comme nous l'avons fait avec try .

@rolandsteiner

Et un return dans une fermeture asynchrone n'est-il pas essentiellement le même que yield toute façon, juste une transition d'état différente ?

yield n'existe pas dans une fermeture asynchrone, c'est une opération introduite lors de l'abaissement de la syntaxe async / await vers les générateurs/ yield . Dans la syntaxe actuelle du générateur, yield est assez différent de return , si l'expansion ? est effectuée avant la transformation du générateur, je ne sais pas comment il saurait quand insérer un return ou un yield .

Si vous voulez attendre le résultat et que nalso se termine tôt sur un résultat d'erreur, alors ce serait l'expression logique, oui.

C'est peut-être logique, mais cela me semble être un inconvénient que de nombreux (la plupart?) Des cas où vous écrivez des fonctions asynchrones seront remplis de doubles ?? pour traiter les erreurs d'E/S.

Malheureusement, je ne vois pas comment gérer la variable de contexte waker...

Je ne comprends pas cette partie. Pourriez-vous détailler ?

La transformation asynchrone prend une variable waker dans la fonction Future::poll générée, celle-ci doit ensuite être transmise à l'opération d'attente transformée. Actuellement, cela est géré avec une variable TLS fournie par std que les deux transformations font référence, si ? était plutôt géré comme un point de retour _au niveau des générateurs_ alors la transformation asynchrone perd sur un chemin pour insérer cette référence de variable.

J'ai écrit un article de blog sur la syntaxe await décrivant mes préférences il y a deux mois. Cependant, il supposait essentiellement une syntaxe de préfixe et ne considérait que la question de la priorité de ce point de vue. Voici maintenant quelques réflexions supplémentaires :

  • Mon opinion générale est que Rust a déjà étiré son budget de méconnaissance. Il serait idéal que la syntaxe async/wait au niveau de la surface soit aussi familière que possible à quelqu'un venant de JavaScript, Python ou C#. Il serait idéal de ce point de vue de ne s'écarter que de façon mineure de la norme. Les syntaxes des suffixes varient selon leur degré de divergence (par exemple, foo await est moins une divergence que certains sceaux comme foo@ ), mais elles sont toutes plus divergentes que le préfixe wait.
  • Je préfère aussi stabiliser une syntaxe qui n'utilise pas ! . Chaque utilisateur traitant d'async/wait se demandera pourquoi wait est une macro au lieu d'une construction de flux de contrôle normale, et je pense que l'histoire ici sera essentiellement "bien, nous n'avons pas pu trouver une bonne syntaxe, nous nous sommes donc contentés de en le faisant ressembler à une macro." Ce n'est pas une réponse convaincante. Je ne pense pas que l'association entre ! et le flux de contrôle soit vraiment suffisante pour justifier cette syntaxe : je pense que ! a une signification assez précise pour l'extension de macro, ce qui n'est pas le cas.
  • Je suis en quelque sorte dubitatif quant aux avantages de postfix wait en général (pas entièrement, juste en quelque sorte ). Je pense que le solde est un peu différent de ? , car l'attente est une opération plus coûteuse (vous cédez en boucle jusqu'à ce qu'il soit prêt, plutôt que de simplement vous brancher et revenir une fois). Je me méfie un peu du code qui attendrait deux ou trois fois dans une seule expression ; il me semble bien de dire que ceux-ci devraient être retirés dans leurs propres fixations let. Donc, le compromis try! vs ? n'est pas aussi fort pour moi ici. Mais aussi, je serais ouvert aux exemples de code qui, selon les gens, ne devraient vraiment pas être extraits dans des let et qui sont plus clairs en tant que chaînes de méthodes.

Cela dit, foo await est la syntaxe de suffixe la plus viable que j'ai vue jusqu'à présent :

  • C'est relativement familier pour la syntaxe postfix. Tout ce que vous avez à apprendre, c'est que wait va après l'expression plutôt qu'avant dans Rust, plutôt qu'une syntaxe significativement différente.
  • Cela résout clairement le problème de préséance sur lequel tout cela portait.
  • Le fait que cela ne fonctionne pas bien avec le chaînage de méthodes me semble presque un avantage, plutôt qu'un inconvénient, pour les raisons que j'ai évoquées précédemment. Je serais peut-être plus obligé si nous avions des règles de grammaire qui empêchaient foo await.method() juste parce que je sens vraiment que la méthode est (absurde) appliquée à await , pas foo (alors qu'il est intéressant Je ne ressens pas ça avec foo await? ).

Je penche toujours pour une syntaxe de préfixe, mais je pense que await est la première syntaxe de suffixe qui a l'impression d'avoir un réel impact sur moi.

Note : il est toujours possible d'utiliser des parenthèses pour rendre la priorité plus claire :

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Ce n'est pas exactement l'idéal, mais étant donné qu'il essaie de rassembler beaucoup de choses sur une seule ligne, je pense que c'est raisonnable.

Et comme @earthengine l'a mentionné précédemment, la version multiligne est très raisonnable (pas de parenthèses supplémentaires) :

let x = x.do_something() await
         .do_another_thing() await;

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Il serait idéal que la syntaxe async/wait au niveau de la surface soit aussi familière que possible à quelqu'un venant de JavaScript, Python ou C#.

Dans le cas de try { .. } , nous avons pris en compte la familiarité avec d'autres langages. Cependant, c'était aussi la bonne conception d'un POV de cohérence interne avec Rust. Donc, avec tout le respect que je dois à ces autres langages, la cohérence interne dans Rust semble plus importante et je ne pense pas que la syntaxe des préfixes corresponde à Rust en termes de priorité ou de structure des API.

  • Je préfère aussi stabiliser une syntaxe qui n'utilise pas ! . Chaque utilisateur traitant d'async/wait se demandera pourquoi wait est une macro au lieu d'une construction de flux de contrôle normale, et je pense que l'histoire ici sera essentiellement "bien, nous n'avons pas pu trouver une bonne syntaxe, nous nous sommes donc contentés de en le faisant ressembler à une macro." Ce n'est pas une réponse convaincante.

Je suis d'accord avec ce sentiment, .await!() n'aura pas l'air assez de première classe.

  • Je suis en quelque sorte dubitatif quant aux avantages de postfix wait en général (pas entièrement, juste _sort of_). Je pense que l'équilibre est un peu différent de ? , car l'attente est une opération plus coûteuse (vous cédez en boucle jusqu'à ce qu'il soit prêt, plutôt que de simplement vous brancher et revenir une fois).

Je ne vois pas ce que la cherté a à voir avec l'extraction de choses dans des liaisons let . Les chaînes de méthodes peuvent être et sont parfois aussi coûteuses. L'avantage des liaisons let est de a) donner aux morceaux suffisamment volumineux un nom où il est logique d'améliorer la lisibilité, b) être capable de se référer à la même valeur calculée plus d'une fois (par exemple par &x ou lorsqu'un type est copiable).

Je me méfie un peu du code qui attendrait deux ou trois fois dans une seule expression ; il me semble bien de dire que ceux-ci devraient être retirés dans leurs propres fixations let.

Si vous pensez qu'ils devraient être retirés dans leurs propres liaisons let vous pouvez toujours faire ce choix avec postfix await :

let temporary = some_computation() await?;

Pour ceux qui ne sont pas d'accord et préfèrent le chaînage des méthodes, le suffixe await donne la possibilité de choisir. Je pense également que postfix suit mieux l'ordre de lecture et de flux de données de gauche à droite, donc même si vous extrayez des liaisons let je préférerais toujours postfix.

Je ne pense pas non plus que vous ayez besoin d'attendre deux ou trois fois pour que le suffixe await soit utile. Considérons par exemple (c'est le résultat de rustfmt ):

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

Mais aussi, je serais ouvert aux exemples de code qui, selon les gens, ne devraient vraiment pas être extraits dans des let et qui sont plus clairs en tant que chaînes de méthodes.

La plupart du code fuchsia que j'ai lu ne semblait pas naturel lorsqu'il était extrait dans des liaisons let et avec let binding = await!(...)?; .

  • C'est relativement familier pour la syntaxe postfix. Tout ce que vous avez à apprendre, c'est que wait va après l'expression plutôt qu'avant dans Rust, plutôt qu'une syntaxe significativement différente.

Ma préférence pour foo.await ici est principalement parce que vous obtenez une belle autocomplétion et la puissance du point. Cela ne semble pas non plus si radicalement différent. L'écriture de foo.await.method() indique également plus clairement que .method() est appliqué à foo.await . Cela résout donc ce problème.

  • Cela résout clairement le problème de préséance sur lequel tout cela portait.

Non, ce n'est pas seulement une question de préséance. Les chaînes de méthodes sont tout aussi importantes.

  • Le fait que cela ne fonctionne pas bien avec le chaînage de méthodes me semble presque un avantage, plutôt qu'un inconvénient, pour les raisons que j'ai évoquées précédemment.

Je ne sais pas pourquoi cela ne fonctionne pas bien avec le chaînage de méthodes.

Je serais peut-être plus obligé si nous avions des règles de grammaire qui empêchaient foo await.method() simplement parce que je pense vraiment que la méthode est (absurde) appliquée à await , pas foo (alors qu'il est intéressant de Je ne ressens pas ça avec foo await? ).

Alors que je ne serais pas obligé d'aller avec foo await si nous introduisions une coupe papier intentionnelle et empêchions le chaînage de méthodes avec la syntaxe suffixe await .

En admettant que toutes les options a un inconvénient, et que l' un d'entre eux devrait néanmoins finir par être choisi ... une chose qui me tracasse foo.await est que, même si l' on suppose qu'il ne sera pas littéralement confondu un champ struct, cela ressemble toujours plus secondaires (elle effectue à la fois les opérations d'E/S créées dans le futur et a des effets de flux de contrôle). Donc, quand je lis foo.await.method() , mon cerveau me dit de sauter le .await parce que c'est relativement inintéressant, et je dois faire attention et faire des efforts pour passer outre cet instinct manuellement.

il _ressemble toujours_ à accéder à un champ de structure.

@glaebhoerl Vous faites de bons points ; Cependant, la coloration syntaxique n'a-t-elle pas/insuffisamment d'impact sur son apparence et sur la façon dont votre cerveau traite les choses ? Au moins pour moi, la couleur et l'audace comptent beaucoup lors de la lecture du code, donc je ne sauterais pas sur .await qui a une couleur différente du reste des choses.

La connotation de l'accès sur le terrain est que rien de particulièrement impactant ne se produit - c'est l'une des opérations les moins efficaces dans Rust. Pendant ce temps, l'attente a un impact considérable, l'une des opérations les plus secondaires (elle effectue à la fois les opérations d'E/S créées dans le futur et a des effets de flux de contrôle).

Je suis tout à fait d'accord avec cela. await est une opération de contrôle de flux comme break ou return , et doit être explicite. La notation postfixée proposée ne semble pas naturelle, comme celle de Python if : comparez if c { e1 } else { e2 } à e1 if c else e2 . Voir l'opérateur à la fin vous fait faire une double prise, indépendamment de toute coloration syntaxique.

Je ne vois pas non plus en quoi e.await est plus cohérent avec la syntaxe Rust que await!(e) ou await e . Il n'y a pas d'autre mot-clé postfix, et comme l'une des idées était de le placer dans un cas particulier dans l'analyseur, je ne pense pas que ce soit une preuve de cohérence.

Il y a aussi le problème de familiarité @withoutboats mentionné. Nous pouvons choisir une syntaxe étrange et merveilleuse si elle présente de merveilleux avantages. Un suffixe await les a-t-il, cependant ?

La coloration syntaxique n'a-t-elle pas/insuffisamment d'impact sur son apparence et sur la façon dont votre cerveau traite les choses ?

(Bonne question, je suis sûr que cela aurait un certain impact, mais il est difficile de deviner à quel point sans l'essayer réellement (et substituer un mot-clé différent ne va pas loin). Pendant que nous sommes sur le sujet... longtemps il y a quelques jours, j'ai mentionné que je pense que la coloration syntaxique devrait mettre en évidence tous les opérateurs avec des effets de flux de contrôle ( return , break , continue , ? ... et maintenant await ) dans une couleur spéciale extra-distinctive, mais je ne suis pas en charge de la coloration syntaxique pour quoi que ce soit et je ne sais pas si quelqu'un le fait réellement.)

Je suis tout à fait d'accord avec cela. await est une opération de contrôle de flux comme break ou return , et doit être explicite.

Nous sommes d'accord. Les notations foo.await , foo await , foo# , ... sont explicites . Il n'y a pas d'attente implicite en cours.

Je ne vois pas non plus en quoi e.await est plus cohérent avec la syntaxe Rust que await!(e) ou await e .

La syntaxe e.await soi n'est pas cohérente avec la syntaxe Rust mais postfix s'adapte généralement mieux à ? et à la façon dont les API Rust sont structurées (les méthodes sont préférées aux fonctions gratuites).

La syntaxe await e? , si elle est associée en tant que (await e)? est totalement incompatible avec la façon dont break et return s'associent. await!(e) est également incohérent car nous n'avons pas de macros pour le flux de contrôle et il a également le même problème que les autres méthodes de préfixe.

Il n'y a pas d'autre mot-clé postfix, et comme l'une des idées était de le placer dans un cas particulier dans l'analyseur, je ne pense pas que ce soit une preuve de cohérence.

Je ne pense pas que vous ayez réellement besoin de changer la syntaxe de lib pour .await car elle devrait déjà être gérée comme une opération de champ. La logique serait plutôt traitée en résolution ou HIR où vous la traduisez en une construction spéciale.

Nous pouvons choisir une syntaxe étrange et merveilleuse si elle présente de merveilleux avantages. Est-ce qu'un suffixe await les a, cependant ?

Comme mentionné ci-dessus, je soutiens que c'est le cas en raison du chaînage des méthodes et de la préférence de Rust pour les appels de méthode.

Je ne pense pas que vous ayez réellement besoin de changer la syntaxe de lib pour .await car il devrait déjà être traité comme une opération sur le terrain.

C'est marrant!
L'idée est donc de réutiliser l'approche de self / super ..., mais pour les champs plutôt que pour les segments de chemin.

Cela fait cependant de await un mot-clé de segment de chemin (puisqu'il passe par la résolution), donc vous voudrez peut-être interdire les identifiants bruts pour cela.

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

Il n'y a pas d'attente implicite en cours.

L'idée est revenue plusieurs fois sur ce fil (la proposition « en attente implicite »).

nous n'avons pas de macros pour le flux de contrôle

Il y a try! (qui a assez bien rempli son rôle) et sans doute le select! obsolète. Notez que await est "plus fort" que return , il n'est donc pas déraisonnable de s'attendre à ce qu'il soit plus visible dans le code que le ? de return .

Je soutiens que c'est le cas en raison du chaînage des méthodes et de la préférence de Rust pour les appels de méthode.

Il a également une préférence (plus notable) pour les opérateurs de flux de contrôle de préfixe.

L'attente e? syntaxe, si associée en tant que (wait e) ? est complètement incompatible avec la façon dont la pause et le retour s'associent.

Je préfère await!(e)? , await { e }? ou peut-être même { await e }? -- je ne pense pas avoir vu ce dernier discuté, et je ne suis pas sûr que cela fonctionne.


J'admets qu'il pourrait y avoir un parti pris de gauche à droite. _Noter_

Mon opinion à ce sujet semble changer à chaque fois que j'examine la question, comme si je me jouais l'avocat du diable. Cela s'explique en partie par le fait que je suis tellement habitué à écrire mon propre avenir et mes propres machines à états. Un futur personnalisé avec poll est tout à fait normal.

Peut-être faudrait-il penser cela d'une autre manière.

Pour moi, les abstractions à coût zéro dans Rust font référence à deux choses : à coût zéro à l'exécution et, plus important encore, à coût zéro mentalement.

Je peux très facilement raisonner sur la plupart des abstractions dans Rust, y compris les futures, car ce ne sont que des machines à états.

A cette fin, une solution simple devrait exister qui présente le moins de magie à l'utilisateur. Les sceaux en particulier sont une mauvaise idée, car ils semblent inutilement magiques. Cela inclut les champs magiques .await .

La meilleure solution est peut-être la plus simple, la macro await! .

Donc, avec tout le respect que je dois à ces autres langages, la cohérence interne dans Rust semble plus importante et je ne pense pas que la syntaxe des préfixes corresponde à Rust en termes de priorité ou de structure des API.

Je ne vois pas comment...? await(foo)? / await { foo }? semble tout à fait correct en termes de priorité des opérateurs et de la façon dont les API sont structurées dans Rust - son inconvénient est la verbosité des parenthèses et (selon votre point de vue) le chaînage, ne pas briser le précédent ou être déroutant.

Il y a try! (qui a assez bien rempli son rôle) et sans doute le select! obsolète.

Je pense que le mot clé ici est obsolète . L'utilisation de try!(...) est une erreur difficile sur Rust 2018. C'est une erreur difficile maintenant parce que nous avons introduit une meilleure syntaxe, de première classe et de suffixe.

Notez que await est "plus fort" que return , il n'est donc pas déraisonnable de s'attendre à ce qu'il soit plus visible dans le code que le ? de return .

L'opérateur ? peut également avoir des effets secondaires (via d'autres implémentations que pour Result ) et exécute un flux de contrôle donc il est également assez "fort". Lorsqu'il a été discuté, ? été accusé de "cacher un retour" et d'être facile à ignorer. Je pense que cette prédiction a complètement échoué. La situation re. await me semble assez similaire.

Il a également une préférence (plus notable) pour les opérateurs de flux de contrôle de préfixe.

Ces opérateurs de flux de contrôle de préfixe sont tapés au type ! . Pendant ce temps, l'autre opérateur de flux de contrôle ? qui prend un contexte impl Try<Ok = T, ...> et vous donne un T est le suffixe.

Je ne vois pas comment...? await(foo)? / await { foo }? semble tout à fait correct en termes de priorité des opérateurs et de structure des API dans Rust-

La syntaxe await(foo) n'est pas la même que await foo si la parenthèse est requise pour la première et non pour la seconde. Le premier est sans précédent, le second a des problèmes de préséance. ? comme nous en avons discuté ici, sur le blog du bateau et sur Discord. La syntaxe await { foo } est problématique pour d'autres raisons (voir https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

son inconvénient est la verbosité des parenthèses et (selon votre point de vue) l'enchaînement, ne pas briser les précédents ou être déroutant.

C'est ce que j'entends par "les API sont structurées". Je pense que les méthodes et le chaînage de méthodes sont courants et idiomatiques dans Rust. Les syntaxes de préfixe et de bloc se composent mal avec celles-ci et avec ? .

Je suis peut-être minoritaire avec cet avis, et si c'est le cas, ignorez-moi :

Serait-il juste de déplacer la discussion préfixe-vs-postfixe vers un fil interne, puis de revenir simplement ici avec le résultat ? De cette façon, nous pouvons garder le problème de suivi pour

@seanmonstar Oui, je sympathise fortement avec le désir de limiter les discussions sur les problèmes de suivi et d'avoir des problèmes qui ne sont en réalité que des mises à jour de statut. C'est l'un des problèmes que j'espère que nous pourrons aborder avec quelques révisions de la façon dont nous gérons le processus RFC et les problèmes en général. Pour l'instant, j'ai ouvert un nouveau numéro ici pour que nous puissions en discuter.

IMPORTANT POUR TOUS : les autres discussions sur la syntaxe de await devraient aller ici .

Verrouillage temporaire pendant une journée pour s'assurer que les futures discussions sur la syntaxe await se produisent sur le problème approprié.

Le 15 janvier 2019 à 07:10:32 -0800, Pauan a écrit :

Note : il est toujours possible d'utiliser des parenthèses pour rendre la priorité plus claire :

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Cela va à l'encontre du principal avantage de postfix wait : « juste garder
écriture/lecture". Postfix wait, comme postfix ? , permet de contrôler le flux
pour continuer à avancer de gauche à droite :

foo().await!()?.bar().await!()

Si await! était un préfixe, ou à l'époque où try! était un préfixe, ou si vous avez
pour mettre entre parenthèses, alors vous devez revenir à la partie gauche de
l'expression en l'écrivant ou en la lisant.

EDIT : je lisais les commentaires du début à la fin par courrier électronique et je n'ai vu les commentaires "déplacer la conversation vers l'autre problème" qu'après avoir envoyé ce courrier.

Rapport d'état d'attente asynchrone :

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Je voulais poster une mise à jour rapide sur l'état de l'async-wait
effort. La version courte est que nous sommes dans la dernière ligne droite pour
une certaine stabilisation, mais il reste des
questions à surmonter.

Annonce du groupe de travail sur la mise en œuvre

Dans le cadre de cette poussée, je suis heureux d'annoncer que nous avons formé un
groupe de travail sur la mise en œuvre asynchrone . Ce groupe de travail
fait partie de l'ensemble de l'effort d'attente asynchrone, mais se concentre sur le
et fait partie de l'équipe de compilateur. Si vous souhaitez
aider à obtenir async-attendre sur la ligne d'arrivée, nous avons une liste de problèmes
où nous aimerions certainement de l'aide (lire la suite).

Si vous êtes intéressé à participer, nous avons des "heures de bureau"prévu pour mardi (voir le [calendrier de l'équipe du compilateur]) -- si vous
peut apparaître alors sur [Zulip], ce serait l'idéal ! (Mais sinon, insérez n'importe quel
temps.)

...

Quand std::future::Future sera-t-il stable ? Doit-il attendre l'attente asynchrone ? Je pense que c'est un très beau design et j'aimerais commencer à y porter du code. (Y a-t-il une cale pour l'utiliser en écurie ?)

@ry voir le nouveau problème de suivi : https://github.com/rust-lang/rust/issues/59113

Un autre problème de compilateur pour async/await : https://github.com/rust-lang/rust/issues/59245

Notez également que https://github.com/rust-lang-nursery/futures-rs/issues/1199 dans le premier message peut être coché, car il est maintenant corrigé.

Il semble qu'il y ait un problème avec les fermetures HRLB et asynchrones : https://github.com/rust-lang/rust/issues/59337. (Bien que le re-skimming de la RFC ne spécifie pas réellement que les fermetures asynchrones sont soumises à la même capture de durée de vie d'argument que la fonction asynchrone).

Oui, les fermetures asynchrones ont un tas de problèmes et ne devraient pas être incluses dans le cycle initial de stabilisation. Le comportement actuel peut être émulé avec un bloc de fermeture + asynchrone, et à l'avenir, j'aimerais voir une version qui permette de référencer les upvars de fermeture du futur renvoyé.

Je viens de remarquer qu'actuellement await!(fut) nécessite que fut soit Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

C'est prévu ? Il ne semble pas être dans le RFC.

@Ekleog qui n'est pas await! donnant l'erreur, await! empile conceptuellement le futur passé pour permettre l'utilisation des futurs !Unpin ( exemple de terrain de jeu rapide ). L'erreur vient de la contrainte sur impl Future for Box<impl Future + Unpin> , qui nécessite que le futur soit Unpin pour vous empêcher de faire quelque chose comme :

// where Foo: Future + !Unpin
let mut foo: Box<Foo> = ...;
Pin::new(&mut foo).poll(cx);
let mut foo = Box::new(*foo);
Pin::new(&mut foo).poll(cx);

Parce que Box est Unpin et permet d'en déplacer la valeur, vous pouvez interroger le futur une fois dans un emplacement de tas, puis sortir le futur de la boîte et le mettre dans un nouvel emplacement de tas et interroger encore une fois.

wait devrait éventuellement être un cas spécial pour autoriser Box<dyn Future> car il consomme le futur

Peut-être que le trait IntoFuture devrait être ressuscité pour await! ? Box<dyn Future> peut implémenter cela en convertissant en Pin<Box<dyn Future>> .

Voici mon prochain bug avec async/await : cela ressemble à l'utilisation d'un type associé à un paramètre de type dans le type de retour d'un async fn interrompt l'inférence : https://github.com/rust-lang/rust/ problèmes/60414

En plus d'ajouter potentiellement #60414 à la liste des premiers messages (je ne sais pas s'il est toujours utilisé - il serait peut-être préférable de pointer vers l'étiquette github ?), Je pense que la « Résolution de rust-lang/ rfcs#2418" peut être coché, car le trait Future IIRC a récemment été stabilisé.

Je viens d'un article sur Reddit et je dois dire que je n'aime pas du tout la syntaxe postfix. Et il semble que la majorité de Reddit ne l'aime pas non plus.

je préfère écrire

let x = (await future)?

que d'accepter cette syntaxe bizarre.

Quant au chaînage, je peux refactoriser mon code pour éviter d'avoir plus de 1 await .

De plus, JavaScript à l'avenir peut le faire ( proposition de pipeline intelligent ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Si le préfixe await est implémenté, cela ne signifie pas que await ne peut pas être enchaîné.

@KSXGitHub ce n'est vraiment pas l'endroit pour cette discussion mais la logique est décrite ici et il y a de très bonnes raisons à cela qui ont été réfléchies pendant de nombreux mois par de nombreuses personnes https://boats.gitlab.io/blog/post /attendre-decision/

@KSXGitHub Bien que je n'aime pas non plus la syntaxe finale, elle a été longuement discutée dans #57640, https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https://internals.rust- lang.org/t/a-final-proposal-for-await-syntax/ , et à divers autres endroits. Beaucoup de gens y ont exprimé leur préférence, et vous n'apportez pas de nouveaux arguments sur le sujet.

Veuillez ne pas discuter des décisions de conception ici, il y a un fil à cet effet explicite

Si vous prévoyez d'y commenter, gardez à l'esprit que la discussion a déjà beaucoup évolué : assurez-vous d'avoir quelque chose de substantiel à dire et assurez-vous que cela n'a pas déjà été dit dans le fil.

@withoutboats à ma connaissance, la syntaxe finale est déjà convenue, il est peut-être temps de la marquer comme Terminé ? :rougir:

L'intention est-elle de se stabiliser à temps pour la prochaine version bêta du 4 juillet, ou le blocage des bogues nécessitera-t-il un autre cycle pour être résolus ? Il y a beaucoup de problèmes ouverts sous la balise A-async-await, bien que je ne sache pas combien d'entre eux sont critiques.

Aha, ne tenez pas compte de cela, je viens de découvrir l'

Bonjour! Quand doit-on s'attendre à la sortie stable de cette fonctionnalité ? Et comment puis-je l'utiliser dans les builds nocturnes ?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 contient des informations sur la date de sortie cible et plus

Est-il prévu d'implémenter automatiquement Unpin pour les contrats à terme générés par async fn ?

Plus précisément, je me demande si Unpin n'est pas disponible automatiquement en raison du code Future généré lui-même, ou si nous pouvons utiliser des références comme arguments

@DoumanAsh Je suppose que si un fn async n'a jamais d'auto-références actives aux points de rendement, le futur généré pourrait éventuellement implémenter Unpin, peut-être?

Je pense que cela devrait être accompagné de messages d'erreur assez utiles disant "pas Unpin cause de _ce_ emprunt" + un indice de "autrement, vous pouvez boxer ce futur"

Le PR de stabilisation au #63209 note que "Tous les bloqueurs sont maintenant fermés." et a atterri dans la nuit du 20 août, se dirigeant donc vers la version bêta plus tard cette semaine. Il semble intéressant de noter que depuis le 20 août, de nouveaux problèmes de blocage ont été signalés (tels que suivis par la balise AsyncAwait-Blocking). Deux d'entre eux (#63710, #64130) semblent être de bons à avoir qui n'empêcheraient pas réellement la stabilisation, mais il y a trois autres problèmes (#64391, #64433, #64477) qui semblent mériter d'être discutés. Ces trois derniers problèmes sont liés, tous résultant du PR #64292, qui lui-même a été posé pour résoudre le problème de blocage d'AsyncAwait #63832. Un PR, n ° 64584, a déjà atterri pour tenter de résoudre l'essentiel des problèmes, mais les trois problèmes restent ouverts pour le moment.

Le côté positif est que les trois bloqueurs ouverts sérieux semblent concerner du code qui devrait être compilé, mais ne compile pas actuellement. En ce sens, il serait rétrocompatible d'atterrir ultérieurement sans entraver la bêta-isation et la stabilisation éventuelle d'async/wait. Cependant, je me demande si quelqu'un de l'équipe lang pense que quelque chose ici est suffisamment préoccupant pour suggérer qu'async/wait devrait cuire tous les soirs pour un autre cycle (ce qui, aussi désagréable que cela puisse paraître, est le but du calendrier de publication rapide après tout).

@bstrie Nous réutilisons simplement "AsyncAwait-Blocking" faute d'une meilleure étiquette pour les noter comme "hautement prioritaire", ils ne bloquent pas réellement. Nous devrions bientôt réorganiser le système d'étiquetage pour le rendre moins déroutant, cc @nikomatsakis.

... Pas bon... nous avons raté l'attente asynchrone dans le 1.38 attendu. Devoir attendre 1.39, juste à cause de quelques "problèmes" qui ne comptaient pas...

@earthengine Je ne pense pas que ce soit une évaluation juste de la situation. Les problèmes qui ont été soulevés méritent tous d'être pris au sérieux. Il ne serait pas bon d'atterrir asynchrone en attendant que les gens rencontrent ensuite ces problèmes en essayant de l'utiliser dans la pratique :)

Cette page vous a été utile?
0 / 5 - 0 notes