Rust: Tracking-Problem für Async/Await (RFC 2394)

Erstellt am 8. Mai 2018  ·  308Kommentare  ·  Quelle: rust-lang/rust

Dies ist das Tracking-Problem für RFC 2394 (rust-lang/rfcs#2394), das der Sprache asynchrone und erwartete Syntax hinzufügt.

Ich werde die Implementierungsarbeit dieses RFC anführen, würde mich aber über ein Mentoring freuen, da ich relativ wenig Erfahrung mit Rustc habe.

MACHEN:

Ungelöste Fragen:

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

Hilfreichster Kommentar

Zur Syntax: Ich hätte gerne await als einfaches Schlüsselwort. Schauen wir uns zum Beispiel ein Anliegen aus dem Blog an:

Wir sind uns nicht ganz sicher, welche Syntax wir für das Schlüsselwort await verwenden möchten. Wenn etwas eine Zukunft eines Ergebnisses ist - wie jede IO-Zukunft wahrscheinlich sein wird - möchten Sie in der Lage sein, darauf zu warten und dann den Operator ? darauf anzuwenden. Aber die Rangfolge, um dies zu ermöglichen, mag überraschend erscheinen - await io_future? würde await zuerst und ? zweitens, obwohl ? lexikalisch enger gebunden ist als zu erwarten.

Ich stimme hier zu, aber Zahnspangen sind böse. Ich denke, es ist einfacher, sich daran zu erinnern, dass ? eine niedrigere Priorität hat als await und damit enden:

let foo = await future?

Es ist einfacher zu lesen, es ist einfacher umzugestalten. Ich glaube, das ist der bessere Ansatz.

let foo = await!(future)?

Ermöglicht ein besseres Verständnis einer Reihenfolge, in der Operationen ausgeführt werden, aber imo ist es weniger lesbar.

Ich glaube, dass, sobald Sie bekommen, dass await foo? await ausführt, dann haben Sie keine Probleme damit. Es ist wahrscheinlich lexikalisch gebundener, aber await befindet sich auf der linken Seite und ? auf der rechten Seite. Es ist also immer noch logisch, zuerst await und danach Result bearbeiten.


Sollten Meinungsverschiedenheiten bestehen, äußern Sie diese bitte, damit wir diskutieren können. Ich verstehe nicht, wofür das stille Downvote steht. Wir alle wünschen dem Rust alles Gute.

Alle 308 Kommentare

Die Diskussion hier scheint zum Erliegen gekommen zu sein, daher wird sie hier als Teil der await Syntaxfrage verlinkt: https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

Die Implementierung ist auf #50307 blockiert.

Zur Syntax: Ich hätte gerne await als einfaches Schlüsselwort. Schauen wir uns zum Beispiel ein Anliegen aus dem Blog an:

Wir sind uns nicht ganz sicher, welche Syntax wir für das Schlüsselwort await verwenden möchten. Wenn etwas eine Zukunft eines Ergebnisses ist - wie jede IO-Zukunft wahrscheinlich sein wird - möchten Sie in der Lage sein, darauf zu warten und dann den Operator ? darauf anzuwenden. Aber die Rangfolge, um dies zu ermöglichen, mag überraschend erscheinen - await io_future? würde await zuerst und ? zweitens, obwohl ? lexikalisch enger gebunden ist als zu erwarten.

Ich stimme hier zu, aber Zahnspangen sind böse. Ich denke, es ist einfacher, sich daran zu erinnern, dass ? eine niedrigere Priorität hat als await und damit enden:

let foo = await future?

Es ist einfacher zu lesen, es ist einfacher umzugestalten. Ich glaube, das ist der bessere Ansatz.

let foo = await!(future)?

Ermöglicht ein besseres Verständnis einer Reihenfolge, in der Operationen ausgeführt werden, aber imo ist es weniger lesbar.

Ich glaube, dass, sobald Sie bekommen, dass await foo? await ausführt, dann haben Sie keine Probleme damit. Es ist wahrscheinlich lexikalisch gebundener, aber await befindet sich auf der linken Seite und ? auf der rechten Seite. Es ist also immer noch logisch, zuerst await und danach Result bearbeiten.


Sollten Meinungsverschiedenheiten bestehen, äußern Sie diese bitte, damit wir diskutieren können. Ich verstehe nicht, wofür das stille Downvote steht. Wir alle wünschen dem Rust alles Gute.

Ich habe gemischte Ansichten darüber, dass await ein Schlüsselwort ist, @Pzixel. Obwohl es sicherlich einen ästhetischen Reiz hat und vielleicht konsistenter ist, ist "Keyword Bloat" in jeder Sprache ein echtes Problem, da async ein Schlüsselwort ist. Das heißt, macht es überhaupt Sinn, async ohne await zu haben, in Bezug auf die Funktionen? Wenn ja, können wir es vielleicht so lassen, wie es ist. Wenn nicht, würde ich dazu neigen, await einem Keyword zu machen.

Ich denke, es ist einfacher, sich daran zu erinnern, dass ? eine niedrigere Priorität hat als await und damit enden

Es ist vielleicht möglich, das zu lernen und zu verinnerlichen, aber es gibt eine starke Intuition, dass Dinge, die sich berühren, enger gebunden sind als Dinge, die durch Leerzeichen getrennt sind. Ich denke, es würde sich in der Praxis auf den ersten Blick immer falsch lesen.

Es hilft auch nicht in allen Fällen, zB eine Funktion, die ein Result<impl Future, _> zurückgibt:

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

Hier geht es nicht nur darum, "können Sie die Vorrangigkeit eines einzelnen Wait+ ? verstehen", sondern auch "wie sieht es aus, mehrere Waits zu verketten?" Selbst wenn wir nur einen Vorrang gewählt hätten, hätten wir immer noch das Problem von await (await (await first()?).second()?).third()? .

Eine Zusammenfassung der Optionen für die await Syntax, einige aus dem RFC und den Rest aus dem RFC-Thread:

  • Erfordern Sie irgendeine Art von Trennzeichen: await { future }? oder await(future)? (dies ist laut).
  • Wählen Sie einfach eine Priorität, sodass await future? oder (await future)? tut, was erwartet wird (beide wirken überraschend).
  • Kombinieren Sie die beiden Operatoren zu etwa await? future (dies ist ungewöhnlich).
  • Machen Sie await irgendwie postfix, wie in future await? oder future.await? (dies ist beispiellos).
  • Verwenden Sie ein neues Siegel, wie es ? getan hat, wie in future@? (dies ist "Linienrauschen").
  • Verwenden Sie überhaupt keine Syntax und machen Sie wait implizit (dadurch sind Unterbrechungspunkte schwerer zu erkennen). Damit dies funktioniert, muss auch der Akt der Zukunftskonstruktion explizit gemacht werden. Dies ist das Thema des oben verlinkten internen Threads .

Das heißt, macht es überhaupt Sinn, async ohne await zu haben, in Bezug auf die Funktionen?

@alexreg Es tut es. So funktioniert zum Beispiel Kotlin. Dies ist die Option "implizites Warten".

@rpjohnst Interessant. Nun, ich bin generell dafür, async und await als explizite Merkmale der Sprache zu belassen, da ich denke, dass das eher im Sinne von Rust ist, aber dann bin ich kein Experte für asynchrone Programmierung. ..

@alexreg async/await ist eine wirklich nette Funktion, da ich täglich in C# (das ist meine Hauptsprache) damit arbeite. @rpjohnst hat alle Möglichkeiten sehr gut klassifiziert. Ich bevorzuge die zweite Option, ich stimme anderen Erwägungen zu (laut/ungewöhnlich/...). Ich habe in den letzten 5 Jahren oder so mit Async/Await-Code gearbeitet, es ist wirklich wichtig, solche Flag-Keywords zu haben.

@rpjohnst

Selbst wenn wir nur einen Vorrang gewählt hätten, hätten wir immer noch das Problem von wait (wait (wait first()?).second()?).third()?.

In meiner Praxis schreibt man nie zwei await 's in eine Zeile. In sehr seltenen Fällen, wenn Sie es brauchen, schreiben Sie es einfach in then und verwenden wait überhaupt nicht. Sie können selbst sehen, dass es viel schwieriger zu lesen ist als

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

Ich denke also, es ist in Ordnung, wenn die Sprache davon abhält, Code auf diese Weise zu schreiben, um den primären Fall einfacher und besser zu machen.

hero away future await? sieht zwar ungewohnt interessant aus, aber ich sehe keine logischen Gegenargumente dagegen.

In meiner Praxis schreibt man nie zwei await 's in eine Zeile.

Aber ist dies unabhängig von der Syntax eine schlechte Idee oder einfach nur, weil die vorhandene await Syntax von C# es hässlich macht? Die Leute argumentierten ähnlich um try!() (der Vorläufer von ? ).

Die Postfix- und die impliziten Versionen sind weit weniger hässlich:

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

Aber ist dies unabhängig von der Syntax eine schlechte Idee oder einfach nur, weil die vorhandene Awart-Syntax von C# es hässlich macht?

Ich halte es unabhängig von der Syntax für eine schlechte Idee, da eine Zeile pro async Operation bereits komplex genug ist, um sie zu verstehen und schwer zu debuggen. Sie in einer einzigen Aussage verkettet zu haben, scheint noch schlimmer zu sein.

Schauen wir uns zum Beispiel echten Code an (ich habe einen Teil aus meinem Projekt genommen):

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

Es zeigt, dass es sich in der Praxis nicht lohnt, await s zu verketten, selbst wenn die Syntax dies zulässt, weil es völlig unlesbar würde await macht Oneliner nur noch schwieriger zu schreiben und zu lesen, aber ich tue es glaube, es ist nicht der einzige Grund, warum es schlecht ist.

Die Postfix- und die impliziten Versionen sind weit weniger hässlich

Die Möglichkeit, Task-Start und Task-Await zu unterscheiden, ist wirklich wichtig. Zum Beispiel schreibe ich oft Code wie diesen (wieder ein Ausschnitt aus dem Projekt):

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

Hier erstellen wir N asynchrone Anforderungen und warten darauf. Wir warten nicht bei jeder Schleifeniteration, sondern erstellen zuerst ein Array von asynchronen Anforderungen und warten dann auf alle auf einmal.

Ich kenne Kotlin nicht, also lösen sie das vielleicht irgendwie. Aber ich sehe nicht, wie Sie es ausdrücken können, wenn "Laufen" und "Warten" der Aufgabe gleich sind.


Daher denke ich, dass diese implizite Version in noch viel mehr impliziten Sprachen wie C# ein No-way ist.
In Rust mit seinen Regeln, die es nicht einmal erlauben, u8 implizit in i32 umzuwandeln, wäre das viel verwirrender.

@Pzixel Ja, die zweite Option klingt wie eine der vorzuziehenden. Ich habe async/await in C# verwendet, aber nicht sehr viel, da ich seit einigen Jahren nicht mehr hauptsächlich in C# programmiere. Was den Vorrang angeht, ist await (future?) für mich natürlicher.

@rpjohnst Ich mag die Idee eines Postfix-Operators, aber ich mache mir auch Sorgen um die Lesbarkeit und die Annahmen, die die Leute machen werden – es könnte leicht für ein Mitglied eines struct namens await verwechselt werden .

Die Möglichkeit, Task-Start und Task-Await zu unterscheiden, ist wirklich wichtig.

Für was es wert ist, tut dies die implizite Version. Es wurde zum Tod sowohl in dem RFC - Thread und in dem Interna Thread diskutiert, so dass ich hier nicht in eine Menge Detail gehen, aber die Grundidee ist nur , dass es die Eindeutigkeit von der Aufgabe await bewegt sich zu Aufgabe es Bau- doesn 'keine neue Implizitheit einführen.

Dein Beispiel würde ungefähr so ​​aussehen:

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[..])
}

Das meinte ich mit "damit dies funktioniert, muss auch der Akt des Konstruierens einer Zukunft explizit gemacht werden." Es ist der Arbeit mit Threads im Sync-Code sehr ähnlich – der Aufruf einer Funktion wartet immer, bis sie abgeschlossen ist, bevor der Aufrufer wieder aufgenommen wird, und es gibt separate Tools zum Einführen von Parallelität. Schließungen und thread::spawn / join entsprechen beispielsweise asynchronen Blöcken und join_all / select /etc.

Für was es wert ist, tut dies die implizite Version. Es wurde sowohl im RFC-Thread als auch im Internals-Thread zu Tode diskutiert, daher werde ich hier nicht viel ins Detail gehen, aber die Grundidee ist nur, dass es die Explizitheit von der Aufgabenerwartung zur Aufgabenkonstruktion verschiebt - das tut es 'keine neue Implizitheit einführen.

Ich glaube, das tut es. Ich kann hier nicht sehen, welcher Ablauf in dieser Funktion wäre, wo gibt es Punkte, an denen die Ausführung unterbrochen wird, bis Wait abgeschlossen ist. Ich sehe nur den async Block, der sagt "Hallo, hier gibt es irgendwo asynchrone Funktionen, versuche herauszufinden welche, du wirst überrascht sein!".

Ein weiterer Punkt: Rust ist in der Regel eine Sprache, in der man alles ausdrücken kann, nahe an blankem Metall und so weiter. Ich würde gerne etwas künstlichen Code bereitstellen, aber ich denke, er veranschaulicht die Idee:

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
}

Rost neigt immer dazu, die volle Kontrolle über das Geschehen zu geben. await ermöglichen es Ihnen, Punkte anzugeben, an denen der Fortsetzungsprozess durchgeführt wird. Es ermöglicht Ihnen auch, einen Wert in der Zukunft unwrap . Wenn Sie die implizite Konvertierung auf der Verwendungsseite zulassen, hat dies mehrere Auswirkungen:

  1. Zuallererst müssen Sie schmutzigen Code schreiben, um dieses Verhalten zu emulieren.
  2. Jetzt sollten RLS und IDEs erwarten, dass unser Wert entweder Future<T> oder erwartet T selbst ist. Es ist kein Problem mit Schlüsselwörtern - es existiert, dann ist das Ergebnis T , andernfalls ist es Future<T>
  3. Es macht den Code schwerer zu verstehen. In Ihrem Beispiel sehe ich nicht, warum es die Ausführung in der Zeile get_status_updates unterbricht, aber nicht in der Zeile get_status_update . Sie sind einander ziemlich ähnlich. Es funktioniert also entweder nicht so, wie der ursprüngliche Code war, oder es ist so kompliziert, dass ich es selbst dann nicht sehen kann, wenn ich mit dem Thema vertraut bin. Beide Alternativen machen diese Option nicht zu einem Gefallen.

Ich kann hier nicht sehen, welcher Ablauf in dieser Funktion wäre, wo gibt es Punkte, an denen die Ausführung unterbrochen wird, bis Wait abgeschlossen ist.

Ja, das meinte ich mit "dadurch sind Aufhängungspunkte schwerer zu erkennen". Wenn Sie den verlinkten Internals-Thread lesen, habe ich argumentiert, warum dies kein so großes Problem ist. Sie müssen keinen neuen Code schreiben, sondern setzen die Anmerkungen einfach an eine andere Stelle ( async Blöcke statt await ed Ausdrücke). IDEs haben kein Problem damit, den Typ zu erkennen (es ist immer T für Funktionsaufrufe und Future<Output=T> für async Blöcke).

Ich werde auch darauf hinweisen, dass Ihr Verständnis unabhängig von der Syntax wahrscheinlich falsch ist. Rust async Funktionen ausführen keinen Code überhaupt , bis sie in irgendeiner Weise abgewartet werden, so dass Ihr b.Status != TaskStatus.RanToCompletion Scheck wird immer passieren. Dies wurde auch im RFC-Thread zu Tode diskutiert, wenn Sie daran interessiert sind, warum es so funktioniert.

In Ihrem Beispiel sehe ich nicht, warum es die Ausführung in der Zeile get_status_updates unterbricht, aber nicht in der Zeile get_status_update . Sie sind einander ziemlich ähnlich.

Es tut Interrupt - Ausführung in beiden Orten. Der Schlüssel ist, dass async Blöcke nicht ausgeführt werden, bis sie erwartet werden, da dies für alle Futures in Rust gilt, wie ich oben beschrieben habe. In meinem Beispiel ruft (und wartet) get_statuses get_status_updates , dann in der Schleife konstruiert (aber nicht erwartet) count Futures, dann ruft es auf (und wartet somit) ) join_all , an welchem ​​Punkt diese Futures gleichzeitig get_status_update rufen (und somit erwarten).

Der einzige Unterschied zu Ihrem Beispiel besteht darin, wann genau die Futures zu laufen beginnen – in Ihrem ist es während der Schleife; in meinem ist es während join_all . Aber dies ist ein grundlegender Teil der Funktionsweise von Rust-Futures und hat nichts mit der impliziten Syntax oder gar mit async / await zu tun.

Ich werde auch darauf hinweisen, dass Ihr Verständnis unabhängig von der Syntax wahrscheinlich falsch ist. Die asynchronen Funktionen von Rust führen überhaupt keinen Code aus, bis sie in irgendeiner Weise erwartet werden, sodass Ihre b.Status != TaskStatus.RanToCompletion-Prüfung immer bestanden wird.

Ja, C#-Aufgaben werden bis zum ersten Unterbrechungspunkt synchron ausgeführt. Vielen Dank für den Hinweis.
Es spielt jedoch keine Rolle, da ich immer noch in der Lage sein sollte, einige Aufgaben im Hintergrund auszuführen, während ich den Rest der Methode ausführe, und dann überprüfen, ob die Hintergrundaufgabe abgeschlossen ist. ZB könnte es sein

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

Ich habe Ihre Idee zu async Blöcken und wie ich sehe, sind sie dasselbe Biest, aber mit mehr Nachteilen. Im ursprünglichen Vorschlag ist jede asynchrone Aufgabe mit await gepaart. Mit async Blöcken würde jede Aufgabe zum Zeitpunkt der Konstruktion mit einem async Block gepaart werden, also sind wir in fast der gleichen Situation wie zuvor (1:1 Beziehung), aber noch ein bisschen schlimmer, weil es sich anfühlt unnatürlicher und schwerer zu verstehen, da das Verhalten der Anrufsite kontextabhängig wird. Mit wait kann ich let a = foo() oder let b = await foo() und ich würde wissen, dass diese Aufgabe gerade konstruiert oder konstruiert und erwartet wird. Wenn ich let a = foo() mit async Blöcken sehe, muss ich schauen, ob oben ein paar async , wenn ich dich richtig verstehe, denn in diesem Fall

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[..])
}

Wir warten hier auf alle Aufgaben gleichzeitig

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 
}

Wir führen sie eins zu eins aus.

Daher kann ich nicht sagen, wie sich dieser Teil genau verhält:

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

Ohne mehr Kontext zu haben.

Und mit verschachtelten Blöcken wird es noch seltsamer. Ganz zu schweigen von Fragen zu Werkzeugen etc.

Callsite-Verhalten wird kontextabhängig

Dies ist bereits bei normalem Sync-Code und Closures der Fall. Beispielsweise:

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

Eine andere Sache, die Sie bei dem vollständigen impliziten Wait-Vorschlag beachten sollten, ist, dass Sie async fn s nicht aus Nicht- async Kontexten aufrufen können . Dies bedeutet , dass die Funktionsaufrufsyntax some_function(arg1, arg2, etc) läuft immer some_function ‚s Körper bis zur Fertigstellung , bevor der Anrufer weiter, unabhängig davon , ob some_function ist async . So Eintritt in einen async Kontext wird immer explizit markiert und Funktionsaufruf Syntax ist eigentlich mehr konsistent.

Bezüglich der Wait-Syntax: Wie wäre es mit einem Makro mit Methodensyntax? Ich kann keinen tatsächlichen RFC finden, um dies zuzulassen, aber ich habe ein paar Diskussionen ( 1 , 2 ) auf reddit gefunden, sodass die Idee nicht beispiellos ist. Dies würde es await , in der Postfix-Position zu arbeiten, ohne es zu einem Schlüsselwort zu machen / eine neue Syntax nur für diese Funktion einzuführen.

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

Es gibt eine Bibliothek aus der Scala-Welt, die das Komponieren von Monaden vereinfacht: http://monadless.io

Vielleicht sind einige Ideen für Rust interessant.

Zitat aus der Doku:

Die meisten Mainstream-Sprachen unterstützen asynchrone Programmierung mit dem async/await-Idiom oder implementieren es (zB F#, C#/VB, Javascript, Python, Swift). Obwohl nützlich, ist async/await normalerweise an eine bestimmte Monade gebunden, die asynchrone Berechnungen darstellt (Task, Future usw.).

Diese Bibliothek implementiert eine Lösung ähnlich wie async/await, jedoch verallgemeinert auf jeden Monadentyp. Diese Verallgemeinerung ist ein wichtiger Faktor, wenn man bedenkt, dass einige Codebasen neben Future auch andere Monaden wie Task für asynchrone Berechnungen verwenden.

Bei einer Monade M die Generalisierung das Konzept, reguläre Werte in eine Monade zu heben ( T => M[T] ) und Werte aus einer Monadeninstanz aufzuheben ( M[T] => T ). > Anwendungsbeispiel:

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

Beachten Sie, dass Lift Async und Unlift Await entspricht.

Dies ist bereits bei normalem Sync-Code und Closures der Fall. Beispielsweise:

Ich sehe hier mehrere Unterschiede:

  1. Lambda-Kontext ist unvermeidlich, aber nicht für await . Bei await wir keinen Kontext, bei async wir einen haben. Ersteres gewinnt, weil es die gleichen Funktionen bietet, aber weniger Kenntnisse über den Code erfordert.
  2. Lambdas ist in der Regel kurz, höchstens mehrere Zeilen, sodass wir den gesamten Körper auf einmal sehen können, und einfach. async Funktionen können ziemlich groß (so groß wie normale Funktionen) und kompliziert sein.
  3. Lambdas werden selten verschachtelt (außer für then Aufrufe, aber dafür wird await vorgeschlagen), async Blöcke werden häufig verschachtelt.

Eine andere Sache, die Sie aus dem vollständigen impliziten Wait-Vorschlag beachten sollten, ist, dass Sie asynchrone Fns nicht aus nicht asynchronen Kontexten aufrufen können.

Hm, das ist mir nicht aufgefallen. Es hört sich nicht gut an, da Sie in meiner Praxis oft asynchron aus einem nicht asynchronen Kontext ausführen möchten. In C# ist async nur ein Schlüsselwort, das es dem Compiler ermöglicht, den Funktionskörper neu zu schreiben, es beeinflusst die Funktionsschnittstelle in keiner Weise, so dass async Task<Foo> und Task<Foo> vollständig austauschbar sind entkoppelt Implementierung und API.

Manchmal möchten Sie vielleicht eine async Aufgabe blockieren, zB wenn Sie eine Netzwerk-API von main aufrufen möchten. Sie müssen blockieren (sonst kehren Sie zum Betriebssystem zurück und das Programm endet), aber Sie müssen eine asynchrone HTTP-Anforderung ausführen. Ich bin mir nicht sicher, welche Lösung hier sein könnte, außer main zu hacken, damit es asynchron ist, so wie wir es mit dem Hauptrückgabetyp Result tun, wenn Sie es nicht von einem nicht asynchronen Haupt aufrufen können .

Eine weitere Überlegung zugunsten des aktuellen await ist, wie es in anderen gängigen Sprachen funktioniert (wie von @fdietze angemerkt ). Es erleichtert die Migration von anderen Sprachen wie C#/TypeScript/JS/Python und ist somit ein besserer Ansatz, um neue Leute zu gewinnen.

Ich sehe hier mehrere Unterschiede

Sie sollten auch wissen, dass der Haupt-RFC bereits async Blöcke enthält, mit der gleichen Semantik wie die implizite Version.

Es hört sich nicht gut an, da Sie in meiner Praxis oft asynchron aus einem nicht asynchronen Kontext ausführen möchten.

Dies ist kein Thema. Sie können async Blöcke immer noch in Nicht- async Kontexten verwenden (was in Ordnung ist, da sie wie immer nur zu F: Future ausgewertet werden), und Sie können immer noch Futures spawnen oder blockieren mit genau der gleichen API wie zuvor.

Sie können einfach nicht async fn s anrufen, sondern den Anruf stattdessen in einen async Block einschließen – wie Sie es unabhängig vom Kontext, in dem Sie sich befinden, tun, wenn Sie ein F: Future möchten

async ist nur ein Schlüsselwort, das es dem Compiler ermöglicht, den Funktionskörper neu zu schreiben, es beeinflusst die Funktionsschnittstelle in keiner Weise

Ja, das ist ein legitimer Unterschied zwischen den Vorschlägen. Es war auch im Innengewinde abgedeckt. Es ist wohl nützlich, unterschiedliche Schnittstellen für die beiden zu haben, weil es Ihnen zeigt, dass die async fn Version keinen Code als Teil der Konstruktion ausführt, während die -> impl Future Version beispielsweise eine Anfrage initiieren kann, bevor Sie sie angibt ein F: Future . Es macht async fn s konsistenter mit normalen fn s, da das Aufrufen von etwas, das als -> T deklariert ist, immer ein T ergibt, unabhängig davon, ob es async .

(Sie sollen auch beachten , dass in Rust gibt es noch einen ganzen Sprung zwischen async fn und Future -returning Version, wie im RFC beschrieben. Die async fn Version nicht erwähnt Future irgendwo in der Signatur; und die manuelle Version erfordert impl Trait , was einige Probleme mit sich bringt, die mit der Lebensdauer zu tun haben.Dies ist tatsächlich ein Teil der Motivation für async fn für den Anfang.)

Es erleichtert die Migration von anderen Sprachen wie C#/TypeScript/JS/Python

Dies ist nur für die literale await future Syntax von Vorteil, die in Rust für sich genommen ziemlich problematisch ist. Alles andere, was wir am Ende haben könnten, stimmt auch mit diesen Sprachen nicht überein, während implizites Warten zumindest a) Ähnlichkeiten mit Kotlin und b) Ähnlichkeiten mit synchronem, threadbasiertem Code aufweist.

Ja, das ist ein legitimer Unterschied zwischen den Vorschlägen. Es war auch im Innengewinde abgedeckt. Es ist wohl sinnvoll, unterschiedliche Schnittstellen für die beiden zu haben

