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 :
await
.Try
dans lesquelles nous voulons nous engagerLa 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?
seraitawait
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 :
await { future }?
ou await(future)?
(c'est bruyant).await future?
ou (await future)?
fasse ce qui est attendu (les deux semblent surprenants).await? future
(c'est inhabituel).await
suffixe d'une manière ou d'une autre, comme dans future await?
ou future.await?
(c'est sans précédent).?
, comme dans future@?
(il s'agit du "bruit de ligne").Cela dit, avoir
async
sansawait
-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 :
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>
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 surget_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 :
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.async
peuvent être assez grosses (aussi grosses que des fonctions normales) et compliquées.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):
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 :
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)yield return
tendance à se démarquer)await
+ Async
suffixeRemarquez 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 :
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
.task.rs
ce qui rend la navigation plus difficile. Je suggère de le diviser à nouveau.TaskObj#from_poll_task()
a un nom étrange. Je suggère de le nommer new()
placeTaskObj#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 fonctionWaker
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'appliqueJe 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 faisantawait 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!
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.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 commereturn
,break
etcontinue
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 :
await
, ce qui rend _difficile_ de repérer les points de suspension dans une fonction.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.@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 !
await
, mais aussi un mot-clé async
sur l'appel de fonction site .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 souventOk::<(), MyErrorType>(())
à la fin des blocsasync { ... }
. 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.
@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
Un nouveau numéro : https://github.com/rust-lang/rust/issues/53447
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édierstd
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
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 :
|| await!()
.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).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é pouryield
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 T
où foo: 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 :
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.
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.
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.
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.
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
).
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
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 »)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>;
}
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,?
explicitementreturn
s le résultat deTry
de la fonction actuelle tandis que wait a besoin deyield
.
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 soityield
oureturn
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-vouslet 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 queyield
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 :
foo await
est moins une divergence que certains sceaux comme foo@
), mais elles sont toutes plus divergentes que le préfixe wait.!
. 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.?
, 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 :
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
, pasfoo
(alors qu'il est intéressant de Je ne ressens pas ça avecfoo 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 commebreak
oureturn
, 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 queawait!(e)
ouawait 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 leselect!
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" quereturn
, il n'est donc pas déraisonnable de s'attendre à ce qu'il soit plus visible dans le code que le?
dereturn
.
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.
Mises à jour des liens : il semble que https://github.com/rust-lang/rust/issues/53259 et https://github.com/rust-lang/rust/issues/53447 aient été fermés. https://github.com/rust-lang-nursery/futures-rs/issues/1199 semble avoir été déplacé vers https://github.com/rust-lang/rust/issues/53548.
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.
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 :)
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 :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 :C'est plus facile à lire, c'est plus facile à refactoriser. Je crois que c'est la meilleure approche.
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'abordawait
alors vous n'avez aucun problème avec cela. C'est probablement lexicalement plus lié, maisawait
est à gauche et?
à droite. Il est donc toujours assez logiqueawait
commencer parResult
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.