Ich würde sagen, _unterschiedliche Schnittstellen für die beiden zu haben, hat einige Vorteile_, weil es für mich nicht gut klingt, die API von den Implementierungsdetails abhängig zu machen. Sie schreiben beispielsweise einen Vertrag, der lediglich einen Anruf an die interne Zukunft delegiert

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

Und dann möchten Sie nur noch etwas Protokollierung hinzufügen

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

Und es wird eine bahnbrechende Veränderung. Whoa?

Dies ist nur für die Literal-Awart-Future-Syntax von Vorteil, die allein in Rust ziemlich problematisch ist. Alles andere, was wir am Ende haben könnten, stimmt auch mit diesen Sprachen nicht überein, während implizites Warten zumindest a) Ähnlichkeiten mit Kotlin und b) Ähnlichkeiten mit synchronem, threadbasiertem Code aufweist.

Es ist ein Vorteil für jede await Syntax, await foo / foo await / foo@ / foo.await /... Dasselbe, der einzige Unterschied besteht darin, dass Sie es vor / nach platzieren oder ein Siegel anstelle eines Schlüsselworts haben.

Zu beachten ist auch, dass in Rust noch ein ziemlicher Sprung zwischen async fn und der Future-Returning-Version besteht, wie im RFC beschrieben

Ich weiß es und es beunruhigt mich sehr.

Und es wird eine bahnbrechende Veränderung.

Sie können das umgehen, indem Sie einen async Block zurückgeben. Unter dem impliziten Erwartungsvorschlag sieht Ihr Beispiel so aus:

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

Und mit Protokollierung:

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

Das größere Problem mit dieser Unterscheidung entsteht während des Übergangs des Ökosystems von manuellen zukünftigen Implementierungen und Kombinatoren (der einzige Weg heute) zu Async/Await. Aber selbst dann ermöglicht Ihnen der Vorschlag, die alte Schnittstelle beizubehalten und daneben eine neue asynchrone bereitzustellen. C# ist zum Beispiel voll von diesem Muster.

Nun, das klingt vernünftig.

Ich glaube jedoch, dass eine solche Implizitheit (wir sehen nicht, ob foo() hier eine asynchrone oder eine Synchronisierungsfunktion ist) zu den gleichen Problemen führte, die bei Protokollen wie COM+ auftraten und ein Grund für die Implementierung von WCF war, wie es war . Die Leute hatten Probleme, wenn asynchrone Remote-Anfragen wie einfache Methodenaufrufe aussahen.

Dieser Code sieht vollkommen in Ordnung aus, außer dass ich nicht sehen kann, ob einige Anfragen asynchron oder synchronisiert sind. Ich glaube, das sind wichtige Informationen. Beispielsweise:

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

Es ist wichtig zu wissen, ob bar eine Sync- oder Async-Funktion ist. Ich sehe await in der Schleife oft als Markierung dafür, dass dieser Code geändert werden muss, um eine bessere Auslastung und Leistung zu erzielen. Dies ist ein Code, den ich gestern überprüft habe (der Code ist suboptimal, aber einer von Überprüfungsiterationen):

image

Wie Sie sehen können, habe ich leicht bemerkt, dass wir hier eine Schleife erwarten, und ich habe darum gebeten, dies zu ändern. Als die Änderung festgeschrieben wurde, wurde das Laden der Seite um das Dreifache beschleunigt. Ohne await könnte ich dieses Fehlverhalten leicht übersehen.

Ich gebe zu, dass ich Kotlin nicht verwendet habe, aber das letzte Mal, als ich mir diese Sprache ansah, schien sie hauptsächlich eine Variante von Java mit weniger Syntax zu sein, bis zu dem Punkt, an dem es einfach war, eine Maschine mechanisch in die andere zu übersetzen. Ich kann mir auch vorstellen, warum es in der Java-Welt (die tendenziell ein wenig Syntax-lastig ist) beliebt ist, und ich bin mir bewusst, dass es in letzter Zeit an Popularität gewonnen hat, insbesondere weil es nicht Java ist (die Situation zwischen Oracle und Google). ).

Wenn wir jedoch Popularität und Bekanntheit berücksichtigen, möchten wir vielleicht einen Blick darauf werfen, was JavaScript tut, was ebenfalls explizit await .

Das heißt, await wurde durch C# in die Mainstream-Sprachen eingeführt, was vielleicht eine Sprache ist, bei der Benutzerfreundlichkeit als äußerst wichtig angesehen wurde . In C# werden asynchrone Aufrufe nicht nur durch das Schlüsselwort await , sondern auch durch das Suffix Async der Methodenaufrufe. Das andere Sprachfeature, das die meisten mit await teilt, yield return ist auch im Code hervorragend sichtbar.

Warum ist das so? Meiner Meinung nach sind Generatoren und asynchrone Aufrufe zu mächtige Konstrukte, um sie im Code unbemerkt passieren zu lassen. Es gibt eine Hierarchie von Kontrollflussoperatoren:

  • sequentielle Ausführung von Anweisungen (implizit)
  • Funktions-/Methodenaufrufe (ganz offensichtlich, vergleichen Sie z. B. mit Pascal wo es an der Aufrufstelle keinen Unterschied zwischen einer nullären Funktion und einer Variablen gibt)
  • goto (in Ordnung, es ist keine strenge Hierarchie)
  • Generatoren ( yield return fällt eher auf)
  • await + Async Suffix

Beachten Sie, wie sie je nach Ausdrucksstärke oder Kraft auch von weniger zu ausführlicher werden.

Natürlich verfolgten andere Sprachen andere Ansätze. Schemafortsetzungen (wie in call/cc , was sich nicht allzu sehr von await ) oder Makros haben keine Syntax, um anzuzeigen, was Sie aufrufen. Bei Makros verfolgte Rust den Ansatz, sie leicht sichtbar zu machen.

Ich würde also argumentieren, dass weniger Syntax an sich nicht wünschenswert ist (dafür gibt es Sprachen wie APL oder Perl), und dass die Syntax nicht nur ein Musterbeispiel sein muss und eine wichtige Rolle bei der Lesbarkeit spielt.

Es gibt auch ein paralleles Argument (sorry, ich kann mich nicht an die Quelle erinnern, aber es könnte von jemandem aus dem Sprachteam stammen), dass die Leute sich mit verrauschter Syntax für neue Funktionen wohler fühlen, wenn sie neu sind, aber dann mit a gut zurechtkommen weniger ausführlich, sobald sie allgemein verwendet werden.


Was die Frage von await!(foo)? vs. await foo? , ich bin im ehemaligen Lager. Sie können so ziemlich jede Syntax verinnerlichen, wir sind jedoch zu daran gewöhnt, Hinweise auf Abstände und Nähe zu nehmen. Bei await foo? eine große Chance, dass man sich über die Rangfolge der beiden Operatoren Gedanken macht, während die geschweiften Klammern deutlich machen, was passiert. Es lohnt sich nicht, drei Zeichen zu speichern. Und was die Praxis des Verkettens von await! s angeht, obwohl es in einigen Sprachen ein beliebtes Idiom sein könnte, habe ich das Gefühl, dass es zu viele Nachteile wie schlechte Lesbarkeit und Interaktion mit Debuggern hat, um es zu optimieren.

Es lohnt sich nicht, drei Zeichen zu speichern.

Nach meiner anekdotischen Erfahrung sind zusätzliche Zeichen (zB längere Namen) kein großes Problem, aber zusätzliche Token können wirklich nervig sein. In Bezug auf eine CPU-Analogie ist ein langer Name geradliniger Code mit guter Lokalität - ich kann ihn einfach aus dem Muskelgedächtnis eingeben - während die gleiche Anzahl von Zeichen, wenn es sich um mehrere Token handelt (zB Satzzeichen), verzweigt und voller Cache-Fehltreffer ist.

(Ich stimme voll und ganz zu, dass await foo? höchst nicht offensichtlich wäre und wir es vermeiden sollten, und dass es weit vorzuziehen wäre, mehr Token einzugeben; meine Beobachtung ist nur, dass nicht alle Zeichen gleich erstellt werden.)


@rpjohnst Ich denke, Ihr alternativer Vorschlag könnte einen etwas besseren Empfang haben, wenn er als "explizit asynchron" und nicht als "implizites Warten" präsentiert würde :-)

Es ist wichtig zu wissen, ob bar eine Sync- oder Async-Funktion ist.

Ich bin mir nicht sicher, ob dies wirklich etwas anderes ist, als zu wissen, ob eine Funktion billig oder teuer ist, oder ob sie IO ausführt oder nicht, oder ob sie einen globalen Zustand berührt oder nicht. (Dies gilt auch für die Hierarchie von @lnicola - wenn asynchrone Aufrufe genau wie Synchronisierungsaufrufe

Die Tatsache, dass sich der Anruf beispielsweise in einer Schleife befand, ist genauso wichtig, wenn nicht sogar wichtiger als die Tatsache, dass er asynchron war. Und in Rust, wo die Parallelisierung so viel einfacher ist, könnten Sie genauso gut vorschlagen, teuer aussehende synchrone Schleifen auf Rayon-Iteratoren umzustellen!

Ich glaube also nicht, dass await wirklich so wichtig ist, um diese Optimierungen zu erfassen. Schleifen sind bereits immer gute Orte, um nach Optimierung zu suchen, und async fn s sind bereits ein guter Indikator dafür, dass Sie billige IO-Parallelität erzielen können. Wenn Sie diese Gelegenheiten verpassen, können Sie sogar einen Clippy-Lint für "async call in a loop" schreiben, den Sie gelegentlich ausführen. Es wäre toll, auch für synchronen Code einen ähnlichen Lint zu haben!

Die Motivation für "explizit asynchron" ist nicht einfach "weniger Syntax", wie @lnicola andeutet. Es dient dazu, das Verhalten der Funktionsaufrufsyntax konsistenter zu machen, sodass foo() Rumpf von foo immer vollständig ausführt. Bei diesem Vorschlag erhalten Sie durch das Weglassen einer Annotation nur weniger gleichzeitigen Code, und so verhält sich praktisch bereits der gesamte Code. Unter "explizites Warten" führt das Weglassen einer Annotation zu einer versehentlichen Parallelität oder zumindest zu einer versehentlichen Verschachtelung, was problematisch ist .

Ich denke, Ihr alternativer Vorschlag könnte etwas besser angenommen werden, wenn er als "explizit asynchron" und nicht als "implizites Warten" präsentiert würde :-)

Der Thread heißt "explizite zukünftige Konstruktion, implizites Warten", aber es scheint, dass letzterer Name hängen geblieben ist. :P

Ich bin mir nicht sicher, ob dies wirklich etwas anderes ist, als zu wissen, ob eine Funktion billig oder teuer ist, oder ob sie IO ausführt oder nicht, oder ob sie einen globalen Zustand berührt oder nicht. (Dies gilt auch für die Hierarchie von @lnicola - wenn asynchrone Aufrufe genau wie Synchronisierungsaufrufe

Ich denke, dies ist genauso wichtig wie zu wissen, dass die Funktion einen Zustand ändert, und wir haben bereits ein Schlüsselwort mut sowohl auf der Anrufseite als auch auf der Anruferseite.

Die Motivation für "explizit asynchron" ist nicht einfach "weniger Syntax", wie @lnicola andeutet. Es dient dazu, das Verhalten der Syntax von Funktionsaufrufen konsistenter zu machen, sodass foo() den Rumpf von foo immer vollständig ausführt.

Einerseits ist es eine gute Überlegung. Auf der anderen Seite können Sie zukünftige Erstellung und zukünftige Ausführung einfach trennen. Ich meine, wenn foo Ihnen eine Abstraktion zurückgibt, die es Ihnen ermöglicht, run aufzurufen und ein Ergebnis zu erhalten, macht das foo keinen nutzlosen Müll, der nichts tut, es macht einen sehr nützliche Sache: Es konstruiert ein Objekt, das Sie später Methoden aufrufen können. Es macht es nicht anders. Die Methode foo , die wir aufrufen, ist nur eine Blackbox und wir sehen ihre Signatur Future<Output=T> und sie gibt tatsächlich ein Future zurück. Also await wir es explizit, wenn wir dies tun wollen.

Der Thread heißt "explizite zukünftige Konstruktion, implizites Warten", aber es scheint, dass letzterer Name hängen geblieben ist. :P

Ich persönlich denke, dass die bessere Alternative "explizites asynchrones explizites Warten" ist :)


PS

Heute Abend kam mir auch ein Gedanke: Haben Sie versucht, mit C# LDM zu kommunizieren? Zum Beispiel Typen wie @HaloFour , @gafter oder @CyrusNajmabadi . Es kann wirklich eine gute Idee sein, sie zu fragen, warum sie die Syntax verwendet haben, die sie genommen haben. Ich würde vorschlagen, auch Leute aus anderen Sprachen zu fragen, aber ich kenne sie einfach nicht :) Ich bin sicher, sie hatten mehrere Debatten über die vorhandene Syntax und konnten bereits viel darüber diskutieren und haben möglicherweise einige nützliche Ideen.

Es bedeutet nicht, dass Rust diese Syntax haben muss, wie es C# tut, aber es ermöglicht nur, gewichtigere Entscheidungen zu treffen.

Ich persönlich denke, dass die bessere Alternative "explizites asynchrones explizites Warten" ist :)

Der Hauptvorschlag ist jedoch nicht "explizit asynchron", deshalb habe ich den Namen gewählt. Es ist " implizite Asynchronität", weil Sie nicht auf einen Blick erkennen können, wo Asynchronität eingeführt wird. Jeder nicht kommentierte Funktionsaufruf könnte ein Future konstruieren, ohne darauf zu warten, obwohl Future nirgendwo in seiner Signatur vorkommt.

Für was es wert ist , wird der Interna Thread eine „explizite async expliziten await“ Alternative gehören, denn das mit beide wichtigsten alternativen zukunfts kompatibel ist. (Siehe den letzten Abschnitt des ersten Beitrags.)

haben Sie versucht, mit C# LDM zu kommunizieren?

Der Autor des wichtigsten RFC hat es getan. Der wichtigste Punkt, der dabei herauskam, war, soweit ich mich erinnere, die Entscheidung, Future in die Signatur von async fn . In C# können Sie Task durch andere Typen ersetzen, um eine gewisse Kontrolle darüber zu haben, wie die Funktion gesteuert wird. Aber in Rust haben (und werden) wir keinen solchen Mechanismus – alle Futures durchlaufen ein einziges Merkmal, also müssen Sie dieses Merkmal nicht jedes Mal ausschreiben.

Wir haben auch mit den Sprachdesignern von Dart kommuniziert, und das war ein großer Teil meiner Motivation, den Vorschlag für "explizite asynchrone" zu schreiben. Dart 1 hatte ein Problem, weil Funktionen beim Aufrufen nicht bis zum ersten Wait ausgeführt wurden (nicht ganz so wie Rust, aber ähnlich), und das führte zu so massiver Verwirrung, dass sie sich in Dart 2 so änderten, dass Funktionen zu ihrem ersten ausgeführt wurden warten, wenn Sie angerufen werden. Rust kann das aus anderen Gründen nicht, aber es könnte die gesamte Funktion ausführen, wenn es aufgerufen wird, was ebenfalls diese Verwirrung vermeiden würde.

Wir haben auch mit den Sprachdesignern von Dart kommuniziert, und das war ein großer Teil meiner Motivation, den Vorschlag für "explizite asynchrone" zu schreiben. Dart 1 hatte ein Problem, weil Funktionen beim Aufrufen nicht bis zum ersten Wait ausgeführt wurden (nicht ganz so wie Rust, aber ähnlich), und das führte zu so massiver Verwirrung, dass sie sich in Dart 2 so änderten, dass Funktionen zu ihrem ersten ausgeführt wurden warten, wenn Sie angerufen werden. Rust kann das aus anderen Gründen nicht, aber es könnte die gesamte Funktion ausführen, wenn es aufgerufen wird, was ebenfalls diese Verwirrung vermeiden würde.

Tolle Erfahrung, war mir nicht bewusst. Schön zu hören, dass Sie so eine gewaltige Arbeit geleistet haben. Gut gemacht

Heute Abend kam mir auch ein Gedanke: Haben Sie versucht, mit C# LDM zu kommunizieren? Zum Beispiel Typen wie @HaloFour , @gafter oder @CyrusNajmabadi . Es kann wirklich eine gute Idee sein, sie zu fragen, warum sie die Syntax verwendet haben, die sie genommen haben.

Ich freue mich, Ihnen alle Informationen zur Verfügung zu stellen, an denen Sie interessiert sind. Ich werde jedoch nur überfliegen. Wäre es möglich, spezifische Fragen, die Sie derzeit haben, zusammenzufassen?

In Bezug auf die await Syntax (das könnte völlig dumm sein, schreien Sie mich gerne an; ich bin ein asynchroner Programmier-Noob und habe keine Ahnung, wovon ich rede):

Anstatt das Wort "warten" zu verwenden, können wir kein Symbol/Operator einführen, ähnlich wie ? . Es könnte beispielsweise # oder @ oder etwas anderes sein, das derzeit nicht verwendet wird.

Wenn es sich beispielsweise um einen Postfix-Operator handelt:

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

Es ist sehr prägnant und liest sich natürlich von links nach rechts: zuerst warten ( # ), dann Fehler behandeln ( ? ). Es hat nicht das Problem, das das postfix-Schlüsselwort await hat, wobei .await wie ein struct-Member aussieht. # ist eindeutig ein Operator.

Ich bin mir nicht sicher, ob Postfix der richtige Ort dafür ist, aber es fühlte sich aufgrund der Priorität so an. Als Präfix:

let stuff = #func()?;

Oder zum Teufel sogar:

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

Wurde das schon mal diskutiert?

(Mir ist klar, dass sich dies irgendwie der Syntax des "zufälligen Keyboard-Mash of Symbols" nähert, für die Perl berüchtigt ist ... :-D)

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

@CyrusNajmabadi danke fürs Kommen. Die Hauptfrage ist, welche der aufgelisteten Optionen Ihrer Meinung nach besser zur aktuellen Rust-Sprache passen, oder gibt es vielleicht eine andere Alternative? Dieses Thema ist nicht wirklich lang, sodass Sie es schnell von oben nach unten scrollen können. Die Hauptfrage: Sollte Rust dem aktuellen C#/TS/... await Weg folgen oder vielleicht seinen eigenen implementieren. Ist die aktuelle Syntax eine Art "Legacy", die Sie in irgendeiner Weise ändern möchten, oder passt sie am besten zu C# und ist auch die beste Option für neue Sprachen?

Die Hauptüberlegung gegenüber der C#-Syntax ist die Operator-Priorität. await foo? sollte zuerst warten und dann den ? Operator auswerten sowie den Unterschied, dass im Gegensatz zu C# die Ausführung im Aufrufer-Thread nicht vor dem ersten await , startet aber überhaupt nicht, genauso wie das aktuelle Code-Snippet keine Negativitätsprüfungen ausführt, bis GetEnumerator zum ersten Mal aufgerufen wird:

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

Ausführlicher in meinem ersten Kommentar und späterer Diskussion.

@Pzixel Oh, das habe ich wohl übersehen, als ich diesen Thread

Auf jeden Fall habe ich nicht viel Diskussion darüber gesehen, außer dieser kurzen Erwähnung.

Gibt es gute Argumente dafür/dagegen?

@rayvector Ich habe hier ein wenig für eine ausführlichere Syntax argumentiert. Einer der Gründe ist der, den Sie nennen:

die "zufällige Tastatur-Mischung von Symbolen"-Syntax, für die Perl berüchtigt ist

Um das klarzustellen, ich glaube nicht, dass await!(f)? wirklich im Rennen um die endgültige Syntax ist. Es wurde speziell ausgewählt, weil es eine solide Möglichkeit ist, sich nicht auf eine bestimmte Wahl festzulegen. Hier sind Syntaxen (einschließlich des ? Operators), die meiner Meinung nach noch "in Arbeit" sind:

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

Oder möglicherweise eine Kombination davon. Der Punkt ist, dass einige von ihnen geschweifte Klammern enthalten, um die Rangfolge klarer zu machen und es gibt hier viele Optionen - aber die Absicht ist, dass await in der endgültigen Version ein Schlüsselwortoperator und kein Makro sein wird ( abgesehen von einigen größeren Änderungen, wie sie rpjohnst vorgeschlagen hat).

Ich stimme entweder für einen einfachen Postfix-Await-Operator (zB ~ ) oder das Schlüsselwort ohne Klammern und höchste Priorität.

Ich habe mir diesen Thread durchgelesen und möchte folgendes vorschlagen:

  • await f? wertet zuerst den Operator ? und wartet dann auf die resultierende Zukunft.
  • (await f)? erwartet zuerst die Zukunft und wertet dann den ? Operator gegen das Ergebnis aus (aufgrund der gewöhnlichen Rust-Operator-Priorität)
  • await? f ist als syntaktischer Zucker für `(wait f)? verfügbar. Ich glaube, dass die "zukünftige Rückgabe eines Ergebnisses" ein sehr häufiger Fall sein wird, daher ist eine dedizierte Syntax sehr sinnvoll.

Ich stimme anderen Kommentatoren zu, dass await explizit sein sollte. Es ist ziemlich schmerzlos, dies in JavaScript zu tun, und ich schätze die Explizitheit und Lesbarkeit von Rust-Code sehr, und ich habe das Gefühl, dass die implizite Asynchronisierung dies für Async-Code ruinieren würde.

Mir ist aufgefallen, dass "impliziter asynchroner Block" als proc_macro implementierbar sein sollte, das einfach ein await Schlüsselwort vor jedem Future einfügt.

Die Hauptfrage ist, welche der aufgelisteten Optionen Ihrer Meinung nach besser zur aktuellen Rust-Sprache passen.

Einen C#-Designer zu fragen, was am besten zur Rost-Sprache passt, ist ... interessant :)

Ich fühle mich nicht qualifiziert, eine solche Entscheidung zu treffen. Ich mag Rost und experimentiere damit. Aber es ist keine Sprache, die ich tagein, tagaus benutze. Ich habe es auch nicht tief in meiner Psyche verwurzelt. Daher glaube ich nicht, dass ich qualifiziert bin, irgendwelche Behauptungen darüber aufzustellen, welche Auswahlmöglichkeiten für diese Sprache hier angemessen sind. Sie möchten mich zu Go/TypeScript/C#/VB/C++ befragen. Klar, ich würde mich viel wohler fühlen. Aber Rost liegt zu weit außerhalb meines Fachgebiets, um sich mit solchen Gedanken wohl zu fühlen.

Die Hauptüberlegung gegenüber der C#-Syntax ist die Operatorrangfolge await foo?

Das ist etwas, zu dem ich das Gefühl habe, etwas dazu sagen zu können. Wir haben bei 'wait' viel über den Vorrang nachgedacht und viele Formen ausprobiert, bevor wir uns auf die gewünschte Form gesetzt haben. Eines der wichtigsten Dinge, die wir herausgefunden haben, war, dass für uns und die Kunden (intern und extern), die diese Funktion nutzen wollten, es selten der Fall war, dass die Leute wirklich etwas über ihren asynchronen Anruf hinaus "verketten" wollten. Mit anderen Worten, die Leute schienen eine starke Anziehungskraft darauf zu haben, dass 'Warten' der wichtigste Teil eines jeden vollständigen Ausdrucks ist und daher in der Nähe der Spitze steht. Hinweis: Mit 'vollständiger Ausdruck' meine ich Dinge wie den Ausdruck, den Sie oben in einer Ausdrucks-Anweisung erhalten, oder den Ausdruck rechts von einer Zuordnung der obersten Ebene, oder den Ausdruck, den Sie als 'Argument' an etwas übergeben.

Die Tendenz, dass Leute mit dem „Warten“ in einem Ausdruck „weitermachen“ wollten, war selten. Wir sehen gelegentlich Dinge wie (await expr).M() , aber diese scheinen weniger verbreitet und weniger wünschenswert zu sein als die Anzahl der Leute, die await expr.M() tun.

Aus diesem Grund haben wir auch keine 'implizite' Form für 'warten' gewählt. In der Praxis war es etwas, worüber die Leute sehr klar nachdenken wollten und das in ihrem Code im Vordergrund stehen sollte, damit sie darauf achten konnten. Interessanterweise ist diese Tendenz auch Jahre später geblieben. dh manchmal bedauern wir viele Jahre später, dass etwas zu ausführlich ist. Einige Funktionen sind auf diese Weise von Anfang an gut, aber wenn die Leute sich damit wohl fühlen, sind sie mit etwas Knapperem besser geeignet. Das war bei 'warten' nicht der Fall. Die Leute scheinen die Schwergewichtigkeit dieses Keywords und die von uns gewählte Priorität immer noch sehr zu mögen.

Bisher waren wir mit der Vorrangwahl für unser Publikum sehr zufrieden. In Zukunft könnten wir hier einige Änderungen vornehmen. Aber insgesamt besteht kein starker Druck, dies zu tun.

--

sowie der Unterschied, dass die Ausführung im Gegensatz zu C# nicht im Aufrufer-Thread bis zum ersten Warten ausgeführt wird, sondern überhaupt nicht gestartet wird, genauso wie der aktuelle Codeausschnitt keine Negativitätsprüfungen ausführt, bis GetEnumerator zum ersten Mal aufgerufen wird:

IMO, die Art und Weise, wie wir Enumeratoren erstellt haben, war ein Fehler und hat im Laufe der Jahre zu einer Menge Verwirrung geführt. Es war besonders schlimm, weil viel Code so geschrieben werden muss:

```c#
void SomeEnumerator(X args)
{
// Args validieren, synchrone Arbeit ausführen.
return SomeEnumeratorImpl(args);
}

void SomeEnumeratorImpl(X args)
{
// ...
Ertrag
// ...
}

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

Sie können diese Methode vollständig aufrufen und nie auf ein Await treffen. Wenn "Ausführung nicht im Aufrufer-Thread ausgeführt wird, bis das erste Warten erfolgt" was passiert hier eigentlich?

erwarten? f steht als syntaktischer Zucker für `(wait f)? zur Verfügung. Ich glaube, dass die "zukünftige Rückgabe eines Ergebnisses" ein sehr häufiger Fall sein wird, daher ist eine dedizierte Syntax sehr sinnvoll.

Das kommt bei mir am meisten an. Es erlaubt 'Wait' als oberstes Konzept, erlaubt aber auch eine einfache Handhabung von Ergebnistypen.

Eine Sache, die wir aus C# wissen, ist, dass die Intuition der Leute bezüglich der Rangfolge an Leerzeichen gebunden ist. Wenn Sie also "Warten x?" dann fühlt es sich sofort so an, als hätte await weniger Vorrang als ? weil ? an den Ausdruck angrenzt. Wenn das obige tatsächlich als (await x)? geparst würde, wäre das für unser Publikum überraschend.

Es als await (x?) zu parsen, würde sich allein von der Syntax am natürlichsten anfühlen und würde der Notwendigkeit entsprechen, ein 'Ergebnis' einer Zukunft/Aufgabe zurückzubekommen und darauf zu 'warten', wenn Sie tatsächlich einen Wert erhalten . Wenn dies dann selbst ein Ergebnis zurückgibt, ist es angebracht, dies mit dem "Warten" zu kombinieren, um zu signalisieren, dass es danach passiert. also await? x? jedes ? bindet sich eng an den Teil des Codes, auf den es sich am natürlichsten bezieht. Das erste ? bezieht sich auf das await (und insbesondere auf das Ergebnis davon) und das zweite bezieht sich auf das x .

Wenn "Ausführung nicht im Aufrufer-Thread ausgeführt wird, bis das erste Warten erfolgt" was passiert hier eigentlich?

Nichts passiert, bis der Aufrufer den Rückgabewert von FooAsync erwartet. An diesem Punkt wird der Rumpf von FooAsync , bis entweder ein await oder es zurückgegeben wird.

Es funktioniert auf diese Weise, weil Rust Future s poll-gesteuert, Stack-alloziert und nach dem ersten Aufruf von poll unverrückbar sind. Der Aufrufer muss die Möglichkeit haben, sie an ihren Platz zu bringen – auf dem Heap für Future s der obersten Ebene, oder nach Wert innerhalb eines übergeordneten Future , oft auf dem "Stack-Frame" eines aufrufenden async fn --bevor irgendein Code ausgeführt wird.

Dies bedeutet, dass wir entweder bei a) C#-Generator-ähnlicher Semantik feststecken, bei der kein Code beim Aufrufen ausgeführt wird, oder b) Kotlin-Koroutinen-ähnlicher Semantik, bei der der Aufruf der Funktion auch sofort und implizit darauf wartet (mit Closure-ähnlichen async { .. } Blöcke, wenn Sie gleichzeitige Ausführung benötigen).

Ich bevorzuge Letzteres, weil es das von Ihnen erwähnte Problem mit C#-Generatoren vermeidet und auch die Frage nach der Operatorrangfolge vollständig vermeidet.

@CyrusNajmabadi In Rust Future normalerweise nicht, bis es als Task (es ist viel ähnlicher wie F# Async ):

let bar = foo();

In diesem Fall foo() kehrt einen Future , aber es wahrscheinlich nicht wirklich etwas zu tun. Sie müssen es manuell spawnen (was auch F# Async ähnelt):

tokio::run(bar);

Wenn es gespawnt ist, wird es dann Future ausführen. Da dies das Standardverhalten von ist Future , wäre es für async / await in Rust konsequenter sein keinen Code ausgeführt werden, bis sie hervorgebracht wird.

Offensichtlich ist die Situation in C# anders, denn in C# wird beim Aufrufen von foo() sofort mit der Ausführung von Task . Daher ist es in C# sinnvoll, Code bis zum ersten await auszuführen .

Außerdem... wie funktionieren die Dinge dann, wenn Sie so etwas haben [...] Sie können diese Methode vollständig aufrufen und nie auf einen warten. Wenn "Ausführung nicht im Aufrufer-Thread ausgeführt wird, bis das erste Warten erfolgt" was passiert hier eigentlich?

Wenn Sie FooAsync() aufrufen, passiert nichts, es wird kein Code ausgeführt. Wenn Sie es dann spawnen, wird der Code synchron ausgeführt, await wird nie ausgeführt und gibt sofort () (das ist Rusts Version von void ).

Mit anderen Worten, es ist nicht "Ausführung wird nicht im Aufrufer-Thread ausgeführt, bis das erste Warten erfolgt", sondern "Ausführung wird nicht ausgeführt, bis sie explizit gestartet wird (z. B. mit tokio::run )"

Nichts geschieht, bis der Aufrufer auf den Rückgabewert von FooAsync wartet. An diesem Punkt wird der Text von FooAsync ausgeführt, bis entweder ein Await oder eine Rückgabe erfolgt.

Ick. Das scheint schade. Es kommt oft vor, dass ich nicht dazu komme, auf etwas zu warten (oft aufgrund von Absage und Zusammensetzung mit Aufgaben). Als Entwickler würde ich mich immer noch freuen, wenn diese Fehler frühzeitig auftreten (was einer der häufigsten Gründe ist, warum die Ausführung bis zum Erwarten läuft).

Das bedeutet, dass wir entweder bei a) C#-Generator-ähnlicher Semantik stecken bleiben, bei der beim Aufruf kein Code ausgeführt wird, oder b) Kotlin-Koroutinen-ähnlicher Semantik, bei der der Aufruf der Funktion auch sofort und implizit darauf wartet (mit Closure-like async { . .}-Blöcke, wenn Sie gleichzeitige Ausführung benötigen).

In Anbetracht dessen würde ich ersteres bei weitem vorziehen als letzteres. Aber nur meine persönliche Präferenz. Wenn sich der kotlin-Ansatz für Ihre Domain natürlicher anfühlt, dann entscheiden Sie sich dafür!

@CyrusNajmabadi Ick. Das scheint schade. Es kommt oft vor, dass ich nicht dazu komme, auf etwas zu warten (oft aufgrund von Absage und Zusammensetzung mit Aufgaben). Als Entwickler würde ich mich immer noch freuen, wenn diese Fehler frühzeitig auftreten (was einer der häufigsten Gründe ist, warum die Ausführung bis zum Erwarten läuft).

Ich empfinde das genaue Gegenteil. Nach meiner Erfahrung mit JavaScript wird häufig vergessen, await . In diesem Fall wird Promise weiterhin ausgeführt, aber die Fehler werden verschluckt (oder es passieren andere seltsame Dinge).

Beim Rust/Haskell/F#-Stil läuft entweder Future (mit korrekter Fehlerbehandlung) oder es läuft überhaupt nicht. Dann bemerken Sie, dass es nicht ausgeführt wird, also untersuchen und beheben Sie es. Ich glaube, dies führt zu robusterem Code.

@Pauan @rpjohnst Danke für die Erklärungen. Das waren auch Ansätze, die wir in Betracht gezogen haben. Aber es stellte sich heraus, dass es in der Praxis nicht wirklich wünschenswert war.

In den Fällen, in denen Sie nicht wollten, dass es "eigentlich etwas tut. Sie müssen es manuell starten", fanden wir es sauberer, dies so zu modellieren, dass etwas zurückgegeben wird, das bei Bedarf Aufgaben generiert. dh etwas so Einfaches wie Func<Task> .

Ich empfinde das genaue Gegenteil. Nach meiner Erfahrung mit JavaScript ist es sehr üblich, die Verwendung von await zu vergessen.

C# funktioniert, um sicherzustellen, dass Sie die Aufgabe entweder abgewartet oder anderweitig sinnvoll verwendet haben.

aber die Fehler werden geschluckt

Das ist das Gegenteil von dem, was ich sage. Ich möchte damit sagen, dass der Code eifrig ausgeführt wird, damit ich sofort auf Fehler treffe, selbst für den Fall, dass ich nie dazu komme, den Code in der Aufgabe auszuführen. Dies ist bei Iteratoren genauso. Ich würde viel lieber wissen, dass ich es zu dem Zeitpunkt falsch erstellt habe, zu dem ich die Funktion aufrufe, im Gegensatz zu möglicherweise viel weiter unten, wenn / wenn der Iterator gestreamt wird.

Dann bemerken Sie, dass es nicht ausgeführt wird, also untersuchen und beheben Sie es.

In den Szenarien, die ich meine, ist "nicht ausgeführt" völlig vernünftig. Schließlich kann meine Anwendung jederzeit entscheiden, dass sie die Aufgabe nicht ausführen muss. Das ist nicht der Fehler, den ich beschreibe. Der Fehler, den ich beschreibe, ist, dass ich die Validierung nicht bestanden habe, und ich möchte dies so kurz wie möglich an dem Punkt herausfinden, an dem ich die Arbeit logisch erstellt habe, im Gegensatz zu dem Punkt, an dem die Arbeit tatsächlich ausgeführt werden muss. Da dies Modelle zur Beschreibung der asynchronen Verarbeitung sind, ist es oft so, dass diese weit voneinander entfernt sind. Daher ist es wichtig, dass die Informationen über Probleme so früh wie möglich erfolgen.

Auch dies ist, wie gesagt, nicht hypothetisch. Ähnliches passiert mit Streams/Iteratoren. Die Leute erschaffen sie oft, realisieren sie dann aber erst später. Es war eine zusätzliche Belastung für die Leute, diese Dinge bis zu ihrer Quelle zurückverfolgen zu müssen. Aus diesem Grund müssen jetzt so viele APIs (einschließlich Hte BCL) die Aufteilung zwischen der synchronen/frühen Arbeit und der eigentlichen verzögerten/faulen Arbeit erledigen.

Das ist das Gegenteil von dem, was ich sage. Ich möchte damit sagen, dass der Code eifrig ausgeführt wird, damit ich sofort auf Fehler treffe, selbst für den Fall, dass ich nie dazu komme, den Code in der Aufgabe auszuführen.

Ich kann den Wunsch nach frühen Fehlern verstehen, aber ich bin verwirrt: In welcher Situation würden Sie jemals "am Ende nicht dazu kommen, die Future "?

Die Funktionsweise von Future s in Rust besteht darin, dass Sie Future s auf verschiedene Weise zusammensetzen (einschließlich async/await, einschließlich paralleler Kombinatoren usw.). einzelnes verschmolzenes Future das alle Unter- Future s enthält. Und dann auf der obersten Ebene Ihres Programms ( main ) verwenden Sie dann tokio::run (oder ähnlich), um es zu starten.

Abgesehen von diesem einzelnen tokio::run Aufruf in main , werden Sie normalerweise Future s nicht manuell erzeugen, sondern sie einfach erstellen. Und die Komposition behandelt natürlich das Spawnen/Fehlerbehandlung/Abbrechen/usw. korrekt.

Ich möchte auch etwas klarstellen. Wenn ich etwas sage wie:

Aber es stellte sich heraus, dass es in der Praxis nicht wirklich wünschenswert war.

Ich spreche ganz konkret über Dinge mit unserer Sprache/Plattform. Ich kann nur einen Einblick in die Entscheidungen geben, die für C#/.Net/CoreFx etc. sinnvoll waren. Es kann durchaus sein, dass Ihre Situation anders ist und was Sie optimieren möchten und welche Ansätze Sie verfolgen sollten, gehen Sie in eine völlig andere Richtung.

Ich kann den Wunsch nach frühen Fehlern verstehen, bin aber verwirrt: In welcher Situation würden Sie jemals "nicht dazu kommen, die Zukunft zu spawnen"?

Die ganze Zeit :)

Überlegen Sie, wie Roslyn (der C#/VB-Compiler/die IDE-Codebasis) selbst geschrieben ist. Es ist stark asynchron und interaktiv . dh der primäre Anwendungsfall ist die gemeinsame Nutzung durch viele Clients, die darauf zugreifen. Client-Dienste interagieren häufig mit dem Benutzer über eine Fülle von Funktionen, von denen viele entscheiden, dass sie Aufgaben, die sie ursprünglich für wichtig hielten, nicht mehr ausführen müssen, da der Benutzer eine Vielzahl von Aktionen ausführt. Während der Benutzer beispielsweise tippt, führen wir Tonnen von Aufgabenzusammenstellungen und -manipulationen durch, und wir entscheiden uns möglicherweise, sie nicht einmal auszuführen, weil ein paar ms später ein anderes Ereignis eintrat.

Während der Benutzer beispielsweise tippt, führen wir Tonnen von Aufgabenzusammenstellungen und -manipulationen durch, und wir entscheiden uns möglicherweise, sie nicht einmal auszuführen, weil ein paar ms später ein anderes Ereignis eintrat.

Aber wird das nicht einfach per Stornierung gehandhabt?

Und die Komposition behandelt natürlich das Spawnen/Fehlerbehandlung/Abbrechen/usw. korrekt.

Es hört sich einfach so an, als hätten wir zwei sehr unterschiedliche Modelle, um die Dinge darzustellen. Das ist in Ordnung :) Meine Erklärungen sind im Kontext des von uns gewählten Modells zu sehen. Sie sind für das von Ihnen gewählte Modell möglicherweise nicht sinnvoll.

Es hört sich einfach so an, als hätten wir zwei sehr unterschiedliche Modelle, um die Dinge darzustellen. Das ist in Ordnung :) Meine Erklärungen sind im Kontext des von uns gewählten Modells zu sehen. Sie sind für das von Ihnen gewählte Modell möglicherweise nicht sinnvoll.

Absolut, ich versuche nur, Ihre Perspektive zu verstehen und auch unsere Perspektive zu erklären. Vielen Dank, dass Sie sich die Zeit genommen haben, die Dinge zu erklären.

Aber wird das nicht einfach per Stornierung gehandhabt?

Die Aufhebung ist (für uns) ein orthogonales Konzept zur Asynchronität. Sie werden häufig zusammen verwendet. Aber keines erfordert das andere.

Sie könnten ein System ganz ohne Abbruch haben, und es kann einfach der Fall sein, dass Sie einfach nie dazu kommen, den Code auszuführen, der auf die von Ihnen erstellten Aufgaben 'wartet'. dh aus logischen Gründen kann Ihr Code einfach lauten "Ich muss nicht auf 't' warten, ich werde nur etwas anderes tun". Nichts an Aufgaben (in unserer Welt) diktiert oder erfordert, dass erwartet werden sollte, dass diese Aufgabe erwartet wird. In einem solchen System würde ich eine frühzeitige Validierung wünschen.

Hinweis: Dies ähnelt dem Iteratorproblem. Sie können jemanden anrufen Ergebnisse zu erhalten , die Sie in Ihrem Code später verwenden möchten. Aus einer Reihe von Gründen müssen Sie die Ergebnisse jedoch möglicherweise nicht tatsächlich verwenden. Mein persönlicher Wunsch wäre es immer noch, die Validierungsergebnisse frühzeitig zu erhalten, auch wenn ich sie technisch nicht hätte bekommen können und mein Programm erfolgreich gewesen wäre.

Ich denke, es gibt vernünftige Argumente für beide Richtungen. Aber meine Meinung ist, dass der synchrone Ansatz mehr Vor- als Nachteile hatte. Wenn der synchrone Ansatz buchstäblich nicht passt, weil Ihr eigentliches Impl arbeiten möchte, scheint dies die Frage zu beantworten, was Sie tun müssen: D

Mit anderen Worten, ich denke nicht, dass Ihr Ansatz hier schlecht ist. Und wenn es starke Vorteile rund um dieses Modell hat, von dem Sie glauben, dass es das Richtige für Rust ist, dann machen Sie es auf jeden Fall :)

Sie könnten ein System ganz ohne Abbruch haben, und es kann einfach der Fall sein, dass Sie einfach nie dazu kommen, den Code auszuführen, der auf die von Ihnen erstellten Aufgaben 'wartet'. dh aus logischen Gründen kann Ihr Code einfach lauten "Ich muss nicht auf 't' warten, ich werde nur etwas anderes tun".

Persönlich denke ich, dass dies am besten mit der üblichen if/then/else Logik gehandhabt wird:

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

Aber wie Sie sagen, es ist nur eine ganz andere Perspektive als C#.

Persönlich denke ich, dass das am besten mit der üblichen Wenn / Dann / Sonst-Logik gehandhabt wird:

Ja. das wäre in Ordnung, wenn die Überprüfung der Bedingung an der gleichen Stelle erfolgen könnte, an der die Aufgabe erstellt wird (und so viele Fälle sind so). Aber in unserer Welt ist es normalerweise nicht so, dass die Dinge so gut miteinander verbunden sind. Schließlich möchten wir als Reaktion auf Benutzer eifrig asynchrone Arbeit leisten (damit die Ergebnisse bei Bedarf bereitstehen), aber wir können später entscheiden, dass es uns nicht mehr wichtig ist.

In unseren Domänen geschieht das "Warten" an dem Punkt, an dem die Person "den Wert braucht", was eine andere Bestimmung/Komponente/usw. ist. von der Entscheidung "soll ich anfangen, am Wert zu arbeiten?"

In gewisser Weise sind diese sehr entkoppelt, und das wird als Tugend angesehen. Der Produzent und der Verbraucher können völlig unterschiedliche Richtlinien haben, können aber durch die schöne Abstraktion der „Aufgabe“ effektiv über die asynchrone Arbeit kommunizieren.

Wie auch immer, ich ziehe mich von der sync/async-Meinung zurück. Hier sind offensichtlich ganz unterschiedliche Modelle im Spiel. :)

In Bezug auf die Rangfolge habe ich einige Informationen darüber gegeben, wie C# über Dinge denkt. Ich hoffe, es ist hilfreich. Lassen Sie es mich wissen, wenn Sie dort weitere Informationen wünschen.

@CyrusNajmabadi Ja, Ihre Erkenntnisse waren sehr hilfreich. Persönlich stimme ich Ihnen zu, dass await? foo der richtige Weg ist (obwohl ich auch den Vorschlag "explizit async " mag).

Übrigens, wenn Sie eine der besten Expertenmeinungen zu allen Feinheiten des .net-Modells rund um die Modellierung von Async/Sync-Arbeiten und allen Vor- und Nachteilen dieses Systems wünschen , dann wäre richtige Ansprechpartner . Er wäre etwa 100x besser als ich darin, Dinge zu erklären, die Vor- und Nachteile zu klären und wahrscheinlich in der Lage zu sein, tief in die Modelle auf beiden Seiten einzutauchen. Er ist mit der Herangehensweise von .net hier (einschließlich der getroffenen und abgelehnten Entscheidungen) und der Entwicklung von .net seit den Anfängen bestens vertraut. Er ist sich auch der Leistungskosten der Ansätze von .net schmerzlich bewusst (was einer der Gründe dafür ist, warum ValueTask jetzt existiert), an die Sie meiner Meinung nach in erster Linie mit Ihrem Wunsch nach Zero/Low denken würden -Kostenabstraktionen.

Nach meiner Erinnerung wurden in den frühen Tagen ähnliche Gedanken über diese Spaltungen in den Ansatz von .net eingebracht, und ich denke, er konnte sehr gut zu den endgültigen Entscheidungen sprechen, die getroffen wurden und wie angemessen sie waren.

Ich würde immer noch für await? future stimmen, auch wenn es etwas ungewohnt aussieht. Gibt es wirkliche Nachteile beim Komponieren?

Hier ist eine weitere gründliche Analyse der Vor- und Nachteile von kalten (F#) vs. heißen (C#,JS) Asyncs: http://tomasp.net/blog/async-csharp-differences.aspx

Es gibt jetzt einen neuen RFC für Postfix-Makros, der das Experimentieren mit Postfix await ohne dedizierte Syntaxänderung ermöglicht: https://github.com/rust-lang/rfcs/pull/2442

await {} ist mein Favorit hier, erinnert an unsafe {} und zeigt Vorrang.

let value = await { future }?;

@seunlanlege
Ja, es ist remeniszent, also haben die Leute die falsche Annahme, dass sie Code wie diesen schreiben können

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

Aber sie können nicht.

@Pixel
Wenn ich Sie richtig verstehe, gehen Sie davon aus, dass Futures implizit innerhalb eines await {} Blocks erwartet werden? Dem stimme ich nicht zu. await {} würde nur auf den Ausdruck warten, zu dem der Block ausgewertet wird.

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

Und es sollte ein Muster sein, von dem abgeraten wird

vereinfacht

let value = await { create_future() };

Sie schlagen eine Aussage vor, bei der mehr als ein Ausdruck "abzuraten wäre". Siehst du nichts Falsches daran?

Ist es günstig, await einem Muster zu machen (abgesehen von ref usw.)?
Etwas wie:

let await n = bar();

Ich nenne das lieber ein async Muster als await , obwohl ich keinen großen Vorteil darin sehe, es zu einer Mustersyntax zu machen. Mustersyntaxen arbeiten im Allgemeinen doppelt in Bezug auf ihre Ausdrucksgegenstücke.

Laut aktueller Seite von https://doc.rust-lang.org/nightly/std/task/index.html besteht der Task-Mod sowohl aus Reexporten aus libcore als auch aus Reexporten für liballoc, was das Ergebnis etwas ... suboptimal. Ich hoffe, das wird irgendwie behoben, bevor es sich stabilisiert.

Ich habe mir den Code angeschaut. Und ich habe ein paar Vorschläge:

  • [x] Das Merkmal UnsafePoll und die Aufzählung Poll haben sehr ähnliche Namen, sind aber nicht verwandt. Ich schlage vor, UnsafePoll umzubenennen, zB in UnsafeTask .
  • [x] In der Futures-Kiste wurde der Code in verschiedene Submodule aufgeteilt. Jetzt ist der meiste Code in task.rs gebündelt, was die Navigation erschwert. Ich schlage vor, es wieder aufzuteilen.
  • [x] TaskObj#from_poll_task() hat einen seltsamen Namen. Ich schlage vor, es stattdessen new() zu nennen
  • [x] TaskObj#poll_task könnte einfach poll() . Das Feld mit dem Namen poll könnte poll_fn heißen, was auch darauf hindeutet, dass es sich um einen Funktionszeiger handelt
  • Waker könnte möglicherweise dieselbe Strategie wie TaskObj anwenden und die Vtable auf den Stack legen. Nur eine Idee, ich weiß nicht, ob wir das wollen. Wäre es schneller, weil es etwas weniger indirekt ist?
  • [ ] dyn ist jetzt in der Beta stabil. Der Code sollte wahrscheinlich dyn wo er zutrifft

Ich kann auch eine PR für dieses Zeug liefern. @cramertj @aturon Sie können mich kontaktieren , um die Details zu besprechen.

Wie wäre es, wenn Sie einfach eine Methode await() für alle Future hinzufügen?

    /// 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 Das Problem ist, dass await nicht wie eine normale Methode funktioniert. Überlegen Sie tatsächlich, was die Methode await tun würde, wenn sie sich nicht in einem async Block/einer

@xfix das ist im Allgemeinen nicht wahr. Der Compiler kann tun, was er will, und könnte den Methodenaufruf speziell in diesem Fall behandeln. Der Aufruf des Methodenstils löst das Einstellungsproblem, aber es ist unerwartet (await funktioniert in anderen Sprachen nicht so) und wäre wahrscheinlich ein hässlicher Hack im Compiler.

@elszben Dass der Compiler tun kann , was er will, bedeutet nicht, dass er tun sollte , was er will.

future.await() klingt wie ein normaler Funktionsaufruf, ist es aber nicht. Wenn Sie diesen Weg gehen möchten, würde die oben vorgeschlagene future.await!() Syntax die gleiche Semantik ermöglichen und deutlich mit einem Makro markieren „Hier geht etwas Seltsames vor, ich weiß“.

Edit: Beitrag entfernt

Ich habe diesen Beitrag in den Futures RFC verschoben. Verknüpfung

Hat sich jemand die Interaktion zwischen async fn und #[must_use] ?

Wenn Sie ein async fn haben, wird beim direkten Aufrufen kein Code ausgeführt und ein Future ; es scheint, als ob alle async fn ein inhärentes #[must_use] auf dem "äußeren" impl Future Typ haben sollten, sodass Sie sie nicht anrufen können, ohne etwas mit dem Future zu tun.

Wenn Sie außerdem selbst ein #[must_use] an das async fn anhängen, scheint dies für die Rückgabe der inneren Funktion zu gelten. Wenn Sie also #[must_use] async fn foo() -> T { ... } schreiben, können Sie nicht await!(foo()) schreiben, ohne etwas mit dem Ergebnis von await zu tun.

Hat sich jemand die Interaktion zwischen async fn und #[must_use] angesehen?

Andere, die an dieser Diskussion interessiert sind, finden Sie unter https://github.com/rust-lang/rust/issues/51560.

Ich habe darüber nachgedacht, wie asynchrone Funktionen implementiert werden, und habe festgestellt, dass diese Funktionen weder Rekursion noch gegenseitige Rekursion unterstützen.

für die Awart-Syntax bin ich persönlich für Post-Fix-Makros, keinen impliziten Awarte-Ansatz, für seine einfache Verkettung und dass er auch sozusagen wie ein Methodenaufruf verwendet werden kann

@warlord500 Sie ignorieren völlig die gesamte Erfahrung von Millionen von Entwicklern, die oben beschrieben wurden. Sie möchten keine await 's verketten.

@Pzixel bitte gehe nicht davon aus, dass ich den Thread oder das, was ich will, nicht gelesen habe.
Ich weiß, dass einige Mitwirkende möglicherweise keine Verkettung zulassen möchten, aber es gibt einige von uns
Entwickler, die das tun. Ich bin mir nicht sicher, woher du überhaupt die Vorstellung hast, dass ich es ignoriere
Entwicklermeinungen, mein Kommentar gab nur die Meinung eines Mitglieds der Community und meine Gründe für diese Meinung an.

EDIT : Wenn Sie eine Meinungsverschiedenheit haben, teilen Sie es bitte mit! Ich bin gespannt warum du sagst
Wir sollten das Verketten von Waits über eine Methode wie Syntax nicht zulassen?

@warlord500, weil das MS-Team seine Erfahrungen mit Tausenden von Kunden und Millionen von Entwicklern geteilt hat. Ich weiß es selbst, weil ich täglich Async/Await-Code schreibe und man sie nie verketten möchte. Hier ist ein genaues Zitat, wenn Sie es wünschen:

Wir haben bei 'wait' viel über den Vorrang nachgedacht und viele Formen ausprobiert, bevor wir uns auf die gewünschte Form gesetzt haben. Eines der wichtigsten Dinge, die wir herausgefunden haben, war, dass für uns und die Kunden (intern und extern), die diese Funktion nutzen wollten, es selten der Fall war, dass die Leute wirklich etwas über ihren asynchronen Anruf hinaus „verketten“ wollten. Mit anderen Worten, die Leute schienen eine starke Anziehungskraft darauf zu haben, dass 'Warten' der wichtigste Teil eines jeden vollständigen Ausdrucks ist und daher in der Nähe der Spitze steht. Hinweis: Mit 'vollständiger Ausdruck' meine ich Dinge wie den Ausdruck, den Sie oben in einer Ausdrucks-Anweisung erhalten, oder den Ausdruck rechts von einer Zuordnung der obersten Ebene, oder den Ausdruck, den Sie als 'Argument' an etwas übergeben.

Die Tendenz, dass Leute mit dem „Warten“ in einem Ausdruck „weitermachen“ wollten, war selten. Wir sehen gelegentlich Dinge wie (await expr).M() , aber diese scheinen weniger verbreitet und weniger wünschenswert zu sein als die Anzahl der Leute, die await expr.M() tun.

Ich bin jetzt ziemlich verwirrt, wenn ich dich richtig verstehe, sollten wir nicht unterstützen
warten Sie auf den einfachen Kettenstil nach der Korrektur, weil er nicht häufig verwendet wird? Sie sehen erwarten, dass der wichtigste Teil eines Ausdrucks ist.
Ich vermute in diesem Fall nur, dass ich Sie richtig verstehe.
Wenn ich falsch liege, zögere nicht, mich zu korrigieren.

Sie können auch den Link posten, wo Sie das Angebot erhalten haben,
Danke schön.

Mein Widerspruch zu den beiden obigen Punkten ist, dass Sie etwas nicht allgemein verwenden, was nicht unbedingt bedeutet, dass es schädlich wäre, wenn es den Code sauberer macht.

manchmal erwarten inst den wichtigsten Teil eines Ausdrucks, wenn der zukünftige erzeugende Ausdruck ist
der wichtigste Teil ist und Sie ihn nach oben setzen möchten, können Sie dies immer noch tun, wenn wir zusätzlich zum normalen Makrostil einen Postfix-Makrostil zulassen

Sie können auch den Link posten, wo Sie das Angebot erhalten haben,
Danke schön.

Aber... aber du hast gesagt, dass du den ganzen Thread gelesen hast... 😃

Aber ich habe kein Problem damit, es zu teilen: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886 . Ich empfehle Ihnen, alle Cyrus-Beiträge zu lesen, es ist wirklich eine Erfahrung mit dem gesamten C#/.Net-Ökosystem, es ist eine unbezahlbare Erfahrung, die von Rust wiederverwendet werden kann.

warten manchmal auf den wichtigsten Teil eines Ausdrucks

Das Zitat sagt eindeutig das Gegenteil 😄 Und weißt du, ich habe selbst das gleiche Gefühl, wenn ich täglich asynchron/wartet.

Hast du Erfahrung mit async/await? Kannst du es dann bitte teilen?

Wow, ich kann nicht glauben, dass ich das übersehen habe. Vielen Dank, dass Sie sich die Zeit nehmen, das zu verlinken.
Ich habe keine Erfahrung, also denke ich, dass meine Meinung im Großen und Ganzen nicht so wichtig ist

@Pzixel Ich async / await teilen, aber bitte seien Sie respektvoll gegenüber anderen Mitwirkenden. Sie müssen den Erfahrungsstand anderer nicht kritisieren, um Ihren eigenen technischen Punkten Gehör zu verschaffen.

Anmerkung der Moderatorin: @Pzixel Persönliche Angriffe auf Community-Mitglieder sind nicht erlaubt. Ich habe es aus Ihrem Kommentar herausgeschnitten. Mach es nicht noch einmal. Wenn Sie Fragen zu unserer Moderationsrichtlinie haben, wenden Sie sich bitte an

@crabtw Ich habe niemanden dafür kritisiert. Ich entschuldige mich für alle Unannehmlichkeiten, die hier Platz haben könnten.

Ich habe einmal nach Erfahrung gefragt, als ich wissen wollte, ob eine Person ein tatsächliches Bedürfnis hat, 'Warten' zu verketten, oder es ist seine Extrapolation von heutigen Merkmalen. Ich wollte mich nicht an die Autorität wenden, es ist nur eine nützliche Sammlung von Informationen, bei denen ich sagen kann: "Sie müssen es selbst ausprobieren und diese Wahrheit selbst erkennen". Nichts Anstößiges hier.

Persönliche Angriffe auf Community-Mitglieder sind nicht erlaubt. Ich habe es aus Ihrem Kommentar herausgeschnitten.

Keine persönlichen Angriffe. Wie ich sehen kann, haben Sie meine Referenz zu Downvotes auskommentiert. Nun, es war nur mein Reaktor bei meinem Post-Downvote, nichts Besonderes. Da es entfernt wurde, ist es auch vernünftig, diesen Verweis zu entfernen (es kann für weitere Leser sogar verwirrend sein), also danke für das Entfernen.

Danke für den Hinweis. Ich wollte erwähnen, dass Sie nichts von dem, was ich sage, als 'Evangelium' verstehen sollten :) Rust und C# sind unterschiedliche Sprachen mit unterschiedlichen Gemeinschaften, Paradigmen und Redewendungen. Sie sollten auf jeden Fall die besten Entscheidungen für Ihre Sprache treffen. Ich hoffe, meine Worte sind hilfreich und können Aufschluss geben. Aber seien Sie immer offen für verschiedene Möglichkeiten, Dinge zu tun.

Ich hoffe, dass Sie sich etwas Erstaunliches für Rust einfallen lassen. Dann können wir sehen , was Sie getan haben und steal adoptieren sie gnädigst für C # :)

Soweit ich das beurteilen kann, spricht das verlinkte Argument hauptsächlich von der Priorität von await und argumentiert insbesondere, dass es sinnvoll ist, await x.y() als await (x.y()) anstatt als (await x).y() zu parsen. await!(x.y()) die Mehrdeutigkeit beseitigt.

Ich glaube jedoch nicht, dass dies eine bestimmte Antwort auf den Wert der Verkettung wie x.y().await!().z() nahelegt.

Der zitierte Kommentar ist zum Teil deshalb interessant, weil es einen großen Unterschied in Rust gibt, der einer der Hauptfaktoren dafür war, dass wir die endgültige Wait-Syntax verzögert haben: C# hat keinen ? Operator, also haben sie keinen Code, der müsste (await expr)? . Sie beschreiben (await expr).M() als wirklich ungewöhnlich, und ich neige dazu zu denken, dass dies auch in Rust zutreffen würde, aber die einzige Ausnahme hiervon ist aus meiner Sicht ? , was sehr häufig vorkommen wird weil viele Futures zu Ergebnissen führen werden (zum Beispiel alle , die jetzt existieren).

@ohneboote ja, das stimmt. Ich möchte diesen Teil noch einmal zitieren:

die einzige Ausnahme ist aus meiner Sicht ?

Wenn es nur eine Ausnahme gibt, erscheint es sinnvoll, await? foo als Abkürzung für (await foo)? zu erstellen und das Beste aus beiden Welten zu haben.

Zumindest jetzt erlaubt die vorgeschlagene Syntax von await!() die eindeutige Verwendung von ? . Wir können uns über eine kürzere Syntax für die Kombination von await und ? Gedanken machen, wenn wir uns entscheiden, die Basissyntax für await zu ändern. (Und je nachdem , was wir es ändern, könnten wir nicht ein Problem überhaupt haben.)

@joshtriplett diese zusätzlichen Klammern beseitigen Mehrdeutigkeiten, aber sie sind wirklich sehr schwer. B. in meinem aktuellen Projekt suchen:

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

Ich habe 139 Waits in 2743 sloc. Vielleicht ist es keine große Sache, aber ich denke, wir sollten die bügellose Alternative als sauberer und besser betrachten. Davon abgesehen ist ? die einzige Ausnahme, also könnten wir await foo ohne geschweifte Klammern verwenden und nur für diesen speziellen Fall eine spezielle Syntax einführen. Es ist keine große Sache, könnte aber einige Klammern für ein LISP-Projekt sparen.

Ich habe einen Blogbeitrag darüber erstellt, warum asynchrone Funktionen meiner Meinung nach den äußeren Rückgabetypansatz für ihre Signatur verwenden sollten. Viel Spaß beim Lesen!

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

Ich habe nicht alle Diskussionen verfolgt, also zögern Sie nicht, mich darauf hinzuweisen, wo dies bereits diskutiert worden wäre, wenn ich es verpasst hätte.

Hier ist ein zusätzliches Anliegen bezüglich des Ansatzes des inneren Rückgabetyps: Wie würde die Syntax für Stream s aussehen, wenn sie angegeben wird? Ich würde denken, dass async fn foo() -> impl Stream<Item = T> nett und konsistent mit async fn foo() -> impl Future<Output = T> aussehen würde, aber es würde nicht mit dem Ansatz des inneren Rückgabetyps funktionieren. Und ich glaube nicht, dass wir das Schlüsselwort async_stream einführen wollen.

@Ekleog Stream müsste ein anderes Schlüsselwort verwenden. Es kann nicht async weil impl Trait umgekehrt funktioniert. Es kann nur sicherstellen, dass bestimmte Merkmale implementiert werden, aber die Merkmale selbst müssen bereits auf dem zugrunde liegenden konkreten Typ implementiert werden.

Der Ansatz des äußeren Rückgabetyps wäre jedoch praktisch, wenn wir eines Tages asynchrone Generatorfunktionen hinzufügen möchten:

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

Stream könnte für alle asynchronen Generatoren mit Return = () implementiert werden. Dies macht dies möglich:

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

Hinweis: Generatoren sind bereits in Nightly, verwenden diese Syntax jedoch nicht. Sie sind sich im Gegensatz zu Stream in Futures 0.3 derzeit auch nicht bewusst.

Bearbeiten: Dieser Code verwendete zuvor ein Generator . Ich habe einen Unterschied zwischen Stream und Generator übersehen. Streams sind asynchron. Das bedeutet, dass sie einen Wert liefern können, aber nicht müssen. Sie können entweder mit Poll::Ready oder Poll::Pending antworten. Ein Generator hingegen muss immer synchron nachgeben oder abschließen. Ich habe es jetzt in AsyncGenerator geändert, um dies widerzuspiegeln.

Edit2: @Ekleog Die aktuelle Implementierung von Generatoren verwendet eine Syntax ohne Marker und scheint zu erkennen, dass es sich um einen Generator handeln sollte, indem sie nach einem yield im Körper sucht. Dies bedeutet , dass Sie richtig wäre zu sagen , dass async wieder verwendet werden kann. Ob dieser Ansatz sinnvoll ist, ist allerdings eine andere Frage. Aber ich denke, das ist für ein anderes Thema ^^'

Tatsächlich dachte ich, dass async wiederverwendet werden könnte, wäre es nur, weil async gemäß diesem RFC nur mit Future s erlaubt wäre und somit erkennen könnte es generiert ein Stream indem es den Rückgabetyp betrachtet (der entweder ein Future oder ein Stream ).

Der Grund, warum ich das jetzt erhebe, ist, dass, wenn wir das gleiche Schlüsselwort async haben wollen, um sowohl Future s als auch Stream s zu generieren, dann denke ich die äußere Rendite type-Ansatz wäre viel sauberer, weil er explizit wäre, und ich glaube nicht, dass irgendjemand erwarten würde, dass ein async fn foo() -> i32 einen Stream von i32 ergibt (was möglich wäre, wenn der Textkörper a yield und es wurde der Ansatz der inneren Rendite gewählt).

Wir könnten ein zweites Schlüsselwort für Generatoren haben (zB gen fn ) und dann Streams erstellen, indem wir einfach beide anwenden (zB async gen fn ). Der äußere Rückgabetyp muss dabei überhaupt nicht berücksichtigt werden.

@rpjohnst Ich habe es festzulegen .

Wir möchten nicht zwei verknüpfte Typen festlegen. Ein Stream ist immer noch nur ein einzelner Typ, nicht impl Iterator<Item=impl Future>> oder ähnliches.

@rpjohnst Ich meinte die zugehörigen Typen Yield und Return von (async) Generatoren

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

Dies war meine ursprüngliche Skizze, aber ich denke, es geht uns zu weit, über Generatoren zu sprechen, zumindest für das Tracking-Problem:

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

Generell denke ich, dass wir mehr Erfahrung mit der Umsetzung haben sollten, bevor wir irgendwelche Entscheidungen im RFC überdenken. Wir kreisen um die gleichen Argumente, die wir bereits vorgebracht haben, wir brauchen Erfahrung mit dem vom RFC vorgeschlagenen Feature, um zu sehen, ob eine Neugewichtung erforderlich ist.

Ich möchte Ihnen voll und ganz zustimmen, aber frage mich nur: Wenn ich Ihren Kommentar richtig gelesen habe, wird die Stabilisierung der async/await-Syntax auf eine anständige Syntax und Implementierung für asynchrone Streams warten und Erfahrungen mit den beiden sammeln? (da es nicht möglich wäre, zwischen äußeren und inneren Rückgabetypen zu wechseln, wenn es sich stabilisiert hat)

Ich dachte, async/await würde für Rust 2018 erwartet und würde nicht hoffen, dass bis dahin Async-Generatoren fertig sind, aber…?

(Auch wurde mein Kommentar nur als zusätzliches Argument zu @MajorBreakfast ‚s bestimmt Blog -

Der enge Anwendungsfall des Schlüsselworts await verwirrt mich immer noch. (Insbes. Future vs. Stream vs. Generator)

Wäre ein Yield-Keyword nicht für alle Anwendungsfälle ausreichend? Wie in

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

Was den Rückgabetyp explizit hält und daher nur ein Schlüsselwort für alle "Fortsetzungs"-basierte Semantik benötigt wird, ohne Schlüsselwort und Bibliothek zu eng miteinander zu verschmelzen.

(Wir haben das übrigens in der Tonsprache gemacht)

@aep await liefert kein Future vom Generator - es pausiert die Ausführung von Future und gibt die Kontrolle an den Aufrufer zurück.

@cramertj Nun, es hätte genau das tun können (ein Future zurückgeben, das die Fortsetzung nach dem Yield-Schlüsselwort enthält), was ein viel breiterer Anwendungsfall ist.
aber ich schätze, ich komme für diese Diskussion etwas zu spät zur Party? :)

@aep Der Grund für ein await -spezifisches Schlüsselwort ist die Zusammensetzbarkeit mit einem zukünftigen Generator-spezifischen yield Schlüsselwort. Wir möchten asynchrone Generatoren unterstützen, und das bedeutet zwei unabhängige Fortsetzungs-"Bereiche".

Außerdem kann es kein Future zurückgeben, das die Fortsetzung enthält, da Rust-Futures poll-basiert und nicht callback-basiert sind, zumindest teilweise aus Gründen der Speicherverwaltung. Für poll es viel einfacher, ein einzelnes Objekt zu mutieren, als für yield , Verweise darauf herumzuwerfen.

Ich denke, async/await sollte kein Schlüsselwort sein, um die Sprache selbst zu verschmutzen, da async nur eine Funktion ist, nicht die interne der Sprache.

@sackery Es ist Teil der Interna der Sprache und kann nicht nur als Bibliothek implementiert werden.

also machen Sie es einfach als Schlüsselwort, genau wie nim, c#'s!

Frage: Wie sollte die Signatur von async non-move Closures sein, die Werte durch veränderliche Referenzen erfassen? Derzeit sind sie einfach komplett verboten. Es scheint, als ob wir eine Art GAT-Ansatz wollen, der es ermöglicht, die Ausleihe der Schließung zu dauern, bis die Zukunft tot ist, z.

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

@cramertj Hier gibt es ein allgemeines Problem mit der Rückgabe veränderlicher Verweise auf die erfasste Umgebung einer Schließung. Eventuell muss die Lösung nicht an async fn gebunden sein?

@Withoutboats richtig, es wird in async Situationen einfach viel häufiger vorkommen, als es wahrscheinlich anderswo der Fall wäre.

Wie wäre es mit fn async statt async fn ?
Mir gefällt let mut besser als mut let .

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

Nachdem Sie pub fn durchsucht haben, können Sie immer noch alle öffentlichen Funktionen im Quellcode finden.aber derzeit ist die Syntax nicht.

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

Dieser Vorschlag ist nicht sehr wichtig, es ist eine Frage des persönlichen Geschmacks.
Also respektiere ich die Meinung zu euch allen :)

Ich glaube, alle Modifikatoren sollten vor fn stehen. Es ist klar und wie es in anderen Sprachen gemacht wird. Es ist nur ein gesunder Menschenverstand.

@Pzixel Ich weiß, dass Zugriffsmodifizierer vor fn weil es wichtig ist.
aber ich denke, async ist es wahrscheinlich nicht.

@xmeta Ich habe diese Idee noch nie vorgeschlagen gesehen. Wir möchten wahrscheinlich async vor fn , um konsistent zu sein, aber ich denke, es ist wichtig, alle Optionen zu berücksichtigen. Danke fürs Schreiben!

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

@MajorBreakfast Danke für deine Antwort, ich dachte so.

{ 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 ist unteilbar, es repräsentiert eine asynchrone Funktion。

async fn ist ein Ganzes.

Sie suchen pub fn ,das bedeutet, dass Sie nach einer öffentlichen Synchronisationsfunktion suchen.
Auf die gleiche Weise durchsuchen Sie pub async fn ,das bedeutet, dass Sie nach einer öffentlichen asynchronen Funktion suchen.

@ZhangHanDong

  • async fn definiert eine normale Funktion, die ein Future zurückgibt. Alle Funktionen, die ein Future zurückgeben, gelten als "asynchron". Die Funktionszeiger von async fn s und anderen Funktionen, die ein Future zurückgeben, sind identisch°. Hier ist ein Spielplatzbeispiel . Eine Suche nach "async fn" kann nur die Funktionen finden, die die Notation verwenden, es werden nicht alle asynchronen Funktionen gefunden.
  • Eine Suche nach pub fn findet keine unsafe oder const Funktionen.

° Der konkrete Typ, der von einem async fn ist natürlich anonym. Ich meine, dass beide einen Typ zurückgeben, der Future implementiert

@xmeta Beachten Sie, dass mut nicht "nach let" geht, oder besser gesagt, dass mut let nicht modifiziert. let nimmt ein Muster, das heißt

let PATTERN = EXPRESSION;

mut ist Teil von PATTERN , nicht von let selbst. Beispielsweise:

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

@steveklabnik Ich verstehe. Ich wollte nur den Zusammenhang zwischen hierarchischer Struktur und Wortstellung zeigen. Danke schön

Was denken die Leute über das gewünschte Verhalten von return und break innerhalb von async Blöcken? Derzeit kehrt return vom asynchronen Block zurück – wenn wir return überhaupt zulassen, ist dies wirklich die einzig mögliche Option. Wir könnten return komplett sperren und etwas wie 'label: async { .... break 'label x; } , um von einem asynchronen Block zurückzukehren. Dies hängt auch mit der Diskussion darüber zusammen, ob das Schlüsselwort break oder return für die Break-to-Blocks-Funktion verwendet werden soll (https://github.com/rust-lang/rust/issues/ 48594).

Ich bin dafür, dass ich return . Das Hauptanliegen, dies zu verbieten, besteht darin, dass es verwirrend sein könnte, da es nicht von der aktuellen Funktion, sondern vom asynchronen Block zurückkehrt. Ich bezweifle jedoch, dass es verwirrend sein wird. Schließungen erlauben bereits return und ich fand es nie verwirrend. Zu lernen, dass return für asynchrone Blöcke gilt, ist IMO einfach und es ist IMO sehr wertvoll.

@cramertj return sollte immer die enthaltende Funktion verlassen, niemals einen inneren Block; Wenn es keinen Sinn macht, dass das funktioniert, was sich so anhört, als ob es nicht funktioniert, dann sollte return überhaupt nicht funktionieren.

Die Verwendung von break dafür scheint unglücklich zu sein, aber da wir leider einen Label-Break-Wert haben, ist es zumindest konsistent damit.

Sind asynchrone Umzüge und Schließungen noch geplant? Folgendes ist aus dem RFC:

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

und weiter unten auf der Seite

async { /* body */ }

// is equivalent to

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

wodurch return mit Verschlüssen ausgerichtet wird und recht einfach zu erfassen und zu erklären scheint.

Plant der Break-to-Block-RFC das Herausspringen eines Innenverschlusses mit einem Etikett? Wenn nicht (und ich behaupte nicht, dass es dies zulassen sollte), wäre es sehr bedauerlich, das konsistente Verhalten von returns zu untersagen, dann verwenden Sie eine Alternative, die auch mit dem rfc von Break-to-Blocks inkonsistent ist.

@memoryruins async || { ... return x; ... } sollte unbedingt funktionieren. Ich sage, dass async { ... return x; ... } nicht sollte, gerade weil async kein Abschluss ist. return hat eine ganz bestimmte Bedeutung: "Rückkehr von der enthaltenden Funktion". Verschlüsse sind eine Funktion. asynchrone Blöcke sind es nicht.

@memoryruins Beides ist bereits implementiert.

@joshtriplett

asynchrone Blöcke sind es nicht.

Ich denke , ich denke immer noch über sie als Funktionen in dem Sinne , dass sie ein Körper mit einem separat definierten Ausführungskontext aus dem Block sind , das sie enthält, so ist es für mich Sinn macht , dass return auf den internen ist async Block. Die Verwirrung scheint hier hauptsächlich syntaktisch zu sein, da Blöcke normalerweise nur Wrapper für einen Ausdruck sind und nicht Dinge, die Code in einen neuen Ausführungskontext bringen, wie es || und async tun.

@cramertj "syntaktisch" ist jedoch wichtig.

Denken Sie so darüber nach. Wenn Sie etwas haben, das nicht wie eine Funktion aussieht (oder wie eine Closure, und Sie es gewohnt sind, Closures als Funktionen zu erkennen) und Sie ein return , wohin denkt Ihr mentaler Parser, dass es hingehört?

Alles, was return entführt, macht es verwirrender, den Code von jemand anderem zu lesen. Die Leute sind zumindest an die Idee gewöhnt, dass break zu einem übergeordneten Block zurückkehrt und sie den Kontext lesen müssen, um zu wissen, welcher Block. return schon immer der größere Hammer, der von der gesamten Funktion zurückkommt.

Wenn sie nicht ähnlich behandelt werden wie sofort bewertete Schließungen, stimme ich zu, dass die Rückgabe dann insbesondere syntaktisch inkonsistent wäre. Wenn ? in asynchronen Blöcken bereits entschieden wurde (der RFC sagt immer noch, dass es unentschieden war), dann stelle ich mir vor, dass es darauf abgestimmt wäre.

@joshtriplett Es fühlt sich für mich willkürlich an zu sagen, dass Sie Funktionen und Schließungen (die syntaktisch sehr unterschiedlich sind) als "Rückgabebereiche" erkennen können, aber asynchrone Blöcke können nicht auf dieselbe Weise erkannt werden. Warum sind zwei verschiedene syntaktische Formen akzeptabel, aber nicht drei?

Es gab einige vorherige Diskussionen zu diesem Thema im RFC . Wie ich dort sagte, befürworte ich asynchrone Blöcke, die break _ohne_ ein Label bereitstellen zu müssen (es gibt keine Möglichkeit, aus dem asynchronen Block in eine äußere Schleife auszubrechen, damit Sie keine Expressivität verlieren).

@Withoutboats Eine Closure ist nur eine andere Art von Funktion; Sobald Sie "ein Abschluss ist eine Funktion" gelernt haben, können Sie alles, was Sie über Funktionen wissen, auf Abschlüsse anwenden, einschließlich " return kehrt immer von der enthaltenden Funktion zurück".

@Nemo157 Auch wenn Sie ein unbeschriftetes break Ziel auf den async Block haben, müssen Sie einen Mechanismus (wie 'label: async ) bereitstellen, um vorzeitig aus einer Schleife innerhalb eines asynchronen Blocks zurückzukehren .

@joshtriplett

Ein Verschluss ist nur eine andere Art von Funktion; Sobald Sie "eine Closure ist eine Funktion" gelernt haben, können Sie alles, was Sie über Funktionen wissen, auf Closures anwenden, einschließlich "return kehrt immer von der enthaltenden Funktion zurück".

Ich denke, async Blöcke sind auch eine Art "Funktion" - eine ohne Argumente, die asynchron bis zum Abschluss ausgeführt werden kann. Sie sind ein Sonderfall von async Verschlüssen, die keine Argumente haben und vorab angewendet wurden.

@cramertj ja, ich ging davon aus, dass jeder implizite Breakpoint bei Bedarf auch beschriftet werden kann (wie ich glaube, dass sie derzeit alle können).

Alles, was das Nachvollziehen des Kontrollflusses erschwert und insbesondere neu definiert, was return bedeutet, belastet die Fähigkeit, Code reibungslos zu lesen, stark.

Entsprechend lautet die Standardanleitung in C "keine Makros schreiben, die aus der Mitte des Makros zurückkehren". Oder als weniger verbreiteter, aber immer noch problematischer Fall: Wenn Sie ein Makro schreiben, das wie eine Schleife aussieht, sollten break und continue funktionieren. Ich habe gesehen, wie Leute Schleifenmakros schreiben, die tatsächlich zwei Schleifen einbetten, also funktioniert break nicht wie erwartet, und das ist extrem verwirrend.

Ich denke, asynchrone Blöcke sind auch eine Art "Funktion"

Ich denke, das ist eine Perspektive, die auf der Kenntnis der Interna der Implementierung basiert.

Ich sehe sie überhaupt nicht als Funktionen.

Ich sehe sie überhaupt nicht als Funktionen.

@joshtriplett

Mein Verdacht ist, dass Sie das gleiche Argument vorgebracht hätten, wenn Sie zum ersten Mal zu einer Sprache mit Closures kamen – dass return nicht innerhalb der Closure, sondern innerhalb der definierenden Funktion funktionieren sollte. Und tatsächlich gibt es Sprachen, die diese Interpretation übernehmen, wie Scala.

@cramertj würde ich nicht, nein; Für Lambdas und/oder Funktionen, die innerhalb einer Funktion definiert sind, fühlt es sich ganz natürlich an, dass es sich um eine Funktion handelt. (Mein erster Kontakt mit diesen war in Python, FWIW, wo Lambdas return nicht verwenden können und in verschachtelten Funktionen return von der Funktion zurückgibt, die return .)

Ich denke, wenn man einmal weiß, was ein asynchroner Block tut, ist intuitiv klar, wie sich return verhalten muss. Sobald Sie wissen, dass es sich um eine verzögerte Ausführung handelt, ist klar, dass return nicht auf die Funktion angewendet werden kann. Es ist klar, dass die Funktion bereits zurückgekehrt ist, wenn der Block ausgeführt wird. IMO sollte dies keine große Herausforderung sein. Wir sollten es zumindest ausprobieren und sehen.

Dieser RFC schlägt nicht vor, wie der ? -Operator und Kontrollflusskonstrukte wie return , break und continue in asynchronen Blöcken funktionieren sollten.

Wäre es am besten, Kontrollflussoperatoren zu verbieten oder Blöcke zu verschieben, bis ein dedizierter RFC geschrieben wurde? Es wurden andere gewünschte Merkmale erwähnt, die später erörtert werden sollten. In der Zwischenzeit werden wir asynchrone Funktionen, Closures und await! :)

Ich stimme @memoryruins hier zu, ich denke, es wäre sinnvoll, einen weiteren RFC zu erstellen, um diese Besonderheiten genauer zu besprechen.

Was halten Sie von einer Funktion, die uns den Zugriff auf den Kontext innerhalb einer asynchronen Fn ermöglicht, die vielleicht core::task::context() ? Es würde einfach in Panik geraten, wenn es von außerhalb eines asynchronen Fn aufgerufen würde. Ich denke, das wäre ganz praktisch, um zB auf den Executor zuzugreifen, um etwas zu spawnen.

@MajorBreakfast diese Funktion heißt lazy

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

Für etwas Spezifischeres wie das Laichen wird es wahrscheinlich Hilfsfunktionen geben, die es ergonomischer machen

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

@Nemo157 Eigentlich würde ich das gerne in spawn_with_handle verwenden. Beim Konvertieren des Codes in 0.3 ist mir aufgefallen, dass spawn_with_handle eigentlich nur eine Zukunft ist, weil es Zugriff auf den Kontext benötigt ( siehe Code ). Was ich tun möchte, ist, eine spawn_with_handle Methode zu ContextExt hinzuzufügen und spawn_with_handle einer kostenlosen Funktion zu machen, die nur innerhalb von asynchronen Funktionen funktioniert:

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

Dies würde all den doppelten Unsinn beseitigen, den wir derzeit haben.

Wenn Sie es sich vorstellen, müsste die Methode core::task::with_current_context() heißen und etwas anders funktionieren, da es unmöglich sein muss, eine Referenz zu speichern.

Edit: Diese Funktion existiert bereits unter dem Namen get_task_cx . Es befindet sich derzeit aus technischen Gründen in libstd. Ich schlage vor, es zur öffentlichen API zu machen, sobald es in libcore eingefügt werden kann.

Ich bezweifle, dass es möglich sein wird, eine Funktion zu haben, die von einer Nicht- async Funktion aufgerufen werden kann, die Ihnen den Kontext einer übergeordneten async Funktion liefern könnte, sobald sie aus TLS verschoben wurde. An diesem Punkt wird der Kontext wahrscheinlich wie eine versteckte lokale Variable in der Funktion async , sodass Sie ein Makro haben könnten, das direkt auf den Kontext in dieser Funktion zugreift, aber es gäbe keine Möglichkeit, spawn_with_handle zieht den Kontext auf magische Weise aus seinem Aufrufer.

Also möglicherweise so etwas wie

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 Ich denke, Sie haben Recht: Eine Funktion, wie ich sie vorschlage, könnte wahrscheinlich nicht funktionieren, wenn sie nicht direkt aus dem asynchronen Fn aufgerufen wird. Vielleicht ist der beste Weg, spawn_with_handle einem Makro zu machen, das intern await (wie select! und join! ):

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

Das sieht nett aus und kann einfach über await!(lazy(|ctx| { ... })) innerhalb des Makros implementiert werden.

async_context!() ist problematisch, weil es mich nicht daran hindern kann, die Kontextreferenz über Waitpoints hinweg zu speichern.

async_context!() ist problematisch, weil es mich nicht daran hindern kann, die Kontextreferenz über Waitpoints hinweg zu speichern.

Je nach Ausführung kann dies der Fall sein. Wenn vollständige Generatorargumente wiederbelebt werden, müssen sie begrenzt werden, sodass Sie sowieso keine Referenz über die Fließgrenze hinweg beibehalten können, der Wert hinter dem Argument hätte eine Lebensdauer, die nur bis zur Fließgrenze läuft. Async/await würde diese Einschränkung nur erben.

@Nemo157 Meinst du sowas?

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

@Pzixel Tut

Ja, ich mag es, dass die Syntax von await!() Mehrdeutigkeiten beseitigt, wenn sie mit Dingen wie ? kombiniert wird, aber ich stimme auch zu, dass diese Syntax nervig ist, wenn man sie in einem einzigen Projekt tausendmal eintippt. Ich glaube auch, dass es laut ist und sauberer Code wichtig ist.

Deshalb frage ich mich, was das eigentliche Argument gegen ein angehängtes Symbol ist (das schon ein paar Mal erwähnt wurde), wie something_async()@ Vergleich zu etwas mit await , vielleicht weil await ist ein bekanntes Schlüsselwort aus anderen Sprachen? Das @ könnte lustig sein, da es einem a von await ähnelt, aber es kann jedes beliebige Symbol sein, das gut passen würde.

Ich würde argumentieren, dass eine solche Syntaxwahl logisch wäre, da etwas Ähnliches mit try!() passiert ist, das im Grunde ein Suffix mit ? (ich weiß, dass dies nicht genau dasselbe ist). Es ist prägnant, leicht zu merken und leicht zu tippen.

Eine weitere tolle Sache an einer solchen Syntax ist, dass das Verhalten sofort klar ist, wenn es mit dem ? Symbol kombiniert wird (zumindest glaube ich, dass es so wäre). Schauen Sie sich Folgendes an:

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

Dies hat nicht das Problem wie bei await future? , wo auf den ersten Blick nicht klar ist, was passieren wird, es sei denn, Sie kennen eine solche Situation. Und doch stimmt die Implementierung mit ? überein.

Nun, mir fallen nur ein paar _kleinere_ Dinge ein, die dieser Idee widersprechen würden:

  • vielleicht ist es _zu_ prägnant und weniger sichtbar/ausführlich im Gegensatz zu await , was es _schwierig_ macht, Aufhängungspunkte in einer Funktion zu erkennen.
  • Vielleicht ist es asymmetrisch mit dem Schlüsselwort async , wobei eines ein Schlüsselwort und das andere ein Symbol ist. await!() leidet jedoch unter dem gleichen Problem, das ein Schlüsselwort gegenüber einem Makro ist.
  • Die Auswahl eines Symbols fügt noch ein weiteres Syntaxelement hinzu, und es gibt etwas zu lernen. Aber unter der Annahme, dass dies etwas allgemein verwendet werden könnte, denke ich nicht, dass dies ein Problem ist.

@phaux erwähnte auch die Verwendung des ~ Symbols. Ich glaube jedoch, dass dieses Zeichen auf einigen Tastaturlayouts funky zu tippen ist, daher würde ich empfehlen, diese Idee fallen zu lassen.

Was sind eure Gedanken Jungs? Findest du es ähnlich wie try!() _became_ ? ? Bevorzugen Sie await oder ein Symbol und warum? Bin ich verrückt, darüber zu diskutieren, oder übersehe ich vielleicht etwas?

Entschuldigung für die falsche Terminologie, die ich möglicherweise verwendet habe.

Die größte Sorge, die ich bei der siegelbasierten Syntax habe, ist, dass sie sich leicht in Glyphensuppe verwandeln kann, wie Sie freundlicherweise demonstriert haben. Jeder, der mit Perl (vor 6) vertraut ist, wird verstehen, wohin ich damit gehe. Das Vermeiden von Leitungsrauschen ist der beste Weg, um voranzukommen.

Das heißt, vielleicht ist der beste Weg, genau wie bei try! ? Das heißt, beginnen Sie mit einem expliziten async!(foo) Makro und fügen Sie bei Bedarf ein Siegel hinzu, das Zucker dafür wäre. Sicher, das verschiebt das Problem auf später, aber async!(foo) ist für eine erste Iteration von async/await ausreichend, mit dem Vorteil, dass es relativ unumstritten ist. (und der Präzedenzfall try! / ? sollte aus einem Siegel hervorgehen)

@ withoutboats Ich habe diesen Thread nicht ganz gelesen, aber hilft jemand bei der Implementierung? Wo ist Ihr Entwicklungszweig?

Und hat in Bezug auf die verbleibenden ungelösten Überlegungen jemand um Hilfe von Experten außerhalb der Rust-Community gebeten? Joe Duffy kennt und interessiert sich sehr für Parallelität und versteht die fummeligen Details ziemlich gut , und er hat eine Keynote auf der RustConf gehalten , daher vermute ich, dass er einer Bitte um Anleitung zugänglich sein könnte, wenn eine solche Anfrage gerechtfertigt ist.

@BatmanAoD eine erste Implementierung wurde in https://github.com/rust-lang/rust/pull/51580 gelandet

Der ursprüngliche RFC-Thread enthielt Kommentare von einer Reihe von Experten im PLT-Bereich, sogar außerhalb von Rust :)

Ich möchte das '$'-Symbol für das Warten auf Futures vorschlagen, denn Zeit ist Geld, und ich möchte den Compiler daran erinnern.

Nur ein Scherz. Ich glaube nicht, dass es eine gute Idee ist, ein Symbol für das Warten zu haben. Bei Rust geht es darum, explizit zu sein und es den Leuten zu ermöglichen, Low-Level-Code in einer mächtigen Sprache zu schreiben, die es einem nicht zulässt, sich selbst in den Fuß zu schießen. Ein Symbol ist viel ungenauer als ein await! Makro und ermöglicht es den Leuten, sich auf andere Weise in den Fuß zu schießen, indem sie schwer lesbaren Code schreiben. Ich würde bereits argumentieren, dass ? ein Schritt zu weit ist.

Ich bin auch nicht damit einverstanden, dass es ein async Schlüsselwort gibt, das in der Form async fn . Es impliziert eine Art "Voreingenommenheit" in Richtung Asynchronität. Warum verdient async ein Schlüsselwort? Asynchroner Code ist für uns nur eine weitere Abstraktion, die nicht immer notwendig ist. Ich denke, ein asynchrones Attribut verhält sich eher wie eine "Erweiterung" des grundlegenden Rust-Dialekts, die es uns ermöglicht, leistungsfähigeren Code zu schreiben.

Ich bin kein Spracharchitekt, aber ich habe ein wenig Erfahrung mit dem Schreiben von asynchronem Code mit Promises in JavaScript, und ich denke, die Art und Weise, wie es dort gemacht wird, macht das Schreiben von asynchronem Code zu einem Vergnügen.

@steveklabnik Ah, okay, danke. Können ( / soll ich) die Problembeschreibung aktualisieren? Vielleicht sollte der Aufzählungspunkt "Erstimplementierung" in "Implementierung ohne move Unterstützung" und "Vollständige Implementierung" unterteilt werden?

Wird an der nächsten Implementierungsiteration in einem öffentlichen Zweig/Zweig gearbeitet? Oder geht das erst, wenn RFC 2418 akzeptiert wird?

Warum wird das Problem der async/await-Syntax hier und nicht in einem RFC diskutiert?

@c-edw Ich denke, die Frage zum Schlüsselwort async wird beantwortet durch Welche Farbe ist Ihre Funktion?

@parasyte Mir wurde vorgeschlagen, dass dieser Beitrag tatsächlich ein Argument gegen die gesamte Idee von asynchronen Funktionen ohne automatisch verwaltete Parallelität im Green-Thread-Stil ist.

Ich stimme dieser Position nicht zu, da grüne Threads ohne eine (verwaltete) Laufzeit nicht transparent implementiert werden können und es für Rust gute Gründe gibt, asynchronen Code zu unterstützen, ohne dies zu erfordern.

Aber es scheint, dass Sie den Beitrag lesen, dass die Semantik von async / await in Ordnung ist, aber es gibt eine Schlussfolgerung über das Schlüsselwort? Würde es Ihnen etwas ausmachen, das auszuweiten?

Auch da stimme ich deiner Ansicht zu. Ich habe kommentiert, dass das Schlüsselwort async notwendig ist, und der Artikel erläutert die Gründe dafür. Anders die Schlussfolgerungen des Autors.

@parasyte Ah, okay. Ich bin froh, dass ich gefragt habe - wegen der Abneigung des Autors gegen Rot-Blau-Dichotomien dachte ich, Sie sagen das Gegenteil!

Ich möchte das noch klarstellen, da ich das Gefühl habe, dass ich dem nicht ganz gerecht wurde.

Die Dichotomie ist unausweichlich . Einige Projekte haben versucht, sie zu löschen, indem sie jeden Funktionsaufruf asynchron machten, wodurch erzwungen wurde, dass keine Synchronisierungsfunktionsaufrufe vorhanden sind. Midori ist ein offensichtliches Beispiel. Und andere Projekte haben versucht, die Dichotomie aufzulösen, indem sie die asynchronen Funktionen hinter der Fassade der Synchronisierungsfunktionen versteckt haben. gevent ist ein Beispiel dieser Art.

Beide haben das gleiche Problem; sie brauchen immer noch die Dichotomie, um zwischen dem Warten auf den Abschluss einer asynchronen Task und dem asynchronen Starten einer Task zu unterscheiden !

  • Midori hat nicht nur das Schlüsselwort await , sondern auch ein Schlüsselwort async auf der Funktionsaufruf-Site eingeführt .
  • gevent bietet zusätzlich zum impliziten Warten von normal aussehenden Funktionsaufrufen gevent.spawn .

Das war der Grund, warum ich den Color-a-Function-Artikel aufgerufen habe, da er die Frage beantwortet: "Warum verdient Async ein Schlüsselwort?"

Nun, sogar Thread-basierter synchroner Code kann "Warten auf den Abschluss einer Aufgabe" (Join) und "Starten einer Aufgabe" (Spawn) unterscheiden. Sie können sich eine Sprache vorstellen, in der alles asynchron ist (implementierungstechnisch), aber es gibt keine Anmerkung zu await (da dies das Standardverhalten ist) und Midoris async ist stattdessen eine Closure, die an ein spawn API. Dies stellt All-Async auf genau die gleiche syntaktische/Funktionsfarbbasis wie All-Sync.

Während ich also zustimme, dass async ein Schlüsselwort verdient, scheint mir dies eher daran zu liegen, dass Rust sich um den Implementierungsmechanismus auf dieser Ebene kümmert und aus diesem Grund beide Farben bereitstellen muss.

@rpjohnst Ja, ich habe deine Vorschläge gelesen. Es ist konzeptionell dasselbe wie das Verstecken der Farben à la gevent. Was ich im Rostforum im selben Thread kritisiert habe; Jeder Funktionsaufruf sieht synchron aus, was eine besondere Gefahr darstellt, wenn eine Funktion sowohl synchron ist als auch in einer asynchronen Pipeline blockiert. Diese Art von Fehler ist unvorhersehbar und eine echte Katastrophe für die Fehlerbehebung.

Ich spreche nicht über meinen Vorschlag im Besonderen, ich spreche von einer Sprache, in der alles asynchron ist. Sie können die Dichotomie entkommen , dass mein Vorschlag Weg- bedeutet das nicht versuchen.

IIUC ist genau das, was Midori versucht hat. An diesem Punkt ist Keywords vs Closures nur eine Argumentation der Semantik.

Am Do, 12. Juli 2018 um 15:01 Uhr, Russell Johnston [email protected]
schrieb:

Sie haben das Vorhandensein von Schlüsselwörtern als Argument dafür verwendet, warum die Dichotomie
existiert noch in Midori. Wenn Sie sie entfernen, wo ist die Dichotomie? Der
Syntax ist identisch mit All-Sync-Code, aber mit den Fähigkeiten von Async
Code.

Denn wenn Sie eine asynchrone Funktion aufrufen, ohne auf ihr Ergebnis zu warten, wird es
gibt synchron ein Promise zurück. Was später erwartet werden kann. 😐

_Wow, weiß jemand etwas über Midori? Ich dachte immer, dass es ein geschlossenes Projekt ist, an dem fast keine lebenden Kreaturen arbeiten. Es wäre interessant, wenn jemand von euch ausführlicher darüber schreiben würde._

/offtopic

@Pzixel kein lebendes Wesen arbeitet noch daran, weil das Projekt geschlossen wurde. Aber Joe Duffys Blog enthält viele interessante Details. Siehe meine Links oben.

Wir sind hier aus den Fugen geraten, und ich habe das Gefühl, ich wiederhole mich, aber das ist ein Teil der "Präsenz von Schlüsselwörtern" - dem Schlüsselwort await . Wenn Sie die Schlüsselwörter durch APIs wie spawn und join ersetzen, können Sie vollständig asynchron sein (wie Midori), aber ohne Dichotomie (im Gegensatz zu Midori).

Oder anders gesagt, wie ich schon sagte, es ist nicht grundlegend – wir haben es nur, weil wir die Wahl haben wollen .

@CyrusNajmabadi Entschuldigung, dass ich Sie noch einmal hier sind einige zusätzliche Informationen zur Entscheidungsfindung.

Wenn Sie nicht noch einmal erwähnt werden möchten, sagen Sie es mir bitte. Ich dachte nur, Sie könnten interessiert sein.

Aus dem #wg-net Discord-Kanal :

@cramertj
Denkanstoß: Ich schreibe oft Ok::<(), MyErrorType>(()) am Ende von async { ... } Blöcken. Vielleicht können wir uns etwas einfallen lassen, um die Einschränkung des Fehlertyps zu vereinfachen?

@ohneboote
[...] möglicherweise wollen wir, dass es mit [ try ] übereinstimmt?

(Ich erinnere mich an eine relativ neue Diskussion darüber, wie try Blöcke ihren Rückgabetyp deklarieren könnten, aber ich kann sie jetzt nicht finden ...)

genannte Farbtöne:

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

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

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

Eine Sache, die try tun kann, was mit async weniger ergonomisch ist, ist die Verwendung einer variablen Bindung oder Typzuordnung, z

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

Ich hatte zuvor die Idee, eine fn-ähnliche Syntax für das Merkmal Future zuzulassen, z. B. Future -> io::Result<()> . Das würde die manuelle Eingabeoption etwas besser aussehen lassen, obwohl es immer noch viele Zeichen sind:

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

wäre meine wahl.

Sie ähnelt der bestehenden Closure-Syntax :

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

Edit: Und schließlich , wenn es möglich ist für TryFuture implementieren Future :

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

Edit2: Um genau zu sein, würde das obige mit den heutigen Merkmalsdefinitionen funktionieren. Es ist nur so, dass ein TryFuture Typ heute nicht so nützlich ist, weil er derzeit Future nicht implementiert

@MajorBreakfast Warum -> impl Future<Output = io::Result<()>> statt -> io::Result<()> ? Wir machen bereits die Entzuckerung vom Rückgabetyp für async fn foo() -> io::Result<()> , also IMO, wenn wir eine -> -basierte Syntax verwenden, scheint es klar zu sein, dass wir hier den gleichen Zucker wollen.

@cramertj Ja, es sollte konsistent sein. Mein Beitrag oben geht irgendwie davon aus, dass ich Sie alle davon überzeugen kann, dass der Ansatz des äußeren Renditetyps überlegen ist 😁

Falls wir async -> R { .. } , sollten wir vermutlich auch try -> R { .. } sowie expr -> TheType im Allgemeinen für die Typzuordnung verwenden. Mit anderen Worten, die von uns verwendete Typzuordnungssyntax sollte überall einheitlich angewendet werden.

@Centril Ich stimme zu. Es soll überall einsetzbar sein. Ich bin mir nur nicht mehr sicher, ob -> wirklich die richtige Wahl ist. Ich verbinde -> damit, anrufbar zu sein. Und asynchrone Blöcke sind es nicht.

@MajorBreakfast Ich stimme grundsätzlich zu; Ich denke, wir sollten : für die Typzuordnung verwenden, also async : Type { .. } , try : Type { .. } und expr : Type . Wir haben die potenziellen Unklarheiten auf Discord diskutiert und ich denke, wir haben mit : einen Weg gefunden, der Sinn macht...

Eine andere Frage betrifft die Aufzählung von Either . Wir haben bereits Either in einer futures Kiste. Es ist auch verwirrend, weil es genauso aussieht wie Either aus einer either Kiste, wenn dies nicht der Fall ist.

Da Futures in std zusammengeführt zu sein scheint (zumindest sehr grundlegende Teile davon), könnten wir dort auch Either einfügen? Es ist wichtig, sie zu haben, um impl Future von der Funktion zurückgeben zu können.

Zum Beispiel schreibe ich oft Code wie den folgenden:

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

Ich würde es gerne so schreiben können:

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

Aber wie ich verstehe, wenn async erweitert wird, muss hier Either eingefügt werden, weil wir entweder in eine Bedingung gehen oder nicht.

_Wenn Sie möchten, finden Sie hier den aktuellen Code.

@Pzixel Sie brauchen Either innerhalb von async Funktionen, solange Sie await die Futures haben, dann wird die Codetransformation, die async , diese beiden verbergen Typen intern und präsentieren dem Compiler einen einzigen Rückgabetyp.

@Pzixel Außerdem hoffe ich (persönlich), dass Either mit diesem RFC nicht eingeführt wird, da dies eine eingeschränkte Version von https://github.com/rust-lang/rfcs/issues einführen würde Future s), wodurch wahrscheinlich API-Cruft hinzugefügt wird, wenn jemals eine allgemeine Lösung zusammengeführt wird -- und wie @Nemo157 erwähnt, scheint dies kein Notfall zu sein habe gerade Either :)

@Ekleog sicher, ich wurde gerade von dieser Idee getroffen, dass ich tatsächlich Tonnen von either in meinem vorhandenen asynchronen Code habe und ich sie wirklich gerne loswerden möchte. Dann erinnerte ich mich an meine Verwirrung, als ich ~ eine halbe Stunde damit verbrachte, bis mir klar wurde, dass es nicht kompiliert werden kann, weil ich eine either Kiste in Abhängigkeiten hatte (zukünftige Fehler sind ziemlich schwer zu verstehen, daher dauerte es ziemlich lange). Deshalb habe ich den Kommentar geschrieben, um sicherzugehen, dass dieses Problem irgendwie angegangen wird.

Natürlich bezieht sich dies nicht nur auf async/await , sondern ist allgemeiner und verdient daher einen eigenen RFC. Ich wollte nur betonen, dass entweder futures über either Bescheid wissen sollte oder umgekehrt (um IntoFuture richtig zu implementieren).

@Pzixel Das von der Futures-Kiste exportierte Either ist ein Reexport aus der either Kiste. Die futures Crate 0.3 kann Future für Either wegen der Waisenregeln nicht implementieren. Es ist sehr wahrscheinlich, dass wir aus Konsistenzgründen auch die Impls Stream und Sink für Either entfernen und stattdessen eine Alternative anbieten ( hier diskutiert). Außerdem könnte die either Kiste dann Future , Stream und Sink selbst implementieren, wahrscheinlich unter einem Feature-Flag.

Wie @Nemo157 bereits erwähnt hat, ist es bei der Arbeit mit Futures jedoch besser, nur asynchrone Funktionen anstelle von Either .

Das async : Type { .. } Zeug wird jetzt in https://github.com/rust-lang/rfcs/pull/2522 vorgeschlagen

Sind Async/Await-Funktionen, die Send automatisch implementieren, bereits implementiert?

Es sieht so aus, als ob die folgende asynchrone Funktion (noch?) nicht Send :

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

    await!(chan.next());
}

Link zum vollständigen Reproduzierer (der aus Mangel an futures-0.3 nicht auf dem Spielplatz kompiliert wird, denke ich) ist hier .

Außerdem stieß ich bei der Untersuchung dieses Problems auf https://github.com/rust-lang/rust/issues/53249, das meiner Meinung nach in die Tracking-Liste des obersten Beitrags aufgenommen werden sollte :)

Hier ist eine Spielwiese, die zeigt, dass async/await-Funktionen, die Send implementieren, funktionieren _sollten_. Das Entkommentieren der Rc Version erkennt diese Funktion korrekt als Nicht- Send . Ich kann mir Ihr spezifisches Beispiel in Kürze ansehen (kein Rust-Compiler auf diesem Computer :slightly_frowning_face:), um herauszufinden, warum es nicht funktioniert.

@Ekleog std::mpsc::Receiver ist nicht Sync , und das async fn Sie geschrieben haben, enthält einen Verweis darauf. Verweise auf !Sync Elemente sind !Send .

@cramertj Hmm … aber eigenes mpsc::Receiver , das Send wenn der generische Typ Send ? (Außerdem ist es kein std::mpsc::Receiver sondern ein futures::channel::mpsc::Receiver , was auch Sync ist, wenn der Typ Send ist. Entschuldigung, dass ich mpsc::Receiver nicht bemerkt habe

@Nemo157 Danke! Ich habe https://github.com/rust-lang/rust/issues/53259 geöffnet, um zu viel Lärm bei diesem Thema zu vermeiden :)

Die Frage, ob und wie async Blöcke erlauben ? und anderer Kontrollfluss könnte einige Interaktion mit rechtfertigen try Blöcken (zB try async { .. } , damit ? ohne ähnliche Verwirrung wie return ?).

Dies bedeutet, dass der Mechanismus zum Festlegen des Typs eines async Blocks möglicherweise mit dem Mechanismus zum Festlegen des Typs eines try Blocks interagieren muss. Ich habe einen Kommentar zur RFC-Zuordnungssyntax hinterlassen: https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Klicken Sie einfach auf das, was ich zuerst für ein futures-rs Problem hielt, aber es stellt sich heraus, dass es sich möglicherweise tatsächlich um ein asynchrones / wartendes Problem handelt, also hier ist es: https://github.com/rust-lang-nursery/ futures-rs/issues/1199#issuecomment -413089012

Wie vor einigen Tagen auf Discord besprochen, wurde await noch nicht als Schlüsselwort reserviert. Es ist ziemlich wichtig, diese Reservierung vor der Veröffentlichung von 2018 einzutragen (und zum Schlüsselwort lint der Ausgabe 2018 hinzuzufügen). Es ist eine etwas komplizierte Reservierung, da wir vorerst weiterhin die Makrosyntax verwenden möchten.

Wird die Future/Task API eine Möglichkeit haben, lokale Futures hervorzubringen?
Ich sehe, dass es SpawnLocalObjError , aber es scheint unbenutzt zu sein.

@panicbit In der Arbeitsgruppe diskutieren wir derzeit, ob es überhaupt Sinn macht, Spawning-Funktionalität in den Kontext https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError ist nicht ganz ungenutzt: LocalPool der Futures-Kiste verwendet es. Sie haben jedoch Recht, dass es in libcore nicht verwendet wird)

@ withoutboats Mir ist aufgefallen, dass einige der Links in der https://github.com/rust-lang/rfcs/pull/2418 wurde geschlossen und https://github.com/rust-lang-nursery/futures-rs/issues/1199 wurde nach https verschoben

Achtung. Der Name dieses Tracking-Problems ist async/await, aber es ist auch der Aufgaben-API zugewiesen! Für die Aufgaben-API ist derzeit ein Stabilisierungs-RFC ausstehend: https://github.com/rust-lang/rfcs/pull/2592

Gibt es eine Möglichkeit, die Schlüsselwörter für alternative asynchrone Implementierungen wiederverwendbar zu machen? Derzeit erstellt es eine Zukunft, aber es ist eine Art verpasste Gelegenheit, um Push-basiertes Async besser nutzbar zu machen.

@aep Es ist möglich, einfach von einem Push-basierten System in das Pull-basierte Future-System zu konvertieren, indem Sie oneshot::channel .

JavaScript Promises sind beispielsweise push-basiert, daher verwendet stdweb oneshot::channel , um JavaScript Promises in Rust Futures umzuwandeln . Es verwendet auch oneshot::channel für einige andere Push-basierte Callback-APIs, wie setTimeout .

Aufgrund des Speichermodells von Rust haben Push-basierte Futures im Vergleich zu Pull zusätzliche Leistungskosten . Daher ist es besser, diese Leistungskosten nur bei Bedarf zu bezahlen (zB durch die Verwendung von oneshot::channel ), anstatt das gesamte Future-System auf Push-Basis zu betreiben.

Allerdings gehöre ich nicht zu den Kern- oder Lang-Teams, daher hat nichts, was ich sage, irgendeine Autorität. Es ist nur meine persönliche Meinung.

Bei ressourcenbeschränktem Code ist es genau umgekehrt. Pull-Modelle haben einen Nachteil, weil Sie die Ressource innerhalb des Dings benötigen, das gezogen wird, anstatt den nächsten Ready-Wert durch einen Stapel wartender Funktionen zu füttern. Das Design von futures.rs ist einfach zu teuer für alles, was in der Nähe von Hardware liegt, wie zum Beispiel Netzwerk-Switches (mein Anwendungsfall) oder Spiele-Renderer.

In diesem Fall müssten wir hier jedoch nur async veranlassen, etwas wie Generator auszugeben. Wie ich bereits sagte, denke ich, dass Async und Generatoren tatsächlich dasselbe sind, wenn Sie es ausreichend abstrahieren, anstatt zwei Schlüsselwörter an eine einzige Bibliothek zu binden.

In diesem Fall müssten wir hier jedoch nur async veranlassen, etwas wie Generator auszugeben.

async an dieser Stelle buchstäblich ein minimaler Wrapper um ein Generatorliteral. Ich habe Schwierigkeiten zu sehen, wie Generatoren bei Push-basierter asynchroner E/A helfen. Brauchen Sie dafür nicht eher eine CPS-Transformation?

Könnten Sie genauer sagen, was Sie mit "Sie benötigen die Ressourcen in dem Ding, das abgerufen wird" meinen? Ich bin mir nicht sicher, warum Sie das brauchen oder wie sich "den nächsten bereiten Wert durch einen Stapel von Wartefunktionen füttern" von poll() .

Ich hatte den Eindruck, dass Push-basierte Futures teurer sind (und daher in eingeschränkten Umgebungen schwieriger zu verwenden sind). Das Zulassen, dass beliebige Callbacks an einen Future angehängt werden, erfordert eine Form der Indirektion, normalerweise eine Heap-Zuweisung. Anstatt also einmal mit dem Root-Future zuzuweisen, weisen Sie jedem Kombinator zu. Außerdem wird das Abbrechen aufgrund von Threadsicherheitsproblemen komplexer, sodass Sie es entweder nicht unterstützen oder alle Callback-Vervollständigungen benötigen, um atomare Operationen zu verwenden, um Rennen zu vermeiden. All dies summiert sich zu einem viel schwieriger zu optimierenden Framework, soweit ich das beurteilen kann.

Brauchen Sie dafür nicht eher eine CPS-Transformation?

Ja, die aktuelle Generator-Syntax funktioniert dafür nicht, weil sie keine Argumente für die Fortsetzung hat, weshalb ich gehofft hatte, dass Async die Möglichkeit bieten würde, dies zu tun.

Sie brauchen die Ressourcen in dem Ding, das gezogen wird?

Das ist meine schreckliche Art zu sagen, dass das Umkehren der asynchronen Reihenfolge zweimal Kosten verursacht hat. Dh einmal von Hardware zu Futures und wieder zurück über Kanäle. Sie müssen eine ganze Reihe von Dingen mit sich führen, die in fast hardwarenahem Code keine Vorteile haben.

Ein gängiges Beispiel wäre, dass Sie den Future-Stack nicht einfach aufrufen können, wenn Sie wissen, dass ein Dateideskriptor eines Sockets bereit ist, sondern stattdessen die gesamte Ausführungslogik implementieren müssen, um reale Ereignisse auf Futures abzubilden, was externe Kosten wie Sperren, Codegröße und vor allem Codekomplexität.

Um willkürliche Callbacks an einen Future anhängen zu können, ist eine Art Umweg erforderlich

Ja, ich verstehe, dass Callbacks in einigen Umgebungen teuer sind (nicht in meiner, wo die Ausführungsgeschwindigkeit irrelevant ist, aber ich habe 1 MB Gesamtspeicher, also passt futures.rs nicht einmal auf Flash), aber Sie brauchen überhaupt keinen dynamischen Versand, wenn Sie haben so etwas wie Fortsetzungen (die das aktuelle Generatorkonzept zur Hälfte implementiert).

Und auch die Stornierung wird aufgrund der Thread-Sicherheit komplexer

Ich glaube, wir verwechseln hier die Dinge. Ich plädiere nicht für Rückrufe. Fortsetzungen können ganz gut statische Stapel sein. Was wir zum Beispiel in der Clay-Sprache implementiert haben, ist nur ein Generatormuster, das Sie für Push oder Pull verwenden können. dh:

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

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

Ich denke, ich kann das einfach mit einem Makro machen, aber ich habe das Gefühl, dass es hier eine verpasste Gelegenheit ist, ein Sprachschlüsselwort für ein bestimmtes Konzept zu verschwenden.

nicht in meinem, wo die Ausführungsgeschwindigkeit irrelevant ist, aber ich habe 1 MB Gesamtspeicher, also passt futures.rs nicht einmal auf Flash

Ich bin mir ziemlich sicher, dass die aktuellen Futures in Umgebungen mit eingeschränktem Speicher ausgeführt werden sollen. Was genau nimmt so viel Platz ein?

Bearbeiten: Dieses Programm benötigt 295 KB Festplattenspeicher, wenn es kompiliert wird --release auf meinem Macbook (grundlegende Hello World benötigt 273 KB):

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

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

nicht in meinem, wo die Ausführungsgeschwindigkeit irrelevant ist, aber ich habe 1 MB Gesamtspeicher, also passt futures.rs nicht einmal auf Flash

Ich bin mir ziemlich sicher, dass die aktuellen Futures in Umgebungen mit eingeschränktem Speicher ausgeführt werden sollen. Was genau nimmt so viel Platz ein?

Und was meinst du mit Gedächtnis? Ich habe aktuellen async/await-basierten Code auf Geräten mit 128 kB Flash/16 kB RAM ausgeführt. Es gibt derzeit definitiv Probleme mit der Speichernutzung bei async/await, aber das sind hauptsächlich Implementierungsprobleme und können durch Hinzufügen einiger zusätzlicher Optimierungen verbessert werden (zB https://github.com/rust-lang/rust/issues/52924).

Ein gängiges Beispiel wäre, dass Sie den Future-Stack nicht einfach aufrufen können, wenn Sie wissen, dass ein Dateideskriptor eines Sockets bereit ist, sondern stattdessen die gesamte Ausführungslogik implementieren müssen, um reale Ereignisse auf Futures abzubilden, was externe Kosten wie Sperren, Codegröße und vor allem Codekomplexität.

Wieso den? Dies scheint immer noch nichts zu sein, wozu die Zukunft Sie zwingt. Sie können poll genauso einfach aufrufen wie einen Push-basierten Mechanismus.

Und was meinst du mit Gedächtnis?

Ich glaube nicht, dass dies relevant ist. Diese ganze Diskussion hat einen Punkt entkräftet, den ich nicht einmal vorbringen wollte. Ich bin nicht hier, um Futures zu kritisieren, außer zu sagen, dass es ein Fehler ist, ihr Design in die Kernsprache zu integrieren.

Mein Punkt ist, dass das async-Schlüsselwort zukunftssicher gemacht werden kann, wenn es richtig gemacht wird. Fortsetzungen sind das, was ich will, aber vielleicht haben andere Leute noch bessere Ideen.

Sie können Poll genauso einfach aufrufen wie einen Push-basierten Mechanismus.

Ja, das würde Sinn machen, wenn Future:poll Aufrufargumente hätte. Es kann sie nicht haben, weil die Umfrage abstrakt sein muss. Stattdessen schlage ich vor, eine Fortsetzung aus dem async-Schlüsselwort auszugeben und Future für jede Fortsetzung mit null Argumenten zu implizieren.

Es handelt sich um eine einfache Änderung mit geringem Aufwand, die keine Kosten für Futures verursacht, aber die Wiederverwendbarkeit von Schlüsselwörtern ermöglicht, die derzeit ausschließlich für eine Bibliothek bestimmt sind.

Aber Fortsetzungen können natürlich auch mit einem Präprozessor implementiert werden, was wir auch tun werden. Leider kann der Deszucker nur ein Verschluss sein, der teurer ist als eine richtige Fortsetzung.

@aep Wie würden wir es ermöglichen, die Schlüsselwörter ( async und await ) wiederzuverwenden?

@Centril meine naive schnelle Lösung wäre, eine Asynchronität zu einem Generator und nicht zu einer Zukunft zu senken. Das gibt Zeit, um den Generator für richtige Fortsetzungen nützlich zu machen, anstatt ein exklusives Backend für Futures zu sein.

Es ist wie ein 10-Zeilen-PR vielleicht. Aber ich habe nicht die Geduld, einen Bienenstock deswegen zu bekämpfen, also baue ich einfach ein Preproc, um ein anderes Schlüsselwort zu entzuckern.

Ich habe das asynchrone Zeug nicht verfolgt. Entschuldigung, wenn dies zuvor / an anderer Stelle besprochen wurde, aber wie sieht der (Implementierungs-)Plan zur Unterstützung von asynchronem / warten in no_std ?

AFAICT die aktuelle Implementierung verwendet TLS, um einen Waker herumzureichen, aber es gibt keine TLS- (oder Thread-) Unterstützung in no_std / core . Ich habe von @alexcrichton gehört, dass es möglich sein könnte, das TLS loszuwerden, wenn / wenn Generator.resume Unterstützung für Argumente erhält.

Wird der Plan, die Stabilisierung von Async / Await auf no_std Unterstützung zu blockieren, implementiert? Oder sind wir sicher, dass no_std Unterstützung hinzugefügt werden kann, ohne die Teile zu ändern, die stabilisiert werden, um std asynchron / warten auf Stable zu liefern?

@japaric poll nimmt den Kontext jetzt explizit. AFAIK, TLS sollte nicht mehr erforderlich sein.

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

Edit: nicht relevant für das Async/Await, nur für Futures.

[...] sind wir sicher, dass no_std Unterstützung hinzugefügt werden kann, ohne die Teile zu ändern, die stabilisiert werden, um std asynchron / warten auf Stable zu versenden?

Ich glaube schon. Die relevanten Teile sind die Funktionen in std::future , diese sind alle hinter einem zusätzlichen gen_future instabilen Feature verborgen, das niemals stabilisiert wird. Die async Transformation verwendet set_task_waker , um den Waker in TLS zu speichern, dann verwendet await! poll_with_tls_waker , um darauf zuzugreifen. Wenn Generatoren Unterstützung für das Wiederaufnahme-Argument erhalten, kann die async Transformation stattdessen den Waker als Wiederaufnahme-Argument übergeben und await! kann ihn aus dem Argument auslesen.

BEARBEITEN: Auch ohne Generatorargumente glaube ich, dass dies auch mit etwas komplizierterem Code in der asynchronen Transformation möglich ist. Ich persönlich würde gerne Generatorargumente für andere Anwendungsfälle hinzugefügt sehen, aber ich bin mir ziemlich sicher, dass das Entfernen der TLS-Anforderung mit / ohne sie möglich ist.

@japaric Das gleiche Boot. Selbst wenn jemand Futures dazu gebracht hat, eingebettet zu arbeiten, ist es sehr riskant, da es sich um alles Tier3 handelt.

Ich habe einen hässlichen Hack gefunden, der weitaus weniger Arbeit erfordert als das Reparieren von Async: Weben Sie in einem Arcdurch einen Stapel von Generatoren.

  1. siehe das "Poll"-Argument https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 es ist ein Arc
  2. etwas in der Umfrage in Zeile 87 registrieren
  3. Yield, um einen Fortsetzungspunkt in Zeile 92 . zu erzeugen
  4. Rufen Sie einen Generator von einem Generator auf, um einen höherstufigen Stapel in Zeile 207 zu erstellen
  5. schließlich den gesamten Stack ausführen, indem eine Laufzeit in Zeile 215 übergeben wird

Im Idealfall würden sie einfach asynchron zu einem "reinen" Closure-Stack und nicht zu einem Future absenken, sodass Sie keine Laufzeitannahmen benötigen und dann die unreine Umgebung als Argument an der Wurzel einfügen können.

Ich war auf halbem Weg, das umzusetzen

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

Aber irgendwie sinnlos, den ganzen Weg zu gehen, wenn ich der einzige bin, der es will.

Und ich konnte nicht aufhören darüber nachzudenken, ob TLS-loses async/await ohne Generatorargumente möglich ist, also habe ich ein no_std proc-makrobasiertes async_block! / await! Makropaar implementiert nur lokale Variablen verwenden.

Es erfordert definitiv viel subtilere Sicherheitsgarantien als die aktuelle TLS-basierte Lösung oder eine auf Generatorargumenten basierende Lösung (zumindest wenn Sie einfach davon ausgehen, dass die zugrunde liegenden Generatorargumente solide sind), aber ich bin mir ziemlich sicher, dass es solide ist (solange niemand verwendet das ziemlich große Hygieneloch, das ich nicht umgehen konnte, wäre dies kein Problem für eine In-Compiler-Implementierung, da sie nicht benennbare Gensym-Identifikationen verwenden kann, um zwischen der asynchronen Transformation und dem Wait-Makro zu kommunizieren).

Mir ist gerade aufgefallen, dass es im OP nicht erwähnt wird, await! von std auf core zu verschieben, vielleicht könnte #56767 der Liste der Probleme hinzugefügt werden, die gelöst werden müssen, bevor die Stabilisierung verfolgt wird Dies.

@Nemo157 Da nicht erwartet wird, dass await! stabilisiert wird, ist es sowieso kein Blocker.

@Centril Ich weiß nicht, wer dir gesagt hat, dass await! voraussichtlich nicht stabilisiert wird ... :wink:

@cramertj Er meinte die

@crlf0710 Was ist mit der impliziten Async-Block- Version?

@crlf0710 habe ich auch gemacht :)

@cramertj Wollen wir das Makro nicht entfernen, weil es derzeit einen hässlichen Hack im Compiler gibt, der die Existenz von await und await! möglich macht? Wenn wir das Makro stabilisieren, können wir es nie entfernen.

@stjepang Die Syntax von await! interessiert mich wirklich in keiner Richtung, abgesehen von einer allgemeinen Vorliebe für Postfix-Notationen und einer Abneigung gegen Mehrdeutigkeit und unaussprechbare / nicht Google-fähige Symbole. Soweit mir bekannt sind die aktuellen Vorschläge (mit ? um den Vorrang zu klären):

  • await!(x)? (was wir heute haben)
  • await x? ( await bindet enger als ? , immer noch Präfixnotation, benötigt Klammern um Methoden zu verketten)
  • await {x}? (wie oben, aber vorübergehend {} erforderlich, um eindeutig zu sein)
  • await? x ( await bindet weniger fest, immer noch Präfixnotation, benötigt Klammern um Methoden zu verketten)
  • x.await? (sieht aus wie ein Feldzugriff)
  • x# / x~ /usw. (einige Symbole)
  • x.await!()? (Postfix-Makro-Stil, @Withoutboats und ich denke, vielleicht sind andere keine Fans von Postfix-Makros, weil sie erwarten, dass . typbasierten Versand ermöglicht, was bei Postfix-Makros nicht der Fall wäre )

Ich denke, dass der beste Weg zum Versand darin besteht, await!(x) landen, await Schlüsselwort zu entfernen und schließlich eines Tages den Leuten die Schönheit von Postfix-Makros zu verkaufen, was es uns ermöglicht, x.await!() hinzuzufügen

Ich verfolge dieses Thema sehr locker, aber hier ist meine Meinung:

Persönlich mag ich das await! Makro so wie es ist und wie es hier beschrieben wird: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Es ist keine Art von Magie oder neuer Syntax, sondern nur ein normales Makro. Weniger ist schließlich mehr.

Andererseits habe ich auch try! bevorzugt, da Try immer noch nicht stabilisiert ist. await!(x)? ist jedoch ein anständiger Kompromiss zwischen Zucker und offensichtlich benannten Aktionen, und ich denke, es funktioniert gut. Darüber hinaus könnte es möglicherweise durch ein anderes Makro in einer Drittanbieterbibliothek ersetzt werden, um zusätzliche Funktionen wie Debug-Tracing zu verarbeiten.

Inzwischen ist async / yield "nur" syntaktischer Zucker für Generatoren. Es erinnert mich an die Tage, als JavaScript asynchrone/wartete Unterstützung erhielt und Sie Projekte wie Babel und Regenerator hatten , die asynchronen Code transpilierten, um Generatoren und Versprechen/Zukunft für asynchrone Operationen zu verwenden, im Wesentlichen genau wie wir es tun.

Denken Sie daran, dass wir schließlich möchten, dass Async und Generatoren unterschiedliche Funktionen sind, die möglicherweise sogar miteinander komponierbar sind (was ein Stream ). await! als Makro zu belassen, das nur auf yield ist keine dauerhafte Lösung.

Verlassen warten! als Makro, das sich nur bis zum Ertrag absenkt, ist keine dauerhafte Lösung.

Es kann nicht dauerhaft für den Benutzer sichtbar sein, dass es auf yield , aber es kann auf jeden Fall so weiter implementiert werden. Selbst wenn Sie async + Generatoren = Stream , können Sie zB yield Poll::Pending; vs. yield Poll::Ready(next_value) .

Denken Sie daran, dass wir irgendwann möchten, dass Async und Generatoren unterschiedliche Funktionen sind

Sind Async und Generatoren keine unterschiedlichen Funktionen? Natürlich verwandt, aber wenn ich dies noch einmal mit JavaScript vergleiche, dachte ich immer, dass Async auf Generatoren aufbauen würde; dass der einzige Unterschied darin besteht, dass eine asynchrone Funktion im Gegensatz zu jedem regulären Wert Future s zurückgibt und liefert. Ein Executor wäre erforderlich, um die asynchrone Funktion auszuwerten und auf die Ausführung zu warten. Plus ein paar Extralebensdauer-Sachen, bei denen ich mir nicht sicher bin.

Tatsächlich habe ich einmal eine Bibliothek über genau diese Sache geschrieben, die sowohl asynchrone Funktionen als auch Generatorfunktionen rekursiv auswertet, die Promises/Futures zurückgeben.

@cramertj Es kann nicht auf diese Weise implementiert werden, wenn die beiden unterschiedliche "Effekte" sind. Hier wird darüber diskutiert: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. Wir wollen nicht yield Poll::Ready(next_value) , sondern yield next_value und haben await s an anderer Stelle in derselben Funktion.

@rpjohnst

Wir wollen Poll::Ready(next_value) nicht liefern, wir wollen next_value liefern und haben an anderer Stelle in der gleichen Funktion Waits.

Ja, natürlich sieht es so für den Benutzer aus, aber in Bezug auf die Entzuckerung müssen Sie nur yield s in Poll::Ready einpacken und ein Poll::Pending hinzufügen das aus yield generierte await! . Syntaktisch erscheinen sie Endbenutzern als separate Funktionen, können aber dennoch eine Implementierung im Compiler teilen.

@cramertj Auch dieser hier:

  • await? x

@novacrazy Ja, sie sind verschiedene Funktionen, aber sie sollten zusammensetzbare zusammen sein.

Und in der Tat in JavaScript sind sie zusammensetzbare:

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

„Asynchrone Generatoren und Iteratoren sind das, was Sie erhalten, wenn Sie eine asynchrone Funktion und einen Iterator kombinieren, sodass es wie ein asynchroner Generator ist, auf den Sie warten können, oder eine asynchrone Funktion, von der Sie profitieren können“, erklärte er. Zuvor erlaubte ECMAScript Ihnen, eine Funktion zu schreiben, die Sie nachgeben oder warten konnten, aber nicht beides. „Dies ist wirklich praktisch für den Konsum von Streams, die immer mehr Teil der Webplattform werden, insbesondere wenn das Fetch-Objekt Streams verfügbar macht.“

Der asynchrone Iterator ähnelt dem Observable-Muster, ist jedoch flexibler. „Ein Observable ist ein Push-Modell; Sobald Sie es abonniert haben, werden Sie mit voller Geschwindigkeit mit Ereignissen und Benachrichtigungen überhäuft, ob Sie bereit sind oder nicht. Sie müssen also Puffer- oder Sampling-Strategien implementieren, um mit Geschwätzigkeit umzugehen“, erklärte Terlson. Der asynchrone Iterator ist ein Push-Pull-Modell – Sie fragen nach einem Wert und er wird an Sie gesendet – was für Dinge wie Netzwerk-IO-Primitive besser funktioniert.

@Centril ok, geöffnet Nr. 56974, ist das richtig genug, um dem OP als ungelöste Frage hinzugefügt zu werden?


Ich möchte wirklich nicht noch einmal in die await Syntax einsteigen, aber ich muss auf mindestens einen Punkt antworten:

Persönlich mag ich das await! Makro so wie es ist und wie es hier beschrieben wird: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Beachten Sie, dass ich auch gesagt habe, dass ich nicht glaube, dass das Makro ein in einer Bibliothek implementiertes Makro bleiben kann (unabhängig davon, ob es Benutzern weiterhin als Makro angezeigt wird oder nicht), um die Gründe zu erläutern:

  1. Ausblenden der zugrunde liegenden Implementierung, da eines der ungelösten Probleme besagt, dass Sie derzeit einen Generator erstellen können, indem Sie || await!() .
  2. Die Unterstützung von asynchronen Generatoren, wie @cramertj erwähnt, erfordert eine Unterscheidung zwischen den yield s, die von await hinzugefügt wurden, und anderen yield s, die vom Benutzer geschrieben wurden. Dies _könnte_ als Vor-Makro-Erweiterungsstufe durchgeführt werden, _wenn_ Benutzer nie yield innerhalb von Makros machen wollten, aber es gibt sehr nützliche yield -in-Makro-Konstrukte wie yield_from! . Mit der Einschränkung, dass yield s in Makros unterstützt werden müssen, erfordert dies, dass await! zumindest ein eingebautes Makro ist (wenn nicht die tatsächliche Syntax).
  3. Unterstützung von async fn auf no_std . Ich kenne zwei Möglichkeiten, dies zu implementieren, beide erfordern die async fn -created- Future und await , um eine Kennung zu teilen, in der der Waker gespeichert ist können sehen, dass eine hygienisch sichere Kennung zwischen diesen beiden Stellen geteilt wird, wenn beide im Compiler implementiert sind.

Ich denke, hier herrscht ein wenig Verwirrung - es war nie die Absicht, await! öffentlich sichtbar zu einem Wrapper um Aufrufe von yield . Jede zukünftige für die await! Makro-ähnliche Syntax wird auf einer Implementierung beruhen, die der der aktuellen vom Compiler unterstützten compile_error! , assert! , format_args! usw. nicht unähnlich ist. und wäre in der Lage, je nach Kontext zu unterschiedlichem Code zu entsäuern.

Das einzige, was hier zu verstehen ist, ist, dass es keinen signifikanten semantischen Unterschied zwischen den vorgeschlagenen Syntaxen gibt – es handelt sich nur um Oberflächensyntax.

Ich würde eine Alternative schreiben, um die await Syntax zu lösen.

Zuallererst gefällt mir die Idee, dass await als Postfix-Operator verwendet wird. Aber expression.await ist zu sehr wie ein Feld, wie bereits erwähnt.

Mein Vorschlag lautet also expression awaited . Der Nachteil hier ist, dass awaited noch nicht als Schlüsselwort erhalten ist, aber im Englischen natürlicher ist und dennoch gibt es keine solchen Ausdrücke (ich meine, Grammatikformen wie expression [token] ) sind in Rust gültig jetzt, so dass dies gerechtfertigt werden kann.

Dann können wir expression? awaited für das Warten auf ein Result<Future,_> und expression awaited? für das Warten auf ein Future<Item=Result<_,_>> schreiben.

@earthengine

Das Schlüsselwort awaited mich zwar nicht überzeugt, aber ich glaube, Sie haben etwas herausgefunden.

Die wichtigste Erkenntnis hier ist: yield und await sind wie return und ? .

return x gibt den Wert x , während x? das Ergebnis x entpackt und vorzeitig zurückgibt, wenn es Err .
yield x ergibt den Wert x , während x awaited auf zukünftige x wartet und früher zurückkehrt, wenn es Pending .

Es hat eine schöne Symmetrie. Vielleicht sollte await wirklich ein Postfix-Operator sein.

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

Ich bin kein Fan einer erwarteten Postfix-Syntax aus dem genauen Grund, den @cramertj gerade gezeigt hat. Es verringert die allgemeine Lesbarkeit, insbesondere bei langen Ausdrücken oder verketteten Ausdrücken. Es gibt kein Gefühl der Verschachtelung, wie es await! / await würde. Es hat nicht die Einfachheit von ? , und uns gehen die Symbole für einen Postfix-Operator aus ...

Persönlich bin ich immer noch für await! aus den oben beschriebenen Gründen. Es fühlt sich Rusty und No-Nonsense an.

Es verringert die allgemeine Lesbarkeit, insbesondere bei langen Ausdrücken oder verketteten Ausdrücken.

In Rustfmt-Standards soll das Beispiel geschrieben werden

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

Ich kann kaum erkennen, wie sich dies auf die Lesbarkeit auswirkt.

Ich mag Postfix Wait auch. Ich denke, dass die Verwendung eines Raums ungewöhnlich wäre und dazu neigen würde, die mentale Gruppierung zu durchbrechen. Aber ich glaube , dass .await!() schön paaren würde, mit ? Einbau entweder vor oder nach, und den ! würde für Steuerfluss Interaktionen ermöglichen.

(Dafür ist kein vollständig allgemeiner Postfix-Makromechanismus erforderlich; der Compiler könnte .await!() Sonderfälle

Anfangs mochte ich das Postfix await (ohne . oder () ) wirklich nicht, da es ziemlich seltsam aussieht - Leute aus anderen Sprachen werden ein gutes Lachen über unsere bekommen Kosten sicher. Das sind Kosten, die wir ernst nehmen sollten. Allerdings ist x await eindeutig kein Funktionsaufruf oder ein Feldzugriff ( x.await / x.await() / await(x) alle dieses Problem) und es gibt weniger funky Vorrangprobleme. Diese Syntax würde eindeutig ? und Methodenzugriffspriorität auflösen, zB foo await? und foo? await beide eine klare Prioritätsreihenfolge für mich, ebenso wie foo await?.x und foo await?.y (nicht bestreiten, dass sie seltsam aussehen, nur argumentieren, dass die Rangfolge klar ist).

ich denke das auch

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

liest sich schöner als

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

Alles in allem würde ich das unterstützen.

@joshtriplett RE .await!() wir sollten getrennt reden-- Ich war anfangs auch dafür, aber ich denke nicht, dass wir das landen sollten, wenn wir nicht auch Postfix-Makros im Allgemeinen bekommen können, und ich denke es gibt eine Menge Widerstand gegen sie (mit ziemlich guten, wenn auch unglücklichen Gründen), und ich möchte wirklich, dass dies die await Stabilisierung nicht blockiert.

Warum nicht beide?

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

Ich sehe den Reiz von Postfix jetzt mehr und grenze in einigen Szenarien daran, dass ich ihn mehr mag. Vor allem mit dem obigen Cheat, der so einfach ist, dass er nicht einmal von Rust selbst bereitgestellt werden muss.

Also +1 für Postfix.

Ich denke, wir sollten neben der Postfix-Version auch eine Präfix-Funktion haben.

Was die Besonderheiten der Postfix-Syntax angeht, möchte ich nicht sagen, dass .await!() die einzige brauchbare Postfix-Syntax ist; Ich bin einfach kein Fan von Postfix await mit einem führenden Leerzeichen.

Es sieht passabel (wenn auch immer noch ungewöhnlich) aus, wenn Sie es mit einer Anweisung pro Zeile formatieren, aber viel weniger vernünftig, wenn Sie einfache Anweisungen in einer Zeile formatieren.

Für diejenigen, die Postfix-Schlüsselwortoperatoren nicht mögen, können wir einen geeigneten symbolischen Operator für await .

Im Moment gingen uns die Operatoren in einfachen ASCII-Zeichen für den Postfix-Operator aus. Wie wäre es jedoch mit

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

Wenn wir wirklich einfaches ASCII brauchen, habe ich mir das ausgedacht (inspiriert von der Form oben)

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

oder (ähnliche Form in horizontaler Position)

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

Eine andere Idee ist, die await Struktur zu einer Klammer zu machen.

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

All diese ASCII-Lösungen haben das gleiche Problem <..> der Übergabe, dass < und > . Allerdings könnten >< oder >=< dafür besser geeignet sein, da sie keinen Platz innerhalb des Operators und keine offenen < s an der aktuellen Position benötigen.


Für diejenigen, die das Leerzeichen dazwischen nicht mögen, aber OK für Postfix-Schlüsselwortoperatoren, wie wäre es mit Bindestrichen:

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

Über viele verschiedene Möglichkeiten, denselben Code zu schreiben, mag ich persönlich nicht. Der Hauptgrund dafür ist, dass es für Neulinge viel schwieriger ist, richtig zu verstehen, was der richtige Weg oder Sinn ist. Der zweite Grund ist, dass wir viele verschiedene Projekte haben werden, die unterschiedliche Syntax verwenden und es schwieriger wäre, dazwischen zu springen und sie zu lesen (insbesondere für Neulinge im Rost). Ich denke, dass eine andere Syntax nur implementiert werden sollte, wenn es tatsächlich Unterschiede gibt und sie einige Vorteile bietet. Viel Code-Zucker macht es nur viel schwieriger, die Sprache zu lernen und mit ihr zu arbeiten.

@goffrie Ja, ich stimme zu, dass wir nicht viele verschiedene Möglichkeiten haben sollten, dasselbe zu tun. Ich habe jedoch nur verschiedene Alternativen vorgeschlagen, die Community muss sich nur für eine entscheiden. Daher ist dies nicht wirklich ein Problem.

Darüber hinaus gibt es in Bezug auf das await! Makro keine Möglichkeit, den Benutzer daran zu hindern, eigene Makros zu erfinden, um es anders zu machen, und Rust soll dies ermöglichen. Daher ist es unvermeidlich, "viele verschiedene Möglichkeiten zu haben, dasselbe zu tun".

Ich denke, das einfache dumme Makro, das ich gezeigt habe, zeigt, dass der Benutzer, egal was wir tun, sowieso tun wird, was er will. Ein Schlüsselwort, sei es Präfix oder Postfix, kann entweder zu einem funktionsähnlichen Präfix-Makro oder vermutlich zu einem postfix-Methoden-ähnlichen Makro gemacht werden, sofern diese existieren. Selbst wenn wir funktions- oder methodenähnliche Makros für await , könnten sie mit einem weiteren Makro invertiert werden. Es ist wirklich egal.

Daher sollten wir uns auf Flexibilität und Formatierung konzentrieren. Stellen Sie eine Lösung bereit, die all diese Möglichkeiten am einfachsten ausfüllt.

Obwohl ich in dieser kurzen Zeit an die Postfix-Schlüsselwort-Syntax gewachsen bin, sollte await spiegeln, was für yield mit Generatoren entschieden wird, was wahrscheinlich ein Präfix-Schlüsselwort ist. Für Benutzer, die eine Postfix-Lösung wünschen, wird es wahrscheinlich irgendwann methodenähnliche Makros geben.

Meine Schlussfolgerung ist, dass ein Präfix-Schlüsselwort await derzeit die beste Standard-Syntax ist, vielleicht mit einer normalen Crate, die Benutzern ein funktionsähnliches await! Makro und in Zukunft eine Postfix-Methode ähnlich bietet .await!() Makro.

@novacrazy

Außerdem, obwohl ich in dieser kurzen Zeit an die Postfix-Schlüsselwortsyntax gewachsen bin, sollte await alles widerspiegeln, was für yield mit Generatoren entschieden wird, was wahrscheinlich ein Präfix-Schlüsselwort ist.

Der Ausdruck yield 42 hat den Typ ! , während foo.await Typ T mit foo: impl Future<Output = T> . @stjepang macht hier die richtige Analogie mit ? und return . await ist nicht wie yield .

Warum nicht beide?

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

Sie müssen dem Makro einen anderen Namen geben, da await ein echtes Schlüsselwort bleiben sollte.


Aus verschiedenen Gründen lehne ich das Präfix await und noch mehr die Blockform await { ... } .

Zuerst gibt es die Rangfolgeprobleme mit await expr? wobei die konsistente Rangfolge await (expr?) aber Sie wollen (await expr)? . Als Lösung für die Vorrangprobleme haben einige await? expr zusätzlich zu await expr . Dies beinhaltet await? als Einheit und Spezialgehäuse; das scheint ungerechtfertigt, eine Verschwendung unseres Komplexitätsbudgets und ein Hinweis darauf, dass await expr ernsthafte Probleme hat.

Noch wichtiger ist, dass Rust-Code und insbesondere die Standardbibliothek stark auf die Leistungsfähigkeit der Punkt- und Methodenaufrufsyntax ausgerichtet sind. Wenn await ein Präfix ist, ermutigt es den Benutzer, temporäre let-Bindungen zu erfinden, anstatt einfach Methoden zu verketten. Aus diesem Grund ist ? postfix, und aus dem gleichen Grund sollte await auch postfix sein.

Noch schlimmer wäre await { ... } . Diese Syntax würde, wenn sie konsistent gemäß rustfmt formatiert würde, zu Folgendem werden:

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

Dies wäre nicht ergonomisch und würde die vertikale Länge der Funktionen erheblich aufblähen.


Stattdessen denke ich, dass Warten, wie ? , ein Postfix sein sollte, da dies zum Rust-Ökosystem passt, das sich auf die Methodenverkettung konzentriert. Es wurde eine Reihe von Postfix-Syntaxen erwähnt; Ich werde einige von ihnen durchgehen:

  1. foo.await!() -- Dies ist die Postfix-Makrolösung . Obwohl ich stark für Postfix-Makros bin, stimme ich @cramertj in https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040 zu, dass wir dies nicht tun sollten, es sei denn, wir verpflichten uns auch zum Postfix Makros im Allgemeinen. Ich denke auch, dass die Verwendung eines Postfix-Makros auf diese Weise ein eher nicht erstklassiges Gefühl vermittelt; wir sollten imo vermeiden, dass ein Sprachkonstrukt Makrosyntax verwendet.

  2. foo await -- Das ist nicht so schlimm, es funktioniert wirklich wie ein Postfix-Operator ( expr op ), aber ich habe das Gefühl, dass bei dieser Formatierung etwas fehlt (dh es fühlt sich "leer" an); Im Gegensatz dazu hängt expr? ? direkt an expr ; hier ist kein Platz. Dadurch sieht ? optisch ansprechend aus.

  3. foo.await -- Dies wurde kritisiert, weil es wie ein Feldzugriff aussieht; und das ist wahr. Wir sollten jedoch daran denken, dass await ein Schlüsselwort ist und daher in der Syntax als solches hervorgehoben wird. Wenn Sie Rust-Code in Ihrer IDE oder gleichwertig auf GitHub lesen, hat await eine andere Farbe oder Fettschrift als foo . Mit einem anderen Schlüsselwort können wir dies demonstrieren:

    let x = foo.match?;
    

    Normalerweise sind Felder auch Substantive, während await ein Verb ist.

    Obwohl foo.await anfänglich einen Lächerlichkeitsfaktor hat, denke ich, dass er als optisch ansprechende und gleichzeitig lesbare Syntax ernsthaft in Betracht gezogen werden sollte.

    Als Bonus erhalten Sie durch die Verwendung von .await die Kraft des Punktes und die automatische Vervollständigung, die der Punkt normalerweise in IDEs hat (siehe Seite 56). Sie können beispielsweise foo. schreiben und wenn foo zufällig ein Future ist, wird await als erste Wahl angezeigt. Dies erleichtert sowohl die Ergonomie als auch die Entwicklerproduktivität, da viele Entwickler das Muskelgedächtnis trainiert haben, nach dem Punkt zu greifen.

    Bei all den möglichen Postfix-Syntaxen bleibt dies trotz der Kritik, wie Feldzugriff auszusehen, meine Lieblingssyntax.

  4. foo# -- Dies verwendet das Siegel # um auf foo zu warten. Ich denke, es ist eine gute Idee, ein Siegel in Betracht zu ziehen, da ? auch ein Siegel ist und weil es das Warten leicht macht. Kombiniert mit ? würde es aussehen wie foo#? -- das sieht in Ordnung aus. # jedoch keine spezielle Begründung. Es ist vielmehr nur ein Siegel, das noch verfügbar ist.

  5. foo@ -- Ein weiteres Siegel ist @ . In Kombination mit ? wir foo@? . Eine Rechtfertigung für dieses spezielle Siegel ist, dass es a -ish ( @wait ) aussieht.

  6. foo! -- Schließlich gibt es noch ! . In Kombination mit ? wir foo!? . Leider hat das ein gewisses WTF-Feeling. ! sieht jedoch so aus, als würde man den Wert erzwingen, der zu "warten" passt. Es gibt einen Nachteil darin, dass foo!() bereits ein zulässiger Makroaufruf ist, sodass das Warten und Aufrufen einer Funktion (foo)!() geschrieben werden müsste. Die Verwendung von foo! als Syntax würde uns auch die Chance nehmen, Schlüsselwort-Makros zu haben (zB foo! expr ).

Ein weiteres einzelnes Siegel ist foo~ . Die Welle kann als „Echo“ oder „braucht Zeit“ verstanden werden. Es wird jedoch nirgendwo in der Sprache Rust verwendet.

Tilde ~ wurde früher für den Heap-Zuweisungstyp verwendet: https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

Kann ? wiederverwendet werden? Oder ist das zu viel Magie? Wie würde impl Try for T: Future aussehen?

@parasyte Ja, ich erinnere mich. Aber es war trotzdem lange vorbei.

@jethrogb es gibt keine Möglichkeit, dass impl Try direkt funktioniert, ? explizit return das Ergebnis von Try aus der aktuellen Funktion, während await muss yield .

Vielleicht könnte ? mit einer speziellen Schreibweise versehen werden, um im Kontext eines Generators etwas anderes zu tun, sodass es entweder yield oder return abhängig von der Art des Ausdrucks, auf den es angewendet wird , aber ich bin mir nicht sicher, wie verständlich das wäre. Und wie würde das mit Future<Output=Result<...>> interagieren, müssten Sie let foo = bar()??; tun, um beide "Warten" zu machen und dann die Ok Variante von Result ( oder würde ? in Generatoren auf einem Tristate-Merkmal basieren, das yield , return oder mit einer einzigen Anwendung in einen Wert auflösen kann)?

Diese letzte Bemerkung in Klammern lässt mich tatsächlich denken, dass sie praktikabel sein könnte. Klicken Sie hier, um eine kurze Skizze zu sehen
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>;
}
mit `foo?` innerhalb eines Generators erweitert auf etwas wie (obwohl dies ein Eigentumsproblem hat und auch das Ergebnis von `foo` stapeln muss)
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()),
    }
}

Leider sehe ich nicht, wie ich die Waker-Kontextvariable in einem solchen Schema behandeln soll, vielleicht wenn ? für async anstelle von Generatoren ein Sonderfall wäre, aber wenn es etwas Besonderes sein soll -cased hier wäre es schön, wenn es für andere Anwendungsfälle von Generatoren verwendbar wäre.

Ich hatte den gleichen Gedanken bezüglich der Wiederverwendung von ? wie @jethrogb.

@Nemo157

Ich kann nicht sehen, dass impl Try direkt funktioniert, ? explizit return das Ergebnis von Try aus der aktuellen Funktion, während wait yield .

Vielleicht fehlen mir einige Details zu ? und dem Merkmal Try , aber wo/warum ist das explizit? Und ist ein return in einem asynchronen Closure nicht im Wesentlichen dasselbe wie yield , nur ein anderer Zustandsübergang?

Vielleicht könnte ? mit Sonderzeichen versehen werden, um im Kontext eines Generators etwas anderes zu tun, sodass es entweder yield oder return abhängig von der Art des Ausdrucks, auf den es angewendet wird , aber ich bin mir nicht sicher, wie verständlich das wäre.

Ich verstehe nicht, warum das verwirrend sein sollte. Wenn Sie sich ? als "fortsetzen oder divergieren" vorstellen, dann erscheint es IMHO natürlich. Zugegeben, es würde helfen, das Try Merkmal zu ändern, um andere Namen für die zugehörigen Rückgabetypen zu verwenden.

Wie würde das auch mit Future<Output=Result<...>> interagieren, müssten Sie let foo = bar()?? ;

Wenn Sie das Ergebnis abwarten und dann auch bei einem Fehlerergebnis vorzeitig beenden möchten, dann wäre das der logische Ausdruck, ja. Ich glaube nicht, dass ein spezielles Tri-State- TryGen überhaupt nötig wäre.

Leider sehe ich nicht, wie man die Waker-Kontextvariable in einem Schema wie diesem behandelt, vielleicht wenn ? wurden für asynchrone anstelle von Generatoren in Sonderfällen angegeben, aber wenn sie hier mit Sonderfällen versehen werden, wäre es schön, wenn sie für andere Anwendungsfälle von Generatoren verwendet werden könnten.

Ich verstehe diesen Teil nicht. Könnten Sie das näher erläutern?

@jethrogb @rolandsteiner Eine Struktur könnte sowohl Try als auch Future implementieren. Welches sollte in diesem Fall ? ausgepackt werden?

@jethrogb @rolandsteiner Eine Struktur könnte sowohl Try als auch Future implementieren. Welcher sollte in diesem Fall verwendet werden? auspacken?

Nein, das konnte nicht wegen des pauschalen Versuchs für T: Zukunft.

Warum spricht niemand von der expliziten Konstruktion und dem impliziten Erwartungsvorschlag ? Es entspricht sync io, nur dass es die Aufgabe anstelle des Threads blockiert. Ich würde sogar sagen, dass das Blockieren eines Threads invasiver ist als das Blockieren einer Aufgabe. Warum gibt es also keine spezielle "Await" -Syntax für Thread-Blocking-io?

aber es ist alles nur Bike-Shading, ich denke, wir sollten uns zumindest vorerst mit der einfachen Makro-Syntax await!(my_future) zufrieden geben

aber es ist alles nur Bike-Shading, ich denke, wir sollten uns zumindest vorerst mit der einfachen Makro-Syntax await!(my_future) zufrieden geben

Nein, es ist nicht "nur" Fahrradschuppen, als ob das etwas Banales und Unbedeutendes wäre. Ob await als Präfix oder Postfix geschrieben wird, beeinflusst grundlegend, wie asynchroner Code bezüglich der Schreibweise geschrieben wird. Methodenverkettung und wie zusammensetzbar es sich anfühlt. Die Stabilisierung auf await!(future) auch zur Folge, dass await als Schlüsselwort aufgegeben wird, was eine zukünftige Verwendung von await als Schlüsselwort unmöglich macht. "Zumindest vorerst" suggeriert, dass wir später eine bessere Syntax finden können, und lässt die damit verbundene technische Schuld außer Acht. Ich bin dagegen, wissentlich Schulden für eine Syntax einzuführen, die später ersetzt werden soll.

Das Stabilisieren auf await!(future) hat auch zur Folge, dass await als Schlüsselwort aufgegeben wird, was die zukünftige Verwendung von await als Schlüsselwort unmöglich macht.

wir könnten es in der nächsten Epoche zu einem Schlüsselwort machen, das die rohe Ident-Syntax für das Makro erfordert, genau wie wir es bei try getan haben.

@rolandsteiner

Und ist ein return in einem asynchronen Closure nicht im Wesentlichen dasselbe wie yield , nur ein anderer Zustandsübergang?

yield existiert nicht in einem asynchronen Closure, es ist eine Operation, die während des Absenkens von async / await Syntax auf Generatoren/ yield . In der aktuellen Generator-Syntax ist yield ziemlich anders als return . Wenn die Erweiterung ? vor der Generatortransformation durchgeführt wird, weiß ich nicht, wie sie wissen würde, wann sie eingefügt werden soll ein return oder ein yield .

Wenn Sie das Ergebnis abwarten und bei einem Fehlerergebnis auch frühzeitig beenden möchten, dann wäre das der logische Ausdruck, ja.

Es mag logisch sein, aber es scheint mir ein Nachteil zu sein, dass viele (die meisten?) Fälle, in denen Sie asynchrone Funktionen schreiben, mit doppelten ?? gefüllt werden, um die IO-Fehler zu beheben.

Leider sehe ich nicht, wie ich mit der Waker-Kontextvariable umgehe...

Ich verstehe diesen Teil nicht. Könnten Sie das näher erläutern?

Die asynchrone Transformation nimmt eine Waker-Variable in die generierte Future::poll Funktion auf, die dann an die transformierte Wait-Operation übergeben werden muss. Derzeit wird dies mit einer von std bereitgestellten TLS-Variablen gehandhabt, die beide Transformationen referenzieren. Wenn ? stattdessen als Re-Yield-Point _auf Generatorebene_ behandelt würden, dann verliert die asynchrone Transformation einen Weg um diese Variablenreferenz einzufügen.

Ich habe vor zwei Monaten einen Blogbeitrag über die Syntax von await , in dem ich meine Präferenzen darlegte. Es ging jedoch im Grunde genommen von einer Präfixsyntax aus und betrachtete nur das Vorrangproblem aus dieser Perspektive. Hier noch einige zusätzliche Gedanken:

  • Meine allgemeine Meinung ist, dass Rust sein Unbekanntes-Budget bereits wirklich ausgereizt hat. Es wäre ideal, wenn die async/await-Syntax auf Oberflächenebene jemandem, der aus JavaScript oder Python oder C# kommt, so vertraut wie möglich ist. Aus dieser Perspektive wäre es ideal, nur geringfügig von der Norm abzuweichen. Postfix-Syntaxen variieren je nachdem, wie weit sie von der Divergenz abweichen (zB foo await ist weniger Divergenz als irgendein Siegel wie foo@ ), aber sie sind alle divergenter als Präfix-Awarte.
  • Ich bevorzuge es auch, eine Syntax zu stabilisieren, die nicht ! . Jeder Benutzer, der sich mit Async/Await beschäftigt, wird sich fragen, warum Wait ein Makro ist und kein normales Kontrollflusskonstrukt, und ich glaube, die Geschichte hier wird im Wesentlichen sein: "Nun, wir konnten keine gute Syntax herausfinden, also haben wir uns einfach darauf festgelegt." Es sieht aus wie ein Makro." Dies ist keine zwingende Antwort. Ich glaube nicht, dass die Verbindung zwischen ! und Kontrollfluss wirklich ausreicht, um diese Syntax zu rechtfertigen: Ich glaube, dass ! ziemlich spezifisch bedeutet, dass Makros erweitert werden, was nicht der Fall ist.
  • Ich bin irgendwie skeptisch bezüglich des Nutzens von Postfix-Await im Allgemeinen (nicht ganz, nur irgendwie ). Ich habe das Gefühl, dass sich der Saldo etwas von ? , da das Warten eine teurere Operation ist (Sie geben in einer Schleife nach, bis sie fertig ist, anstatt nur einmal zu verzweigen und zurückzukehren). Ich bin etwas misstrauisch gegenüber Code, der zwei- oder dreimal in einem einzigen Ausdruck warten würde; es scheint mir in Ordnung zu sagen, dass diese in ihre eigenen Bindungen gezogen werden sollten. Der Kompromiss zwischen try! und ? zieht mich hier also nicht so stark an. Aber ich wäre auch offen für Codebeispiele, von denen die Leute denken, dass sie nicht in Lets gezogen werden sollten und die als Methodenketten klarer sind.

Das heißt, foo await ist die praktikabelste Postfix-Syntax, die ich bisher gesehen habe:

  • Es ist relativ bekannt für die Postfix-Syntax. Alles, was Sie lernen müssen, ist, dass await in Rust nach dem Ausdruck statt vor ihm steht, anstatt eine deutlich andere Syntax.
  • Es löst eindeutig das Vorrangproblem, um das es bei all dem ging.
  • Die Tatsache, dass es mit Methodenverkettung nicht gut funktioniert, scheint mir aus den Gründen, auf die ich zuvor angespielt habe, eher ein Vorteil als ein Nachteil zu sein. Ich wäre vielleicht mehr gezwungen, wenn wir einige Grammatikregeln hätten, die foo await.method() nur weil ich wirklich das Gefühl habe, dass die Methode (unsinnig) auf await angewendet wird, nicht auf foo (wobei interessanterweise Ich fühle das bei foo await? ).

Ich tendiere immer noch zu einer Präfix-Syntax, aber ich denke, dass await die erste Postfix-Syntax ist, die sich anfühlt, als hätte sie eine echte Chance für mich.

Randnotiz: Es ist immer möglich, Klammern zu verwenden, um die Rangfolge klarer zu machen:

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

Das ist nicht gerade ideal, aber wenn man bedenkt, dass es versucht, viel auf eine einzige Zeile zu stopfen, halte ich es für vernünftig.

Und wie @earthengine bereits erwähnt, ist die mehrzeilige Version sehr vernünftig (keine zusätzlichen Klammern):

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

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Es wäre ideal, wenn die async/await-Syntax auf Oberflächenebene jemandem, der aus JavaScript oder Python oder C# kommt, so vertraut wie möglich ist.

Bei try { .. } haben wir die Vertrautheit mit anderen Sprachen berücksichtigt. Es war jedoch auch das richtige Design aus einem POV der internen Konsistenz mit Rust. Bei allem Respekt vor diesen anderen Sprachen scheint die interne Konsistenz in Rust wichtiger zu sein, und ich glaube nicht, dass die Präfixsyntax zu Rust passt, weder in Bezug auf die Rangfolge noch in Bezug auf die Struktur von APIs.

  • Ich bevorzuge es auch, eine Syntax zu stabilisieren, die nicht ! . Jeder Benutzer, der sich mit Async/Await beschäftigt, wird sich fragen, warum Wait ein Makro ist und kein normales Kontrollflusskonstrukt, und ich glaube, die Geschichte hier wird im Wesentlichen sein: "Nun, wir konnten keine gute Syntax herausfinden, also haben wir uns einfach darauf festgelegt." Es sieht aus wie ein Makro." Dies ist keine zwingende Antwort.

Ich stimme diesem Gefühl zu, .await!() wird nicht erstklassig genug aussehen.

  • Ich bin irgendwie skeptisch bezüglich des Nutzens von Postfix-Await im Allgemeinen (nicht ganz, nur _irgendwie_). Ich habe das Gefühl, dass sich der Saldo etwas von ? , da das Warten eine teurere Operation ist (Sie geben in einer Schleife nach, bis sie fertig ist, anstatt nur einmal zu verzweigen und zurückzukehren).

Ich verstehe nicht, was teuer damit zu tun hat, Dinge in let Bindungen zu extrahieren. Methodenketten können und sind manchmal auch teuer. Der Vorteil von let Bindungen besteht darin, a) ausreichend großen Teilen einen Namen zu geben, wo es zur besseren Lesbarkeit sinnvoll ist, b) auf denselben berechneten Wert mehr als einmal verweisen zu können (zB durch &x oder wenn ein Typ kopierbar ist).

Ich bin etwas misstrauisch gegenüber Code, der zwei- oder dreimal in einem einzigen Ausdruck warten würde; es scheint mir in Ordnung zu sagen, dass diese in ihre eigenen Bindungen gezogen werden sollten.

Wenn Sie der Meinung sind, dass sie in ihre eigenen let Bindungen gezogen werden sollten, können Sie diese Wahl immer noch mit dem Postfix await treffen:

let temporary = some_computation() await?;

Für diejenigen, die anderer Meinung sind und Methodenverkettung bevorzugen, gibt Postfix await die Möglichkeit, zu wählen. Ich denke auch, dass Postfix hier besser der Lese- und Datenflussreihenfolge von links nach rechts folgt. Selbst wenn Sie also in let Bindungen extrahieren, würde ich immer noch Postfix bevorzugen.

Ich glaube auch, dass Sie nicht zwei- oder dreimal warten müssen, bis Postfix await nützlich ist. Betrachten Sie zum Beispiel (dies ist das Ergebnis von rustfmt ):

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

Aber ich wäre auch offen für Codebeispiele, von denen die Leute denken, dass sie nicht in Lets gezogen werden sollten und die als Methodenketten klarer sind.

Der größte Teil des fuchsiafarbenen Codes, den ich gelesen habe, fühlte sich unnatürlich an, wenn er in let Bindungen und mit let binding = await!(...)?; extrahiert wurde.

  • Es ist relativ bekannt für die Postfix-Syntax. Alles, was Sie lernen müssen, ist, dass await in Rust nach dem Ausdruck statt vor ihm steht, anstatt eine deutlich andere Syntax.

Meine Vorliebe für foo.await hier ist hauptsächlich, weil Sie eine schöne Autovervollständigung und die Kraft des Punktes erhalten. Es fühlt sich auch nicht so radikal anders an. Das Schreiben von foo.await.method() macht auch klarer, dass .method() auf foo.await angewendet wird. Es löst also diese Bedenken.

  • Es löst eindeutig das Vorrangproblem, um das es bei all dem ging.

Nein, es geht nicht nur um Vorrang. Ebenso wichtig sind Methodenketten.

  • Die Tatsache, dass es mit Methodenverkettung nicht gut funktioniert, scheint mir aus den Gründen, auf die ich zuvor angespielt habe, eher ein Vorteil als ein Nachteil zu sein.

Ich bin mir nicht sicher, warum es mit Methodenverkettung nicht gut funktioniert.

Ich wäre vielleicht mehr gezwungen, wenn wir einige Grammatikregeln hätten, die foo await.method() nur weil ich wirklich das Gefühl habe, dass die Methode (unsinnig) auf await angewendet wird, nicht auf foo (wobei interessanterweise Ich fühle das bei foo await? ).

Wobei ich nicht gezwungen wäre, mit foo await wenn wir einen absichtlichen Design-Papierschnitt einführen und Methodenketten mit der Postfix-Syntax await .

Zugegeben, dass jede Option eine Kehrseite hat und dass trotzdem eine von ihnen gewählt werden sollte ... eine Sache, die mich an foo.await stört, ist, dass, auch wenn wir davon ausgehen, dass es nicht buchstäblich verwechselt wird ein Struct-Feld, sieht es immer noch so meisten Nebenwirkungen (es führt sowohl die in der Zukunft aufgebauten E/A-Operationen aus als auch Kontrollflusseffekte hat). Wenn ich also foo.await.method() lese, sagt mir mein Gehirn, dass ich das .await überspringen soll, weil es relativ uninteressant ist und ich Aufmerksamkeit und Anstrengung aufwenden muss, um diesen Instinkt manuell zu überschreiben.

es sieht immer noch so aus, als ob_ auf ein struct-Feld zugegriffen würde.

@glaebhoerl Du machst gute Punkte; Hat das Syntax-Highlighting jedoch keinen/unzureichenden Einfluss darauf, wie es aussieht und wie Ihr Gehirn Dinge verarbeitet? Zumindest für mich sind Farbe und Kühnheit beim Lesen von Code sehr wichtig, daher würde ich .await nicht überspringen, das eine andere Farbe hat als der Rest der Dinge.

Der Feldzugriff bedeutet, dass nichts besonders Wirkungsvolles passiert – es ist eine der am wenigsten effektiven Operationen in Rust. In der Zwischenzeit ist das Warten sehr wirkungsvoll, eine der Operationen mit den meisten Nebenwirkungen (es führt sowohl die in der Zukunft aufgebauten E/A-Operationen aus als auch Kontrollflusseffekte hat).

Dem stimme ich ausdrücklich zu. await ist eine Ablaufsteuerungsoperation wie break oder return und sollte explizit sein. Die vorgeschlagene Postfix-Notation fühlt sich unnatürlich an, wie if Python: vergleichen Sie if c { e1 } else { e2 } mit e1 if c else e2 . Wenn Sie den Operator am Ende sehen, müssen Sie unabhängig von der Syntaxhervorhebung eine doppelte Aufnahme machen.

Ich sehe auch nicht, wie e.await mit der Rust-Syntax konsistenter ist als await!(e) oder await e . Es gibt kein anderes Postfix-Schlüsselwort, und da eine der Ideen darin bestand, es im Parser in Sonderbuchstaben zu schreiben, glaube ich nicht, dass dies ein Beweis für die Konsistenz ist.

Es wird auch das Vertrautheitsproblem @ withoutboats erwähnt. Wir können eine seltsame und wunderbare Syntax wählen, wenn sie einige wunderbare Vorteile bietet. Hat ein Postfix await sie jedoch?

Hat das Syntax-Highlighting keinen/unzureichenden Einfluss darauf, wie es aussieht und wie Ihr Gehirn Dinge verarbeitet?

(Gute Frage, ich bin mir sicher, dass es einige Auswirkungen haben würde, aber es ist schwer zu erraten, wie viel, ohne es tatsächlich zu versuchen (und ein anderes Schlüsselwort zu ersetzen kommt nur so weit). Wo wir beim Thema sind ... lange Zeit Ich habe vorhin erwähnt, dass ich denke, dass Syntaxhervorhebung alle Operatoren mit Kontrollflusseffekten hervorheben sollte ( return , break , continue , ? ... und jetzt await ) in einer speziellen, besonders markanten Farbe, aber ich bin nicht für die Syntaxhervorhebung für irgendetwas verantwortlich und weiß nicht, ob dies tatsächlich jemand tut.)

Dem stimme ich ausdrücklich zu. await ist eine Ablaufsteuerungsoperation wie break oder return und sollte explizit sein.

Sind wir uns einig. Die Notationen foo.await , foo await , foo# , ... sind explizit . Es gibt kein implizites Erwarten, das erledigt wird.

Ich sehe auch nicht, wie e.await mit der Rust-Syntax konsistenter ist als await!(e) oder await e .

Die Syntax e.await an sich stimmt nicht mit der Rust-Syntax überein, aber Postfix passt im Allgemeinen besser zu ? und wie Rust-APIs strukturiert sind (Methoden werden gegenüber freien Funktionen bevorzugt).

Die await e? Syntax, wenn sie als (await e)? verknüpft ist, stimmt völlig mit der Assoziation break und return überein. await!(e) ist auch inkonsistent, da wir keine Makros für den Kontrollfluss haben und es hat auch das gleiche Problem wie andere Präfix-Methoden.

Es gibt kein anderes Postfix-Schlüsselwort, und da eine der Ideen darin bestand, es im Parser in Sonderbuchstaben zu schreiben, glaube ich nicht, dass dies ein Beweis für die Konsistenz ist.

Ich glaube nicht, dass Sie die libsyntax für .await wirklich ändern müssen, da sie bereits als Feldoperation behandelt werden sollte. Die Logik wird eher in Resolve oder HIR behandelt, wo Sie sie in ein spezielles Konstrukt übersetzen.

Wir können eine seltsame und wunderbare Syntax wählen, wenn sie einige wunderbare Vorteile bietet. Hat ein Postfix await sie jedoch?

Wie bereits erwähnt, argumentiere ich, dass dies aufgrund der Methodenverkettung und Rusts Präferenz für Methodenaufrufe der Fall ist.

Ich glaube nicht, dass Sie die libsyntax für .await wirklich ändern müssen, da sie bereits als Feldoperation behandelt werden sollte.

Das macht Spaß.
Die Idee ist also, den Ansatz von self / super wiederzuverwenden, jedoch für Felder und nicht für Pfadsegmente.

Dies macht await effektiv zu einem Pfadsegment-Schlüsselwort (da es die Auflösung durchläuft), sodass Sie möglicherweise Rohkennungen dafür verbieten möchten.

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

Es gibt kein implizites Erwarten, das erledigt wird.

Die Idee kam in diesem Thread ein paar Mal auf (der Vorschlag "implizites Warten").

Wir haben keine Makros für den Kontrollfluss

Es gibt try! (das seinen Zweck ziemlich gut erfüllt hat) und wohl das veraltete select! . Beachten Sie, dass await "stärker" als return , daher ist es nicht unvernünftig zu erwarten, dass es im Code sichtbarer ist als ? return .

Ich behaupte, dass dies aufgrund der Methodenverkettung und der Präferenz von Rust für Methodenaufrufe der Fall ist.

Es hat auch eine (auffälligere) Präferenz für Präfix-Kontrollflussoperatoren.

Die warten e? Syntax, falls zugeordnet als (wait e)? ist völlig unvereinbar damit, wie sich Pause und Rückkehr verbinden.

Ich bevorzuge await!(e)? , await { e }? oder vielleicht sogar { await e }? -- Ich glaube nicht, dass ich letzteres diskutiert gesehen habe und ich bin mir nicht sicher, ob es funktioniert.


Ich gebe zu, dass es möglicherweise eine Vorspannung von links nach rechts gibt. _Notiz_

Meine Meinung dazu scheint sich jedes Mal zu ändern, wenn ich mir das Thema ansehe, als ob ich den Anwalt des Teufels für mich selbst spielen würde. Das liegt zum Teil daran, dass ich es so gewohnt bin, meine eigenen Futures und State Machines zu schreiben. Ein benutzerdefinierter Future mit poll ist völlig normal.

Vielleicht sollte man sich das anders überlegen.

Für mich beziehen sich Null-Kosten-Abstraktionen in Rust auf zwei Dinge: Null-Kosten zur Laufzeit und vor allem Null-Kosten im Kopf.

Ich kann sehr leicht über die meisten Abstraktionen in Rust nachdenken, einschließlich Futures, weil sie nur Zustandsautomaten sind.

Dazu sollte es eine einfache Lösung geben, die dem Benutzer möglichst wenig Magie einbringt. Vor allem Siegel sind eine schlechte Idee, da sie sich unnötig magisch anfühlen. Dies beinhaltet .await magische Felder.

Die vielleicht beste Lösung ist die einfachste, das ursprüngliche await! Makro.

Bei allem Respekt vor diesen anderen Sprachen scheint die interne Konsistenz in Rust wichtiger zu sein, und ich glaube nicht, dass die Präfixsyntax zu Rust passt, weder in Bezug auf die Rangfolge noch in Bezug auf die Struktur von APIs.

Ich verstehe nicht wie...? await(foo)? / await { foo }? scheint in Bezug auf die Operator-Priorität und die Struktur von APIs in Rust völlig in Ordnung zu sein verwirrend.

Es gibt try! (das seinen Zweck ziemlich gut erfüllt hat) und wohl das veraltete select! .

Ich denke, das entscheidende Wort hier ist veraltet . Mit try!(...) ist ein harter Fehler auf Rust 2018. Es ist ein schwerwiegender Fehler ist jetzt , weil wir eine bessere eingeführt, erstklassige und Postfix - Syntax.

Beachten Sie, dass await "stärker" ist als return , daher ist es nicht unvernünftig zu erwarten, dass es im Code sichtbarer ist als ? return .

Der ? Operator kann ebenfalls Nebeneffekte haben (durch andere Implementierungen als für Result ) und führt einen Kontrollfluss durch, so dass er auch ziemlich "stark" ist. Als es diskutiert wurde, wurde ? vorgeworfen, "eine Rendite zu verbergen" und leicht zu übersehen. Ich denke, diese Vorhersage hat sich völlig nicht bewahrheitet. Die Situation bzgl. await scheint mir ziemlich ähnlich zu sein.

Es hat auch eine (auffälligere) Präferenz für Präfix-Kontrollflussoperatoren.

Diese Präfix-Kontrollflussoperatoren werden ! Typ ? , der einen Kontext impl Try<Ok = T, ...> und Ihnen ein T gibt, ein Postfix.

Ich verstehe nicht wie...? await(foo)? / await { foo }? scheint in Bezug auf die Rangfolge der Operatoren und die Struktur der APIs in Rust völlig in Ordnung zu sein.

Die Syntax von await(foo) ist nicht dieselbe wie await foo wenn für erstere Klammern erforderlich sind und für letztere nicht. Ersteres ist beispiellos, letzteres hat Vorrangprobleme. ? wie wir hier, im Blogbeitrag von Boat und auf Discord besprochen haben. Die Syntax await { foo } ist aus anderen Gründen problematisch (siehe https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

seine Kehrseite ist die Wortwahl von Parens und (je nach Perspektive) Verkettung, die keinen Präzedenzfall bricht oder verwirrend ist.

Das meine ich mit "APIs sind strukturiert". Ich denke, Methoden und Methodenketten sind in Rust üblich und idiomatisch. Die Präfix- und Blocksyntax komponieren schlecht mit diesen und mit ? .

Mit dieser Meinung bin ich vielleicht in der Minderheit, und wenn ja, ignoriere mich:

Wäre es fair, die Diskussion über Präfix vs. Postfix in einen Internals-Thread zu verschieben und dann einfach mit dem Ergebnis hierher zurückzukommen? Auf diese Weise können wir das Tracking-Problem auf die Verfolgung des Status der Funktion beschränken?

@seanmonstar Ja, ich sympathisiere stark mit dem Wunsch, die Diskussion über Tracking-Probleme hier für uns zur Diskussion zu verwenden.

WICHTIG FÜR ALLE: Weitere Diskussionen über die await Syntax sollten hier erfolgen .

Vorübergehende Sperre für einen Tag, um sicherzustellen, dass zukünftige Diskussionen über die await Syntax zu dem entsprechenden Thema stattfinden.

Am Dienstag, den 15. Januar 2019 um 07:10:32 -0800 schrieb Pauan:

Randnotiz: Es ist immer möglich, Klammern zu verwenden, um die Rangfolge klarer zu machen:

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

Damit wird der Hauptvorteil von Postfix-Await zunichte gemacht: "einfach behalten"
Schreiben/Lesen". Postfix wait, wie postfix ? , ermöglicht Kontrollfluss
um sich von links nach rechts zu bewegen:

foo().await!()?.bar().await!()

Wenn await! ein Präfix war, oder zurück, wenn try! ein Präfix war, oder wenn Sie
zu klammern, dann musst du zurück auf die linke Seite von springen
der Ausdruck beim Schreiben oder Lesen.

BEARBEITEN: Ich habe Kommentare von Anfang bis Ende per E-Mail gelesen und die Kommentare "Konversation zum anderen Problem verschieben" erst nach dem Senden dieser E-Mail gesehen.

Async-Warten-Statusbericht:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Ich wollte ein kurzes Update zum Status des Async-Wait posten
Anstrengung. Die Kurzfassung ist, dass wir auf der Zielgeraden sind für
eine Art Stabilisierung, aber es bleiben einige signifikante
Fragen zu überwinden.

Ankündigung der Umsetzungsarbeitsgruppe

Als Teil dieses Vorstoßes freue ich mich, Ihnen mitteilen zu können, dass wir a . gegründet haben
Arbeitsgruppe für async-await-Implementierung . Diese Arbeitsgruppe
ist Teil der gesamten Bemühungen um asynchrones Warten, konzentriert sich jedoch auf die
Implementierung und ist Teil des Compiler-Teams. Wenn Sie möchten
Helfen Sie mit, Async-Warten über die Ziellinie zu bringen, wir haben eine Liste mit Problemen
wo wir auf jeden Fall Hilfe brauchen (lesen Sie weiter).

Bei Interesse an einer Teilnahme haben wir eine "Sprechstunde"für Dienstag geplant (siehe [Kalender des Compilerteams]) -- wenn Sie
kann dann auf [Zulip] auftauchen, das wäre ideal! (Aber wenn nicht, schau einfach irgendwas rein
Zeit.)

...

Wann wird std::future::Future im Stall sein? Muss es auf async warten warten? Ich denke, es ist ein sehr schönes Design und würde gerne damit beginnen, Code darauf zu portieren. (Gibt es eine Unterlegscheibe, um es im Stall zu verwenden?)

@ry siehe das neue Tracking-Problem dafür: https://github.com/rust-lang/rust/issues/59113

Ein weiteres Compilerproblem für async/await: https://github.com/rust-lang/rust/issues/59245

Beachten Sie auch, dass https://github.com/rust-lang-nursery/futures-rs/issues/1199 im oberen Beitrag abgehakt werden kann, da dies jetzt behoben ist.

Es scheint ein Problem mit HRLB und asynchronen Schließungen zu geben: https://github.com/rust-lang/rust/issues/59337. (Obwohl das erneute Skimming des RFC nicht angibt, dass asynchrone Schließungen derselben Argumentlebensdauer-Erfassung unterliegen wie die asynchrone Funktion).

Ja, asynchrone Schließungen haben eine Reihe von Problemen und sollten nicht in die erste Stabilisierungsrunde aufgenommen werden. Das aktuelle Verhalten kann mit einem Closure + Async-Block emuliert werden, und in Zukunft würde ich gerne eine Version sehen, die es ermöglicht, auf die Upvars der Closure aus der zurückgegebenen Zukunft zu verweisen.

Mir ist gerade aufgefallen, dass await!(fut) derzeit erfordert, dass fut Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

Ist das zu erwarten? Es scheint nicht im RFC zu sein.

@Ekleog dass nicht ist await! den Fehler geben, await! konzeptionell Stapel-pins der übergebene Zukunft zu ermöglichen !Unpin (verwendet wird Futures auf schnellen Spielplatz Beispiel ). Der Fehler kommt von der Einschränkung von impl Future for Box<impl Future + Unpin> , die erfordert, dass die Zukunft Unpin , um Sie daran zu hindern, Folgendes zu tun:

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

Da Box Unpin und es ermöglicht, den Wert daraus zu verschieben, können Sie die Zukunft einmal an einem Heap-Speicherort abfragen, dann die Zukunft aus der Box verschieben und an einen neuen Heap-Speicherort legen und abfragen es wieder.

Await sollte möglicherweise ein Sonderfall sein, um Box<dyn Future> zuzulassen, da es die Zukunft verbraucht

Vielleicht sollte die Eigenschaft IntoFuture für await! wiederbelebt werden? Box<dyn Future> kann dies implementieren, indem es in Pin<Box<dyn Future>> konvertiert wird.

Hier kommt mein nächster Fehler mit async/await: Es sieht so aus, als ob ein zugeordneter Typ einem Typparameter im Rückgabetyp einer async fn Unterbrechungsinferenz verwendet wird: https://github.com/rust-lang/rust/ Ausgaben/60414

Zusätzlich zum möglichen Hinzufügen von #60414 zur Liste des Top-Posts (weiß nicht, ob es noch verwendet wird – vielleicht wäre es besser, auf das Github-Label zu verweisen?), denke ich, dass die „Resolution of rust-lang/ rfcs#2418“ angekreuzt werden, da IIRC das Merkmal Future kürzlich stabilisiert wurde.

Ich komme gerade von einem Reddit-Post und muss sagen, dass ich die Postfix-Syntax überhaupt nicht mag. Und es scheint, als ob die Mehrheit von Reddit es auch nicht mag.

ich schreibe lieber

let x = (await future)?

als diese seltsame Syntax zu akzeptieren.

Was die Verkettung betrifft, kann ich meinen Code umgestalten, um zu vermeiden, dass mehr als 1 await .

Auch JavaScript kann dies in Zukunft tun ( Vorschlag für eine intelligente Pipeline ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Wenn das Präfix await implementiert ist, bedeutet das nicht, dass await nicht verkettet werden kann.

@KSXGitHub dies ist wirklich nicht der https://boats.gitlab.io/blog/post /warten-entscheidung/

@KSXGitHub Obwohl ich auch die endgültige Syntax nicht mag, wurde sie in #57640, https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https://internals.rust- ausführlich diskutiert.

Bitte diskutieren Sie hier nicht die Designentscheidungen, es gibt einen Thread für diesen expliziten Zweck

Wenn Sie vorhaben, dort zu kommentieren, denken Sie bitte daran, dass die Diskussion bereits ziemlich ins Rollen gekommen ist: Stellen Sie sicher, dass Sie etwas Wesentliches zu sagen haben, und stellen Sie sicher, dass dies noch nicht im Thread gesagt wurde.

@ withoutboats Nach meinem Verständnis ist die endgültige Syntax bereits vereinbart, vielleicht ist es an der Zeit, sie als Fertig zu markieren? :erröten:

Besteht die Absicht, sich rechtzeitig für die nächste Betaversion am 4. Juli zu stabilisieren, oder wird das Blockieren von Fehlern einen weiteren Zyklus zur Behebung erfordern? Es gibt viele offene Probleme unter dem Tag A-async-await, obwohl ich nicht sicher bin, wie viele davon kritisch sind.

Aha, ignorieren Sie das, ich habe gerade das AsyncAwait-Blocking- Label entdeckt.

Hallo! Wann müssen wir mit der stabilen Veröffentlichung dieser Funktion rechnen? Und wie kann ich das in nächtlichen Builds verwenden?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 enthält Informationen zum angestrebten Release-Datum und mehr

Ist geplant, Unpin automatisch für Futures zu implementieren, die von async fn generiert werden?

Insbesondere frage ich mich, ob Unpin aufgrund des generierten Future-Codes selbst nicht automatisch verfügbar ist oder ob wir Referenzen als Argumente verwenden können

@DoumanAsh Ich nehme an, wenn ein asynchroner Fn niemals aktive Selbstreferenzen an Fließpunkten hat, könnte die generierte Future möglicherweise Unpin implementieren?

Ich denke, das müsste von einigen ziemlich hilfreichen Fehlermeldungen begleitet werden, die sagen "nicht Unpin wegen _diesem_ Ausleihen" + einem Hinweis "alternativ können Sie diese Zukunft boxen"

Der Stabilisierungs-PR unter #63209 stellt fest, dass "Alle Blocker sind jetzt geschlossen." und wurde am 20. August in Nightly gelandet, sodass wir später in dieser Woche auf den Beta-Schnitt zusteuern. Es scheint erwähnenswert, dass seit dem 20. August einige neue Blockierungsprobleme eingereicht wurden (wie durch das AsyncAwait-Blocking-Tag verfolgt). Zwei davon (#63710, #64130) scheinen Nice-to-haves zu sein, die die Stabilisierung nicht wirklich behindern würden, aber es gibt drei andere Probleme (#64391, #64433, #64477), die es wert sind, diskutiert zu werden. Diese drei letztgenannten Probleme sind miteinander verbunden, und sie alle entstanden aufgrund von PR #64292, das selbst gelandet wurde, um das AsyncAwait-Blocking-Problem #63832 zu beheben. Ein PR, #64584, ist bereits gelandet, um den Großteil der Probleme anzugehen, aber die drei Probleme bleiben vorerst offen.

Der Silberstreif am Horizont ist, dass die drei ernsthaften offenen Blocker Code betreffen, der kompiliert werden sollte , aber derzeit nicht kompiliert wird. In diesem Sinne wäre es abwärtskompatibel, Fixes später zu landen, ohne die Betaisierung und eventuelle Stabilisierung von Async/Await zu behindern. Aber ich frage mich , ob jemand aus dem Team lang denkt hier , dass alles über genug , um vermuten , dass async / await sollte jede Nacht für einen weiteren Zyklus backen (die als geschmacklos wie das klingen mag, ist der Punkt , der schnellen Release - Zeitplan schließlich).

@bstrie Wir verwenden "AsyncAwait-Blocking" nur wieder, weil es keine bessere Bezeichnung gibt, um sie als "hohe Priorität" zu kennzeichnen, sie blockieren nicht wirklich. Wir sollten das Kennzeichnungssystem bald überarbeiten, um es weniger verwirrend zu machen, cc @nikomatsakis.

... Nicht gut... wir haben async-await in der erwarteten Version 1.38 verpasst. Musste auf 1.39 warten, nur weil einige "Probleme" nicht zählten...

@earthengine Ich denke, das ist keine faire Einschätzung der Situation. Die aufgetretenen Probleme waren alle es wert, ernst genommen zu werden. Es wäre nicht gut, asynchrones Warten zu landen, nur damit Leute auf diese Probleme stoßen, die versuchen, es in der Praxis zu verwenden :)

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen