Rust: Lösen Sie die "Warten" -Syntax auf

Erstellt am 15. Jan. 2019  ·  512Kommentare  ·  Quelle: rust-lang/rust

Bevor Sie in diesem Thread einen Kommentar abgeben, überprüfen Sie bitte https://github.com/rust-lang/rust/issues/50547 und versuchen Sie zu überprüfen, ob Sie keine Argumente duplizieren, die dort bereits vorgebracht wurden.


Notizen von Hirten:

Wenn Sie neu in diesem Thread sind, beginnen Sie mit https://github.com/rust-lang/rust/issues/57640#issuecomment -456617889, gefolgt von drei großartigen zusammenfassenden Kommentaren, von denen die letzten https waren: //github.com/rust-lang/rust/issues/57640#issuecomment -457101180. (Danke, @traviscross!)

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

Hilfreichster Kommentar

Ich dachte, es könnte nützlich sein, aufzuschreiben, wie andere Sprachen mit einem erwarteten Konstrukt umgehen.


Kotlin

val result = task.await()

C.

var result = await task;

F.

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Hacken

$result = await task;

Pfeil

var result = await task;

Denken Sie bei all dem daran, dass Rust-Ausdrücke zu mehreren verketteten Methoden führen können. Die meisten Sprachen neigen dazu, dies nicht zu tun.

Alle 512 Kommentare

Ich dachte, es könnte nützlich sein, aufzuschreiben, wie andere Sprachen mit einem erwarteten Konstrukt umgehen.


Kotlin

val result = task.await()

C.

var result = await task;

F.

let! result = task()

Scala

val result = Await.result(task, timeout)

Python

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Hacken

$result = await task;

Pfeil

var result = await task;

Denken Sie bei all dem daran, dass Rust-Ausdrücke zu mehreren verketteten Methoden führen können. Die meisten Sprachen neigen dazu, dies nicht zu tun.

Denken Sie bei all dem daran, dass Rust-Ausdrücke zu mehreren verketteten Methoden führen können. Die meisten Sprachen neigen dazu, dies nicht zu tun.

Ich würde sagen, dass Sprachen, die Erweiterungsmethoden unterstützen, diese haben. Dazu gehören Rust, Kotlin, C # (z. B. Methodensyntax LINQ und verschiedene Builder) und F #, obwohl letzterer den Pipe-Operator für denselben Effekt häufig verwendet.

Rein anekdotisch meinerseits, aber ich laufe regelmäßig in mehr als einem Dutzend verketteten Ausdrücken in Rust-Code in freier Wildbahn und es liest und läuft gut. Ich habe das anderswo nicht erlebt.

Ich würde gerne sehen, dass dieses Problem im oberen Beitrag von # 50547 (neben dem Kontrollkästchen "Endgültige Syntax zum Warten") erwähnt wurde.

Kotlin

val result = task.await()

Kotlins Syntax lautet:

val result = doTask()

Das await ist nur ein suspendable function , keine erstklassige Sache.

Vielen Dank, dass Sie das erwähnt haben. Kotlin fühlt sich impliziter, weil Futures standardmäßig eifrig sind. Es ist jedoch immer noch ein gängiges Muster in einem verzögerten Block, diese Methode zu verwenden, um auf andere verzögerte Blöcke zu warten. Ich habe es sicherlich mehrmals gemacht.

@cramertj Könnten Sie die dort https://github.com/rust-lang/rust/issues/50547 276 Kommentare gibt? (Vielleicht fügen Sie sie hier zum OP hinzu?)

Kotlin fühlt sich impliziter, weil Futures standardmäßig eifrig sind. Es ist jedoch immer noch ein gängiges Muster in einem verzögerten Block, diese Methode zu verwenden, um auf andere verzögerte Blöcke zu warten. Ich habe es sicherlich mehrmals gemacht.

Vielleicht sollten Sie beide Anwendungsfälle mit ein wenig Kontext / Beschreibung hinzufügen.

Auch was ist mit anderen langs, die implizite Wartezeiten verwenden, wie go-lang?

Ein Grund, sich für eine Post-Fix-Syntax zu entscheiden, besteht darin, dass sich ein Warten aus Sicht des Aufrufers ähnlich wie ein Funktionsaufruf verhält: Sie geben die Flusskontrolle auf und wenn Sie es zurückerhalten, wartet ein Ergebnis auf Ihren Stapel. In jedem Fall würde ich eine Syntax bevorzugen, die das funktionsähnliche Verhalten berücksichtigt, indem sie Funktionsparanthesis enthält. Und es gibt gute Gründe, die Konstruktion von Coroutinen von ihrer ersten Ausführung an aufzuteilen, damit dieses Verhalten zwischen synchronen und asynchronen Blöcken konsistent ist.

Aber während der implizite Coroutine-Stil diskutiert wurde und ich auf der Seite der Explizität stehe , könnte es nicht explizit genug sein .await!() ist mehr oder weniger ein Versuch, zwischen normalen Anrufen und Coroutine-Anrufen zu unterscheiden.

Nachdem wir hoffentlich eine etwas neue Sichtweise gegeben haben, warum Post-Fix bevorzugt werden könnte, ein bescheidener Vorschlag für die Syntax:

  • future(?)
  • oder future(await) das natürlich seine eigenen Kompromisse hat, aber als weniger verwirrend akzeptiert zu werden scheint, siehe unten im Beitrag.

Anpassen eines ziemlich beliebten Beispiels aus einem anderen Thread (unter der Annahme, dass logger.log auch eine Coroutine ist, um zu zeigen, wie ein sofortiger Aufruf aussieht):

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.log("beginning service call")(?);
   let output = service(?); // Actually wait for its result
   self.logger.log("foo executed with result {}.", output)(?);
   output
}

Und mit der Alternative:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.log("beginning service call")(await);
   let output = service(await);
   self.logger.log("foo executed with result {}.", output)(await);
   output
}

Um unlesbaren Code zu vermeiden und das Parsen zu erleichtern, lassen Sie nur Leerzeichen nach dem Fragezeichen ein, nicht zwischen diesem und dem geöffneten Paran. future(? ) ist also gut, future( ?) nicht. Dieses Problem tritt nicht bei future(await) bei dem alle aktuellen Token wie zuvor verwendet werden können.

Die Interaktion mit anderen Post-Fix-Operatoren (wie dem aktuellen ? -Versuch) ist ebenfalls genau wie bei Funktionsaufrufen:

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(?);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(?)?;
    logger.timestamp()(?);
    Ok(length)
}

Oder

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = acquire_lock()(await);
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.log_into(message)(await)?;
    logger.timestamp()(await);
    Ok(length)
}

Ein paar Gründe, dies zu mögen:

  • Im Vergleich zu .await!() es nicht auf ein Mitglied an, das andere Verwendungszwecke haben könnte.
  • Es folgt der natürlichen Priorität von Anrufen, wie z. B. Verketten und Verwenden von ? . Dies hält die Anzahl der Präzisionsklassen niedriger und hilft beim Lernen. Und Funktionsaufrufe waren in der Sprache immer etwas Besonderes (obwohl sie eine Eigenschaft haben), so dass nicht zu erwarten ist, dass Benutzercode ihre eigenen my_await!() , die eine sehr ähnliche Syntax und Wirkung haben.
  • Dies könnte sich auf Generatoren und Streams sowie auf Generatoren verallgemeinern, die erwarten, dass bei der Wiederaufnahme mehr Argumente bereitgestellt werden. Im Wesentlichen verhält sich dies wie ein FnOnce während sich Streams wie ein FnMut verhalten würde. Zusätzliche Argumente können ebenfalls leicht berücksichtigt werden.
  • Für diejenigen, die zuvor das aktuelle Futures haben, wird erfasst, wie ein ? mit Poll die ganze Zeit hätte funktionieren sollen (unglückliche Stabilisierungssequenz hier). Als Lernschritt entspricht dies auch der Erwartung, dass ein auf ? basierender Operator den Kontrollfluss umleitet. (await) würde dies andererseits nicht erfüllen, aber schließlich wird die Funktion immer erwarten, an dem abweichenden Punkt wieder aufgenommen zu werden.
  • Es wird eine funktionsähnliche Syntax verwendet, obwohl dieses Argument nur gut ist, wenn Sie mir zustimmen: smile:

Und Gründe, dies nicht zu mögen:

  • ? scheint ein Argument zu sein, wird aber nicht einmal auf einen Ausdruck angewendet. Ich glaube, dies könnte durch Lehren gelöst werden, da das Token, auf das es angewendet zu werden scheint, der Funktionsaufruf selbst ist, was der etwas korrekte Begriff ist. Dies bedeutet auch positiv, dass die Syntax eindeutig ist, hoffe ich.
  • Mehr (und andere) Mischung aus Paranthesis und ? kann schwierig zu analysieren sein. Besonders wenn eine Zukunft ein Ergebnis einer anderen Zukunft zurückgibt: construct_future()(?)?(?)? . Sie könnten jedoch das gleiche Argument vorbringen, um ein Ergebnis eines fn -Objekts zu erhalten, was dazu führt, dass ein Ausdruck wie dieser zulässig ist: foobar()?()?()? . Da ich dies jedoch noch nie benutzt oder beanstandet habe, scheint es in solchen Fällen selten genug erforderlich zu sein, es in separate Aussagen aufzuteilen. Dieses Problem besteht auch nicht für construct_future()(await)?(await)? -
  • future(?) ist mein bester Schuss bei einer knappen und immer noch etwas prägnanten Syntax. Die Argumentation basiert jedoch auf Implementierungsdetails in Coroutinen (vorübergehende Rückgabe und Versand nach Wiederaufnahme), die es möglicherweise für eine Abstraktion ungeeignet machen. future(await) wäre eine Alternative, die noch erklärbar sein könnte, nachdem await als Schlüsselwort verinnerlicht wurde, aber die Argumentposition ist für mich etwas schwer zu schlucken. Es könnte in Ordnung sein, und es ist sicherlich besser lesbar, wenn die Coroutine ein Ergebnis zurückgibt.
  • Eingriffe in andere Funktionsaufrufvorschläge?
  • Dein eigenes? Sie brauchen es nicht zu mögen, es fühlte sich einfach wie eine Verschwendung an, diese knappe Post-Fix-Syntax nicht zumindest vorzuschlagen.

future(?)

Result nichts Besonderes: Futures können jeden Rust-Typ zurückgeben. Es kommt einfach so vor, dass einige Futures Result

Wie würde das für Futures funktionieren, die nicht Result ?

Es scheint nicht klar zu sein, was ich meinte. future(?) wurde zuvor als future.await!() oder ähnliches besprochen. Die Verzweigung auf eine Zukunft, die ebenfalls ein Ergebnis liefert, wäre future(?)? (zwei verschiedene Möglichkeiten, wie wir den Kontrollfluss frühzeitig aufgeben können). Dies macht zukünftige Abfragen (?) und Ergebnistests ? orthogonal. Bearbeiten: Ein zusätzliches Beispiel dafür wurde hinzugefügt.

Die Verzweigung auf eine Zukunft, die ebenfalls ein Ergebnis liefert, wäre future(?)?

Danke fürs klarstellen. In diesem Fall bin ich definitiv kein Fan davon.

Das bedeutet, dass der Aufruf einer Funktion, die ein Future<Output = Result<_, _>> zurückgibt, wie foo()(?)?

Es ist sehr syntaxlastig und verwendet ? für zwei völlig unterschiedliche Zwecke.

Wenn es speziell der Hinweis auf den Operator ? ist, der schwer ist, könnte man ihn natürlich durch das neu reservierte Schlüsselwort ersetzen. Ich hatte nur anfangs gedacht, dass dies zu sehr ein tatsächliches Argument von rätselhaftem Typ ist, aber der Kompromiss könnte dazu beitragen, die Aussage mental zu analysieren. Die gleiche Aussage für impl Future<Output = Result<_,_>> würde also lauten:

  • foo()(await)?

Das beste Argument, warum ? angemessen ist, ist, dass der verwendete interne Mechanismus etwas ähnlich ist (andernfalls könnten wir Poll in aktuellen Bibliotheken nicht verwenden), aber dies könnte den Punkt einer guten Abstraktion verfehlen.

Es ist sehr syntaxlastig

Ich dachte, das ist der springende Punkt beim expliziten Warten?

es verwendet ? für zwei völlig unterschiedliche Zwecke.

Ja, also wäre die foo()(await) -Syntax viel schöner.

Diese Syntax ähnelt dem Aufrufen einer Funktion, die einen Abschluss zurückgibt, und dem Aufrufen dieses Abschlusses in JS.

Meine Lektüre von "syntaxlastig" war näher an "Siegellastig", eine Sequenz von ()(?)? ist ziemlich irritierend. Dies wurde im ursprünglichen Beitrag erwähnt:

Mehr (und andere) Mischung aus Paranthesis und ? kann schwierig zu analysieren sein. Besonders wenn eine Zukunft ein Ergebnis einer anderen Zukunft zurückgibt: construct_future()(?)?(?)?

Sie könnten jedoch das gleiche Argument vorbringen, um ein Ergebnis eines fn -Objekts zu erhalten, was dazu führt, dass ein Ausdruck wie dieser zulässig ist: foobar()?()?()? . Da ich dies jedoch noch nie benutzt oder beanstandet habe, scheint es in solchen Fällen selten genug erforderlich zu sein, es in separate Aussagen aufzuteilen.

Ich denke, die Gegenargumentation hier lautet: Wie oft haben Sie -> impl Fn in freier Wildbahn gesehen (geschweige denn -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _> )? Wie oft erwarten Sie -> impl Future<Output = Result<_, _>> in einer asynchronen Codebasis? Die Angabe eines seltenen Rückgabewerts von impl Fn , um das Lesen des Codes zu erleichtern, unterscheidet sich stark von der Angabe eines erheblichen Teils der temporären Rückgabewerte von impl Future .

Das Benennen eines seltenen impl Fn-Rückgabewerts, um das Lesen des Codes zu erleichtern, unterscheidet sich stark vom Benennen eines signifikanten Teils der temporären impl Future-Rückgabewerte.

Ich sehe nicht, wie diese Wahl der Syntax einen Einfluss darauf hat, wie oft Sie Ihren Ergebnistyp explizit benennen müssen. Ich glaube nicht, dass es die Typinferenz anders beeinflusst als await? future .

Sie alle haben hier jedoch sehr gute Punkte gemacht, und je mehr Beispiele ich erfinde (ich habe den ursprünglichen Beitrag so bearbeitet, dass er immer beide Syntaxversionen enthält), desto mehr neige ich selbst zu future(await) . Es ist nicht unangemessen zu tippen und behält dennoch die Klarheit der Funktionsaufrufsyntax bei, die dies hervorrufen sollte.

Wie oft erwarten Sie -> impl Future> in einer asynchronen Codebasis?

Ich erwarte, dass das Typäquivalent dazu (ein asynchrones fn, das ein Ergebnis zurückgibt) die ganze Zeit angezeigt wird, wahrscheinlich sogar die Mehrheit aller asynchronen fns, denn wenn Sie auf ein E / A warten, werden Sie mit ziemlicher Sicherheit werfen E / A-Fehler nach oben.


Verlinkung zu meinem vorherigen Beitrag zum Thema Tracking und Hinzufügen einiger weiterer Gedanken.

Ich denke, es gibt sehr wenig Chancen, dass eine Syntax, die die Zeichenfolge await nicht enthält, für diese Syntax akzeptiert wird. Ich denke, an diesem Punkt wäre es nach einem Jahr Arbeit an dieser Funktion produktiver, zu versuchen, die Vor- und Nachteile der bekannten Alternativen abzuwägen, um herauszufinden, welche am besten ist, als neue Syntaxen vorzuschlagen. Die Syntax, die ich denke, ist angesichts meiner vorherigen Beiträge realisierbar:

  • Das Präfix wartet mit obligatorischen Trennzeichen. Hier ist dies auch eine Entscheidung darüber, welche Begrenzer (entweder Klammern oder Parens oder beide akzeptieren; alle haben ihre eigenen Vor- und Nachteile). Das heißt, await(future) oder await { future } . Dies löst die Vorrangprobleme vollständig, ist jedoch syntaktisch verrauscht und beide Trennzeichenoptionen bieten mögliche Verwirrungsquellen.
  • Das Präfix wartet mit der "nützlichen" Priorität in Bezug auf ? . (Das heißt, das Warten bindet fester als?). Dies mag einige Benutzer überraschen, die Code lesen, aber ich glaube, dass Funktionen, die Futures von Ergebnissen zurückgeben, überwiegend häufiger sind als Funktionen, die Futures von Ergebnissen zurückgeben.
  • Das Präfix wartet mit der "offensichtlichen" Priorität in Bezug auf ? . (Das heißt, das? Bindet fester als erwartet). Zusätzliche Syntax Zucker await? für ein kombiniertes Warten und? Operator. Ich denke, dieser Syntaxzucker ist notwendig, um diese Rangfolge überhaupt realisierbar zu machen, sonst schreibt jeder die ganze Zeit (await future)? , was eine schlechtere Variante der ersten Option ist, die ich aufgezählt habe.
  • Postfix warten mit dem Syntaxraum warten. Dies löst das Vorrangproblem, indem eine klare visuelle Reihenfolge zwischen den beiden Operatoren besteht. Diese Lösung ist mir in vielerlei Hinsicht unangenehm.

Mein eigenes Ranking unter diesen Entscheidungen ändert sich jedes Mal, wenn ich das Problem untersuche. Ab diesem Moment denke ich, dass die Verwendung des offensichtlichen Vorrangs mit dem Zucker das beste Gleichgewicht zwischen Ergonomie, Vertrautheit und Verständnis zu sein scheint. Aber in der Vergangenheit habe ich eine der beiden anderen Präfixsyntaxen bevorzugt.

Zur Diskussion werde ich diesen vier Optionen folgende Namen geben:

Name | Zukunft | Zukunft des Ergebnisses | Ergebnis der Zukunft
--- | --- | --- | --- ---.
Obligatorische Trennzeichen await(future) oder await { future } | await(future)? oder await { future }? | await(future?) oder await { future? }
Nützlicher Vorrang await future | await future? | await (future?)
Offensichtliche Priorität mit Zucker await future | await? future oder (await future)? | await future?
Postfix-Schlüsselwort | future await | future await? | future? await

(Ich habe speziell "postfix keyword" verwendet, um diese Option von anderen Postfix-Syntaxen wie "postfix macro" zu unterscheiden.)

Einer der Nachteile des "Segens" von await future? in der nützlichen Priorität, aber auch anderer, die nicht als Nachkorrektur funktionieren, besteht darin, dass die üblichen Muster der manuellen Konvertierung von Ausdrücken mit ? möglicherweise nicht mehr gelten. oder verlangen, dass Future die Result -Methoden explizit auf kompatible Weise repliziert. Ich finde das überraschend. Wenn sie repliziert werden, wird es plötzlich so verwirrend, welche der Kombinatoren an einer zurückgegebenen Zukunft arbeiten und welche eifrig sind. Mit anderen Worten, es wäre genauso schwer zu entscheiden, was ein Kombinator tatsächlich tut, wie im Fall eines impliziten Wartens. (Bearbeiten: siehe zwei Kommentare unten, wo ich eine technischere Perspektive habe, was ich mit überraschendem Ersetzen von ? meine)

Ein Beispiel, in dem wir einen Fehlerfall beheben können:

async fn previously() -> Result<_, lib::Error> {
    let _ = await get_result()?;
}

async fn with_recovery() -> Result<_, lib::Error> {
    // Does `or_recover` return a future or not? Suddenly very important but not visible.
    let _ = await get_result().unwrap_or_else(or_recover);
    // If `or_recover` is sync, this should still work as a pattern of replacing `?` imho.
    // But we also want `or_recover` returning a future to work, as a combinator for futures?

    // Resolving sync like this just feel like wrong precedence in a number of ways
    // Also, conflicts with `Result of future` depending on choice.
    let _ = await get_result()?.unwrap_or_else(or_recover);
}

Dieses Problem tritt bei tatsächlichen Post-Fix-Operatoren nicht auf:

async fn with_recovery() -> Result<_, lib::Error> {
    // Also possible in 'space' delimited post-fix await route, but slightly less clear
    let _ = get_result()(await)
        // Ah, this is sync
        .unwrap_or_else(or_recover);
    // This would be future combinator.
    // let _ = get_result().unwrap_or_else(or_recover)(await);
}
// Obvious precedence syntax
let _ = await get_result().unwrap_or_else(or_recover);
// Post-fix function argument-like syntax
let _ = get_result()(await).unwrap_or_else(or_recover);

Dies sind verschiedene Ausdrücke. Der Punktoperator hat eine höhere Priorität als der Operator "offensichtliche Priorität" await Das Äquivalent lautet also:

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

Dies hat genau die gleiche Mehrdeutigkeit, ob or_recover asynchron ist oder nicht. (Was meiner Meinung nach keine Rolle spielt, Sie wissen, dass der Ausdruck als Ganzes asynchron ist, und Sie können sich die Definition von or_recover ansehen, wenn Sie aus irgendeinem Grund wissen müssen, ob dieser bestimmte Teil asynchron ist.)

Dies hat genau die gleiche Mehrdeutigkeit, ob or_recover asynchron ist oder nicht.

Nicht genau das gleiche. unwrap_or_else muss eine Coroutine erzeugen, da sie erwartet wird. Die Unklarheit ist also, ob get_result eine Coroutine (also wird ein Kombinator gebaut) oder eine Result<impl Future, _> (und Ok enthält bereits eine Coroutine und Err eine). Beide haben nicht die gleichen Bedenken, einen Effizienzgewinn auf einen Blick erkennen zu können, indem ein await Sequenzpunkt auf einen join , was eines der Hauptprobleme von ist implizit warten. Der Grund hierfür ist , dass in jedem Fall diese Zwischenrechen sync sein muß und vor dem Await auf den Typ angewendet worden sein und müssen in der erwarteten Koroutine geführt haben. Hier gibt es einander größere Bedenken:

Dies sind verschiedene Ausdrücke. Der Punktoperator hat eine höhere Priorität als der Operator "Offensichtliche Priorität", also das Äquivalent

Das ist ein Teil der Verwirrung. Durch das Ersetzen von ? durch eine Wiederherstellungsoperation wurde die Position von await grundlegend geändert. Im Kontext der ? -Syntax erwarte ich bei einem Teilausdruck expr vom Typ T die folgende Semantik von einer Transformation (unter der Annahme, dass T::unwrap_or_else existiert) ::

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

Unter 'Nützlicher Vorrang' und await expr? ( await expr ergibt T ) erhalten wir stattdessen

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

Während in offensichtlichem Vorrang diese Transformation ohne zusätzliche Paranthesis überhaupt nicht mehr gilt, funktioniert zumindest die Intuition immer noch für 'Ergebnis der Zukunft'.

Und was ist mit dem noch interessanteren Fall, in dem Sie an zwei verschiedenen Punkten in einer Kombinatorsequenz warten? Bei jeder Präfixsyntax sind meiner Meinung nach Klammern erforderlich. Der Rest der Rust-Sprache versucht dies ausführlich zu vermeiden, damit "Ausdrücke von links nach rechts ausgewertet werden". Ein Beispiel hierfür ist die automatische Ref-Magie.

Beispiel, um zu zeigen, dass dies bei längeren Ketten mit mehreren Warte- / Versuchs- / Kombinationspunkten noch schlimmer wird.

// Chain such that we
// 1. Create a future computing some partial result
// 2. wait for a result 
// 3. then recover to a new future in case of error, 
// 4. then try its awaited result. 
async fn await_chain() -> Result<usize, Error> {
    // Mandatory delimiters
    let _ = await(await(partial_computation()).unwrap_or_else(or_recover))?
    // Useful precedence requires paranthesis nesting afterall
    let _ = await { await partial_computation() }.unwrap_or_else(or_recover)?;
    // Obivious precendence may do slightly better, but I think confusing left-right-jumps after all.
    let _ = await? (await partial_computation()).unwrap_or_else(or_recover);
    // Post-fix
    let _ = partial_computation()(await).unwrap_or_else(or_recover)(await)?;
}

Was ich gerne vermieden sehen würde, ist das Erstellen des Rust-Analogons von Cs Typ-Parsing, zwischen dem Sie springen
linke und rechte Ausdrucksseite für Kombinatoren 'Zeiger' und 'Array'.

Tabelleneintrag im Stil von @withoutboats :

| Name | Zukunft | Zukunft des Ergebnisses | Ergebnis der Zukunft |
| - | - | - | - |
| Obligatorische Trennzeichen await(future) | await(future)? | await(future?) |
| Nützlicher Vorrang await future | await future? | await (future?) |
| Offensichtliche Priorität | await future | await? future | await future? |
| Postfix Call | future(await) | future(await)? | future?(await) |

| Name | Verkettet |
| - | - |
| Obligatorische Trennzeichen await(await(foo())?.bar())? |
| Nützlicher Vorrang await(await foo()?).bar()? |
| Offensichtliche Priorität | await? (await? foo()).bar() |
| Postfix Call | foo()(await)?.bar()(await) |

Ich bin aus verschiedenen Gründen stark für ein Postfix- Warten , aber ich mag die von nicht , hauptsächlich scheint es aus den gleichen Gründen. Z.B. foo await.method() ist verwirrend.

Schauen wir uns zunächst eine ähnliche Tabelle an, fügen aber einige weitere Postfix-Varianten hinzu:

| Name | Zukunft | Zukunft des Ergebnisses | Ergebnis der Zukunft |
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| Obligatorische Trennzeichen await { future } | await { future }? | await { future? } |
| Nützlicher Vorrang await future | await future? | await (future?) |
| Offensichtliche Priorität | await future | await? future | await future? |
| Postfix-Schlüsselwort | future await | future await? | future? await |
| Postfix-Feld | future.await | future.await? | future?.await |
| Postfix-Methode | future.await() | future.await()? | future?.await() |

Schauen wir uns nun einen verketteten zukünftigen Ausdruck an:

| Name | Verkettete Zukunft der Ergebnisse |
| ---------------------- | -------------------------- ----------- |
| Obligatorische Trennzeichen await { await { foo() }?.bar() }? |
| Nützlicher Vorrang await (await foo()?).bar()? |
| Offensichtliche Priorität | await? (await? foo()).bar() |
| Postfix-Schlüsselwort | foo() await?.bar() await? |
| Postfix-Feld | foo().await?.bar().await? |
| Postfix-Methode | foo().await()?.bar().await()? |

Und nun zu einem realen Beispiel aus reqwests , wo Sie vielleicht auf eine verkettete Zukunft der Ergebnisse warten möchten (unter Verwendung meines bevorzugten Warteformulars).

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

Eigentlich denke ich, dass jedes Trennzeichen für die Postfix-Syntax gut aussieht, zum Beispiel:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
Aber ich habe keine feste Meinung darüber, welche ich verwenden soll.

Könnte ein Postfix-Makro (dh future.await!() ) noch eine Option sein? Es ist klar, prägnant und eindeutig:

| Zukunft | Zukunft des Ergebnisses | Ergebnis der Zukunft |
| --- | --- | --- |
| future.await! () | future.await! ()? | Zukunft? .await! () |

Auch die Implementierung eines Postfix-Makros erfordert weniger Aufwand und ist leicht zu verstehen und zu verwenden.

Auch die Implementierung eines Postfix-Makros erfordert weniger Aufwand und ist leicht zu verstehen und zu verwenden.

Außerdem wird nur eine allgemeine Lang-Funktion verwendet (oder zumindest würde es wie ein normales Postfix-Makro aussehen).

Ein Postfix-Makro wäre hilfreich, da es die Prägnanz und Verkettbarkeit von Postfix mit den nicht magischen Eigenschaften und dem offensichtlichen Vorhandensein von Makros kombiniert und sich gut in Benutzermakros von Drittanbietern einfügt, z. B. einige .await_debug!() , .await_log!(WARN) oder .await_trace!()

Ein Postfix-Makro wäre schön, da es die [...] nicht magischen Eigenschaften von Makros kombiniert

@novacrazy Das Problem mit diesem Argument ist, dass jedes await! -Makro _would_ magisch ist. Es führt eine Operation aus, die in benutzerdefiniertem Code nicht möglich ist (derzeit ist die zugrunde liegende generatorbasierte Implementierung etwas offengelegt, aber ich verstehe das Vor der Stabilisierung wird dies vollständig ausgeblendet (und für die tatsächliche Interaktion im Moment müssen ohnehin einige rustc -interne nächtliche Funktionen verwendet werden).

@ Nemo157 Hmm . Mir war nicht bewusst, dass es so undurchsichtig sein sollte.

Ist es zu spät, die Verwendung eines prozeduralen Makros wie #[async] zu überdenken, um die Transformation von der "asynchronen" Funktion zur Generatorfunktion anstelle eines magischen Schlüsselworts durchzuführen? Es müssen drei zusätzliche Zeichen eingegeben werden, die in den Dokumenten genauso markiert werden können wie #[must_use] oder #[repr(C)] .

Ich mag es wirklich nicht, so viele Abstraktionsebenen zu verstecken, die den Ausführungsfluss direkt steuern. Es fühlt sich entgegengesetzt zu dem, was Rust ist. Benutzer sollten in der Lage sein, den Code vollständig zu verfolgen und herauszufinden, wie alles funktioniert und wohin die Ausführung geht. Sie sollten ermutigt werden , Dinge zu hacken und die Systeme zu betrügen, und wenn sie sicheres Rost verwenden, sollte es sicher sein. Dies verbessert nichts, wenn wir die Kontrolle auf niedriger Ebene verlieren, und ich kann mich genauso gut an rohe Futures halten.

Ich bin der festen Überzeugung, dass Rust, die Sprache (nicht std / core ), nur dann Abstraktionen und Syntax bereitstellen sollte, wenn dies für Benutzer oder std unmöglich (oder höchst unpraktisch) ist . Diese ganze asynchrone Sache ist in dieser Hinsicht außer Kontrolle geraten. Brauchen wir wirklich mehr als die Pin-API und die Generatoren in rustc ?

@novacrazy Ich stimme im Allgemeinen dem Gefühl zu, aber nicht mit der Schlussfolgerung.

sollten nur dann Abstraktionen und Syntax bereitstellen, wenn dies für Benutzer oder Standard unmöglich (oder höchst unpraktisch) ist.

Was ist der Grund für for -Schleifen in der Sprache, wenn sie auch ein Makro sein können, das sich in ein loop mit Unterbrechungen verwandelt? Was ist der Grund für || closure wenn es sich um dedizierte Merkmals- und Objektkonstruktoren handeln könnte? Warum haben wir ? als wir bereits try!() ? Der Grund, warum ich mit diesen Fragen und Ihren Schlussfolgerungen nicht einverstanden bin, ist die Konsistenz. Der Punkt dieser Abstraktionen ist nicht nur das Verhalten, das sie einschließen, sondern auch die Zugänglichkeit davon. for -Ersetzung ersetzt die Veränderbarkeit, den primären Codepfad und die Lesbarkeit. || -Ersetzung ersetzt die Ausführlichkeit der Deklaration - ähnlich wie derzeit Futures . try!() gliedert sich in der erwarteten Reihenfolge der Ausdrücke und der Kompositionsfähigkeit.

Bedenken Sie, dass async nicht nur der Dekorateur einer Funktion ist, sondern auch andere Gedanken, zusätzliche Muster von aync-blocks und async || . Da dies für verschiedene Sprachelemente gilt, scheint die Verwendbarkeit eines Makros nicht optimal zu sein. Nicht einmal an die Implementierung zu denken, wenn sie dann für den Benutzer sichtbar sein muss.

Benutzer sollten in der Lage sein, den Code vollständig zu verfolgen und herauszufinden, wie alles funktioniert und wohin die Ausführung geht. Sie sollten ermutigt werden, Dinge zu hacken und die Systeme zu betrügen, und wenn sie sicheres Rost verwenden, sollte es sicher sein.

Ich denke nicht, dass dieses Argument zutrifft, da die Implementierung von Coroutinen mit vollständig std api wahrscheinlich stark von unsafe abhängen würde. Und dann gibt es das umgekehrte Argument , denn während es ist machbar-und Sie werden nicht einmal angehalten werden , wenn es syntaktische und semantische Art und Weise in der Sprache it-jeder zu tun Änderung stark bruchgefährdet Annahmen gemacht werden wird unsafe -code. Ich behaupte, dass Rust es nicht so aussehen lassen sollte, als würde es versuchen, eine Standardschnittstelle für die Implementierung von Bits anzubieten, die nicht bald stabilisiert werden sollen, einschließlich der Interna von Coroutines. Ein Analogon dazu wäre extern "rust-call" was als aktuelle Magie dient, um deutlich zu machen, dass Funktionsaufrufe keine solche Garantie haben. Wir möchten vielleicht nie wirklich return , obwohl das Schicksal der stapelbaren Coroutinen noch nicht entschieden ist. Wir möchten vielleicht eine Optimierung tiefer in den Compiler einbinden.

Nebenbei: Apropos, theoretisch nicht so ernsthafte Idee, könnte Coroutine als hypothetische extern "await-call" fn () -> T ? Wenn ja, würde dies im Vorspiel a

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

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

aka. future.await() in einem Benutzerbereich dokumentierte Elemente. Oder es könnte auch eine andere Operatorsyntax möglich sein.

@HeroicKatora

Warum haben wir ? als wir bereits try!()

Um fair zu sein, war ich auch dagegen, obwohl es auf mich gewachsen ist. Es wäre akzeptabler, wenn Try jemals stabilisiert würde, aber das ist ein anderes Thema.

Das Problem mit den Beispielen für "Zucker", die Sie geben, ist, dass es sich um sehr, sehr dünnen Zucker handelt. Sogar impl MyStruct ist mehr oder weniger Zucker für impl <anonymous trait> for MyStruct . Dies sind Zucker für die Lebensqualität, die keinerlei Overhead verursachen.

Im Gegensatz dazu verursachen Generatoren und asynchrone Funktionen einen nicht ganz unbedeutenden Overhead und einen signifikanten mentalen Overhead. Generatoren sind als Benutzer sehr schwer zu implementieren und könnten effektiver und einfacher als Teil der Sprache selbst verwendet werden, während Async darüber hinaus relativ einfach implementiert werden könnte.

Der Punkt über asynchrone Blöcke oder Schließungen ist zwar interessant, und ich gebe zu, dass ein Schlüsselwort dort nützlicher wäre, lehne jedoch die Unfähigkeit ab, bei Bedarf auf untergeordnete Elemente zuzugreifen.

Im Idealfall wäre es wunderbar, das Schlüsselwort async und ein Attribut- / Verfahrensmakro #[async] , wobei das erstere den Zugriff auf den generierten Generator (ohne Wortspiel beabsichtigt) auf niedriger Ebene ermöglicht. In der Zwischenzeit sollte yield in Blöcken oder Funktionen mit async als Schlüsselwort nicht zugelassen werden. Ich bin sicher, sie könnten sogar Implementierungscode teilen.

Wie für await , wenn beide der oben genannten möglich sind, könnten wir etwas Ähnliches tun und begrenzen das Schlüsselwort await bis async Keyword - Funktionen / Blöcke, und verwenden Sie eine Art von await!() Makro in #[async] Funktionen.

Pseudocode:

// imaginary generator syntax stolen from JavaScript
fn* my_generator() -> T {
    yield some_value;

    // explicit return statements are only included to 
    // make it clear the generator/async functions are finished.
    return another_value;
}

// `await` keyword would not be allowed here, but the `yield` keyword is
#[async]
fn* my_async_generator() -> Result<T, E> {
    let item = some_op().await!()?; // uses the `.await!()` macro
    // which would really just use `yield` internally, but with the pinning API

    yield future::ok(item.clone());

    return Ok(item);
}

// `yield` would not be allowed here, but the `await` keyword is.
async fn regular_async() -> Result<T, E> {
   let some_op = async || { /*...*/ };

   let item = some_op() await?;

   return Ok(item);
}

Beste aus beiden Welten.

Dies fühlt sich wie ein natürlicherer Fortschritt der Komplexität an, der dem Benutzer präsentiert werden soll, und kann für mehr Anwendungen effektiver verwendet werden.

Bitte denken Sie daran, dass dieses Problem speziell zur Diskussion der Syntax von await . Andere Gespräche darüber, wie async Funktionen und Blöcke implementiert werden, liegen außerhalb des Geltungsbereichs, außer um die Leute daran zu erinnern, dass await! nicht etwas ist, was Sie in Rusts schreiben können oder werden Oberflächensprache.

Ich möchte speziell die Vor- und Nachteile aller Post-Fix-Syntaxvorschläge abwägen. Wenn eine der Syntaxen mit wenigen Nachteilen auffällt, sollten wir uns vielleicht dafür entscheiden. Wenn dies jedoch nicht der Fall ist, ist es am besten, die durch das Syntaxpräfix getrennte Syntax zu unterstützen, die bei Bedarf mit einem noch zu bestimmenden Post-Fix vorwärtskompatibel ist. Da Postfix für einige Mitglieder am prägnantesten zu sein scheint, erscheint es praktisch, diese zuerst gründlich zu bewerten, bevor Sie zu anderen wechseln.

Der Vergleich ist syntax , example (der Bedarf von @mehcode sieht aus wie ein in dieser Hinsicht verwendbarer realer Benchmark), dann eine Tabelle mit ( concerns und optional resolution , zB wenn vereinbart, dass es auf den Unterricht ankommt). Fühlen Sie sich frei, Syntax und / oder Bedenken hinzuzufügen, ich werde sie in diesem kollektiven Beitrag bearbeiten. Soweit ich weiß, wird sich jede Syntax, die nicht await , für Neulinge und erfahrene Benutzer sehr wahrscheinlich fremd anfühlen, aber alle derzeit aufgeführten enthalten sie.

Beispiel in einer Präfixsyntax nur als Referenz, bitte nicht mit dem Fahrrad fahren:

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

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

    • | Sorge | Auflösung |

      | - | - |

      | Eine Verkettung ohne ? kann verwirrend oder unzulässig sein |

  • Postfix-Feld foo().await?

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

    • | Sorge | Auflösung |

      | - | - |

      | Sieht aus wie ein Feld | |

  • Postfix-Methode foo().await()?

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

    • | Sorge | Auflösung |

      | - | - |

      | Sieht aus wie eine Methode oder ein Merkmal Es kann als ops:: Merkmal dokumentiert werden? |

      | Kein Funktionsaufruf | |

  • Postfix-Aufruf foo()(await)?

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

    • | Sorge | Auflösung |

      | - | - |

      | Kann mit dem tatsächlichen Argument verwechselt werden Schlüsselwort + Hervorheben + nicht überlappen |

  • Postfix-Makro foo().await!()?

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

    • | Sorge | Auflösung |

      | - | - |

      | Wird eigentlich kein Makro sein… | |

      | … Oder await kein Schlüsselwort mehr | |

Ein zusätzlicher Gedanke zu Post-Fix vs. Präfix aus der Sicht der möglichen Einbeziehung von Generatoren: Unter Berücksichtigung von Werten belegen yield und await zwei gegensätzliche Arten von Aussagen. Ersteres gibt einen Wert von Ihrer Funktion nach außen, letzteres akzeptiert einen Wert.

Nebenbemerkung: Nun, Python hat interaktive Generatoren, bei denen yield einen Wert zurückgeben kann. Symmetrisch erfordern Aufrufe eines solchen Generators oder eines Streams zusätzliche Argumente in einer stark typisierten Einstellung. Versuchen wir nicht, zu weit zu verallgemeinern, und wir werden sehen, dass das Argument in beiden Fällen wahrscheinlich übertragen wird.

Dann argumentiere ich, dass es unnatürlich ist, dass diese Aussagen gleich aussehen. Ähnlich wie bei Zuweisungsausdrücken sollten wir hier möglicherweise von einer Norm abweichen, die von anderen Sprachen festgelegt wurde, wenn diese Norm für Rust weniger konsistent und weniger präzise ist. Wie an anderer Stelle ausgedrückt, sollte es keine größere Behinderung für den Übergang geben, selbst wenn wir await einschließen und Ähnlichkeit mit anderen Ausdrücken mit derselben Argumentreihenfolge besteht.

Da scheint implizit vom Tisch zu sein.

Von der Verwendung von async / await in anderen Sprachen bis hin zu den Optionen hier habe ich es nie als syntaktisch angenehm empfunden, Futures zu verketten.

Liegt eine nicht kettenfähige Variante auf dem Tisch?

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

Ich mag das irgendwie, weil man argumentieren könnte, dass es Teil des Musters ist.

Das Problem beim Warten auf eine Bindung ist, dass die Fehlergeschichte weniger als nett ist.

// Error comes _after_ future is awaited
let await res = client.get("http://my_api").send()?;

// Ok
let await res = client.get("http://my_api").send();
let res = res?;

Wir müssen bedenken, dass fast alle in der Community verfügbaren Futures fehlbar sind und mit ? kombiniert werden müssen.

Wenn wir die Syntax Zucker wirklich brauchen:

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

Sowohl await als auch await? müssten als Schlüsselwörter hinzugefügt werden, oder wir erweitern dies auch auf let , dh let? result = 1.divide(0);

In Anbetracht der Häufigkeit der Verkettung im Rust-Code stimme ich voll und ganz zu, dass es wichtig ist, dass verkettete Wartezeiten für den Leser so klar wie möglich sind. Im Fall der Postfix-Variante von await:

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

Dies verhält sich im Allgemeinen ähnlich wie ich erwartet habe, dass sich Rust-Code verhält. Ich habe ein Problem mit der Tatsache, dass sich await() in diesem Kontext wie ein Funktionsaufruf anfühlt, sich aber im Kontext des Ausdrucks magisch (nicht funktionsähnlich) verhält.

Die Postfix-Makroversion würde dies klarer machen. Menschen sind an Ausrufezeichen in Rost gewöhnt, was "hier ist Magie" bedeutet, und aus diesem Grund bevorzuge ich diese Version.

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

Vor diesem Hintergrund ist zu berücksichtigen, dass wir bereits try!(expr) in der Sprache haben und es unser Vorläufer für ? . Ein Hinzufügen von await!(expr) Makro würde jetzt ganz im Einklang mit , wie sein try!(expr) und ? eingeführt wurden , um die Sprache.

Mit der await!(expr) -Version von await haben wir die Möglichkeit, entweder später zu einem Postfix-Makro zu migrieren oder einen neuen Operator im ? -Stil hinzuzufügen, damit die Verkettung einfach wird. Ein Beispiel ähnlich wie ? aber warten auf:

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

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

Ich denke, wir sollten jetzt await!(expr) oder await!{expr} da dies sowohl sehr vernünftig als auch pragmatisch ist. Wir können dann planen, später auf eine Postfix-Version von await (dh .await! oder .await!() ) zu migrieren, wenn / einmal Postfix-Makros zu einer Sache werden. (Oder irgendwann einen zusätzlichen Operator im Stil von ? hinzufügen ... nach langem Bikeshedding zu diesem Thema: P)

Zu Ihrer Information, Scalas Syntax ist nicht Await.result da dies ein blockierender Aufruf ist. Scalas Futures sind Monaden und verwenden daher normale Methodenaufrufe oder das for :

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

Als Ergebnis dieser schrecklichen Notation wurde eine Bibliothek namens scala-async mit der Syntax erstellt, für die ich am meisten bin: Sie lautet wie folgt:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}

val future = async {
  val f1 = async { ...; true }
  val f2 = async { ...; 42 }
  if (await(f1)) await(f2) else 0
}

Dies spiegelt stark wider, wie der Rostcode mit obligatorischen Trennzeichen aussehen soll, und als solches möchte ich den anderen zustimmen, die aktuelle Syntax von await!() . Early Rust war symbollastig und wurde vermutlich aus gutem Grund entfernt. Die Verwendung von syntaktischem Zucker in Form eines Postfix-Operators (oder was haben Sie) ist wie immer abwärtskompatibel, und die Klarheit von await!(future) ist eindeutig. Es spiegelt auch den Fortschritt wider, den wir mit try! , wie bereits erwähnt.

Ein Vorteil der Beibehaltung als Makro besteht darin, dass auf einen Blick sofort ersichtlich ist, dass es sich eher um ein Sprachmerkmal als um einen normalen Funktionsaufruf handelt. Ohne das Hinzufügen von ! die Syntaxhervorhebung des Editors / Viewers der beste Weg, um die Aufrufe zu erkennen, und ich denke, dass es eine schwächere Wahl ist, sich auf diese Implementierungen zu verlassen.

Meine zwei Cent (kein regelmäßiger Beitrag, fwiw) Ich bin am meisten daran interessiert, das Modell von try! zu kopieren. Es wurde schon einmal gemacht, es hat gut funktioniert und nachdem es sehr beliebt wurde, gab es genug Benutzer, um einen Postfix-Operator in Betracht zu ziehen.

Meine Stimme wäre also: Stabilisieren Sie sich mit await!(...) und stechen Sie auf einen Postfix-Operator, um eine schöne Verkettung basierend auf einer Umfrage unter Rust-Entwicklern zu erzielen. Warten ist ein Schlüsselwort, aber das ! zeigt an, dass es für mich etwas "Magisches" ist, und die Klammern halten es eindeutig.

Auch ein Vergleich:

| Postfix | Ausdruck |
| --- | --- |
| .await | client.get("https://my_api").send().await?.json().await? |
| .await! | client.get("https://my_api").send().await!?.json().await!? |
| .await() | client.get("https://my_api").send().await()?.json().await()? |
| ^ | client.get("https://my_api").send()^?.json()^? |
| # | client.get("https://my_api").send()#?.json()#? |
| @ | client.get("https://my_api").send()@?.json()@? |
| $ | client.get("https://my_api").send()$?.json()$? |

Mein dritter Cent ist, dass ich @ (für "Warten") und # (für Multithreading / Parallelität) mag.

Ich mag auch Postfix @ ! Ich denke, es ist eigentlich keine schlechte Option, obwohl es ein gewisses Gefühl zu geben scheint, dass es nicht realisierbar ist.

  • _ @ for await_ ist eine schöne und leicht zu merkende Mnemonik
  • ? und @ wären sehr ähnlich, daher sollte das Lernen von @ nach dem Lernen von ? kein solcher Sprung sein
  • Es hilft beim Scannen einer Kette von Ausdrücken von links nach rechts, ohne vorwärts scannen zu müssen, um ein schließendes Trennzeichen zu finden, um einen Ausdruck zu verstehen

Ich bin sehr für die Syntax await? foo , und ich denke, sie ähnelt einer Syntax in der Mathematik, wo z. sin² x kann verwendet werden, um (sin x) ² zu bedeuten. Anfangs sieht es etwas umständlich aus, aber ich denke, es ist sehr leicht, sich daran zu gewöhnen.

Wie oben erwähnt, bin ich günstig darin, await!() als Makro hinzuzufügen, genau wie try!() , und schließlich zu entscheiden, wie es nachträglich behoben werden soll. Wenn wir die Unterstützung für einen Rustfix berücksichtigen können, der automatisch await!() -Anrufe in den Postfix konvertiert, warten Sie darauf, dass dies noch entschieden werden muss, noch besser.

Die Postfix-Keyword-Option ist für mich ein klarer Gewinner.

  • Es gibt kein Problem mit der Priorität / Reihenfolge, dennoch kann die Reihenfolge mit Klammern angegeben werden. Meistens ist jedoch keine übermäßige Verschachtelung erforderlich (ähnliches Argument für die Bevorzugung von Postfix '?' Als Ersatz für 'try ()!').

  • Es sieht gut aus mit mehrzeiliger Verkettung (siehe früheren Kommentar von @earthengine), und auch hier gibt es keine Verwirrung hinsichtlich der Bestellung oder was erwartet wird. Und keine Verschachtelung / zusätzliche Klammern für Ausdrücke mit mehrfacher Verwendung von erwarten:

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;
  • Es eignet sich für ein einfaches Makro zum Warten! () (Siehe früheren Kommentar von @novacrazy):
macro_rules! await {
    ($e:expr) => {{$e await}}
}
  • Selbst einzeiliges, nacktes (ohne '?') Postfix-Warten auf Schlüsselwortverkettung stört mich nicht, da es von links nach rechts lautet und wir auf die Rückgabe eines Werts warten, mit dem die nachfolgende Methode dann arbeitet (obwohl ich es nur tun würde) bevorzugen mehrzeiligen rostigen Code). Der Raum bricht die Linie auf und ist genug von einem visuellen Indikator / Hinweis, dass das Warten stattfindet:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Um einen anderen Kandidaten vorzuschlagen, den ich noch nicht vorgeschlagen habe (vielleicht weil er nicht analysierbar wäre), was ist mit einem lustigen Doppelpunkt-Postfix-Operator? Es erinnert mich daran, dass wir auf etwas warten (das Ergebnis!) ...

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

Ich mag auch Postfix @ ! Ich denke, es ist eigentlich keine schlechte Option, obwohl es ein gewisses Gefühl zu geben scheint, dass es nicht realisierbar ist.

  • _ @ for await_ ist eine schöne und leicht zu merkende Mnemonik
  • ? und @ wären sehr ähnlich, daher sollte das Lernen von @ nach dem Lernen von ? kein solcher Sprung sein
  • Es hilft beim Scannen einer Kette von Ausdrücken von links nach rechts, ohne vorwärts scannen zu müssen, um ein schließendes Trennzeichen zu finden, um einen Ausdruck zu verstehen

Ich bin kein Fan von @ for await. Es ist umständlich, auf einer Fin / Swe-Layout-Tastatur zu tippen, da ich mit dem rechten Daumen Alt-Gr drücken und dann Taste 2 in der Zahlenreihe drücken muss. Außerdem hat @ eine gut etablierte Bedeutung (at), daher verstehe ich nicht, warum wir die Bedeutung davon zusammenführen sollten.

Ich würde viel lieber einfach await , es ist schneller, da es keine Tastaturakrobatik erfordert.

Hier ist meine eigene, sehr subjektive Einschätzung. Ich habe auch future@await hinzugefügt, was mir interessant erscheint.

| Syntax | Notizen |
| --- | --- |
| await { f } | stark:

  • Sehr einfach
  • Parallelen for , loop , async usw.
schwach:
  • sehr ausführlich (5 Buchstaben, 2 Klammern, 3 optionale, aber wahrscheinlich fusselige Leerzeichen)
  • Verkettung führt zu vielen verschachtelten Klammern ( await { await { foo() }?.bar() }? )
|
| await f | stark:
  • Parallelen await Syntax von Python, JS, C # und Dart
  • unkompliziert, kurz
  • Sowohl nützliche Vorrangstellung als auch offensichtliche Vorrangstellung verhalten sich gut mit ? ( await fut? vs. await? fut )
schwach:
  • mehrdeutig: Nützlicher oder offensichtlicher Vorrang muss gelernt werden
  • Verkettung ist auch sehr umständlich ( await (await foo()?).bar()? vs. await? (await? foo()).bar() )
|
| fut.await
fut.await()
fut.await!() | stark:
  • ermöglicht eine sehr einfache Verkettung
  • kurz
  • schöne Code-Vervollständigung
schwach:
  • täuscht Benutzer vor, es sei ein Feld / eine Funktion / ein Makro, das irgendwo definiert ist. Edit: Ich stimme @jplatte zu, dass await!() sich am wenigsten magisch anfühlt
|
| fut(await) | stark:
  • ermöglicht eine sehr einfache Verkettung
  • kurz
schwach:
  • täuscht Benutzer vor, dass irgendwo eine Variable await definiert ist und dass Futures wie eine Funktion aufgerufen werden können
|
| f await | stark:
  • ermöglicht eine sehr einfache Verkettung
  • kurz
schwach:
  • Parallelen nichts in Rusts Syntax, nicht offensichtlich
  • Mein Gehirn gruppiert die client.get("https://my_api").send() await.unwrap().json() await.unwrap() in client.get("https://my_api").send() , await.unwrap().json() und await.unwrap() (zuerst gruppiert nach , dann . ). das ist nicht richtig
  • für Haskellers: sieht aus wie Curry, ist es aber nicht
|
| f@ | stark:
  • ermöglicht eine sehr einfache Verkettung
  • sehr kurze
schwach:
  • sieht etwas umständlich aus (zumindest zuerst)
  • verbraucht @ was für etwas anderes besser geeignet sein könnte
  • ist möglicherweise leicht zu übersehen, insbesondere bei großen Ausdrücken
  • verwendet @ anders als alle anderen Sprachen
|
| f@await | stark:
  • ermöglicht eine sehr einfache Verkettung
  • kurz
  • schöne Code-Vervollständigung
  • await muss kein Schlüsselwort werden
  • Vorwärtskompatibel: Ermöglicht das Hinzufügen neuer Postfix-Operatoren in der Form @operator . Zum Beispiel könnte das ? als @try .
  • Mein Gehirn gruppiert die client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() in die richtigen Gruppen (zuerst gruppiert nach . , dann nach @ ).
schwach:
  • verwendet @ anders als alle anderen Sprachen
  • Dies könnte einen Anreiz darstellen, zu viele unnötige Postfix-Operatoren hinzuzufügen
|

Meine Ergebnisse:

  • Vertrautheit (fam): Wie nah diese Syntax an bekannten Syntaxen ist (Rust und andere, wie Python, JS, C #)
  • Offensichtlichkeit (obv): Wenn Sie dies zum ersten Mal im Code eines anderen lesen würden, könnten Sie dann die Bedeutung, den Vorrang usw. erraten?
  • Ausführlichkeit (vrb): Wie viele Zeichen zum Schreiben benötigt werden
  • Sichtbarkeit (vis): Wie einfach es ist, Code zu erkennen (oder zu übersehen)
  • Verkettung (cha): Wie einfach es ist, es mit . und anderen await verketten
  • Gruppierung (grp): Gibt an, ob mein Gehirn den Code in die richtigen Blöcke gruppiert
  • Vorwärtskompatibilität (fwd): Gibt an, ob dies später ohne Unterbrechung angepasst werden kann

| Syntax | fam | obv | vrb | vis | cha | grp | fwd |
| --------------------- | ----- | ----- | ----- | ----- | --- - | ----- | ----- |
| await!(fut) | ++ | + | - | ++ | - | 0 | ++ |
| await { fut } | ++ | ++ | - | ++ | - | 0 | + |
| await fut | ++ | - | + | ++ | - | 0 | - |
| fut.await | 0 | - | + | ++ | ++ | + | - |
| fut.await() | 0 | - | - | ++ | ++ | + | - |
| fut.await!() | 0 | 0 | - | ++ | ++ | + | - |
| fut(await) | - | - | 0 | ++ | ++ | + | - |
| fut await | - | - | + | ++ | ++ | - | - |
| fut@ | - | - | ++ | - | ++ | ++ | - |
| fut@await | - | 0 | + | ++ | ++ | ++ | 0 |

Ich habe das Gefühl, wir sollten die Syntax try!() im ersten Schnitt spiegeln und die Verwendung von await!(expr) wirklich nutzen, bevor wir eine andere Syntax einführen.

Wenn wir jedoch eine alternative Syntax erstellen.

Ich denke, dass @ hässlich aussieht, "at" für "async" fühlt sich für mich nicht so intuitiv an, und das Symbol wird bereits für den Mustervergleich verwendet.

async Präfix ohne Parens führt zu nicht offensichtlicher Priorität oder umgebenden Parens, wenn es mit ? (was häufig vorkommt).

Postfix .await!() Ketten gut, fühlt sich in seiner Bedeutung ziemlich sofort offensichtlich an, enthält die ! , um mir zu sagen, dass es Magie machen wird, und ist syntaktisch weniger neu, so dass der "nächste Schnitt" sich mir nähert persönlich würde diesen bevorzugen. Trotzdem bleibt für mich abzuwarten, um wie viel sich der echte Code gegenüber dem ersten Schnitt von await! (expr) verbessern würde.

Ich bevorzuge den Präfixoperator für einfache Fälle:
let result = await task;
Es fühlt sich viel natürlicher an, wenn man bedenkt, dass wir die Art des Ergebnisses nicht aufschreiben. Warten hilft also mental, wenn wir von links nach rechts lesen, um zu verstehen, dass das Ergebnis eine Aufgabe des Wartens ist.
Stellen Sie es sich so vor:
let result = somehowkindoflongtask await;
Bis Sie das Ende der Aufgabe nicht erreichen, erkennen Sie nicht, dass auf den zurückgegebenen Typ gewartet werden muss. Denken Sie auch daran (obwohl dies Änderungen unterliegt und nicht direkt mit der Zukunft der Sprache zusammenhängt), dass IDEs als Intellij den Typ (ohne Personalisierung, falls dies überhaupt möglich ist) zwischen dem Namen und den Gleichen einfügen.
Stellen Sie es sich so vor:
6voler6ykj

Das bedeutet nicht, dass meine Meinung hundertprozentig zum Präfix ist. Ich bevorzuge die Postfix-Version von Future, wenn es um Ergebnisse geht, da sich das viel natürlicher anfühlt. Ohne Kontext kann ich leicht sagen, welche davon was bedeutet:
future await?
future? await
Schauen Sie sich stattdessen diese an, welche dieser beiden aus der Sicht eines Neulings wahr ist:
await future? === await (future?)
await future? === (await future)?

Ich bin für das Präfix-Schlüsselwort: await future .

Es wird von den meisten Programmiersprachen verwendet, die asynchron sind / warten, und ist daher Personen, die eine von ihnen kennen, sofort vertraut.

Was ist der häufigste Fall für die Priorität von await future? ?

  • Eine Funktion, die ein Result<Future> , auf das gewartet werden muss.
  • Eine Zukunft, die abgewartet werden muss und die Result : Future<Result> zurückgibt.

Ich denke, der zweite Fall ist viel häufiger, wenn es um typische Szenarien geht, da E / A-Vorgänge möglicherweise fehlschlagen. Deshalb:

await future? <=> (await future)?

Im selteneren ersten Fall ist es akzeptabel, Klammern zu haben: await (future?) . Dies könnte sogar eine gute Verwendung für das Makro try! wenn es nicht veraltet wäre: await try!(future) . Auf diese Weise befinden sich der Operator await und der Fragezeichen nicht auf verschiedenen Seiten der Zukunft.

Warum nicht await als ersten async Funktionsparameter nehmen?

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

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

let output = future
    .run(await);

Hier ist future.run(await) eine Alternative zu await future .
Es könnte sich nur um eine reguläre async -Funktion handeln, die die Zukunft in Anspruch nimmt und einfach das Makro await!() ausführt.

C ++ (Parallelität TR)

auto result = co_await task;

Dies ist in der Coroutines TS keine Parallelität.

Eine andere Option könnte die Verwendung des Schlüsselworts become anstelle von await :

async fn become_chain() -> Result<usize, Error> {
    let _ = partial_computation_future(become)
        .unwrap_or_else(or_recover_future)
        .start(become)?;
}

client.get("https://my_api")
    .send_future(become)?
    .json_future(become)?

let output = future.start(become);

become könnte jedoch ein Schlüsselwort für garantierte Gesamtbetriebskosten sein

Danke @EyeOfPython dafür [Übersicht]. Das ist besonders nützlich für Leute, die gerade in den Fahrradschuppen eintreten.

Persönlich würde ich hoffen, dass wir uns von f await fernhalten, nur weil es in Bezug auf die Syntax sehr un-rustikal ist und es etwas Besonderes und Magisches erscheinen lässt. Es würde zu einem dieser Dinge werden, über die Benutzer, die neu bei Rust sind, sehr verwirrt sein werden, und ich denke, es bringt selbst Rust-Veteranen nicht so viel Klarheit, das wert zu sein.

@novacrazy Das Problem mit diesem Argument ist, dass jedes await! -Makro magisch sein würde. Es führt eine Operation aus, die in benutzerdefiniertem Code nicht möglich ist

@ Nemo157 Ich stimme zu, dass ein await! Makro magisch wäre, aber ich würde argumentieren, dass dies kein Problem ist. Es gibt bereits mehrere solcher Makros in std , z. B. compile_error! und ich habe niemanden gesehen, der sich über sie beschwert hat. Ich denke, es ist normal, ein Makro zu verwenden, das nur versteht, was es tut, nicht wie es es tut.

Ich stimme früheren Kommentatoren zu, dass Postfix am ergonomischsten wäre, aber ich würde lieber mit dem Präfix-Makro await!(expr) und möglicherweise zum Postfix-Makro übergehen, sobald dies eine Sache ist, anstatt expr.await (Magic Compiler-eingebautes Feld) oder expr.await() (Magic Compiler-eingebaute Methode). Beide würden eine völlig neue Syntax nur für diese Funktion und IMO einführen, wodurch sich die Sprache nur inkonsistent anfühlt.

@EyeOfPython Möchten Sie future(await) zu Ihren Listen und Tabellen hinzufügen? Alle positiven Aspekte Ihrer Einschätzung von future.await() scheinen sich ohne die Schwäche zu übertragen

Da einige argumentiert haben, dass foo.await trotz Syntaxhervorhebung zu sehr wie ein . in # ändern und stattdessen foo#await schreiben . Zum Beispiel:

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

Um zu veranschaulichen, wie GitHub dies mit Syntaxhervorhebung rendern würde, ersetzen wir await durch match da sie gleich lang sind:

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

Dies scheint sowohl klar als auch ergonomisch.

Die Begründung für # und nicht für ein anderes Token ist nicht spezifisch, aber das Token ist gut sichtbar, was hilfreich ist.

Also ein anderes Konzept: Wenn Zukunft Referenz wäre :

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   *self.logger.log("beginning service call");
   let output = *service.exec(); // Actually wait for its result
   *self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = *acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = (*logger.log_into(message))?;
    *logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    *(*partial_computation()).unwrap_or_else(or_recover);
}

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

let output = *future;

Das wäre wirklich hässlich und inkonsistent. Lassen Sie diese Syntax zumindest klarstellen:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   $self.logger.log("beginning service call");
   let output = $service.exec(); // Actually wait for its result
   $self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = ($logger.log_into(message))?;
    $logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    $($partial_computation()).unwrap_or_else(or_recover);
}

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

let output = $future;

Besser, aber immer noch hässlich (und noch schlimmer macht es Githubs Syntax hervorheben). Um dies zu bewältigen, können Sie jedoch die Möglichkeit einführen, den Präfixoperator zu

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.$log("beginning service call");
   let output = service.$exec(); // Actually wait for its result
   self.logger.$log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.$log_into(message)?;
    logger.$timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    ($partial_computation()).$unwrap_or_else(or_recover);
}

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

let output = $future;

Genau das was ich will! Nicht nur zum Warten ( $ ), sondern auch zum Deref ( * ) und Negieren ( ! ).

Einige Einschränkungen in dieser Syntax:

  1. Operator-Vorrang: Für mich ist es offensichtlich, aber für andere Benutzer?
  2. Makros interop: Würde das Symbol $ hier Probleme verursachen?
  3. Ausdrucksinkonsistenz: Der führende Präfixoperator gilt für den gesamten Ausdruck, während der verzögerte Präfixoperator nur für . begrenzte Ausdrücke gilt ( await_chain fn demonstrieren dies). Ist das verwirrend?
  4. Analyse und Implementierung: Ist diese Syntax überhaupt gültig?

@Centril
IMO # kommt der rohen Literal-Syntax r#"A String with "quotes""#

IMO # kommt der rohen Literal-Syntax r#"A String with "quotes""#

Aus dem Zusammenhang scheint ziemlich klar zu sein, was der Unterschied in diesem Fall ist.

@Centril
Es tut es, aber die Syntax ist auch dem Stil, den Rust IMHO verwendet, wirklich fremd. Es ähnelt keiner vorhandenen Syntax mit einer ähnlichen Funktion.

@Laaas Weder ? als es eingeführt wurde. Ich würde gerne mit .await ; aber andere scheinen damit unzufrieden zu sein, also versuche ich, etwas anderes zu finden, das funktioniert (dh klar, ergonomisch, ausreichend einfach zu tippen, verkettbar, hat einen guten Vorrang) und foo#await scheint all das zu befriedigen.

? hat eine Reihe anderer guter Punkte, während #await eher willkürlich erscheint. In jedem Fall, wenn Sie etwas wie .await , warum nicht .await! ?

@Centril Dies war der Hauptgrund für future(await) , um den Feldzugriff zu vermeiden, ohne zusätzliche Operatoren oder fremde Syntax hinzufügen zu müssen.

während #await eher willkürlich erscheint.

# ist willkürlich, ja - ist das make oder break? Wenn irgendwann eine neue Syntax erfunden wird, muss sie willkürlich sein, weil jemand dachte, sie sehe gut aus oder macht Sinn.

warum nicht .await! ?

Das ist ebenso willkürlich, aber ich habe keine Einwände dagegen.

@Centril Dies war der Hauptgrund für future(await) , um den Feldzugriff zu vermeiden, ohne zusätzliche Operatoren oder fremde Syntax hinzufügen zu müssen.

Stattdessen sieht es aus wie eine Funktionsanwendung, bei der Sie await an future . das kommt mir verwirrender vor als "Feldzugang".

Stattdessen sieht es aus wie eine Funktionsanwendung, bei der Sie auf die Zukunft warten. das kommt mir verwirrender vor als "Feldzugang".

Für mich war das Teil der Prägnanz. Warten ist in vielerlei Hinsicht wie ein Funktionsaufruf (Callee wird zwischen Ihnen und Ihrem Ergebnis ausgeführt), und die Übergabe eines Schlüsselworts an diese Funktion hat deutlich gemacht, dass es sich um eine andere Art von Aufruf handelt. Aber ich sehe, wie verwirrend es sein kann, dass das Schlüsselwort jedem anderen Argument ähnlich ist. (Minus Syntax-Hervorhebung und Compiler-Fehlermeldungen, die versuchen, diese Meldung zu verstärken. Futures können nur als erwartet bezeichnet werden und umgekehrt).

@Centril

Willkürlich zu sein ist keine negative Sache, aber Ähnlichkeit mit einer bestehenden Syntax ist eine positive Sache. .await! ist nicht so willkürlich, da es sich nicht um eine völlig neue Syntax handelt. Immerhin ist es nur ein Post-Fix-Makro mit dem Namen await .

Um den Punkt über neue Syntax speziell für await zu verdeutlichen / hinzuzufügen, meine ich damit die Einführung einer neuen Syntax, die sich von der vorhandenen Syntax unterscheidet. Es gibt viele Präfix-Schlüsselwörter, von denen einige nach Rust 1.0 hinzugefügt wurden (z. B. union ), aber AFAIK ist kein einzelnes Postfix-Schlüsselwort (unabhängig davon, ob es durch ein Leerzeichen, einen Punkt oder etwas anderes getrennt ist). . Ich kann mir auch keine andere Sprache vorstellen, die Postfix-Schlüsselwörter enthält.

IMHO ist das Postfix-Makro die einzige Postfix-Syntax, die die Fremdheit von Rust nicht wesentlich erhöht, da es Methodenaufrufe widerspiegelt, aber von jedem, der zuvor ein Rust-Makro gesehen hat, eindeutig als Makro identifiziert werden kann.

Ich mochte auch das Standardmakro await!() . Es ist einfach klar und einfach.
Ich bevorzuge einen feldähnlichen Zugriff (oder einen anderen Postfix) als awaited .

  • Warten Sie, Sie werden auf das warten, was als nächstes / richtig kommt. Erwartet, was vorher kam / ging.
  • Entspricht der cloned() -Methode für Iteratoren.

Und ich mochte auch den @? als ? -ähnlichen Zucker.
Dies ist persönlich, aber ich bevorzuge tatsächlich die Kombination &? , da @ zu groß ist und die Linie unterschreitet, was sehr ablenkbar ist. & ist ebenfalls groß (gut), unterschreitet aber nicht (auch gut). Leider hat & bereits eine Bedeutung, obwohl immer ein ? folgen würde. Ich denke?

Z.B.

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

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

Für mich fühlt sich das @ wie ein aufgeblähter Charakter an und lenkt den Lesefluss ab. Auf der anderen Seite fühlt sich &? angenehm an, lenkt (mein) Lesen nicht ab und hat einen schönen oberen Abstand zwischen & und ? .

Ich persönlich denke, ein einfaches await! Makro sollte verwendet werden. Wenn Post-Fix-Makros in die Sprache gelangen, kann das Makro einfach erweitert werden, oder?

@Laaas So sehr ich es mir auch wünschen würde, es gibt noch keine Postfix-Makros in Rust, daher handelt es sich um eine neue Syntax. Beachten Sie auch, dass foo.await!.bar und foo.await!().bar nicht dieselbe Oberflächensyntax haben. Im letzteren Fall würde es einen tatsächlichen Postfix und ein eingebautes Makro geben (was bedeutet, dass await als Schlüsselwort aufgegeben wird).

@jplatte

Es gibt viele Präfix-Schlüsselwörter, von denen einige nach Rust 1.0 hinzugefügt wurden (z. B. union ).

union ist kein unärer Ausdrucksoperator, daher ist er für diesen Vergleich irrelevant. Es gibt genau 3 stabile unäre Präfixoperatoren in Rust ( return , break und continue ) und alle sind mit ! eingegeben.

Hmm ... Mit @ sieht mein vorheriger Vorschlag etwas besser aus:

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

let output = @future;

let foo = (@alpha())?
    .<strong i="8">@beta</strong>
    .@some_other_stuff()?
    .@even_more_stuff()
    .stuff_and_stuff();

@Centril Das ist mein Punkt, da wir noch keine Post-Fix-Makros haben, sollte es einfach await!() . Ich meinte auch .await!() FYI. Ich denke nicht, dass await ein Schlüsselwort sein muss, obwohl Sie es reservieren könnten, wenn Sie es problematisch finden.

union ist kein unärer Ausdrucksoperator, daher ist er für diesen Vergleich irrelevant. Es gibt genau 3 stabile unäre Präfixoperatoren in Rust ( return , break und continue ) und alle sind mit ! eingegeben.

Dies bedeutet immer noch, dass die Präfix-Schlüsselwort-Variante in die Gruppe der unären Ausdrucksoperatoren passt, auch wenn sie anders eingegeben wird. Die Postfix-Keyword-Variante ist eine viel größere Abweichung von der bestehenden Syntax, die meiner Meinung nach in Bezug auf die Bedeutung von await in der Sprache zu groß ist.

In Bezug auf das Präfix-Makro und die Möglichkeit des Übergangs zum Post-Fix: await muss ein Schlüsselwort bleiben, damit dies funktioniert. Das Makro wäre dann ein spezielles lang-Element, bei dem ein Schlüsselwort als Name ohne zusätzliche r# darf, und r#await!() könnte, sollte aber wahrscheinlich nicht dasselbe Makro aufrufen. Davon abgesehen scheint es die pragmatischste Lösung zu sein, es zur Verfügung zu stellen.

Behalten Sie also await als Schlüsselwort bei, aber lösen Sie await! in ein Lang-Item-Makro auf.

@HeroicKatora Warum muss es ein Schlüsselwort bleiben, damit es funktioniert?

@Laaas Wenn wir die Möglichkeit eines Übergangs offen halten möchten, müssen wir offen bleiben für zukünftige Post-Fix-Syntax, die es als Schlüsselwort verwendet. Daher müssen wir die Keyword-Reservierung beibehalten, damit wir für den Übergang keine Editionsunterbrechung benötigen.

@Centril

Beachten Sie auch, dass foo.await! .Bar und foo.await! (). Bar nicht dieselbe Oberflächensyntax haben. Im letzteren Fall würde es einen tatsächlichen Postfix und ein eingebautes Makro geben (was bedeutet, das Warten als Schlüsselwort aufzugeben).

Könnte dies nicht gelöst werden, indem das Schlüsselwort await Kombination mit ! in ein internes Makro aufgelöst wird (das nicht mit normalen Mitteln definiert werden kann)? Dann bleibt es ein Schlüsselwort, wird aber in ein Makro in der ansonsten Makrosyntax aufgelöst.

@HeroicKatora Warum sollte await in x.await!() ein reserviertes Schlüsselwort sein?

Es wäre nicht so, aber wenn wir die Nachbesserung ungelöst lassen, muss es nicht die Lösung sein, zu der wir in späteren Diskussionen kommen. Wenn dies die eindeutig vereinbarte beste Möglichkeit wäre, sollten wir zunächst genau diese Post-Fix-Syntax anwenden.

Dies ist ein weiteres Mal, dass wir auf etwas stoßen, das als Postfix-Operator viel besser funktioniert. Das große Beispiel dafür ist try! das wir schließlich ein eigenes Symbol ? . Ich denke jedoch, dass dies nicht das letzte Mal ist, dass ein Postfix-Operator optimaler ist und wir nicht alles seinen eigenen speziellen Charakter geben können. Ich denke, wir sollten zumindest nicht mit @ . Es wäre viel besser, wenn wir einen Weg hätten, solche Dinge zu tun. Deshalb unterstütze ich den Postfix-Makrostil .await!() .

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

Damit dies jedoch Sinn macht, müssten Postfix-Makros eingeführt werden. Daher denke ich, dass es am besten ist, mit einer normalen await!(foo) Makrosyntax zu beginnen. Wir könnten dies später entweder auf foo.await!() oder sogar auf foo@ wenn wir wirklich der Meinung sind, dass dies wichtig genug ist, um ein eigenes Symbol zu gewährleisten.
Dass dieses Makro ein bisschen Magie braucht, ist für std nicht neu und für mich kein großes Problem.
Wie @jplatte es ausdrückte:

@ Nemo157 Ich stimme zu, dass ein await! Makro magisch wäre, aber ich würde argumentieren, dass dies kein Problem ist. Es gibt bereits mehrere solcher Makros in std , z. B. compile_error! und ich habe niemanden gesehen, der sich über sie beschwert hat. Ich denke, es ist normal, ein Makro zu verwenden, das nur versteht, was es tut, nicht wie es es tut.

Ein wiederkehrendes Problem, das hier diskutiert wird, betrifft das Verketten und die Verwendung
Warten Sie in verketteten Ausdrücken. Vielleicht könnte es eine andere Lösung geben?

Wenn wir nicht warten für die Verkettung verwenden möchten und nur warten für verwenden
Aufgaben, wir könnten so etwas wie einfach ersetzen durch warten warten:
await foo = future

Dann könnten wir uns zur Verkettung eine Operation wie await res = fut1 -> fut2 oder await res = fut1 >>= fut2 vorstellen.

Der einzige fehlende Fall ist das Warten und Zurückgeben des Ergebnisses, eine Verknüpfung
für await res = fut; res .
Dies könnte leicht mit einem einfachen await fut

Ich glaube nicht, dass ich einen anderen Vorschlag wie diesen gesehen habe (zumindest für die
Verkettung), also lass das hier fallen, da ich denke, es wäre schön, es zu benutzen.

@HeroicKatora Ich habe fut(await) zur Liste hinzugefügt und sie meiner Meinung nach bewertet.

Wenn jemand das Gefühl hat, dass meine Wertung nicht stimmt, sag es mir bitte!

In Bezug auf die Syntax denke ich, dass .await!() sauber ist und eine Verkettung ermöglicht, ohne der Syntax irgendwelche Kuriositäten hinzuzufügen.

Wenn wir jedoch jemals "echte" Postfix-Makros erhalten, wird dies etwas seltsam sein, da vermutlich .r#await!() beschattet werden könnte, während .await!() dies nicht könnte.

Ich bin ziemlich stark gegen die Option "Postfix-Schlüsselwort" (getrennt durch ein Leerzeichen wie: foo() await?.bar() await? ), weil ich die Tatsache finde, dass das await mit dem folgenden Teil des Ausdrucks verbunden ist: und nicht der Teil, an dem es arbeitet. Ich würde hier so ziemlich jedes andere Symbol als Leerzeichen bevorzugen und würde sogar die Präfixsyntax diesem vorziehen, trotz der Nachteile bei langen Ketten.

Ich denke, "offensichtliche Priorität" (mit dem Zucker await? ) ist eindeutig die beste Präfixoption, da obligatorische Trennzeichen für den sehr häufigen Fall, auf eine einzelne Aussage zu warten, ein Problem sind und "nützliche Priorität" überhaupt nicht intuitiv und damit verwirrend.

In Bezug auf die Syntax denke ich, dass .await!() sauber ist und eine Verkettung ermöglicht, ohne der Syntax irgendwelche Kuriositäten hinzuzufügen.

Wenn wir jedoch jemals "echte" Postfix-Makros erhalten, wird dies etwas seltsam sein, da vermutlich .r#await!() beschattet werden könnte, während .await!() dies nicht könnte.

Wenn wir Postfix-Makros erhalten und .await!() , können wir await als Schlüsselwort aufheben und es einfach zu einem Postfix-Makro machen. Die Implementierung dieses Makros würde immer noch ein wenig Magie erfordern, aber es wäre ein echtes Makro, so wie es compiler_error! heute ist.

@EyeOfPython Könnten Sie vielleicht im Detail erklären, welche Änderungen Sie in forward-compatibility berücksichtigen? Ich bin mir nicht sicher, auf welche Weise fut@await höher als fut.await!() und await { future } niedriger als await!(future) bewerten würde. Die Spalte verbosity scheint ebenfalls etwas seltsam zu sein, einige Ausdrücke sind kurz, haben aber eine niedrigere Bewertung, berücksichtigen verkettete Aussagen usw. Alles andere scheint ausgewogen und wie die bisher am besten komprimierte umfassende Bewertung.

Nach dem Lesen dieser Diskussion scheinen viele Leute ein normales await!() -Makro hinzufügen und später die Postfix-Version herausfinden zu wollen. Dies setzt voraus, dass wir tatsächlich eine Postfix-Version wollen, die ich für den Rest dieses Kommentars als wahr ansehen werde.

Deshalb möchte ich hier nur die Meinung aller befragen: Sollten wir uns alle auf die Syntax await!(future) FOR NOW einigen? Die Profis sind, dass diese Syntax keine Mehrdeutigkeit beim Parsen aufweist und dass es sich auch um ein Makro handelt. Daher muss die Syntax der Sprache nicht geändert werden, um diese Änderung zu unterstützen. Die Nachteile sind, dass es für die Verkettung hässlich aussieht, aber das spielt keine Rolle, da diese Syntax leicht automatisch durch eine Postfix-Version ersetzt werden kann.

Sobald wir das geklärt und in die Sprache implementiert haben, können wir die Diskussion über die Postfix-Wait-Syntax mit hoffentlich ausgereifteren Erfahrungen, Ideen und möglicherweise anderen Compiler-Funktionen fortsetzen.

@HeroicKatora Ich habe es höher bewertet, da es await , um natürlich als gewöhnlicher Bezeichner verwendet zu werden, und es würde das Hinzufügen anderer Postfix-Operatoren ermöglichen, während ich denke, dass fut.await!() besser wäre, wenn await wurde reserviert. Ich bin mir jedoch nicht sicher, ob dies vernünftig ist. Es scheint definitiv gültig zu sein, dass fut.await!() höher sein könnte.

Für await { future } vs await!(future) lässt die letztere die Option offen, um zu fast jeder der anderen Optionen zu wechseln, während die erste nur eine der await future -Varianten wirklich zulässt ( wie im Blogeintrag von @withoutboats beschrieben ). Ich denke, es sollte definitiv fwd(await { future }) < fwd(await!(future)) .

Was die Ausführlichkeit betrifft, so haben Sie Recht, nachdem ich Fälle in Untergruppen aufgeteilt habe, habe ich die Ausführlichkeit nicht neu bewertet, was die objektivste von allen sein sollte.

Ich werde es bearbeiten, um Ihre Kommentare zu berücksichtigen, danke!

Die Stabilisierung von await!(future) ist die schlechteste Option, die ich mir vorstellen kann:

  1. Dies bedeutet, dass wir await reservieren müssen, was weiter bedeutet, dass das zukünftige Sprachdesign schwieriger wird.
  2. Es geht wissentlich den gleichen Weg wie bei try!(result) den wir veraltet haben (und für den das Schreiben von r#try!(result) in Rust 2018 erforderlich ist).

    • Wenn wir wissen, dass await!(future) eine schlechte Syntax ist, die wir letztendlich ablehnen wollen, führt dies absichtlich zu technischen Schulden.

    • Darüber hinaus ist try!(..) in Rust definiert, während await!(future) keine Compilermagie sein kann und würde.

    • Das Abwerten von try!(..) war nicht einfach und forderte sozial einen Tribut. Diese Tortur noch einmal durchzugehen scheint nicht ansprechend.

  3. Dies würde die Makrosyntax für einen zentralen und wichtigen Teil der Sprache verwenden. das scheint eindeutig nicht erstklassig.
  4. await!(future) ist laut ; Im Gegensatz zu await future Sie !( ... ) schreiben.
  5. Rust-APIs und insbesondere die Standardbibliothek konzentrieren sich auf die Methodenaufrufsyntax. Zum Beispiel ist es üblich, Methoden zu verketten, wenn es um Iterator s geht. Ähnlich wie bei await { future } und await future erschwert die Syntax await!(future) die Verkettung von Methoden und führt zu temporären let -Bindungen. Dies ist schlecht für die Ergonomie und meiner Ansicht nach für die Lesbarkeit.

@Centril Ich würde gerne zustimmen, aber es gibt einige offene Fragen. Sind Sie sicher über 1. ? Wenn wir es "magisch" machen, könnten wir es dann nicht noch magischer machen, indem wir zulassen, dass dies auf ein Makro verweist, ohne es als Schlüsselwort zu löschen?

Ich bin zu spät zu Rust gekommen, um die soziale Perspektive von 2. zu bewerten. Das Wiederholen eines Fehlers klingt nicht ansprechend, aber da ein Teil des Codes noch nicht von try! konvertiert wurde, sollte klar sein, dass es sich um eine Lösung handelt. Das wirft die Frage auf, ob wir beabsichtigen, async/await als Anreiz für die Migration zur Edition 2018 oder eher geduldig zu sein und dies nicht zu wiederholen.

Zwei weitere (imho) zentrale Funktionen ähneln 3. : vec![] und format! / println! . Ersteres sehr, weil es keine stabile Box-Konstruktion gibt, letzteres aufgrund der Format-String-Konstruktion und ohne abhängig typisierte Ausdrücke. Ich denke, diese Vergleiche bringen 4. in eine andere Perspektive.

Ich lehne jede Syntax ab, die sich nicht wie Englisch liest. IE, "warte auf x" liest so etwas wie Englisch. "x # !! @! &" nicht. "x.await" liest sich verlockend wie Englisch, aber nicht, wenn x eine nicht triviale Zeile ist, wie ein Elementfunktionsaufruf mit langen Namen oder eine Reihe verketteter Iteratormethoden usw.

Insbesondere unterstütze ich "Schlüsselwort x", wobei das Schlüsselwort wahrscheinlich await . Ich komme aus der Verwendung der C ++ - Coroutinen TS und der C # -Koroutinen von Unity, die beide eine ähnliche Syntax verwenden. Und nachdem ich sie jahrelang im Produktionscode verwendet habe, ist es meiner Meinung nach absolut wichtig zu wissen, wo sich Ihre Ertragspunkte auf einen Blick befinden . Wenn Sie die Einrückungslinie Ihrer Funktion überfliegen, können Sie alle co_await / yield return in einer 200-Zeilen-Funktion innerhalb von Sekunden ohne kognitive Belastung auswählen.

Das Gleiche gilt nicht für den Punktoperator mit späterem Warten oder eine andere Postfix-Syntax "Stapel von Symbolen".

Ich glaube, dass await eine grundlegende Kontrollflussoperation ist. Es sollte das gleiche Maß an Respekt erhalten wie 'if , while , match und return . Stellen Sie sich vor, einer dieser Operatoren wäre Postfix-Operatoren - das Lesen von Rust-Code wäre ein Albtraum. Wie bei meinem Argument für das Warten können Sie die Einrückungslinie jeder Rust-Funktion überfliegen und sofort den gesamten Kontrollfluss auswählen. Es gibt Ausnahmen, aber sie sind Ausnahmen und nichts, wonach wir streben sollten.

Ich bin mit @ejmahler einverstanden. Wir sollten die andere Seite der Entwicklung nicht vergessen - die Überprüfung des Codes. Dateien mit Quellcode werden viel häufiger gelesen als geschrieben, daher sollte es einfacher zu lesen und zu verstehen sein als zu schreiben. Das Finden der Fließgrenzen ist bei der Codeüberprüfung sehr wichtig. Und ich persönlich würde für Useful precedence stimmen.
Ich glaube das:

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

ist leichter zu verstehen als dies:

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

@HeroicKatora

@Centril Ich würde gerne zustimmen, aber es gibt einige offene Fragen. Sind Sie sicher über 1. ? Wenn wir es "magisch" machen, könnten wir es dann nicht noch magischer machen, indem wir zulassen, dass dies auf ein Makro verweist, ohne es als Schlüsselwort zu löschen?

Technisch? Möglicherweise. Es sollte jedoch eine starke Rechtfertigung für Sonderfälle geben, und in diesem Fall scheint es nicht gerechtfertigt zu sein, Magie über Magie zu haben.

Das wirft die Frage auf, ob wir beabsichtigen, async/await als Anreiz für die Migration zur Edition 2018 oder eher geduldig zu sein und dies nicht zu wiederholen.

Ich weiß nicht, ob wir jemals gesagt haben, wir wollten ein neues Modulsystem, async / await und try { .. } als Anreize; aber unabhängig von unserer Absicht sind sie, und ich denke, das ist eine gute Sache. Wir möchten, dass die Leute irgendwann neue Sprachfunktionen verwenden, um bessere und idiomatischere Bibliotheken zu schreiben.

Zwei weitere (imho) zentrale Funktionen ähneln 3. : vec![] und format! / println! . Ersteres sehr, weil es keine stabile Boxkonstruktion gibt, afaik,

Ersteres existiert und ist vec![1, 2, 3, ..] , um Array-Literalausdrücke nachzuahmen, z. B. [1, 2, 3, ..] .

@ Ejmahler

"x.await" liest sich verlockend wie Englisch, aber nicht, wenn x eine nicht triviale Zeile ist, wie ein Elementfunktionsaufruf mit langen Namen oder eine Reihe verketteter Iteratormethoden usw.

Was ist los mit einer Reihe verketteter Iteratormethoden? Das ist ausgesprochen idiomatischer Rust.
Das Tool rustfmt formatiert auch Methodenketten in verschiedenen Zeilen, sodass Sie sie erhalten (verwenden Sie erneut match , um die Syntaxhervorhebung anzuzeigen):

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

Wenn Sie .await als "dann warten" lesen, liest es sich perfekt, zumindest für mich.

Und nachdem ich sie jahrelang im Produktionscode verwendet habe, ist es meiner Meinung nach absolut wichtig zu wissen, wo sich Ihre Ertragspunkte auf einen Blick befinden .

Ich sehe nicht, wie Postfix await negiert, insbesondere in der obigen Formatierung von rustfmt . Außerdem können Sie schreiben:

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

wenn du das magst.

in einer 200-Zeilen-Funktion in Sekundenschnelle ohne kognitive Belastung.

Ohne zu viel über die jeweilige Funktion zu wissen, scheint es mir, dass 200 LOC wahrscheinlich gegen das Prinzip der Einzelverantwortung verstoßen und zu viel tun. Die Lösung besteht darin, weniger zu tun und es aufzuteilen. Tatsächlich denke ich, dass dies das Wichtigste für die Wartbarkeit und Lesbarkeit ist.

Ich glaube, dass await eine grundlegende Kontrollflussoperation ist.

So ist ? . Tatsächlich sind await und ? effektive Kontrollflussoperationen, die "Wert aus dem Kontext extrahieren" sagen. Mit anderen Worten, im lokalen Kontext können Sie sich vorstellen, dass diese Operatoren den Typ await : impl Future<Output = T> -> T und ? : impl Try<Ok = T> -> T .

Es gibt Ausnahmen, aber sie sind Ausnahmen und nichts, wonach wir streben sollten.

Und die Ausnahme hier ist ? ?

@andreytkachenko

Ich bin mit @ejmahler einverstanden. Wir sollten die andere Seite der Entwicklung nicht vergessen - die Überprüfung des Codes. Dateien mit Quellcode werden viel häufiger gelesen als geschrieben, daher sollte es einfacher zu lesen und zu verstehen sein als zu schreiben.

Die Meinungsverschiedenheit besteht darin, was für die Lesbarkeit und Ergonomie am besten wäre.

ist leichter zu verstehen als dies:

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

So würde es nicht formatiert; Wenn Sie es durch rustfmt laufen lassen, erhalten Sie:

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

@ejmahler @andreytkachenko Ich stimme @Centril hier zu. Die größte Änderung (einige sagen vielleicht eine Verbesserung, würde ich nicht), die Sie durch die Präfixsyntax erhalten, besteht darin, dass Benutzer dazu angeregt werden, ihre Anweisungen über mehrere Zeilen aufzuteilen, da alles andere nicht lesbar ist. Das ist nicht Rust-y und die üblichen Formatierungsregeln machen dies in der Post-Fix-Syntax wieder wett. Ich halte die Fließgrenze auch für dunkler in der Präfixsyntax, da await nicht an der Code-Stelle platziert wird, an der Sie nachgeben, sondern dagegen.

Wenn Sie diesen Weg gehen, lassen Sie uns experimentieren, um es zu formulieren, und nicht mit await als Ersatz für let im Geiste von @Keruspes Idee, dies wirklich durchzusetzen. Ohne die anderen Syntaxerweiterungen, weil sie wie eine Strecke wirken.

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

Aber für keines von diesen sehe ich genug Nutzen, um ihren Verlust an Zusammensetzbarkeit und Komplikationen in der Grammatik zu erklären.

Hm ... ist es wünschenswert, sowohl eine Präfix- als auch eine Suffixform des Wartens zu haben? Oder nur die Suffixform?

Das Verketten von Methodenaufrufen ist speziell beim Sprechen idiomatisch
Iteratoren und in viel geringerem Maße Optionen, aber abgesehen davon ist Rost
zuerst eine imperative Sprache und dann eine funktionale Sprache.

Ich behaupte nicht, dass ein Postfix buchstäblich unverständlich ist, ich mache einen
Argument der kognitiven Last, das eine Kontrollflussoperation der obersten Ebene verbirgt,
Das Tragen der gleichen Bedeutung wie "Rückkehr" erhöht die kognitive Belastung, wenn
im Vergleich dazu, es so nah wie möglich am Anfang der Zeile zu platzieren - und ich bin es
Dieses Argument basiert auf jahrelanger Produktionserfahrung.

Am Samstag, 19. Januar 2019, um 11:59 Uhr Mazdak Farrokhzad [email protected]
schrieb:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril Ich würde gerne zustimmen, aber es gibt
einige offene Fragen. Bist du dir über 1. sicher? Wenn wir es "magisch" machen könnten
Wir machen es nicht noch magischer, indem wir zulassen, dass sich dies auf ein Makro ohne bezieht
als Schlüsselwort löschen?

Technisch? Möglicherweise. Es sollte jedoch eine starke Rechtfertigung dafür geben
Sonderfälle und in diesem Fall Magie auf Magie zu haben, scheint nicht
gerechtfertigt.

Das wirft die Frage auf, ob wir asynchronisieren / warten wollen
ein Anreiz, auf die Ausgabe 2018 zu migrieren oder eher geduldig zu sein und nicht
Wiederholen Sie dies.

Ich weiß nicht, ob wir jemals gesagt haben, wir wollten ein neues Modulsystem, async /
warte und versuche {..} Anreize zu sein; aber unabhängig von unserer Absicht
Sie sind es, und ich denke, das ist eine gute Sache. Wir wollen, dass die Leute es irgendwann tun
Verwenden Sie neue Sprachfunktionen, um besser und idiomatischer zu schreiben
Bibliotheken.

Zwei weitere (imho) zentrale Funktionen wie 3: vec! [] Und Format! /
println!. Ersteres sehr, weil es keine stabile Box gibt
Bau afaik,

Ersteres existiert und ist vec! [1, 2, 3, ..] geschrieben, um das Array nachzuahmen
wörtliche Ausdrücke, zB [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" liest sich verlockend wie Englisch, aber es wird nicht, wenn x ein ist
Nicht triviale Zeile, wie ein Mitgliedsfunktionsaufruf mit langen Namen oder ein Haufen
von verketteten Iteratormethoden usw.

Was ist los mit einer Reihe verketteter Iteratormethoden? Das ist eindeutig
idiomatischer Rost.
Das rustfmt-Tool formatiert auch Methodenketten in verschiedenen Zeilen, damit Sie
get (wieder Match verwenden, um die Syntaxhervorhebung anzuzeigen):

lass foo = alpha (). // oder alpha() match? , alpha()#match? , alpha().match!()?
.Beta
.some_other_stuff (). match?
.even_more_stuff (). match
.stuff_and_stuff ();

Wenn Sie .await als "dann warten" lesen, liest es sich perfekt, zumindest für mich.

Und nachdem ich sie jahrelang im Produktionscode verwendet habe, glaube ich, dass ich es weißWo Ihre Ertragspunkte liegen, ist auf einen Blick absolut kritisch .

Ich sehe nicht, wie das Warten auf Postfix das negiert, besonders im Rustfmt
Formatierung oben. Außerdem können Sie schreiben:

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

wenn du das magst.

in einer 200-Zeilen-Funktion in Sekundenschnelle ohne kognitive Belastung.

Ohne zu viel über die jeweilige Funktion zu wissen, scheint es mir
dass 200 LOC wahrscheinlich gegen das Prinzip der Einzelverantwortung verstoßen und dies auch tun
zu viel. Die Lösung besteht darin, weniger zu tun und es aufzuteilen. In der Tat, ich
Ich denke, das ist das Wichtigste für die Wartbarkeit und Lesbarkeit.

Ich glaube, dass das Warten eine grundlegende Kontrollflussoperation ist.

Also ist? In der Tat warten und? sind beide effektive Kontrollflussoperationen
das heißt "Wert aus dem Kontext extrahieren". Mit anderen Worten, in der lokalen
Im Kontext können Sie sich vorstellen, dass diese Operatoren den Typ erwarten: impl
Zukunft-> T und? : impl Try-> T.

Es gibt Ausnahmen, aber sie sind Ausnahmen und nicht etwas, das wir sollten
anstreben.

Und die Ausnahme hier ist? ?

@andreytkachenko https://github.com/andreytkachenko

Ich bin mit @ejmahler https://github.com/ejmahler einverstanden. Wir sollten nicht
Vergessen Sie die andere Seite der Entwicklung - Codeüberprüfung. Datei mit Quellcode ist
viel öfter gelesen als geschrieben, daher sollte es meiner Meinung nach einfacher sein
lesen und verstehen dann schreiben.

Die Meinungsverschiedenheit besteht darin, was für die Lesbarkeit und am besten wäre
Ergonomie.

ist leichter zu verstehen als dies:

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

So würde es nicht formatiert; die idiomatische rustfmt-Formatierung
ist:

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

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@ejmahler Wir einig . "versteckt"; Die gleichen Argumente wurden vorgebracht.

Der Operator ? ist sehr kurz und wurde in der Vergangenheit kritisiert, um eine Rückgabe zu "verbergen". Meistens wird Code gelesen und nicht geschrieben. Wenn Sie es in ?! umbenennen, ist es doppelt so lang und daher schwerer zu übersehen.

Trotzdem haben wir letztendlich ? stabilisiert und seitdem denke ich, dass die Prophezeiung nicht zustande gekommen ist.
Für mich folgt der Postfix await der natürlichen Lesereihenfolge (zumindest für Sprecher von links nach rechts). Insbesondere folgt es der Datenflussreihenfolge.

Ganz zu schweigen von der Hervorhebung der Syntax: Alles, was mit dem Warten zu tun hat, kann mit einer hellen Farbe hervorgehoben werden, sodass es auf einen Blick zu finden ist. Selbst wenn wir ein Symbol anstelle des tatsächlichen Wortes await , wäre es dennoch gut lesbar und im Code mit hervorgehobener Syntax auffindbar. Trotzdem bevorzuge ich die Verwendung des Wortes await nur aus Greifgründen - es ist einfacher, den Code für alles zu durchsuchen, was erwartet wird, wenn wir nur das Wort await anstelle eines Symbols verwenden wie @ oder #, deren Bedeutung grammatikalisch abhängig ist.

y'all das ist keine Raketenwissenschaft

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

postfix ... ist extrem lesbar, auf einen Blick kaum zu übersehen und sehr intuitiv, da Sie es natürlich als Code lesen, der nachlässt, während es darauf wartet, dass das Ergebnis der Zukunft verfügbar wird. Keine Vorrang- / Makro-Spielereien erforderlich und kein zusätzliches Linienrauschen durch unbekannte Siegel, da jeder zuvor Ellipsen gesehen hat.

(Entschuldigung an @solson)

@ ben0x539 Bedeutet das, dass ich auf ein Mitglied meines Ergebnisses wie future()....start zugreifen kann? Oder warten Sie auf ein Range-Ergebnis wie range()..... ? Und wie genau meinst du no precedence/macro shenanigans necessary and da derzeit Ellipsen .. ein binärer Operator oder eine Paranthesis auf der rechten Seite sein müssen, und dies ist auf einen Blick furchtbar nah.

Ja das ? Operator existiert. Ich habe bereits anerkannt, dass es solche gibt
Ausnahmen. Aber es ist eine Ausnahme. Die überwiegende Mehrheit der Kontrollflüsse in jedem
Das Rust-Programm erfolgt über Präfix-Schlüsselwörter.

Am Samstag, 19. Januar 2019 um 13:51 Uhr Benjamin Herr [email protected]
schrieb:

y'all das ist keine Raketenwissenschaft

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

postfix ... ist extrem lesbar, auf einen Blick kaum zu übersehen und super
intuitiv, da Sie es natürlich als Code lesen, der nachlässt
während es darauf wartet, dass das Ergebnis der Zukunft verfügbar wird. Nein
Vorrang / Makro-Spielereien notwendig und kein zusätzliches Leitungsrauschen von
ungewohnte Siegel, da jeder schon Ellipsen gesehen hat.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora Das sieht ein bisschen künstlich aus, aber ja sicher. Ich meinte, da es sich um eine Postfix-Operation handelt, wie bei den anderen vorgeschlagenen Postfix-Lösungen, wird die Notwendigkeit einer nicht intuitiven Priorität für await x? vermieden, und es handelt sich nicht um ein Makro.

@ Ejmahler

Ja das ? Operator existiert. Ich habe bereits anerkannt, dass es Ausnahmen gibt. Aber es ist eine Ausnahme.

Es gibt zwei Ausdrucksformen, die zu keyword expr passen, nämlich return expr und break expr . Ersteres ist häufiger als Letzteres. Das Formular continue 'label zählt nicht wirklich, da es zwar ein Ausdruck ist, aber nicht die Form keyword expr . Jetzt haben Sie also zwei unäre Ausdrucksformulare für das gesamte Präfix-Schlüsselwort und 1 unäre Ausdrucksformulare für das Postfix-Schlüsselwort. Bevor wir überhaupt berücksichtigen, dass ? und await ähnlicher sind als await und return , würde ich kaum return/break expr Eine Regel für ? ist eine Ausnahme.

Die überwiegende Mehrheit des Kontrollflusses in einem Rust-Programm erfolgt über Präfix-Schlüsselwörter.

Wie bereits erwähnt, ist break expr nicht allzu häufig ( break; ist typischer und return; sind typischer und sie sind keine unären Ausdrucksformen). Was bleibt, sind frühe return expr; s und es scheint mir überhaupt nicht klar zu sein, dass dies weitaus häufiger vorkommt als match , ? , nur verschachtelte if let s und else s und for Schleifen. Sobald sich try { .. } stabilisiert hat, würde ich erwarten, dass ? noch mehr verwendet werden.

@ ben0x539 Ich denke, wir sollten ... für verschiedene Generika reservieren, sobald wir bereit sind, sie zu haben

Die Idee, hier mit Postfix-Syntax zu innovieren, gefällt mir sehr gut. Es macht viel mehr Sinn mit dem Ablauf und ich erinnere mich, wie viel besser Code gedreht wurde, als wir vom Präfix try! zum Postfix ? übergingen. Ich denke, es gibt viele Leute, die in der Rust-Community die Erfahrung gemacht haben, wie viele Verbesserungen am Code vorgenommen wurden.

Wenn uns die Idee von .await nicht gefällt, kann man sicher kreativ werden, um einen tatsächlichen Postfix-Operator zu finden. Ein Beispiel könnte sein, ++ oder @ zum Warten zu verwenden.

:( Ich will einfach nicht mehr warten.

Jeder ist mit der Makrosyntax vertraut. Die meisten Leute in diesem Thread, die mit anderen Meinungen beginnen, scheinen die Makrosyntax zu bevorzugen.

Sicher, es wird ein „magisches Makro“ sein, aber Benutzer interessieren sich selten dafür, wie die Makroerweiterung aussieht, und für diejenigen, die dies tun, ist es einfach genug, die Nuancen in den Dokumenten zu erklären.

Regelmäßige Makrosyntax ist ein bisschen wie Apfelkuchen, es ist die zweite Lieblingsoption aller, aber als Ergebnis die Lieblingsoption der Familie [0]. Wichtig, wie beim Ausprobieren! wir können es später jederzeit ändern. Aber am wichtigsten ist, je früher wir uns alle einig sind, desto eher können wir alle damit beginnen, es tatsächlich zu nutzen und produktiv zu sein!

[0] (in der ersten Minute referenziert) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=de

Übereinstimmung, wenn, wenn let, while, while let und for sind alle allgegenwärtigen Kontrollflüsse
die Präfixe verwenden. Das Vorgeben von Pause und Fortfahren ist der einzige Kontrollfluss
Schlüsselwörter sind frustrierend irreführend.

Am Samstag, 19. Januar 2019, um 15:37 Uhr Yazad Daruvala [email protected]
schrieb:

:( Ich will einfach nicht mehr warten.

Jeder ist mit der Makrosyntax vertraut, die meisten Leute in diesem Thread
Beginnen Sie mit anderen Meinungen, die am Ende die Makrosyntax bevorzugen.

Sicher, es wird ein "magisches Makro" sein, aber Benutzer interessieren sich selten für das Makro
Erweiterung sieht aus wie und für diejenigen, die dies tun, ist es einfach genug, das zu erklären
Nuance in den Dokumenten.

Regelmäßige Makrosyntax ist ein bisschen wie Apfelkuchen, es ist jedermanns Sekunde
Lieblingsoption, aber als Ergebnis die Lieblingsoption der Familie [0].
Wichtig, wie beim Ausprobieren! wir können es später jederzeit ändern. Aber vorallem
Je früher wir uns alle einig sind, desto eher können wir alle anfangen
tatsächlich nutzen und produktiv sein!

[0] (In der ersten Minute referenziert)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=de

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mitsuhiko Ich stimme zu! Postfix fühlt sich aufgrund der Verkettung rustikaler an. Ich denke, die fut@await mir vorgeschlagene

@ Ejmahler

match, if, if let, while, while let und for sind alle allgegenwärtigen Kontrollflüsse, die Präfixe verwenden. Das Vorgeben von Unterbrechung und Fortfahren sind die einzigen Schlüsselwörter für den Kontrollfluss, die frustrierend irreführend sind.

Es ist überhaupt nicht irreführend. Die relevante Grammatik für diese Konstrukte ist ungefähr:

Expr = kind:ExprKind;
ExprKind =
  | If:{ "if" cond:Cond then:Block { "else" else_expr:ElseExpr }? };
  | Match:{ "match" expr:Expr "{" arms:MatchArm* "}" }
  | While:{ { label:LIFETIME ":" }? "while" cond:Cond body:Block }
  | For:{ { label:LIFETIME ":" }? "for" pat:Pat "in" expr:Expr body:Block }
  ;

Cond =
  | Bool:Expr
  | Let:{ "let" pat:Pat "=" expr:Expr }
  ;

ElseExpr =
  | Block:Block
  | If:If
  ;

MatchArm = pats:Pat+ % "|" { "if" guard:Expr }? "=>" body:Expr ","?;

Hier sind die Formulare if/while expr block , for pat in expr block und match expr { pat0 => expr0, .., patn => exprn } . In all diesen Formen steht ein Schlüsselwort vor dem Folgenden. Ich denke, das ist es, was Sie unter "Präfixe verwenden" verstehen. Dies sind jedoch alles Blockformen und keine unären Präfixoperatoren. Der Vergleich mit await expr ist daher irreführend, da es keine nennenswerte Konsistenz oder Regel gibt. Wenn Sie Konsistenz mit Blockformularen anstreben, vergleichen Sie dies mit await block , nicht mit await expr .

@mitsuhiko Ich stimme zu! Postfix fühlt sich aufgrund der Verkettung rustikaler an.

Rost ist dualistisch. Es unterstützt sowohl imperative als auch funktionale Ansätze. Und ich denke, es ist in Ordnung, weil in verschiedenen Fällen jeder von ihnen besser geeignet sein kann.

Ich weiß es nicht. Es wäre großartig, beides zu haben:

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

Nachdem ich Scala eine Weile benutzt hatte, mochte ich auch viele Dinge, die so funktionierten. Besonders match und if wären in Postfix-Positionen in Rust eine gute Wahl.

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

Zusammenfassung bisher

Optionsmatrizen:

Zusammenfassende Matrix von Optionen (unter Verwendung von @ als Siegel, kann aber meistens alles sein):

| Name | Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
| --- | --- | --- | --- |
| PREFIX | - | - | - |
| Schlüsselwort Makro | await!(fut) | await!(fut)? | await!(fut?) |
| Schlüsselwort Funktion | await(fut) | await(fut)? | await(fut?) |
| Nützlicher Vorrang await fut | await fut? | await (fut?) |
| Offensichtlicher Vorrang | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn Mit Schlüsselwort | fut(await) | fut(await)? | fut?(await) |
| Schlüsselwortfeld | fut.await | fut.await? | fut?.await |
| Schlüsselwortmethode | fut.await() | fut.await()? | fut?.await() |
| Postfix-Schlüsselwortmakro | fut.await!() | fut.await!()? | fut?.await!() |
| Leerzeichen Schlüsselwort | fut await | fut await? | fut? await |
| Siegel Schlüsselwort | fut@await | fut@await? | fut?@await |
| Siegel | fut@ | fut@? | fut?@ |

Das Siegel von "Sigil Keyword" kann nicht # , da dies mit einer Zukunft namens r . ... da das Siegel die Tokenisierung nicht wie meine erste Sorge ändern müsste .

Mehr reale Verwendung (PM mir andere _real_ Anwendungsfälle mit mehreren await auf urlo und ich werde sie hinzufügen):

| Name | (erforderlich) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json | > ? |
| --- | --- |
| PREFIX | - |
| Schlüsselwort Makro | await!(client.get("url").send())?.json()? |
| Schlüsselwort Funktion | await(client.get("url").send())?.json()? |
| Nützlicher Vorrang (await client.get("url").send()?).json()? |
| Offensichtlicher Vorrang | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn Mit Schlüsselwort | client.get("url").send()(await)?.json()? |
| Schlüsselwortfeld | client.get("url").send().await?.json()? |
| Schlüsselwortmethode | client.get("url").send().await()?.json()? |
| Postfix-Schlüsselwortmakro | client.get("url").send().await!()?.json()? |
| Leerzeichen Schlüsselwort | client.get("url").send() await?.json()? |
| Siegel Schlüsselwort | client.get("url").send()@await?.json()? |
| Siegel | client.get("url").send()@?.json()? |

BEARBEITUNGSHINWEIS: Ich wurde darauf hingewiesen, dass es für Response::json möglicherweise sinnvoll ist, auch ein Future , wobei send auf das ausgehende E / A und json wartet

Es scheint ein grober Konsens darüber zu bestehen, dass von den Präfixoptionen die offensichtliche Priorität (zusammen mit dem Zucker await? ) am wünschenswertesten ist. Viele Leute haben sich jedoch für eine Postfix-Lösung ausgesprochen, um die Verkettung wie oben zu vereinfachen. Obwohl die Präfixauswahl einen groben Konsens aufweist, scheint es keinen Konsens darüber zu geben, welche Postfix-Lösung am besten ist. Alle vorgeschlagenen Optionen führen zu leichter Verwirrung (gemindert durch Hervorheben von Schlüsselwörtern):

  • Fn With Keyword => Aufruf eines Fn mit einem Argument namens await
  • Schlüsselwortfeld => Feldzugriff
  • Schlüsselwort Methode => Methodenaufruf
  • Makro (Präfix oder Postfix) => Ist await ein Schlüsselwort oder nicht?
  • Space Keyword => bricht die Gruppierung in eine Zeile (besser über mehrere Zeilen?)
  • Sigil => fügt einer Sprache, die bereits als sigillastig empfunden wird, neue Siegel hinzu

Andere drastischere Vorschläge:

  • Lassen Sie in Zukunft sowohl das Präfix (offensichtliche Priorität) als auch das Postfix "Feld" (dies könnte auf weitere Schlüsselwörter wie match , if usw. angewendet werden, um dies zu einem verallgemeinerten Muster zu machen, ist jedoch unnötig Nachtrag zu dieser Debatte) [[Referenz] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await in Mustern zum Auflösen von Futures (überhaupt keine Verkettung) [[Referenz] (https://github.com/rust-lang/rust/issues/57640)]
  • Verwenden Sie einen Präfixoperator, aber erlauben Sie eine Verzögerung [[Referenz] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

Die Stabilisierung mit einem Schlüsselwortmakro await!(fut) ist natürlich mit praktisch allen oben genannten Funktionen zukunftskompatibel. Dazu muss das Makro jedoch ein Schlüsselwort anstelle eines regulären Bezeichners verwenden.

Wenn jemand ein Beispiel hat, das größtenteils real ist und zwei await in einer Kette verwendet, würde ich es gerne sehen. Bisher hat noch niemand einen geteilt. Das Postfix await ist jedoch auch dann nützlich, wenn Sie await mehr als einmal in einer Kette benötigen, wie im Beispiel reqwest gezeigt.

Wenn ich etwas Bemerkenswertes über diesem zusammenfassenden Kommentar verpasst habe, schreibe mir eine PM auf urlo und ich werde versuchen, es hinzuzufügen.

Persönlich habe ich mich historisch für das Präfix-Schlüsselwort mit der offensichtlichen Priorität ausgesprochen. Ich denke immer noch, dass die Stabilisierung mit einem Schlüsselwortmakro await!(fut) nützlich wäre, um reale Informationen darüber zu sammeln, wo das Warten in realen Anwendungsfällen stattfindet, und es uns weiterhin ermöglichen würde, später eine Nicht-Makro-Präfix- oder Postfix-Option hinzuzufügen .

Während ich die obige Zusammenfassung schrieb, fing ich jedoch an, "Keyword Field" zu mögen. "Space Keyword" fühlt sich gut an, wenn es in mehrere Zeilen aufgeteilt wird:

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

aber in einer Zeile macht es eine unangenehme Unterbrechung, die den Ausdruck schlecht gruppiert: client.get("url").send() await?.json()? . Mit dem Schlüsselwortfeld sieht es jedoch in beiden Formen gut aus: client.get("url").send().await?.json()?

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

obwohl ich nehme an, dass eine "Schlüsselwortmethode" besser fließen würde, da es sich um eine Aktion handelt. Wir könnten es sogar zu einer "echten" Methode für Future wenn wir wollten:

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

( extern "rust-await" würde natürlich die ganze Magie bedeuten, die erforderlich ist, um das Warten tatsächlich durchzuführen, und es wäre eigentlich kein echtes fn, es wäre hauptsächlich nur da, weil die Syntax wie eine Methode aussieht, wenn ein Schlüsselwort Methode verwendet wird.)

Erlaube sowohl Präfix (offensichtliche Priorität) als auch Postfix "Feld" ...

Wenn eine Postfix-Syntax ausgewählt wird (egal ob zusammen oder anstelle der Präfix-Syntax), wäre dies definitiv ein Argument für eine zukünftige Diskussion: Wir haben jetzt Schlüsselwörter, die sowohl in Präfix- als auch in Postfix-Notation funktionieren, gerade weil manchmal eines vorzuziehen ist die andere, also könnten wir vielleicht beides zulassen, wo es Sinn macht, und die Flexibilität der Syntax erhöhen, während wir die Regeln vereinheitlichen. Vielleicht ist es eine schlechte Idee, vielleicht wird sie abgelehnt, aber es ist definitiv eine Diskussion in der Zukunft, wenn die Postix-Notation für await .

Ich denke, es besteht nur eine sehr geringe Wahrscheinlichkeit, dass eine Syntax, die die erwartete Zeichenfolge nicht enthält, für diese Syntax akzeptiert wird.

: +1:


Ein zufälliger Gedanke, den ich hatte, nachdem ich einige Beispiele hier gesehen hatte ( wie zum Beispiel @mehcodes ): Eine der Beschwerden, an die ich mich bei .await erinnere, ist, dass es zu schwer ist, † zu sehen, aber angesichts der erwarteten Dinge In der Regel fehlbar, hilft die Tatsache, dass es oft .await? , ohnehin zusätzliche Aufmerksamkeit darauf zu lenken.

† Wenn Sie etwas verwenden, das keine Schlüsselwörter hervorhebt


@ Ejmahler

Ich lehne jede Syntax ab, die sich nicht wie Englisch liest

So etwas wie request.get().await liest sich genauso gut wie so etwas wie body.lines().collect() . In "einer Reihe von verketteten Iterator-Methoden" liest sich _prefix_ meiner Meinung nach tatsächlich schlechter, da Sie sich daran erinnern müssen, dass sie am Anfang "Warten" gesagt haben und nie wissen, wann Sie etwas hören, wenn es das sein wird, was Sie sind Warten auf, ein bisschen wie ein Gartenpfad Satz .

Und nachdem ich sie jahrelang im Produktionscode verwendet habe, ist es meiner Meinung nach absolut wichtig zu wissen, wo sich Ihre Ertragspunkte auf einen Blick befinden. Wenn Sie die Einrückungslinie Ihrer Funktion überfliegen, können Sie jede co_await / Yield-Rendite in einer 200-Zeilen-Funktion innerhalb von Sekunden ohne kognitive Belastung auswählen.

Dies impliziert, dass es in einem Ausdruck niemals einen gibt, was eine Einschränkung ist, die ich angesichts der ausdrucksorientierten Natur von Rust absolut nicht unterstützen würde. Und zumindest mit C # await ist es absolut plausibel, CallSomething(argument, await whatever.Foo() .

Angesichts der Tatsache, dass async _will_ in der Mitte von Ausdrücken erscheint, verstehe ich nicht, warum es einfacher ist, im Präfix zu sehen als im Postfix.

Es sollte das gleiche Maß an Respekt erhalten wie 'wenn, während, übereinstimmen und zurückkehren. Stellen Sie sich vor, einer dieser Operatoren wäre Postfix-Operatoren - das Lesen von Rust-Code wäre ein Albtraum.

return (und continue und break ) und while sind als völlig nutzlos für die Kette bemerkenswert, da sie immer ! und () . Und während Sie aus irgendeinem Grund for weggelassen haben, haben wir gesehen, dass Code mit .for_each() ohne schlechte Effekte, insbesondere in Rayon, gut geschrieben wurde .

Wir müssen uns wahrscheinlich damit abfinden, dass async/await ein wichtiges Sprachmerkmal sein wird. Es wird in allen Arten von Code angezeigt. Es wird das Ökosystem durchdringen - an einigen Stellen wird es so häufig sein wie ? . Die Leute müssen es lernen.

Infolgedessen möchten wir uns möglicherweise bewusst darauf konzentrieren, wie sich die Syntaxauswahl nach längerer Verwendung anfühlt, anstatt wie sie sich zunächst anfühlt.

Wir müssen auch verstehen, dass await Bezug auf Kontrollflusskonstrukte eine andere Art von Tier ist. Konstrukte wie return , break , continue und sogar yield können intuitiv in Form von jmp . Wenn wir diese sehen, springen unsere Augen über den Bildschirm, weil sich der Kontrollfluss, der uns wichtig ist, an eine andere Stelle bewegt. Während await den Kontrollfluss der Maschine beeinflusst, verschiebt es nicht den Kontrollfluss, der für unsere Augen und für unser intuitives Verständnis des Codes wichtig ist.

Wir sind nicht versucht, bedingungslose Anrufe an return oder break zu ketten, da dies keinen Sinn ergeben würde. Aus ähnlichen Gründen sind wir nicht versucht, unsere Prioritätsregeln zu ändern, um sie zu berücksichtigen. Diese Operatoren haben eine niedrige Priorität. Sie nehmen alles nach rechts und geben es irgendwo zurück, wodurch die Ausführung innerhalb dieser Funktion oder dieses Blocks beendet wird. Der Operator await möchte jedoch verkettet werden. Es ist ein wesentlicher Bestandteil eines Ausdrucks, nicht das Ende.

Nachdem ich die Diskussion und die Beispiele in diesem Thread betrachtet habe, habe ich das nagende Gefühl, dass wir überraschende Vorrangregeln noch bereuen würden.

Der Stalking Horse-Kandidat scheint vorerst mit await!(expr) zu gehen und hofft, dass später etwas Besseres herausgearbeitet wird. Bevor ich die Anmerkungen von habe , hätte ich dies wahrscheinlich unterstützt, um diese wichtige Funktion mit nahezu jeder Syntax herauszubringen. Seine Argumente überzeugen mich jedoch, dass dies nur aufhören würde. Wir wissen, dass die Verkettung von Methodenaufrufen in Rust wichtig ist. Dies führte zur Einführung des Schlüsselworts ? , das weit verbreitet und äußerst erfolgreich ist. Die Verwendung einer Syntax, von der wir wissen, dass sie uns enttäuschen wird, bedeutet in der Tat nur, technische Schulden hinzuzufügen.

Zu Beginn dieses Threads gab @withoutboats an, dass nur vier vorhandene Optionen realisierbar erscheinen. Von diesen wird uns wahrscheinlich nur die Postfix-Syntax expr await langfristig glücklich machen. Diese Syntax sorgt nicht für seltsame Überraschungen. Es zwingt uns nicht, eine Präfixversion des Operators ? zu erstellen. Es funktioniert gut mit Methodenverkettung und unterbricht den Kontrollfluss von links nach rechts nicht. Unser erfolgreicher ? -Operator dient als Präzedenzfall für einen Postfix-Operator, und await ? in der Praxis eher return , break oder yield . Während ein Postfix-Nicht-Symbol-Operator in Rust möglicherweise neu ist, ist die Verwendung von async/await weit genug verbreitet, um ihn schnell bekannt zu machen.

Während alle Optionen für eine Postfix-Syntax praktikabel zu sein scheinen, hat expr await einige Vorteile. Diese Syntax macht deutlich, dass await ein Schlüsselwort ist, mit dessen Hilfe der magische Kontrollfluss hervorgehoben werden kann. Im Vergleich zu expr.await , expr.await() expr.await! , expr.await!() usw. muss nicht erklärt werden, dass dies wie ein Feld / eine Methode / ein Makro aussieht , aber wirklich ist nicht in diesem einen speziellen Fall. Wir würden uns hier alle an den Raumtrenner gewöhnen.

Es ist ansprechend, await als @ oder ein anderes Symbol zu verwenden, das keine Analyseprobleme verursacht. Es ist sicherlich wichtig genug, um dies zu gewährleisten. Aber wenn es am Ende await muss, ist das in Ordnung. Solange es sich in der Postfix-Position befindet.

Wie jemand _real_ Beispiele erwähnte ... Ich pflege eine (laut tokei) 23.858 Zeilen Rost-Codebasis, die sehr stark asynchron ist und Futures von 0,1 await (sehr experimentell, wie ich weiß). Lass uns gehen (redigiert) Höhlenforschung (beachte, dass alles durch rustfmt gelaufen ist):

// A
if !await!(db.is_trusted_identity(recipient.clone(), message.key.clone()))? {
    info!("recipient: {}", recipient);
}

// B
match await!(db.load(message.key))? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await!(client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())?
.error_for_status()?;

// D
let mut res =
    await!(client.get(inbox_url).headers(inbox_headers).send())?.error_for_status()?;

let mut res: InboxResponse = await!(res.json())?;

// E
let mut res = await!(client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())?
.error_for_status()?;

let res: Response = await!(res.json())?;

// F
#[async]
fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await!(self.request(url, Method::GET, None, true))?;
    let user = await!(res.json::<UserResponse>())?
        .user
        .into();

    Ok(user)
}

Lassen Sie uns dies nun in die beliebteste Präfixvariante umwandeln, die offensichtlich Vorrang vor Zucker hat. Aus offensichtlichen Gründen wurde dies nicht durch rustfmt geführt, also entschuldigen Sie, wenn es einen besseren Weg gibt, es zu schreiben.

// A
if await? db.is_trusted_identity(recipient.clone(), message.key.clone()) {
    info!("recipient: {}", recipient);
}

// B
match await? db.load(message.key) {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = (await? client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())
.error_for_status()?;

// D
let mut res =
    (await? client.get(inbox_url).headers(inbox_headers).send()).error_for_status()?;

let mut res: InboxResponse = await? res.json();

// E
let mut res = (await? client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())
.error_for_status()?;

let res: Response = await? res.json();

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await? self.request(url, Method::GET, None, true);
    let user = (await? res.json::<UserResponse>())
        .user
        .into();

    Ok(user)
}

Lassen Sie uns dies schließlich in meine bevorzugte Postfix-Variante "Postfix-Feld" umwandeln.

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()).await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key).await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send().await?
    .error_for_status()?;

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

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true).await?
        .res.json::<UserResponse>().await?
        .user
        .into();

    Ok(user)
}

Nach dieser Übung finde ich eine Reihe von Dingen.

  • Ich bin jetzt stark gegen await? foo . Es liest sich gut für einfache Ausdrücke, aber die ? fühlen sich bei komplexen Ausdrücken verloren. Wenn wir ein Präfix machen müssen, hätte ich lieber einen "nützlichen" Vorrang.

  • Die Verwendung der Postfix-Notation führt dazu, dass ich Anweisungen verbinde und unnötige Let-Bindungen reduziere.

  • Die Verwendung der Postfix _field_-Notation führt dazu, dass ich .await? lieber in der Zeile des erwarteten Objekts als in einer eigenen Zeile im rustfmt-Sprachgebrauch vorziehe.

Ich schätze die Ellipsen-Postfix-Notation "..." oben, sowohl wegen ihrer Prägnanz als auch symbolisch in englischer Sprache, die eine Pause in Erwartung von etwas anderem darstellt. (So ​​wie asynchrones Verhalten funktioniert!), Verkettet es sich auch gut.

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

Ich bezweifle, dass andere Optionen so präzise und visuell aussagekräftig sind.

EIN

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

B.

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

Sollte es überhaupt als gute Form angesehen werden, lange Ketten von Wartezeiten zu haben?

Warum nicht einfach normale Future Kombinatoren verwenden?

Tatsächlich lassen sich einige Ausdrücke nicht gut in Warteketten übersetzen, wenn Sie bei einem Fehler ein Backup-Verhalten wünschen.

Persönlich denke ich das:

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

value.unwrap()

liest viel natürlicher als dies:

let value = match await some_op() {
    Ok(v) => match await v.another_op() {
        Ok(v2) => await v2.final_op(),
        Err(_) => await backup_op(),
    },
    Err(_) => await backup_op(),
};

value.unwrap()

Schließlich haben wir immer noch die volle Kraft von Zero-Cost-Futures zur Hand.

Berücksichtige das.

@EyeOfPython Ich möchte hervorheben, dass wir eine andere Wahl haben als @ in future@await . Wir können future~await schreiben, wobei ~ wie ein Bindestrich funktioniert und für alle möglichen Postfix-Operatoren funktioniert.

Der Bindestrich - wurde bereits als Minusoperator und Negativoperator verwendet. Nicht mehr gut Aber ~ wurde verwendet, um Heap-Objekte in Rust anzuzeigen, und ansonsten wurde es in keiner Programmiersprache verwendet. Es sollte weniger Verwirrung für Menschen aus anderen Sprachen geben.

@earthengine Gute Idee, aber vielleicht verwenden Sie einfach future~ was bedeutet, dass Sie auf eine Zukunft warten müssen, in der das ~ wie das Schlüsselwort await funktioniert (wie das Symbol ?

Zukunft | Zukunft des Ergebnisses | Ergebnis der Zukunft
- | - | - -
Zukunft ~ | Zukunft ~? | Zukunft? ~

Auch verkettete Futures wie:

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

Ich habe mir angesehen, wie Go asynchrone Programmierung implementiert, und dort etwas Interessantes gefunden. Die nächstgelegene Alternative zu Futures in Go sind Kanäle. Und anstatt await oder einer anderen schreienden Syntax, um auf Werte zu warten, bietet Go Channels zu diesem Zweck nur den Operator <- . Für mich sieht es ziemlich sauber und unkompliziert aus. Früher habe ich oft gesehen, wie die Leute Go für seine einfache Syntax und die guten asynchronen Funktionen loben. Es ist also definitiv eine gute Idee, etwas aus seiner Erfahrung zu lernen.

Leider konnten wir nicht genau die gleiche Syntax verwenden, da der Rust-Quellcode viel mehr spitze Klammern enthält als der von Go, hauptsächlich aufgrund von Generika. Dies macht den Operator <- wirklich subtil und es ist nicht angenehm, mit ihm zu arbeiten. Ein weiterer Nachteil ist, dass es in der Funktionssignatur als Gegenteil von -> kann und es keinen Grund gibt, dies so zu betrachten. Ein weiterer Nachteil ist, dass <- sigil als placement new implementiert werden sollte, damit die Leute es falsch interpretieren können.

Nach einigen Experimenten mit der Syntax hörte ich bei <-- sigil auf:

let output = <-- future;

Im Kontext <-- async <-- ziemlich einfach, obwohl weniger als <- . Stattdessen bietet es einen großen Vorteil gegenüber <- sowie gegenüber dem Präfix await - es spielt gut mit Einrückungen.

async fn log_service(&self) -> T {
   let service = self.myService.foo();
   <-- self.logger.log("beginning service call");
   let output = <-- service.exec();
   <-- self.logger.log("foo executed with result {}.", output));
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = <-- acquire_lock();
    let length = <-- logger.log_into(message)?;
    <-- logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    <-- (<-- partial_computation()).unwrap_or_else(or_recover);
}

Für mich sieht dieser Operator noch einzigartiger und leichter zu erkennen aus als await (was eher wie jedes andere Schlüsselwort oder jede andere Variable im Codekontext aussieht). Leider sieht es auf Github dünner aus als in meinem Code-Editor, aber ich denke, das ist nicht fatal und wir können damit leben. Selbst wenn sich jemand unwohl fühlt, werden alle Probleme durch eine andere Hervorhebung der Syntax oder eine bessere Schriftart (insbesondere bei Ligaturen) behoben.

Ein weiterer Grund, diese Syntax zu mögen, ist, dass sie konzeptionell als "etwas, das noch nicht hier ist" ausgedrückt werden könnte. Die Pfeilrichtung von rechts nach links ist entgegengesetzt zu der Richtung, in der wir Text lesen, was es uns ermöglicht, ihn als "etwas, das aus der Zukunft kommt" zu beschreiben. Die lange Form des Operators <-- deutet auch darauf hin, dass "ein dauerhafter Betrieb beginnt". Die spitze Klammer und zwei von future gerichtete Bindestriche könnten "kontinuierliche Abfrage" symbolisieren. Und wir können es immer noch als "warten" lesen, wie es vorher war.
Keine Art von Erleuchtung, könnte aber Spaß machen.


Das Wichtigste in diesem Vorschlag ist, dass auch eine ergonomische Methodenverkettung möglich wäre. Die Idee des verzögerten Präfixoperators, die ich zuvor vorgeschlagen habe, passt hier gut. Auf diese Weise hätten wir das Beste aus beiden Welten der Präfix- und Postfix-Syntax await . Ich hoffe wirklich, dass es auch einige nützliche Extras gibt, die ich persönlich schon oft wollte: verzögerte Dereferenzierung und verzögerte Negationssyntax .

Ich bin mir nicht sicher, ob ein verzögertes Wort hier richtig ist. Vielleicht sollten wir es anders benennen.

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

Die Priorität des Bedieners ist ziemlich offensichtlich: von links nach rechts.

Ich hoffe, dass diese Syntax den Bedarf beim Schreiben von is_not_* -Funktionen verringert, deren Zweck nur darin besteht, eine bool -Eigenschaft zu negieren und zurückzugeben. Und Boolesche / Dereferenzierungsausdrücke sind in einigen Fällen sauberer, wenn sie verwendet werden.


Schließlich habe ich es auf Beispiele aus der Praxis angewendet, die von <-- die Funktion async innerhalb von Methodenaufrufketten richtig hervorhebt . Im Gegensatz dazu sieht Postfix await wie ein normaler Feldzugriff oder Funktionsaufruf aus (abhängig von der Syntax), und es ist fast unmöglich, zwischen ihnen zu unterscheiden, ohne eine spezielle Syntaxhervorhebung oder -formatierung.

// A
if db.<--is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.<--load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .<--send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .<--send()?
    .error_for_status()?
    .<--json()?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.<--request(url, Method::GET, None, true)?
        .res.<--json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

Immerhin: Das ist die Syntax, die ich verwenden möchte.

@novacrazy

Sollte es überhaupt als gute Form angesehen werden, lange Ketten von Wartezeiten zu haben? Warum nicht einfach normale Future-Kombinatoren verwenden?

Ich bin mir nicht sicher, ob ich Sie nicht falsch verstanden habe, aber diese sind nicht exklusiv. Sie können auch weiterhin Kombinatoren verwenden

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

Damit dies funktioniert, muss die and_then -Klausel mit FnOnce(T) -> impl Future<_> eingegeben werden, nicht nur mit FnOnce(T) -> U . Wenn Sie in Zukunft verkettete Kombinatoren verwenden und das Ergebnis nur ohne Paranthesis in Postfix sauber funktioniert:

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

In diesem Beitrag werde ich mich auf die Frage der Priorität des

  • Methodenaufruf ( future.await() )
  • Feldausdruck ( future.await )
  • Funktionsaufruf ( future(await) )

Die Unterschiede in der Funktionalität sind eher gering, aber vorhanden. Beachten Sie, dass ich all dies akzeptieren würde, dies ist meistens eine Feinabstimmung. Um sie alle zu zeigen, brauchen wir einige Typen. Bitte kommentieren Sie nicht die Erfindung des Beispiels, dies ist die am meisten komprimierte Version, die alle Unterschiede auf einmal zeigt.

struct Foo<A, F, S> where A: Future<Output=F>, F: FnOnce(usize) -> S {
    member: A,
}

// What we want to do, in macro syntax:
let foo: Foo<_, _, _> = …;
(await!(foo.member))(42)
  • Methodenaufruf: foo.member.await()(42)
    Bindet am stärksten, also überhaupt keine Paranthesis
  • Mitglied: (foo.member.await)(42)
    Benötigt eine Klammer um das erwartete Ergebnis, wenn dies ein Callable ist. Dies steht im Einklang mit einem Callable als Mitglied, andernfalls Verwechslung mit der Call-to-Member-Funktion. Dies deutet auch darauf hin, dass man mit Mustern zerstören könnte: let … { await: value } = foo.member; value(42) irgendwie?
  • Funktionsaufruf: (foo.member)(await)(42)
    Benötigt eine Klammer für die Destrukturierung (wir verschieben das Mitglied), da es sich wie ein Funktionsaufruf verhält.

Sie sehen alle gleich aus, wenn wir weder eine Eingabestruktur durch Verschieben eines Mitglieds zerstören noch das Ergebnis als aufrufbar bezeichnen, da diese drei Prioritätsklassen direkt nacheinander kommen. Wie sollen sich Futures verhalten?

Die beste Parallele zum Methodenaufruf sollte nur ein weiterer Methodenaufruf sein, der self .

Die beste Parallele zum Mitglied ist eine Struktur, die nur das implizite Mitglied await und daher durch Verschieben von diesem zerstört wird, und dieser Verschieben wartet implizit auf die Zukunft. Dieser fühlt sich am wenigsten obivös an.

Die Parallele zur Funktion ist Aufruf ist das Verhalten von Schließungen. Ich würde diese Lösung als sauberste Ergänzung zum Sprachkorpus bevorzugen (wie in der Syntax am besten mit den Möglichkeiten des Typs übereinstimmt), aber es gibt einige positive Punkte für den Methodenaufruf und .await ist nie länger als die anderen.

@HeroicKatora Wir könnten .await in libsyntax als Sonderfall verwenden, um foo.await(42) zuzulassen, aber dies wäre inkonsistent / ad-hoc. (foo.await)(42) scheint jedoch brauchbar zu sein, da es zwar Futures gibt, die Schließungen ausgeben, diese aber wahrscheinlich nicht allzu häufig sind. Wenn wir also für den allgemeinen Fall optimieren, gewinnt es wahrscheinlich unter dem Strich, wenn wir nicht () zu .await hinzufügen müssen.

@Centril Ich stimme zu, Konsistenz ist wichtig. Wenn ich ein paar ähnliche Verhaltensweisen sehe, möchte ich andere durch Synthese ableiten. Aber hier scheint .await umständlich zu sein, insbesondere wenn das obige Beispiel deutlich zeigt, dass es Parallelen zur impliziten Destrukturierung aufweist (es sei denn, Sie finden eine andere Syntax, in der diese Effekte auftreten?). Wenn ich eine Destrukturierung sehe, frage ich mich sofort, ob ich dies mit Let-Bindings usw. verwenden kann. Dies wäre jedoch nicht möglich, da wir den ursprünglichen Typ zerstören würden, der kein solches Mitglied hat, oder insbesondere, wenn der Typ nur impl Future<Output=F> (Meist irrelevante Nebenbemerkung: Wenn wir diese Arbeit machen, kehren wir zu einem alternativen Präfix zurück await _ = anstelle von let _ = , lustig¹).

Das verbietet uns nicht, die Syntax per se zu verwenden. Ich denke, ich könnte damit umgehen, sie zu lernen, und wenn sich herausstellt, dass es die letzte ist, werde ich sie mit Nachdruck verwenden, aber es scheint eine klare Schwäche zu sein.


¹ Dies kann konsistent sein, während ? indem ? hinter Namen in einem Muster zugelassen werden

  • await value? = failing_future();

um den Ok Teil eines Result . Dies scheint auch in anderen Kontexten interessant zu sein, aber eher außerhalb des Themas. Dies würde auch dazu führen, dass Präfix- und Suffixsyntax für await gleichzeitig übereinstimmen.

Das verbietet uns nicht, die Syntax per se zu verwenden. Ich denke, ich könnte damit umgehen, sie zu lernen, und wenn sich herausstellt, dass es die letzte ist, werde ich sie mit Nachdruck verwenden, aber es scheint eine klare Schwäche zu sein.

Ich denke, jede Lösung wird in irgendeiner Dimension oder in jedem Fall einen Nachteil haben. Konsistenz, Ergonomie, Verkettbarkeit, Lesbarkeit, ... Dies macht es zu einer Frage des Grades, der Wichtigkeit der Fälle, der Eignung für typischen Rust-Code und APIs usw.

Im Fall eines Benutzers, der foo.await(42) schreibt ...

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

    foo.closure(42);
}

... bieten wir bereits eine gute Diagnose:

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

Eine Anpassung an foo.await(42) scheint durchaus erreichbar zu sein. Soweit ich sehen kann, wissen wir, dass der Benutzer (foo.await)(42) beabsichtigt, wenn foo.await(42) geschrieben wird, so dass dies cargo fix in einem MachineApplicable Weise. In der Tat, wenn wir foo.await stabilisieren, aber foo.await(42) nicht zulassen, können wir die Priorität meiner Meinung nach später sogar ändern, wenn dies erforderlich ist, da foo.await(42) nicht legal ist.

Weitere Verschachtelungen würden funktionieren (z. B. Zukunft des Ergebnisses der Schließung - nicht, dass dies üblich sein wird):
`` `Rost
struct HasClosure fn _foo () -> Ergebnis <(), ()> {
let foo: HasClosure <_ i = "27"> = HasClosure {Schließung: Ok (| x | {})};

foo.closure?(42);

Ok(())

}}

zB Zukunft des Ergebnisses der Schließung - nicht, dass dies üblich sein wird

Das zusätzliche Suffix ? macht dies eindeutig, ohne dass Änderungen an der Syntax vorgenommen werden - in einem der Post-Fix-Beispiele. Keine Optimierung erforderlich . Die Probleme bestehen nur darin, dass .member explizit ein Feld ist und dass zuerst eine Umstrukturierung des Umzugs erforderlich ist. Und ich möchte wirklich nicht sagen, dass dies schwer zu schreiben wäre. Ich möchte vor allem sagen, dass dies nicht mit anderen .member -Verwendungen vereinbar zu sein scheint, die zB in Matching umgewandelt werden können. Der ursprüngliche Beitrag wog diesbezüglich positive und negative Aspekte ab.

Bearbeiten: Das Anpassen an future.await(42) birgt das wahrscheinlich unbeabsichtigte zusätzliche Risiko, dass dies a) nicht mit Schließungen vereinbar ist, bei denen dies nicht der Fall ist, da Methoden mit demselben Namen wie das Mitglied zulässig sind; b) Hemmung zukünftiger Entwicklungen, bei denen wir await argumentieren Future optimieren, um einen Abschluss zurückzugeben.

@novacrazy Warum nicht einfach normale Future-Kombinatoren verwenden?

Ich bin nicht sicher, wie viel Erfahrung Sie mit Futures 0.3 haben, aber die allgemeine Erwartung ist, dass Kombinatoren nicht viel verwendet werden und die primäre / idiomatische Verwendung asynchron / warten wird.

Async / await hat gegenüber Kombinatoren mehrere Vorteile, z. B. unterstützt es das Ausleihen über Renditepunkte hinweg.

Kombinatoren existierten lange vor async / await, aber async / await wurde trotzdem erfunden, und das aus gutem Grund!

Async / await ist hier, um zu bleiben, was bedeutet, dass es ergonomisch sein muss (auch mit Methodenketten).

Natürlich können die Benutzer die Kombinatoren auch verwenden, wenn sie dies wünschen, aber sie sollten nicht erforderlich sein, um eine gute Ergonomie zu erzielen.

Wie @cramertj sagte, versuchen wir, die Diskussion auf async / await zu konzentrieren, nicht auf Alternativen zu async / await.

Tatsächlich lassen sich einige Ausdrücke nicht gut in Warteketten übersetzen, wenn Sie bei einem Fehler ein Backup-Verhalten wünschen.

Ihr Beispiel kann erheblich vereinfacht werden:

let value = try {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => await backup_op(),
}.unwrap()

Dies macht deutlich, welche Teile den Fehler behandeln und welche Teile sich auf dem normalen glücklichen Weg befinden.

Dies ist eines der großartigen Dinge bei async / await: Es funktioniert gut mit anderen Teilen der Sprache, einschließlich Schleifen, Zweigen, match , ? , try usw.

Abgesehen von await ist dies derselbe Code, den Sie schreiben würden, wenn Sie keine Futures verwenden würden.

Eine andere Möglichkeit, es zu schreiben, wenn Sie den Kombinator or_else bevorzugen:

let value = await async {
    try {
        let v = await some_op()?;
        let v2 = await v.another_op()?;
        await v2.final_op()?
    }
}.or_else(|_| backup_op());

value.unwrap()

Und das Beste ist, den normalen Code in eine separate Funktion zu verschieben, um den Fehlerbehandlungscode noch deutlicher zu machen:

async fn doit() -> Result<Foo, Bar> {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()
}
let value = await doit().or_else(|_| backup_op());

value.unwrap()

(Dies ist eine Antwort auf @joshtripletts Kommentar ).

Um klar zu sein, Sie müssen nicht klammern, ich habe es erwähnt , weil einige Leute gesagt , dass es zu schwer ist , ohne dass die Klammern zu lesen. Die Klammern sind daher eine optionale Stilwahl (nützlich nur für komplexe Einzeiler).

Alle Syntaxen profitieren in bestimmten Situationen von Klammern. Keine der Syntaxen ist perfekt. Es ist eine Frage, für welche Situationen wir optimieren möchten.

Nachdem Sie Ihren Kommentar erneut gelesen haben, dachten Sie vielleicht, ich würde mich für das Präfix await ? Ich war es nicht, mein Beispiel war die Verwendung von Postfix await . Insgesamt mag ich Postfix await , obwohl ich auch einige der anderen Syntaxen mag.

Ich fange an, mich mit fut.await . Ich denke, die erste Reaktion der Leute wird sein: "Warte, so wartest du? Seltsam." aber später würden sie es für die Bequemlichkeit lieben. Das Gleiche gilt natürlich auch für @await , was viel mehr als .await .

Mit dieser Syntax können wir einige der Lets im Beispiel weglassen:

`.await``@ warte`
let value = try {
    some_op().await?
        .another_op().await?
        .final_op().await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op().await,
}.unwrap()
let value = try {
    some_op()@await?
        .another_op()@await?
        .final_op()@await?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => backup_op()<strong i="21">@await</strong>,
}.unwrap()

Dies macht auch klarer, was mit ? ausgepackt wird, für await some_op()? ist es nicht offensichtlich, ob some_op() ausgepackt wird oder das erwartete Ergebnis.

@ Pauan

Ich versuche hier nicht, den Fokus vom Thema weg zu verlagern, ich möchte darauf hinweisen, dass es in einer Blase nicht existiert. Wir müssen überlegen, wie die Dinge zusammenarbeiten .

Selbst wenn die ideale Syntax gewählt würde, würde ich in einigen Situationen immer noch benutzerdefinierte Futures und Kombinatoren verwenden wollen. Die Idee, dass diese leicht veraltet sein könnten, lässt mich die gesamte Richtung von Rust in Frage stellen.

Die Beispiele, die Sie geben, sehen im Vergleich zu den Kombinatoren immer noch schrecklich aus, und mit dem Generator-Overhead werden sie wahrscheinlich etwas langsamer sein und mehr Maschinencode produzieren.

Was das Warten angeht, ist all dieses Präfix- / Postfix-Siegel- / Schlüsselwort-Bikeshedding wunderbar, aber vielleicht sollten wir pragmatisch sein und die einfachste Option wählen, die Benutzern, die zu Rust kommen, am vertrautesten ist. Dh Präfix-Schlüsselwort

Dieses Jahr wird schneller vergehen als wir denken. Auch der Januar ist meistens vorbei. Wenn sich herausstellt, dass Benutzer mit einem Präfix-Schlüsselwort nicht zufrieden sind, kann es in einer Ausgabe 2019/2020 geändert werden. Wir können sogar einen Witz über „Rückblick ist 2020“ machen.

@novacrazy

Allgemeiner Konsens, den ich gesehen habe, ist, dass die früheste Ausgabe, die wir uns wünschen würden, 2022 ist. Wir wollen definitiv keine weitere Ausgabe planen; Die Ausgabe 2018 war großartig, aber nicht ohne Kosten. (Und einer der Punkte der Ausgabe 2018 ist das Ermöglichen von Async / Warten. Stellen Sie sich vor, Sie nehmen das zurück und sagen: "Nein, Sie müssen jetzt auf die Ausgabe 2020 aktualisieren!")

Auf jeden Fall denke ich nicht, dass ein Präfix-Schlüsselwort -> Postfix-Schlüsselwortübergang in einer Edition möglich ist, selbst wenn dies wünschenswert wäre. Die Regel für Editionen lautet, dass es eine Möglichkeit geben muss, idiomatischen Code in Edition X so zu schreiben, dass er ohne Warnungen kompiliert wird und in Edition X + 1 genauso funktioniert.

Aus demselben Grund möchten wir uns lieber nicht mit einem Keyword-Makro stabilisieren, wenn wir einen Konsens über eine andere Lösung erzielen können. Die bewusste Stabilisierung einer Lösung, von der wir wissen, dass sie unerwünscht ist, ist selbst problematisch.

Ich denke, wir haben gezeigt, dass eine Postfix-Lösung optimaler ist, selbst für Ausdrücke mit nur einem Wartepunkt. Ich bezweifle jedoch, dass eine der vorgeschlagenen Postfix-Lösungen offensichtlich besser ist als alle anderen.

Nur meine zwei Cent (ich bin ein Niemand, aber ich verfolge die Diskussion ziemlich lange). Meine Lieblingslösung wäre die Postfix-Version @await . Vielleicht könnten Sie ein Postfix !await Betracht ziehen, wie eine neue Postfix-Makrosyntax?

Beispiel:

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

Nach einigen Sprachiterationen wäre es fantastisch, unsere eigenen Postfix-Makros implementieren zu können.

... alle neuen Sygils-Vorschläge

Rust ist bereits syntax- / sygillastig, wir haben das Schlüsselwort await reserviert, stuff@await (oder ein anderes Sygil) sieht seltsam / hässlich aus (subjektiv, ich weiß) und ist nur eine Ad-hoc-Syntax integriert sich nicht in irgendetwas anderes in der Sprache, was eine große rote Fahne ist.

Ich habe mir angesehen, wie Go implementiert
... <- ... Vorschlag

@ I60R : Go hat eine schreckliche Syntax voller Ad-hoc-Lösungen und ist im Gegensatz zu Rust absolut unerlässlich. Dieser Vorschlag ist wieder sygil / syntaxlastig und nur für diese spezielle Funktion vollständig ad-hoc.

@ I60R : Go hat eine schreckliche Syntax

Lassen Sie uns hier bitte keine anderen Sprachen verprügeln. "X hat eine schreckliche Syntax" führt nicht zu Erleuchtung und Konsens.

Als Python / JavaScript / Rust-Benutzer und Informatikstudent bevorzuge ich persönlich das Präfix await + f.await() , um beide in der Sprache zu sein.

  1. Sowohl Python als auch JavaScript haben das Präfix await . Ich würde erwarten, dass await am Anfang erscheint. Wenn ich tief in die Zeile lesen muss, um zu erkennen, dass dies asynchroner Code ist, fühle ich mich sehr unwohl. Mit der WASM-Funktion von Rust könnte es viele JS-Entwickler anziehen. Ich glaube, Vertrautheit und Komfort sind wirklich wichtig, wenn man bedenkt, dass Rust bereits viele andere neue Konzepte hat.

  2. Postfix await scheint in Verkettungseinstellungen praktisch zu sein. Ich mag jedoch keine Lösungen wie .await , @await , f await weil sie wie eine Ad-hoc-Lösung für die Syntax von await aussehen, während es sinnvoll ist, an .await() zu denken future .

Rust ist bereits eine Abkehr von Javascript und Warten ist nichts anderes als das Aufrufen einer Funktion (dh die Funktionalität kann nicht über eine Funktion emuliert werden). Die Verwendung von Funktionen zur Bezeichnung des Wartens macht es verwirrend, wenn Anfänger zum asynchronen Warten eingeführt werden. Daher denke ich, dass die Syntax anders sein sollte.

Ich habe mich selbst davon überzeugt, dass .await() wahrscheinlich wesentlich wünschenswerter ist als .await , obwohl der Rest dieses Beitrags diese Position ein wenig absichert.

Der Grund dafür ist, dass await!(fut) _ fut nach Wert_ verbrauchen muss. Es wie einen Feldzugriff aussehen zu lassen, ist _bad_, da dies nicht die Konnotation hat, sich wie ein Präfix-Schlüsselwort zu bewegen, oder das Potenzial, sich wie ein Makro oder ein Methodenaufruf zu bewegen.

Interessanterweise sieht die Syntax der Schlüsselwortmethode fast wie ein implizites Wartedesign aus . Leider ist "Explicit Async, Implicit Await" für Rust nicht möglich (also bitte nicht erneut in diesem Thread prozessieren), da async fn() -> T identisch mit fn() -> Future<T> soll. anstatt ein "implizites Warten" zu aktivieren.

Die Tatsache, dass eine .await() -Syntax wie ein implizites Wartesystem aussieht (wie Kotlin es verwendet), könnte ein Kritiker sein und würde aufgrund der Magie um das Nicht- fast das Gefühl eines "impliziten asynchronen, impliziten Wartens" vermitteln. wirklich-eine-Methode-Aufruf .await() Syntax. Könnten Sie das mit UFCS als await(fut) verwenden? Wäre es Future::await(fut) für UFCS? Jede Syntax, die wie eine andere Dimension der Sprache aussieht, wirft Probleme auf, es sei denn, sie kann zumindest syntaktisch etwas damit vereinheitlicht werden, auch wenn sie nicht funktional ist.

Ich bleibe skeptisch, wenn die Vorteile einer einzelnen Postfix-Lösung die Nachteile derselben Lösung überwiegen, obwohl das Konzept einer Postfix-Lösung im Allgemeinen wünschenswerter ist als ein Präfix.

Ich bin ein bisschen überrascht, dass dieser Thread voller Vorschläge ist, die gemacht zu werden scheinen, weil sie möglich sind - und nicht, weil sie gegenüber den ursprünglichen Vorschlägen keinen signifikanten Nutzen zu bringen scheinen.
Können wir aufhören, über $ , # , @ , ! , ~ usw. zu sprechen, ohne ein signifikantes Argument vorzubringen, was falsch ist? mit await , was ist gut zu verstehen und hat sich in verschiedenen anderen Programmiersprachen bewährt?

Ich denke, der Beitrag von https://github.com/rust-lang/rust/issues/57640#issuecomment -455361619 hat bereits alle guten Optionen aufgelistet.

Von diesen:

  • Obligatorische Trennzeichen Trennzeichen scheinen in Ordnung zu sein, zumindest ist es offensichtlich, welche Priorität sie haben, und wir können anderen Code ohne weitere Klarheit lesen. Und zwei Klammern einzugeben ist gar nicht so schlecht. Vielleicht ist der einzige Nachteil, dass es wie ein Funktionsaufruf aussieht, obwohl es sich um eine andere Kontrollflussoperation handelt.
  • Nützlicher Vorrang könnte die bevorzugte Option sein. Das scheint der Weg zu sein, den die meisten anderen Sprachen gegangen sind, also ist es vertraut und bewährt.
  • Ich persönlich denke, dass das Postfix-Schlüsselwort mit Leerzeichen seltsam aussieht, wenn mehrere in einer einzigen Anweisung warten:
    client.get("url").send() await?.json()? . Das Leerzeichen dazwischen sieht fehl am Platz aus. Mit Klammern wäre es für mich etwas sinnvoller: (client.get("url").send() await)?.json()?
    Aber ich finde den Kontrollfluss immer noch schwieriger zu verfolgen als bei der Präfixvariante.
  • Ich mag kein Postfix-Feld. await führt eine sehr komplexe Operation aus - Rust hat keine berechenbaren Eigenschaften und der Feldzugriff ist ansonsten eine sehr einfache Operation. Es scheint also einen falschen Eindruck über die Komplexität dieser Operation zu vermitteln.
  • Die Postfix-Methode könnte in Ordnung sein. Könnte einige Leute dazu ermutigen, sehr lange Aussagen mit mehreren Wartezeiten zu schreiben, die die Ertragspunkte möglicherweise mehr verbergen. Es lässt die Dinge auch wieder wie einen Methodenaufruf aussehen, obwohl es etwas anderes ist.

Aus diesen Gründen bevorzuge ich den "nützlichen Vorrang", gefolgt von "obligatorischen Trennzeichen".

Go hat eine schreckliche Syntax voller Ad-hoc-Lösungen und ist im Gegensatz zu Rust absolut unerlässlich. Dieser Vorschlag ist wieder sygil / syntaxlastig und nur für diese spezielle Funktion vollständig ad-hoc.

@dpc , wenn Sie den Vorschlag <-- vollständig lesen, werden Sie feststellen , dass diese Syntax nur von Go inspiriert ist, jedoch ziemlich unterschiedlich ist und sowohl im imperativen als auch im Funktionsverkettungskontext verwendet werden kann. Ich verstehe auch nicht, dass die Syntax await keine Ad-hoc-Lösung ist. Für mich ist sie viel spezifischer und viel ungeschickter als <-- . Es ist ähnlich, deref reference / reference.deref / etc anstelle von *reference haben oder try result / result.try / etc anstelle von result? . Ich sehe entweder keinen Vorteil der Verwendung des Schlüsselworts await außer der Vertrautheit mit JS / Python / etc, die ohnehin weniger wichtig sein sollte als eine konsistente und zusammensetzbare Syntax. Und ich sehe keinen Nachteil darin, <-- Sigil zu haben, außer dass es kein await was sowieso nicht so einfach ist wie einfaches Englisch und Benutzer sollten verstehen, was es zuerst tut.

Bearbeiten: Dies könnte auch eine gute Antwort auf den Beitrag von @ Matthias247 sein , da er einige Argumente gegen await und eine mögliche Alternative vorschlägt, die nicht von denselben Problemen betroffen ist


Es ist wirklich interessant für mich, Kritik gegen <-- Syntax zu lesen, frei von Argumenten, die historische und voreingenommene Gründe ansprechen.

Lassen Sie uns die tatsächlichen Besonderheiten in Bezug auf die Priorität erwähnen:

Das heutige Prioritätsdiagramm:

Operator / Ausdruck | Assoziativität
- | - -
Pfade |
Methodenaufrufe |
Feldausdrücke | links nach rechts
Funktionsaufrufe, Array-Indizierung |
? |
Unary - * ! & &mut |
as | links nach rechts
* / % | links nach rechts
+ - | links nach rechts
<< >> | links nach rechts
& | links nach rechts
^ | links nach rechts
\| | links nach rechts
== != < > <= >= | Klammern erforderlich
&& | links nach rechts
\|\| | links nach rechts
.. ..= | Klammern erforderlich
= += -= *= /= %= &= \|= ^= <<= >>= | rechts nach links
return break Schließungen |

Nützliche Priorität setzt await vor ? so dass es enger als ? bindet. Ein ? in der Kette bindet also ein Warten auf alles davor.

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

Ja, mit nützlicher Priorität, das "funktioniert einfach". Wissen Sie auf einen Blick, was das bedeutet? Ist das (wahrscheinlich) ein schlechter Stil? Wenn ja, kann rustfmt das automatisch beheben?

Offensichtliche Priorität setzt await _somewhere_ unter ? . Ich bin mir nicht sicher, wo genau, obwohl diese Details wahrscheinlich nicht allzu wichtig sind.

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

Wissen Sie auf einen Blick, was das bedeutet? Kann rustfmt es in einen nützlichen Stil bringen, der nicht automatisch vertikalen und horizontalen Raum verschwendet?


Wohin würden Postfix-Keywords fallen? Wahrscheinlich Keyword-Methode mit Methodenaufrufen und Keyword-Feld mit Feldausdrücken, aber ich bin mir nicht sicher, wie die anderen binden sollen. Welche Optionen führen zu den am wenigsten möglichen Konfigurationen, bei denen await ein überraschendes "Argument" erhält?

Für diesen Vergleich vermute ich, dass "obligatorische Trennzeichen" (was ich in der Zusammenfassung als Keyword-Funktion bezeichnet habe) leicht gewinnen, da dies einem normalen Funktionsaufruf entsprechen würde.

@ CAD97 Um klar zu sein, denken Sie daran, dass .json() auch eine Zukunft ist (zumindest in reqwests ).

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

Je mehr ich mit der Konvertierung komplexer Rostausdrücke spiele (selbst solche, bei denen nur 1 Wartezeit erforderlich ist, beachten Sie jedoch, dass in meiner über 20.000 zukünftigen Codebasis fast jeder einzelne asynchrone Ausdruck ein Wartezeitpunkt ist, direkt gefolgt von einem weiteren Wartezeitpunkt), desto weniger mag ich Präfix für Rust.

Dies ist _all_ wegen des Operators ? . Keine andere Sprache hat einen Postfix-Kontrollflussoperator _und_ await , die im Wesentlichen immer in echtem Code gepaart sind.


Ich bevorzuge immer noch das Postfix-Feld . Als Postfix-Steuerungsoperator ist meiner Meinung nach die enge visuelle Gruppierung erforderlich, die future.await über future await bietet. Und im Vergleich zu .await()? bevorzuge ich, wie .await? seltsam aussieht, damit es nicht bemerkt wird und Benutzer nicht davon ausgehen, dass es sich um eine einfache Funktion handelt (und daher nicht fragen, warum UFCS nicht funktioniert). .


Als ein weiterer Datenpunkt zugunsten von Postfix wäre, wenn dies stabilisiert ist, ein rustfix von await!(...) zu dem, was wir entscheiden, sehr dankbar. Ich sehe nicht, wie etwas anderes als die Postfix-Syntax eindeutig übersetzt werden könnte, ohne unnötig Dinge in ( ... ) zu verpacken.

Ich denke, zuerst sollten wir die Frage beantworten: "Wollen wir die Verwendung von await in verketteten Kontexten fördern?". Ich glaube, die vorherrschende Antwort ist "Ja", daher wird es ein starkes Argument für Postfix-Varianten. Während await!(..) am einfachsten hinzuzufügen ist, sollten wir die try!(..) -Geschichte meiner Meinung nach nicht wiederholen. Ich persönlich bin auch nicht mit dem Argument einverstanden, dass "Verketten potenziell kostspielige Operationen verbirgt". Wir haben bereits viele Verkettungsmethoden, die sehr schwer sein können, so dass Verketten keine Faulheit mit sich bringt.

Während das Präfix await Schlüsselwort für Benutzer aus anderen Sprachen am bekanntesten ist, sollten wir unsere Entscheidung nicht auf dieser Grundlage treffen und uns stattdessen auf längerfristige, dh Benutzerfreundlichkeit, Benutzerfreundlichkeit und Lesbarkeit konzentrieren . @withoutboats sprach über "Vertrautheitsbudget", aber ich bin der festen Überzeugung, dass wir nicht nur aus Gründen der Vertrautheit suboptimale Lösungen einführen sollten.

Jetzt wollen wir wahrscheinlich nicht zwei Möglichkeiten, dasselbe zu tun, also sollten wir nicht sowohl Postfix- als auch Präfixvarianten einführen. Nehmen wir also an, wir haben unsere Optionen auf Postfix-Varianten beschränkt.

Beginnen wir zuerst mit fut await . Ich mag diese Variante überhaupt nicht, da sie ernsthaft die Art und Weise beeinträchtigt, wie Menschen Code analysieren, und beim Lesen von Code ständig Verwirrung stiftet. (Vergessen Sie nicht, dass der Code hauptsächlich zum Lesen dient.)

Weiter fut.await , fut.await() und fut.await!() . Ich denke, die konsistenteste und weniger verwirrende Variante wird das Postfix-Makro sein. Ich denke nicht, dass es sich lohnt, eine neue Entität "Schlüsselwortfunktion" oder "Schlüsselwortmethode" einzuführen, um nur ein paar Zeichen zu speichern.

Zuletzt Siegel-basierte Varianten: fut@await und fut@ . Ich mag die fut@await -Variante nicht. Wenn wir das Siegel einführen, warum sollten wir uns dann mit dem await -Teil beschäftigen? Haben wir Pläne für zukünftige Erweiterungen fut@something ? Wenn nicht, fühlt es sich einfach überflüssig an. Ich mag die Variante fut@ , sie löst die Vorrangprobleme, Code wird leicht zu verstehen, zu schreiben und zu lesen. Sichtbarkeitsprobleme können durch Hervorheben von Code gelöst werden. Es wird nicht schwer sein, diese Funktion als "@ for await" zu erklären. Der größte Nachteil ist natürlich, dass wir für dieses Feature aus dem sehr begrenzten "Siegelbudget" bezahlen, aber angesichts der Bedeutung des Features und der Häufigkeit, mit der es in asynchronen Codebasen verwendet wird, glaube ich, dass es sich auf lange Sicht lohnen wird . Und natürlich können wir bestimmte Parallelen zu ? . (Obwohl wir auf Perl-Witze von Rust-Kritikern vorbereitet sein müssen)

Fazit: Meiner Meinung nach sollten wir, wenn wir bereit sind, das "Siegelbudget" zu belasten, mit fut@ , und wenn nicht mit fut.await!() .

Wenn wir über Vertrautheit sprechen, denke ich nicht, dass wir uns zu sehr um Vertrautheit mit JS / Python / C # kümmern sollten, da Rust sich in einer anderen Nische befindet und in vielen Dingen bereits anders aussieht. Die Bereitstellung einer Syntax, die diesen Sprachen ähnlich ist, ist ein kurzfristiges Ziel mit geringer Belohnung. Niemand wird Rust nur für die Verwendung eines vertrauten Schlüsselworts auswählen, wenn es unter der Haube völlig anders funktioniert.

Aber Vertrautheit mit Go ist wichtig, da Rust sich in einer ähnlichen Nische befindet und selbst nach Philosophie Go näher steht als anderen Sprachen. Und trotz aller Vorurteile ist eine der Stärken in beiden, dass sie Features nicht blind kopieren, sondern Lösungen implementieren, für die es wirklich Grund gibt.

IMO, in diesem Sinne ist die Syntax <-- hier am stärksten

Denken Sie bei all dem daran, dass Rust-Ausdrücke zu mehreren verketteten Methoden führen können. Die meisten Sprachen neigen dazu, dies nicht zu tun.

Ich möchte an die Erfahrung des C # dev-Teams erinnern:

Die Hauptüberlegung gegen die C # -Syntax ist, dass der Vorrang des Operators auf foo wartet.

Ich habe das Gefühl, dass ich dies kommentieren kann. Wir haben viel über Vorrang mit "Warten" nachgedacht und viele Formulare ausprobiert, bevor wir das gewünschte Formular festgelegt haben. Eines der wichtigsten Dinge, die wir gefunden haben, war, dass es für uns und die Kunden (intern und extern), die diese Funktion nutzen wollten, selten der Fall war, dass die Leute wirklich etwas über ihren asynchronen Anruf hinaus "verketten" wollten.

Die Tendenz der Menschen, mit dem „Warten“ in einem Ausdruck „fortzufahren“, war selten. Wir sehen gelegentlich Dinge wie (warte auf Ausdruck) .M (), aber diese scheinen weniger verbreitet und weniger wünschenswert zu sein als die Anzahl der Leute, die auf Ausdruck warten. M ().

und

Dies ist auch der Grund, warum wir kein "implizites" Formular für "Warten" gewählt haben. In der Praxis war es etwas, worüber die Leute sehr klar nachdenken wollten und was sie in ihrem Code im Vordergrund haben wollten, damit sie darauf achten konnten. Interessanterweise ist diese Tendenz auch Jahre später geblieben. dh manchmal bedauern wir viele Jahre später, dass etwas übermäßig ausführlich ist. Einige Funktionen sind auf diese Weise schon früh gut, aber sobald die Leute damit vertraut sind, sind sie besser für etwas Terseres geeignet. Dies war bei "Warten"

Ist ein guter Punkt gegen Siegel statt dediziertes (Schlüssel-) Wort.

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

Sie sollten den Jungs mit Millionen von Benutzern wirklich zuhören.

Sie möchten also nichts verketten, Sie möchten nur mehrere await , und meine Erfahrung ist dieselbe. Ich habe mehr als 6 Jahre lang async/await Code geschrieben, und ich wollte nie eine solche Funktion. Die Postfix-Syntax sieht wirklich fremd aus und wird als Lösung für eine Situation angesehen, die wahrscheinlich nie eintreten wird. Async Anruf ist wirklich eine kühne Sache, so dass mehrere Wartezeiten in einer Zeile zu schwer sind.

Die Tendenz der Menschen, mit dem „Warten“ in einem Ausdruck „fortzufahren“, war selten. Wir sehen gelegentlich Dinge wie (warte auf Ausdruck) .M (), aber diese scheinen weniger verbreitet und weniger wünschenswert zu sein als die Anzahl der Leute, die auf Ausdruck warten. M ().

Das scheint eine a-posteriori-Analyse zu sein. Vielleicht ist einer der Gründe, warum sie nicht fortfahren, dass es äußerst umständlich ist, dies in der Präfixsyntax zu tun (vergleichbar damit, dass Sie in einer Anweisung nicht mehrmals try! möchten, da dies über den Operator ? lesbar bleibt

@ I60R ,

  1. Ich denke, Vertrautheit ist wichtig. Rust ist eine relativ neue Sprache und die Menschen werden aus anderen Sprachen migrieren. Wenn Rust vertraut aussieht, fällt es ihnen leichter, eine Entscheidung für den Rust zu treffen.
  2. Ich habe keinen großen Spaß an Verkettungsmethoden - es ist viel schwieriger, lange Ketten zu debuggen, und die Verkettung erschwert nur die Lesbarkeit des Codes und ist möglicherweise nur als zusätzliche Option zulässig (z. B. als Makros .await!() ). Das Präfixformular zwingt Entwickler, Code in Methoden zu extrahieren, anstatt sie zu verketten:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

in so etwas:

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

Das scheint eine a-posteriori-Analyse zu sein. Vielleicht ist einer der Gründe, warum sie nicht fortfahren, dass es äußerst umständlich ist, dies in der Präfixsyntax zu tun. Das Obige berücksichtigt nur die Vorrangstellung, nicht die Position. Und ich möchte Sie daran erinnern, dass C # nicht Rust ist und dass Mitglieder von Merkmalen den Wunsch, Methoden für Ergebnisse aufzurufen, möglicherweise erheblich ändern.

Nein, es geht um interne C # -Team-Experimente, bei denen beide Präfix- / Postfix- / implizite Formen vorhanden waren. Und ich spreche von meiner Erfahrung, die nicht nur eine Gewohnheit ist, bei der ich keine Profis für Postfix-Formulare sehen kann.

@mehcode Dein Beispiel motiviert mich nicht. reqwest entscheidet sich bewusst dafür, dass der anfängliche Anforderungs- / Antwortzyklus und die anschließende Behandlung der Antwort (Körperstrom) separate gleichzeitige Prozesse sind. Daher sollten sie zweimal abgewartet werden, wie @andreytkachenko zeigt.

reqwest könnte die folgenden APIs vollständig verfügbar machen:

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

oder

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

(Letzteres ist ein einfacher Zucker über and_then ).

Ich finde es beunruhigend, dass viele der Postfix-Beispiele hier diese Kette als Beispiel verwenden, da die beste API-Entscheidung von reqwest s und hyper besteht, diese Dinge getrennt zu halten.

Ich bin der festen Überzeugung, dass die meisten Beispiele für Postfix-Wartezeiten hier mit Kombinatoren (und ggf. Zucker) umgeschrieben oder ähnliche Vorgänge beibehalten und mehrmals abgewartet werden sollten.

@andreytkachenko

Das Präfixformular zwingt Entwickler, Code in Methoden zu extrahieren, anstatt sie zu verketten:

Ist das eine gute oder eine schlechte Sache? In Fällen, in denen es N Methoden gibt, die jeweils zu M Folgemaßnahmen führen können, sollte der Entwickler N * M Methoden bereitstellen? Ich persönlich mag zusammensetzbare Lösungen, auch wenn sie etwas länger sind.

in so etwas:

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

@ Pixel

Ich glaube nicht, dass Sie in C # so viel verketteten / funktionalen Code erhalten würden wie in Rust. Oder liege ich falsch? Das macht Erfahrungen von C # -Entwicklern / Benutzern interessant, aber nicht unbedingt für Rust anwendbar. Ich wünschte, wir könnten uns von Ocaml oder Haskell abheben.

Ich denke, das ist die Hauptursache für die Meinungsverschiedenheit, dass einige von uns einen imperativen und andere einen funktionalen Stil genießen. Und das ist es. Rust unterstützt beide, und beide Seiten der Debatte möchten, dass Async gut in die Art und Weise passt, wie sie normalerweise Code schreiben.

@dpc

Ich glaube nicht, dass Sie in C # so viel verketteten / funktionalen Code erhalten würden wie in Rust. Oder liege ich falsch? Das macht Erfahrungen von C # -Entwicklern / Benutzern interessant, aber nicht unbedingt für Rust anwendbar. Ich wünschte, wir könnten uns von Ocaml oder Haskell abheben.

LINQ und funktionaler Stil sind in C # sehr beliebt .

@dpc ,
Wenn es um Lesbarkeit geht - ich denke, dass so viel Code explizit ist - wie besser, und meine Erfahrung zeigt, dass es nicht notwendig ist, die Folgemaßnahmen durchzuführen, wenn Methoden- / Funktionsnamen gut selbstbeschreibend sind.

Wenn Sie sich mit Overhead beschäftigen - der Rust-Compiler ist ziemlich schlau, sie zu integrieren (trotzdem haben wir immer #[inline] ).

@dpc

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

Mein Gefühl ist, dass dies im Grunde das Problem der Futures-API wiederholt: Es erleichtert die Verkettung, aber der Typ dieser Kette wird viel schwieriger zu durchdringen und zu debuggen.

Gilt das nicht auch für das? Betreiber aber? Dies ermöglicht eine stärkere Verkettung und erschwert somit das Debuggen. Aber warum haben wir uns entschieden? über versuchen! dann? Und warum bevorzugt Rust APIs mit Builder-Muster? Daher hat Rust bereits eine Reihe von Entscheidungen getroffen, die die Verkettung in APIs begünstigen. Und ja, das kann das Debuggen erschweren, aber sollte das nicht auf Flusenebene passieren - vielleicht eine neue Flusen für Clippy für Ketten, die zu groß sind? Ich muss noch genügend Motivation sehen, wie unterschiedlich das Warten hier ist.

Ist das eine gute oder eine schlechte Sache? In Fällen, in denen es N Methoden gibt, die jeweils zu M Folgemaßnahmen führen können, sollte der Entwickler N * M Methoden bereitstellen? Ich persönlich mag zusammensetzbare Lösungen, auch wenn sie etwas länger sind.

N und M sind nicht unbedingt groß, und auch möglicherweise sind nicht alle von Interesse / nützlich zum Extrahieren.

@andreytkachenko ,

  1. Ich stimme nicht zu, Vertrautheit wird hier überbewertet. Bei der Migration aus einer anderen Sprache würden die ersten Benutzer die Möglichkeit suchen, Programme im Stil von async-await auszuführen, jedoch nicht für genau dieselbe Syntax. Wenn es anders implementiert würde, dies aber ein besseres Programmiererlebnis bieten würde, wäre dies ein weiterer Vorteil.

  2. Die Unfähigkeit, lange Ketten zu debuggen, ist eine Einschränkung von Debuggern, die keinen Codestil haben. Über die Lesbarkeit denke ich, dass es davon abhängt, und die Durchsetzung des imperativen Stils ist hier unnötig einschränkend. Wenn async Funktionsaufrufe immer noch Funktionsaufrufe sind, ist es überraschend, die Verkettung nicht zu unterstützen. Außerdem ist <-- ein Präfixoperator, der, wie Sie sagten, in Funktionsketten als zusätzliche Option verwendet werden kann

@skade Einverstanden.

Alle Beispiele mit langen Warteketten sind ein Codegeruch. Sie könnten mit Kombinatoren oder aktualisierten APIs weitaus eleganter gelöst werden, als diese Generator-Spaghetti-Zustandsmaschinen unter der Haube zu erstellen. Extreme Shorthand-Syntax ist ein Rezept für ein noch schwierigeres Debugging als aktuelle Futures.

Ich bin immer noch ein Fan von beide Präfix Schlüsselwort await und Makro await!(...) / .await!() mit, in Kombination async und #[async] Generator - Funktionen , wie in meinem Kommentar hier beschrieben .

Wenn alles richtig läuft, könnte wahrscheinlich ein einzelnes Makro für await! , um sowohl Präfix als auch Suffix zu verarbeiten, und das Schlüsselwort await wäre nur ein Schlüsselwort innerhalb von async -Funktionen.

@skade ,
Für mich ist ein großer Nachteil der Verwendung von Kombinatoren für future dass sie async Funktionsaufrufe verbergen und es unmöglich wäre, sie von regulären Funktionen zu unterscheiden. Ich möchte alle suspendierbaren Punkte sehen, da es sehr wahrscheinlich ist, dass sie in Ketten im Builder-Stil verwendet werden.

@ I60R Das Debuggen langer Aufrufketten ist kein Debugger-Problem, da das Problem beim Schreiben dieser Ketten darin besteht, dass die Typinferenz richtig ist. Angesichts der Tatsache, dass viele dieser Methoden generische Parameter verwenden und generische Parameter verteilen, die möglicherweise an einen Abschluss gebunden sind, ist dies ein ernstes Problem.

Ich bin mir nicht sicher, ob es ein Ziel der Funktion ist, alle Aufhängepunkte sichtbar zu machen. Dies liegt bei den Implementierern zu entscheiden. Und es ist mit allen vorgeschlagenen Versionen der await -Syntax durchaus möglich.

@novacrazy guter Punkt, jetzt, wo Sie es erwähnen, als ehemaliger Javascripter verwende ich

Ich denke, es würde ungefähr so ​​aussehen

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

Was ich nicht einmal weiß, wenn es möglich ist, muss ich in Rust nach Futures suchen

@richardanaya das ist

Wie die Box impliziert:

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

Wir können ein neues Schlüsselwort await eingeben und Await wie folgt kennzeichnen:

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

Und verwenden Sie await als Methode und auch als Schlüsselwort:

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

@richardanaya Einverstanden. Ich war einer der ersten Entwickler, die vor vielen Jahren Async / Warten auf meine Webdev-Inhalte eingeführt haben, und die wahre Kraft bestand darin, Async / Warten mit den vorhandenen Versprechen / Zukünften zu kombinieren . Auch Ihr Beispiel könnte wahrscheinlich vereinfacht werden als:

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

let value = result.unwrap();

Wenn Sie Futures oder Ergebnisse von Futures oder Futures von Ergebnissen verschachtelt haben, kann der Kombinator .flatten() drastisch vereinfachen. Es wäre eine schlechte Form, jeden einzelnen manuell auszupacken und abzuwarten.

@XX Das ist redundant und ungültig. await existiert nur in async -Funktionen und kann nur mit Typen arbeiten, die Future / IntoFuture implementieren, sodass keine neuen erforderlich sind Merkmal.

@novacrazy , @richardanaya

Sie könnten mit Kombinatoren oder aktualisierten APIs weitaus eleganter gelöst werden, als diese Generator-Spaghetti-Zustandsmaschinen unter der Haube zu erstellen. Extreme Shorthand-Syntax ist ein Rezept für ein noch schwierigeres Debugging als aktuelle Futures.

@novacrazy guter Punkt, jetzt, wo Sie es erwähnen, als ehemaliger Javascripter verwende ich

Kombinatoren haben in Rusts async / await völlig unterschiedliche Eigenschaften und Kräfte, z. B. in Bezug auf die Ausleihe über Fließgrenzen hinweg. Sie können Kombinatoren nicht sicher schreiben, die dieselben Fähigkeiten wie asynchrone Blöcke haben. Lassen wir die Kombinator-Diskussion aus diesem Thread heraus, da sie nicht hilfreich ist.

@ I60R

Für mich ist ein großer Nachteil der zukünftigen Verwendung von Kombinatoren, dass sie asynchrone Funktionsaufrufe verbergen und es unmöglich wäre, sie von regulären Funktionen zu unterscheiden. Ich möchte alle suspendierbaren Punkte sehen, da es sehr wahrscheinlich ist, dass sie in Ketten im Builder-Stil verwendet werden.

Sie können Suspend-Punkte nicht ausblenden. Das einzige, was Kombinatoren erlauben, ist die Schaffung anderer Zukünfte. Irgendwann müssen diese abgewartet werden.

Beachten Sie, dass C # nicht das Problem hat, dass der meiste Code, der ein Präfix await , ihn mit dem (bereits vorhandenen) Postfix ? -Operator kombiniert. Insbesondere Fragen nach dem Vorrang und der allgemeinen Unbeholfenheit, zwei ähnliche "Dekorateure" zu haben, erscheinen auf gegenüberliegenden Seiten eines Ausdrucks.

@ Matthias247 Wenn Sie Daten ausleihen müssen, können Sie auch mehrere await -Anweisungen verwenden. Oft müssen Sie jedoch nur Daten verschieben, und Kombinatoren sind dafür vollkommen gültig und haben das Potenzial, zu effizienterem Code zu kompilieren. Manchmal komplett weg optimiert.

Wieder besteht die wahre Kraft darin , Dinge miteinander zu wartbaren, performanten und lesbaren Code zu erstellen . In 80% der Fälle bezweifle ich, dass ich sogar async / await berühren werde, unabhängig von der Syntax, nur um die stabilsten und leistungsfähigsten APIs mit einfachen Futures bereitzustellen.

In dieser Hinsicht sind das Präfix-Schlüsselwort await und / oder das gemischte Makro await!(...) / .await!() die am besten lesbaren, vertrauten und am einfachsten zu debuggenden Optionen.

@andreytkachenko

Ich habe Ihr Posting gesehen und bin völlig einverstanden, da ich jetzt über "Vertrautheit" nachdenke, denke ich, dass es zwei Arten von Vertrautheit gibt:

  1. Die Syntax "Warten" sieht aus wie ähnliche Sprachen, die viel Warten verwenden. Javascript ist hier ziemlich groß und für unsere WASM-Fähigkeiten als Community sehr relevant.

  2. Die zweite Art der Vertrautheit besteht darin, Code Entwicklern bekannt zu machen, die immer nur mit synchronem Code arbeiten. Ich denke, der größte Aspekt des Wartens ist, den asynchronen Code LOOK synchron zu machen. Dies ist tatsächlich eine Sache, die Javascript wirklich falsch macht, ist, dass es lange dann Ketten normalisiert ist, die für hauptsächlich synchrone Entwickler völlig fremd aussehen.

Eine Sache, die ich aus meinen asynchronen Javascript-Tagen anbieten würde, war für mich als Entwickler im Nachhinein weitaus nützlicher, die Fähigkeit, Versprechen / Zukunftsgruppen zusammenzufassen. Promise.all (p1 (), p2 ()), damit ich die Arbeit leicht paralleln kann. Die Verkettung von then () war immer nur ein Echo der versprochenen Vergangenheit von Javascript, aber jetzt, wo ich darüber nachdenke, ziemlich archaisch und unnötig.

Ich würde vielleicht diese Idee des Wartens anbieten. "Versuchen Sie, die Unterschiede zwischen Async-Code und Sync-Code so gering wie möglich zu halten."

@novacrazy Die asynchrone Funktion gibt einen impliziten Future -Typ zurück, oder? Was hindert uns daran, die Methode await zu Future hinzuzufügen? So :

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

@XX Soweit ich weiß, werden async -Funktionen in Rust mithilfe von Generatoren in Zustandsmaschinen umgewandelt. Dieser Artikel ist eine gute Erklärung. await benötigt also eine async -Funktion, damit der Compiler beide korrekt transformieren kann. await kann ohne den Teil async nicht funktionieren.

Future hat eine wait -Methode, die der von Ihnen vorgeschlagenen ähnelt, jedoch den aktuellen Thread blockiert.

@skade ,

Aber wie könnte sich der Codestil auf die Typinferenz auswirken? Ich kann keinen Unterschied in demselben Code erkennen, der in verkettetem und imperativem Stil geschrieben ist. Typen sollten genau gleich sein. Wenn der Debugger sie nicht herausfinden kann, ist dies definitiv ein Problem im Debugger, nicht im Code.

@skade , @ Matthias247 , @XX

Bei Kombinatoren haben Sie genau einen Aufhängepunkt am Anfang der Funktionskette. Alle anderen wären im Inneren implizit. Das ist genau das gleiche Problem wie bei impliziten mut , was für mich persönlich einer der größten Verwirrungspunkte in Rust war. Einige APIs würden Futures zurückgeben, während andere Ergebnisse von Futures zurückgeben würden - das möchte ich nicht. Aufhängepunkte sollten nach Möglichkeit explizit sein, und eine ordnungsgemäß zusammensetzbare Syntax würde dies fördern

@ I60R let -Bindungen sind Knotenpunkte für die Typinferenz und helfen bei der Fehlerberichterstattung.

@novacrazy

Warten auf braucht also eine asynchrone Funktion, um darin zu arbeiten

Kann dies in der Rückgabeart ausgedrückt werden? Beispiel: Der Rückgabetyp ist impl Future + Async anstelle von impl Future .

@skade Ich habe immer gedacht, dass . in der Methodenaufrufsyntax genau den gleichen Zweck erfüllt

@dpc

Ich glaube nicht, dass Sie in C # so viel verketteten / funktionalen Code erhalten würden wie in Rust. Oder liege ich falsch? Das macht Erfahrungen von C # -Entwicklern / Benutzern interessant, aber nicht unbedingt für Rust anwendbar. Ich wünschte, wir könnten uns von Ocaml oder Haskell abheben.

Sie bekommen so viel wie Rust. Schauen Sie sich einen LINQ-Code an und Sie werden ihn sehen.

Typischer asynchroner Code sieht folgendermaßen aus:

async Task<List<IGroping<int, PageMetadata>>> GetPageMetadata(string url, DbSet<Page> pages)
{
    using(var client = new HttpClient())
    using(var r = await client.GetAsync(new Uri(url)))
    {
        var content = await r.Content.ReadAsStringAsync();
                return await pages
                   .Where(x => x.Content == content)
                   .Select(x => x.Metadata)
                   .GroupBy(x => x.Id)
                   .ToListAsync();
    }
}

Oder allgemeiner

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

Sie verketten keine Aufrufe, Sie weisen sie einer Variablen zu und verwenden sie dann irgendwie. Dies gilt insbesondere dann, wenn Sie sich mit dem Ausleihscheck befassen.


Ich kann zustimmen, dass Sie Futures in einer einzigen Situation verketten können. Wenn Sie einen Fehler behandeln möchten, der während des Anrufs auftreten kann. zB let a = await!(service_a)? . Dies ist die einzige Situation, in der eine Postfix-Alternative besser ist. Ich konnte sehen, dass es hier von Vorteil ist, aber ich denke nicht, dass es alle Nachteile überwiegt.

Ein weiterer Grund: Was ist mit impl Add for MyFuture { ... } und let a = await a + b; ?

Ich möchte an Generalized Type Ascription RFC im Zusammenhang mit dem Schlüsselwort postfix await erinnern. Es würde den folgenden Code erlauben:

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

Sehr ähnlich wie das Postfix Wait Keyword:

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

Mit den gleichen Nachteilen bei schlechter Formatierung:

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

Ich denke, wenn wir die Pille von Type Ascription RFC schlucken, sollten wir aus Gründen der Konsistenz fut await schlucken.

Übrigens, wussten Sie, dass dies eine gültige Syntax ist:

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

Dennoch habe ich im realen Code keine Vorkommen davon gesehen.

Bitte erlauben Sie mir, ein wenig abzuschweifen. Ich denke, wir sollten Postfix-Makros geben, wie von @BenoitZugmeyer vorgeschlagen , ein weiterer Gedanke. Diese können entweder als expr!macro oder als expr@macro oder vielleicht sogar als expr.macro!() . Ich denke, die erste Option wäre vorzuziehen. Ich bin mir nicht sicher, ob sie eine gute Idee sind, aber wenn wir ein allgemeines Konzept und keine Ad-hoc-Lösung extrahieren möchten, während noch ein Postfix erwartet wird , sollten wir zumindest Postfix-Makros als mögliche Lösung in Betracht ziehen.

Denken Sie daran, dass selbst wenn wir await einem Postfix-Makro machen würden, es immer noch magisch wäre (wie compile_error! ). @Jplatte und andere haben jedoch bereits festgestellt, dass dies kein Problem ist.

Wie ich vorgehen würde, wenn wir diesen Weg gehen würden, würde ich zuerst genau festlegen, wie Postfix-Makros funktionieren würden, dann nur await als magisches Postfix-Makro zulassen und später eigene definierte Postfix-Makros zulassen.

Lexing

Beim Lexen / Parsen kann dies ein Problem sein. Wenn wir uns expr!macro ansehen, könnte der Compiler denken, dass es ein Makro namens expr! und danach gibt es einige ungültige Buchstaben macro . Es sollte jedoch möglich sein, expr!macro durch einen Lookahead von einem zu lexen, und etwas wird zu einem Postfix-Makro, wenn ein expr gefolgt von einem ! , direkt gefolgt von identifier . Ich bin kein Sprachentwickler und ich bin mir nicht sicher, ob dies das Lexing zu komplex macht. Ich gehe einfach davon aus, dass Postfix-Makros die Form von expr!macro .

Wären Postfix-Makros nützlich?

Auf den ersten Blick habe ich mir diese anderen Anwendungsfälle für Postfix-Makros ausgedacht. Ich denke jedoch nicht, dass alle von benutzerdefinierten Makros implementiert werden können, daher bin ich mir nicht sicher, ob diese Liste sehr hilfreich ist.

  • stream!await_all : zum Warten auf Streams
  • option!or_continue : Wenn option None ist, setzen Sie die Schleife fort
  • monad!bind : für =<< ohne es an einen Namen zu binden

stream!await_all

Dies ermöglicht es uns, nicht nur auf Futures, sondern auch auf Streams zu warten.

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

wäre äquivalent zu (in einem async -stream-esque-Block):

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

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

Auf diese Weise können wir eine Option auspacken. Wenn es sich um None , setzen Sie die Schleife fort.

loop {
    let event = match engine.event() {
        Some(event) => event,
        None => continue,
    }
    let button = match event.button() {
        Some(button) => button,
        None => continue,
    }
    handle_button_pressed(button);
}

wäre gleichbedeutend mit:

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

monad!bind

Dieser würde es uns ermöglichen, Monaden auf eine ziemlich rustikale (dh ausdruckszentrierte) Weise zu bekommen. Rust hat noch nichts wie Monaden und ich bin nicht davon überzeugt, dass sie zu Rust hinzugefügt werden. Wenn sie jedoch hinzugefügt werden, kann diese Syntax hilfreich sein.

Ich habe folgendes von hier genommen .

nameDo :: IO ()
nameDo = do putStr "What is your first name? "
            first <- getLine
            putStr "And your last name? "
            last <- getLine
            let full = first ++ " " ++ last
            putStrLn ("Pleased to meet you, " ++ full ++ "!")

wäre gleichbedeutend mit:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    let last = getLine()!bind;
    let full = first + " " + &last
    putStrLn("Pleased to meet you, " + &full + "!")!bind;
}

Oder mehr inline mit weniger let s:

do {
    putStr("What is your first name? ")!bind;
    let first = getLine()!bind;
    putStr("And your last name? ")!bind;
    putStrLn(
        "Pleased to meet you, " + &first + " " + &getLine()!bind + "!"
    )!bind;
}

Auswertung

Da bin ich sehr gespalten. Der mathematische Teil meines Gehirns möchte eine verallgemeinerte Lösung für await , und Postfix-Makros könnten eine Möglichkeit sein, diese zu erreichen. Der pragmatische Teil meines Gehirns denkt: Warum sollte man sich die Mühe machen, eine Sprache zu erstellen, deren Makrosystem bereits niemand noch komplizierter versteht?

Von ? und await haben wir jedoch zwei Beispiele für super nützliche Postfix-Operatoren. Was ist, wenn wir andere finden, die wir in Zukunft hinzufügen möchten, vielleicht ähnlich denen, die ich erwähnt habe? Wenn wir sie verallgemeinert haben, können wir sie natürlich zu Rust hinzufügen. Wenn wir das nicht getan hätten, müssten wir uns jedes Mal eine weitere Syntax einfallen lassen, die die Sprache wahrscheinlich mehr aufblähen würde als die Postfix-Makros.

Was denkt ihr?

@EyeOfPython

Ich bin auf eine sehr ähnliche Idee gekommen und bin mir sicher, dass Postfix-Makros in Rust eine Frage der Zeit sind.
Ich denke jedes Mal darüber nach, wenn ich mich mit ndarray Slicing beschäftige:

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

aber viel besser wäre

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

und viele Kombinatoren können verschwunden sein, wie solche mit _with oder _else Postfixes:

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

@EyeOfPython @andreytkachenko Postfix-Makros sind derzeit keine Funktion in Rust und IMHO würde eine vollständige RFC + FCP +

Dies ist keine RFC-Diskussion, sondern eine Diskussion über einen akzeptierten RFC, der implementiert werden muss.

Aus diesem Grund halte ich es nicht für praktisch, sie hier zu diskutieren oder für die asynchrone Syntax vorzuschlagen. Dies würde das Merkmal weiter massiv verzögern.

Um diese bereits massive Diskussion einzudämmen, halte ich es für nicht nützlich, sie hier zu diskutieren. Sie können als außerhalb des diskutierten Themas angesehen werden.

Nur ein paar Gedanken: Während dieser Thread speziell für await gedacht ist, werden wir wahrscheinlich die gleiche Diskussion über yield Ausdrücke führen, die ebenfalls verkettet werden könnten. Insofern würde ich es vorziehen, eine Syntax zu sehen, die verallgemeinert werden kann, wie sie auch aussieht.

Meine Gründe, hier keine Makros zu verwenden:

  1. Makros werden für domänenspezifische Dinge bereitgestellt, und ihre Verwendung auf eine Weise, die den Programmfluss ändert oder die Kernsprachenfunktionen emuliert, ist ein Overkill
  2. Postfix-Makros würden sofort missbraucht, um benutzerdefinierte Operatoren oder andere esotherische Sprachfunktionen zu implementieren, die zu fehlerhaftem Code führen
  3. Sie würden davon abhalten, geeignete Sprachfunktionen zu entwickeln:

    • stream.await_all ist der perfekte Anwendungsfall für den Kombinator

    • option.or_continue und das Ersetzen von _else Kombinatoren ist der perfekte Anwendungsfall für den Null-Koaleszenz-Operator

    • monad.bind ist der perfekte Anwendungsfall für if-let Ketten

    • ndarray slicing ist der perfekte Anwendungsfall für const Generika

  4. Sie würden den bereits implementierten Operator ? in Frage stellen

@collinanderson

was ebenfalls verkettet werden könnte

Warum um alles in der Welt glauben Sie, dass sie angekettet werden könnten? Null Vorkommen in echtem Code, genau wie im obigen Beispiel println .

@Pzixel Es ist wahrscheinlich, dass Generator::resume irgendwann einen Wert annehmen wird und daher yield expr einen Nicht- () -Typ hat.

@vlaff Ich sehe kein await mit der Typzuweisung übereinstimmen muss. Das sind sehr unterschiedliche Dinge.

Außerdem ist die Typzuweisung eine weitere Funktion, die wiederholt ausprobiert wird. Es gibt keine Garantie dafür, dass diese Funktion ausgeführt wird. Obwohl ich mich dem nicht widersetzen möchte, ist TA ein zukünftiger RFC und dies ist eine Diskussion über eine akzeptierte Funktion mit einer bereits vorgeschlagenen Syntax.

Das Lesen der vielen Kommentare, um eine weitere Zusammenfassung darüber hinzuzufügen, warum warten! (...) erscheint ein sehr idealer Weg:

  1. Der vorhandene Code ist am vertrautesten, da Makros bekannt sind
  2. Es gibt bereits Arbeiten, die wait verwenden! (...) https://github.com/alexcrichton/futures-await und dazu beitragen könnten, das Umschreiben von Code zu reduzieren
  3. Da Postfix-Makros nicht auf dem Tisch sind, können sie niemals Teil der Sprache sein und "warten!" In einem nicht standardmäßigen Kontext scheint dies nicht einmal eine Möglichkeit gemäß RFCs zu sein
  4. Dies gibt der Community mehr Zeit, um über die langfristige Ausrichtung nach einer weit verbreiteten stabilen Nutzung nachzudenken und gleichzeitig etwas bereitzustellen, das nicht völlig außerhalb der Norm liegt (z. B. try! ()).
  5. könnte als ähnliches Muster für den bevorstehenden Ertrag verwendet werden! (), bis wir einen offiziellen Weg gefunden haben, der das Warten und den Ertrag befriedigen könnte
  6. Die Verkettung ist möglicherweise nicht so wertvoll wie erhofft, und die Klarheit wird wahrscheinlich sogar durch mehrere Wartezeiten in mehreren Zeilen verbessert
  7. Für IDE-Syntax-Textmarker müssten keine Änderungen vorgenommen werden
  8. Personen aus anderen Sprachen werden wahrscheinlich nicht von einem erwarteten Makro (...) abgeschreckt, nachdem sie andere Makronutzungen gesehen haben (weniger kognitive Überlastung).
  9. Es ist wahrscheinlich der Weg mit dem geringsten Aufwand aller Wege, um zur Stabilisierung voranzukommen

erwarten! Makro ist hier schon, die Frage ist über Verkettung.

Je mehr ich darüber nachdenke, desto mehr Postfix sieht für mich gut aus. Zum Beispiel:

let a = foo await;
let b = bar await?;
let c = baz? await;
let d = booz? await?;
let e = kik? + kek? await? + kuk? await?;
// a + b is `impl Add for MyFuture {}` which alises to `a.select(b)`

Ermöglicht das Verschachteln einer beliebigen Ebene und ist lesbar. Funktioniert nahtlos mit ? und möglichen zukünftigen Betreibern. Für Neulinge sieht es ein bisschen fremd aus, aber der Grammatikgewinn kann es übergewichten.

Der Hauptgrund ist, dass await ein separates Schlüsselwort sein sollte, kein Funktionsaufruf. Es ist zu wichtig, damit es ein eigenes Highlight und eine eigene Stelle im Text hat.

@Pzixel , @HeroicKatora , @skade

Siehe zum Beispiel Python yield und yield from Ausdrücke: Dort kann die äußere Funktion einen Wert bereitstellen, der das Ergebnis der yield wenn der Generator fortgesetzt wird. Dies ist, was @valff bedeutete und nichts mit Daher hat yield auch einen anderen Typ als ! oder () .

Ab dem Punkt der Coroutine setzen sowohl yield als auch await den Wert aus und können (eventuell) einen Wert zurückgeben. Insofern sind sie nur zwei Seiten derselben Medaille.

Und um eine andere Syntaxmöglichkeit auszuschließen, _square-brackets-with-keyword_, teilweise um dies hervorzuheben (unter Verwendung von yield für die Syntaxhervorhebung):

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

"postfix keyword" ist für mich am sinnvollsten. Postfix mit einem anderen Zeichen als Leerzeichen, das den zukünftigen Ausdruck und await trennt, macht für mich ebenfalls Sinn, aber nicht '.' da dies für Methoden gilt und await keine Methode ist. Wenn Sie jedoch await als Präfix-Schlüsselwort möchten, gefällt mir der Merkmals oder einer Methode, die nur await self für diejenigen aufruft, die eine Reihe von Dingen miteinander verketten möchten (obwohl wir dies wahrscheinlich tun) konnte die Methode await nicht benennen; nur wait würde reichen, obwohl ich denke). Persönlich würde ich wahrscheinlich ein Warten pro Zeile machen und nicht so viel verketten, weil ich lange Ketten weniger lesbar finde, so dass Präfix oder Postfix für mich funktionieren würden.

[Bearbeiten] Ich habe vergessen, dass wait bereits existiert und die Zukunft blockiert, also kratz diesen Gedanken.

@rolandsteiner also schreibst du

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

Wenn ich es schreiben würde wie:

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

@skade Oh, du

@Pzixel Es ist wahrscheinlich, dass Generator::resume irgendwann einen Wert annehmen wird und daher yield expr einen Nicht- () -Typ hat.

Daher hat yield auch einen anderen Typ als ! oder () .

@valff , @rolandsteiner Ich finde es unwahrscheinlich, dass yield den wiederaufnehmenden Wert zurückgibt , der sich nur schwer in eine statisch typisierte Sprache einfügt, ohne dass die Generatorsyntax und / oder das Merkmal störend wirken. Der ursprüngliche Prototyp mit Lebenslaufargumenten verwendete das Schlüsselwort gen arg , um auf dieses Argument zu verweisen. So etwas funktioniert IMO viel eher. Unter diesem Gesichtspunkt wird yield immer noch () , sollte also die Diskussion über await stark beeinflussen.

Ich denke, await sollte entweder ein Präfixoperator sein, weil async ist (und ich erwarte, dass yield auch ein Präfix ist). Oder lassen Sie es dann als reguläre Methode in der Postfix-Position verwenden.

Allerdings läuft hier nicht das ganze Bikeshedding weiter: Warum überhaupt ein Postfix-Keyword in Betracht ziehen, wo es kein anderer von Rust hat? Es würde die Sprache so komisch machen.

Wenn ich Futures verkette, verstehe ich auch, warum es interessant sein könnte. Aber Verkettung erwartet? Was bedeutet das überhaupt? Eine Zukunft, die eine neue Zukunft zurückgibt? Ist es wirklich eine so verbreitete Redewendung, dass Rust diese Redewendung als erstklassiger Bürger haben soll? Wenn uns das wirklich wichtig ist, sollten wir:

  1. Gehen Sie für die Methode (dh foo.await() ). Es wird kein seltsames Postfix-Schlüsselwort eingeführt, und wir alle wissen, was es bedeutet. Damit können wir auch verketten.
  2. Wenn wir wirklich ein Schlüsselwort / einen Loloperator wollen, können wir uns später mit dieser Frage befassen.

Um meine persönliche Meinung zu äußern, hasse ich außerdem die Postfix-Keyword-Position await .

Das Präfix await wird aus einem bestimmten Grund in anderen Sprachen verwendet - es entspricht der natürlichen Sprache. Sie sagen nicht "Ich werde Ihre Ankunft erwarten". Obwohl ich JavaScript geschrieben habe, bin ich vielleicht ein bisschen voreingenommen.

Ich werde auch sagen, dass ich await -Ketten für überbewertet halte. Der Wert von async/await gegenüber zukünftigen Kombinatoren besteht darin, dass man asynchronen Code nacheinander schreiben kann, wobei die Standardsprachenkonstrukte wie if , match usw. verwendet werden. Sie werden sowieso Ihre Wartezeiten auflösen wollen.

Lassen Sie uns nicht zu viel magische Syntax für async/await , es ist nur ein kleiner Teil der Sprache. Die vorgeschlagene Methodensyntax (dh foo.await() ) ist IMO zu orthogonal zu normalen Methodenaufrufen und wirkt zu magisch. Die proc-macro -Maschinerie ist vorhanden, warum nicht dahinter verbergen?

Wie wäre es mit der Verwendung des Schlüsselworts await im Gegensatz zu async bei der Funktionsverteidigung? Zum Beispiel:

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

Dieses await fn kann nur im Kontext von async aufgerufen werden:

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

Und auf let n = (await foo(bar())); .
Dann neben den await Schlüsselwort, merkmal Future könnte die Umsetzung await -Methode für die Verwendung von await Logik in Postfix - Position, zum Beispiel:

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

Kann mir auch jemand die Beziehung zu Generatoren erklären? Ich bin überrascht, dass async / await implementiert werden, bevor Generatoren (sogar stabilisiert) werden.

@phaazon- Generatoren wurden Rust als internes Implementierungsdetail für async / await hinzugefügt. Derzeit gibt es keine Pläne, sie zu stabilisieren, und sie erfordern noch einen nicht experimentellen RFC, bevor sie möglich sind. (Persönlich würde ich sie gerne stabilisieren lassen, aber es scheint wahrscheinlich, dass sie mindestens ein oder zwei Jahre entfernt sind und wahrscheinlich erst dann RFCed werden, wenn async / await stabil ist und es gab einige Erfahrungen damit).

@XX Ich denke, Sie brechen die Semantik von async wenn Sie await in der Funktionsdefinition verwenden. Bleiben wir bei Standards, nicht wahr?

@phaazon Können Sie genauer angeben, wie dies die Semantik von async bricht?

Die meisten Sprachen verwenden async , um etwas einzuführen, das asynchron sein wird, und await wartet darauf. await anstelle von async ist für mich irgendwie komisch.

@phaazon Nein, nicht stattdessen, sondern zusätzlich. Vollständiges Beispiel:

async fn bar() -> i32 {
    5 // will be "converted" to impl Future<Output = i32>
}

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future // will be "converted" to i32 in async context
}

async {
    let a = await bar(); // correct, a == 5
    let b = foo(bar()); // correct, b == 5
}

let c = foo(bar()); // error, can't call desugaring statement `await foo(bar())`

Wenn es gültig ist, kann die Warten-Methode zur Verwendung in einer Kette implementiert werden:

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

Meiner Meinung nach spielt es keine Rolle, ob sich das Warten auf Postfix zu magisch anfühlt und der Sprache nicht vertraut ist. Wenn wir die Methodensyntax .await() oder .await!() ist es nur eine Frage der Erklärung, dass Future eine .await() oder .await!() Methode haben dass Sie sie auch verwenden können, um sie in den Fällen abzuwarten, die Sinn machen. Es ist nicht so schwer zu verstehen, wenn Sie 5 Minuten darüber nachdenken, auch wenn Sie es noch nie gesehen haben.

Wenn wir uns für das Präfix-Schlüsselwort entscheiden, was hindert uns außerdem daran, eine Kiste mit den folgenden Angaben zu schreiben (unvollständiger Pseudocode, da ich momentan nicht über die Infrastruktur verfügt, um mit den Details umzugehen):

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

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

Auf diese Weise können wir Postfix warten lassen, wenn wir wollen. Ich meine, auch wenn das irgendwie nicht möglich ist und wir Postfix Wait auf magische Weise vom Compiler implementieren müssen, können wir so etwas wie das obige Beispiel verwenden, um zu erklären, wie es meistens funktioniert. Es wäre nicht schwer zu lernen.

@ivandardi Ich dachte über das gleiche. Aber diese Definition

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

sagt nicht, dass die Funktion nur im async -Kontext aufgerufen werden kann.

@XX Ja, wie gesagt, ignoriere den kaputten Pseudocode: PI hat derzeit nicht die Infrastruktur, um

@XX , @ivandardi Per Definition await nur in einem async -Kontext. Daher ist dies illegal:

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

Es muss sein

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

Was nur in einem async -Kontext wie diesem aufgerufen werden kann:

await future.await()

Welche Art von Niederlagen den ganzen Zweck.

Die einzige Möglichkeit, dies zum Laufen zu bringen, besteht darin, async vollständig zu ändern, was vom Tisch ist.

@ivandardi Diese Entwicklung meiner Version:

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

`` `Rost
warte fn warte (selbst) -> T {// Weil asynchron bereits genommen wurde
warte auf dich
}}

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

@CryZe @XX Warum fertig ? Ich habe recht.

@XX Was Sie versuchen, ist, die Bedeutung von await -Kontext erstellen, der sich irgendwie von einem async -Kontext unterscheidet). Ich denke nicht, dass es viel Unterstützung von den Sprachentwicklern bekommen würde

@EyeOfPython Diese Definition funktioniert nicht wie erwartet:

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

Wahrscheinlicher:

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

Ich stimme ab, weil Sie ignoriert haben, dass dies eine hypothetische Pseudosyntax ist. Im Wesentlichen geht es darum, dass Rust ein spezielles Merkmal wie Drop and Copy hinzufügen könnte, das die Sprache versteht, wodurch die Möglichkeit hinzugefügt wird, .await () für den Typ aufzurufen, der dann mit dem Typ interagiert, genau wie es ein Schlüsselwort zum Warten tun würde. Darüber hinaus wird die Abstimmung für RFCs ohnehin als irrelevant angesehen, da es darum geht, eine objektive Lösung zu finden, die nicht auf subjektiven Gefühlen basiert, wie sie durch Upvotes / Downvotes sichtbar gemacht werden.

Ich verstehe nicht, was dieser Code tun soll. Dies hat jedoch nichts damit zu tun, wie das asynchrone Warten derzeit funktioniert. Wenn Sie sich dafür interessieren, halten Sie es bitte aus diesem Thread heraus und schreiben Sie einen dedizierten RFC dafür.

@CryZe Dieser Typ ist verfügbar. Es heißt Zukunft. Und es wurde vor langer Zeit vereinbart, dass Async / Warten ein Mechanismus ist, der NUR auf die Transformation von Async-Code abzielt. Es ist nicht als allgemeine Bindungsnotation gedacht.

@ivandari Ihr Postfix-

@ Matthias247 Was sie vorschlagen wollten, war eine Möglichkeit, Postfix auf die Syntax eines Methodenaufrufs warten zu lassen. Auf diese Weise muss Rust keine beliebigen neuen Symbole wie #, @ oder ... einführen, wie wir es mit dem? Betreiber und sehen immer noch ziemlich natürlich aus:

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

Die Idee wäre also, irgendwie darauf zu warten, eine spezielle Methode für das Future-Merkmal zu sein, die der Compiler genauso sieht wie ein normales Warten-Schlüsselwort (das Sie in diesem Fall überhaupt nicht benötigen), und die Asynchronität zu transformieren Code in einen Generator von dort. Dann haben Sie den richtigen logischen Kontrollfluss von links nach rechts anstelle von links rechts links mit await some_future()? und müssen keine seltsamen neuen Symbole einführen.

(Also tl; dr: es sieht aus wie ein Methodenaufruf, wartet aber eigentlich nur auf Postfix)

Was ich in Anbetracht einer Postfix-Syntax nicht explizit erwähnt habe, ist die Verbreitung der Fehler- und Optionskombinatoren. Dies ist der Ort, an dem sich Rost am meisten von allen Sprachen unterscheidet, die auf uns warten - anstelle von automatischen Rückverfolgungen haben wir .map_err()? .

Insbesondere Dinge wie:

let result = await reqwest::get(..).send();
let response = result.map_err(|e| add_context(e))?;
let parser = await response.json();
let parsed = parser.map_err(|e| add_context(e))?;

ist weniger lesbar als (es ist mir egal, welche Postfix-Syntax berücksichtigt wird):

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

Trotzdem hat diese Standardformatierung mehr Zeilen als der Ansatz mit mehreren Variablen. Ausdrücke, die nicht mehrere Wartezeiten haben, sollten mit postfix-await weniger vertikalen Platz beanspruchen.

Ich schreibe momentan keinen asynchronen Code, daher spreche ich aus Unwissenheit. Es tut mir leid, dass ich mich darauf staple. Wenn dies in der Praxis kein Problem darstellt, ist das ausgezeichnet. Ich möchte nur nicht, dass die richtige Fehlerbehandlung weniger ergonomisch ist, nur um anderen Sprachen ähnlicher zu sein, die die Fehlerbehandlung anders durchführen.

Ja, das ist, was ich meine. Es wäre das Beste aus beiden Welten, sodass die Menschen je nach Situation wählen können, ob sie ein Präfix oder ein Postfix verwenden möchten.

@XX Wenn dies nicht im Benutzercode ausgedrückt werden kann, müssen wir auf den Compiler zurückgreifen, um diese Funktionalität zu implementieren. Aber um zu verstehen, wie der Postfix funktioniert, funktioniert die Erklärung, die ich zuvor gepostet habe, immer noch, auch wenn es sich nur um ein oberflächliches Verständnis handelt.

@CryZe , @XX ,

Auf den ersten Blick ist es glatt, entspricht aber absolut nicht der Rust-Philosophie. Selbst die Methode sort auf Iteratoren gibt nicht self und unterbricht die Methodenaufrufkette, um die Zuordnung explizit zu machen. Sie erwarten jedoch, dass etwas nicht weniger Wirkungsvolles auf völlig implizite Weise implementiert wird, das nicht von regulären Funktionsaufrufen zu unterscheiden ist. Es gibt keine Chancen IMO.

Ja, das ist, was ich meine. Es wäre das Beste aus beiden Welten, sodass die Menschen je nach Situation wählen können, ob sie ein Präfix oder ein Postfix verwenden möchten.

Warum sollte das ein Ziel sein? Rust unterstützt dies nicht für alle anderen Kontrollabläufe (z. B. break , continue , if usw. Es gibt auch keinen bestimmten Grund, abgesehen von "dies könnte" Sieh nach deiner Meinung besser aus.

Im Allgemeinen möchte ich alle daran erinnern, dass await ganz Besonderes ist und kein normaler Methodenaufruf:

  • Debugger können sich beim Überschreiten von Wartezeiten seltsam verhalten, da sich der Stack möglicherweise abwickelt und wiederhergestellt wird. Soweit ich mich erinnere, hat es viel Zeit gekostet, das asynchrone Debugging in C # und Javascript zum Laufen zu bringen. Und diese haben Teams bezahlt, die an Debuggern arbeiten!
  • Lokale Objekte, die es nicht über Wartepunkte schaffen, können auf dem realen Betriebssystemstapel gespeichert werden. Diejenigen, die es nicht sind, müssen in die generierte Zukunft verschoben werden, was letztendlich einen Unterschied im erforderlichen Heap-Speicher bewirken wird (wo die Zukunft leben wird).
  • Ausleihen über Wartepunkte sind der Grund, warum generierte Futures !Unpin betragen müssen, und verursachen einige Unannehmlichkeiten mit einigen der vorhandenen Kombinatoren und Mechanismen. Es ist möglicherweise möglich, Unpin Futures aus async Methoden zu generieren, die in Zukunft keine Kredite mehr aufnehmen. Wenn jedoch await s unsichtbar sind, wird dies niemals passieren.
  • Möglicherweise gibt es andere unerwartete Probleme mit dem Kreditprüfer, die vom aktuellen Status von async / await nicht abgedeckt werden.

@Pzixel @lnicola

Die Beispiele, die Sie für C # geben, sind absolut zwingend und nichts anderes als lange Ketten im funktionalen Stil, die wir oft in Rust sehen. Diese LINQ-Syntax ist nur eine DSL-Syntax und entspricht nicht foo().bar().x().wih_boo(x).camboom().space_flight(); Ich habe ein bisschen gegoogelt, und C # -Codebeispiele sehen wie die meisten gängigen Programmiersprachen zu 100% zwingend aus. Deshalb können wir nicht einfach das nehmen, was Sprache X getan hat, weil es einfach nicht dasselbe ist.

Die IMO-Präfixnotation passt perfekt zum imperativen Codierungsstil. Aber Rust unterstützt beide Stile.

@skade

Ich bin mit einigen Ihrer (und anderer) Kommentare zu Problemen mit dem Funktionsstil nicht einverstanden. Um es kurz zu machen - lassen Sie uns einfach zustimmen, dass es Menschen gibt, die den einen oder anderen stark bevorzugen.

@ Matthias247 Nein, es gibt einen anderen Grund als das, wie schön es aussieht. Es ist, weil Rust die Verkettung fördert. Und Sie könnten sagen: "Oh, andere Kontrollflussmechanismen haben keine Postfix-Syntax. Warum sollten Sie also darauf warten, etwas Besonderes zu sein?". Nun, das ist eine falsche Einschätzung. Wir haben einen Postfix-Kontrollfluss. Sie sind nur intern in den Methoden, die wir aufrufen. Option::unwrap_or ist wie eine Postfix-Match-Anweisung. Iterator::filter ist wie eine Postfix-if-Anweisung. Nur weil der Kontrollfluss nicht Teil der Verkettungssyntax selbst ist, heißt das nicht, dass wir noch keinen Postfix-Kontrollfluss haben. Unter diesem Gesichtspunkt würde das Hinzufügen eines erwarteten Postfixes tatsächlich mit dem übereinstimmen, was wir haben. In diesem Fall könnten wir sogar etwas ähnlicheres haben, wie Iterator funktioniert, und anstatt nur einen rohen Postfix warten zu lassen, könnten wir einige Kombinatoren erwarten, wie Future::await_or . In jedem Fall ist das Warten auf Postfix nicht nur eine Frage des Aussehens, sondern auch der Funktionalität und der Lebensqualität. Sonst würden wir immer noch das Makro try!() , oder?

@dpc

Die Beispiele, die Sie für C # geben, sind absolut zwingend und nichts anderes als lange Ketten im funktionalen Stil, die wir oft in Rust sehen. Diese LINQ-Syntax ist nur eine DSL-Syntax und ähnelt nicht foo (). Bar (). X (). Wih_boo (x) .camboom (). Space_flight (); Ich habe ein bisschen gegoogelt, und C # -Codebeispiele sehen wie die meisten gängigen Programmiersprachen zu 100% zwingend aus. Deshalb können wir nicht einfach das nehmen, was Sprache X getan hat, weil es einfach nicht dasselbe ist.

Es ist nicht wahr (Sie könnten jedes Framework überprüfen, z. B. Polly ), aber ich werde nicht darüber streiten. Ich sehe so viele verkettete Codes in beiden Sprachen. Allerdings gibt es tatsächlich eine Sache , die alles anders macht. Und es heißt Try Merkmal. C # hat nichts Ähnliches, daher wird nicht angenommen, dass etwas über await hinaus getan wird. Wenn Sie eine Ausnahme erhalten, wird diese automatisch umbrochen und für den Anrufer ausgelöst.

Dies ist bei Rust nicht der Fall, wo Sie den Operator ? manuell verwenden müssen.

Wenn Sie etwas über await Punkt hinaus haben müssen, sollten Sie entweder einen "kombinierten" Operator await? erstellen, Sprachregeln usw. usw. erweitern oder einfach das Postfix abwarten lassen und die Dinge natürlicher werden (mit Ausnahme von a ein bisschen fremde Syntax, aber wen interessiert das?).

Daher sehe ich derzeit das Warten auf Postfix als praktikablere Lösung. Der einzige Vorschlag, den ich dort habe, sollte ein dediziertes, durch Leerzeichen getrenntes Schlüsselwort sein, nicht await() oder await!() .

Ich glaube, ich habe einen guten Grund gefunden, eine normale .await () -Methode zu rechtfertigen. Wenn Sie darüber nachdenken, ist es nicht anders als jede andere Art des Blockierens. Wenn Sie die .await () -Methode aufrufen, wird die Ausführung des (möglicherweise grünen) Threads geparkt, und irgendwann setzt die ausführende Laufzeit die Ausführung des Threads irgendwann fort. Also in jeder Hinsicht, ob Sie einen Mutex- oder einen Standardkanal wie diesen haben:

let result = my_channel().recv()?.iter().map(...).collect();

oder eine Zukunft wie diese:

let result = my_future().await()?.iter().map(...).collect();

ist nicht anders. Beide blockieren die Ausführung an ihrem jeweiligen recv () / await () und verketten beide auf die gleiche Weise. Der einzige Unterschied besteht darin, dass await () möglicherweise auf einem anderen Executor ausgeführt wird, bei dem es sich nicht um das schwere Threading des Betriebssystems handelt (dies kann jedoch durchaus bei einem einzelnen Thread-Executor der Fall sein). Das Entmutigen starker Verkettungen wirkt sich also genau auf die gleiche Weise aus und sollte wahrscheinlich zu einer tatsächlichen Klippfussel führen.

Mein Punkt hier ist jedoch, dass es aus Sicht der Person, die den Code schreibt, keinen großen Unterschied zwischen .recv () und .await () gibt. Beide Methoden blockieren die Ausführung und geben zurück, sobald das Ergebnis verfügbar ist . In jeder Hinsicht kann es also eine normale Methode sein und erfordert kein vollständiges Schlüsselwort. Wir haben auch kein Schlüsselwort für recv oder lock für Mutex und den Kanal von std.

Allerdings möchte rustc offensichtlich den gesamten Code in einen Generator verwandeln. Aber ist das aus sprachlicher Sicht tatsächlich semantisch notwendig? Ich bin mir ziemlich sicher, dass man einen alternativen Compiler zu rustc schreiben könnte, der das Warten durch Blockieren eines tatsächlichen Betriebssystem-Threads implementiert (die Eingabe eines asynchronen fn müsste einen Thread erzeugen und das Warten würde dann diesen Thread blockieren). Dies wäre zwar eine äußerst naive und langsame Implementierung, würde sich jedoch semantisch genauso verhalten. Die Tatsache, dass tatsächliche Rustc-Wendungen in einen Generator warten, ist semantisch nicht notwendig. Ich würde also tatsächlich argumentieren, dass rustc, das den Aufruf der .await () -Methode in einen Generator verwandelt, als optimierendes Implementierungsdetail von rustc angesehen werden kann. Auf diese Weise können Sie rechtfertigen, dass .await () eine Art vollständige Methode ist und kein vollständiges Schlüsselwort, aber dennoch das Ganze in einen Generator verwandelt.

@CryZe Im Gegensatz zu .recv() wir await sicher von jeder anderen Stelle im Programm aus unterbrechen und dann wird Code neben await nicht ausgeführt. Das ist ein großer Unterschied und das ist der wertvollste Grund, warum wir nicht implizit await tun sollten.

@ I60R Es ist nicht weniger explizit als als Schlüsselwort zu warten. Sie können es in der IDE immer noch auf die gleiche Weise markieren, wenn Sie möchten. Das Schlüsselwort wird nur an die Postfix-Position verschoben, wo ich behaupten würde, dass es weniger leicht zu übersehen ist (da es genau an der Position ist, an der die Ausführung angehalten wird).

Verrückte Idee: implizit warten . Ich bin zu einem separaten Thread gewechselt, da dieser bereits zu beschäftigt mit der Diskussion zwischen Postfix und Präfix ist.

@CryZe

Ich glaube, ich habe einen guten Grund gefunden, eine normale .await () -Methode zu rechtfertigen. Wenn Sie darüber nachdenken, ist es nicht anders als jede andere Art des Blockierens.

Bitte lesen Sie noch einmal https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515

@ Matthias247 Das sind sehr gute Punkte, anscheinend habe ich diese verpasst.

All dieses Gerede darüber, das Warten in eine Funktion, ein Mitglied oder auf andere Weise implizit zu machen, ist grundsätzlich ungültig. Es ist keine Handlung, es ist eine Transformation.

Der Compiler wandelt auf hoher Ebene, sei es über ein Schlüsselwort oder ein Makro, wartende Ausdrücke in spezielle Generator-Ertragsausdrücke um, die vorhanden sind und dabei Ausleihen und andere Dinge berücksichtigen.

Dies sollte explizit sein.

Es sollte entweder so scheinbar wie möglich als Schlüsselwort sein oder offensichtlich Code mit einem Makro generieren.

Magische Methoden, Eigenschaften, Mitglieder usw. sind allzu leicht zu verstehen und zu missverstehen, insbesondere für neue Benutzer.

Das Warten auf das Präfix-Schlüsselwort oder das Warten auf das Präfix-Makro scheint die akzeptabelste Methode zu sein. Dies ist sinnvoll, da dies in vielen anderen Sprachen der Fall ist. Wir müssen nichts Besonderes sein, um es gut zu machen.

@novacrazy

Magische Methoden, Eigenschaften, Mitglieder usw. sind allzu leicht zu verstehen und zu missverstehen, insbesondere für neue Benutzer.

Können Sie das erweitern? Wenn möglich, genauer gesagt bei der Variante .await!() .

Ein foo.await!() ist meiner Meinung nach nicht schwer zu lesen, INSBESONDERE mit der richtigen Syntaxhervorhebung, die nicht ignoriert werden sollte.

Die Bedeutung falsch verstehen? Was getan wird, ist im Wesentlichen Folgendes (ignorieren Sie den Typ Mystakes):

trait Future {
    fn await!(self) -> Self::Item {
        await self
    }
}

AKA await this_foo und this_foo.await!() sind genau gleich. Was ist daran leicht zu missverstehen?

Und zum Thema neue Benutzer: Neue Benutzer zu was? Programmieren im Allgemeinen oder neue Benutzer von Rust als Sprache, aber mit Programmiersprachenhintergrund? Denn wenn erstere, dann bezweifle ich, dass sie sich direkt mit asynchroner Programmierung beschäftigen werden. Und wenn letzteres der Fall ist, ist es einfacher, die Semantik von Postfix Wait (wie oben erläutert) ohne Verwirrung zu erklären.

Wenn alternativ nur das erwartete Präfix hinzugefügt wird, stoppt nichts, von dem ich weiß, die Erstellung eines Programms, das als Eingabe den Rust-Code der Form verwendet

foo.bar().baz().quux().await!().melo().await!()

und verwandelt es in

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() ist in einigen Fällen nett und würde wahrscheinlich neben await!(...) funktionieren wie:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

Postfix-Methodenmakros existieren derzeit jedoch nicht und möglicherweise nie.

Wenn wir glauben, dass dies in Zukunft möglich ist, sollten wir vorerst das Makro await!(...) und in Zukunft einfach das Postfix hinzufügen, wenn dies implementiert ist.

Beide Makros wären ideal, aber wenn nicht beabsichtigt ist, Postfix-Makros in Zukunft zu implementieren, ist das Präfix-Schlüsselwort await wahrscheinlich die beste Wahl.

@novacrazy Ich kann dem zustimmen und es ist mein ursprünglicher Vorschlag. Wir sollten vorerst await!() hinzufügen und dabei ein Postfix-Formular herausfinden. Besprechen Sie möglicherweise auch die Möglichkeit von Postfix-Makros und ob wir ein Postfix .await!() ad-hoc in die Sprache einfügen könnten, bevor wir die vollständige Unterstützung für Postfix-Makros in der Sprache haben. Ein bisschen wie das, was mit ? und dem Merkmal Try passiert ist: Es wurde zuerst als Sonderfall hinzugefügt und danach zu einem allgemeineren Fall erweitert. Das einzige, was wir bei der Entscheidung vorsichtig sein müssen, ist, wie die allgemeine Postfix-Makrosyntax aussehen würde, was möglicherweise eine separate Diskussion verdient.

Das Warten auf das Präfix-Schlüsselwort oder das Warten auf das Präfix-Makro scheint die akzeptabelste Methode zu sein. Dies ist sinnvoll, da dies in vielen anderen Sprachen der Fall ist.

Es ist offensichtlich praktikabel, aber ich erinnere mich nur an zwei Argumente dafür, und ich finde sie nicht überzeugend:

  • So machen es andere Sprachen

    • Das ist schön, aber wir sind schon Dinge anders wie nicht-running-unless- tun poll ed statt Lauf-up-to-the-First - await , weil Rost eine grundsätzlich andere ist Sprache

  • Leute mögen es, await am Anfang der Zeile zu sehen

    • Aber es ist nicht immer da. Wenn dies der einzige Ort ist, an dem man hinschaut, hat man Probleme

    • Es ist genauso einfach, die await in foo(aFuture.await, bFuture.await) als ob sie ein Präfix wären

    • In Rost scannt man bereits das Ende der Linie nach ? wenn man den Kontrollfluss betrachtet

Habe ich etwas verpasst?

Wenn die Debatte "meh, sie sind alle ungefähr gleich" wäre, würde ich absolut zustimmen "na ja, wenn es uns nicht wirklich interessiert, könnten wir genauso gut das tun, was alle anderen tun". Aber ich glaube nicht, dass wir dort sind.

@scottmcm Ja zu "meh, sie sind alle ungefähr gleich." Zumindest aus Sicht des Benutzers.

Daher sollten wir ein angemessenes Gleichgewicht zwischen Lesbarkeit, Vertrautheit, Wartbarkeit und Leistung finden. Daher gilt meine Aussage in meinem letzten Kommentar mit dem Makro await!() wenn wir in Zukunft Makros für Postfix-Methoden ( .await!() ) hinzufügen möchten, oder nur ein langweiliges Präfix-Schlüsselwort await sonst.

Ich sage langweilig, weil langweilig gut ist. Wir möchten uns von der Syntax selbst fernhalten, wenn wir Code mit diesen schreiben.

Wenn f.await() keine gute Idee wäre, dann bevorzuge ich die Präfixsyntax.

  1. Als Benutzer hoffe ich, dass die von mir verwendete Sprache nur wenige Syntaxregeln enthält, und mit diesen Regeln kann ich zuverlässig ableiten, was sie möglicherweise tut. KEINE Ausnahmen hier und da. In Rust steht async am Anfang, await am Anfang ist keine Ausnahme. Allerdings wäre f await , Post-Keyword-Formular. f.await sieht aus wie Feldzugriff, eine Ausnahme . f.await!() hat ein Postfix-Makro, das nie in der Sprache angezeigt wurde, und weiß nicht, für welche anderen Fälle es gut wäre, eine Ausnahme . Wir haben keine Antwort darauf, wie diese Syntax zu Regeln und nicht zu einmaligen Ausnahmen werden würde .

  2. Wenn eine Ausnahme gemacht wird, hoffe ich, dass dies intuitiv sinnvoll ist. Nehmen Sie als Beispiel ? , was als Ausnahme angesehen werden kann, da es in anderen Sprachen nicht häufig vorkommt. f()?.map() liest sich fast wie compute f () und ist dieses Ergebnis gut? Das ? hier erklärt sich von selbst. Aber für f await frage ich, warum es Postfix ist?, Für f.await frage ich await ein Feld, f.await!() Ich frage, warum Makro in dieser Position erscheint? Sie bieten mir zumindest auf den ersten Blick keinen überzeugenden / intuitiven Sinn.

  3. Um den ersten Punkt zu erweitern, möchte Rust am liebsten eine Systemsprache sein. Die Hauptakteure hier, C / C ++ / Go / Java, sind alle etwas zwingend erforderlich. Ich glaube auch, dass die meisten Leute ihre Karriere mit einer imperativen Sprache beginnen, C / Python / Java, nicht Haskell usw. Ich denke, um Systementwickler und Entwickler der nächsten Generation davon zu überzeugen, Rust zu übernehmen, muss Rust zuerst im imperativen Stil gut und dann funktional sein, nicht hoch funktional zu sein, aber kein vertrautes Gefühl zu haben.

  4. Ich denke, es ist nichts Falsches daran, eine Kette zu teilen und in mehrere Zeilen zu schreiben. Es wird nicht ausführlich sein. Es ist nur explizit .

Als ich sah, wie sich die Diskussion kontinuierlich von einer Richtung in eine andere bewegt (Präfix vs Postfix), entwickelte ich die starke Meinung, dass mit dem Schlüsselwort await etwas nicht stimmt, und wir müssen uns vollständig davon zurückziehen. Stattdessen schlage ich die folgende Syntax vor, die meiner Meinung nach einen sehr guten Kompromiss zwischen allen Meinungen darstellt, die im aktuellen Thread veröffentlicht wurden:

// syntax below is exactly the same as with prefix `await`
let response = go client.get("https://my_api").send();
let body: MyResponse = go response.into_json();

Im ersten Schritt würden wir es nur als regulären Präfixoperator implementieren, ohne Fehler bei der Behandlung bestimmter Aufbauten:

// code below don't compiles because `?` takes precedence over `go`
let response = go client.get("https://my_api").send()?;
let body: MyResponse = go response.into_json()?;

Im zweiten Schritt würden wir eine verzögerte Präfixoperatorsyntax implementieren, die auch eine ordnungsgemäße Fehlerbehandlung ermöglicht.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

Das ist alles.


Sehen wir uns nun einige zusätzliche Beispiele an, die mehr Kontext bieten:


// A
if db.go is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .go send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .go send()?
    .error_for_status()?
    .go json()?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.go request(url, Method::GET, None, true)?
        .res.go json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   go self.logger.log("beginning service call");
   let output = go service.exec();
   go self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = go acquire_lock();
    let length = logger.go log_into(message)?;
    go logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    go (go partial_computation()).unwrap_or_else(or_recover);
}

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

Es ist unter der Spoiler-Version mit aktivierter Syntaxhervorhebung versteckt


// A
if db.as is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.as load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .as send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .as send()?
    .error_for_status()?
    .as json()?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.as request(url, Method::GET, None, true)?
        .res.as json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   as self.logger.log("beginning service call");
   let output = as service.exec();
   as self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = as acquire_lock();
    let length = logger.as log_into(message)?;
    as logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    as (as partial_computation()).unwrap_or_else(or_recover);
}

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


IMO ist diese Syntax in jeder Hinsicht besser als Alternativen:

✓ Konsistenz: Sieht im Rust-Code sehr organisch aus und erfordert keinen fehlerhaften Codestil
✓ Kompositionsfähigkeit: lässt sich gut in andere Rust-Funktionen wie Verkettung und Fehlerbehandlung integrieren
✓ Einfachheit: ist entweder kurz, beschreibend, leicht zu verstehen und leicht zu bearbeiten
✓ Wiederverwendbarkeit: Die Syntax eines verzögerten Präfixoperators wäre auch in anderen Kontexten nützlich
✓ Dokumentation: impliziert, dass anrufseitige Sendungen irgendwo ablaufen und warten, bis sie zurückkehren
✓ Vertrautheit: Bietet bereits vertrautes Muster, tut dies jedoch mit weniger Kompromissen
✓ Lesbarkeit: Liest als einfaches Englisch und verzerrt nicht die Bedeutung von Wörtern
✓ Sichtbarkeit: Aufgrund seiner Position ist es sehr schwierig, sich irgendwo im Code zu tarnen
✓ Erreichbarkeit: Kann sehr leicht gegoogelt werden
✓ Gut getestet: Golang ist heutzutage mit einer ähnlich aussehenden Syntax beliebt
✓ Überraschungen: Es ist schwer, diese Syntax zu missverstehen und zu missbrauchen
✓ Befriedigung: Nach wenig Lernen ist jeder Benutzer mit dem Ergebnis zufrieden


Bearbeiten: Wie @ivandardi im Kommentar unten zeigte, sollten einige Dinge geklärt werden:

1. Ja, diese Syntax ist etwas schwer zu analysieren, aber gleichzeitig ist es unmöglich, hier eine Syntax zu erfinden, die keine Lesbarkeitsprobleme hätte. go Syntax von await und in der zurückgestellten Position IMO besser lesbar ist als das Postfix await z.B:

match db.go load(message.key) await {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

Wobei nicht await mehrdeutig aussieht und eine andere Assoziativität aufweist als alle vorhandenen Schlüsselwörter, aber auch blockierend werden könnte, wenn wir uns entscheiden, await Blöcke irgendwann in der Zukunft zu implementieren.

2. Es ist erforderlich, RFC zu erstellen, die Syntax des "verzögerten Präfixoperators" zu implementieren und zu stabilisieren. Diese Syntax wäre nicht spezifisch für asynchronen Code, jedoch wäre die Verwendung in asynchronem Code die Hauptmotivation dafür.

3. In der Syntax "verzögerter Präfixoperator" ist das Schlüsselwort wichtig, weil:

  • Ein langes Schlüsselwort würde den Abstand zwischen Variable und Methode vergrößern, was die Lesbarkeit beeinträchtigt
  • langes Schlüsselwort ist eine seltsame Wahl für den Präfixoperator
  • Das lange Schlüsselwort ist unnötig ausführlich, wenn asynchroner Code als synchron angezeigt werden soll

Wie auch immer, es ist durchaus möglich, await anstelle von go und es dann in der Ausgabe 2022 umzubenennen. Bis zu diesem Zeitpunkt ist es sehr wahrscheinlich, dass ein anderes Schlüsselwort oder ein anderer Operator erfunden wird. Und wir können sogar entscheiden, dass überhaupt keine Umbenennung erforderlich ist. Ich habe festgestellt, dass await auch lesbar ist.

Unter der Spoiler-Version ist await anstelle von go versteckt


// A
if db.await is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.await load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .await send()?
    .error_for_status()?;

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

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.await request(url, Method::GET, None, true)?
        .res.await json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

// G
async fn log_service(&self) -> T {
   let service = self.myService.foo();
   await self.logger.log("beginning service call");
   let output = await service.exec();
   await self.logger.log("foo executed with result {}.", output));
   output
}

// H
async fn try_log(message: String) -> Result<usize, Error> {
    let logger = await acquire_lock();
    let length = logger.await log_into(message)?;
    await logger.timestamp();
    Ok(length)
}

// I
async fn await_chain() -> Result<usize, Error> {
    await (as partial_computation()).unwrap_or_else(or_recover);
}

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


Wenn Sie hier sind, um abzustimmen, stellen Sie Folgendes sicher:

  • dass Sie es richtig verstanden haben, da ich möglicherweise nicht der beste Redner bin, um neue Dinge zu erklären
  • dass Ihre Meinung nicht voreingenommen ist, da es viele Vorurteile gibt, von denen diese Syntax stammt
  • dass Sie unten einen gültigen Grund angeben, da stille Abstimmungen äußerst giftig sind
  • dass es in Ihrem Grund nicht um unmittelbare Gefühle geht, da ich hier versucht habe, ein langfristiges Feature zu entwerfen

@ I60R

Damit habe ich ein paar Probleme.

match db.go load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

Das ist etwas schwer zu analysieren. Auf den ersten Blick würden Sie denken, wir würden auf db.go passen, aber dann ist da noch etwas mehr und Sie sagen "oooh, ok, Laden ist eine Methode von db". IMO, dass die Syntax in erster Linie nicht funktioniert, da Methoden immer in der Nähe des Objekts sein sollten, zu dem sie gehören, ohne Leerzeichen und Schlüsselwortunterbrechung zwischen ihnen.

Zweitens muss für Ihren Vorschlag definiert werden, was "verzögerte Präfixoperatorsyntax" ist, und möglicherweise zuerst in der Sprache verallgemeinert werden. Andernfalls wird es nur für asynchronen Code verwendet, und wir kehren zur Diskussion über "Warum nicht Postfix-Makros?" Zurück. auch.

Drittens denke ich, dass das Schlüsselwort überhaupt nicht wichtig ist. Wir sollten jedoch await bevorzugen, da das Schlüsselwort für die Ausgabe 2018 reserviert wurde, während das Schlüsselwort go keine tiefere Suche nach crates.io hat und durchführen sollte, bevor es vorgeschlagen wird.

Als ich sah, wie sich die Diskussion kontinuierlich von einer Richtung in eine andere bewegt (Präfix vs Postfix), entwickelte ich die starke Meinung, dass etwas mit dem Schlüsselwort wait nicht stimmt und wir uns vollständig davon zurückziehen müssen.

Wir könnten (halb-) implizit warten und alles wäre in Ordnung, aber es wird ein harter Verkauf in der Rust-Community, trotz der Tatsache, dass der Sinn von asynchronen Funktionen darin besteht, die Implementierungsdetails zu verbergen und sie gerecht aussehen zu lassen wie io eins blockieren.

@dpc Ich meine, der beste Weg, den ich mir unsafe .

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

Wo alles innerhalb des await -Blocks automatisch erwartet wird. Damit ist es nicht mehr erforderlich, dass Postfix auf die Verkettung wartet, da Sie stattdessen nur auf die gesamte Kette warten können. Gleichzeitig bin ich mir nicht sicher, ob diese Form wünschenswert wäre. Vielleicht kann jemand anderes einen wirklich guten Grund dagegen finden.

@dpc Bei asynchronen Funktionen geht es nicht darum, die Details auszublenden, sondern sie

Niemand verwechselt asynchrone Funktionen mit synchronen in irgendeiner Sprache. Es geht lediglich darum, Abfolgen von Operationen in einer besser lesbaren und wartbaren Form zu organisieren, anstatt eine Zustandsmaschine von Hand zu schreiben.

Außerdem würden implizite Wartezeiten , wie in Future -Kombinatoren unmöglich machen, die, wie ich bereits gezeigt habe, immer noch unglaublich leistungsfähig sind. Wir können nicht einfach Future und darauf warten, das wäre höchst ineffizient.

Explizit, vertraut, lesbar, wartbar und performant: Präfix-Schlüsselwort await oder Makro await!() , möglicherweise mit Postfix-Makro .await!() in der Zukunft.

@novacrazy Auch wenn die ideale Syntax gewählt wurde, möchte ich in einigen Situationen benutzerdefinierte Futures und Kombinatoren verwenden. Die Idee, dass diese leicht veraltet sein könnten, lässt mich die gesamte Richtung von Rust in Frage stellen.

Natürlich können Sie weiterhin Kombinatoren verwenden, nichts wird veraltet.

Es ist die gleiche Situation wie bei ? : Neuer Code verwendet normalerweise ? (weil er besser als die Kombinatoren ist), aber die Option / Ergebnis-Kombinatoren können weiterhin verwendet werden (und die funkigeren wie or_else werden immer noch regelmäßig verwendet).

Insbesondere kann async / await map und and_then vollständig ersetzen, aber die funkigeren Future-Kombinatoren können (und werden) weiterhin verwendet werden.

Die Beispiele, die Sie geben, sehen im Vergleich zu den Kombinatoren immer noch schrecklich aus.

Ich denke nicht, ich denke, sie sind viel klarer, weil sie Standard-Rust-Funktionen verwenden.

Bei Klarheit geht es selten um die Anzahl der Charaktere, aber es hat alles mit mentalen Konzepten zu tun.

Mit dem Postfix await sieht es noch besser aus:

some_op().await?
    .another_op().await?
    .final_op().await

Sie können das mit Ihrem Original vergleichen:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

und mit dem Generator Overhead wird wahrscheinlich ein bisschen langsamer sein und mehr Maschinencode produzieren.

Warum behaupten Sie, dass durch asynchrones Warten / Warten zusätzliche Kosten entstehen? Sowohl Generatoren als auch Async / Awarit wurden speziell entwickelt, um kostengünstig und stark optimiert zu sein.

Insbesondere wird async / await genau wie Future-Kombinatoren zu einer stark optimierten Zustandsmaschine kompiliert.

anstatt diese Generator-Spaghetti-Zustandsmaschinen unter der Haube zu schaffen.

Die Zukunft combinators auch eine „Spaghetti - Zustandsmaschine“ unter der Haube schaffen, was genau das ist , was sie tun wurden entwickelt , um.

Sie unterscheiden sich nicht grundlegend von async / await.

[Zukünftige Kombinatoren] haben das Potenzial, zu effizienterem Code zu kompilieren.

Bitte hören Sie auf, Fehlinformationen zu verbreiten. Insbesondere hat Async / Warten die Möglichkeit, schneller als zukünftige Kombinatoren zu sein.

Wenn sich herausstellt, dass Benutzer mit einem Präfix-Schlüsselwort nicht zufrieden sind, kann es in einer Ausgabe 2019/2020 geändert werden.

Wie andere gesagt haben, sind Ausgaben keine willkürliche Sache, die wir nur machen, wenn wir Lust dazu haben. Sie sind speziell dafür konzipiert, nur alle paar Jahre (frühestens) zu erscheinen.

Und wie @Centril betonte, ist es eine sehr ineffiziente Methode, eine schlechte Syntax zu entwickeln, um sie später zu ersetzen.

Das Diskussionstempo ist derzeit recht hoch. Um die Dinge zu verlangsamen und den Leuten das Aufholen zu ermöglichen, habe ich das Problem vorübergehend gesperrt. Es wird innerhalb eines Tages freigeschaltet. Versuchen Sie zu diesem Zeitpunkt bitte, in Zukunft so konstruktiv wie möglich zu sein.

Entsperren Sie das Problem einen Tag später ... Bitte denken Sie daran, keine bereits gemachten Punkte zu machen und Kommentare zum Thema zu behalten (z. B. ist dies nicht der Ort, an dem implizites Warten oder andere Dinge außerhalb des Geltungsbereichs liegen.).

Da dies ein langer Thread war, wollen wir einige der interessantesten darin hervorgehobenen Kommentare hervorheben.

@mehcode hat uns umfangreichen realen Code mit await in Präfix- und Postfix-Positionen zur Verfügung gestellt: https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

@Centril argumentierte überzeugend, dass die Stabilisierung von await!(expr) das wissentliche Hinzufügen technischer Schulden bedeutet: https://github.com/rust-lang/rust/issues/57640#issuecomment -455806584

@valff erinnerte uns daran, dass die Postfix-Schlüsselwortsyntax expr await am besten zu Generalized Type Ascription passt : https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@quodlibetor hob hervor, dass der syntaktische Effekt von Optionskombinatoren nur bei Rust auftritt, und spricht sich für die Postfix-Syntax aus: https://github.com/rust-lang/rust/issues/57640#issuecomment -456143523

Am Ende des Tages muss das lang-Team hier einen Anruf tätigen. Im Interesse der Ermittlung von Gemeinsamkeiten, die zu einem Konsens führen könnten, wäre es möglicherweise hilfreich, die angegebenen Positionen der Mitglieder des Lang-Teams zum Hauptproblem der Postfix-Syntax zusammenzufassen:

  • @Centril sprach sich für die Postfix-Syntax aus und untersuchte eine Reihe von Variationen in diesem Ticket. Insbesondere Centril will await!(expr) nicht stabilisieren.

  • @cramertj drückte seine Unterstützung für die Postfix-Syntax und speziell für die Postfix-Schlüsselwortsyntax expr await .

  • @joshtriplett drückte seine Unterstützung für die Postfix-Syntax aus und schlug vor, auch eine Präfix-Version

  • @scottmcm drückte die Unterstützung für die Postfix-Syntax aus.

  • @withoutboats möchte eine Makrosyntax nicht stabilisieren. Withoutboats ist zwar besorgt über die Erschöpfung unseres "Unbekanntheitsbudgets", betrachtet die Postfix-Keyword-Syntax von expr await jedoch als "einen echten Schuss".

  • @aturon , @eddyb , @nikomatsakis und @pnkfelix haben in den Ausgaben Nr. 57640 oder Nr. 50547 noch keine Position zur Syntax geäußert.

Also die Dinge, die wir brauchen, um eine Ja / Nein-Antwort zu finden:

  • Sollten wir eine Präfixsyntax haben?
  • Sollten wir Postfix-Syntax haben?
  • Sollten wir jetzt eine Präfixsyntax haben und später die Postfixsyntax herausfinden?

Wenn wir uns für die Präfixsyntax entscheiden, gibt es zwei Hauptkonkurrenten, die meiner Meinung nach am meisten akzeptiert werden: Nützlich und offensichtlich, wie in diesem Kommentar zu sehen ist . Die Argumente gegen await!() sind ziemlich stark, daher halte ich es für ausgeschlossen. Wir müssen also herausfinden, ob wir eine nützliche oder offensichtliche Syntax wollen. Meiner Meinung nach sollten wir uns für die Syntax entscheiden, die im Allgemeinen weniger Klammern verwendet.

Und was die Postfix-Syntax betrifft, ist das schwieriger. Es gibt auch starke Argumente für das Postfix await Schlüsselwort mit Leerzeichen. Dies hängt jedoch stark davon ab, wie der Code formatiert wird. Wenn der Code schlecht formatiert ist, kann das Postfix await Schlüsselwort mit Leerzeichen sehr schlecht aussehen. Wir müssen also zunächst davon ausgehen, dass der gesamte Rust-Code korrekt mit rustfmt formatiert wird und dass rustfmt immer verkettete Wartezeiten in verschiedene Zeilen aufteilt, selbst wenn sie in eine einzelne Zeile passen. Wenn wir das können, ist diese Syntax sehr in Ordnung, da sie das Problem der Lesbarkeit und Verwechslung mit Leerzeichen in der Mitte einer einzeiligen Kette löst.

Und schließlich müssen wir, wenn wir sowohl Präfix als auch Postfix verwenden, die Semantik beider Syntaxen herausfinden und aufschreiben, um zu sehen, wie sie miteinander interagieren und wie eine in die andere konvertiert wird. Dies sollte möglich sein, da die Auswahl zwischen Postfix oder Prefix Wait nichts an der Ausführung des Codes ändern sollte.

Um meinen Denkprozess zu klären und wie ich Vorschläge zur Oberflächensyntax herangehe und bewerte, schreiben Sie. await ,
Hier sind die Ziele, die ich habe (ohne Reihenfolge der Wichtigkeit):

  1. await sollte ein Schlüsselwort bleiben, um zukünftiges Sprachdesign zu ermöglichen.
  2. Die Syntax sollte sich erstklassig anfühlen.
  3. Das Warten sollte verkettbar sein, um mit ? und Methoden im Allgemeinen gut zu komponieren, da sie in Rust weit verbreitet sind. Man sollte nicht gezwungen werden, temporäre let -Bindungen herzustellen, die möglicherweise keine sinnvollen Unterteilungen sind.
  4. Es sollte einfach sein, grep auf Punkte zu warten.
  5. Es sollte möglich sein, Wartepunkte auf einen Blick zu sehen.
  6. Der Vorrang der Syntax sollte intuitiv sein.
  7. Die Syntax sollte gut mit IDEs und Muskelgedächtnis zusammenpassen.
  8. Die Syntax sollte leicht zu erlernen sein.
  9. Die Syntax sollte ergonomisch zu schreiben sein.

Nachdem ich einige meiner Ziele definiert (und wahrscheinlich vergessen ...) habe, folgt eine Zusammenfassung und Bewertung einiger Vorschläge in Bezug auf diese:

  1. Das Beibehalten von await als Schlüsselwort erschwert die Verwendung einer makrobasierten Syntax, unabhängig davon, ob es sich um await!(expr) oder expr.await!() . Um eine Makrosyntax zu verwenden, wird await als Makro entweder fest codiert und nicht in die Namensauflösung integriert (dh use core::await as foo; wird unmöglich), oder await wird vollständig als Schlüsselwort aufgegeben.

  2. Ich glaube, dass Makros ein ausgesprochen nicht erstklassiges Gefühl haben, da sie dazu gedacht sind, die Syntax im Benutzerbereich zu erfinden. Obwohl dies kein technischer Punkt ist, vermittelt die Verwendung der Makrosyntax in einem solchen zentralen Konstrukt der Sprache einen unpolierten Eindruck. Die Syntaxen .await und .await() sind wohl auch nicht die erstklassigsten Syntaxen. aber nicht so zweitklassig, wie sich Makros anfühlen würden.

  3. Der Wunsch , die Verkettung zu vereinfachen, führt dazu, dass jede Präfixsyntax schlecht funktioniert, während Postfix-Syntaxen natürlich aus Methodenketten und insbesondere ? .

  4. Grepping ist am einfachsten, wenn das Schlüsselwort await verwendet wird, unabhängig davon, ob es sich um Postfix, Präfix, Makro usw. handelt. Wenn eine Siegel-basierte Syntax verwendet wird, z. B. # , @ , ~ , das Greifen wird schwieriger. Der Unterschied ist nicht groß, aber .await ist etwas einfacher zu finden als await und await da beide möglicherweise als Wörter in Kommentaren enthalten sind, während dies eher unwahrscheinlich ist .await .

  5. Siegel sind auf einen Blick wahrscheinlich schwerer zu erkennen, während await leichter zu erkennen und insbesondere die Syntax hervorgehoben ist. Es wurde vermutet, dass .await oder andere Postfix-Syntaxen auf einen Blick schwerer zu erkennen sind oder dass Postfix await eine schlechte Lesbarkeit bietet. Meiner Ansicht nach stellt @scottmcm jedoch zu Recht fest, dass die Zukunft der Ergebnisse häufig ist und dass .await? beiträgt, zusätzliche Aufmerksamkeit auf sich zu lenken. Darüber hinaus stellt Scott fest, dass das Präfix await zu Gartenpfadsätzen führt und dass das in der Mitte der Ausdrücke erwartete Präfix nicht besser lesbar ist als das Postfix. Mit dem Aufkommen des Operators ? müssen Rust-Programmierer bereits das Zeilenende scannen, um nach Kontrollfluss zu suchen .

  6. Die Rangfolge von .await und .await() ist bemerkenswert vorhersehbar. Sie fungieren als Gegenstücke für den Feldzugriff und den Methodenaufruf. Ein Postfix-Makro hätte vermutlich die gleiche Priorität wie ein Methodenaufruf. In der Zwischenzeit hat das Präfix await entweder eine konsistente, vorhersehbare und nicht nützliche Priorität in Bezug auf ? (dh await (expr?) ) oder die Priorität ist inkonsistent und nützlich (dh (await expr)? ). Ein Siegel kann den gewünschten Vorrang erhalten (z. B. seine Intuition von ? ).

  7. Im Jahr 2012 bemerkte @nikomatsakis , dass Simon Peyton Jones einmal bemerkte (S. 56), dass er neidisch auf die "Kraft des Punktes" ist und wie sie IDE-Magie bietet, wodurch Sie die Funktion oder das Feld, das Sie meinen, eingrenzen können. Da die Kraft des Punktes in vielen gängigen Sprachen (z. B. Java, C #, C ++, ..) vorhanden ist, hat dies dazu geführt, dass das "Greifen nach dem Punkt" im Muskelgedächtnis verankert ist. Um zu veranschaulichen, wie mächtig diese Gewohnheit ist, sehen Sie hier einen Screenshot von Visual Studio mit Re # er aufgrund von @scottmcm :
    Re#er intellisense

    In C # wartet kein Postfix. Dies ist jedoch so nützlich, dass es in einer Autocomplete-Liste angezeigt wird, ohne dass eine gültige Syntax vorliegt. Es könnte jedoch vielen, einschließlich mir, nicht in den Sinn kommen, .aw zu versuchen, wenn .await nicht die Oberflächensyntax ist. Betrachten Sie den Vorteil der IDE-Erfahrung, wenn dies der Fall wäre. Die Syntaxen await expr und expr await bieten diesen Vorteil nicht.

  8. Siegel würden wahrscheinlich eine schlechte Vertrautheit bieten. Das Präfix await profitiert von der Vertrautheit mit C #, JS usw. Diese trennen jedoch await und ? in verschiedene Operationen, während Rust dies tut. Es ist auch nicht schwer, von await expr auf expr.await wechseln - die Änderung ist nicht so radikal wie mit einem Siegel. Darüber hinaus ist es aufgrund der oben genannten Punktstärke wahrscheinlich, dass .await gelernt wird, indem Sie expr. eingeben und await als erste Option im Popup für die automatische Vervollständigung sehen.

  9. Siegel sind leicht zu schreiben und bieten eine gute Ergonomie. Während .await länger zu tippen ist, verfügt es auch über Punkt-Potenzen, die den Schreibfluss noch besser machen können. Wenn Sie nicht in let -Anweisungen einbrechen müssen, wird auch die Ergonomie verbessert. Das Präfix await oder noch schlimmer await!(..) fehlt sowohl an Punktkräften als auch an Verkettungsfähigkeiten. Beim Vergleich von .await , .await() und .await!() sind die ersten Angebote am knappsten.

Da keine Syntax am besten geeignet ist, alle Ziele gleichzeitig zu erreichen, muss ein Kompromiss geschlossen werden. Für mich ist die Syntax mit den meisten Vorteilen und den wenigsten Nachteilen .await . Insbesondere kann es verkettet werden, behält await als Schlüsselwort bei, ist greifbar, hat Punktkräfte und ist daher lernbar und ergonomisch, hat offensichtliche Priorität und ist schließlich lesbar (insbesondere bei guter Formatierung und Hervorhebung).

@Centril Nur um ein paar Fragen zu klären. Zunächst möchte ich nur bestätigen, dass nur Postfix warten soll, oder? Zweitens, wie kann man sich der Dualität von .await als Feldzugriff nähern? Wäre es in Ordnung, .await so zu haben, oder sollte .await() in Klammern bevorzugt werden, um zu implizieren, dass dort eine Operation stattfindet? Und drittens, mit der Syntax .await , würde das das Schlüsselwort oder nur eine "Feldzugriff" -Kennung sein?

Zunächst möchte ich nur bestätigen, dass nur Postfix warten soll, oder?

Ja. Zumindest jetzt.

Zweitens, wie kann man sich der Dualität von .await als Feldzugriff nähern?

Es ist ein kleiner Nachteil; Es gibt einen großen Vorteil in der Kraft des Punktes. Ich glaube, dass die Unterscheidung Benutzer schnell lernen werden, insbesondere da das Schlüsselwort hervorgehoben ist und das Konstrukt häufig verwendet wird. Darüber hinaus wird, wie Scott bemerkte, .await? am häufigsten vorkommen, was die Situation weiter lindern sollte. Es sollte auch einfach sein, einen neuen Keyword-Eintrag für await in rustdoc hinzuzufügen, ähnlich wie wir es für fn getan haben.

Wäre es in Ordnung, .await so zu haben, oder sollte .await() in Klammern bevorzugt werden, um zu implizieren, dass dort eine Operation stattfindet?

Ich bevorzuge .await ohne Schwanz () ; () am Ende zu haben, scheint in den meisten Fällen unnötiges Salz zu sein und würde, wie ich vermute, die Benutzer eher dazu neigen, nach der Methode zu suchen.

Und drittens, mit der Syntax .await , würde das das Schlüsselwort oder nur eine "Feldzugriff" -Kennung sein?

await bleibt ein Schlüsselwort. Vermutlich würden Sie libsyntax so ändern, dass es nicht zu Fehlern kommt, wenn Sie nach . auf await stoßen und es dann entweder in AST oder beim Absenken auf HIR anders darstellen ... aber das ist meistens ein Implementierungsdetail .

Danke, dass du das alles geklärt hast! 👍

An diesem Punkt scheint die Schlüsselfrage zu sein, ob eine Postfix-Syntax verfolgt werden soll. Wenn das die Richtung ist, dann können wir sicherlich einschränken, auf welche. Beim Lesen des Raums würden die meisten Befürworter der Postfix-Syntax jede vernünftige Postfix-Syntax gegenüber der Präfix-Syntax akzeptieren.

In diesem Thread schien die Postfix-Syntax der Außenseiter zu sein. Es hat jedoch die klare Unterstützung von vier lang-Teammitgliedern und die Offenheit von einem fünften angezogen (die anderen vier haben bisher geschwiegen). Darüber hinaus hat es beträchtliche Unterstützung von der breiteren Community erhalten, die hier Kommentare abgibt.

Darüber hinaus scheinen die Befürworter der Postfix-Syntax klar davon überzeugt zu sein, dass dies der beste Weg für Rust ist. Es scheint, dass ein tiefgreifendes Problem oder überzeugende Widerlegungen zu den bisher vorgebrachten Argumenten notwendig wären, um diese Unterstützung zurückzudrehen.

Angesichts dessen scheinen wir von @withoutboats , der diesen Thread sicherlich genau verfolgt hat, und von den anderen vier lang-Teammitgliedern zu hören. Ihre Ansichten werden diesen Thread wahrscheinlich antreiben. Wenn sie Bedenken haben, wäre die Erörterung dieser Bedenken die Priorität. Wenn sie von der Postfix-Syntax überzeugt sind, können wir einen Konsens für welche finden.

Hoppla, ich habe gerade bemerkt, dass Gründen der Sauberkeit verstecken.

@ivandardi Angesichts des Ziels (1) aus dem Beitrag ist mein Verständnis, dass Warten immer noch ein Schlüsselwort ist, also niemals ein Feldzugriff, genauso wie loop {} niemals ein Strukturliteralausdruck ist. Und ich würde erwarten, dass es hervorgehoben wird, um es offensichtlicher zu machen (wie https://github.com/rust-lang/rust-enhanced/issues/333 in Sublime darauf wartet).

Meine einzige Sorge ist, dass das Warten auf die Syntax "Warten" wie ein anderer Zugriff möglicherweise zu Verwirrung führen kann, wenn in einer Codebasis der Edition 2015 ohne es zu merken. (2015 ist die Standardeinstellung, wenn nicht angegeben, das Projekt eines anderen geöffnet wird usw.) Solange die Ausgabe 2015 einen eindeutigen Fehler aufweist, wenn kein await -Feld vorhanden ist (und eine Warnung, falls vorhanden), denke ich Dies (zusammen mit Centrils wunderbarem Argument) beseitigt meine persönlichen Sorgen über ein Keyword-Feld (oder eine Methode).

Oh, und eine Sache, die wir auch entscheiden sollten, während wir dabei sind, ist, wie rustfmt es am Ende formatieren würde.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Verwendet await - check
  2. Erstklassig - prüfen
  3. Verkettung - prüfen
  4. Grepping - überprüfen
  5. Nicht Siegel - überprüfen
  6. Vorrang - ¹check
  7. Punktleistung - ²check
  8. Leicht zu erlernen - ³check
  9. Ergonomisch - prüfen

¹Vorrang: Leerzeichen nach await machen es in der zurückgestellten Position nicht offensichtlich. Es ist jedoch genau das gleiche wie im folgenden Code: client.get("https://my_api").await_send()?.await_json()? . Für alle Englisch sprechenden Personen ist dies noch natürlicher als bei allen anderen Vorschlägen

²Dot Power: Es würde zusätzliche Unterstützung für IDE erfordern, um await links vom Methodenaufruf zu verschieben, nachdem als nächstes . , ? oder ; eingegeben wurden. Nicht zu schwer zu implementieren scheint

³Einfach zu lernen: In der Präfixposition ist es Programmierern bereits bekannt. In verzögerter Position wäre es offensichtlich, nachdem diese Syntax stabilisiert wurde


Das Stärkste an dieser Syntax ist jedoch, dass sie sehr konsistent ist:

  • Es kann nicht mit dem Zugang zu Immobilien verwechselt werden
  • Es hat offensichtliche Priorität mit ?
  • Es wird immer die gleiche Formatierung haben
  • Es entspricht der menschlichen Sprache
  • Das variable horizontale Auftreten in der Methodenaufrufkette * ist davon nicht betroffen

* Erklärung ist unter Spoiler versteckt


Mit der Postfix-Variante ist es schwer vorherzusagen, welche Funktion async und wo das nächste Auftreten von await auf der horizontalen Achse auftreten würde:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

Bei einer verzögerten Variante wäre es fast immer dasselbe:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

Wäre da nicht ein. zwischen dem Warten und dem nächsten Methodenaufruf? Mögen
get().await?.json() .

Am Mittwoch, 23. Januar 2019, 05:06 Uhr schrieb I60R < [email protected] :

lass val = auf die Zukunft warten;
let res = client.get ("https: // my_api") .await send ()?. warte auf json ()?;

  1. Verwendungen warten - prüfen
  2. Erstklassig - prüfen
  3. Verkettung - prüfen
  4. Grepping - überprüfen
  5. Nicht Siegel - überprüfen
  6. Vorrang - ¹check
  7. Punktleistung - ²check
  8. Leicht zu erlernen - ³check
  9. Ergonomisch - prüfen

¹Vorrang: Der Raum macht es in der zurückgestellten Position nicht offensichtlich. Wie auch immer
genau das gleiche wie im folgenden Code: client.get ("https: // my_api
") .await_send ()?. await_json ()?. Für alle Englisch sprechenden ist es noch mehr
natürlich als bei allen anderen Vorschlägen

²Dot Power: Es würde zusätzliche Unterstützung für IDE erfordern, um auf etwas zu warten
links vom Methodenaufruf nach. oder ? wird als nächstes eingegeben. Scheint nicht zu sein
schwer zu implementieren

³Einfach zu lernen: In der Präfixposition ist es Programmierern bereits bekannt,
und in verzögerter Position wäre es offensichtlich, nachdem diese Syntax war

stabilisiert

Das Stärkste an dieser Syntax ist jedoch, dass es sehr sein wird
konsistent:

  • Es kann nicht mit dem Zugang zu Immobilien verwechselt werden
  • Es hat offensichtlichen Vorrang mit?
  • Es wird immer die gleiche Formatierung haben
  • Es entspricht der menschlichen Sprache
  • Das variable horizontale Auftreten im Methodenaufruf ist davon nicht betroffen
    Kette*

* Erklärung ist unter Spoiler versteckt

Mit der Postfix-Variante ist es schwer vorherzusagen, welche Funktion asynchron ist und
Wo würde das nächste Mal auf der horizontalen Achse warten:

let res = client.get ("https: // my_api")

.very_long_method_name(param, param, param).await?

.short().await?;

Bei einer verzögerten Variante wäre es fast immer dasselbe:

let res = client.get ("https: // my_api")

.await very_long_method_name(param, param, param)?

.await short()?;

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

Wir sollten auch entscheiden, wie rustfmt es formatieren würde

Das ist die Domain von https://github.com/rust-dev-tools/fmt-rfcs , daher würde ich sagen, dass dies für dieses Problem kein Thema ist. Ich bin mir sicher, dass es etwas geben wird, das mit der gewählten Fixierung und Priorität übereinstimmt.

@ I60R

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

Wie würde das für freie Funktionen funktionieren? Wenn ich es richtig lese, ist es tatsächlich ein Präfix await .

Ich denke, dies zeigt, dass wir die Erfahrung des C # -Sprachenteams nicht so schnell diskreditieren sollten. Microsoft hat viel über Usability-Studien nachgedacht, und wir treffen hier eine Entscheidung, die auf einer einzigen Codezeile und der Prämisse basiert, dass Rust speziell genug ist, um eine andere Syntax als alle anderen Sprachen zu haben. Ich bin mit dieser Prämisse nicht einverstanden - ich habe oben erwähnt, dass LINQ und Erweiterungsmethoden in C # allgegenwärtig sind.

Es könnte jedoch vielen, einschließlich mir, nicht in den Sinn kommen, .aw zu versuchen, wenn .await nicht die Oberflächensyntax ist.

~ Ich glaube nicht, dass es jemandem einfällt, der mit den anderen Sprachen vertraut ist. ~ Abgesehen davon würden Sie erwarten, dass das Popup-Fenster für die Fertigstellung sowohl die Methoden await als auch die Methoden Future ?

[...] und wir treffen hier eine Entscheidung basierend auf einer einzelnen Codezeile und der Prämisse, dass Rust speziell genug ist, um eine andere Syntax als alle anderen Sprachen zu haben. Ich bin mit dieser Prämisse nicht einverstanden [...]

@lnicola Ich möchte nur darauf hinweisen, falls Sie und andere es verpasst haben (es ist leicht in der Flut von Kommentaren zu übersehen): https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

Einige reale Beispiele aus dem tatsächlichen Produktionscode.

Angesichts des obigen Kommentars von @Centril scheinen die am stärksten umstrittenen Postfix-Optionen expr await Postfix-Schlüsselwortsyntax und expr.await Postfix-Feldsyntax zu sein (und möglicherweise ist die Postfix-Methodensyntax noch nicht verfügbar).

Im Vergleich zum Postfix-Schlüsselwort argumentiert @Centril , dass das Postfix-Feld von der "Kraft des Punkts" profitiert - dass IDEs es möglicherweise besser als automatische Vervollständigung vorschlagen können - und dass Instanzen seiner Verwendung mit grep möglicherweise leichter zu finden sind, weil Der führende Punkt unterscheidet sich von der Verwendung des Wortes in Kommentaren.

Umgekehrt hat sich @cramertj für die Postfix-Schlüsselwortsyntax , da diese Syntax deutlich macht, dass es sich nicht um einen Methodenaufruf oder einen Feldzugriff handelt . @withoutboats hat argumentiert, dass das Postfix-Schlüsselwort die bekannteste der Postfix-Optionen ist.

In Bezug auf "die Kraft des Punkts" sollten wir berücksichtigen, dass eine IDE nach einem Punkt immer noch await als Vervollständigung anbieten und dann den Punkt einfach entfernen kann, wenn die Vervollständigung ausgewählt ist. Eine ausreichend intelligente IDE (z. B. mit RLS) könnte sogar die Zukunft bemerken und nach einem Leerzeichen await anbieten.

Das Postfix-Feld und die Methodenoptionen scheinen aufgrund der Mehrdeutigkeit im Vergleich zum Postfix-Schlüsselwort die größere Oberfläche für Einwände zu bieten. Da ein Teil der Unterstützung für das Postfix-Feld / die Postfix-Methode über das Postfix-Schlüsselwort auf ein leichtes Unbehagen mit dem Vorhandensein des Leerzeichens in der Methodenverkettung zurückzuführen zu sein scheint, sollten wir @valffs Beobachtung dieses Postfix-Schlüsselworts ( foo.bar() await überprüfen und darauf eingehen foo.bar() await ) wird nicht überraschender aussehen als die allgemeine Typzuordnung ( foo.bar() : Result<Vec<_>, _> ). Für beide setzt sich die Verkettung nach diesem durch Leerzeichen getrennten Zwischenspiel fort.

@ivandardi , @lnicola ,

Ich habe meinen letzten Kommentar mit einem fehlenden Beispiel für einen kostenlosen Funktionsaufruf aktualisiert. Wenn Sie weitere Beispiele wünschen, können Sie sich auf den letzten Spoiler in meinem vorherigen Kommentar beziehen

Im Interesse der Debatte über future.await? vs. future await? ...

Etwas, das nicht zu viel diskutiert wird, ist das visuelle Gruppieren oder Greifen einer Methodenkette.

Betrachten Sie ( match anstelle von await für die Syntaxhervorhebung)

post(url).multipart(form).send().match?.error_for_status()?.json().match?

verglichen mit

post(url).multipart(form).send() match?.error_for_status()?.json() match?

Wenn ich die erste Kette visuell nach await scanne, identifiziere ich send().await? klar und schnell und sehe, dass wir auf das Ergebnis der send() warten.

Wenn ich jedoch die zweite Kette visuell nach await scanne, sehe ich zuerst await?.error_for_status() und muss gehen, nein, sichern und dann send() await miteinander verbinden.


Ja, einige dieser Argumente gelten für die allgemeine Typzuweisung, dies ist jedoch noch keine akzeptierte Funktion. Während ich die Typzuschreibung in gewissem Sinne mag, denke ich, dass es in einem allgemeinen Ausdruckskontext (vage, ich weiß) erforderlich sein sollte, in Parens eingewickelt zu werden. Für diese Diskussion ist das jedoch alles nicht zum Thema. Denken Sie auch daran, dass der Betrag von await in einer Codebasis eine hohe Änderung aufweist, die deutlich höher ist als der Betrag der Typzuweisung.

Die verallgemeinerte Typzuweisung bindet den Typ auch an den zugeschriebenen Ausdruck, indem ein eindeutiger Operator : . Genau wie bei anderen binären Operatoren macht das Lesen (visuell) deutlich, dass die beiden Ausdrücke ein einziger Baum sind. Während future await keinen Operator hat und leicht mit future_await verwechselt werden kann, weil man selten zwei Namen sieht, die nicht durch einen Operator getrennt sind, außer wenn der erste ein Schlüsselwort ist (Ausnahmen gelten, das ist nicht der Fall) sagen, dass ich Präfixsyntax bevorzuge, ich nicht).

Vergleichen Sie dies mit Englisch, wenn Sie so wollen, wobei ein Bindestrich ( - ) verwendet wird, um Wörter visuell zu gruppieren, die sonst leicht als getrennt interpretiert werden könnten.

Ich denke, dies zeigt, dass wir die Erfahrung des C # -Sprachenteams nicht so schnell diskreditieren sollten.

Ich denke nicht, dass "so schnell" und "diskreditiert" hier fair sind. Ich denke, dass hier dasselbe passiert, was im asynchronen / erwarteten RFC selbst passiert ist: sorgfältig überlegen, warum es in eine Richtung gemacht wurde, und herauszufinden, ob das Gleichgewicht für uns auf die gleiche Weise herauskommt. Das C # -Team wird sogar ausdrücklich in einem erwähnt:

Ich dachte, dass die Idee von await war, dass Sie tatsächlich das Ergebnis und nicht die Zukunft wollten
Vielleicht sollte die Warten-Syntax eher der Ref-Syntax entsprechen

let future = task()
aber
let await result = task()

Zum Verketten sollten Sie also beides tun

task().chained_method(|future| { /* do something with future */ })

aber

task().chained_method(|await result| { /* I've got the result */ })
- foo.await             // NOT a real field
- foo.await()           // NOT a real method
- foo.await!()          // NOT a real macro

Sie alle funktionieren gut mit Verkettung und alle haben Nachteile, die kein echtes Feld / Methode / Makro sind.
Aber da await als Schlüsselwort bereits etwas Besonderes ist, müssen wir es nicht spezieller gestalten.
Wir sollten nur das einfachste auswählen, foo.await . Sowohl () als auch !() sind hier überflüssig.

@liigo

- foo await        // IS neither field/method/macro, 
                   // and clearly seen as awaited thing. May be easily chained. 
                   // Allow you to easily spot all async spots.

@mehcode

Wenn ich die erste Kette visuell auf Warten scanne, identifiziere ich send () eindeutig und schnell. Warten? und sehen, dass wir auf das Ergebnis von send () warten.

Ich mag die Version mit Abstand mehr, da es viel einfacher ist zu sehen, wo die Zukunft aufgebaut ist und wo sie erwartet wird. Es ist viel einfacher zu sehen, was passiert, IMHO

image

Mit der Punkttrennung sieht es sehr nach einem Methodenaufruf aus. Das Hervorheben von Code hilft nicht viel und ist nicht immer verfügbar.

Schließlich glaube ich, dass Verkettung nicht der Hauptanwendungsfall ist (außer ? , aber foo await? ist so klar wie möglich), und mit einmaligem Warten wird es

post(url).multipart(form).send().match?

vs.

post(url).multipart(form).send() match?

Wo letzteres imo viel schlanker aussieht.

Wenn wir uns also auf future await und future.await , können wir dann einfach den magischen "Punkt" optional machen? Damit die Leute wählen können, welche sie wollen, und vor allem aufhören zu streiten und vorwärts gehen!

Es ist nicht schädlich, eine optionale Syntax zu haben. Rust hat viele solcher Beispiele. Der bekannteste ist der letzte Sperator ( ; oder , oder was auch immer, nach dem letzten Element, wie in (A,B,) ), der meistens optional ist. Ich habe keinen starken Grund dafür gesehen, warum wir den Punkt nicht optional machen können.

Ich denke, es wäre ein guter Zeitpunkt, dies einfach in Nightly zu tun und das Ökosystem entscheiden zu lassen, welches für sie das beste ist. Wir können Lints haben, um den bevorzugten Stil durchzusetzen, macht ihn anpassbar.

Bevor wir in Stable landen, überprüfen wir die Community-Nutzung und entscheiden, ob wir eine auswählen oder nur beide verlassen sollen.

Ich bin völlig anderer Meinung als die Vorstellung, dass Makros sich für diese Funktion nicht erstklassig genug anfühlen würden (ich würde sie überhaupt nicht als solche Kernfunktion betrachten), und möchte meinen vorherigen Kommentar zu await!() erneut hervorheben

Auf jeden Fall hat das Sperren dieses Threads für einen Tag nur sehr geholfen und ich werde mich jetzt abmelden. Sie können mich gerne erwähnen, wenn Sie direkt auf einen meiner Punkte antworten.

Mein Argument gegen die Syntax mit . ist, dass await kein Feld ist und daher nicht wie ein Feld aussehen sollte. Selbst mit Syntaxhervorhebung und fetter Schrift sieht es wie ein spezielles Feld aus , und selbst bei der benutzerdefinierten Formatierung kann nicht betont werden, dass es sich nicht um ein Feld handelt, da . gefolgt von einem Wort stark mit dem Feldzugriff verbunden ist.

Es gibt auch keinen Grund, die Syntax mit . für eine bessere IDE-Integration zu verwenden, da wir jede andere Syntax mit . Vervollständigung unterstützen könnten, indem wir die in Intellij IDEA verfügbare Postfix-Vervollständigungsfunktion verwenden .

Wir müssen await als Kontrollfluss-Schlüsselwort wahrnehmen. Wenn wir uns nachdrücklich für Postfix await , sollten wir eine Variante ohne . Betracht ziehen, und ich schlage vor, die folgende Formatierung zu verwenden, um Blockierungspunkte mit möglicher Unterbrechung besser hervorzuheben:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        await?.res.json::<UserResponse>()
        await?.user
        .into();
    Ok(user)
}

Mehr Beispiele

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    await?.error_for_status()?;

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

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

Mit Syntaxhervorhebung

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)
        yield?.res.json::<UserResponse>()
        yield?.user
        .into();
    Ok(user)
}

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()) 
    yield? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)
    yield? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
    yield?.error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()
    yield?.error_for_status()?
    .json()
    yield?;

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


Dies könnte auch eine Antwort auf den @ ivandardi- Punkt zur Formatierung sein

Ich finde, all diese Beispiele für Postfix warten darauf, mehr dagegen als dagegen zu sein. Lange Warteketten fühlen sich falsch an, und die Verkettung sollte an zukünftige Kombinatoren beibehalten werden.

Vergleichen Sie (mit dem match als await Ding zur Syntaxhervorhebung):

post(url)?.multipart(form).send()?.match?.error_for_status()?.json().match?

zu

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

Beachten Sie den Mangel an Wartezeiten in der Mitte? Weil Verkettungen normalerweise durch ein besseres API-Design gelöst werden können. Wenn send() eine benutzerdefinierte Zukunft mit zusätzlichen Methoden zurückgibt, z. B. um Nicht-200-Statusmeldungen in harte Fehler zu konvertieren, sind keine zusätzlichen Wartezeiten erforderlich. Im Moment, zumindest in reqwest , scheint es an Ort und Stelle zu laufen und ein einfaches Result anstatt eine neue Zukunft, aus der das Problem hier stammt.

Das Warten auf Verketten ist meiner Meinung nach ein starker Code-Geruch , und .await oder .await() werden so viele neue Benutzer verwirren, dass es nicht einmal lustig ist. .await fühlt sich besonders wie etwas aus Python oder PHP an, wo variable Mitgliederzugriffe auf magische Weise ein besonderes Verhalten haben können.

await sollte von vornherein offensichtlich sein, als würde man sagen: "Bevor wir fortfahren, warten wir auf dieses" Nicht "und dann warten wir darauf". Letzteres ist buchstäblich der and_then Kombinator.

Eine andere Möglichkeit, darüber nachzudenken, besteht darin, async / await Ausdrücke und Futures als Iteratoren zu betrachten. Sie sind beide faul, können viele Zustände haben und können mit Syntax beendet werden ( for-in für Iteratoren, await für Futures). Wir würden keine Syntaxerweiterung für Iteratoren vorschlagen, um sie als reguläre Ausdrücke wie hier zu verketten, oder? Sie verwenden Kombinatoren zum Verketten, und asynchrone Operationen / Futures sollten dasselbe tun und nur einmal beenden. Es klappt ziemlich gut.

Schließlich und vor allem möchte ich in der Lage sein, die ersten Einrückungen einer Funktion vertikal zu überfliegen und klar zu sehen, wo asynchrone Operationen stattfinden. Meine Vision ist nicht so großartig, und am Ende der Linie zu warten oder

Bitte lesen Sie diesen Blog-Beitrag, bevor Sie ihn kommentieren.

Beim Async / Warten geht es nicht um Bequemlichkeit. Es geht nicht darum, Kombinatoren zu vermeiden.

Es geht darum , neue Dinge zu ermöglichen , die nicht mit combinators getan werden

Daher sind alle Vorschläge für "Verwenden wir stattdessen Kombinatoren" nicht zum Thema und sollten an einer anderen Stelle diskutiert werden.

Beachten Sie den Mangel an Wartezeiten in der Mitte? Weil Verkettungen normalerweise durch ein besseres API-Design gelöst werden können.

In einem echten asynchronen Beispiel erhalten Sie keine hübsche Kette. Echtes Beispiel:

req.into_body().concat2().and_then(move |chunk| {
    from_slice::<Update>(chunk.as_ref())
        .into_future()
        .map_err(|_| {
            ...
        })
        .and_then(|update| {
            ...
        })
        .and_then(move |(user, file_id, chat_id, message_id)| {
            do_thing(&file_id)
                .and_then(move |file| {
                    if some_cond {
                        Either::A(do_yet_another-thing.and_then(move |bytes| {
                            ...

                            if another_cond {
                                ...
                                Either::A(
                                    do_other_thing()
                                        .then(move |res| {
                                            ...
                                        }),
                                )
                            } else {
                                Either::B(future::ok(()))
                            }
                        }))
                    } else {
                        Either::B(future::ok(()))
                    }
                })
                .map_err(|e| {
                    ...
                })
        })
        // ...and here we unify both paths
        .map(|_| {
            Response::new(Body::empty())
        })
        .or_else(Ok)
})

Kombinatoren helfen hier nicht. Das Verschachteln kann entfernt werden, ist aber nicht einfach (ich habe es zweimal versucht und bin fehlgeschlagen). Sie haben am Ende Tonnen von Either:A(Either:A(Either:A(...))) .

OTOH ist mit async / await leicht zu lösen .

@Pauan hat es geschrieben, bevor ich meinen Beitrag beendet habe. So sei es. Erwähnen Sie keine Kombinatoren, async/await sollte nicht ähnlich aussehen. Es ist in Ordnung, wenn dies der Fall ist, gibt es wichtigere Dinge zu beachten.


Das Abstimmen meines Kommentars macht Kombinatoren nicht bequemer.

Beim Lesen des Typzuschreibungskommentars dachte ich darüber nach, wie das in Kombination mit await aussehen würde. Angesichts der Reservierung, die einige Leute mit .await oder .await() als verwirrendem Zugriff auf Mitgliederfelder / Methoden haben, frage ich mich, ob eine allgemeine "Zuschreibungs" -Syntax eine Option sein könnte (dh "gib mir was await zurück ").

Verwenden von yield für await zum Hervorheben der Syntax

let result = connection.fetch("url"):yield?.collect_async():yield?;

Kombiniert mit Typzuschreibung, zB:

let result = connection.fetch("url") : yield?
    .collect_async() : yield Vec<u64>?;

Vorteile: sieht immer noch gut aus (IMHO), andere Syntax als Feld- / Methodenzugriff.
Nachteile: mögliche Konflikte mit der Typzuordnung (?), Mehrfachzuweisungen theoretisch möglich:

foo():yield:yield

Bearbeiten: Mir ist aufgefallen, dass die Verwendung von 2 Zuschreibungen im kombinierten Beispiel logischer wäre:

let result = connection.fetch("url") : yield?
    .collect_async() : yield : Vec<u64>?;

@Pauan es wäre unangemessen, diesen Blog-Beitrag zu überbewerten. Kombinatoren können in Verbindung mit der Warte-Syntax angewendet werden, um die darin beschriebenen Probleme zu vermeiden.

Das Kernproblem war immer, dass eine spawbare Zukunft 'static , aber wenn Sie Variablen durch Referenz in diesen Kombinatoren erfasst haben, haben Sie eine Zukunft erhalten, die nicht 'static . Das Warten auf eine Zukunft, die nicht 'static jedoch nicht dazu, dass Sie sich nicht in 'static . Sie können also Folgendes tun:

`` `Rost
// Slice: & [T]

sei x = warte auf future.and_then (| i | wenn Slice.contains (i) {async_fn (Slice)});

Ich hatte vor, überhaupt keine Kommentare abzugeben, aber ich wollte einige Notizen von jemandem hinzufügen, der nicht viel asynchronen Code schreibt, ihn aber trotzdem lesen muss:

  • Das Präfix await ist viel leichter zu bemerken. Dies ist sowohl, wenn Sie es erwarten (innerhalb eines async fn) als auch außerhalb (im Ausdruck eines Makrokörpers).
  • Postfix await als Keyword kann in Ketten mit wenig bis gar keinen anderen Keywords gut zur Geltung kommen. Sobald Sie jedoch Schließungen oder ähnliches mit anderen Kontrollstrukturen haben, werden Ihre awaits viel weniger auffällig und leichter zu übersehen.
  • Ich finde .await als Pseudofeld sehr seltsam. Es ist in keiner Weise wie jedes andere Gebiet, das mir wichtig ist.
  • Ich bin mir nicht sicher, ob es eine gute Idee ist, sich auf die IDE-Erkennung und die Hervorhebung der Syntax zu verlassen. Die Lesbarkeit ist von Vorteil, wenn keine Syntaxhervorhebung verfügbar ist. Angesichts der jüngsten Diskussionen über die Komplizierung der Grammatik und die damit verbundenen Probleme beim Werkzeugbau ist es möglicherweise keine gute Idee, sich darauf zu verlassen, dass IDEs / Editoren die Dinge richtig machen.

Ich teile nicht die Meinung, dass await!() als Makro "sich nicht erstklassig anfühlt". Makros sind in Rust ein absolut erstklassiger Bürger . Sie sind auch nicht nur zum "Erfinden der Syntax im Benutzercode" da. Viele "eingebaute" Makros sind nicht benutzerdefiniert und aus rein syntaktischen Gründen nicht vorhanden. format!() gibt dem Compiler die Möglichkeit, Formatzeichenfolgen statisch zu überprüfen. include_bytes!() ebenfalls nicht benutzerdefiniert und aus rein syntaktischen Gründen nicht vorhanden. Es handelt sich um ein Makro, da es sich um eine Syntaxerweiterung handeln muss, da es wiederum spezielle Funktionen im Compiler ausführt und dies nicht konnte vernünftigerweise auf andere Weise geschrieben werden, ohne es als Kernsprache hinzuzufügen.

Und das ist eine Art von meinem Punkt: Makros sind eine perfekte Möglichkeit, Compiler-Magie auf einheitliche und nicht überraschende Weise einzuführen und zu verbergen

Daher sehe ich überhaupt keine Notwendigkeit für ein spezielles Keyword. Das heißt, wenn es ein Schlüsselwort sein soll, dann sollte es genau das sein, ein nacktes Schlüsselwort - ich verstehe die Motivation, es als Feld zu tarnen, nicht wirklich. Das scheint einfach falsch zu sein, es ist kein Feldzugriff, es ist nicht einmal aus der Ferne wie ein Feldzugriff (im Gegensatz zu beispielsweise Getter / Setter-Methoden mit Zucker), so dass die Behandlung als eine Methode nicht mit der heutigen Funktionsweise von Feldern in der Sprache vereinbar ist.

@ H2CO3 await! Makro ist in jeder Hinsicht gut, mit Ausnahme von zwei Eigenschaften:

  1. Zusätzliche Klammern sind wirklich ärgerlich, wenn Sie noch tausend weitere Wartezeiten schreiben. Aus dem gleichen Grund entfernte Rust Klammern aus if/while/... -Blöcken. Es mag unwichtig aussehen, aber es ist wirklich der Fall
  2. Asymmetrie mit async als Schlüsselwort. Async sollte etwas Größeres tun, als nur ein Makro im Methodenkörper zuzulassen. Andernfalls könnte es sich nur um ein #[async] -Attribut über der Funktion handeln. Ich verstehe, dass dieses Attribut es Ihnen nicht erlaubt, es in Blöcken usw. zu verwenden, aber es gibt eine gewisse Erwartung, auf das Schlüsselwort await stoßen. Diese Erwartung kann durch Erfahrung in anderen Sprachen verursacht werden, wer weiß, aber ich glaube fest daran, dass es sie gibt. Es kann nicht angesprochen werden, aber es muss berücksichtigt werden.

Andernfalls könnte es sich einfach um ein #[async] -Attribut über der Funktion handeln.

Es war lange Zeit so und es tut mir leid, dass es so weit ist. Wir führen ein weiteres Schlüsselwort ein, während das Attribut #[async] einwandfrei funktioniert hat und jetzt mit stabilen Attribut-ähnlichen prozeduralen Makros sogar syntaktisch und semantisch mit dem Rest der Sprache konsistent ist, selbst wenn es noch bekannt wäre (und speziell behandelt) vom Compiler.

@ H2CO3 Was ist der Zweck oder Vorteil von await als Makro aus Anwendersicht ? Gibt es eingebaute Makros, die den Programmfluss unter der Haube ändern?

@ I60R Der Vorteil ist die Einheitlichkeit mit dem Rest der Sprache, sodass Sie sich nicht um ein weiteres Schlüsselwort kümmern müssen, mit all dem Gepäck, das es mit sich bringen würde - z. B. Vorrang und Gruppierung sind bei einem Makroaufruf offensichtlich.

Ich verstehe nicht, warum der zweite Teil relevant ist - kann es kein eingebautes Makro geben, das etwas Neues bewirkt? Tatsächlich denke ich, dass so ziemlich jedes eingebaute Makro etwas "Seltsames" macht, das ohne Compiler-Unterstützung nicht (vernünftig / effizient / etc.) Erreicht werden kann. await!() wäre in dieser Hinsicht kein neuer Eindringling.

Ich habe zuvor implizites Warten vorgeschlagen und in jüngerer Zeit

Die Community scheint sehr stark für ein explizites Warten zu sein (ich würde es lieben, wenn sich das ändert, aber ...). Die Granularität der Explizität ist jedoch das, was Boilerplate erzeugt (dh wenn es im selben Ausdruck viele Male benötigt wird, scheint Boilerplate zu entstehen).

[ Warnung : Der folgende Vorschlag wird kontrovers sein, aber bitte geben Sie ihm eine aufrichtige Chance]

Ich bin gespannt, ob der Großteil der Community Kompromisse eingehen und "teilweise implizites Warten" zulassen würde. Vielleicht ist es explizit genug, einmal pro Ausdruckskette zu lesen?

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

Dies ist so etwas wie eine Wertdekonstruktion, die ich Ausdrucksdestrukturierung nenne.

// Value de-structuring
let (a, b, c) = ...;

// De-sugars to:
let _abc = ...;
let a = _abc.0;
let b = _abc.1;
let c = _abc.2;
// Expression de-structuring
await response = client.get("https://my_api").send()?.json()?;

// De-sugards to:
// Using: prefix await with explicit precedence
// Since send() and json() return impl Future but ? does not expect a Future, de-structure the expression between those sub-expressions.
let response = await (client.get("https://my_api").send());
let response = await (response?.json());
let response = response?;



md5-eeacf588eb86592ac280cf8c372ef434



```rust
// If needed, given that the compiler knows these expressions creating a, b are independent,
// each of the expressions would be de-structured independently.
await (a, b) = (
    client.get("https://my_api_a").send()?.json()?,
    client.get("https://my_api_b").send()?.json()?,
);
// De-sugars to:
// Not using await syntax like the other examples since await can't currently let us do concurrent polling.
let a: impl Future = client.get("https://my_api_a").send();
let b: impl Future = client.get("https://my_api_b").send();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;

let a: impl Future = a?.json();
let b: impl Future = b?.json();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;
let (a, b) = (a?, b?);

Ich würde mich auch über andere Implementierungen der Kernidee freuen: "Das einmalige Lesen pro Ausdruckskette ist wahrscheinlich explizit genug". Weil es die Boilerplate reduziert, aber auch die Verkettung mit einer Präfix-basierten Syntax ermöglicht, die jedem vertraut ist.

@yazaddaruvala Es gab eine Warnung oben, also sei vorsichtig.

Ich bin gespannt, ob der Großteil der Community Kompromisse eingehen und "teilweise implizites Warten" zulassen würde. Vielleicht ist es explizit genug, einmal pro Ausdruckskette zu lesen?

  1. Ich glaube nicht, dass die meisten Leute teilweise implizite Dinge wollen. Der native Ansatz von Rust ist eher wie "alles explizit". Sogar Casts u8 -> u32 werden explizit ausgeführt.
  2. Es funktioniert nicht gut für kompliziertere Szenarien. Was soll der Compiler mit Vec<Future> ?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Sollte await für jeden verschachtelten Aufruf ausgeführt werden? Ok, wir können wahrscheinlich darauf warten, dass eine Codezeile funktioniert (verschachtelte mapFunc werden also nicht erwartet), aber es ist zu einfach, dieses Verhalten zu brechen. Wenn das Warten für eine Codezeile funktioniert, wird es durch Refactoring wie "Wert extrahieren" unterbrochen.

@yazaddaruvala

Verstehe ich, dass Sie in dem von Ihnen beabsichtigten Präfix "Warten" richtig sind, das Vorrang vor ? mit der zusätzlichen Möglichkeit, "Warten" anstelle des Schlüsselworts "let" zu verwenden, um den Kontext zu ändern und alle impl Future <_ i zu streuen = "7"> kehrt mit warten zurück?

Das heißt, diese drei folgenden Aussagen sind äquivalent:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

Und diese beiden folgenden Aussagen sind äquivalent:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

Ich bin nicht sicher, ob die Syntax ideal ist, aber vielleicht macht ein Schlüsselwort, das sich in einen "impliziten Kontext warten" ändert, die Probleme des Präfix-Schlüsselworts weniger problematisch. Gleichzeitig können Sie bei Bedarf eine genauere Kontrolle darüber vornehmen, wo das Warten verwendet wird.

Meiner Meinung nach hat das Warten auf einzeiliges Präfix den Vorteil der Ähnlichkeit sowohl mit vielen anderen Programmiersprachen als auch mit vielen menschlichen Sprachen. Wenn am Ende entschieden wird, dass Warten nur ein Suffix sein soll, werde ich wahrscheinlich manchmal ein Await!() -Makro verwenden (oder eine ähnliche Form, die nicht mit Schlüsselwörtern kollidiert), wenn ich das Gefühl habe, dass ich mich darauf stütze Eine tief verwurzelte Vertrautheit mit der englischen Satzstruktur wird dazu beitragen, den mentalen Aufwand beim Lesen / Schreiben von Code zu verringern. Die Suffixformulare haben einen bestimmten Wert für länger verkettete Ausdrücke, aber meine explizit nicht auf objektiven Fakten basierende Ansicht ist, dass angesichts der menschlichen Komplexität des Präfixes, das durch die gesprochene Sprache subventioniert wird, der Einfachheitsvorteil besteht, nur eine Form zu haben überwiegen nicht deutlich die potenzielle Klarheit des Codes, beides zu haben. Angenommen, Sie vertrauen darauf, dass der Programmierer auswählt, welcher für den aktuellen Codeabschnitt am besten geeignet ist.

@ Pixel

Ich glaube nicht, dass die meisten Leute teilweise implizite Dinge wollen. Der native Ansatz von Rust ist eher wie "alles explizit".

Ich sehe das nicht so. Ich sehe es immer als einen Kompromiss zwischen explizit und implizit. Zum Beispiel:

  • Im Funktionsumfang sind Variablentypen erforderlich

    • Variablentypen können innerhalb eines lokalen Bereichs implizit sein.

    • Clousure-Kontexte sind implizit

    • Da wir bereits über die lokalen Variablen nachdenken können.

  • Lebenszeiten sind erforderlich, um dem Kreditprüfer zu helfen.

    • Innerhalb eines lokalen Bereichs können sie jedoch abgeleitet werden.

    • Auf Funktionsebene müssen die Lebensdauern nicht explizit sein, wenn sie alle den gleichen Wert haben.

  • Ich bin sicher, es gibt noch mehr.

Die Verwendung von await nur einmal pro Ausdruck / Anweisung mit allen Vorteilen der Verkettung ist ein sehr vernünftiger Kompromiss zwischen explizitem und reduzierendem Boilerplate.

Es funktioniert nicht gut für kompliziertere Szenarien. Was soll der Compiler mit Vec machen??

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Ich würde argumentieren, dass der Compiler Fehler machen sollte. Es gibt einen großen Typunterschied, auf den nicht geschlossen werden kann. In der Zwischenzeit wird das Beispiel mit Vec<impl Future> durch keine der Syntaxen in diesem Thread gelöst.

// Given that json() returns a Vec<imple Future>,
// do I use .await on the `Vec`? That seems odd.
// Given that the client.get() returns an `impl Future`,
// do I .await inside the .map? That wont work, given Iterator methods are not `async`
client.get("https://my_api").send().await?.json()[UNCLEAR]?.parse_as::<Vec<String>>()?.map(|x| client.get(x)[UNCLEAR])?;

Ich würde argumentieren, dass Vec<impl Future> ein schlechtes Beispiel ist, oder wir sollten jede vorgeschlagene Syntax auf dem gleichen Standard halten. In der Zwischenzeit funktioniert die von mir vorgeschlagene Syntax "partiell implizites Warten" besser als die aktuellen Vorschläge für kompliziertere Szenarien wie await (a, b) = (client.get("a")?, client.get("b")?); Verwendung eines normalen Postfixes oder Präfixes warten auf let (a, b) = (client.get("a") await?, client.get("b") await?); führt zu sequentiellen Netzwerkoperationen, bei denen das partielle -implizite Version kann gleichzeitig vom Compiler ausgeführt werden, der entsprechend desugardiert (wie ich in meinem ursprünglichen Beitrag gezeigt habe).

Im Jahr 2011, als die Funktion async in C # noch getestet wurde, fragte ich nach await als Präfixoperator und erhielt eine Antwort von der PM in C # -Sprache. Da diskutiert wird, ob Rust einen Präfixoperator verwenden soll, hielt ich es für sinnvoll, diese Antwort hier zu posten. Von Warum wird auf einen Präfixoperator anstelle eines Postfixoperators gewartet? ::

Wie Sie sich vorstellen können, war die Syntax der erwarteten Ausdrücke ein großer Diskussionspunkt, bevor wir uns für das entschieden haben, was jetzt da draußen ist :-). Postfix-Operatoren haben wir jedoch nicht viel berücksichtigt. Postfix (vgl. HP Caluclators und die Programmiersprache Forth) macht die Dinge im Prinzip einfacher und in der Praxis weniger zugänglich oder lesbar. Vielleicht ist es nur die Art und Weise, wie uns die mathematische Notation als Kinder einer Gehirnwäsche unterzieht ...

Wir haben definitiv festgestellt, dass der Präfix-Operator (mit seinem buchstäblich zwingenden Geschmack - "mach das!") Bei weitem der intuitivste war. Die meisten unserer unären Operatoren haben bereits ein Präfix, und das Warten scheint dazu zu passen. Ja, es übertönt in komplexen Ausdrücken, aber auch die Bewertungsreihenfolge im Allgemeinen, da z. B. die Funktionsanwendung auch kein Postfix ist. Sie werten zuerst die Argumente (die rechts sind) aus und rufen dann die Funktion (die links ist) auf. Ja, es gibt einen Unterschied in der Funktionsanwendungssyntax in C #, in der bereits Klammern integriert sind, während Sie diese normalerweise abwarten löschen können.

Was ich allgemein sehe (und selbst übernommen habe), ist ein Stil, der viele temporäre Variablen verwendet. Ich bevorzuge

var bResult = await A().BAsync();
var dResult = await bResult.C().DAsync();
dResult.E()

oder etwas ähnliches. Im Allgemeinen würde ich normalerweise vermeiden, dass mehr als ein Warten auf alle wartet, außer auf die einfachsten Ausdrücke (dh sie sind alle Argumente für dieselbe Funktion oder denselben Operator; diese sind wahrscheinlich in Ordnung), und ich würde es vermeiden, wartende Ausdrücke in Klammern zu setzen, und es vorziehen, zusätzliche Lokale für beide Jobs zu verwenden .

Sinn ergeben?

Mads Torgersen, C # Sprache PM


Meine persönliche Erfahrung als C # -Entwickler ist, dass die Syntax mich dazu veranlasst, diesen Stil mehrerer Anweisungen zu verwenden, und dass dies das Lesen und Schreiben von asynchronem Code viel umständlicher macht als das synchrone Äquivalent.

@yazaddaruvala

Ich sehe das nicht so. Ich sehe es immer als einen Kompromiss zwischen explizit und implizit

Es ist immer noch explizit. Ich kann übrigens einen guten Artikel zum Thema verlinken .

Ich würde argumentieren, dass der Compiler Fehler machen sollte. Es gibt einen großen Typunterschied, auf den nicht geschlossen werden kann. Inzwischen ist das Beispiel mit Vecwird durch keine der Syntaxen in diesem Thread gelöst.

Warum sollte es ein Fehler sein? Ich bin froh, Vec<impl future> , dass ich mich dann zusammenschließen kann. Sie schlagen ohne Grund zusätzliche Einschränkungen vor.

Ich würde argumentieren, dass der Vecist ein schlechtes Beispiel, oder wir sollten jede vorgeschlagene Syntax auf dem gleichen Standard halten.

Wir sollten jeden Fall untersuchen. Wir können Randfälle nicht einfach ignorieren, weil "meh, weißt du, niemand schreibt das sowieso". Bei der Sprachgestaltung geht es in erster Linie um Randfälle.

@chescock Ich stimme Ihnen zu, aber wie bereits gesagt, hat Rust hier einen großen Unterschied zu C #: In C # schreiben Sie niemals (await FooAsync()).Bar() . Aber in Rust tust du das. Und ich spreche von ? . In C # gibt es eine implizite Ausnahme, die über asynchrone Aufrufe weitergegeben wird. Aber in Rost muss man explizit sein und ? auf das Funktionsergebnis schreiben, nachdem es erwartet wurde.

Natürlich können Sie Benutzer bitten, zu schreiben

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

aber es ist viel seltsamer als

let foo = await? bar();
let bar = await? foo.baz();

Dies sieht viel besser aus, erfordert jedoch die Einführung einer neuen Kombination aus await und ? doint (await expr)? . Wenn wir andere Operatoren haben, könnten wir außerdem await?&@# schreiben und alle Kombinationen zusammen funktionieren lassen ... Sieht etwas kompliziert aus.

Aber dann können wir das Warten einfach als Postfix platzieren und es wird natürlich in die aktuelle Sprache passen:

let foo = bar() await?;
let bar = foo.baz() await?;

Jetzt sind await? zwei getrennte Token, aber sie funktionieren insgesamt. Sie können await?&@# und müssen sich nicht darum kümmern, es in den Compiler selbst zu hacken.

Natürlich sieht es etwas seltsamer aus als das Präfix, aber wenn man sagt, dreht sich alles um den Unterschied zwischen Rust und C #. Wir können C # -Erfahrung verwenden, um sicherzustellen, dass wir eine explizit wartende Syntax benötigen, die wahrscheinlich ein getrenntes Schlüsselwort enthält, aber wir sollten nicht blindlings dem gleichen Weg folgen und Rust / C # -Differenzen ignorieren.

Ich war selbst lange Zeit ein Befürworter des Präfixes await und habe sogar C # -Sprachentwickler in das Thema eingeladen (wir haben also Informationen, die neuer sind als ab 2011 😄), aber jetzt denke ich, dass das Postfix await Schlüsselwort

Vergessen Sie nicht, dass es eine Nacht gibt, damit wir sie später überarbeiten können, wenn wir einen besseren Weg finden.

Mir ist der Gedanke gekommen, dass wir möglicherweise das Beste aus beiden Welten mit einer Kombination aus:

  • ein Präfix async Schlüsselwort
  • eine allgemeine Postfix-Makrofunktion

Was zusammen ermöglichen würde, dass ein Postfix .await!() Makro ohne Compiler-Magie implementiert wird.

Im Moment würde ein Postfix .await!() Makro Compiler-Magie erfordern. Wenn jedoch eine Präfixvariante des Schlüsselworts await stabilisiert würde, wäre dies nicht mehr der Fall: Das Makro postfix .await!() könnte trivial als Makro implementiert werden, das seinem Argument (einem Ausdruck) vorangestellt ist. mit dem Schlüsselwort await und alles in Klammern gesetzt.

Vorteile dieses Ansatzes:

  • Das Präfix await ist verfügbar und für Benutzer zugänglich, die mit diesem Konstrukt aus anderen Sprachen vertraut sind.
  • Das Präfix-Async-Schlüsselwort kann verwendet werden, wenn es wünschenswert ist, die Async-Natur eines Ausdrucks "hervorzuheben".
  • Es würde eine Postfix-Variante .await!() , die in Verkettungssituationen oder in jeder anderen Situation verwendet werden könnte, in der die Präfixsyntax umständlich ist.
  • Für die Postfix-Variante wäre keine (möglicherweise unerwartete) Compiler-Magie erforderlich (da dies sonst ein Problem für das Postfix-Feld, die Methode oder die Makrooptionen darstellen würde).
  • Postfix-Makros sind auch für andere Situationen verfügbar (z. B. .or_else!(continue) ), in denen aus ähnlichen Gründen nebenbei die Postfix-Makrosyntax erforderlich ist (andernfalls muss der vorhergehende Ausdruck auf unangenehme und unlesbare Weise umbrochen werden).
  • Das Präfix await Schlüsselwort konnte relativ schnell stabilisiert werden (damit sich das Ökosystem entwickeln kann), ohne dass wir auf die Implementierung von Postfix-Makros warten müssen. Mittel- bis langfristig würden wir jedoch die Möglichkeit einer Postfix-Syntax für das Warten offen lassen.

@nicoburns , @ H2CO3 ,

Wir sollten keine Kontrollflussoperatoren wie await!() und .or_else!(continue) als Makros implementieren, da es aus Anwendersicht keinen sinnvollen Grund dafür gibt. In beiden Formen hätten Benutzer genau die gleichen Funktionen und sollten beide lernen. Wenn diese Funktionen jedoch als Makros implementiert würden, würden sich die Benutzer zusätzlich Gedanken darüber machen, warum genau sie auf diese Weise implementiert werden. Es ist unmöglich, das nicht zu bemerken, denn es würde nur einen bizarren und künstlichen Unterschied zwischen regulären erstklassigen Operatoren und regulären Operatoren geben, die als Makros implementiert sind (da Makros an sich erstklassig sind, die von ihnen implementierten Dinge jedoch nicht).

Es ist genau die gleiche Antwort wie für zusätzliche . vor await : Wir brauchen sie nicht. Makros können keinen erstklassigen Kontrollfluss imitieren: Sie haben unterschiedliche Formen, unterschiedliche Anwendungsfälle, unterschiedliche Hervorhebungen, unterschiedliche Funktionen und eine völlig unterschiedliche Wahrnehmung.

Für die Funktionen .await!() und .or_else!(continue) gibt es eine geeignete und ergonomische Lösung mit schönen Syntaxen: await Schlüsselwort und none -Koaleszenzoperator. Wir sollten es vorziehen, sie anstelle von generischen, selten verwendeten und hässlich aussehenden Postfix-Makros zu implementieren.

Es gibt keinen Grund, Makros zu verwenden, da es nichts Kompliziertes wie format!() gibt, es gibt nichts, was selten verwendet wird wie include_bytes!() , es gibt kein benutzerdefiniertes DSL, es gibt keine Entfernung von Duplikaten im Benutzercode, es gibt Keine vararg-ähnliche Syntax erforderlich, es ist nicht erforderlich, dass etwas anders aussieht, und wir können diesen Weg nicht verwenden, nur weil es möglich ist.

Wir sollten Kontrollflussoperatoren nicht als Makros implementieren, da es aus Anwendersicht keinen sinnvollen Grund dafür gibt.

try!() wurde als Makro implementiert. Es gab einen guten Grund dafür.

Ich habe bereits beschrieben, was der Grund dafür ist, await einem Makro zu machen - es ist kein neues Sprachelement erforderlich, wenn eine vorhandene Funktion das Ziel ebenfalls erreichen kann.

Es gibt nichts Kompliziertes wie format!()

Ich bin anderer Meinung: Bei format!() geht es nicht um Komplikationen, sondern um die Überprüfung der Kompilierungszeit. Wenn es nicht zur Überprüfung der Kompilierungszeit wäre, könnte es eine Funktion sein.

Ich habe übrigens nicht vorgeschlagen, dass es sich um ein Postfix-Makro handelt. (Ich denke es sollte nicht.)

Für beide Funktionen gibt es eine richtige und ergonomische Lösung mit schönen Syntaxen

Wir sollten nicht jedem einzelnen Funktionsaufruf, Flusssteuerungsausdruck oder kleinen Komfortmerkmal eine ganz eigene Syntax geben. Das führt nur zu einer aufgeblähten Sprache, die ohne Grund voller Syntax ist, und es ist genau das Gegenteil von "schön".

(Ich habe gesagt, was ich könnte; ich werde auf diesen Aspekt der Frage nicht mehr antworten.)

@nicoburns

Mir ist der Gedanke gekommen, dass wir möglicherweise das Beste aus beiden Welten mit einer Kombination aus:

  • ein Präfix await Schlüsselwort
  • eine allgemeine Postfix-Makrofunktion

Dies bedeutet, dass await ein kontextbezogenes Schlüsselwort ist, im Gegensatz zu dem tatsächlichen Schlüsselwort, das es derzeit ist. Ich denke nicht, dass wir das tun sollten.

Was zusammen ermöglichen würde, dass ein Postfix .await!() Makro ohne Compiler-Magie implementiert wird.

Dies ist in Bezug auf die Implementierung eine komplexere Lösung als foo await oder foo.await (insbesondere letzteres). Es gibt immer noch "Magie", genauso viel davon; Sie haben gerade ein Buchhaltungsmanöver durchgeführt .

Vorteile dieses Ansatzes:

  • Das Präfix await Schlüsselwort ist verfügbar und für Benutzer zugänglich, die mit diesem Konstrukt aus anderen Sprachen vertraut sind.

Wenn wir später .await!() hinzufügen, geben wir den Benutzern nur mehr zu lernen (sowohl await foo als auch foo.await!() ), und jetzt werden Benutzer fragen, "was ich verwenden soll wann..". Dies scheint mehr des Komplexitätsbudgets zu verbrauchen als foo await oder foo.await als einzelne Lösungen.

  • Postfix-Makros sind auch für andere Situationen verfügbar (z. B. .or_else!(continue) ), in denen aus ähnlichen Gründen nebenbei die Postfix-Makrosyntax erforderlich ist (andernfalls muss der vorhergehende Ausdruck auf unangenehme und unlesbare Weise umbrochen werden).

Ich glaube, Postfix-Makros haben Wert und sollten der Sprache hinzugefügt werden. Das bedeutet jedoch nicht, dass sie für await ing verwendet werden müssen; Wie Sie bereits erwähnt haben, gibt es .or_else!(continue) und viele andere Stellen, an denen Postfix-Makros nützlich wären.

  • Das Präfix await Schlüsselwort konnte relativ schnell stabilisiert werden (damit sich das Ökosystem entwickeln kann), ohne dass wir auf die Implementierung von Postfix-Makros warten müssen. Mittel- bis langfristig würden wir jedoch die Möglichkeit einer Postfix-Syntax für das Warten offen lassen.

Ich sehe keinen Wert darin, "offene Möglichkeiten zu lassen "; Der Wert von Postfix für Komposition, IDE-Erfahrung usw. ist heute bekannt. Ich bin nicht daran interessiert, " await foo heute zu stabilisieren und hoffe, dass wir morgen einen Konsens über foo.await!() erzielen können".


@ H2CO3

try!() wurde als Makro implementiert. Es gab einen guten Grund dafür.

try!(..) war veraltet, was bedeutet, dass wir es für die Sprache nicht geeignet hielten, insbesondere weil es kein Postfix war. Es scheint seltsam, es als Argument zu verwenden - außer für das, was wir nicht tun sollten. Darüber hinaus wird try!(..) ohne Unterstützung des Compilers definiert .

Ich habe übrigens nicht vorgeschlagen, dass es sich um ein Postfix-Makro handelt. (Ich denke es sollte nicht.)

await ist nicht "jeder einzelne ..." - insbesondere ist es keine geringfügige Annehmlichkeitsfunktion; Vielmehr wird eine wichtige Sprachfunktion hinzugefügt.

@ Phaylon

Angesichts der jüngsten Diskussionen über die Komplizierung der Grammatik und die damit verbundenen Probleme beim Werkzeugbau ist es möglicherweise keine gute Idee, sich darauf zu verlassen, dass IDEs / Editoren die Dinge richtig machen.

Die Syntax foo.await soll Probleme beim Tooling .ident bereits versteht. Der Editor muss lediglich await zur Liste der Schlüsselwörter hinzufügen. Darüber hinaus haben gute IDEs bereits eine Code-Vervollständigung basierend auf . - daher scheint es einfacher zu sein, RLS (oder Äquivalente ...) zu erweitern, um await als ersten Vorschlag bereitzustellen, wenn der Benutzer . nach my_future .

Was die Komplikation der Grammatik angeht, ist es am unwahrscheinlichsten, dass .await die Grammatik kompliziert, da die Unterstützung von .await in der Syntax im Wesentlichen .$ident analysiert und sich dann nicht auf ident == keywords::Await.name() irrt

Die Syntax foo.await soll Probleme beim Tooling reduzieren, da jeder Editor mit dem Anschein einer guten Unterstützung für Rust die .ident-Form bereits versteht. Was der Editor tun muss, ist nur das Warten auf die Liste der Schlüsselwörter hinzuzufügen. Darüber hinaus haben gute IDEs bereits eine Code-Vervollständigung basierend auf. - Es scheint also einfacher zu sein, RLS (oder Äquivalente ...) zu erweitern, um das Warten als ersten Vorschlag beim Eingeben des Benutzers bereitzustellen. nach my_future.

Ich finde das nur im Widerspruch zur Grammatikdiskussion. Und das Problem ist nicht nur jetzt, sondern in 5, 10, 20 Jahren nach ein paar Ausgaben. Aber wie ich hoffe, dass Sie es bemerkt haben, erwähne ich auch, dass selbst wenn ein Schlüsselwort hervorgehoben ist, die Wartepunkte unbemerkt bleiben können, wenn andere Schlüsselwörter beteiligt sind.

Was die Komplikation der Grammatik angeht, ist es am unwahrscheinlichsten, dass .await die Grammatik kompliziert, da die Unterstützung von .await in der Syntax im Wesentlichen das Parsen von. $ Ident und dann keine Fehler bei ident == keywords :: Await.name () bedeutet.

Ich denke, dass Ehre zu await!(future) , da sie bereits vollständig von der Grammatik unterstützt wird.

@Centril try! schließlich überflüssig, weil der Operator ? genau mehr kann. Es ist nicht "ungeeignet für die Sprache". Es könnte dir aber nicht gefallen , was ich akzeptiere. Aber für mich ist es tatsächlich eines der besten Dinge, die Rust erfunden hat, und es war eines der Verkaufsargumente. Und ich weiß, dass es ohne Compiler-Unterstützung implementiert ist - aber ich kann nicht erkennen, wie relevant dies ist, wenn diskutiert wird, ob es einen Kontrollfluss ausführt oder nicht. Es tut es trotzdem.

Warten ist nicht "jeder einzelne .."

Aber andere, die hier erwähnt werden (z. B. or_else ), sind es, und mein Punkt gilt trotzdem für wichtige Funktionen. Das Hinzufügen von Syntax nur zum Hinzufügen von Syntax ist kein Vorteil. Wenn also etwas bereits in einem allgemeineren Fall funktioniert, sollte es dem Erfinden einer neuen Notation vorgezogen werden. (Ich weiß, dass das andere Argument gegen Makros lautet: "Sie sind kein Postfix". Ich glaube einfach nicht, dass die Vorteile des Wartens als eigener Postfix-Operator hoch genug sind, um die Kosten zu rechtfertigen. Wir haben verschachtelte Funktionsaufrufe überlebt. Wir wird genauso gut sein, nachdem Sie ein paar verschachtelte Makros geschrieben haben.)

Am Mittwoch, 23. Januar 2019, um 09:59:36 Uhr +0000 schrieb Mazdak Farrokhzad:

  • Postfix-Makros sind auch für andere Situationen verfügbar (z. B. .or_else!(continue) ), in denen aus ähnlichen Gründen nebenbei die Postfix-Makrosyntax erforderlich ist (andernfalls muss der vorhergehende Ausdruck auf unangenehme und unlesbare Weise umbrochen werden).

Ich glaube, Postfix-Makros haben Wert und sollten der Sprache hinzugefügt werden. Das bedeutet jedoch nicht, dass sie für await ing verwendet werden müssen; Wie Sie bereits erwähnt haben, gibt es .or_else!(continue) und viele andere Stellen, an denen Postfix-Makros nützlich wären.

Der Hauptgrund für die Verwendung von .await!() ist das Aussehen eines Makros
es ist klar, dass es den Kontrollfluss beeinflussen kann.

.await sieht aus wie ein Feldzugriff, .await() sieht aus wie eine Funktion
Aufruf, und weder Feldzugriffe noch Funktionsaufrufe können die Steuerung beeinflussen
fließen. .await!() sieht aus wie ein Makro, und Makros können die Steuerung beeinflussen
fließen.

@joshtriplett Ich habe keinen Einfluss auf den Kontrollfluss im üblichen Sinne. Dies ist die grundlegende Tatsache, um zu rechtfertigen, dass der Kreditprüfer in definierten Futures arbeiten sollte (und Pin ist die Begründung dafür ). Aus Sicht der lokalen Funktionsausführung ist das Ausführen von Warten wie die meisten anderen Funktionsaufrufe. Sie fahren dort fort, wo Sie aufgehört haben, und haben einen Rückgabewert auf dem Stapel.

Ich finde das nur im Widerspruch zur Grammatikdiskussion. Und das Problem ist nicht nur jetzt, sondern in 5, 10, 20 Jahren nach ein paar Ausgaben.

Ich habe keine Ahnung, was du damit meinst.

Ich denke, dass Ehre zu await!(future) , da sie bereits vollständig von der Grammatik unterstützt wird.

Ich verstehe, aber an diesem Punkt denke ich, dass diese Syntax aufgrund der zahlreichen anderen Nachteile effektiv ausgeschlossen ist.

@Centril try! schließlich überflüssig, weil der Operator ? genau mehr kann. Es ist nicht "ungeeignet für die Sprache". Sie mögen es vielleicht nicht, was ich akzeptiere.

Es ist ausdrücklich veraltet und außerdem ein schwerer Fehler, try!(expr) auf Rust 2018 zu schreiben. Wir haben uns gemeinsam dafür entschieden und es wurde daher als nicht geeignet angesehen.

Der Hauptgrund für die Verwendung von .await!() ist, dass das Aussehen eines Makros deutlich macht, dass es den Kontrollfluss beeinflussen kann.

.await sieht aus wie ein Feldzugriff, .await() sieht aus wie ein Funktionsaufruf, und weder Feldzugriffe noch Funktionsaufrufe können den Kontrollfluss beeinflussen.

Ich glaube, die Unterscheidung ist etwas, das Benutzer relativ leicht lernen werden, zumal auf .await normalerweise ? folgt (was ein funktionslokaler Kontrollfluss ist). Was Funktionsaufrufe betrifft, und wie bereits erwähnt, kann man mit Recht sagen, dass Iteratormethoden eine Form des Kontrollflusses sind. Außerdem , nur weil ein Makro Steuerfluss beeinflussen kann, bedeutet nicht , es so tun. Viele Makros tun dies nicht (z. B. dbg! , format! , ...). Das Verständnis, dass .await!() oder .await den Kontrollfluss beeinflussen (obwohl in einem viel schwächeren Sinne als ? , wie in der Anmerkung von @HeroicKatora angegeben) , await selbst.

@Centril

Dies bedeutet, dass Sie auf ein kontextbezogenes Schlüsselwort warten müssen, im Gegensatz zu dem tatsächlichen Schlüsselwort, das es derzeit ist. Ich denke nicht, dass wir das tun sollten.

Eek. Das ist etwas schmerzhaft. Vielleicht könnte das Makro einen anderen Namen wie .wait!() oder .awaited!() (letzteres ist ganz nett, da es klar macht, dass es für den vorhergehenden Ausdruck gilt).

"Was zusammen die Implementierung eines Postfix .await! () Makros ohne Compilermagie ermöglichen würde."
Dies ist in Bezug auf die Implementierung eine komplexere Lösung als foo await oder foo.await (insbesondere letzteres). Es gibt immer noch "Magie", genauso viel davon; Sie haben gerade ein Buchhaltungsmanöver durchgeführt.

Außerdem wird try! (..) ohne Unterstützung des Compilers definiert.

Und wenn wir ein Präfix await Schlüsselwort und Postfix-Makros hätten, dann könnten .await!() (vielleicht mit einem anderen Namen) auch ohne Compiler-Unterstützung implementiert werden, oder? Natürlich würde das Schlüsselwort await selbst immer noch eine erhebliche Menge an Compilermagie mit sich bringen, aber das würde einfach auf das Ergebnis eines Makros angewendet und unterscheidet sich nicht try!() der Beziehung von return Schlüsselwort.

Wenn wir später .await! () Hinzufügen, geben wir den Benutzern nur mehr zu lernen (beide warten auf foo und foo.await! ()) Und jetzt werden Benutzer fragen, "was soll ich verwenden, wenn ...". Dies scheint mehr Komplexität zu verbrauchen als foo await oder foo.await als einzelne Lösungen.

Ich denke, die Komplexität, die dies dem Benutzer hinzufügt

"Es gibt zwei Möglichkeiten, um auf eine Zukunft in Rust zu warten: await foo.bar(); und foo.bar().await!() . Die Verwendung ist eine stilistische Präferenz und macht keinen Unterschied für den Ausführungsfluss."

Ich sehe keinen Wert darin, "offene Möglichkeiten zu lassen"; Der Wert von Postfix für Komposition, IDE-Erfahrung usw. ist heute bekannt.

Das ist wahr. Ich denke, es geht eher darum, sicherzustellen, dass unsere Implementierung die Möglichkeit einer einheitlicheren Sprache in Zukunft nicht ausschließt.

Ich habe keine Ahnung, was du damit meinst.

Macht nicht viel aus. Aber im Allgemeinen bevorzuge ich es auch, wenn die Syntax ohne gute Hervorhebung offensichtlich ist. Die Dinge fallen also in git diff und anderen solchen Kontexten auf.

Theoretisch ja und für triviale Programme ja, aber in Wirklichkeit Programmierer
müssen wissen, wo sich ihre Suspend-Punkte befinden.

In einem einfachen Beispiel können Sie eine RefCell über einen Suspend-Punkt halten und
Das Verhalten Ihres Programms unterscheidet sich von dem der RefCell
vor dem Suspend-Punkt freigegeben. In großen Programmen wird es geben
unzählige Feinheiten wie diese, bei denen die Tatsache, dass die aktuelle Funktion
Suspendieren ist eine wichtige Information.

Am Mittwoch, 23. Januar 2019, um 14:21 Uhr HeroicKatora [email protected]
schrieb:

@joshtriplett https://github.com/joshtriplett Ich habe keinen Einfluss auf die Kontrolle
fließen im üblichen Sinne. Dies ist die grundlegende Tatsache, um zu rechtfertigen, dass die
Der Kreditprüfer sollte in definierten Futures funktionieren. Aus der Sicht der
Die Ausführung lokaler Funktionen und das Ausführen von Warten ist wie bei jedem anderen Funktionsaufruf.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

Am Mittwoch, 23. Januar 2019, um 02:26:07 Uhr -0800 schrieb Mazdak Farrokhzad:

Der Hauptgrund für die Verwendung von .await!() ist, dass das Aussehen eines Makros deutlich macht, dass es den Kontrollfluss beeinflussen kann.

.await sieht aus wie ein Feldzugriff, .await() sieht aus wie ein Funktionsaufruf, und weder Feldzugriffe noch Funktionsaufrufe können den Kontrollfluss beeinflussen.

Ich glaube, die Unterscheidung ist etwas, was Benutzer relativ leicht lernen werden

Warum sollten sie müssen?

Ich glaube, die Unterscheidung ist etwas, das Benutzer relativ leicht lernen werden, zumal .await normalerweise gefolgt wird von? (Dies ist ein funktionslokaler Kontrollfluss).

Oft ja, aber nicht immer, und die Syntax sollte auf jeden Fall in Fällen funktionieren, in denen dies nicht der Fall ist.

Was Funktionsaufrufe betrifft, und wie bereits erwähnt, kann man mit Recht sagen, dass Iteratormethoden eine Form des Kontrollflusses sind.

Nicht auf die gleiche Weise wie return oder ? (oder sogar break ). Es wäre sehr überraschend, wenn ein Funktionsaufruf zwei Ebenen der aufgerufenen Funktion zurückgeben könnte (einer der Gründe, warum Rust keine Ausnahmen hat, ist genau, weil dies überraschend ist) oder aus einer Schleife im Aufruf ausbrechen könnte Funktion.

Nur weil ein Makro den Kontrollfluss beeinflussen kann, heißt das noch lange nicht, dass dies der Fall ist. Viele Makros nicht (zB dbg!, Format!, ...). Das Verständnis, dass .await! () Oder .await den Kontrollfluss beeinflussen (obwohl in einem viel schwächeren Sinne als ?, Gemäß der Anmerkung von ergibt sich aus dem Wort, das sich selbst erwartet.

Das gefällt mir überhaupt nicht. Die Beeinflussung des Kontrollflusses ist eine wirklich wichtige Nebenwirkung. Es sollte eine andere syntaktische Form haben als Konstrukte, die dies normalerweise nicht können. Konstrukte, die dies in Rust tun können, sind: Schlüsselwörter ( return , break , continue ), Operatoren ( ? ) und Makros (durch Auswertung zu einem der anderen Formen). Warum das Wasser durch Hinzufügen einer Ausnahme trüben?

@ejmahler Ich sehe nicht, wie sich das Halten einer RefCell für den Vergleich mit einem normalen Funktionsaufruf unterscheidet. Es könnte auch sein, dass die innere Funktion dieselbe RefCell erhalten möchte. Aber niemand scheint ein großes Problem damit zu haben, nicht sofort das gesamte potenzielle Anrufdiagramm zu erfassen. In ähnlicher Weise erhalten Probleme mit der Stapelerschöpfung keine besonderen Bedenken, dass Sie den bereitgestellten Stapelspeicher mit Anmerkungen versehen müssen. Hier wäre der Vergleich für Coroutinen günstig! Sollten wir normale Funktionsaufrufe sichtbarer machen, wenn sie nicht inline sind? Benötigen wir explizite Tail Calls, um dieses zunehmend schwierige Problem mit Bibliotheken in den Griff zu bekommen?

Die Antwort ist, imho, dass das größte Risiko bei der Verwendung von RefCell und Warten auf Unbekanntheit besteht. Wenn die anderen oben genannten Probleme von der Hardware abstrahiert werden können, glaube ich, dass wir als Programmierer auch lernen können, RefCell usw. nicht über Fließgrenzen hinweg zu halten, außer wenn dies überprüft wurde.

Am Mittwoch, den 23. Januar 2019 um 22:30:10 Uhr +0000 schrieb Elliott Mahler:

Theoretisch ja und für triviale Programme ja, aber in Wirklichkeit Programmierer
müssen wissen, wo sich ihre Suspend-Punkte befinden.

: +1:

@HeroicKatora

Ein entscheidender Unterschied ist, wie viel Code Sie überprüfen müssen, wie viele
Möglichkeiten, die Sie berücksichtigen müssen. Programmierer sind bereits daran gewöhnt
Stellen Sie sicher, dass nichts, was sie aufrufen, auch eine von ihnen überprüfte RefCell verwendet
aus. Aber mit Warten haben wir keine, anstatt einen einzelnen Aufrufstapel zu inspizieren
Kontrolle darüber, was während des Wartens ausgeführt wird - buchstäblich jede Zeile der
Programm könnte ausgeführt werden. Meine Intuition sagt mir, dass der durchschnittliche Programmierer ist
viel weniger gerüstet, um mit dieser Explosion von Möglichkeiten umzugehen, als sie
müssen viel seltener damit umgehen.

Ein weiteres Beispiel dafür, dass Suspend-Punkte im Spiel wichtige Informationen sind
Beim Programmieren schreiben wir normalerweise Coroutinen, die wieder aufgenommen werden sollen
einmal pro Frame, und wir ändern den Spielstatus bei jedem Lebenslauf. Wenn wir a übersehen
Suspend Point in einer dieser Coroutinen, können wir das Spiel in einem gebrochenen verlassen
Status für mehrere Frames.

All dies kann behoben werden, indem man sagt: „Sei schlauer und verdiene weniger
Fehler “natürlich, aber dieser Ansatz funktioniert historisch nicht.

Am Mittwoch, 23. Januar 2019, um 14:44 Uhr Josh Triplett [email protected]
schrieb:

Am Mittwoch, den 23. Januar 2019 um 22:30:10 Uhr +0000 schrieb Elliott Mahler:

Theoretisch ja und für triviale Programme ja, aber in Wirklichkeit Programmierer
müssen wissen, wo sich ihre Suspend-Punkte befinden.

: +1:

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

Als weiteres Beispiel dafür, dass Suspend-Punkte wichtige Informationen sind, schreiben wir in der Spielprogrammierung normalerweise Coroutinen, die einmal pro Frame fortgesetzt werden sollen, und ändern den Spielstatus bei jedem Lebenslauf. Wenn wir einen Suspend-Punkt in einer dieser Coroutinen übersehen, können wir das Spiel für mehrere Frames in einem fehlerhaften Zustand belassen.

Wenn Sie den Schlüssel zu einer SlotMap ablegen, verlieren Sie Speicher. Die Sprache selbst hilft Ihnen dabei nicht. Was ich sagen möchte, ist, dass Ihre Beispiele dafür, wie das Warten vage nicht sichtbar genug ist (solange es sich um ein Schlüsselwort handelt, dessen Vorkommen eindeutig ist), nicht auf Probleme zu verallgemeinern scheinen, die bei anderen Funktionen nicht aufgetreten sind. Ich denke, Sie sollten weniger darauf warten, welche Funktion die Sprache Ihnen sofort bietet, als vielmehr darauf, wie Sie Ihre eigenen Funktionen ausdrücken können. Bewerten Sie Ihre Tools und wenden Sie sie dann an. Schlagen Sie eine Lösung für den Spielstatus vor und wie dies gut genug angegangen wird: Schreiben Sie einen Wrapper, der bei der Rückkehr bestätigt, dass der Spielstatus tatsächlich den erforderlichen Betrag angekreuzt hat. Mit Async können Sie Ihren eigenen Status aus diesem Grund ausleihen. Das Verbot anderer wartet in diesem unterteilten Code.

Die Statusexplosion in der RefCell kommt nicht von Ihnen, wenn Sie nicht überprüfen, ob sie nicht anderweitig verwendet wird, sondern von anderen Codepunkten, die nicht wissen, dass Sie sich auf diese Invariante verlassen haben, und sie stillschweigend hinzufügen. Diese werden auf die gleiche Weise behandelt, Dokumentation, Kommentare, korrekte Verpackung.

Das Halten von RefCell sich nicht wesentlich vom Hodeln von Mutex beim Blockieren von Io. Sie können einen Deadlock bekommen, der schlimmer und schwerer zu debuggen ist als eine Panik. Und dennoch kommentieren wir Blockierungsvorgänge nicht mit dem expliziten Schlüsselwort block . :).

Das Teilen von RefCell zwischen asynchronen Funktionen schränkt deren allgemeine Benutzerfreundlichkeit stark ein ( !Send ), daher denke ich nicht, dass dies üblich sein wird. Und RefCell sollte generell vermieden und bei Verwendung so kurz wie möglich ausgeliehen werden.

Ich denke also nicht, dass es eine große Sache ist, versehentlich RefCell über eine Rendite zu humpeln.

Wenn wir einen Suspend-Punkt in einer dieser Coroutinen übersehen, können wir das Spiel für mehrere Frames in einem fehlerhaften Zustand belassen.

Coroutinen und asynchrone Funktionen sind etwas anderes. Wir versuchen nicht, yield implizit zu machen.

Für alle, die sich für die Makrosyntax anstelle der Schlüsselwortsyntax entscheiden: Bitte beschreiben Sie, worum es bei await , was bedeutet, dass es sich um ein Makro handeln sollte und wie sich dies beispielsweise von while , die _could_ haben könnte war gerade ein while! Makro ( selbst wenn nur macro_rules! ; überhaupt kein Spezialgehäuse).

Bearbeiten: Dies ist nicht rhetorisch. Ich bin wirklich an so etwas interessiert, genauso wie ich dachte, ich wollte auf den ersten Blick warten (wie C #), aber der RFC hat mich anders überzeugt.

Für alle, die sich für die Makrosyntax anstelle der Schlüsselwortsyntax entscheiden: Bitte beschreiben Sie, worum es geht, warten Sie, das heißt, es sollte ein Makro sein und wie sich das beispielsweise von while unterscheidet, was nur eine Weile hätte dauern können! Makro (auch nur mit macro_rules !; überhaupt kein Spezialgehäuse).

Ich stimme dieser Argumentation größtenteils zu und möchte ein Schlüsselwort für die Präfixsyntax verwenden.

Wie oben beschrieben (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), bin ich jedoch dafür, auch ein Postfix-Makro für die Postfix-Syntax zu haben (möglicherweise .awaited!() , um Namenskonflikte mit dem Präfix-Schlüsselwort zu vermeiden). Meine Argumentation ist teilweise, dass es einfacher ist, ein Schlüsselwort zu haben, das nur auf eine Weise verwendet werden kann, und weitere Variationen für Makros zu speichern, aber hauptsächlich, soweit ich sehen kann, konnte niemand einen Postfix finden Schlüsselwortsyntax, die ich für akzeptabel halten würde:

  • foo.bar().await und foo.bar().await() wären technisch gesehen Schlüsselwörter, aber sie sehen aus wie ein Feldzugriff oder ein Methodenaufruf, und ich denke nicht, dass ein solches Konstrukt zur Änderung des Kontrollflusses so versteckt werden sollte.
  • foo.bar() await ist nur verwirrend, insbesondere bei weiteren Verkettungen wie foo.bar() await.qux() . Ich habe noch nie ein Schlüsselwort gesehen, das als solches Postfix verwendet wurde (ich bin sicher, dass es sie gibt, aber ich kann mir kein einziges Beispiel in einer Sprache vorstellen, die ich für Schlüsselwörter kenne, die so funktionieren). Und ich denke, es liest sich sehr verwirrend, als ob das Warten auf die qux() nicht auf die foo.bar() zutrifft.
  • foo.bar()@await oder eine andere Interpunktion könnte funktionieren. Aber es ist nicht besonders schön und es fühlt sich ziemlich ad-hoc an. Ich glaube nicht, dass es bei dieser Syntax signifikante Probleme gibt (im Gegensatz zu den oben genannten Optionen). Ich habe nur das Gefühl, sobald wir mit dem Hinzufügen von Siegeln beginnen, beginnt die Balance, die benutzerdefinierte Syntax aufzugeben und ein Makro zu sein.

Ich werde eine frühere Idee noch einmal wiederholen, obwohl sie mit neuen Gedanken und Überlegungen optimiert wurde, und dann versuchen, zu schweigen.

await als Schlüsselwort ist ohnehin nur innerhalb einer async -Funktion gültig, daher sollten wir den Bezeichner await anderer Stelle zulassen. Wenn eine Variable in einem äußeren Bereich await heißt, kann nicht innerhalb eines async -Blocks auf sie zugegriffen werden, es sei denn, sie wird als Rohbezeichner verwendet. Oder zum Beispiel das Makro await! .

Darüber hinaus würde eine solche Anpassung des Schlüsselworts meinen Hauptpunkt berücksichtigen: Wir sollten das Mischen und Anpassen von Generatoren und asynchronen Funktionen wie folgt zulassen:

// imaginary generator syntax stolen from JavaScript
fn* my_generator() -> T {
    yield some_value;

    // explicit return statements are only included to 
    // make it clear the generator/async functions are finished.
    return another_value;
}

// `await` keyword would not be allowed here, but the `yield` keyword is
#[async]
fn* my_async_generator() -> Result<T, E> {
    let item = some_op().await!()?; // uses the `.await!()` macro
    // which would really just use `yield` internally, but with the pinning API
    // same as the current nightly macro.

    yield future::ok(item.clone());

    return Ok(item);
}

// `yield` would not be allowed here, but the `await` keyword is.
async fn regular_async() -> Result<T, E> {
   let some_op = async || { /*...*/ };

   let item = await? some_op();

   return Ok(item);
}

Ich glaube, dies ist ausreichend transparent für extrem fortgeschrittene Benutzer, die funky Sachen machen wollen, aber es neuen und moderaten Benutzern ermöglicht, nur das zu verwenden, was notwendig ist. 2 und 3 sind praktisch identisch, wenn sie ohne yield -Anweisungen verwendet werden.

Ich denke auch, dass das Präfix await? eine gute Abkürzung ist, um ? zum Ergebnis der asynchronen Operation hinzuzufügen, aber das ist nicht unbedingt erforderlich.

Wenn Postfix-Makros zu einer offiziellen Sache werden (was ich hoffe), könnten sowohl await!(...) als auch .await!() zusammen existieren, was drei äquivalente Vorgehensweisen für bestimmte Anwendungsfälle und Stile ermöglicht . Ich glaube nicht, dass dies einen kognitiven Aufwand verursachen würde, aber eine größere Flexibilität ermöglichen würde.

Im Idealfall könnten async fn und #[async] fn* sogar Implementierungscode für die Umwandlung des zugrunde liegenden Generators in eine asynchrone Zustandsmaschine gemeinsam nutzen.

Diese Probleme haben deutlich gemacht, dass es keinen wirklich bevorzugten Stil gibt. Das Beste, auf das ich hoffen kann, ist, einen sauberen, flexiblen, lesbaren und leicht zu erreichenden Komplexitätsgrad bereitzustellen. Ich denke, das obige Schema ist ein guter Kompromiss für die Zukunft.

await als Schlüsselwort ist ohnehin nur innerhalb einer async -Funktion gültig, daher sollten wir den Bezeichner an anderer Stelle warten lassen.

Ich weiß nicht, ob das bei Rust angesichts hygienischer Makros praktisch ist. Wenn ich foo!(x.await) oder foo!(await { x }) wenn ein expr gewünscht wird, sollte es meiner Meinung nach eindeutig sein, dass das Schlüsselwort await gewünscht wird - nicht das Schlüsselwort await Feld oder der await Strukturliteralausdruck mit Feld Init Abkürzung - auch in einer synchronen Methode.

Es werden drei gleichwertige Vorgehensweisen erwartet

Bitte nicht. (Zumindest in core . Offensichtlich können Leute Makros in ihrem eigenen Code erstellen. Wenn sie wollen)

Im Moment würde ein Postfix .await! () -Makro Compiler-Magie erfordern. Wenn jedoch eine Präfixvariante des Schlüsselworts await stabilisiert würde, wäre dies nicht mehr der Fall: Das Makro postfix .await! () Könnte trivial als Makro implementiert werden, das seinem Argument (einem Ausdruck) das Schlüsselwort await voranstellt und umschließt alles in Klammern.

Ich werde bemerken, dass dies ebenso wahr ist - aber einfacher! - in die andere Richtung: Wenn wir die Schlüsselwortsyntax .await stabilisieren, können Benutzer bereits ein awaited!() Präfixmakro erstellen, wenn sie Postfix nicht genug mögen.

Ich mag die Idee nicht, mehrere zulässige Varianten (wie Präfix und Postfix) hinzuzufügen, aber aus einem etwas anderen Grund, als die Benutzer fragen werden. Ich arbeite in einer ziemlich großen Firma und Kämpfe um den Codestil sind real. Ich möchte nur eine offensichtliche und korrekte Form verwenden. Schließlich könnte das Zen der Python in diesem Punkt richtig sein.

Ich mag die Idee nicht, mehrere zulässige Varianten (wie Präfix und Postfix) hinzuzufügen, aber aus einem etwas anderen Grund, als die Benutzer fragen werden. Ich arbeite in einer ziemlich großen Firma und Kämpfe um den Codestil sind real. Ich möchte nur eine offensichtliche und korrekte Form verwenden. Schließlich könnte das Zen der Python in diesem Punkt richtig sein.

Ich weiß, was du meinst. Es gibt jedoch keine Möglichkeit, Programmierer daran zu hindern, ihre eigenen Makros zu definieren, indem sie Dinge wie await!() . Simulare Strukturen sind immer möglich. Es wird also sowieso wirklich verschiedene Varianten geben.

Nun, zumindest nicht await!(...) da dies ein Fehler wäre. Aber wenn ein Benutzer macro_rules! wait { ($e:expr) => { e.await }; } und dies dann als wait!(expr) , sieht er deutlich unidiomatisch aus und fällt wahrscheinlich schnell aus der Mode. Dies verringert die Wahrscheinlichkeit von Variationen im Ökosystem erheblich und ermöglicht es Benutzern, weniger Stile zu lernen. Daher denke ich, dass @yasammez 'Punkt passend ist.

@Centril Wenn jemand schlechte Dinge tun will, kann er kaum aufgehalten werden. Was ist mit _await!() oder awai!() ?

oder, wenn die Unicode-Kennung aktiviert ist, so etwas wie àwait!() .

...

@earthengine Das Ziel ist es, Community- Normen festzulegen (ähnlich wie bei Style Lints und rustfmt ) und nicht zu verhindern, dass verschiedene Leute absichtlich seltsame Dinge tun. Wir haben es hier mit Wahrscheinlichkeiten zu tun, nicht mit absoluten Garantien, _await!() nicht zu sehen.

Lassen Sie uns die Argumente für jede Postfix-Syntax zusammenfassen und überprüfen:

  • __ expr await (Postfix-Schlüsselwort) __: Die Postfix-Schlüsselwortsyntax verwendet das bereits reservierte Schlüsselwort await . Warten ist eine magische Transformation, und die Verwendung eines Schlüsselworts hilft dabei, sich angemessen hervorzuheben. Es sieht nicht wie ein Feldzugriff oder ein Methoden- oder Makroaufruf aus, und wir müssen dies nicht als Ausnahme erklären. Es passt gut zu den aktuellen Parsing-Regeln und zum Operator ? . Der Platz in der Methodenverkettung ist wohl nicht schlechter als bei Generalized Type Ascription , einem RFC, der wahrscheinlich in irgendeiner Form akzeptiert wird. Auf der anderen Seite müssen IDEs möglicherweise mehr Magie anwenden, um eine automatische Vervollständigung für await bereitzustellen. Der Platz in der Methodenverkettung ist möglicherweise zu lästig (obwohl @withoutboats argumentiert hat await jede Zeile beendet. Es wird ein Schlüsselwort verwendet, das wir möglicherweise vermeiden könnten, wenn wir einen anderen Ansatz wählen würden.

  • __ expr.await (Postfix-Feld) __: Die Postfix-Feldsyntax nutzt die "Kraft des Punkts" - sie sieht in der Verkettung natürlich aus und fühlt sich natürlich an und ermöglicht es IDEs, await automatisch zu vervollständigen, ohne andere Operationen auszuführen (z als automatisch den Punkt entfernen ). Es ist genauso präzise wie das Postfix-Keyword. Der Punkt vor dem Warten kann das Auffinden von Instanzen mit grep erleichtern. Auf der anderen Seite sieht es aus wie ein Feldzugang. Als offensichtlicher Feldzugang gibt es keinen visuellen Hinweis darauf, dass "dies etwas bewirkt".

  • __ expr.await() (Postfix-Methode) __: Die Syntax der Postfix-Methode ähnelt dem Postfix-Feld. Auf der anderen Seite zeigt die Aufrufsyntax von () dem Leser an, dass "dies etwas bewirkt". Lokal betrachtet ist es fast genauso await als Methode zu tarnen, könnte sich als verwirrend erweisen. Wir könnten sagen, dass await eine Methode in demselben begrenzten Sinne ist, wie call/cc im Schema eine Funktion ist. Als Methode müssten wir überlegen, ob Future::await(expr) konsistent mit UFCS funktionieren soll .

  • __ expr.await!() (Postfix-Makro) __: Die Postfix-Makrosyntax nutzt in ähnlicher Weise die "Kraft des Punkts". Das Makro ! bang zeigt an, dass "dies etwas Magisches bewirken könnte". Auf der anderen Seite ist dies sogar noch lauter, und während Makros zaubern, zaubern sie normalerweise nicht den umgebenden Code wie await . Unter der Annahme, dass wir eine allgemeine Postfix-Makrosyntax standardisieren, kann es auch Probleme geben , await als Schlüsselwort zu behandeln.

  • __ expr@ , expr# , expr~ und andere Einzelzeichensymbole__: Die Verwendung eines einzelnen Zeichens wie bei ? maximiert die Prägnanz und führt möglicherweise zu einer Postfix-Syntax scheinen natürlicher. Wie bei ? können wir feststellen, dass wir diese Prägnanz schätzen, wenn await beginnt, unseren Code zu durchdringen. Auf der anderen Seite ist es schwer zu erkennen, dass sich ein Konsens über die Kompromisse bildet, die mit der Einführung einer solchen Syntax verbunden sind, bis der Schmerz, dass unser Code mit await übersät ist, zu einem Problem wird.

Ich wollte einen Beitrag schreiben, um mich bei

Ich habe die Idee, dass beim Hinzufügen von "Pipe-Operatoren" wie F # Benutzer entweder Präfix oder Postfix verwenden können (mit explizitem Syntaxunterschied).

// use `|>` for instance, Rust can choose other sigils if there are conflicts with current syntax
await expr
expr |> await

// and we can use this operator on normal function calls too
f(g(h(x))) 
x |> h |> g |> f
// this is more convenient than "postfix macro"
x.h!().g!().f!()

@traviscross Ausgezeichnete Zusammenfassung. Es gab auch einige Diskussionen über das Kombinieren von Siegel und Schlüsselwort, z. B. fut@await , daher füge ich dies hier nur für Leute hinzu, die zu diesem Thread kommen.

Ich habe die Vor- und Nachteile dieser Syntax eingetragen hier . @earthengine sagt, dass andere Siegel als @ möglich sind, wie ~ . @BenoitZugmeyer bevorzugt @await und fragt, ob Postfix-Makros ala expr!await eine gute Idee wären. @dpc argumentiert, dass @await zu ad-hoc ist und sich nicht gut in das integrieren lässt, was wir bereits haben. Außerdem ist Rust bereits siegellastig. @cenwangumass stimmt zu, dass es zu ad-hoc ist. @newpavlov sagt, dass sich der Teil await überflüssig anfühlt, insbesondere wenn wir in Zukunft keine anderen ähnlichen Schlüsselwörter hinzufügen. @nicoburns sagt, dass die Syntax funktionieren könnte und dass es nicht viele Probleme damit gibt, aber dass es zu ad-hoc für eine Lösung ist.

@traviscross tolle Zusammenfassung!

Meine 0,02 $ in der Reihenfolge vom schlechtesten zum besten meiner Meinung nach:

  • 3 ist definitiv ein No-Go, weil es nicht einmal annähernd ein Methodenaufruf ist.
  • 2 ist kein Feld, es ist sehr verwirrend, besonders für Neulinge. await auf der Abschlussliste zu haben, ist nicht wirklich hilfreich. Nachdem Sie Tonnen von ihnen geschrieben haben, schreiben Sie es einfach als fn oder extern . Eine zusätzliche Vervollständigung ist noch schlimmer als nichts, da mir anstelle nützlicher Methoden / Felder ein Schlüsselwort vorgeschlagen wird.
  • 4 Makro ist etwas, das hier passt, aber es passt nicht gut zu mir. Ich meine Asymmetrie mit async als Schlüsselwort, wie ich oben erwähnt habe
  • 5 Siegel mögen zu knapp und schwerer zu erkennen sein, aber es ist eine getrennte Einheit und kann so behandelt werden. Sieht nichts anderem ähnlich und führt daher zu keiner Verwirrung für die Benutzer
  • 1 Bester Ansatz IMHO, es ist nur ein Siegel, das so leicht zu erkennen ist und das bereits ein reserviertes Schlüsselwort ist. Raumtrennung ist, wie oben erwähnt, ein Vorteil, kein Fehler. Es ist in Ordnung zu existieren, wenn keine Formatierung durchgeführt wird, aber mit rustfmt es noch weniger bedeutsam.

Als jemand, der gespannt auf diesen Moment gewartet hat, sind hier auch meine $ 0,02.

Zum größten Teil stimme ich @Pzixel zu , außer in den letzten beiden Punkten, in dem Sinne, dass ich sie vertauschen oder vielleicht sogar beide als Option geben würde, wie bei try!() / ? Paar. Ich habe immer noch Leute gesehen, die aus Gründen der Klarheit für try!() über ? gestritten haben, und ich denke, eine Agentur über das Aussehen Ihres Codes in diesem speziellen Fall zu haben, ist eher ein Vorteil als ein Nachteil , auch auf Kosten von zwei verschiedenen Syntaxen.

Insbesondere ein await Postfix-Schlüsselwort ist großartig, wenn Sie Ihren asynchronen Code als explizite Folge von Schritten schreiben, z

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

Auf der anderen Seite können Sie es vorziehen, stattdessen etwas Komplizierteres zu tun, wie bei der typischen Rust-y-Methode, z

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

Ich denke, in diesem Fall ist aus dem Zusammenhang bereits ziemlich klar, dass es sich um eine Zukunft handelt, und die Ausführlichkeit der await -Tastatur ist überflüssig. Vergleichen Sie:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

Eine andere Sache, die ich an der Postfix-Syntax mit einem Symbol mag, ist, dass sie klar macht, dass das Symbol zum Ausdruck gehört , während (für mich persönlich) das freistehende await ein bisschen ... verloren aussieht ? :) :)

Ich denke, ich versuche zu sagen, dass await in Anweisungen besser aussieht, während ein Postfix mit einem Symbol in Ausdrücken besser aussieht.

Das sind sowieso nur meine Gedanken.

Da dies bereits die Mutter aller Fahrradabwurffäden ist, wollte ich ein weiteres Siegel hinzufügen, das bisher nicht erwähnt wurde. AFAICT: -> .

Die Idee ist, dass es das -> aus der Rückgabetypdeklaration der folgenden Funktion widerspiegelt, wobei - die Funktion ist async - der erwartete Rückgabetyp ist.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

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

Oben erhalten Sie von send()-> ein Result<Response, HttpError> , genau wie in der Funktionsdeklaration geschrieben.

Dies sind meine $ 0,02, nachdem ich den größten Teil der obigen Diskussion gelesen und einige Tage über die vorgeschlagenen Optionen nachgedacht habe. Es gibt keinen Grund, meiner Meinung Gewicht zu verleihen - ich bin nur eine zufällige Person im Internet. Ich werde wahrscheinlich nicht weiter darauf eingehen, da ich vorsichtig bin, der Diskussion keinen weiteren Lärm hinzuzufügen.


Ich mag ein Postfix-Siegel. Postfix-Siegel haben Vorrang, und ich denke, es würde sich mit dem Rest der Sprache vereinbar anfühlen. Ich habe keine besondere Vorliebe für das verwendete Siegel - keines ist überzeugend, aber ich denke, das wird mit der Vertrautheit verblassen. Ein Siegel führt im Vergleich zu anderen Postfix-Optionen das geringste Rauschen ein.

Ich habe nichts dagegen, . (beim Verketten) durch -> ersetzen, um zu warten. Es ist immer noch prägnant und eindeutig, aber ich würde etwas bevorzugen, das in den gleichen Kontexten wie ? .

Ich mag die anderen Postfix-Optionen, die vorgestellt wurden, nicht. await ist weder ein Feld noch eine Methode und fühlt sich daher völlig inkonsistent mit dem Rest der Sprache, wenn ein Kontrollflusskonstrukt als solches dargestellt wird. Es gibt keine Postfix-Makros oder Schlüsselwörter in der Sprache, daher scheint dies auch inkonsistent zu sein.

Wenn ich neu in der Sprache wäre und sie nicht vollständig kenne, würde ich annehmen, dass .await oder .await() keine speziellen Sprachfunktionen sind und Felder oder Methoden für einen Typ sind - wie dies bei der Fall ist jedes andere Feld und jede andere Methode in der Sprache, die der Benutzer sehen wird. Wenn .await!() ausgewählt wurde, kann der Benutzer versuchen, zu lernen, wie er seine eigenen Postfix-Makros definiert (genau wie er gelernt hat, seine eigenen Präfix-Makros zu definieren). Wenn dieser Benutzer ein Siegel gesehen hat, muss er möglicherweise die Dokumentation nachschlagen (genau wie ? ), aber er würde es nicht mit etwas anderem verwechseln und Zeit damit verschwenden, die Definition von .await() oder Dokumentation für das Feld .await .

Ich mag ein Präfix await { .. } . Dieser Ansatz hat eindeutigen Vorrang, weist jedoch Probleme auf (die bereits ausführlich erörtert wurden). Trotzdem denke ich, dass es für diejenigen von Vorteil wäre, die Kombinatoren bevorzugen. Ich möchte nicht, dass dies die einzige implementierte Option ist, da es bei der Methodenverkettung nicht ergonomisch ist, aber ich denke, es würde ein Postfix-Siegel gut ergänzen.

Ich mag andere Präfixoptionen nicht, sie fühlen sich nicht so konsistent mit anderen Kontrollflusskonstrukten in der Sprache an. Ähnlich wie bei einer Postfix-Methode ist eine Präfixfunktion inkonsistent. In der für den Kontrollfluss verwendeten Sprache sind keine anderen globalen Funktionen integriert. Es werden auch keine Makros für den Kontrollfluss verwendet (mit Ausnahme von try!(..) aber das ist veraltet, weil wir eine bessere Lösung haben - ein Postfix-Siegel).


Fast alle meine Vorlieben beschränken sich auf das, was sich (für mich) natürlich und beständig anfühlt. Unabhängig davon, welche Lösung gewählt wird, sollte vor der Stabilisierung Zeit zum Experimentieren eingeräumt werden. Durch praktische Erfahrungen kann weitaus besser beurteilt werden, welche Option am besten ist als Spekulation.

Es ist auch zu bedenken, dass es möglicherweise eine stille Mehrheit gibt, die völlig andere Meinungen haben könnte - diejenigen, die an diesen Diskussionen teilnehmen (einschließlich meiner selbst), sind nicht unbedingt repräsentativ für alle Rust-Benutzer (dies ist nicht als geringfügig oder zu sein gemeint in irgendeiner Weise beleidigend).

tl; dr Postfix-Siegel sind eine natürliche Art, das Warten auszudrücken (aufgrund der Priorität) und ein prägnanter, konsistenter Ansatz. Ich würde ein Präfix await { .. } und ein Postfix @ hinzufügen (das in den Kontexten als ? ). Für mich ist es am wichtigsten, dass Rust intern konsistent bleibt.

@ SamuelMoriarty

Ich denke, in diesem Fall ist aus dem Zusammenhang bereits ziemlich klar, dass es sich um eine Zukunft handelt, und die Ausführlichkeit der erwarteten Tastatur ist überflüssig. Vergleichen Sie:

Entschuldigung, aber ich habe ~ auf einen Blick nicht einmal gesehen. Ich habe den ganzen Kommentar noch einmal gelesen und meine zweite Lektüre war erfolgreicher. Macht eine gute Erklärung, warum ich denke, dass Siegel schlimmer ist. Es ist nur eine Meinung, aber es wurde nur noch einmal bestätigt.

Ein weiterer Punkt gegen Siegel ist, dass Rust zu J wird. Zum Beispiel:

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

?@? steht für "Funktion, die einen Fehler oder eine Zukunft zurückgeben kann, auf die gewartet werden sollte, wobei ein Fehler, falls vorhanden, an einen Anrufer weitergegeben wird".

Ich hätte gerne mehr

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

@rolandsteiner

Da dies bereits die Mutter aller Fahrradabwurffäden ist, wollte ich ein weiteres Siegel hinzufügen, das bisher nicht erwähnt wurde. AFAICT: ->.

Grammatik kontextabhängig zu machen, macht die Dinge nicht besser, sondern nur schlechter. Dies führt zu schlimmeren Fehlern und langsameren Kompilierungszeiten.

Wir haben eine separate Bedeutung für -> , es hat nichts mit async/await zu tun.

Ich würde das Präfix await { .. } und, wenn möglich, ein Postfix ! Sigil bevorzugen.
Ein Ausrufezeichen würde die Faulheit von Futures subtil betonen. Sie werden erst ausgeführt, wenn ein Befehl mit einem Ausrufezeichen angegeben wurde.

Das obige Beispiel würde dann so aussehen:

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

Es tut mir leid, wenn meine Meinung nicht relevant ist, da ich wenig Erfahrung mit Async und keine Erfahrung mit dem Async-Funktions-Ökosystem von Rust habe.

Wenn ich mir jedoch .send()?!?.json()?!?; und andere Kombinationen wie diese anschaue, verstehe ich die grundlegenden Gründe, warum der auf Siegeln basierende Vorschlag für mich falsch aussieht.

Erstens habe ich das Gefühl, dass verkettete Siegel schnell unlesbar werden, wo immer es ?!? oder ?~? oder ?->? . Dies ist eine weitere Sache, über die Anfänger stolpern und erraten, ob es sich um einen oder mehrere Operatoren handelt. Die Informationen sind zu dicht gepackt.

Zweitens habe ich im Allgemeinen das Gefühl, dass Wartepunkte sowohl seltener als Fehlerausbreitungspunkte als auch bedeutender sind. Die Wartepunkte sind so bedeutend, dass ich es verdient habe, eine Stufe in der Kette für sich zu sein, keine "kleine Transformation", die mit einer anderen Stufe verbunden ist (und insbesondere nicht "nur eine von kleinen Transformationen"). Ich würde wahrscheinlich auch mit dem Präfix-Schlüsselwort nur in Ordnung sein (was fast ein Warten zwingt, um die Kette zu brechen), aber im Allgemeinen fühlt sich das zu restriktiv an. Meine ideale Option wäre wahrscheinlich ein .await!() methodenähnliches Makro mit der zukünftigen Möglichkeit, das Makrosystem zu erweitern, um methodenähnliche Benutzermakros zuzulassen.

Die minimale Basis für die Stabilisierung ist meiner Meinung nach der Präfixoperator await my_future . Alles andere kann folgen.

expr....await würde den Kontext für etwas widerspiegeln, das bis zum Warten läuft und mit den Rustlang-Operatoren übereinstimmt. Das asynchrone Warten ist auch ein paralleles Muster. Das Warten kann nicht als Methode oder ähnliche Eigenschaft ausgedrückt werden

Ich habe meine Neigung zu await!(foo) , aber da andere darauf hingewiesen haben, dass dies uns zwingen würde, das Warten abzuwarten und somit seine zukünftige Verwendung als Betreiber bis 2021 auszuschließen, werde ich mit await { foo } als meine Präferenz. Was Postfix erwartet, habe ich keine besondere Meinung.

Ich weiß, dass es vielleicht nicht der beste Ort ist, um über implizites Warten zu sprechen, aber würde so etwas wie ein explizites implizites Warten funktionieren? Wir müssen also zuerst das Präfix auf die Landung warten:

await { future }?

Und später fügen wir etwas Ähnliches hinzu

let result = implicit await { client.get("https://my_api").send()?.json()?; }

oder

let result = auto await { client.get("https://my_api").send()?.json()?; }

Bei Auswahl des impliziten Modus wird automatisch alles zwischen {} erwartet.

Dies hat die Syntax von await vereinheitlicht und würde die Notwendigkeit des Wartens auf Präfixe, der Verkettung und der möglichst expliziten Darstellung ausgleichen.

Ich entschied mich für rg --type csharp '[^ ]await' um Beispiele von Stellen zu untersuchen, an denen das Präfix nicht optimal war. Es ist möglich, dass nicht alle perfekt sind, aber es handelt sich um echten Code, der eine Codeüberprüfung durchlaufen hat. (Beispiele leicht bereinigt, um bestimmte Domain-Modell-Dinge zu entfernen.)

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Verwenden von FluentAssertions als eine assert_eq! Assert.Equal .

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Diese allgemeine Sache von "Schau, ich brauchte wirklich nur eine Sache davon" ist ein Haufen von ihnen.

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Ein weiteres "Ich brauchte nur eine Eigenschaft". (Nebenbei: "Mann, bin ich froh, dass Rust keine CancellationToken s braucht.)

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

Das gleiche .collect() , das die Leute in Rust erwähnt haben.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

Ich hatte darüber nachgedacht, vielleicht ein Postfix-Schlüsselwort mit rustfmt-Zeilenumbrüchen danach zu mögen (und nach einem ? , falls vorhanden), aber es ist so, dass ich denke, dass der Zeilenumbruch im Allgemeinen nicht gut ist.

else if (!await container.ExistsAsync())

Eine der seltenen, bei denen das Präfix tatsächlich nützlich war.

var response = (HttpWebResponse)await request.GetResponseAsync();

Es gab einige Casts, obwohl Casts natürlich ein weiterer Ort sind, an dem Rust Postfix ist, C # jedoch Präfix.

using (var response = await this.httpClient.SendAsync(requestMsg))

Dieses eine Präfix gegen Postfix spielt keine Rolle, aber ich denke, es ist ein weiterer interessanter Unterschied: Da C # nicht Drop , müssen einige Dinge in Variablen eingegeben und nicht verkettet werden.

einige von @scottmcm ‚s Beispiele in den verschiedenen Postfix rustified Varianten:

// keyword
response.content.read_as_string()) await?.should().be(text);
// field
response.content.read_as_string()).await?.should().be(text);
// function
response.content.read_as_string()).await()?.should().be(text);
// macro
response.content.read_as_string()).await!()?.should().be(text);
// sigil
response.content.read_as_string())@?.should().be(text);
// sigil + keyword
response.content.read_as_string())@await?.should().be(text);
// keyword
let previous = branch.list_history(timestamp_utc, None, 1) await?.history_entries.single_or_default();
// field
let previous = branch.list_history(timestamp_utc, None, 1).await?.history_entries.single_or_default();
// function
let previous = branch.list_history(timestamp_utc, None, 1).await()?.history_entries.single_or_default();
// macro
let previous = branch.list_history(timestamp_utc, None, 1).await!()?.history_entries.single_or_default();
// sigil
let previous = branch.list_history(timestamp_utc, None, 1)@?.history_entries.single_or_default();
// sigil + keyword
let previous = branch.list_history(timestamp_utc, None, 1)@await?.history_entries.single_or_default();
// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;
// field
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await?.identity).await?;
// function
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await()?.identity).await()?;
// macro
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await!()?.identity).await!()?;
// sigil
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@?.identity)@?;
// sigil + keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@await?.identity)@await?;

(danke an @ Nemo157 für Korrekturen)

// keyword
let pending = transaction.connection.query(command) await.into_iter().collect::<Vec<EventView>>();
// field
let pending = transaction.connection.query(command).await.into_iter().collect::<Vec<EventView>>();
// function
let pending = transaction.connection.query(command).await().into_iter().collect::<Vec<EventView>>();
// macro
let pending = transaction.connection.query(command).await!().into_iter().collect::<Vec<EventView>>();
// sigil
let pending = transaction.connection.query(command)@.into_iter().collect::<Vec<EventView>>();
// sigil + keyword
let pending = transaction.connection.query(command)@await.into_iter().collect::<Vec<EventView>>();

Nach dem Lesen ist für mich @ Sigil vom Tisch, da es gerade vor ? nur unsichtbar ist.

Ich habe noch niemanden in diesem Thread gesehen, der über die erwartete Variante für Stream diskutiert hat. Ich weiß, dass es außerhalb des Rahmens liegt, aber sollten wir darüber nachdenken?

Es wäre eine Schande, wenn wir eine Entscheidung treffen würden, die sich als Blocker für das Warten auf Streams herausstellt.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

Sie können await in einem Abschluss wie diesem verwenden. Es müsste eine zusätzliche Erweiterungsmethode für Option , die einen asynchronen Abschluss verwendet und einen Future selbst zurückgibt (at Momentan glaube ich, dass die Signatur der Erweiterungsmethode nicht spezifizierbar ist, aber wir hoffen, dass wir irgendwann einen Weg finden, um asynchrone Abschlüsse nutzbar zu machen .

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(oder eine direktere Übersetzung des Codes mit if let anstelle eines Kombinators)

@ Nemo157 Yeal, aber wir können es wahrscheinlich ohne zusätzliche Funktionen tun:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Aber der Ansatz von if let scheint mir in der Tat natürlicher zu sein.

Nach dem Lesen ist @ sigil für mich vom Tisch, da es gerade vor? Unsichtbar ist.

Vergessen Sie nicht alternative Code-Hervorhebungsschemata. Für mich sieht dieser Code folgendermaßen aus:
1

Persönlich denke ich nicht, dass "Unsichtbarkeit" hier ein größeres Problem ist als für eigenständige ? .

Und ein cleveres Farbschema kann es noch auffälliger machen (z. B. durch Verwendung einer anderen Farbe als für ? ).

@newpavlov Sie können das Farbschema in externen Tools nicht auswählen, z. B. in den Registerkarten für die Überprüfung von Gitlab / Github.

Trotzdem ist es keine gute Praxis, sich nur auf das Hervorheben zu verlassen. Andere haben möglicherweise andere Vorlieben.

Hallo Leute, ich bin ein neuer Rust-Lerner aus C ++. Ich möchte nur kommentieren, dass so etwas nicht wäre

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

oder

id = id.or_else_async(async || { 
    self.storage_coordinator.get_default_widget_async() await?.identity 
}) await?;

grundsätzlich unverständlich sein? Das await wird gegen Ende der Zeile verschoben, während unser Fokus hauptsächlich am Anfang konzentriert ist (ähnlich wie bei der Verwendung einer Suchmaschine).
Etwas wie

id =  await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

oder

id = auto await {
    id.or_else_async(async || { self.storage_coordinator.get_default_widget_async()?.identity })
}?;

vorgeschlagen früher sieht für mich viel besser aus.

Genau. Die ersten paar Wörter sind das erste, was jemand beim Scannen von Code, Suchergebnissen, Textabschnitten usw. betrachtet. Dies benachteiligt sofort jede Art von Postfix.

Für die Platzierung von ? bin ich überzeugt, dass await? foo ausreichend eindeutig und leicht zu erlernen ist.

Wenn wir dies jetzt stabilisieren und nach zwei Jahren der Nutzung entscheiden, dass wir wirklich etwas Besseres für die Verkettung wollen, können wir Postfix-Makros als allgemeine Funktion betrachten.

Ich möchte eine Variation der Idee vorschlagen, dass @nicoburns sowohl Pre- als auch Postfix erwartet.
Wir könnten zwei Dinge haben:

  • ein Präfix await Schlüsselwort (zum Beispiel das Aroma mit einer stärkeren Bindung als ? , aber das ist weniger wichtig)
  • eine neue Methode für std::Future , zum Beispiel fn awaited(self) -> Self::Output { await self } . Sein Name könnte auch block_on oder blocking oder etwas Besseres, das sich jemand anderes einfallen lässt.

Dies würde sowohl die "einfache" Präfixverwendung als auch die Verkettung ermöglichen, ohne dass ein kontextbezogenes Schlüsselwort abgewartet werden muss.

Technisch gesehen kann der zweite Aufzählungspunkt auch mit einem Postfix-Makro erreicht werden. In diesem Fall würden wir .awaited!() schreiben.

Dies würde Code ermöglichen, der so aussieht:

let done = await delayed;

let value = await delayed_result?;

let value2 = await some.thing()?;

let value3 = some.other().thing().awaited()?;

let value4 = promise
        .awaited()
        .map_err(|e| e.into())?
        .obtain_other_future()
        .awaited();

Abgesehen von anderen Aspekten besteht der Hauptpunkt dieses Vorschlags darin, await als Grundbaustein des Mechanismus zu verwenden, genau wie match , und dann Kombinatoren zu haben, damit wir nicht viel eingeben müssen Schlüsselwörter und Klammern aller Art. Ich denke, dies bedeutet, dass sie auf die gleiche Weise unterrichtet werden können: zuerst die einfachen await , dann, um zu viele Klammern zu vermeiden, kann man .awaited() und wegketten.


Alternativ könnte eine magischere Version das Schlüsselwort await vollständig löschen und sich auf eine magische Methode .awaited() auf std :: Future stützen, die nicht von anderen implementiert werden kann, die ihre eigenen Futures schreiben, aber ich denke Dies wäre ziemlich eingängig und ein zu großer Sonderfall.

eine neue Methode für std::Future , zum Beispiel fn awaited(self) -> Self::Output { await self }

Ich bin mir ziemlich sicher, dass dies unmöglich ist (ohne die Funktion magisch zu machen), denn um darin zu warten, müsste es async fn awaited(self) -> Self::Output { await self } , was selbst immer noch await ed sein müsste. Und wenn wir darüber nachdenken, die Funktion magisch zu machen, könnte es genauso gut das Schlüsselwort IMO sein.

Es gibt bereits Future::wait (obwohl anscheinend 0.3 es noch nicht hat?), Das eine Zukunft ausführt, die den Thread blockiert.

Das Problem ist, dass der springende Punkt von await besteht, den Thread nicht zu blockieren. Wenn die zu wartende Syntax das Präfix ist und wir eine Postfix-Option wünschen, muss es sich um ein Postfix-Makro und nicht um eine Methode handeln.

Und wenn Sie sagen wollen, verwenden Sie eine magische Methode, nennen Sie sie einfach .await() , was bereits mehrfach im Thread diskutiert wurde, sowohl als Keyword-Methode als auch als magische extern "rust-await-magic" "real "fn.

EDIT: scottmcm ninja'd mich und GitHub haben mich nicht informiert (weil mobil?), Ich werde dies aber trotzdem verlassen.

@scottmcm Könnten Sie auch die Häufigkeit ermitteln, bei der await OK oder suboptimal schien? Ich denke, eine Umfrage zur Häufigkeit von Präfixen und Postfixen könnte helfen, mehrere Fragen zu beantworten.

  1. Ich habe den Eindruck, dass der bisher beste Anwendungsfall für Postfix await war
client.get("https://my_api").send() await?.json() await?

so viele Beiträge als Beispiel verwenden. Vielleicht habe ich etwas verpasst, aber gibt es noch andere Fälle? Wäre es nicht gut, wenn Sie diese Zeile in eine Funktion extrahieren, wenn sie häufig in der gesamten Codebasis angezeigt wird?

  1. Wie ich bereits besprochen habe, wenn einige Leute schreiben
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Was sie tun, ist genau das await visuell zu verbergen, anstatt es explizit zu machen, damit jede Fließgrenze klar erkennbar ist .

  1. Für das Postfix-Schlüsselwort müsste etwas erfunden werden, das in anderen gängigen Sprachen nicht vorhanden ist. Wenn es kein wesentlich besseres Ergebnis bietet, lohnt es sich nicht, es zu erfinden.

Das Warten wird gegen Ende der Zeile verschoben, während sich unser Fokus hauptsächlich auf den Anfang konzentriert (ähnlich wie bei der Verwendung einer Suchmaschine).

Ich kann natürlich nicht widerlegen, dass Leute Webinhalte mit einem F-förmigen Muster scannen , @ dowchris97.

Es ist jedoch nicht selbstverständlich, dass sie dasselbe Muster für Code verwenden. Zum Beispiel klingt dieses andere auf der Seite so, als würde es besser dazu passen, wie Leute nach ? suchen, und daher nach await :

Das gefleckte Muster besteht darin, große Textblöcke zu überspringen und zu scannen, als ob nach etwas Bestimmtem gesucht würde, z. B. nach einem Link, Ziffern, einem bestimmten Wort oder einer Reihe von Wörtern mit einer bestimmten Form (z. B. einer Adresse oder Unterschrift).

Nehmen wir zum Vergleich dasselbe Beispiel, wenn es Result anstelle von impl Future :

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

Ich denke, es ist ziemlich klar, als ein F-förmiges Lesemuster für solchen Code nicht hilft, die ? zu finden, und es hilft auch nicht, wenn es in mehrere Zeilen aufgeteilt ist

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

Ich denke, eine Beschreibung analog zu Ihrer könnte verwendet werden, um zu argumentieren, dass die Postfix-Position "die ? genau visuell verbirgt, anstatt sie explizit zu machen, damit jeder Rückgabepunkt klar erkennbar ist", dem ich zustimme Die ursprünglichen Bedenken bezüglich ? , aber es scheint in der Praxis kein Problem gewesen zu sein.

Insgesamt denke ich, dass es der beste Weg ist, um sicherzustellen, dass die Leute es sehen, wenn die Leute bereits darauf trainiert sind, nach ? zu suchen. Ich würde definitiv lieber, dass sie nicht zwei verschiedene Scans gleichzeitig verwenden müssen.

@scottmcm

Es ist jedoch nicht selbstverständlich, dass sie dasselbe Muster für Code verwenden. Zum Beispiel klingt dieses andere auf der Seite so, als würde es besser dazu passen, wie Leute nach ? suchen, und daher nach await :

Das gefleckte Muster besteht darin, große Textblöcke zu überspringen und zu scannen, als ob nach etwas Bestimmtem gesucht würde, z. B. nach einem Link, Ziffern, einem bestimmten Wort oder einer Reihe von Wörtern mit einer bestimmten Form (z. B. einer Adresse oder Unterschrift).

Es gibt sicherlich auch keine Hinweise darauf, dass die Verwendung von Fleckenmustern das Verständnis von Code besser / schneller erleichtert. Besonders wenn Menschen am häufigsten F-förmige Muster verwenden, die ich später erwähnen werde.

Nehmen Sie diese Zeile als Beispiel. Wenn Sie mit der Verwendung von Fleckenmustern beginnen, nehmen Sie an, dass Sie dies noch nie zuvor gelesen haben.

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Für mich habe ich angefangen, als ich async , dann gehe ich zuerst zu await? , dann zu self.storage_coordinator.get_default_widget_async() , dann zu .identity , dann habe ich endlich die gesamte Linie realisiert ist asynchron. Ich würde sagen, dass dies definitiv nicht das Leseerlebnis ist, das ich mag. Ein Grund ist, dass unser geschriebenes Sprachsystem diese Art von verschachteltem Vorwärts- und Rückwärtssprung nicht hat . Wenn Sie springen, unterbricht es die Erstellung eines mentalen Modells dessen, was diese Linie tut.

Was macht diese Zeile zum Vergleich, woher weißt du das?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

Sobald ich await? , hatte ich sofort ein Heads-up, dass dies asynchron ist. Ich las dann let widget = await? , wieder ohne Schwierigkeiten, ich wusste, dass dies asynchron ist, es passiert etwas. Ich habe das Gefühl, dem F-Form-Muster zu folgen. Laut https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/ ist die F-Form das am häufigsten verwendete Muster. Werden wir also ein System entwerfen, das zur menschlichen Natur passt, oder ein System erfinden, das besondere Bildung benötigt und gegen unsere Natur arbeitet ? Ich bevorzuge die erste. Der Unterschied wäre noch größer, wenn asynchrone und normale Linien auf diese Weise verschachtelt würden

await? id.or_else_async(async || {
    let widget1 = await? self.storage_coordinator.get_default_widget_async();
    let result1 = do_some_wierd_computation_on(widget1.identity);
    let widget2 = await? self.network_coordinator.get_default_widget_async();
    let result2 = do_some_strange_computation_on(widget2.identity);
});

Soll ich über Zeilen hinweg nach await? suchen?

let id = id.try_or_else (|| Ok (self.storage_coordinator.try_get_default_widget ()?. identity))?;

Dafür finde ich den Vergleich nicht gut. Erstens ändert ? das mentale Modell nicht so sehr. Zweitens liegt der Erfolg von ? in der Tatsache, dass Sie nicht vorwärts und rückwärts zwischen den Wörtern springen müssen. Das Gefühl, das ich beim Lesen von ? in .try_get_default_widget()? ist: "Okay, Sie erzielen ein gutes Ergebnis daraus." Das ist es. Ich muss nicht zurückspringen, um etwas anderes zu lesen, um diese Zeile zu verstehen.

Meine allgemeine Schlussfolgerung ist also, dass Postfix beim Schreiben von Code möglicherweise nur einen begrenzten Komfort bietet. Es kann jedoch größere Probleme beim Lesen von Code verursachen, was meiner Meinung nach häufiger ist als das Schreiben.

Wie dies tatsächlich mit einer vorgeschlagenen Hervorhebung von rustfmt Stil und Syntax aussehen würde ( while -> async , match -> await ):

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

Ich weiß nichts über dich, aber ich sehe die match sofort.

(Hey @ CAD97 , ich habe es behoben!)

@ dowchris97

Wenn Sie argumentieren wollen, dass ? das mentale Modell nicht ändert, was ist dann falsch an meinem Argument, dass await Ihr mentales Modell des Codes nicht ändern sollte? (Aus diesem Grund habe ich zuvor implizite Warteoptionen bevorzugt, obwohl ich überzeugt war, dass dies nicht für Rust geeignet ist.)

Insbesondere lesen Sie async am Anfang des Funktionsheaders. Wenn die Funktion so groß ist, dass sie nicht auf einen Bildschirm passt, ist sie mit ziemlicher Sicherheit zu groß und Sie haben andere, größere Lesbarkeitsprobleme als das Finden der await -Punkte.

Sobald Sie async , müssen Sie diesen Kontext beibehalten. Aber zu diesem Zeitpunkt wartet nur noch eine Transformation, die eine verzögerte Berechnung Future in das Ergebnis Output umwandelt, indem der aktuelle Ausführungszug geparkt wird.

Es sollte keine Rolle spielen, dass dies eine komplizierte Zustandsmaschinentransformation bedeutet. Das ist ein Implementierungsdetail. Der einzige Unterschied zu Betriebssystem-Threads und zum Blockieren besteht darin, dass möglicherweise anderer Code auf dem aktuellen Thread ausgeführt wird, während Sie auf die verzögerte Berechnung warten, sodass Sync Sie nicht so sehr schützt. (Wenn überhaupt, würde ich das als Voraussetzung dafür lesen, dass async fn Send + Sync anstatt abgeleitet zu werden und threadsicher zu sein.)

In einem viel funktionsorientierteren Stil würden Ihre Widgets und Ergebnisse so aussehen (ja, ich weiß, dass hier keine Monade verwendet wird und echte Reinheit usw. verzeihen Sie mir):

    let widget1 = await(get_default_widget_async(storage_coordinator(self)));
    let result1 = do_some_wierd_computation_on(identity(widget1));
    let widget2 = await(get_default_widget_async(network_coordinator(self)));
    let result2 = do_some_strange_computation_on(identity(widget2));

Da dies jedoch die umgekehrte Reihenfolge gegenüber der Prozesspipeline ist, hat die funktionale Masse den Operator "Pipeline" erfunden: |> :

    let widget1 = self |> storage_coordinator |> get_default_widget_async |> await;
    let result1 = widget1 |> identity |> do_some_wierd_computation_on;
    let widget2 = self |> network_coordinator |> get_default_widget_async |> await;
    let result2 = widget2 |> identity |> do_some_strange_computation_on;

Und in Rust ist dieser Pipelining-Operator . , der einen Überblick darüber gibt, was durch Methodenanwendung per Pipeline und typgesteuert nachgeschlagen werden kann:

    let widget1 = self.storage_coordinator.get_default_widget_async().await();
    let result1 = widget1.identity.do_some_wierd_computation_on();
    let widget2 = self.network_coordinator.get_default_widget_async().await;
    let result2 = widget2.identity.do_some_strange_computation_on();

Wenn Sie sich . als Pipelining-Daten vorstellen, wie dies bei |> Fall ist, sind die in Rust häufig vorkommenden längeren Ketten sinnvoller, und wenn Sie gut formatiert sind (wie in Centrils Beispiel), ziehen Sie sie an Verpassen Sie nicht die Lesbarkeit, da Sie nur eine vertikale Pipeline von Transformationen für die Daten haben.

await sagt dir nicht "Hey, das ist asynchron". async tut es. await ist genau das, was Sie parken und auf die verzögerte Berechnung warten, und es ist absolut sinnvoll, sie dem Pipelining-Betreiber von Rust zur Verfügung zu stellen.

(Hey @Centril, du hast vergessen, das zu einem async fn (oder while fn ) zu machen, was meinen Standpunkt etwas verwässert 😛)

ob wir den Makroaufruf neu definieren könnten oder nicht

m!(item1, item2)

ist das gleiche wie

item1.m!(item2)

So können wir sowohl den Präfix- als auch den Postfix-Stil abwarten

await!(future)

und

future.await!()

@ CAD97

await sagt dir nicht "Hey, das ist asynchron". async tut es. await ist genau das, was Sie parken und auf die verzögerte Berechnung warten, und es ist absolut sinnvoll, sie dem Pipelining-Betreiber von Rust zur Verfügung zu stellen.
Ja, ich glaube, ich verstehe richtig, habe aber nicht rigoros geschrieben.

Ich verstehe auch Ihren Punkt. Aber nicht überzeugt, ich denke immer noch, dass es enorm besser sein wird, await in den Vordergrund zu stellen.

Ich weiß, dass |> in anderen Sprachen verwendet wird, um etwas anderes zu bedeuten, aber es sieht für mich in Rust anstelle des Präfixes await ziemlich gut und extrem klar aus:

// A
if |> db.is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match |> db.load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = |> client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()?
    .error_for_status()?;

// D
let mut res: InboxResponse =
    |> client.get(inbox_url)
        .headers(inbox_headers)
        .send()?
        .error_for_status()?
    |> .json()?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = |> self.request(url, Method::GET, None, true)?
               |> .res.json::<UserResponse>()?
                  .user
                  .into();

    Ok(user)
}

Das Argument über die Lesereihenfolge würde ebenso gut für den Operator ? , der try!() . Schließlich ist "hey, das könnte nachgeben" wichtig, aber "hey, das könnte früh zurückkehren" ist auch wichtig, wenn nicht noch wichtiger. Tatsächlich wurden in der Diskussion über ? (einschließlich dieses internen Threads und dieses GitHub-Problems ) wiederholt Bedenken hinsichtlich der Sichtbarkeit geäußert. Aber die Gemeinde entschied sich schließlich dafür, es zu genehmigen, und die Leute gewöhnten sich daran. Es wäre seltsam, jetzt die Modifikatoren ? und await auf gegenüberliegenden Seiten eines Ausdrucks zu haben, nur weil die Community im Grunde genommen ihre Meinung darüber geändert hat, wie wichtig Sichtbarkeit ist.

Das Argument über die Lesereihenfolge würde ebenso gut für den Operator ? , der try!() . Schließlich ist "hey, das könnte nachgeben" wichtig, aber "hey, das könnte früh zurückkehren" ist auch wichtig, wenn nicht noch wichtiger. Tatsächlich wurden in der Diskussion über ? (einschließlich dieses internen Threads und dieses GitHub-Problems ) wiederholt Bedenken hinsichtlich der Sichtbarkeit geäußert. Aber die Gemeinde entschied sich schließlich dafür, es zu genehmigen, und die Leute gewöhnten sich daran. Es wäre seltsam, jetzt die Modifikatoren ? und await auf gegenüberliegenden Seiten eines Ausdrucks zu haben, nur weil die Community im Grunde genommen ihre Meinung darüber geändert hat, wie wichtig Sichtbarkeit ist.

Es geht wirklich nicht darum, was Ihr Code tut. Es geht um Ihr mentales Modell dessen, was Ihr Code tut, ob das Modell einfach erstellt werden kann oder nicht, ob es Schocks gibt oder nicht, ob es intuitiv ist oder nicht. Diese können sich stark voneinander unterscheiden.

Ich möchte das nicht weiter diskutieren. Ich denke, die Community ist weit davon entfernt, sich auf eine Postfix-Lösung zu einigen, obwohl viele Leute hier diese möglicherweise unterstützen. Aber ich denke, es könnte eine Lösung geben:

Mozilla baut Firefox richtig? Alles dreht sich um UI / UX! Was ist mit einer ernsthaften HCI-Untersuchung dieses Problems? So können wir uns wirklich gegenseitig überzeugen, indem wir Daten verwenden, die keine Vermutungen sind.

@ dowchris97 In diesem Thread gab es (nur) zwei Fälle, in denen Code aus der realen Welt verglichen wurde: Ein Vergleich von ~ 24.000 Zeilen mit Datenbanken und reqwest und ein Vergleich , der veranschaulicht, warum C # kein genauer Vergleich zur Basis-Rust-Syntax ist . Beide stellen sich heraus, dass das Warten in Postfix im realen Rust-Code natürlicher erscheint und nicht unter Problemen leidet, die andere Sprachen ohne natürliche Wertbewegungssemantik haben. Ich bin ziemlich davon überzeugt, dass die Präfixsyntax eine Notwendigkeit ist, die andere Sprachen sich selbst auferlegen, es sei denn, jemand taucht mit einem anderen Beispiel aus der realen Welt auf, das das Gegenteil zeigt von links nach rechts macht fast immer genau das, was das mentale Modell vorschlagen würde).

Bearbeiten: Nur wenn es nicht klar genug ist, hat keine von C #, Python, C ++, Javascript Mitgliedsmethoden, die self Wert anstelle der Referenz verwenden. C ++ hat die engsten Werte mit rWert-Referenzen, aber die Destruktorreihenfolge ist im Vergleich zu Rust immer noch verwirrend.

Ich denke, das Argument, dass await als Präfix besser ist, beruht nicht darauf, wie Sie Ihr mentales Modell des Codes ändern müssen, sondern vielmehr darauf, wie wir in Rost Präfix-Schlüsselwörter haben, aber keine Postfix-Schlüsselwörter (für Postfixes, Rost verwendet Siegel, wie durch ? veranschaulicht). Das ist der Grund, warum await foo() leichter zu lesen ist als foo() await und warum manche Leute @ am Ende einer Aussage wollen und es nicht mögen, await dort zu haben.

Aus einem ähnlichen Grund fühlt sich die Verwendung von .await seltsam an: Der Punktoperator wird verwendet, um auf Felder und Methoden einer Struktur zuzugreifen (weshalb er auch nicht als reiner Pipeline-Operator angesehen werden kann), also .await ist wie zu sagen "Punkt wird verwendet, um auf Felder und Methoden einer Struktur zuzugreifen oder um auf await zuzugreifen, das weder ein Feld noch eine Funktion ist".

Persönlich würde ich entweder das Präfix await oder ein Postfix-Siegel sehen (muss nicht @ ).

Beide stellen sich heraus, dass im realen Rust-Code das Warten in Postfix natürlicher erscheint

Das ist eine umstrittene Aussage. Das reqwest-Beispiel zeigt nur eine Version der Postfix-Syntax.

Wenn es bei dieser Diskussion darum geht, wer was mehr mag, erwähnen Sie dies bitte auf reddit, damit sich die Leute nicht wie bei Funktionsargumenten mit impl Trait beschweren.

@ eugene2k Für die grundlegende Diskussion, ob Postfix zum mentalen Modell von Rust-Programmierern passt, sind die meisten oder alle Postfix-Syntaxen im Vergleich zum Präfix ungefähr gleich groß. Ich glaube nicht, dass zwischen den Postfix-Varianten ein so großer Unterschied in der Lesbarkeit besteht wie zwischen Präfix und Postfix. Siehe auch meinen Vergleich der Operatorrangfolge auf niedriger Ebene, der zu dem Schluss kommt, dass ihre Semantik in den meisten Fällen gleich ist. Daher ist es sehr wichtig, welcher Operator die beste Bedeutung vermittelt (ich würde derzeit eine tatsächliche Funktionsaufrufsyntax bevorzugen, aber nicht haben auch eine starke Präferenz gegenüber den anderen).

@ eugene2k Entscheidungen in Rust werden niemals durch Abstimmung getroffen. Rost ist keine Demokratie, es ist eine Meritokratie.

Die Core / Lang-Teams betrachten alle verschiedenen Argumente und Perspektiven und entscheiden dann. Diese Entscheidung wird im Konsens (unter den Teammitgliedern) getroffen und nicht gewählt.

Obwohl die Rust-Teams die allgemeinen Wünsche der Community absolut berücksichtigen, entscheiden sie letztendlich auf der Grundlage dessen, was ihrer Meinung nach langfristig für Rust am besten ist.

Der beste Weg, Rust zu beeinflussen, besteht darin, neue Informationen zu präsentieren, neue Argumente vorzubringen oder neue Perspektiven aufzuzeigen.

Das Wiederholen bestehender Argumente oder das Sagen von "Ich auch" (oder ähnlichem) erhöht nicht die Wahrscheinlichkeit, dass ein Vorschlag angenommen wird. Vorschläge werden aufgrund ihrer Beliebtheit niemals angenommen.

Das bedeutet auch, dass die verschiedenen Upvotes / Downvotes in diesem Thread überhaupt keine Rolle spielen, für welchen Vorschlag angenommen wird.

(Ich beziehe mich nicht speziell auf Sie, sondern erkläre, wie die Dinge für alle in diesem Thread funktionieren.)

@Pauan Es wurde bereits gesagt, dass die Core / Lang-Teams verschiedene Vorschläge

Wenn also alle diese Kontexte betrachtet wurden und die Entscheidungsträger im Team einen Ansatz mögen, während die meisten anderen Benutzer, die nicht wie ein anderer Ansatz im Team sind, die endgültige Entscheidung sein sollten?

Die Entscheidung wird immer vom Team getroffen. Zeitraum. So wurden die Regeln absichtlich gestaltet.

Und die Funktionen werden häufig von Teammitgliedern implementiert . Und die Teammitglieder haben auch Vertrauen in die Community aufgebaut. Sie haben also sowohl de jure als auch de facto Autorität.

Wenn sich die Situation ändert (möglicherweise basierend auf Feedback) und die Teammitglieder ihre Meinung ändern, können sie ihre Entscheidung ändern. Aber selbst dann wird die Entscheidung immer von den Teammitgliedern getroffen.

Wie Sie sagen, sind Entscheidungen oft mit einem gewissen Maß an Subjektivität verbunden, und daher ist es unmöglich, alle zufrieden zu stellen, aber es muss eine Entscheidung getroffen werden. Um eine Entscheidung zu treffen, basiert das von Rust verwendete System darauf, dass die Teammitglieder einen Konsens erzielen.

Jede Diskussion darüber, ob Rust anders geregelt werden sollte , ist nicht zum Thema und sollte an einem anderen Ort diskutiert werden.

(PS: Ich bin nicht in den Kern- oder Lang-Teams, daher habe ich keine Autorität in dieser Entscheidung, daher muss ich mich auf sie beschränken, genauso wie Sie.)

@HeroicKatora

Ich denke nicht, dass es einen so großen Unterschied in der Lesbarkeit zwischen den Postfix-Varianten gibt

Ich stimme dir nicht zu. Ich finde, dass foo().await()?.bar().await()? leichter zu lesen ist als foo() await?.bar() await? oder sogar foo()@?.bar()@? Trotzdem habe ich das Gefühl, dass Methoden, die nicht wirklich Methoden sind, einen schlechten Präzedenzfall darstellen.

Ich möchte eine andere Idee vorschlagen. Ich bin damit einverstanden, dass das Warten auf das Präfix nicht einfach mit anderen Funktionen zu verketten ist. Wie wäre es mit dieser Postfix-Syntax: foo(){await}?.bar()?{await} ? Es kann nicht mit Funktionsaufrufen verwechselt werden und scheint mir einfach genug zu sein, um in einer Kette gelesen zu werden.

Und noch ein Vorschlag von mir. Betrachten Sie die folgende Syntax für Methodenaufrufe:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

Was macht es einzigartig unter den anderen Vorschlägen:

  • Eckige Klammern machen Vorrang und Umfang viel sauberer.
  • Die Syntax ist erweiterbar genug, um das Entfernen temporärer Bindungen auch in anderen Kontexten zu ermöglichen.

Ich denke, dass die Erweiterbarkeit hier der größte Vorteil ist. Diese Syntax würde es ermöglichen, Sprachfunktionen zu implementieren, die in ihrer gemeinsamen Form in Rust aufgrund des hohen Verhältnisses von Komplexität zu Nützlichkeit nicht möglich sind. Die Liste möglicher Sprachkonstrukte ist unten angegeben:

  1. Verschieben aller Präfixoperatoren (einschließlich await - so soll es funktionieren):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Pipeline-Operator-Funktionalität:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
  1. Überschreiben der Funktionsrückgabe:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Wither-Funktionalität:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. Kettenaufteilung:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
  1. Postfix-Makros:
let x = long().method().[dbg!(it)].chain();

Ich denke, die Einführung einer neuen Art von Syntax (magische Felder, Postfix-Makros, Klammern) hat einen größeren Einfluss auf die Sprache als diese Funktion allein und sollte einen RFC erfordern.

Gibt es ein Repository, das bereits stark await ? Ich würde anbieten, einen größeren Teil in jedem vorgeschlagenen Stil neu zu schreiben, damit wir ein besseres Gefühl dafür bekommen, wie diese aussehen und wie verständlich der Code ist.

Ich schreibe in obligatorische Trennzeichen um:

// A
if await {db.is_trusted_identity(recipient.clone(), message.key.clone())}? {
    info!("recipient: {}", recipient);
}

// B
match await {db.load(message.key)}  {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await { client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()
}?.error_for_status()?;

// D
let mut res = await {client.get(inbox_url).headers(inbox_headers).send()}?.error_for_status()?;

let mut res: InboxResponse = await {res.json()}?;

// E
let mut res = await { client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send()
}?.error_for_status()?;

let res: Response = await {res.json()}?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await {self.request(url, Method::GET, None, true)}?;
    let user = await {res.json::<UserResponse>()}?
        .user
        .into();

    Ok(user)
}

Es ist fast identisch mit await!() . Es sieht also wunderschön aus! Nachdem Sie await!() für ein oder zwei Jahre verwendet haben, warum sollten Sie sich plötzlich ein Postfix zulegen, das nirgendwo in der Geschichte der Programmiersprache auftaucht?

Huh. Die expr.[await it.foo()] -Syntax von @ I60R mit dem kontextbezogenen Schlüsselwort it ist ziemlich ordentlich. Ich hatte nicht erwartet, ausgefallene neue Syntaxvorschläge zu mögen, aber das ist wirklich sehr schön, ist eine clevere Verwendung des Syntaxraums (weil IIRC .[ derzeit nirgendwo eine gültige Syntax ist) und würde viel mehr lösen Probleme als nur zu warten.

Einverstanden, dass es definitiv einen RFC erfordern würde, und es sich möglicherweise nicht als die beste Option herausstellen wird. Aber ich denke, es ist ein weiterer Punkt auf der Seite, sich vorerst für eine Präfixsyntax für await zu entscheiden, mit dem Wissen, dass es eine Reihe von Möglichkeiten gibt, das Problem zu lösen: "Präfixoperatoren sind umständlich zu verketten". auf eine allgemeinere Art und Weise, die mehr als nur asynchron ist / in Zukunft wartet.

Und noch ein Vorschlag von mir. Betrachten Sie die folgende Syntax für Methodenaufrufe:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[await request(url, Method::GET, None, true)]?
        .res.[await json::<UserResponse>()]?
        .user
        .into();

    Ok(user)
}

Was macht es einzigartig unter den anderen Vorschlägen:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

Ich denke, dass die Erweiterbarkeit hier der größte Vorteil ist. Diese Syntax würde es ermöglichen, Sprachmerkmale zu implementieren, die in ihrer gemeinsamen Form in Rust aufgrund des hohen Verhältnisses von Komplexität zu Nützlichkeit nicht möglich sind. Die Liste möglicher Sprachkonstrukte ist unten angegeben:

1. Deferring of all prefix operators (including `await` - it's how it supposed to work):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
1. Pipeline operator functionality:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
1. Overriding function return:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
1. Wither functionality:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
1. Chain splitting:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
1. Postfix macros:
let x = long().method().[dbg!(it)].chain();

Wirklich unverständlich.

@tajimaha Wenn Sie sich Ihr Beispiel await {} tatsächlich viel besser ist als await!() wenn wir obligatorische Trennzeichen verwenden, da dadurch das Problem "zu viele Klammern" vermieden wird, das die Lesbarkeit beeinträchtigen kann Probleme mit der await!() -Syntax.

Vergleichen Sie:
`` `c #
warte auf {foo.bar (url, false, qux.clone ())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps Sie können eine Syntaxhervorhebung von async und await für einfache Beispiele erhalten, indem Sie die Sprache auf c # setzen.)

@nicoburns Sie können mit dem Makro beliebig () , {} oder [] .

@sgrif Das ist ein guter Punkt. Und da dies der Fall ist, würde ich vorschlagen, dass "Präfix-Schlüsselwort mit obligatorischen Trennzeichen" als Option sehr wenig Sinn macht. Da es die Konsistenz mit anderen Makros bricht, hat dies im Grunde keinen Nutzen.

(FWIW, ich denke immer noch, dass "Präfix ohne Trennzeichen" mit einer allgemeinen Lösung wie Postfix-Makros oder @ I60Rs Vorschlag für Postfix am sinnvollsten ist. Aber die Option "Bleib einfach beim vorhandenen Makro" wächst bei mir ...)

Ich würde vorschlagen, dass "Präfix-Schlüsselwort mit obligatorischen Trennzeichen" als Option sehr wenig Sinn macht. Da es die Konsistenz mit anderen Makros bricht, hat dies im Grunde keinen Nutzen.

Warum macht es wenig Sinn und warum ist ein Schlüsselwort, das nicht die gleiche Syntax wie ein Makro hat, ein Problem?

@ Tajimaha

Ich würde vorschlagen, dass "Präfix-Schlüsselwort mit obligatorischen Trennzeichen" als Option sehr wenig Sinn macht. Da es die Konsistenz mit anderen Makros bricht, hat dies im Grunde keinen Nutzen.

Warum macht es wenig Sinn und warum ist ein Schlüsselwort, das nicht die gleiche Syntax wie ein Makro hat, ein Problem?

Wenn await ein Makro wäre, hätte dies den Vorteil, dass der Sprache keine zusätzliche Syntax hinzugefügt wird. Dadurch wird die Komplexität der Sprache reduziert. Es gibt jedoch ein gutes Argument für die Verwendung eines Schlüsselworts: return , break , continue und andere Konstrukte zur Änderung des Kontrollflusses sind ebenfalls Schlüsselwörter. Diese funktionieren jedoch alle ohne Trennzeichen. Um mit diesen Konstrukten konsistent zu sein, müsste await auch ohne Trennzeichen funktionieren.

Wenn Sie mit obligatorischen Trennzeichen warten müssen, haben Sie:

// Macros using `macro!(foo);` syntax 
format!("{}", foo);
println!("hello world");

// Normal keywords using `keyword foo;`
continue foo;
return foo;

// *and* the await keyword which is kind of in between the other two syntaxes:
await(foo);
await{foo};

Dies ist möglicherweise verwirrend, da Sie sich jetzt 3 Syntaxformen anstelle von 2 merken müssen. Da Keyword-mit-obligatorischen Trennzeichen keine Vorteile gegenüber der Makrosyntax bieten, ist es meiner Meinung nach vorzuziehen, sich nur an den Standard zu halten Makrosyntax, wenn wir Trennzeichen erzwingen möchten (von denen ich überhaupt nicht überzeugt bin, dass wir das sollten).

Eine Frage für diejenigen, die heute in Rust viel Async / Warten verwenden: Wie oft warten Sie auf Funktionen / Methoden im Vergleich zu Variablen / Feldern?

Kontext:

Ich weiß, dass es in C # üblich ist, Dinge zu tun, die auf dieses Muster hinauslaufen:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

Auf diese Weise laufen beide parallel. (Einige werden sagen, dass hier Task.WhenAll verwendet werden sollte, aber der Perf-Unterschied ist winzig und macht den Code unübersichtlicher, da Array-Indizes durchlaufen werden müssen.)

Aber ich verstehe, dass dies in Rust überhaupt nicht parallel läuft, da die poll für fooTask erst aufgerufen werden, wenn bar ihren Wert erhalten hat. und es muss vielleicht einen Kombinator verwenden

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

Angesichts dessen bin ich gespannt, ob man regelmäßig die Zukunft in einer Variablen oder einem Feld hat, auf die man warten muss, oder ob man fast immer auf Aufrufausdrücke wartet. Denn wenn es das letztere ist, gibt es eine winzige Formatierungsvariante des Postfix-Schlüsselworts, die wir nicht wirklich besprochen haben: das Postfix-Schlüsselwort _no-space_.

Ich habe nicht viel darüber nachgedacht, ob es etwas Gutes ist, aber es wäre möglich, Code so einfach wie möglich zu schreiben

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

(Ich möchte eigentlich keine Rustfmt-Diskussion führen, wie ich bereits sagte, aber ich erinnere mich, dass einer der Gründe für die Abneigung gegen das Postfix-Schlüsselwort der Raum war, der das visuelle Chunking auflöst.)

Wenn wir damit weitermachen, können wir auch die Syntax .await nutzen
die Kraft des Punktes, nein?

Eine Frage für diejenigen, die heute in Rust viel Async / Warten verwenden: Wie oft warten Sie auf Funktionen / Methoden im Vergleich zu Variablen / Feldern?

Aber ich verstehe, dass in Rust das überhaupt nicht parallel läuft [...]

Richtig. Hier ist das folgende Beispiel aus derselben Codebasis wie zuvor:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(move |id| {
        self__.request(URL, Method::GET, vec![("info".into(), id.into())])
            .and_then(|mut response| response.json().from_err())
    });

    await!(futures_unordered(futures).collect())?
};

Wenn ich den Abschluss mit einem async Abschluss umschreiben würde:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(async move |id| {
        let mut res =
            await!(self__.request(URL, Method::GET, vec![("info".into(), id.into())]))?;

        Ok(await!(res.json())?)
    });

    await!(futures_unordered(futures).collect())?
};

Wenn ich zur Syntax .await wechseln würde (und sie verketten würde):

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Gibt es ein Repository, das bereits stark ausgelastet ist? Ich würde anbieten, einen größeren Teil in jedem vorgeschlagenen Stil neu zu schreiben

@gralpli Leider verwendet nichts, was ich Open Source kann, stark await! . Es eignet sich im Moment definitiv mehr für Anwendungscode (besonders wenn es so instabil ist).

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Diese Zeilen zeigen genau, wie Code durch übermäßige Verwendung von Postfix und Verkettung durcheinander gebracht wird.

Sehen wir uns die Präfixversion an:

let func = async move |id| {
    let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
    Ok(await(req.json())?)
}
let responses: Vec<Response> = await {
    futures_unordered(all_ids.into_iter().map(func)).collect()
}?;

Die beiden Versionen verwenden beide 7 Zeilen, aber IMO ist die zweite sauberer. Es gibt auch zwei Möglichkeiten zum Verwenden obligatorischer Trennzeichen:

  1. Der await { future }? sieht nicht laut aus, wenn der future Teil lang ist. Siehe let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. Wenn die Leitung kurz ist, könnte die Verwendung von await(future) besser sein. Siehe Ok(await(req.json())?)

IMO, durch das Umschalten zwischen den beiden Varianten ist die Lesbarkeit dieses Codes viel besser als zuvor.

Das erste Beispiel ist schlecht formatiert. Ich denke nicht, dass es rustfmt-Formate mögen
Das. Könnten Sie bitte rustfmt darauf ausführen und es hier erneut posten?

@ivandardi @mehcode Könnten Sie das tun? Ich weiß nicht, wie ich die Syntax .await formatieren kann. Ich habe gerade den Code kopiert. Vielen Dank!

Ich möchte hinzufügen, dass dieses Beispiel zeigt:

  1. Produktionscode ist nicht nur eine schöne und einfache Kette wie:
client.get("https://my_api").send().await?.json().await?
  1. Menschen können Verkettungen überbeanspruchen oder missbrauchen.
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Hier behandelt der asynchrone Abschluss jede ID, er hat nichts mit der Kontrolle futures_unordered höherer Ebene

Alles wurde von meinem Beitrag durch rustfmt (mit einigen geringfügigen Änderungen, damit er kompiliert werden kann). Wo das .await? platziert ist, ist noch nicht entschieden und derzeit platziere ich es am Ende der erwarteten Zeile.


Jetzt stimme ich zu, dass alles ziemlich schrecklich aussieht. Dies ist Code, der zu einem bestimmten Zeitpunkt geschrieben wurde, und Dinge werden bestimmt schrecklich aussehen, wenn Sie einen Raum mit Köchen haben, die sich drehen, um etwas herauszuholen.

Ich möchte (aus Ihrer Sicht) darauf hinweisen, dass der Missbrauch von Präfixen (meiner Meinung nach natürlich) weitaus schlimmer aussehen kann:

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

Jetzt lasst uns Spaß haben und es im Nachhinein und mit einigen neuen Adaptern in Futures v0.3 viel schöner machen

Präfix mit "Offensichtlicher" Vorrang (und Zucker)
let responses: Vec<Response> = await? stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect();
Präfix mit "Nützlicher" Vorrang
let responses: Vec<Response> = await stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect()?;
Präfix mit obligatorischen Trennzeichen
let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;
Postfix-Feld
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect().await?;
Postfix-Schlüsselwort
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect() await?;

Minor nit hier. Es gibt kein TryStreamExt::and_then , das sollte es wahrscheinlich geben. Klingt nach einer einfachen PR für alle, die Zeit haben und einen Beitrag leisten möchten.


  • Ich möchte noch einmal meine starke Abneigung gegen await? ausdrücken. In langen Ketten verliere ich den Überblick über die ? denen ich am Ende der Ausdrücke gesucht habe, um anzuzeigen, dass dieser Ausdruck ist fehlbar und kann die Funktion verlassen.

  • Ich möchte weiterhin meine wachsende Abneigung gegen await .... ? (nützliche Priorität) zum Ausdruck bringen, wenn ich überlege, was passieren würde, wenn wir ein fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

Ich möchte (aus Ihrer Sicht) darauf hinweisen, dass der Missbrauch von Präfixen (meiner Meinung nach natürlich) weitaus schlimmer aussehen kann:

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

Dies ist eigentlich kein Problem, da in Python-Javascript die Wahrscheinlichkeit größer ist, dass Benutzer sie in separaten Zeilen schreiben. Ich habe await (await f) in Python nicht wirklich gesehen.

Präfix mit obligatorischen Trennzeichen

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

Dies scheint auch auf die Verwendung von Kombinatoren zurückzuführen zu sein. Während der Sinn der Einführung von async / await besteht, die Verwendung gegebenenfalls zu reduzieren.

Nun, das ist der springende Punkt beim Warten auf Postfix. Da dies Rust ist, ist es weniger wahrscheinlich, dass Leute sie in getrennten Zeilen schreiben, da Rust die Verkettung fördert. Und dafür ist die Postfix-Syntax im Wesentlichen obligatorisch, damit der Anweisungsfluss dem gleichen Zeilenlesefluss folgt. Wenn wir keine Postfix-Syntax haben, wird es viel Code mit temporären Elementen geben, die ebenfalls verkettet sind, während wenn wir Postfix warten mussten, könnte dies alles auf eine einzige Kette reduziert werden.

@ivandardi @mehcode Kopieren von Rust RFC auf asynchron / warten:

Nachdem wir Erfahrungen und Benutzerfeedback mit dem zukunftsbasierten Ökosystem gesammelt hatten, entdeckten wir bestimmte ergonomische Herausforderungen. Die Verwendung von Zuständen, die auf Wartepunkte verteilt werden müssen, war äußerst unergonomisch - entweder waren Bögen oder Verknüpfungsketten erforderlich - und obwohl Kombinatoren häufig ergonomischer waren als das manuelle Schreiben einer Zukunft, führten sie dennoch häufig zu unordentlichen Sätzen verschachtelter und verketteter Rückrufe.

... mit einem syntaktischen Zucker verwenden, der in vielen Sprachen mit asynchronem E / A üblich geworden ist - dem asynchronen und wartenden Schlüsselwort.

Aus der Sicht eines Benutzers können sie async / await verwenden, als wäre es synchroner Code, und müssen nur ihre Funktionen und Aufrufe mit Anmerkungen versehen.

Der Sinn der Einführung von asynchronem Warten besteht also darin, die Verkettung zu reduzieren und asynchronen Code so zu erstellen, als ob sie synchron wären . Die Verkettung wird in diesem RFC nur zweimal erwähnt, "erfordert entweder Arcs oder Join-Verkettung" und "führt immer noch häufig zu unordentlichen Sätzen verschachtelter und verketteter Rückrufe". Klingt für mich nicht zu positiv.

Das Argumentieren für eine Verkettung und damit für ein Postfix-Schlüsselwort würde möglicherweise eine umfassende Neufassung dieses RFC erfordern.

@tajimaha Du map , and_then usw.), es geht nicht um Verkettung im Allgemeinen (z. B. Methoden, die impl Future ).

Ich denke, man kann mit Sicherheit sagen, dass async -Methoden weit verbreitet sind, daher ist Verkettung in der Tat sehr wichtig.

Außerdem verstehen Sie den Prozess falsch: Der RFC muss nicht neu geschrieben werden. Der RFC ist ein Ausgangspunkt, aber keine Spezifikation. Keiner der RFCs ist in Stein gemeißelt (und sollte es auch nicht sein!)

Der RFC-Prozess ist flüssig und bei weitem nicht so starr, wie Sie meinen. Nichts im RFC hindert uns daran, über Postfix await diskutieren.

Außerdem werden alle Änderungen in den Stabilisierungs-RFC übernommen , nicht in den ursprünglichen RFC (der bereits akzeptiert wurde und daher nicht geändert wird).

Das Argumentieren für eine Verkettung und damit für ein Postfix-Schlüsselwort würde möglicherweise eine umfassende Neufassung dieses RFC erfordern.

Ich scherze hier.

Eines ist wahrscheinlich wahr, wenn der RFC geschrieben wird. Die Leute wollten speziell ein neues Tool, "das Async / Warten verwenden kann, als wäre es synchroner Code". Tradition nach Syntax würde dieses Versprechen besser erfüllen.
Und hier sehen Sie, es sind keine zukünftigen Kombinatoren,

let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Dennoch "führten sie immer noch oft zu unordentlichen Ansätzen verschachtelter und verketteter Rückrufe."

Die Leute wollten speziell ein neues Tool, "das Async / Warten verwenden kann, als wäre es synchroner Code". Tradition nach Syntax würde dieses Versprechen besser erfüllen.

Ich verstehe nicht, wie das stimmt: Sowohl Präfix als auch Postfix await erfüllen diesen Wunsch.

Tatsächlich erfüllt Postfix await diesen Wunsch wahrscheinlich besser, da es bei Methodenketten (die im synchronen Rust-Code sehr häufig vorkommen!) Sehr natürlich ist.

Die Verwendung des Präfixes await fördert stark viele temporäre Variablen, was häufig kein idiomatischer Rust-Stil ist.

Dennoch "führten sie immer noch oft zu unordentlichen Ansätzen verschachtelter und verketteter Rückrufe."

Ich sehe genau einen Abschluss, und es ist kein Rückruf, es ruft nur map auf einem Iterator (überhaupt nichts mit Futures zu tun!).

Bitte versuchen Sie nicht, die Wörter des RFC umzudrehen, um das Präfix await zu rechtfertigen.

Die Verwendung des RFC zur Rechtfertigung des Präfixes await ist sehr seltsam, da der RFC selbst angibt, dass die Syntax nicht endgültig ist und später entschieden wird. Die Zeit für diese Entscheidung ist jetzt.

Die Entscheidung wird auf der Grundlage der Vorzüge der verschiedenen Vorschläge getroffen. Der ursprüngliche RFC ist völlig irrelevant (außer als nützliche historische Referenz).

Beachten Sie, dass das neueste Beispiel von @ mehcode hauptsächlich _stream_-Kombinatoren verwendet (und der eine zukünftige Kombinator könnte trivial durch einen asynchronen Block ersetzt werden). Dies entspricht der Verwendung von Iterator-Kombinatoren in synchronem Code und kann daher in einigen Situationen verwendet werden, in denen sie besser geeignet sind als Schleifen.

Dies ist offtopisch, aber der größte Teil des Gesprächs hier wird von etwa einem Dutzend Kommentatoren geführt. Von den 383 Kommentaren zu dem Zeitpunkt, als ich diese Ausgabe abkratzte, gab es nur 88 einzigartige Poster. Um zu vermeiden, dass jemand, der diese Kommentare lesen müsste, ausgebrannt oder überlastet wird, würde ich empfehlen, Ihre Kommentare so gründlich wie möglich zu gestalten und sicherzustellen, dass es sich nicht um eine Wiederholung eines früheren Punktes handelt.


Histogramm der Kommentare

HeroicKatora:(32)********************************
Centril:(22)**********************
ivandardi:(21)*********************
I60R:(21)*********************
Pzixel:(16)****************
novacrazy:(15)***************
scottmcm:(13)*************
EyeOfPython:(11)***********
mehcode:(11)***********
Pauan:(10)**********
XX:(9)*********
nicoburns:(9)*********
tajimaha:(9)*********
skade:(8)********
CAD97:(8)********
Laaas:(8)********
dpc:(8)********
ejmahler:(7)*******
Nemo157:(7)*******
yazaddaruvala:(6)******
traviscross:(6)******
CryZe:(6)******
Matthias247:(5)*****
dowchris97:(5)*****
rolandsteiner:(5)*****
earthengine:(5)*****
H2CO3:(5)*****
eugene2k:(5)*****
jplatte:(4)****
lnicola:(4)****
andreytkachenko:(4)****
cenwangumass:(4)****
richardanaya:(4)****
chpio:(3)***
joshtriplett:(3)***
phaylon:(3)***
phaazon:(3)***
ben0x539:(2)**
newpavlov:(2)**
comex:(2)**
DDOtten:(2)**
withoutboats:(2)**
valff:(2)**
darkwater:(2)**
tanriol:(1)*
liigo:(1)*
yasammez:(1)*
mitsuhiko:(1)*
mokeyish:(1)*
unraised:(1)*
mzji:(1)*
swfsql:(1)*
spacekookie:(1)*
sgrif:(1)*
nikonthethird:(1)*
edwin-durai:(1)*
norcalli:(1)*
quodlibetor:(1)*
chescock:(1)*
BenoitZugmeyer:(1)*
F001:(1)*
FuGangqiang:(1)*
Keruspe:(1)*
LegNeato:(1)*
MSleepyPanda:(1)*
SamuelMoriarty:(1)*
Swoorup:(1)*
Uristqwerty:(1)*
alexmaco:(1)*
arabidopsis:(1)*
arielb1:(1)*
axelf4:(1)*
casey:(1)*
lholden:(1)*
cramertj:(1)*
crlf0710:(1)*
davidtwco:(1)*
dyxushuai:(1)*
eaglgenes101:(1)*
AaronFriel:(1)*
gralpli:(1)*
huxi:(1)*
ian-p-cooke:(1)*
jonimake:(1)*
josalhor:(1)*
jsdw:(1)*
kjetilkjeka:(1)*
kvinwang:(1)*

Beachten Sie, dass das neueste Beispiel von @ mehcode hauptsächlich _stream_-Kombinatoren verwendet (und der eine zukünftige Kombinator könnte trivial durch einen asynchronen Block ersetzt werden). Dies entspricht der Verwendung von Iterator-Kombinatoren in synchronem Code und kann daher in einigen Situationen verwendet werden, in denen sie besser geeignet sind als Schleifen.

Gleiches kann hier argumentiert werden, dass ich Präfix warten kann / sollte, wo sie geeigneter sind als Verketten.

@Pauan Anscheinend sind es nicht nur verdrehte Worte. Ich zeige ein aktuelles Codeproblem, das von einem Postfix-Syntax-Unterstützer geschrieben wurde. Und wie gesagt, der Präfix-Stilcode veranschaulicht Ihre Absicht besser, hat aber nicht unbedingt viele Provisorien, da sich Postfix-Unterstützer beschweren (zumindest in diesem Fall). Angenommen, Ihr Code hat eine Einzeilerkette mit zwei Wartezeiten. Wie kann ich die erste debuggen? (Dies ist eine wahre Frage und ich weiß es nicht).
Zweitens wird die Rostgemeinschaft immer größer, Menschen mit unterschiedlichem Hintergrund (wie ich, ich benutze Python / C / Java am häufigsten) sind sich nicht alle einig, dass Methodenketten die besten Möglichkeiten sind, Dinge zu tun. Ich hoffe, wenn ich eine Entscheidung treffe, basiert dies nicht nur auf dem Standpunkt der frühesten Anwender.

@tajimaha Die größte Änderung der Klarheit von Post-Fix zu Präfix scheint darin zu bestehen, einen lokalen Abschluss zu verwenden, um einige verschachtelte Funktionsargumente zu entfernen. Dies scheint mir nicht eindeutig als Präfix zu sein, diese sind ziemlich orthogonal. Sie können das gleiche für Postfix tun, und ich denke, es ist noch klarer. Ich bin damit einverstanden, dass dies möglicherweise ein Missbrauch der Verkettung für einige Codebasen ist, aber ich sehe nicht, wie dieser Missbrauch eindeutig ist oder in erheblichem Maße mit Postfix zusammenhängt.

let get_one_id = async move |id| {
    self.request(URL, Method::GET, vec![("info".into(), id.into())])
        .await?
        .json().await
};

let responses: Vec<Response> = futures_unordered(all_ids.into_iter().map(get_one_id))
    .collect().await?;

In Postfix können jedoch die let -Bindung und die Ok für das Ergebnis zusammen mit den letzten ? , um das Ergebnis direkt zu liefern, und dann ist der Codeblock auch nicht erforderlich, abhängig davon auf persönlichen Geschmack. Dies funktioniert im Präfix überhaupt nicht gut, da zwei in derselben Anweisung warten.

Ich verstehe das regelmäßig festgestellte Gefühl nicht, dass Bindungen im Rust-Code unidiomatisch sind. Sie sind in Codebeispielen ziemlich häufig und häufig, insbesondere im Bereich der Ergebnisbehandlung. Ich sehe selten mehr als 2 ? im Code, mit dem ich mich befasse.

Außerdem ändert sich idiomatic im Laufe der Lebensdauer einer Sprache, sodass ich sehr darauf achten würde, sie als Argument zu verwenden.

Ich weiß nicht, ob so etwas schon einmal vorgeschlagen wurde, aber könnte ein Präfix await Schlüsselwort für einen gesamten Ausdruck gelten? Nehmen wir das Beispiel, das zuvor angesprochen wurde:

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

Dabei sind get , send und json asynchron.

Für mich (mit wenig asynchroner Erfahrung in anderen Programmiersprachen) sieht das Postfix expr await natürlich aus: "Tun Sie dies, dann warten Sie auf das Ergebnis."

Es gab einige Bedenken, dass die folgenden Beispiele seltsam aussehen:

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

Ich würde jedoch argumentieren, dass dies in mehrere Zeilen aufgeteilt werden sollte:

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

Dies ist viel klarer und hat den zusätzlichen Vorteil, dass das await leicht zu erkennen ist, wenn es immer am Ende der Zeile steht.

In einer IDE fehlt dieser Syntax die "Potenz des Punkts", sie ist jedoch immer noch besser als die Präfixversion: Wenn Sie den Punkt eingeben und dann feststellen, dass Sie await benötigen, müssen Sie nur den Punkt löschen und geben Sie " await " ein. Das heißt, wenn die IDE keine automatische Vervollständigung für Schlüsselwörter anbietet.

Die Punktsyntax expr.await ist verwirrend, da kein anderes Kontrollfluss-Schlüsselwort einen Punkt verwendet.

Ich denke, das Problem ist, dass wir, obwohl wir Verkettungen haben, was manchmal ziemlich sein kann, nicht extrem sagen sollten, dass alles in Verkettung gemacht werden sollte. Wir sollten Tools auch für die Programmierung im C- oder Python-Stil bereitstellen. Obwohl Python dort fast keine Verkettungskomponente hat, wird sein Code oft als lesbar gelobt. Python-Programmierer beschweren sich auch nicht, dass wir zu viele temporäre Variablen haben.

Wie wäre es mit einem Postfix then ?

Ich verstehe das regelmäßig festgestellte Gefühl nicht, dass Bindungen im Rust-Code unidiomatisch sind. Sie sind in Codebeispielen ziemlich häufig und häufig, insbesondere im Bereich der Ergebnisbehandlung. Ich sehe selten mehr als 2 ? im Code, mit dem ich mich befasse.

Außerdem ändert sich idiomatic im Laufe der Lebensdauer einer Sprache, sodass ich sehr darauf achten würde, sie als Argument zu verwenden.

Dies hat mich dazu inspiriert, den aktuellen Rust-Code zu untersuchen, bei dem zwei oder mehr ? in einer Zeile stehen (möglicherweise kann jemand die mehrzeilige Nutzung überwachen). Ich befragte XI-Editor, Alacritty, Ripgrep, Fledermaus, Röntgen, FD, Kracher, Eibe, Rakete, Exa, Eisen, Parity-Ethereum, Tikv. Dies sind Rust-Projekte mit den meisten Stars.

Was ich finde, ist, dass ungefähr ungefähr 40 Zeilen von insgesamt 585562 Zeilen zwei oder mehr ? in einer Zeile verwenden. Das sind 0,006% .

Ich möchte auch darauf hinweisen, dass das Studium vorhandener Code-Verwendungsmuster nicht die Benutzererfahrung beim Schreiben von neuem Code offenbart.

Angenommen, Sie erhalten einen Auftrag zur Interaktion mit einer neuen API oder sind neu in der Verwendung von Anforderungen. Ist es wahrscheinlich, dass Sie schreiben werden?

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

in nur einem Schuss ? Wenn Sie neu in der API sind, bezweifle ich, dass Sie sicherstellen möchten, dass Sie die Anforderung korrekt erstellen, den Rückgabestatus anzeigen, Ihre Annahme über die Rückgabe dieser API überprüfen oder einfach wie folgt mit der API spielen:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

Eine Netzwerk-API ist nichts anderes als Speicherdaten. Sie wissen nicht, was sich dort befindet. Dies ist für das Prototyping ziemlich natürlich. Und wenn Sie Prototypen erstellen, sorgen Sie sich, dass alles richtig läuft, NICHT zu viele temporäre Variablen . Sie können sagen, wir können Postfix-Syntax verwenden wie:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = request.send().await?;
dbg!(response);
let data = response.json().await?;
dbg!(data);

Aber wenn Sie dies bereits haben:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

Alles, was Sie tun müssen, ist wahrscheinlich, es in eine Funktion zu packen, und Ihre Arbeit ist erledigt. In diesem Prozess entsteht nicht einmal eine Verkettung.

Was ich finde ist, dass ungefähr nur ungefähr 40 von insgesamt 585562 Zeilen zwei oder mehr verwenden? in einer Zeile.

Ich möchte vorschlagen, dass dies keine hilfreiche Messung ist. Was wirklich zählt, ist mehr als ein Kontrollflussoperator pro Ausdruck. Gemäß dem typischen (rustfmt) Stil landen sie fast immer in unterschiedlichen Zeilen in der Datei, obwohl sie zum selben Ausdruck gehören, und werden daher in der Menge verkettet, die Postfix (theoretisch) für await wichtig ist.

Wir sollten nicht bis zum Äußersten sagen, dass alles in Verkettung getan werden sollte

Hat jemand gesagt, dass alles mit Verkettung gemacht werden sollte? Alles, was ich bisher gesehen habe, ist, dass es _ergonomisch_ sein sollte, in Fällen zu verketten, in denen es sinnvoll ist, genau wie im synchronen Code.

Nur etwa 40 von insgesamt 585562 Zeilen verwenden zwei oder mehr? in einer Zeile.

Ich bin mir nicht sicher, ob dies für Präfix oder Postfix relevant ist. Ich werde bemerken, dass _none_ meiner C # -Beispiele für den await s in einer Zeile oder sogar mehrere await s in einer Anweisung enthielt. Und @Centrils Beispiel für ein mögliches Postfix-Layout hat auch nicht mehrere await in eine Zeile gesetzt.

Ein besserer Vergleich könnte mit Dingen sein, die an ? angekettet sind, wie diese Beispiele aus dem Compiler:

Ok(&self.get_bytes(cx, ptr, size_with_null)?[..size])
self.try_to_scalar()?.to_ptr().ok()
let idx = decoder.read_u32()? as usize;
.extend(self.at(cause, param_env).eq(v1, v2)?.into_obligations());
for line in BufReader::new(File::open(path)?).lines() {

Edit: Sieht so aus, als hättest du mich diesmal geschlagen , @ CAD97 :

Dies ist schockierend ähnlich wie der Versprechen-Code von javascipt mit vielen then s. Ich würde das nicht synchron nennen. Es ist fast sicher, dass Verkettung zufällig ein await und vorgibt, synchron zu sein.

Präfix mit obligatorischen Trennzeichen

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

@ CAD97 @scottmcm Guter Punkt. Ich wusste, dass das, was ich messe, begrenzt ist:

(Möglicherweise kann jemand die mehrzeilige Nutzung überwachen.)

Ich mache das, weil @skade die Ähnlichkeit zwischen await und ? , also habe ich eine schnelle Analyse durchgeführt. Ich biete eine Idee, die keine ernsthafte Forschung betreibt. Ich denke, eine feinkörnigere Analyse wäre schwierig, wenn man sich den Code ansieht, oder? Möglicherweise müssen Ausdrücke analysiert und identifiziert werden, mit denen ich nicht vertraut bin. Ich hoffe, jemand kann diese Analyse durchführen.

Hat jemand gesagt, dass alles mit Verkettung gemacht werden sollte? Alles, was ich bisher gesehen habe, ist, dass es ergonomisch sein sollte, in Fällen zu verketten, in denen es sinnvoll ist, genau wie im synchronen Code.

Was ich sage ist, wenn nur Postfix hinzugefügt wird, wäre es unergonomisch, wenn der C / Python-Stil gut aussieht (natürlich imo). Ich spreche auch die

Ich habe das Gefühl, dass die Richtung dieses Threads viel zu sehr auf übermäßige Verkettung ausgerichtet ist und wie man das Warten auf Code so präzise wie möglich macht.

Ich möchte stattdessen alle dazu ermutigen, zu untersuchen, wie sich asynchroner Code von den synchronen Varianten unterscheidet und wie sich dies auf die Nutzung und die Ressourcennutzung auswirkt. Weniger als 5 Kommentare in den 400 in diesem Thread erwähnen diese Unterschiede. Wenn Sie sich dieser Unterschiede nicht bewusst sind, greifen Sie zur aktuellen nächtlichen Version und versuchen Sie, einen anständigen Teil des asynchronen Codes zu schreiben. Einschließlich des Versuchs, eine idiomatische (nicht kombinatorische) asynchrone / wartende Version der Code-Kompilierung zu erhalten, über die in den letzten 20 Beiträgen gesprochen wurde.

Es wird einen Unterschied machen, ob Dinge zwischen rein zwischen Ertrags- / Wartepunkten existieren, ob Referenzen über Wartepunkte hinweg bestehen bleiben und oft einige besondere Anforderungen in Bezug auf Futures es nicht möglich machen, Code so präzise zu schreiben, wie in diesem Thread vorgestellt. Beispielsweise können wir keine beliebigen asynchronen Funktionen in beliebige Kombinatoren einfügen, da diese möglicherweise nicht mit !Unpin -Typen funktionieren. Wenn wir Futures aus asynchronen Blöcken erstellen, sind diese möglicherweise nicht direkt mit Kombinatoren wie join! oder select! kompatibel, da sie angeheftete und verschmolzene Typen benötigen, sodass zusätzliche Aufrufe an pin_mut! und .fuse() möglicherweise innerhalb von erforderlich.

Zusätzlich funktionieren die neuen makrobasierten Dienstprogramme für die Arbeit mit asynchronen Blöcken join! und select! weitaus besser als die alten Kombinatorvarianten. Und diese sind in der übertriebenen Weise, die hier oft als Beispiele angeführt wird

Ich weiß nicht, wie Postfix Wait mit .unwrap() in diesem einfachsten Tokio-Beispiel funktionieren kann

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

Wenn das Präfix übernommen wird, wird es

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

Aber wenn Postfix übernommen wird,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

Gibt es eine intuitive Erklärung, die wir Benutzern geben können? Es widerspricht bestehenden Regeln. await ist ein Feld? oder await ist bindend und hat eine Methode namens unwrap() ? SCHADE! Wir packen viel aus, wenn wir ein Projekt starten. Mehrere Designregeln in The Zen of Python verletzt.

Sonderfälle sind nicht speziell genug, um gegen die Regeln zu verstoßen.
Wenn die Implementierung schwer zu erklären ist, ist es eine schlechte Idee.
Verweigern Sie angesichts von Zweideutigkeiten die Versuchung zu raten.

Gibt es eine intuitive Erklärung, die wir Benutzern geben können? Es widerspricht bestehenden Regeln. await ist ein Feld? oder await ist bindend und hat eine Methode namens unwrap() ? SCHADE! Wir packen viel aus, wenn wir ein Projekt starten. Mehrere Designregeln in The Zen of Python verletzt.

Ich würde sagen, obwohl zu viele Dokumente in docs.rs unwrap anrufen, sollte unwrap ? in vielen Fällen der realen Welt durch

Was ich finde ist, dass ungefähr nur ungefähr 40 von insgesamt 585562 Zeilen zwei oder mehr verwenden? in einer Zeile.

Ich möchte vorschlagen, dass dies keine hilfreiche Messung ist. Was wirklich zählt, ist mehr als ein Kontrollflussoperator pro Ausdruck. Gemäß dem typischen (rustfmt) Stil landen sie fast immer in unterschiedlichen Zeilen in der Datei, obwohl sie zum selben Ausdruck gehören, und werden daher in der Menge verkettet, die Postfix (theoretisch) für await wichtig ist.

Ich lehne ab, dass es für diesen Ansatz Grenzen geben kann.

Ich habe erneut nach Xi-Editor, Alacritty, Ripgrep, Fledermaus, Röntgen, FD, Kracher, Eibe, Rakete, Exa, Eisen, Parity-Ethereum, Tikv gesucht. Dies sind Rust-Projekte mit den meisten Stars. Diesmal habe ich nach Muster gesucht:

xxx
  .f1()
  .f2()
  .f3()
  ...

und ob diese Ausdrücke mehrere Kontrollflussoperatoren enthalten.

Ich habe festgestellt, dass NUR 15 von 7066 Ketten mehrere Kontrollflussoperatoren haben. Das sind 0,2% . Diese Zeilen umfassen 167 von 585562 Codezeilen. Das sind 0,03% .

@cenwangumass Danke, dass hast . :Herz:

Eine Überlegung ist, dass Rust, da es eine variable Neubindung mit let hat, ein überzeugendes Argument für das Präfix await , da bei konsistenter Verwendung für jedes await eine separate Codezeile erwartet wird -Punkt. Es gibt zwei Vorteile: Stapelverfolgungen, da die Zeilennummer einen besseren Kontext für den Ort des Problems bietet, und die Erleichterung des Debuggens von Haltepunkten, da es üblich ist, für jeden einzelnen Wartepunkt einen Haltepunkt festzulegen, um Variablen zu untersuchen, können die Kürze von überwiegen eine einzelne Codezeile.

Persönlich bin ich zwischen Präfix-Stil und Postfix-Siegel hin und her gerissen, obwohl ich nach dem Lesen von https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 wahrscheinlich hauptsächlich für ein Postfix-Siegel bin.

Gerenderter Präfixstil:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = await? self.request(url, Method::GET, None, true));
    let user = await? user.res.json::<UserResponse>();
    let user = user.user.into();

    Ok(user)
}

Gerenderter Postfix-Stil:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)) await?;
    let user = user.res.json::<UserResponse>() await?;
    let user = user.user.into();

    Ok(user)
}

Postfix sigil @ gerendert:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))@?;
    let user = user.res.json::<UserResponse>()@?;
    let user = user.user.into();

    Ok(user)
}

Gerendertes Postfix-Siegel #:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true))#?;
    let user = user.res.json::<UserResponse>()#?;
    let user = user.user.into();

    Ok(user)
}

Ich habe nicht genug Leute gesehen, die über await für Stream s gesprochen haben. Während es außerhalb des Rahmens liegt, könnte es sich lohnen, die Entscheidung um await mit ein wenig Voraussicht von Stream zu treffen.

Wir werden wahrscheinlich so etwas brauchen:
Präfixsyntax

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Postfix-Syntax

for response in stream await {
    let response = response?;
    ...
}
for response in stream.await!() {
    let response = response?;
    ...
}

// Or a specialized variant of `await` and `?`
//     Note (Not Obvious): The `?` actually applies to each response of `await`
for response in stream await? {
    ...
}
for response in stream.await!()? {
    ...
}

Möglicherweise ergonomischste / konsistenteste Syntax

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

Ich möchte nur sicherstellen, dass diese Beispiele zumindest ein wenig besprochen werden, da Postfix zwar gut zum Verketten von Future geeignet ist, auf den ersten Blick jedoch für Stream am wenigsten intuitiv erscheint. Ich bin mir nicht sicher, was hier die richtige Lösung ist. Vielleicht Postfix await für Future und einen anderen Stil von await für Stream ? Für verschiedene Syntaxen müssten wir jedoch sicherstellen, dass das Parsen nicht mehrdeutig ist.

Dies sind alles nur meine ersten Gedanken. Es könnte sich lohnen, dem Stream -Verwendungsfall weitere Gedanken aus der Postfix-Perspektive zu geben. Hat jemand irgendwelche Gedanken darüber, wie Postfix mit Stream gut funktionieren könnte, oder ob wir zwei Syntaxen haben sollten?

Die Syntax für die Stream-Iteration wird für einige Zeit, wie ich mir vorstellen kann, nicht passieren. Es ist ziemlich einfach, eine while let -Schleife zu verwenden und dies manuell zu tun:

Postfix-Feld
while let Some(value) = stream.try_next().await? {
}
Präfix mit "konsistenter" Priorität und Zucker
while let Some(value) = await? stream.try_next() {
}
Präfix mit obligatorischen Trennzeichen
while let Some(value) = await { stream.try_next() }? {
}

Wäre die aktuelle Art, Dinge zu tun (plus await ).


Ich werde bemerken, dass dieses Beispiel "Präfix mit Trennzeichen" für mich besonders schlecht aussehen lässt. Während await(...)? ein bisschen besser aussehen könnte, würde ich hoffen, dass wir nur eine Art von Trennzeichen zulassen würden (wie try { ... } ), wenn wir "Präfix mit Trennzeichen" machen würden.

Schnelle Tangente, aber würde "Warten mit Trennzeichen" nicht einfach ... normales Präfix erwarten? await wartet auf einen Ausdruck, daher sind await expr und await { expr } im Wesentlichen gleich. Es wäre nicht sinnvoll, nur mit Trennzeichen zu warten und nicht auch ohne Trennzeichen, zumal jeder Ausdruck von {} umgeben sein kann und weiterhin derselbe Ausdruck ist.

Die Frage nach Streams führte mich zu dem Gedanken, dass es für Rust ganz natürlich sein könnte, das Warten nicht nur als Operator in Ausdrücken, sondern auch als Modifikator in Mustern zur Verfügung zu haben:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

was dann natürlich mit for funktionieren kann

for async x in my_stream { ... }

@ivandardi

Das Problem, das "Präfix mit obligatorischen Trennzeichen abwarten" löst, ist die Vorrangfrage um ? . Bei obligatorischen Trennzeichen gibt es keine Vorrangfrage.

Siehe try { .... } für die ähnliche (stabile) Syntax. Try hat aus dem gleichen Grund obligatorische Trennzeichen - wie es mit ? interagiert -, da sich ein ? innerhalb der geschweiften Klammern stark von einem außerhalb unterscheidet.

@yazaddaruvala Ich denke nicht, dass es eine asynchrone Methode geben sollte, um eine for-Schleife mit Iterator<Item = Result<...>> .

// postfix syntax
for response in stream await {
    ...
}

Dies bedeutet, dass stream: impl Future<Output = Iterator> und Sie darauf warten, dass der Iterator verfügbar ist, nicht jedes Element. Ich sehe keine große Chance für etwas Besseres als async for item in stream (ohne eine allgemeinere Funktion wie @tanriol erwähnt).


@ivandardi Der Unterschied ist der Vorrang, sobald Sie anfangen, an das Ende der Trennzeichen zu

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

welche zu analysieren (mit genügend Trennzeichen, dass diese für alle drei Varianten eindeutig sein sollten)

// obvious precedence prefix
await ((foo.bar()).baz()?);
await ({ let foo = quux(); foo.bar() }.baz()?);

// useful precedence prefix
(await ((foo.bar()).baz()))?;
(await ({ let foo = quux(); foo.bar() }.baz())?;

// mandatory delimiters prefix
(await (foo.bar())).baz()?;
(await { let foo = quux(); foo.bar() }).baz()?;

Meiner Meinung nach sehen die Beispiele von https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 gut aus, wenn die Trennzeichen von der Position (await foo).bar auf verschoben werden die await(foo).bar Position:

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Und so weiter. Dieses Layout ist aus normalen Funktionsaufrufen, aus dem vorhandenen schlüsselwortbasierten Steuerungsfluss und aus anderen Sprachen (einschließlich C #) bekannt. Dies vermeidet es, das Budget für Fremdheit zu sprengen, und scheint keine Probleme für die Verkettung nach await .

Obwohl es zugegebenermaßen das gleiche Problem hat wie try! für Multi- await Verkettung, scheint das keine so große Sache zu sein wie für Result ? Ich würde die Seltsamkeit der Syntax hier viel lieber einschränken, als ein relativ ungewöhnliches und wohl unlesbares Muster zu berücksichtigen.

und aus anderen Sprachen bekannt (einschließlich C #)

So funktioniert die Priorität in C # (oder Javascript oder Python) nicht

await(response.Content.ReadAsStringAsync()).Should().Be(text);

ist äquivalent zu

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

(Der Punktoperator hat eine höhere Priorität als await und bindet daher enger, selbst wenn Sie versuchen, await wie einen Funktionsaufruf aussehen zu lassen.)

Ich weiß, das ist nicht die Behauptung, die ich gemacht habe. Lediglich die Reihenfolge bleibt gleich, sodass nur Klammern hinzugefügt werden müssen und die (separat) vorhandene Syntax des Funktionsaufrufs den richtigen Vorrang hat.

Dieses Layout ist [...] aus dem vorhandenen schlüsselwortbasierten Kontrollfluss bekannt

Ich stimme dir nicht zu. Wir warnen tatsächlich davor, dieses Layout in einem vorhandenen schlüsselwortbasierten Kontrollfluss zu verwenden:

warning: unnecessary parentheses around `return` value
 --> src/lib.rs:2:9
  |
2 |   return(4);
  |         ^^^ help: remove these parentheses
  |
  = note: #[warn(unused_parens)] on by default

Und rustfmt ändert dies in return (4); und fügt ein Leerzeichen hinzu.

Das hat keinen wirklichen Einfluss auf den Punkt, den ich versuche, und fühlt sich an, als würde ich Haare spalten. return (4) , return(4) , es ist nichts weiter als ein Stilproblem.

Ich las die gesamte Ausgabe und bekam das Gefühl, dass {} Klammern und Präfix in Ordnung sind, aber dann verschwand es

Wir werden sehen:

let foo = await some_future();
let bar = await {other_future()}?.bar

Sieht gut aus, nicht wahr? Aber was ist, wenn wir mehr await in einer Kette verketten wollen?

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

IMHO sieht es viel schlimmer aus als

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

Allerdings glaube ich nicht an asynchrone Verkettung, wie in @cenwangumass 'Post geschrieben . Hier ist mein Beispiel für die Verwendung von async/await in einem Hyper-Server. Zeigen Sie anhand dieses konkreten Beispiels, wo Verkettung nützlich sein kann. Ich bin wirklich versucht, keine Witze.


Bei früheren Beispielen werden die meisten von ihnen auf irgendeine Weise von Kombinatoren "beschädigt". Sie brauchen sie fast nie, da Sie erwartet werden. Z.B

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

ist nur

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())]) await?;
      Ok(res.json() await?)
   })
   .join_all() await
   .collect()?

Wenn Futures einen Fehler zurückgeben können, ist dies genauso einfach wie:

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())])? await?;
      Ok(res.json()? await?)
   })
   .join_all()? await
   .collect()?

@lnicola , @nicoburns ,

Ich habe einen Pre-RFC-Thread für die Syntax val.[await future] unter https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await erstellt -Kette / 9304

@ Dowwie- Verfahrensfragen sollten an einer anderen Stelle besprochen werden, z. B. https://internals.rust-lang.org

Ich denke, wir haben zwei Lager in dieser Diskussion: Leute, die Ertragspunkte hervorheben möchten, gegenüber Menschen, die sie weniger hervorheben möchten.

Async in Rust, circa 2018, enthält den folgenden Klappentext:

Die Async / Warten-Notation ist eine Möglichkeit, die asynchrone Programmierung der synchronen Programmierung ähnlicher zu machen.

Nehmen Sie das einfache Tokio-Beispiel von oben (Ändern von unwrap() in ? ):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

und das Anwenden von Postfix Sigil führt zu

let response = client.get(uri).timeout(Duration::from_secs(10))!?

Dies ähnelt stark der synchronen Programmierung und es fehlen störende await sowohl der Präfix- als auch der Postfix-Notation.

Ich habe in diesem Beispiel ! als Postfix-Siegel verwendet, obwohl mir dieser Vorschlag in einem früheren Kommentar einige Abstimmungen eingebracht hat. Ich tat dies, weil das Ausrufezeichen in diesem Zusammenhang eine inhärente Bedeutung für mich hat, die sowohl @ (die mein Gehirn als "at" liest) als auch # fehlt. Aber das ist einfach Geschmackssache und nicht mein Punkt.

Ich würde einfach jedes Postfix-Siegel mit einem Zeichen allen anderen Alternativen vorziehen, gerade weil es sehr unauffällig ist, wodurch es einfacher wird, einen Überblick darüber zu bekommen, was der Code, den Sie lesen, tatsächlich tut, im Vergleich dazu, ob er asynchron ist oder nicht Ich würde ein Implementierungsdetail in Betracht ziehen. Ich würde nicht sagen, dass es überhaupt nicht wichtig ist, aber ich würde argumentieren, dass es für den Codefluss viel weniger wichtig ist als die vorzeitige Rückkehr im Fall von ? .

Anders ausgedrückt: Sie kümmern sich beim Schreiben von asynchronem Code hauptsächlich um Ertragspunkte, nicht so sehr beim Lesen. Da Code häufiger gelesen als geschrieben wird, wäre eine unauffällige Syntax für await hilfreich.

Ich möchte an die Erfahrung des C # -Teams erinnern (Hervorhebungen sind meine):

Dies ist auch der Grund, warum wir kein "implizites" Formular für "Warten" gewählt haben. In der Praxis war es etwas, worüber die Leute sehr klar nachdenken wollten und was sie in ihrem Code im Vordergrund haben wollten, damit sie darauf achten konnten. Interessanterweise ist diese Tendenz auch Jahre später geblieben. dh manchmal bedauern wir viele Jahre später, dass etwas übermäßig ausführlich ist . Einige Funktionen sind auf diese Weise schon früh gut, aber sobald die Leute damit vertraut sind, sind sie besser für etwas Terseres geeignet. Dies war bei "Warten" nicht der Fall. Die Leute scheinen die Schwere dieses Schlüsselworts immer noch wirklich zu mögen

Siegelzeichen sind ohne richtige Hervorhebung fast unsichtbar und für Neulinge weniger freundlich (wie ich wahrscheinlich schon sagte).


Aber wenn wir über Siegel sprechen, wird @ wahrscheinlich nicht englischsprachige (z. B. mich) nicht verwirren, weil wir es nicht als at lesen. Wir haben einen völlig anderen Namen dafür, der nicht automatisch ausgelöst wird, wenn Sie den Code lesen, sodass Sie ihn nur als ganze Hieroglyphe mit seiner eigenen Bedeutung verstehen.

@huxi Ich habe das Gefühl, dass Siegel schon ziemlich ausgeschlossen sind. Siehe Centrils Beitrag noch einmal aus Gründen, warum wir das Wort await .


Außerdem sollten alle, die Postfix nicht mögen, so viel warten und das pragmatische Argument verwenden: "Nun, es gibt nicht viel Code aus dem wirklichen Leben, der verkettet werden könnte. Daher sollte das Warten auf Postfix nicht hinzugefügt werden." Hier ist eine kleine Anekdote (die ich wahrscheinlich wegen schlechten Gedächtnisses schlachten werde):

Im Zweiten Weltkrieg verlor eine der kämpfenden Nationen viele Flugzeuge. Sie mussten ihre Flugzeuge irgendwie verstärken. Der naheliegende Weg, um zu wissen, wo sie sich konzentrieren sollten, besteht darin, sich die zurückgekehrten Flugzeuge anzusehen und zu sehen, wo die Kugeln am meisten getroffen haben. Es stellt sich heraus, dass in einem Flugzeug durchschnittlich 70% der Löcher in den Tragflächen, 10% im Triebwerksbereich und 20% in anderen Bereichen des Flugzeugs waren. Mit diesen Statistiken wäre es also sinnvoll, die Flügel zu verstärken, oder? Falsch! Der Grund dafür ist, dass Sie nur die Flugzeuge betrachten, die zurückgekommen sind. Und in diesen Flugzeugen scheint es nicht so schlimm zu sein, dass die Flügel durch Kugeln beschädigt werden. Alle zurückgekehrten Flugzeuge erlitten jedoch nur geringfügige Schäden am Triebwerksbereich, was zu der Schlussfolgerung führen kann, dass größere Schäden am Triebwerksbereich tödlich sind. Daher sollte stattdessen der Motorbereich verstärkt werden.

Mein Punkt bei dieser Anekdote ist: Vielleicht gibt es nicht viele echte Codebeispiele, die das Warten auf Postfix nutzen können, da in anderen Sprachen kein Warten auf Postfix vorhanden ist. Jeder ist es gewohnt, Präfix-Warte-Code zu schreiben, und er ist sehr daran gewöhnt, aber wir können nie wissen, ob die Leute anfangen würden, mehr zu verketten, wenn wir Postfix-Warten hätten.

Daher denke ich, dass die beste Vorgehensweise darin besteht, die Flexibilität zu nutzen, die uns nächtliche Builds bieten, und eine Präfixsyntax und eine Postfixsyntax auszuwählen, die der Sprache hinzugefügt werden sollen. Ich stimme für das Präfix "Obvious Precedence" und das Postfix .await . Dies sind nicht die endgültigen Syntaxoptionen, aber wir müssen zunächst eine auswählen, und ich denke, diese beiden Syntaxoptionen bieten im Vergleich zu anderen Syntaxoptionen eine neuere Erfahrung. Nachdem wir sie in der Nacht implementiert haben, können wir Nutzungsstatistiken und Meinungen zur Arbeit mit echtem Code mit beiden Optionen abrufen und dann die Syntaxdiskussion fortsetzen, diesmal unterstützt durch ein besseres pragmatisches Argument.

@ivandardi Die Anekdote ist ziemlich hartnäckig und IMHO passt nicht: Es ist eine motivierende Geschichte zu Beginn einer Reise, um die Leute daran zu erinnern, nach dem Nicht-Offensichtlichen zu suchen und alle Winkel abzudecken. Es ist keine, die gegen Opposition eingesetzt werden kann eine Diskussion. Es war passend für Rust 2018, wo es angesprochen und nicht an ein bestimmtes Thema gebunden wurde. Um es in einer Debatte gegen eine andere Seite einzusetzen, ist es unhöflich. Sie müssen die Position der Person behaupten, die mehr sieht oder mehr Visionen hat, damit es funktioniert. Ich glaube nicht, dass du das willst. Wenn Sie im Bild bleiben, ist Postfix möglicherweise in keiner der Sprachen verfügbar, da Postfix es nie nach Hause geschafft hat;).

Die Leute haben tatsächlich gemessen und geschaut: Wir haben einen verkettbaren Operator in Rust ( ? ), der selten zum Verketten verwendet wird. https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

Es wurde auch gut behandelt, dass dies nur eine Messung des aktuellen Zustands ist, wie @cenwangumass hier schön ausgedrückt hat: https://github.com/rust-lang/rust/issues/57640#issuecomment -457962730. Es ist also nicht so, dass die Leute dies als endgültige Zahlen verwenden.

Ich möchte jedoch eine Geschichte, in der Verkettung zu einem halbwegs dominanten Stil wird, wenn Postfix wirklich der richtige Weg ist. Davon bin ich allerdings nicht überzeugt. Ich habe oben auch erwähnt, dass das dominierende Beispiel, das hier verwendet wird ( reqwest ), nur 2 Wartezeiten erfordert, weil die API dies gewählt hat und eine bequeme Ketten-API ohne die Notwendigkeit von 2 Wartezeiten heute leicht erstellt werden kann.

Obwohl ich den Wunsch nach einer Forschungsphase sehr schätze, möchte ich darauf hinweisen, dass sich das Warten bereits stark verzögert, jede Forschungsphase wird dies noch verschlimmern. Wir haben auch keine Möglichkeit, Statistiken über viele Codebasen zu sammeln. Wir müssten selbst eine Sprechgruppe zusammenstellen. Ich würde hier gerne mehr Benutzerrecherchen durchführen, aber das braucht Zeit, ein noch nicht vorhandenes Setup und Leute, die es ausführen.

@Pzixel wegen

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

wie

`` `
lass foo = auf some_future warten ();
let bar = warte auf {other_future ()}?. bar_async ();
lass bar = auf {bar} warten?;

@Pzixel Haben Sie eine Quelle für dieses C # -Team-Erfahrungszitat? Denn der einzige, den ich finden konnte, war dieser Kommentar . Dies ist nicht als Anschuldigung oder so etwas gemeint. Ich möchte nur den vollständigen Text lesen.

Mein Gehirn übersetzt @ aufgrund seiner Verwendung in E-Mail-Adressen in "at". Dieses Symbol heißt in meiner Muttersprache "Klammeraffe", was grob "klammernder Affe" bedeutet. Ich weiß es wirklich zu schätzen, dass sich mein Gehirn stattdessen für "at" entschieden hat.

Meine 2 Cent als relativ neuer Rust-Benutzer (mit C ++ - Hintergrund, aber das hilft nicht wirklich).

Einige von Ihnen haben Neuankömmlinge erwähnt. Ich denke Folgendes:

  • Zunächst scheint mir ein await!( ... ) -Makro unverzichtbar zu sein, da es kinderleicht zu bedienen ist und nicht missverstanden werden kann. Sieht ähnlich aus wie println!() , panic!() , ... und genau das ist schließlich für try! passiert.
  • Obligatorische Trennzeichen sind ebenfalls einfach und eindeutig.
  • Eine Postfix-Version, entweder Feld, Funktion oder Makro, wäre meiner Meinung nach nicht schwer zu lesen oder zu schreiben, da Neulinge einfach sagen würden: "OK, so machen Sie das." Dieses Argument gilt auch für das Postfix-Schlüsselwort. Das ist "ungewöhnlich", aber "warum nicht".
  • In Bezug auf die Präfixnotation mit nützlicher Priorität denke ich, dass dies verwirrend aussehen würde. Die Tatsache, dass await enger bindet, wird einige umhauen, und ich denke, dass einige Benutzer es einfach vorziehen, viele Klammern zu setzen, um dies klar zu machen (Verkettung oder nicht).
  • Ein offensichtlicher Vorrang ohne Zucker ist einfach zu verstehen und zu lehren. Um den Zucker einzuführen, nennen Sie einfach await? ein nützliches Schlüsselwort. Nützlich, weil es die Notwendigkeit umständlicher Klammern beseitigt:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids? warten ... unwraps the future, so you have to use ? to unwrap the Result // but there is some sugar if you want, thanks to the warten? `Operator
    Antwort warten lassen? http :: get ("https://www.rust-lang.org/");
    // aber du solltest nicht verketten, da diese Syntax nicht zu lesbarem verkettetem Code führt
- sigils can be understood quite easily *if the chosen character makes sense* if it is introduced to be "the `?` for futures".

That being said, since no agreement seems to be reached, I think it would be reasonable to ship `await!()` to stable Rust. Then this discussion can be extended without blocking the whole process. Same that what happened for `try!()`/`?`, so again newcomers won't be lost. And if [Simple postfix macros](https://github.com/rust-lang/rfcs/pull/2442) get accepted, the problem will disappear since we'll get postfix macro for "free".

---

Just a thought, what about a postfix keyword, but which can be put as prefix as well, similar in some ways to the `const` keyword of C++? (I don't know if that was already proposed) In prefix position, it behaves like "prefix `await` with obvious precedence and optional sugar":
```c#
// preferred without chaining:
let response = await? http::get("https://www.rust-lang.org/");

// but also possible: (rustfmt warning)
let response = http::get("https://www.rust-lang.org/") await?;
let response = (http::get("https://www.rust-lang.org/") await)?;
let response = (await http::get("https://www.rust-lang.org/"))?;

// chains well
let matches = http::get("https://www.rust-lang.org/") await?
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;

// any of these are also allowed, but arguably ugly (rustfmt warning again)
let matches = await ((http::get("https://www.rust-lang.org/") await?)
    .body?
    .async_regex_search("(?=(\d+))\w+\1"));
let matches = (await? http::get("https://www.rust-lang.org/"))
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;
let matches = await http::get("https://www.rust-lang.org/") await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1");
let matches = await (await http::get("https://www.rust-lang.org/"))?
    .body?
    .async_regex_search("(?=(\d+))\w+\1");
let matches = await!(
    http::get("https://www.rust-lang.org/")) await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
);
let matches = await { // <-- parenthesis or braces optional here, but they clarify
    (await? http::get("https://www.rust-lang.org/"))
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
};

Wie man das lehrt:

  • ( await!() Makro möglich)
  • Präfix empfohlen, wenn keine Verkettung mit Zucker erfolgt (siehe oben)
  • Postfix mit Verkettung empfohlen
  • Es ist möglich, sie zu mischen, aber nicht empfohlen
  • Bei der Verkettung mit Kombinatoren kann das Präfix verwendet werden

Aus eigener Erfahrung würde ich sagen, dass das Warten auf das Präfix kein Problem für die Verkettung darstellt.
Verkettung hängt viel an Javascript-Code mit Präfix-Warten und Kombinatoren wie f.then(x => ...) ohne meiner Meinung nach die Lesbarkeit zu verlieren, und sie scheinen keine Notwendigkeit zu haben, Kombinatoren gegen Postfix-Warten zu tauschen.

Machen:

let ret = response.await!().json().await!().to_string();

ist das gleiche wie:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

Ich sehe nicht wirklich die Vorteile von Postfix über Kombinatorketten.
Ich finde es einfacher zu verstehen, was im zweiten Beispiel passiert.

Im folgenden Code werden keine Probleme mit der Lesbarkeit, Verkettung oder Priorität angezeigt:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Ich würde für dieses Präfix warten, weil:

  • der Vertrautheit mit anderen Sprachen.
  • der Ausrichtung mit den anderen Schlüsselwörtern (Rückgabe, Unterbrechung, Fortsetzung, Ausbeute usw.).
  • es fühlt sich immer noch wie Rust an (mit Kombinatoren wie mit Iteratoren).

Async / await wird eine großartige Ergänzung der Sprache sein und ich denke, dass mehr Leute nach dieser Ergänzung und ihrer Stabilisierung mehr zukunftsbezogenes Material verwenden werden.
Einige entdecken möglicherweise sogar zum ersten Mal asynchrone Programmierung mit Rust.
Daher kann es hilfreich sein, die Sprachkomplexität gering zu halten, während die Konzepte hinter Async bereits schwer zu erlernen sind.

Und ich denke nicht, dass der Versand von Präfix und Postfix eine gute Idee ist, aber dies ist nur eine persönliche Meinung.

@huxi Ja , ich habe es oben bereits verlinkt, aber natürlich kann ich es wiederholen: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Mein Gehirn übersetzt @ aufgrund seiner Verwendung in E-Mail-Adressen in "at". Dieses Symbol heißt in meiner Muttersprache "Klammeraffe", was grob "klammernder Affe" bedeutet. Ich weiß es wirklich zu schätzen, dass sich mein Gehirn stattdessen für "at" entschieden hat.

Ich habe eine ähnliche Geschichte: In meiner Sprache ist es "Hund", aber es hat keinen Einfluss auf das Lesen von E-Mails.
Schön, Ihre Erfahrung zu sehen.

@llambda die Frage war über die Verkettung. Natürlich können Sie auch zusätzliche Variablen einführen. Aber trotzdem sieht dieses await { foo }? anstelle von await? foo oder foo await? werid aus.

@ Motorigolo
Netter Post. Aber ich denke nicht, dass Ihr zweiter Vorschlag ein guter Weg ist. Wenn Sie zwei Möglichkeiten einführen, um etwas zu tun, verursachen Sie nur Verwirrung und Probleme, z. B. benötigen Sie die Option rustfmt, oder Ihr Code wird zu einem Chaos.

@Hirevo async/await sollen die Notwendigkeit in Kombinatoren beseitigen. Kehren wir nicht zu "aber Sie können es nur mit Kombinatoren tun" zurück. Ihr Code ist der gleiche wie

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

Entfernen wir also async/await insgesamt?

Allerdings sind Kombinatoren weniger ausdrucksstark und weniger praktisch, und manchmal kann man einfach nicht ausdrücken, was await könnte, z. B. das Ausleihen zwischen Wartepunkten (wofür Pin wurde).

Ich sehe im folgenden Code keine Probleme mit der Lesbarkeit, Verkettung oder Priorität

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Ich mache:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = serde_json::from_str(user);
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = serde_json::from_str(permissions );
    Ok(user)
}

Ist etwas Seltsames los? Kein Problem:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = dbg!(fetch(format!("/user/{0}", name).as_str()) await?);
    let user: User = dbg!(serde_json::from_str(user));
    let permissions = dbg!(fetch(format!("/permissions/{0}", x.id).as_str()) await?);
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions));
    Ok(user)
}

Es ist umständlicher, es mit Kombinatoren zum Laufen zu bringen. Ganz zu schweigen davon, dass Ihre ? in then / map -Funktionen nicht wie erwartet funktionieren und Ihr Code ohne into_future() und einige andere nicht funktioniert seltsame Dinge, die Sie im async/await Flow nicht brauchen.

Ich bin damit einverstanden, dass mein Vorschlag nicht optimal ist, da er eine Menge legaler Syntax mit dem Schlüsselwort async einführt. Ich glaube jedoch, dass die Regeln leicht zu verstehen sind und alle unsere Anwendungsfälle erfüllen.

Aber auch das würde viele erlaubte Variationen bedeuten:

  • await!(...) , aus Gründen der Konsistenz mit try!() und bekannten Makros wie println!()
  • await , await(...) , await { ... } , dh. Präfixsyntax ohne Zucker
  • await? , await?() , await? {} , dh. Präfixsyntax mit Zucker
  • ... await , dh. Postfix-Syntax (plus ... await? , aber das ist kein Zucker)
  • alle Kombinationen davon, die aber nicht empfohlen werden (siehe meinen vorherigen Beitrag )

In der Praxis erwarten wir jedoch nur:

  • Präfix ohne Result s: await oder await { ... } zur Verdeutlichung mit langen Ausdrücken
  • Präfix mit Result s: await? oder await? {} , um mit langen Ausdrücken zu verdeutlichen
  • Postfix beim Verketten: ... await , ... await?

+1 zu versenden warten! () Makro zu stabil. Morgen, wenn möglich 😄

Es gibt viele Spekulationen darüber, wie dieses Muster verwendet wird, und Bedenken hinsichtlich der Ergonomie in solchen Fällen. Ich schätze diese Bedenken, sehe aber keinen zwingenden Grund, warum dies keine iterative Änderung sein könnte. Dies ermöglicht die Generierung realer Nutzungsmetriken, die später die Optimierung beeinflussen können (sofern dies erforderlich ist).

Wenn die Übernahme von Async Rust für größere Kisten / Projekte eine größere Anstrengung darstellt als jede spätere optionale Umgestaltung, um zu einer Syntax zu gelangen, die prägnanter / ausdrucksvoller ist, dann empfehle ich nachdrücklich, dass wir diese Adoptionsbemühungen sofort beginnen lassen. Die anhaltende Unsicherheit verursacht Schmerzen.

@ Pixel
(Zuerst habe ich einen Fehler beim Aufrufen der Funktion fetch_user gemacht. Ich werde ihn in diesem Beitrag beheben.)

Ich sage nicht, dass async / await nutzlos ist und dass wir es für Kombinatoren entfernen sollten.
Await ermöglicht es, einen Wert aus einer Zukunft an eine Variable innerhalb desselben Bereichs zu binden, nachdem er aufgelöst wurde. Dies ist sehr nützlich und nicht einmal mit Kombinatoren allein möglich.
Warten zu entfernen war einfach nicht mein Punkt.
Ich habe gerade gesagt, dass Kombinatoren sehr gut mit Warten spielen können und dass das Verkettungsproblem behoben werden kann, indem nur der gesamte von Kombinatoren erstellte Ausdruck abgewartet wird (wodurch die Notwendigkeit von await (await fetch("test")).json() oder await { await { fetch("test") }.json() } entfällt).

In meinem Codebeispiel verhält sich ? wie durch Kurzschließen beabsichtigt, sodass der Abschluss ein Err(...) , nicht die gesamte Funktion (dieser Teil wird von await? an der ganzen Kette).

Sie haben mein Codebeispiel effektiv umgeschrieben, aber den Verkettungsteil davon entfernt (durch Binden).
Für den Debugging-Teil hat das Folgende genau das gleiche Verhalten mit nur der erforderlichen Datenbank! und nichts weiter (nicht einmal zusätzliche Klammern):

async fn fetch_permissions(name: &str) -> Result<Vec<Permission>, Error> {
    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| dbg!(serde_json::from_str::<User>(dbg!(x)?)))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str())))
        .map(|x| dbg!(serde_json::from_str::<Vec<Permission>>(dbg!(x)?)));
    Ok(user)
}

Ich weiß nicht, wie ich dasselbe ohne Kombinatoren und ohne zusätzliche Bindungen oder Klammern machen soll.
Einige Leute verketten, um zu vermeiden, dass der Bereich mit temporären Variablen gefüllt wird.
Ich wollte nur sagen, dass Kombinatoren nützlich sein können und nicht ignoriert werden sollten, wenn eine Entscheidung über eine bestimmte Syntax hinsichtlich ihrer Verkettungsfähigkeit getroffen wird.

Und schließlich, aus Neugier, warum würde der Code ohne .into_future() nicht funktionieren, sind sie nicht bereits Futures (ich bin kein Experte in diesem Bereich, aber ich würde erwarten, dass sie bereits Futures sind)?

Ich möchte ein bedeutendes Problem mit fut await hervorheben: Es beeinträchtigt ernsthaft das Lesen von Code. Programmierausdrücke haben einen gewissen Wert, der Phrasen ähnelt, die in einer natürlichen Sprache verwendet werden. Dies ist einer der Gründe, warum wir Konstrukte wie for value in collection {..} und warum wir in den meisten Sprachen a + b schreiben ("a plus b") anstelle von a b + , und das Schreiben / Lesen von "Warten auf etwas" ist für Englisch (und andere SVO- Sprachen) viel natürlicher als "Etwas wartet". Stellen Sie sich vor, wir hätten anstelle von ? ein Postfix-Schlüsselwort try : let val = foo() try; .

fut.await!() und fut.await() haben dieses Problem nicht, da sie wie vertraute Blockierungsaufrufe aussehen (aber "Makro" betont zusätzlich die zugehörige "Magie"), sodass sie anders wahrgenommen werden als ein getrennter Raum Stichwort. Sigil wird auch abstrakter und ohne direkte Parallelen zu Phrasen in natürlicher Sprache anders wahrgenommen, und aus diesem Grund halte ich es für irrelevant, wie die Leute die vorgeschlagenen Sigils lesen.

Fazit: Wenn entschieden wird, sich an das Schlüsselwort zu halten, sollten wir meines Erachtens nur aus Präfixvarianten wählen.

@rpjohnst Ich bin mir nicht sicher, was dein Punkt ist. actual_fun(a + b)? und break (a + b)? sind mir wegen der unterschiedlichen Priorität wichtig, daher weiß ich nicht, was await(a + b)? soll.

Ich habe diese Diskussion von weitem verfolgt und habe ein paar Fragen und Kommentare.

Mein erster Kommentar ist, dass ich glaube, dass das Postfix-Makro .await!() alle Hauptziele von Centril erfüllt, mit Ausnahme des ersten Punktes:

" await sollte ein Schlüsselwort bleiben, um zukünftiges Sprachdesign zu ermöglichen."

Welche anderen Verwendungen des Schlüsselworts await sehen wir in Zukunft?


Bearbeiten : Ich habe falsch verstanden, wie genau await funktioniert. Ich habe meine falschen Aussagen durchgestrichen und die Beispiele aktualisiert.

Mein zweiter Kommentar ist, dass das Schlüsselwort await in Rust völlig andere Dinge tut als await in einer anderen Sprache, und dies kann zu Verwirrung und unerwarteten Rennbedingungen führen.

async function waitFor6SecondThenReturn6(){
  let result1 = await waitFor1SecondThenReturn1(); // executes first
  let result2 = await waitFor2SecondThenReturn2(); // executes second
  let result3 = await waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

Ich glaube, dass ein Präfix async einen viel klareren Job macht, was darauf hinweist, dass die asynchronen Werte Teile einer vom Compiler erstellten Zustandsmaschine sind:

async function waitFor6SecondThenReturn6(){
  let async result1 = waitFor1SecondThenReturn1(); // executes first
  let async result2 = waitFor2SecondThenReturn2(); // executes second
  let async result3 = waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

Es ist intuitiv, dass Sie mit async Funktionen async Werte verwenden können. Dies schafft auch die Intuition, dass im Hintergrund asynchrone Maschinen am Werk sind und wie sie möglicherweise funktionieren.

Diese Syntax enthält einige ungelöste Fragen und eindeutige Probleme mit der Kompositionsfähigkeit. Sie macht jedoch deutlich, wo asynchrone Arbeit stattfindet, und dient als synchrone Ergänzung zu den komponierbareren und verkettbareren .await!() .

Ich hatte Schwierigkeiten, das Postfix await am Ende einiger Ausdrücke in früheren Beispielen im prozeduralen Stil zu bemerken. So würde das mit dieser vorgeschlagenen Syntax aussehen:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let async user = dbg!(fetch(format!("/user/{0}", name).as_str()));
    let user: User = dbg!(serde_json::from_str(user?));
    let async permissions = dbg!(fetch(format!("/permissions/{0}", user.id).as_str()));
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions?));
    Ok(user)
}

(Es gibt auch das Argument für ein leicht zusammensetzbares und verkettbares .dbg!() -Makro, aber das ist für ein anderes Forum.)

Am Dienstag, den 29. Januar 2019 um 23:31:32 Uhr -0800 schrieb Sphericon:

Ich habe diese Diskussion von weitem verfolgt und habe ein paar Fragen und Kommentare.

Mein erster Kommentar ist, dass ich glaube, dass das Postfix-Makro .await!() alle Hauptziele von Centril erfüllt, mit Ausnahme des ersten Punktes:

" await sollte ein Schlüsselwort bleiben, um zukünftiges Sprachdesign zu ermöglichen."

Als einer der Hauptbefürworter der .await!() -Syntax habe ich
Denken Sie absolut, dass await ein Schlüsselwort bleiben sollte. Vorausgesetzt, wir werden
Ich brauche sowieso .await!() , um in den Compiler eingebaut zu werden
trivial.

@Hirevo

Sie haben mein Codebeispiel effektiv umgeschrieben, aber den Verkettungsteil davon entfernt (durch Binden).

Ich verstehe diesen Ansatz "Verketten für Verketten" nicht. Verkettung ist gut, wenn sie gut ist, z. B. nicht, um nutzlose temporäre Variablen zu erstellen. Sie sagen "Sie haben die Verkettung entfernt", aber Sie können sehen, dass sie hier keinen Wert liefert.

Für den Debugging-Teil hat das Folgende genau das gleiche Verhalten mit nur der erforderlichen Datenbank! und nichts mehr (nicht einmal zusätzliche Klammern)

Nein, du hast das getan

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = dbg!(serde_json::from_str(dbg!(user)));
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
    Ok(user)
}

Es ist nicht dasselbe.

Ich habe gerade gesagt, dass Kombinatoren sehr gut mit Warten spielen können und dass das Verkettungsproblem gelöst werden kann, indem nur der gesamte von Kombinatoren erstellte Ausdruck abgewartet wird (wodurch die Notwendigkeit des Wartens (Warten auf Abrufen ("Test")) beseitigt wird. Json () oder Warten {warte auf {fetch ("test")} .json ()}).

Es ist nur ein Problem für das Präfix await , es existiert nicht für andere Formen davon.

Ich weiß nicht, wie ich dasselbe ohne Kombinatoren und ohne zusätzliche Bindungen oder Klammern machen soll.

Warum nicht diese Bindungen erstellen? Bindungen sind zum Beispiel immer besser für den Leser und wenn Sie einen Debugger haben, der einen Bindungswert drucken kann, aber keinen Ausdruck auswerten kann.

Schließlich haben Sie tatsächlich keine Bindungen entfernt. Sie haben gerade die labmda |user| -Syntax verwendet, bei der ich let user = ... . Es hat Ihnen nichts erspart, aber jetzt ist dieser Code schwerer zu lesen, schwerer zu debuggen und hat keinen Zugriff auf das übergeordnete Element (z. B. müssen Sie Fehler extern in die Methodenkette einschließen, anstatt sie selbst aufzurufen). wahrscheinlich).


Kurz gesagt: Verkettung bietet keinen Wert für sich. Es kann in einigen Szenarien nützlich sein, aber es ist keines davon. Und da ich async / await - Code für mehr als sechs Jahre schreiben, ich glaube Sie jemals nicht schreiben Asynchron - Code verkettet werden sollen. Nicht weil Sie es nicht konnten, sondern weil es unpraktisch und verbindlich ist, immer besser zu lesen und oft zu schreiben. Um nicht zu sagen, dass Sie überhaupt keine Kombinatoren benötigen. Wenn Sie async/await , benötigen Sie diese Hunderte von Methoden nicht für futures / streams , sondern nur zwei: join und select . Alles andere kann über Iteratoren / Bindungen / ... erledigt werden, dh über gängige Sprachwerkzeuge, mit denen Sie keine weitere Infrastruktur erlernen können.

Die AFSIK- Community von await entweder als Schlüsselwort oder als Siegel zu verwenden. Für Ihre Ideen zu async ein weiterer RFC erforderlich.

Mein zweiter Kommentar ist, dass das Schlüsselwort "Warten" in Rust völlig andere Funktionen hat als das Warten in einer anderen Sprache. Dies kann zu Verwirrung und unerwarteten Rennbedingungen führen.

Können Sie detaillierter sein? Ich habe keinen Unterschied zwischen JS / C # erwartet, außer dass Futures auf Umfragen basieren, aber es hat wirklich nichts mit async/await zu tun.

In Bezug auf die an mehreren Stellen hier vorgeschlagene Präfixsyntax await? :
`` `C #
foo = warten lassen? bar_async ();

How would this look with ~~futures of futures~~ result of results *) ? I.e., would it be arbitrarily extensible:
```C#
let foo = await?? double_trouble();

IOW, Präfix await? sieht aus wie eine Syntax, die für mich zu speziell ist.

) * bearbeitet.

Wie würde dies mit Futures of Futures aussehen? Dh wäre es willkürlich erweiterbar:

let foo = await?? double_trouble();

IOW, await? sieht aus wie eine Syntax, die für mich zu speziell ist.

@rolandsteiner mit "Futures of Futures" meinen Sie impl Future<Output = Result<Result<_, _>, _>> (ein await + zwei ? bedeutet für mich, eine einzelne Zukunft und zwei Ergebnisse "auszupacken", ohne auf verschachtelte Futures zu warten ).

await? _ist_ ein Sonderfall, aber es ist ein Sonderfall, der wahrscheinlich für 90% + Verwendungen von await . Der gesamte Punkt der Futures dient dazu, auf asynchrone _IO_-Operationen zu warten. E / A ist fehlbar, sodass 90% + von async fn wahrscheinlich io::Result<_> (oder einen anderen Fehlertyp, der eine E / A-Variante enthält) zurückgeben ). Funktionen, die Result<Result<_, _>, _> sind derzeit ziemlich selten, daher würde ich nicht erwarten, dass sie eine Sonderfall-Syntax erfordern.

@ Nemo157 Sie haben natürlich Recht: Ergebnis der Ergebnisse. Mein Kommentar wurde aktualisiert.

Heute schreiben wir

  1. await!(future?) für future: Result<Future<Output=T>,E>
  2. await!(future)? für future: Future<Output=Result<T,E>>

Und wenn wir await future? schreiben, müssen wir herausfinden, welches es bedeutet.

Aber ist es so, dass aus Fall 1 immer Fall 2 werden kann? In Fall 1 erzeugt der Ausdruck entweder eine Zukunft oder einen Fehler. Der Fehler kann jedoch verzögert und in die Zukunft verschoben werden. Wir können also nur Fall 2 behandeln und hier eine automatische Konvertierung durchführen.

Aus Sicht des Programmierers garantiert Result<Future<Output=T>,E> eine frühzeitige Rückkehr für den Fehlerfall, außer dass die beiden das gleiche Sementic haben. Ich kann mir vorstellen, dass der Compiler funktionieren kann, und den zusätzlichen Aufruf von poll vermeiden, wenn der Fehlerfall unmittelbar bevorsteht.

Der Vorschlag lautet also:

await exp? kann als await (exp?) interpretiert werden, wenn exp Result<Future<Output=T>,E> , und als (await exp)? interpretiert werden, wenn exp Future<Output=Result<T,E>> . In beiden Fällen wird es fälschlicherweise frühzeitig zurückkehren und bei Ausführung in Ordnung zum wahren Ergebnis aufgelöst.

Für kompliziertere Fälle können wir so etwas wie die automatische Empfängerreferenzierung anwenden:

> Wenn wir await exp???? interperten, überprüfen wir zuerst exp und wenn es Result , try und machen Sie weiter, wenn das Ergebnis immer noch Result bis ? oder etwas haben, das nicht Result . Dann muss es eine Zukunft sein und wir await darauf und wenden den Rest ? s an.

Ich war ein Postfix-Keyword- / Sigil-Unterstützer und bin es immer noch. Ich möchte jedoch nur zeigen, dass die Priorität des Präfixes in der Praxis möglicherweise kein großes Problem darstellt und Problemumgehungen aufweist.

Ich weiß, dass die Mitglieder des Rust-Teams implizite Dinge nicht mögen, aber in einem solchen Fall gibt es nur einen zu geringen Unterschied zwischen den potenziellen Sementics und wir haben einen guten Weg, um sicherzustellen, dass wir das Richtige tun.

await? ist ein Sonderfall, aber es ist ein Sonderfall, der wahrscheinlich für 90% + Verwendungen von await . Der gesamte Punkt der Futures dient dazu, auf asynchrone E / A-Operationen zu warten. E / A ist fehlbar, sodass 90% + von async fn wahrscheinlich io::Result<_> (oder einen anderen Fehlertyp, der eine E / A-Variante enthält) zurückgeben ). Funktionen, die Result<Result<_, _>, _> sind derzeit ziemlich selten, daher würde ich nicht erwarten, dass sie eine Sonderfall-Syntax erfordern.

Sonderfälle sind schlecht zu komponieren, zu erweitern oder zu lernen und werden schließlich zu Gepäck. Es ist kein guter Kompromiss, Ausnahmen von den Regeln der Sprache für einen einzelnen theoretischen Usability-Anwendungsfall zu machen.

Wäre es möglich, Future für Result<T, E> where T: Future zu implementieren? Auf diese Weise können Sie nur await result_of_future ohne es mit ? auspacken zu müssen. Und das würde natürlich ein Ergebnis zurückgeben, also würden Sie es await result_of_future , was (await result_of_future)? bedeuten würde. Auf diese Weise würden wir die await? -Syntax nicht benötigen und die Präfixsyntax wäre etwas konsistenter. Lassen Sie mich wissen, wenn daran etwas nicht stimmt.

Zusätzliche Argumente für await mit obligatorischen Trennzeichen sind (ich persönlich bin mir nicht sicher, welche Syntax mir insgesamt am besten gefällt):

  • Kein spezielles Gehäuse des Operators ? , kein await? oder await??
  • Kongruent mit vorhandenen Kontrollflussoperatoren wie loop , while und for , für die ebenfalls obligatorische Trennzeichen erforderlich sind
  • Fühlt sich bei ähnlichen bestehenden Rust-Konstrukten am wohlsten
  • Durch den Wegfall eines speziellen Gehäuses können Probleme beim Schreiben von Makros vermieden werden
  • Verwendet keine Siegel oder Postfix, um Ausgaben aus dem Fremdbudget zu vermeiden

Beispiel:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

Nach dem Herumspielen in einem Editor fühlt es sich jedoch immer noch umständlich an. Ich denke, ich hätte lieber await und await? ohne Trennzeichen, wie bei break und return .

Wäre es möglich, Future for Result zu implementieren?wo T: Zukunft?

Sie würden das Gegenteil wollen. Am häufigsten wird eine Zukunft erwartet, deren Ausgabetyp ein Ergebnis ist.

Es gibt dann das explizite Argument, das ? zu verbergen oder auf andere Weise zu absorbieren. Was ist, wenn Sie mit dem Ergebnis usw. übereinstimmen möchten?

Wenn Sie ein Result<Future<Result<T, E2>>, E1> und darauf warten, wird ein Result<Result<T, E2>, E1> .

Wenn Sie ein Future<Result<T, E1>> , wird beim Warten darauf einfach das Result<T, E1> .

Es gibt kein Verstecken oder Absorbieren der ? in der Wartezeit, und Sie können mit dem Ergebnis anschließend alles tun, was benötigt wird.

Oh. Ich muss dich dann missverstanden haben. Ich sehe jedoch nicht, wie das hilft, da wir immer noch die ? kombinieren müssen, um 99% der Zeit zu warten.


Oh. Die Syntax await? soll (await future)? implizieren, was der übliche Fall wäre.

Genau. Wir würden also die Bindung von await in await expr? enger machen, und wenn dieser Ausdruck ein Result<Future<Result<T, E2>>, E1> ist, würde er zu etwas vom Typ Result<T, E2> ausgewertet . Es würde bedeuten, dass es kein spezielles Gehäuse gibt, auf das bei Ergebnistypen gewartet werden kann. Es folgt nur den normalen Trait-Implementierungen.

@ivandardi was ist mit Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Beachten Sie, dass diese Form der Zukunft weg ist. Es gibt jetzt einen einzigen zugeordneten Typ, Ausgabe (was wahrscheinlich ein Ergebnis sein wird).


@ivandardi Okay. Ich sehe es jetzt. Das einzige, was Sie gegen Sie haben würden, ist, dass der Vorrang etwas Seltsames ist, das Sie dort lernen müssten, da es eine Abweichung ist, aber das meiste von allem mit await nehme ich an.

Obwohl ein Ergebnis, das eine Zukunft zurückgibt, so selten ist, habe ich keinen Fall gefunden, abgesehen von etwas im Tokio-Kern, das entfernt wurde. Ich glaube, wir brauchen keine Zucker- / Merkmal-Geräte, um in diesem Fall zu helfen.

@ivandardi was ist mit Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Nun, ich würde annehmen, dass dies nicht möglich ist, da dem Merkmal Future nur ein Output Typ zugeordnet ist.


@mehcode Nun, es würde einige der Bedenken ansprechen, die zuvor

Nun, ich würde annehmen, dass dies nicht möglich ist, da das Future-Merkmal nur einen Output-Typ hat.

warum nicht?

fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
    match val {
        0 => Ok(ok(15)),
        1 => Ok(err(100500)),
        _ => Err("Coulnd't create a future"),
    }
}

@Pzixel Siehe https://doc.rust-lang.org/std/future/trait.Future.html

Sie sprechen über das alte Merkmal, das sich in der futures -Kiste befand.

Ehrlich gesagt glaube ich nicht, dass ein Schlüsselwort in der Präfixposition so ist, wie es sein sollte:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (yield self.request(url, Method::GET, None, true)))?;
    let user = (yield user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

hat einen starken Vorteil gegenüber einem Siegel in derselben Position:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (*self.request(url, Method::GET, None, true))?;
    let user = (*user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}

Ich denke, dass es nur inkonsistent mit anderen Präfixoperatoren aussieht, redundante Leerzeichen vor dem Ausdruck hinzufügt und den Code in spürbarer Entfernung nach rechts verschiebt.


Wir können versuchen, sigil mit erweiterter Punktsyntax ( Pre-RFC ) zu verwenden, um Probleme mit tief verschachtelten Bereichen zu beheben:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?;
    let user = user.res.[*json::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}

sowie fügt Möglichkeit zu Kettenmethoden hinzu:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[*request(url, Method::GET, None, true)]?
        .res.[*json::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

Und natürlich lassen Sie * durch @ ersetzen, was hier sinnvoller ist:

async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = (@self.request(url, Method::GET, None, true))?;
    let user = (@user.res.json::<UserResponse>())?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?;
    let user = user.res.[<strong i="27">@json</strong>::<UserResponse>()]?;
    let user = user.user.into();
    Ok(user)
}
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.[@request(url, Method::GET, None, true)]?
        .res.[<strong i="30">@json</strong>::<UserResponse>()]?
        .user
        .into();
    Ok(user)
}

Was mir hier gefällt, ist, dass @ await das auf der LHS der Funktionsdeklaration platziert ist, während ? Result<User> widerspiegelt, das auf der RHS platziert ist der Funktionsdeklaration. Dies macht @ extrem konsistent mit ? .


Irgendwelche Gedanken dazu?

Irgendwelche Gedanken dazu?

Alle zusätzlichen Zahnspangen sind nur ein No-Go

@mehcode Ja , ich wusste nicht, dass Futures jetzt keinen Error Typ mehr haben. Der Punkt ist jedoch weiterhin gültig: Sie können eine Funktion haben, die wahrscheinlich eine Funktion zurückgibt, die nach Abschluss möglicherweise Ergebnisse oder Fehler zurückgibt.

Irgendwelche Gedanken dazu?

Ja! Laut Centrils Kommentar sind Siegel nicht sehr greifbar. Ich würde nur anfangen, Siegel in Betracht zu ziehen, wenn man einen regulären Ausdruck erstellen kann, der alle erwarteten Punkte identifiziert.

Ihr Vorschlag für eine erweiterte Punktsyntax müsste viel ausführlicher erläutert werden, um die Semantik und das Desugaring jeder Verwendung zu gewährleisten. Derzeit kann ich die Bedeutung der Code-Schnipsel, die Sie mit dieser Syntax gepostet haben, nicht verstehen.


Der Punkt ist jedoch weiterhin gültig: Sie können eine Funktion haben, die wahrscheinlich eine Funktion zurückgibt, die nach Abschluss möglicherweise Ergebnisse oder Fehler zurückgibt.

Sie hätten also ein Future<Item=Result<T, E>> , oder? Nun, in diesem Fall warten Sie einfach ... auf die Zukunft und beschäftigen sich mit dem Ergebnis 😐

Angenommen, foo ist vom Typ Future<Item=Result<T, E>> . Dann ist await foo ein Result<T, E> und Sie können Folgendes verwenden, um die Fehler zu beheben:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

Sie hätten also eine Zukunft>, richtig? Nun, in diesem Fall warten Sie einfach ... auf die Zukunft und beschäftigen sich mit dem Ergebnis 😐

Nein, ich meine Result<Future<Item=Result<T, E>>, OtherE>

Mit der Postfix-Variante tun Sie es einfach

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

@melodicstream Ich habe meinen Beitrag aktualisiert und einen Link zu Pre-RFC für diese Funktion hinzugefügt

Oh je ... Ich habe gerade zum dritten Mal alle Kommentare noch einmal gelesen ...

Das einzig beständige Gefühl ist meine Abneigung gegen die Postfix-Notation .await in all ihren Variationen, da ich ernsthaft erwarten würde, dass await Future mit dieser Syntax Teil von

Ich würde mit der super einfachen await Präfixnotation ohne obligatorische Klammern gehen, hauptsächlich wegen des KISS-Prinzips und weil es viel wert ist, es ähnlich wie die meisten anderen Sprachen zu machen.

Das Entzuckern von await? future auf (await future)? wäre in Ordnung und würde geschätzt, aber alles darüber hinaus scheint mir immer mehr eine Lösung ohne Probleme zu sein. Das einfache erneute Binden von let verbesserte in den meisten Beispielen die Lesbarkeit des Codes, und ich persönlich würde diesen Weg wahrscheinlich gehen, während ich Code schreibe, selbst wenn eine einfache Verkettung eine Option wäre.

Trotzdem bin ich ziemlich froh, dass ich nicht in der Lage bin, darüber zu entscheiden.

Ich werde dieses Problem jetzt in Ruhe lassen, anstatt weiteren Lärm hinzuzufügen, und auf das endgültige Urteil warten (Wortspiel beabsichtigt).

... zumindest werde ich ehrlich versuchen, das zu tun ...

@Pzixel Angenommen, foo ist vom Typ Result<Future<Item=Result<T, E1>>, E2> , dann wäre await foo vom Typ Result<Result<T, E1>, E2> und dann können Sie einfach mit diesem Ergebnis entsprechend umgehen.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream nein, wird es nicht. Sie können nicht auf Result, warten. Sie können auf Future. So you have to do foo () warten? to unwrap Future from Ergebnis , then do warten auf to get a result, then again ? `, Um das Ergebnis aus der Zukunft auszupacken.

In Postfix-Weise wird es foo? await? , in Präfix ... Ich bin nicht sicher.

Ihre Beispiele funktionieren also einfach nicht, besonders das letzte, weil es so sein sollte

(await foo.unwrap()).unwrap()

@Huxi kann jedoch richtig sein, wir lösen das Problem, das wahrscheinlich nicht existiert. Der beste Weg, um herauszufinden, ob Postfix-Makros zulässig sind und die echte Codebasis nach der grundlegenden Übernahme von async/await angezeigt wird.

@Pzixel Deshalb habe ich den Vorschlag gemacht, Future für alle Arten von Result<Future<Item=T>, E> zu implementieren. Das würde erlauben, was ich sage.

Obwohl ich mit await foo?? für Result<Future<Output=Result<T, E1>>, E2> einverstanden bin, bin ich mit await foo.unwrap().unwrap() NICHT zufrieden. In meinem ersten Gehirnmodell muss dies sein

(await foo.unwrap()).unwrap()

Sonst werde ich wirklich verwirrend sein. Der Grund ist, dass ? ein allgemeiner Operator ist und unwrap eine Methode. Der Compiler kann etwas Besonderes für Operatoren wie . tun, aber wenn es sich um eine normale Methode handelt, gehe ich davon aus, dass sie sich immer nur auf den nächsten Ausdruck auf der linken Seite bezieht.

Die Postfix-Syntax foo.unwrap() await.unwrap() ist für mich ebenfalls in Ordnung, da ich weiß, dass await nur ein Schlüsselwort ist, kein Objekt, daher muss es Teil des Ausdrucks vor unwrap() .

Das Makro im Postfix-Stil löst viele dieser Probleme auf gute Weise, aber nur die Frage, ob wir mit den vorhandenen Sprachen vertraut bleiben und das Präfix beibehalten möchten. Ich würde für den Postfix-Stil stimmen.

Habe ich recht, dass folgender Code nicht gleich ist:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

und

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

Der erste füllt die Panik, wenn er versucht, eine Zukunft zu schaffen, während der zweite bei der ersten Umfrage in Panik gerät. Wenn ja, sollten wir etwas damit anfangen? Es könnte wie umgangen werden

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

Aber es ist viel weniger bequem.

@Pzixel : Dies ist eine Entscheidung, die getroffen wurde. async fn sind völlig faul und werden erst nach Abfrage ausgeführt. Dadurch werden alle Eingangslebensdauern für die gesamte zukünftige Lebensdauer erfasst. Die Problemumgehung ist eine Umhüllungsfunktion, die impl Future als explizit zurückgibt und einen async Block (oder fn) für die verzögerte Berechnung verwendet. Dies ist _erforderlich_, um Future Kombinatoren ohne Zuordnung zwischen den einzelnen zu stapeln, da sie nach der ersten Abfrage nicht verschoben werden können.

Dies ist eine erledigte Entscheidung und warum implizite Wartesysteme für Rust nicht (gut) funktionieren.

C # -Entwickler hier, und ich werde mich für den Präfixstil einsetzen, also machen Sie sich bereit für die Abstimmung.

Die Postfix-Syntax mit einem Punkt ( read().await oder read().await() ) ist sehr irreführend und schlägt einen Feldzugriff oder einen Methodenaufruf vor, und await ist keines dieser Dinge. Es ist eine Sprachfunktion. Ich bin keineswegs ein erfahrener Rustacean, aber mir sind keine anderen .something Fälle bekannt, bei denen es sich tatsächlich um ein Schlüsselwort handelt, das vom Compiler neu geschrieben wird. Am nächsten kann ich mir das Suffix ? vorstellen.

Nachdem ich seit einigen Jahren in C # asynchronisiert / gewartet habe, hat mir die Standard-Syntax für Präfixe mit Leerzeichen ( await foo() ) in all dieser Zeit keine Probleme bereitet. In Fällen, in denen ein verschachteltes await erforderlich ist, ist es nicht lästig, Parens zu verwenden. Dies ist die Standardsyntax für alle expliziten Operatorprioritäten und daher leicht zu verstehen, z

`` `c #
var list = (warte auf GetListAsync ()). ToList ();


`await` is essentially a unary operator, so treating it as such makes sense.

In C# 8.0, currently in preview, we have async enumerables (iterators), which comes with an `IAsyncEnumerable<T>` interface and `await foreach (var x in QueryAsync())` syntax. The lack of async enumerables has been an issue since async was added in C# 5; for example, we have ORMs that return a `Task<IEnumerable>` which is only half asynchronous; we have to block on calls to `MoveNext()`. Having proper async enumerables is a big deal.

If a postfix `await` is used and similar async iterator support is added to Rust, that would look something like

```rust
for val in v_async_iter {
    println!("Got: {}", val);
} await // or .await or .await()

Das scheint komisch.

Ich denke, dass die Übereinstimmung mit anderen Sprachen ebenfalls gewichtet werden muss. Derzeit ist ein Schlüsselwort await vor dem asynchronen Ausdruck die Syntax für C #, VB.NET, JavaScript und Python sowie die vorgeschlagene Syntax für C ++. Das sind fünf der sieben besten Sprachen der Welt. C und Java haben noch kein Async / Warten, aber ich wäre überrascht, wenn sie einen anderen Weg gehen würden.

Wenn es nicht wirklich gute Gründe gibt, einen anderen Weg zu gehen oder es anders auszudrücken, wenn sowohl Präfix als auch Postfix in Bezug auf Vorteile und Kompromisse gleich sind, würde ich vorschlagen, dass für Inoffizielle viel zu sagen ist. " Standard "-Syntax.

@markrendle Ich bin auch C # dev und schreibe seit 2013 async/await . Und ich mag auch Präfixe. Aber Rust ist sehr unterschiedlich.

Sie wissen wahrscheinlich, dass Ihr (await Foo()).ToList() ein seltener Fall ist. In den meisten Fällen schreiben Sie einfach await Foo() , und wenn Sie einen anderen Typ benötigen, möchten Sie wahrscheinlich die Signatur Foo korrigieren.

Aber Rost ist eine andere Sache. Wir haben hier ? und müssen Fehler verbreiten. In C # werden async / await automatisch Ausnahmen umbrochen, über Wartepunkte geworfen und so weiter. Sie haben es nicht in Rost. Überlegen Sie jedes Mal, wenn Sie await in C # schreiben, dass Sie schreiben müssen

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

Es ist sehr langweilig, all diese Zahnspangen zu haben.

OTOH mit Postfix hast du

var response = client.GetAsync("www.google.com") await.HandleException();
var json =  response.ReadAsStreamAsync() await.HandleException();
var somethingElse = DoMoreAsyncStuff(json) await.HandleException();
...

oder in Rostform

let response = client.get_async("www.google.com") await?;
let json =  response.read_as_Stream_async() await?;
let somethingElse = do_more_async_stuff(json) await?;
...

Und imaine werden wir einen weiteren Transformator haben, zusätzlich zu ? und await , nennen wir es @ . Wenn wir eine zusätzliche Syntax für await? erfinden, müssen wir await@ , @? , await@? , ... haben, um sie konsistent zu machen.


Ich liebe auch das Präfix await , aber ich bin mir nicht ganz sicher, ob Rust damit einverstanden ist. Es mag in Ordnung sein, zusätzliche Regeln in die Sprache aufzunehmen, aber es ist eine Belastung, die wahrscheinlich keine bessere Lesbarkeit wert ist.

Lesen Sie alle Kommentare erneut, einschließlich des Threads zum Verfolgen von Problemen (fast 3 Stunden damit verbracht). Und meine Zusammenfassung lautet: Das Postfix @ scheint die beste Lösung zu sein, und leider ist es die am meisten unterschätzte.

Es gibt viele (voreingenommene) Meinungen dazu, und unten sind meine Kommentare dazu:


async-await wäre mit Postfix @ nicht vertraut

Tatsächlich wäre es nur halb unbekannt, da dieselbe async fn -Syntax bereitgestellt würde und dieselbe Funktionalität wie bei await weiterhin verfügbar wäre.

Rust hat viele Siegel und wir sollten kein weiteres Siegel einführen

Aber Siegel sind in Ordnung, bis sie lesbar sind und mit anderer Syntax übereinstimmen. Und die Einführung eines neuen Siegels bedeutet nicht unbedingt, dass es die Dinge irgendwie noch schlimmer machen wird. In der Konsistenzperspektive ist Postfix @ eine weitaus bessere Syntax als jedes Präfix / Postfix await .

Die Schnipsel wie ?!@# erinnern mich an Perl

In Wirklichkeit würden wir selten eine andere Kombination als @? die IMO ziemlich einfach und ergonomisch aussieht. Rückgabetypen wie impl Try<impl Future<T>> , impl Future<impl Try<impl Try<T>>> und sogar impl Future<T> würden höchstens in 10% der Verwendungen auftreten, wie wir anhand von Beispielen aus der Praxis sehen konnten. Darüber hinaus würde eine Siegelkonglomeration in 90% dieser 10% auf einen Codegeruch hinweisen, wenn Sie entweder Ihre API reparieren oder stattdessen eine temporäre Bindung einführen müssen, um dies zu verdeutlichen.

Postfix @ ist so schwer zu bemerken!

Und das ist tatsächlich ein großer Vorteil im Kontext von async-await . Diese Syntax bietet die Möglichkeit, asynchronen Code wie synchron auszudrücken, sodass aufdringlichere await nur den ursprünglichen Zweck beeinträchtigen. Die obigen Beispiele zeigen deutlich, dass das Auftreten von Fließgrenzen dicht genug sein kann, um Code aufzublähen, wenn sie zu explizit sind. Natürlich müssen wir sie in Sichtweite haben, aber @ Sigil wäre dafür vollkommen in Ordnung, ebenso wie await ohne den Code aufzublähen.

Das Warten auf die Zukunft ist eine kostspielige Operation, und das sollten wir ausdrücklich sagen

Es macht wirklich Sinn, aber ich denke nicht, dass wir es so oft und so laut sagen sollten. Ich denke, dass es eine Analogie zur Mutationssyntax geben könnte: mut wird explizit nur einmal benötigt und außerdem können wir die veränderliche Bindung implizit verwenden. Eine andere Analogie könnte mit unsicherer Syntax versehen werden: explizites unsafe wird nur einmal benötigt und weiter können wir rohe Zeiger dereferenzieren, unsichere Funktionen aufrufen usw. Und noch eine andere Analogie könnte mit dem Blasenoperator bereitgestellt werden: explizites impl Try return oder kommender try Block sind nur einmal erforderlich und außerdem können wir den Operator ? sicher verwenden. Auf die gleiche Weise kommentiert explizit async möglicherweise dauerhafte Vorgänge nur einmal, und außerdem können wir den Operator @ anwenden, der dies tatsächlich tun würde.

Es ist komisch, weil ich es anders lese als await und es nicht einfach tippen kann

Wenn Sie & als "ref" und ! als "not" lesen, dann denke ich nicht, dass das Lesen von @ derzeit ein starkes Argument ist, dies nicht zu verwenden Symbol. Es spielt auch keine Rolle, wie schnell Sie es eingeben, da das Lesen von Code immer eine wichtigere Priorität hat. In beiden Fällen ist die Übernahme von @ sehr einfach.

Es ist nicht eindeutig, da @ bereits für den Mustervergleich verwendet wird

Ich denke nicht, dass dies ein Problem ist, da ! bereits in Makros verwendet wird, & bereits beim Ausleihen verwendet wird und im Operator && | verwendet wird In Verschlüssen, in Mustern und im Operator || + / - auch in Präfixposition angewendet werden, und . könnte noch verwendet werden mehr verschiedene Kontexte. Es ist nicht überraschend und nichts Falsches, dieselben Symbole in verschiedenen Syntaxkonstrukten wiederzuverwenden.

Es wäre schwer zu greifen

Wenn Sie überprüfen möchten, ob einige Quellen asynchrone Muster enthalten, ist rg async eine viel einfachere und unkompliziertere Lösung. Wenn Sie wirklich alle Ertragspunkte auf diese Weise finden möchten, können Sie so etwas wie rg "@\s*[;.?|%^&*-+)}\]]" schreiben, was nicht einfach ist, aber auch nicht umwerfend. In Anbetracht dessen, wie oft es benötigt wird, ist diese Regex IMO absolut in Ordnung.

@ ist nicht anfängerfreundlich und es ist schwer zu googeln

Ich denke, dass es für Anfänger tatsächlich einfacher zu verstehen ist, da es konsistent mit dem Operator ? funktioniert. Im Gegensatz dazu würde Präfix / Postfix await nicht mit anderer Rust-Syntax übereinstimmen und zusätzlich Kopfschmerzen mit Assoziativität / Formatierung / Bedeutung verursachen. Ich denke auch nicht, dass await einen selbstdokumentierenden Wert hinzufügen würde, da ein einzelnes Schlüsselwort nicht das gesamte zugrunde liegende Konzept beschreiben kann und Anfänger es sowieso aus dem Buch lernen würden. Und Rust war nie eine Sprache, die Ihnen für jedes mögliche Syntaxkonstrukt Hinweise im Klartext gibt. Wenn jemand vergisst, was das Symbol @ bedeutet (ich bezweifle wirklich, dass es ein Problem wäre, weil es auch viele Assoziationen gibt, um sich richtig daran zu erinnern), sollte das Googeln auch erfolgreich sein, da derzeit rust @ symbol Gibt alle relevanten Informationen zu @ im Mustervergleich zurück.

Wir haben bereits das Schlüsselwort await reserviert, daher sollten wir es trotzdem verwenden

Warum? Es ist in Ordnung, ein nicht verwendetes reserviertes Schlüsselwort zu haben, wenn sich herausstellt, dass die endgültige Implementierung der Funktion ohne dieses Schlüsselwort besser ist. Es wurde auch kein Versprechen gegeben, dass genau das Schlüsselwort await eingeführt wird. Und diese Kennung ist im Code der realen Welt nicht sehr verbreitet, sodass wir nichts verlieren würden, wenn sie bis zur nächsten Ausgabe reserviert bleiben würde.

? war ein Fehler und @ wäre ein zweiter Fehler

Wahrscheinlich denken Sie lexikalischer und arbeiten deshalb lieber mit Wörtern als mit Symbolen. Und aus diesem Grund sehen Sie keinen Wert in der dünneren Syntax. Denken Sie jedoch daran, dass mehr Menschen visueller denken und es für sie schwieriger ist, aufdringliche Worte zu verwenden. Wahrscheinlich wäre ein guter Kompromiss hier die Bereitstellung von await!() Makro neben try!() neben ? , daher könnten Sie es anstelle von @ wenn Sie sich mit @ wirklich unwohl

^^^^^^^ Hast du meinen Kommentar auch noch einmal gelesen?

Ich hatte eine große Offenbarung, in der ich den Kommentar von @ I60R gelesen habe, der mich für das Postfix-Siegel entschieden hat, und ich habe es bis jetzt nicht gemocht, ein Siegel zu verwenden (stattdessen bevorzuge ich eine Form von Postfix-Warten).

Unsere Warteoperation ist nicht await . Zumindest nicht so, wie await in C # und JavaScript verwendet wird, den beiden größten Quellen für async / await Vertrautheit.

In diesen Sprachen besteht die Semantik des Startens eines Task (C #) / Promise (JS) darin, dass die Aufgabe sofort in die auszuführende Aufgabenwarteschlange gestellt wird. In C # ist dies ein Multithread-Kontext, in JS ein Single-Thread.

In beiden Sprachen bedeutet await foo semantisch "diese Aufgabe parken und auf den Abschluss der Aufgabe warten foo . In der Zwischenzeit können die von dieser Aufgabe verwendeten Rechenressourcen von anderen verwendet werden Aufgaben".

Aus diesem Grund erhalten Sie Parallelität mit separaten await s, nachdem Sie mehrere Task / Promise s erstellt haben (auch bei einzelnen Thread-Executoren): die Semantik von await aren "Führen Sie diese Aufgabe nicht aus", sondern "führen Sie eine Aufgabe aus", bis Sie fertig sind und wir fortfahren können.

Dies ist völlig unabhängig von dem faulen oder eifrigen Warten auf den ersten Start von Aufgaben, wenn diese erstellt werden, obwohl sie eng miteinander verbunden sind. Ich stelle den Rest der Aufgabe in die Warteschlange, nachdem die synchrone Arbeit erledigt wurde.

Wir haben bereits Verwirrung in dieser Richtung bei Benutzern von faulen Iteratoren und aktuellen Futures. Ich gehe davon aus, dass die Syntax von await uns hier keinen Gefallen tut.

Unsere await!(foo) also nicht await foo in der "traditionellen" C # - oder JS-Art. Welche Sprache ist also enger mit unserer Semantik verbunden? Ich bin jetzt fest davon überzeugt, dass es Kotlins suspend fun ist ( @matklad korrigiert mich, wenn ich falsche Angaben habe). Oder wie es in Rust-angrenzenden Diskussionen erwähnt wurde, "explizites asynchrones, implizites Warten".

Kurzer Rückblick: In Kotlin können Sie ein suspend fun innerhalb eines suspend -Kontexts aufrufen. Dadurch wird die aufgerufene Funktion sofort vollständig ausgeführt, und der gestapelte Kontext wird gemäß den Anforderungen der untergeordneten Funktion angehalten. Wenn Sie untergeordnete suspend fun parallel ausführen möchten, erstellen Sie das Äquivalent von asynchronen Abschlüssen und kombinieren diese mit einem suspend fun -Kombinator, um mehrere suspend Abschlüsse parallel auszuführen. (Es ist etwas komplizierter.) Wenn Sie ein untergeordnetes Element suspend fun im Hintergrund ausführen möchten, erstellen Sie einen Aufgabentyp, der diese Aufgabe auf den Executor legt und ein .await() suspend fun Methode, um Ihre Aufgabe mit der Hintergrundaufgabe zu verbinden.

Diese Architektur, obwohl mit anderen Verben als Rusts Future , kommt mir sehr bekannt vor.

Ich habe zuvor erklärt, warum implizites Warten bei Rust nicht funktioniert, und stehe zu dieser Position. Wir brauchen async fn() -> T und fn() -> Future<T> , um in der Verwendung gleichwertig zu sein, damit das Muster "Erste Arbeiten erledigen" mit faulen Zukünften möglich ist (damit Lebenszeiten in schwierigen Fällen funktionieren). Wir brauchen faule Futures, damit wir Futures ohne Indirektionsebene zwischen den einzelnen stapeln können, um sie festzunageln.

Aber ich bin jetzt davon überzeugt, dass die "explizite, aber leise" Syntax von foo@ (oder einem anderen Siegel) Sinn macht. So sehr ich es auch hasse, das Wort hier zu sagen, @ in diesem Fall eine monadische Operation in async ähnlich wie ? die monadische Operation in try .

Es gibt eine Meinung, dass das Streuen von ? in Ihren Code nur dazu dient, dass der Compiler aufhört, sich zu beschweren, wenn Sie Result . Und in gewissem Sinne tun Sie dies. Aber es geht darum, unser Ideal der lokalen Klarheit des Codes zu bewahren. Ich muss wissen, ob diese Operation durch die Monade entpackt oder buchstäblich lokal durchgeführt wird.

(Wenn ich falsche Monadendetails habe, wird mich sicher jemand korrigieren, aber ich glaube, mein Standpunkt bleibt bestehen.)

Wenn sich @ genauso verhält, ist dies meiner Meinung nach eine gute Sache. Es geht nicht um eine bestimmte Anzahl von Aufgaben Spinnen bis dann await Zieleinlauf ing, es geht um eine Zustandsmaschine aufzubauen , dass Bedürfnisse jemand anderes zu Ende zu pumpen, ob das , indem sie in ihren eigenen Staat Maschinenbau oder es in einen Ausführungskontext setzen.

TL; DR: Wenn Sie eines aus diesem Mini-Blog-Beitrag entnehmen, lassen Sie es so sein: Rusts await!() ist nicht await wie in anderen Sprachen. Die Ergebnisse sind ähnlich, aber die Semantik, wie Aufgaben behandelt werden, ist sehr unterschiedlich. Davon profitieren wir davon, dass wir nicht die gemeinsame Syntax verwenden, da uns das gemeinsame Verhalten fehlt.

(In einer früheren Diskussion wurde eine Sprache erwähnt, die sich von fauler zu eifriger Zukunft geändert hat. Ich glaube, das liegt daran, dass await foo darauf hindeutet, dass foo bereits stattfindet, und wir warten nur darauf, dass eine Ergebnis.)

BEARBEITEN: Wenn Sie dies synchroner diskutieren möchten, pingen Sie mich @ CAD auf der Rostentwicklung Discord (Link ist 24h) oder Interna oder Benutzer (@ CAD97). Ich würde gerne meine Position gegen eine direkte Prüfung stellen.

EDIT 2: Sorry GitHub @ cad für den versehentlichen Ping; Ich habe versucht, dem @ zu entkommen und wollte dich nicht dazu ziehen.

@ I60R

Die Schnipsel wie ?!@# erinnern mich an Perl

In Wirklichkeit würden wir selten eine andere Kombination als @? die IMO ziemlich einfach und unkompliziert aussieht

Sie können es nicht wissen, bis es erscheint. Wenn wir noch einen weiteren Codetransformator hätten - zum Beispiel yield sigil - wäre dies ein echtes Perl-Skript.

Postfix @ ist so schwer zu bemerken!

Und das ist tatsächlich ein großer Vorteil im Kontext von async-await .

Schwierigkeiten beim Erkennen von await Punkten können kein Vorteil sein.

Das Warten auf die Zukunft ist eine kostspielige Operation, und das sollten wir ausdrücklich sagen

Es macht wirklich Sinn, aber ich denke nicht, dass wir es so oft und so laut sagen sollten.

Lesen Sie bitte meine Links zu C # -Erfahrungen erneut. Es ist eigentlich so wichtig, es so laut und so oft zu sagen.


Andere Punkte sind durchaus gültig, aber die, auf die ich geantwortet habe, sind nicht behebbare Lücken.

@ CAD97 Als C # async in Rust unterscheidet. Nun, es ist nicht so unterschiedlich, wie Sie beschreiben. IMHO basieren Ihre Implikationen auf einer falschen Prämisse

Unsere await!(foo) also nicht await foo in der "traditionellen" C # - oder JS-Art.

In C # dev ist es ziemlich dasselbe. Natürlich sollten Sie daran denken, dass Futures erst nach Abfrage ausgeführt werden, aber in den meisten Fällen spielt dies keine Rolle. In den meisten Fällen bedeutet await nur "Ich möchte den unverpackten Wert von zukünftigen F in die Variable x ". Die Zukunft erst nach einer Umfrage laufen zu lassen, ist eine Erleichterung nach C # world, sodass die Leute dankbar glücklich sind. Abgesehen davon hat C # ähnliche Konzepte mit Iteratoren / Generaten, die auch faul sind, so dass die C # -Menge mit der ganzen Sache ziemlich vertraut ist.

Kurz gesagt, es ist einfacher, await fangen, wenn man ein bisschen anders arbeitet, als wenn ein Siegel genau wie await in ihrer% younameit% -Sprache funktioniert, aber nicht ganz. Die Leute werden durch den Unterschied nicht verwirrt sein, da es nicht so sehr um den Deal geht, als Sie denken. Situationen, in denen eifrige / faule Ausführungsfragen wirklich selten sind, sollten daher nicht das Hauptanliegen des Designs sein.

@Pzixel In C # dev ist es ziemlich dasselbe. Natürlich sollten Sie daran denken, dass Futures erst nach Abfrage ausgeführt werden, aber in den meisten Fällen spielt dies keine Rolle.

Situationen, in denen eifrige / faule Ausführungsfragen wirklich selten sind, sollten daher nicht das Hauptanliegen des Designs sein.

Leider stimmt das nicht: Das Muster "einige Versprechen hervorbringen und später await sie" ist sehr verbreitet und wünschenswert , funktioniert aber nicht mit Rust .

Das ist ein sehr großer Unterschied, der sogar einige Rust-Mitglieder überrascht hat!

async / await in Rust ist wirklich näher an einem monadischen System, es hat im Grunde nichts mit JavaScript / C # gemeinsam:

  • Die Implementierung ist völlig anders (Erstellen einer Zustandsmaschine und anschließendes Ausführen einer Task, die mit Monaden übereinstimmt).

  • Die API ist völlig anders (Pull vs Push).

  • Das Standardverhalten, faul zu sein, ist völlig anders (was mit Monaden übereinstimmt).

  • Es kann nur einen Verbraucher geben, nicht viele (was mit Monaden übereinstimmt).

  • Das Trennen von Fehlern und die Verwendung von ? zur Behandlung dieser Fehler ist völlig anders (was mit Monadentransformatoren übereinstimmt).

  • Das Speichermodell ist völlig anders, was einen großen Einfluss sowohl auf die Implementierung von Rust Futures als auch auf die tatsächliche Verwendung von Futures durch Benutzer hat.

Das einzige, was gemeinsam ist, ist, dass alle Systeme so konzipiert sind, dass asynchroner Code einfacher wird. Das ist es. Das hat wirklich überhaupt nicht viel gemeinsam.

Viele, viele Leute haben C # erwähnt. Wir wissen.

Wir wissen, was C # getan hat, wir wissen, was JavaScript getan hat. Wir wissen, warum diese Sprachen diese Entscheidungen getroffen haben.

Offizielle Teammitglieder von C # haben mit uns gesprochen und ausführlich erklärt, warum sie diese Entscheidungen mit async / await getroffen haben.

Niemand ignoriert diese Sprachen, diese Datenpunkte oder diese Anwendungsfälle. Sie werden berücksichtigt.

Aber Rust unterscheidet sich wirklich von C # oder JavaScript (oder einer anderen Sprache). Wir können also nicht einfach blind kopieren, was andere Sprachen tun.

Aber in einer dieser Sprachen bedeutet "auf foo warten" semantisch "diese Aufgabe parken und auf den Abschluss der Aufgabe foo warten"

In Rust ist es genauso. Die Semantik der asynchronen Funktionen ist dieselbe. Wenn Sie ein asynchrones fn aufrufen (das eine Zukunft erstellt), wird die Ausführung noch nicht gestartet. Das ist in der Tat anders als in JS und C # world.

Das Warten darauf wird die Zukunft zum Abschluss bringen, was das gleiche ist wie überall sonst.

Leider stimmt das nicht: Das Muster "einige Versprechen hervorbringen und später await sie" ist sehr verbreitet und wünschenswert , funktioniert aber nicht mit Rust .

Könnten Sie näher sein? Ich habe den Beitrag gelesen, aber nicht gefunden, was los ist

let foos = (1..10).map(|x| some_future(x)); // create futures, unlike C#/JS don't actually run anything
let results = await foos.join(); // awaits them

Natürlich werden sie erst in Zeile 2 ausgelöst, aber in den meisten Fällen ist dies ein wünschenswertes Verhalten, und ich bin immer noch davon überzeugt, dass dies ein großer Unterschied ist, der die Verwendung des Schlüsselworts await nicht mehr zulässt. Der Themenname selbst weist darauf hin, dass das Wort await hier Sinn macht.

Niemand ignoriert diese Sprachen, diese Datenpunkte oder diese Anwendungsfälle. Sie werden berücksichtigt.

Ich werde nicht nerven, die gleichen Argumente immer und immer wieder zu wiederholen. Ich glaube, ich habe geliefert, was ich sollte, also werde ich es nicht mehr tun.


PS

  • Die API ist völlig anders (Pull vs Push).

Es macht einen Unterschied, wenn Sie nur Ihre eigenen Futures implementieren. Aber in 99 (100?)% Fällen verwenden Sie nur Kombinatoren für Futures, die diesen Unterschied verbergen.

  • Das Speichermodell ist völlig anders, was einen großen Einfluss sowohl auf die Implementierung von Rust Futures als auch auf die tatsächliche Verwendung von Futures durch Benutzer hat.

Könnten Sie detaillierter sein? Ich habe bereits beim Schreiben eines einfachen Webservers auf Hyper mit asynchronem Code gespielt und keinen Unterschied bemerkt. Setzen Sie async hier, await dort, fertig .

Das Argument, dass Rost async / await eine andere Implementierung hat als andere Sprachen und wir dann etwas anderes tun müssen, ist ziemlich nicht überzeugend. C's

for (int i = 0; i < n; i++)

und Pythons

for i in range(n)

definitiv nicht den gleichen zugrunde liegenden Mechanismus teilen, aber warum wählt Python for ? Es sollte i in range(n) @ oder i in range(n) --->> oder was auch immer verwenden, um diesen wichtigen Unterschied zu zeigen!

Die Frage ist hier, ob der Unterschied für die tägliche Arbeit des Benutzers wichtig ist!

Ein typischer Rost Benutzer ist das tägliche Leben ist ungefähr:

  1. Interaktion mit einigen HTTP-APIs
  2. Schreiben Sie einen Netzwerkcode

und er kümmert sich wirklich darum

  1. Er kann seine Arbeit schnell und ergonomisch erledigen.
  2. Rust ist für seinen Job performant.
  3. Er hat eine niedrige Kontrolle, wenn er will

NICHT Rost hat eine Million Implementierungsdetails, die sich von C #, JS unterscheiden . Es ist die Aufgabe eines Sprachdesigners, irrelevante Unterschiede zu verbergen und nur die nützlichen den Benutzern zugänglich zu machen .

Außerdem beschwert sich derzeit niemand über die await!() aus Gründen wie "Ich weiß nicht, dass es sich um eine Zustandsmaschine handelt", "Ich weiß nicht, dass sie Pull-basiert ist".

Benutzer interessieren sich nicht für diese Unterschiede.

Das Warten darauf wird die Zukunft zum Abschluss bringen, was das gleiche ist wie überall sonst.

Nun, nicht ganz, oder? Wenn ich nur let result = await!(future) in ein async fn schreibe und diese Funktion aufrufe, passiert nichts, bis sie auf einen Executor gelegt und von diesem abgefragt wird.

@ ben0x539 ja, ich habe es noch einmal gelesen ... aber momentan habe ich nichts hinzuzufügen.

@ CAD97 froh zu sehen, dass mein Beitrag inspirierend war. Ich kann über Kotlin antworten: Standardmäßig funktioniert es genauso wie andere Sprachen, obwohl Sie ein rostartiges faules Verhalten erzielen können, wenn Sie es möchten (z. B. val lazy = async(start=LAZY) { deferred_operation() }; ). Und zu ähnlicher Semantik: IMO wird in der reaktiven Programmierung die nächstgelegene Semantik bereitgestellt, da reaktive Observablen standardmäßig kalt (faul) sind (z. B. val lazy = Observable.fromCallable { deferred_operation() }; ). Wenn Sie sie abonnieren, wird die tatsächliche Arbeit auf die gleiche Weise geplant wie bei await!() in Rust. Es gibt jedoch auch einen großen Unterschied, dass das Abonnieren standardmäßig nicht blockierend ist und asynchrone berechnete Ergebnisse fast immer in Abschlüssen getrennt vom aktuellen Kontrollfluss behandelt werden. Und es gibt einen großen Unterschied in der Funktionsweise der Stornierung. Ich denke also, dass das Verhalten von Rust async einzigartig ist, und ich unterstütze voll und ganz Ihr Argument, dass await verwirrend ist und eine andere Syntax hier nur von Vorteil ist!

@Pzixel Ich bin mir ziemlich sicher, dass kein neues Siegel erscheinen würde. Das Implementieren eines yield als Siegel macht keinen Sinn, da es sich um ein Kontrollflusskonstrukt wie if / match / loop / return . Offensichtlich sollten alle Kontrollflusskonstrukte Schlüsselwörter verwenden, da sie Geschäftslogik beschreiben und Geschäftslogik immer Vorrang hat. Im Gegensatz dazu sind @ sowie ? Konstrukte await -Schlüsselwort eine gute Sache ist (wenn man bedenkt, wie viel Text ich vermisse), weil sie alle an Autorität oder Erfahrung appellieren (was jedoch nicht bedeutet, dass ich jemandes Autorität oder Erfahrung entlassen - es kann hier einfach nicht in vollem Umfang angewendet

@tajimaha Das von Ihnen bereitgestellte Python-Beispiel enthält tatsächlich mehr "Implementierungsdetails". Wir können for als async , (int i = 0; i < n; i++) als await und i in range(n) als @ und hier ist es offensichtlich, dass Python führte ein neues Schlüsselwort ein - in (in Java ist es tatsächlich ein Siegel - : ) und führte zusätzlich eine neue Bereichssyntax ein. Es könnte etwas Bekannteres wie (i=0, n=0; i<n; i++) wiederverwenden, anstatt viele Implementierungsdetails einzuführen. Derzeit sind die Auswirkungen auf die Benutzererfahrung jedoch nur positiv, die Syntax ist einfacher und die Benutzer kümmern sich wirklich darum.

@Pzixel Ich bin mir ziemlich sicher, dass kein neues Siegel erscheinen würde. Das Implementieren eines yield als Siegel macht keinen Sinn, da es sich um ein Kontrollflusskonstrukt wie if / match / loop / return . Offensichtlich sollten alle Kontrollflusskonstrukte Schlüsselwörter verwenden, da sie Geschäftslogik beschreiben und Geschäftslogik immer Vorrang hat. Im Gegensatz dazu sind @ sowie ? Konstrukte

await ist kein "Ausnahmebehandlungskonstrukt".
Basierend auf einer ungültigen Prämisse sind Ihre Implikationen ebenfalls falsch.

Die Ausnahmebehandlung ist ebenfalls ein Kontrollfluss, aber niemand hält ? für eine schlechte Sache.

Ich habe keine starken Argumente dafür gefunden, dass ein großes Schlüsselwort von await eine gute Sache ist (wenn man bedenkt, wie viel Text ich vermisse), weil sie alle an Autorität oder Erfahrung appellieren (was jedoch nicht bedeutet, dass ich Jemanden Autorität oder Erfahrung entlassen - es kann einfach nicht in vollem Umfang angewendet werden.

Da Rust keine eigenen Erfahrungen hat, konnte es nur sehen, was andere Sprachen darüber denken, welche Erfahrungen andere Teams machen und so weiter. Sie sind so zuversichtlich in Siegel, aber ich bin mir nicht sicher, ob Sie überhaupt versucht haben, es tatsächlich zu verwenden.

Ich möchte in Rust kein Argument zur Syntax vorbringen, aber ich habe viele Postfix- und Präfix-Argumente gesehen, und vielleicht können wir das Beste aus beiden Welten haben. Und jemand anderes hat bereits versucht, dies in C ++ vorzuschlagen. Der C ++ - und Coroutine TS- Vorschlag wurde hier einige Male erwähnt, aber meiner Ansicht nach verdient ein alternativer Vorschlag namens Core Coroutines mehr Aufmerksamkeit.

Autoren von Core Coroutines schlagen vor, co_await durch ein neues Operator-ähnliches Token zu ersetzen.
Mit der sogenannten Unwrap-Operator-Syntax kann sowohl die Präfix- als auch die Postfix-Notation verwendet werden (ohne Mehrdeutigkeit):

future​<string>​ g​();
string​ s ​=​​ [<-]​ f​();

optional_struct​[->].​optional_sub_struct​[->].​field

Ich dachte, es könnte interessant sein, oder zumindest wird es die Diskussion vollständiger machen.

Aber Rust unterscheidet sich wirklich von C # oder JavaScript (oder einer anderen Sprache). Wir können also nicht einfach blind kopieren, was andere Sprachen tun.

Bitte verwenden Sie "Unterschied" nicht als Entschuldigung für Ihre persönliche Neigung zur unorthodoxen Syntax.

Wie viele Unterschiede gibt es? Nehmen Sie eine beliebige gängige Sprache, Java, C, Python, JavaScript, C #, PHP, Ruby, Go, Swift usw., statisch, dynamisch, kompiliert, interpretiert. Sie unterscheiden sich stark im Funktionsumfang, haben jedoch noch viele Gemeinsamkeiten hinsichtlich ihrer Syntax. Gibt es einen Moment, in dem Sie glauben, dass eine dieser Sprachen wie "brainf * ck" ist?

Ich denke, wir sollten uns darauf konzentrieren, verschiedene, aber nützliche Funktionen bereitzustellen, nicht seltsame Syntax.

Nach dem Lesen des Threads denke ich auch, dass die Debatte weitgehend beendet ist, wenn jemand die dringende Notwendigkeit einer Verkettung ungültig macht, jemand die Behauptung ungültig macht, dass Menschen durch zu viele temporäre Variablen verärgert sind.

IMO, Sie haben die Debatte über die Anforderungen der Postfix-Syntax verloren. Sie können nur auf "Unterschied" zurückgreifen. Es ist ein weiterer verzweifelter Versuch.

@tensorduruk Ich habe das Gefühl, dass deine Worte den anderen Benutzern etwas zu feindlich gegenüberstehen. Bitte versuchen Sie, sie in Schach zu halten.


Und ehrlich gesagt, wenn es viele Leute gibt, die gegen die Postfix-Syntax sind, sollten wir uns zunächst für eine Präfix-Syntax entscheiden, abwarten, wie Code mit der Präfix-Syntax geschrieben wird, und dann eine Analyse durchführen, um zu sehen, wie viel Code geschrieben wird könnte davon profitieren, dass Postfix auf Sie wartet. Auf diese Weise beschwichtigen wir alle, die mit einer innovativen Änderung wie dem Warten auf Postfix nicht zufrieden sind, und erhalten gleichzeitig ein gutes pragmatisches Argument dafür, ob Postfix warten soll oder nicht.

Das schlimmste Szenario dabei ist, wenn wir all das tun und eine Postfix-Warte-Syntax entwickeln, die irgendwie völlig besser ist als das Präfix-Warten. Das würde zu viel Code-Abwanderung führen, wenn die Leute sich die Mühe machen würden, auf die bessere Form des Wartens umzusteigen.

Und ich denke, dass all diese Syntaxdiskussionen wirklich auf Verkettung hinauslaufen. Wenn Verkettung keine Sache wäre, wäre das Warten auf Postfix vollständig aus dem Fenster und es wäre viel einfacher, sich auf das Warten auf Präfixe zu einigen. Verkettung ist in Rust jedoch sehr wichtig und eröffnet daher die Diskussion zu folgenden Themen:

if we should have only postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
else if we should have only prefix await:
    what's the best syntax for it that:
         isn't ambiguous in the sense of order of operation (useful vs obvious)
else if we should have both prefix and postfix await:
    what's the best syntax for it that:
         benefits chaining?
         is also ok in non-chaining scenarios
         is readable in both chainable and non-chainable contexts?
         isn't ambiguous in the sense of order of operation (useful vs obvious)
    should it be a single unified syntax that somehow works for both prefix and postfix?
    would there be clear situations where prefix syntax is favored over postfix?
    would there be a situation where postfix syntax isn't allowed, but prefix is, and vice-versa?

Oder etwas ähnliches. Wenn jemand ein besseres Entscheidungsmuster mit besseren Punkten als ich finden kann, tun Sie dies bitte! XD

Anstatt die Syntax zu diskutieren, sollten wir uns zunächst entscheiden, ob wir Postfix, Präfix oder sowohl Postfix als auch Präfix möchten und warum wir die Wahl haben möchten, die wir treffen. Sobald wir das geklärt haben, können wir auf kleinere Probleme bei der Wahl der Syntax eingehen.

@tensorduruk

IMO, Sie haben die Debatte über die Anforderungen der Postfix-Syntax verloren. Sie können nur auf "Unterschied" zurückgreifen. Es ist ein weiterer verzweifelter Versuch.

Im Ernst, warum nicht einfach Ihre besagten Sprachen anstelle der unaufgeforderten Feindseligkeit hier verwenden?
Nach Ihrer Logik sollte Rost Klassen haben, weil andere konventionelle Sprachen dies haben. Rust sollte keinen Kredit haben, weil andere Sprachen ihn nicht haben.

Nicht mit Verkettung umzugehen, wäre eine verpasste Gelegenheit. i, e Ich möchte keine Bindungen für eine temporäre Variable erstellen, wenn die Verkettung diese anonymisieren kann.

Ich denke jedoch, wir sollten uns einfach an das aktuelle Makro-Keyword halten, um wie in der Nacht zu warten. Haben Sie Postfix-Makros als nächtliche Funktion und lassen Sie die Leute damit spielen. Ja, sobald wir uns eingelebt haben, wird es Abwanderungen geben, aber das kann durch rustfix erledigt werden.

@tensorduruk

IMO, Sie haben die Debatte über die Anforderungen der Postfix-Syntax verloren. Sie können nur auf "Unterschied" zurückgreifen. Es ist ein weiterer verzweifelter Versuch.

Im Ernst, warum nicht einfach Ihre besagten Sprachen anstelle der unaufgeforderten Feindseligkeit hier verwenden?
Nach Ihrer Logik sollte Rost Klassen haben, weil andere konventionelle Sprachen dies haben. Rust sollte keinen Kredit haben, weil andere Sprachen ihn nicht haben.

Nicht mit Verkettung umzugehen, wäre eine verpasste Gelegenheit. i, e Ich möchte keine Bindungen für eine temporäre Variable erstellen, wenn die Verkettung diese anonymisieren kann.

Ich denke jedoch, wir sollten uns einfach an das aktuelle Makro-Keyword halten, um wie in der Nacht zu warten. Haben Sie Postfix-Makros als nächtliche Funktion und lassen Sie die Leute damit spielen. Ja, sobald wir uns eingelebt haben, wird es Abwanderungen geben, aber das kann durch rustfix erledigt werden.

Bitte aufmerksam lesen. Ich sagte, wir sollten nützliche Funktionen bereitstellen, nicht seltsame Syntax. Struktur? leihen? Eigenschaften!

Bitte lösen Sie auch Probleme, die wirklich existieren. Menschen zeigten anhand von Beispielen oder Statistiken, dass temporäre Bindungen möglicherweise kein Problem darstellen. versucht ihr, f await f.await jemals die andere Seite mit Beweisen zu überzeugen?

mich auch herabzustimmen ist nutzlos. Um Postfix zu akzeptieren, müssen Sie sich darüber streiten, wo Postfix nützlich ist (wahrscheinlich wiederholen Sie das Verkettungsargument nicht, das ist eine Sackgasse; wahrscheinlich ein häufiges Problem, das die Leute nervt, mit einigen Beweisen, kein Spielzeugproblem).

Können wir die Syntax wie C # oder JS erhalten? Die meisten Entwickler verwenden es auf der Welt. Ich mag es nicht, wenn Rost eine neue Syntax oder Inkonsistenz verwendet. Rost ist auch für neue Leute schwer zu lernen.

Das Folgende wäre ein Anhang zu meinem Beitrag über die Verwendung von Postfix @ anstelle von await


Wir sollten stattdessen die Erfahrung vieler anderer Sprachen nutzen

Und wir benutzen es tatsächlich. Das heißt aber nicht, dass wir die gleiche Erfahrung vollständig wiederholen müssen. Wenn Sie auf diese Weise gegen @ argumentieren, würden Sie höchstwahrscheinlich kein nützliches Ergebnis erzielen, da es nicht überzeugend ist, auf frühere Erfahrungen zurückzugreifen :

Genetische Darstellungen eines Problems mögen zutreffen, und sie können dazu beitragen, die Gründe zu beleuchten, warum das Problem seine gegenwärtige Form angenommen hat, aber sie sind nicht schlüssig bei der Bestimmung seiner Vorzüge.

Wir sollten await weil viele Leute es lieben

Diese Leute mögen await aus vielen anderen Gründen lieben, nicht nur, weil es await . Diese Gründe existieren möglicherweise auch in Rust nicht. Und auf diese Weise gegen @ argumentieren, würde höchstwahrscheinlich keine neuen Punkte in die Diskussion bringen, da es nicht überzeugend ist, die Öffentlichkeit anzusprechen :

Das Argumentum ad populum kann ein gültiges Argument in der induktiven Logik sein. Beispielsweise kann eine Umfrage unter einer beträchtlichen Bevölkerung ergeben, dass 100% eine bestimmte Produktmarke einer anderen vorziehen. Es kann dann ein schlüssiges (starkes) Argument vorgebracht werden, dass die nächste zu berücksichtigende Person höchstwahrscheinlich auch diese Marke bevorzugen wird (jedoch nicht immer zu 100%, da es Ausnahmen geben könnte), und die Umfrage ist ein gültiger Beweis für diese Behauptung. Als Argument für deduktives Denken als Beweis ist es jedoch ungeeignet, beispielsweise zu sagen, dass die Umfrage beweist, dass die bevorzugte Marke in ihrer Zusammensetzung der Konkurrenz überlegen ist oder dass jeder diese Marke der anderen vorzieht.

@ Pixel

Aber Rost ist eine andere Sache. Wir haben hier ? und müssen Fehler verbreiten. In C # werden async / await automatisch Ausnahmen umbrochen, über Wartepunkte geworfen und so weiter. Sie haben es nicht in Rost. Überlegen Sie jedes Mal, wenn Sie await in C # schreiben, dass Sie schreiben müssen

var response = (await client.GetAsync("www.google.com")).HandleException();
var json =  (await response.ReadAsStreamAsync()).HandleException();
var somethingElse = (await DoMoreAsyncStuff(json)).HandleException();
...

Es ist sehr langweilig, all diese Zahnspangen zu haben.

In C # schreiben Sie Folgendes:

try
{
  var response = await client.GetAsync("www.google.com");
  var json =  await response.ReadAsStreamAsync();
  var somethingElse = await DoMoreAsyncStuff(json);
}
catch (Exception ex)
{
  // handle exception
}

Gibt es in Bezug auf das Propagate-Error-Chaining-Argument, z. B. foo().await? , einen Grund, warum der ? nicht dem Präfix await im Präfix hinzugefügt werden konnte?

let response = await? getProfile();

Eine andere Sache, die mir gerade eingefallen ist: Was ist, wenn Sie match auf einem Future<Result<...>> ? Welche davon ist leichter zu lesen?

// Prefix
let userId = match await response {
  Ok(u) => u.id,
  _ => -1
};
// Postfix
let userId = match response {
  Ok(u) => u.id,
  _ => -1
} await;

Wäre ein async match Ausdruck eine Sache? Möchten Sie, dass der Text eines Übereinstimmungsausdrucks asynchron ist? Wenn ja, würde es einen Unterschied zwischen match await response und await match response . Da match und await beide effektiv unäre Operatoren sind und match bereits ein Präfix ist, wäre es einfacher zu unterscheiden, ob await auch ein Präfix wäre. Mit einem Präfix und einem Postfix wird es schwierig anzugeben, ob Sie auf die Übereinstimmung oder die Antwort warten.

let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await; // Are we awaiting match or response here?

Wenn Sie beide Dinge abwarten müssen, würden Sie sich so etwas wie ansehen

// Prefix - yes, double await is weird and ugly but...
let userId = await match await response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;
// Postfix - ... this is weirder and uglier
let userId = match response {
  Ok(u) => somethingAsync(u),
  _ => -1
} await await;

Obwohl ich denke, das könnte sein

// Postfix - ... this is weirder and uglier
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => -1
} await;

(Das Design der Programmiersprache ist schwierig.)

Unabhängig davon werde ich wiederholen, dass Rust Vorrang hat, wenn unäre Operatoren match vorangestellt werden und await ein unärer Operator ist.

C# // Postfix - ... this is weirder and uglier let userId = match response await { ... } await;

Schönheit liegt im Auge des Betrachters.

Unabhängig davon werde ich wiederholen, dass Rust Vorrang hat, wenn unäre Operatoren match vorangestellt werden und await ein unärer Operator ist.

? hingegen ist unär, aber postfix.

Unabhängig davon habe ich das Gefühl, dass die Diskussion jetzt den Abfluss umkreist. Ohne neue Diskussionspunkte aufzurufen, ist es sinnlos, dieselben Positionen immer wieder zu wiederholen.

FWIW, ich freue mich über die Unterstützung von await unabhängig von der Syntax - obwohl ich meine eigenen Vorlieben habe, halte ich keinen der realistischen Vorschläge für zu schrecklich, um ihn zu verwenden.

@markrendle Ich bin mir nicht sicher, worauf du antwortest

In C # schreiben Sie Folgendes:

Ich weiß, wie ich in C # schreibe. Ich sagte: "Stell dir vor, wie könnte es aussehen, wir hatten keine Ausnahmen." Weil Rust es nicht tut.

Gibt es in Bezug auf das Propagate-Error-Chaining-Argument, z. B. foo().await? , einen Grund, warum der ? nicht dem Präfix await im Präfix hinzugefügt werden konnte?

Es wurde bereits zweimal oder dreimal besprochen, bitte lesen Sie das Thema. Kurz gesagt: Es ist ein künstliches Konstrukt, das nicht gut funktioniert, wenn wir etwas zusätzlich zu ? . await? als Suffix funktioniert nur, wenn await? als Präfix zusätzliche Unterstützung im Compiler erfordert. Und es erfordert immer noch Klammern zum Verketten (was ich hier persönlich nicht mag, aber die Leute erwähnen es immer als wichtig), wenn Postfix nicht wartet.

Eine andere Sache, die mir gerade eingefallen ist: Was ist, wenn Sie match auf einem Future<Result<...>> ? Welche davon ist leichter zu lesen?

// Real postfix
let userId = match response await {
  Ok(u) => u.id,
  _ => -1
};
// Real Postfix 2 - looks fine, except it's better to be
let userId = match response await {
  Ok(u) => somethingAsync(u),
  _ => ok(-1)
} await;
// Real Postfix 2
let userId = match response await {
  Ok(u) => somethingAsync(u) await,
  _ => -1
};

Als ein anderer Benutzer von C # werde ich sagen, dass seine Präfixsyntaxen: new , await und Casts im C-Stil meine Intuition am meisten gestolpert haben. Ich unterstütze die Postfix-Operator-Option nachdrücklich.

Jede Syntax ist jedoch besser als die explizite Verkettung von Futures, selbst ein Pseudomakro. Ich werde jede Entschließung begrüßen.

@orthoxerox Sie

@ Pixel

@markrendle Ich bin mir nicht sicher, worauf du antwortest

In C # schreiben Sie Folgendes:

Ich weiß, wie ich in C # schreibe. Ich sagte: "Stell dir vor, wie könnte es aussehen, wir hatten keine Ausnahmen." Weil Rust es nicht tut.

Ich vermute, dass dies wahrscheinlich eine Sache mit Sprachbarrieren ist, weil Sie das überhaupt nicht gesagt haben, aber ich werde akzeptieren, dass es möglicherweise das war, was Sie gemeint haben.

Wie @rolandsteiner sagte, ist es wichtig, dass wir eine Form von Async /

@yasammez Komm zu C #. In v8.0 können wir einfach new () ohne den Typnamen verwenden :)

Ich werde nur ein paar Ideen für den Postfix-Operator rauswerfen.

foo()~; // the pause operator
foo()^^; // the road bumps operator
foo()>>>; // the fast forward operator

Ich sage nicht, ob der Postfix-Operator der richtige Weg ist oder nicht, aber ich persönlich finde @ eine der lautesten und seltsamsten, die aus allen möglichen Optionen hervorgehen. ~ aus dem @ phaux- Kommentar wirkt viel eleganter und weniger "beschäftigt". Wenn ich nichts vermisse, verwenden wir es noch nicht für irgendetwas in Rust.

Mir wurde ~ @ ~ vor @phaux vorgeschlagen, obwohl ich kein Patent beanspruchen möchte; P.

Ich habe dies vorgeschlagen, weil es wie ein Echo ist, das spricht:

Hi~~~~~
Where r u~~~~~

Hay~~~~~
I am in another mountain top~~~~~

~ wird manchmal nach einem Satz verwendet, um ein Nachlassen anzuzeigen, das zum Warten geeignet ist!

Ich kann nicht sagen, ob dieser Thread die höchste Lächerlichkeit erreicht hat oder ob wir hier auf etwas stehen.

Ich denke, ~ auf einigen Tastaturen nicht einfach zu beantworten, insbesondere auf einigen kleinen und empfindlichen mechanischen Tastaturen.

Könnte sein:

let await userId = match response {
  Ok(u) => u.id,
  _ => -1
};
let await userId = match response {
  await Ok(u) => somethingAsync(u),
  _ => ok(-1)
};

Wir könnten einen Trigraph wie ... für Benutzer in Tastaturlayouts einführen, bei denen ~ unpraktisch ist.

Anfangs war ich stark auf der Seite einer durch Trennzeichen erforderlichen Präfixsyntax wie await(future) oder await{future} da diese so eindeutig und einfach visuell zu analysieren ist. Ich verstehe jedoch die Vorschläge anderer, dass eine Rust Future nicht wie die meisten anderen Futures aus anderen Sprachen ist, da sie nicht sofort eine Aufgabe an einen Executor überträgt, sondern eher eine Kontrollflussstruktur ist, die den Kontext in das verwandelt, was ist im Wesentlichen homomorph zu einer Monaden-Anrufkette.

Ich denke, es ist etwas bedauerlich, dass es jetzt eine Verwirrung gibt, wenn man versucht, es in dieser Hinsicht mit anderen Sprachen zu vergleichen. Das nächste Analogon ist wirklich die Monadennotation do in Haskell oder das Verständnis von for in Scala (die einzigen, mit denen ich auf Anhieb vertraut bin). Plötzlich schätze ich die Überlegung, eine eindeutige Syntax vorzuschlagen, aber ich befürchte, dass die Existenz des Operators ? die Verwendung anderer Siegel sowohl ermutigt als auch entmutigt hat. Alle anderen auf Siegel basierenden Operatoren neben ? lassen es laut und verwirrend aussehen, z. B. future@? . Der Präzedenzfall, der durch einen Postfix-Siegeloperator festgelegt wurde, bedeutet jedoch, dass ein anderer nicht so lächerlich ist.

Ich bin daher vom Verdienst des Postfix Sigil-Operators überzeugt. Der Nachteil dabei ist, dass das Siegel, das ich am meisten bevorzugt hätte, vom Nie-Typ genommen wird. Ich hätte ! vorgezogen, da ich denke, dass future!? mich zum Lachen bringen würde, wenn ich es schrieb, und es macht für mich den visuellsten Sinn, es zu sehen. Ich nehme an, $ wäre der nächste, da es visuell erkennbar ist future$? . Das Sehen von ~ erinnert mich immer noch an die frühen Tage von Rust, als ~ der Präfixoperator für die Heapzuweisung war. Es ist jedoch alles sehr persönlich, deshalb beneide ich die endgültigen Entscheidungsträger nicht. Wäre ich sie, würde ich wahrscheinlich nur mit der harmlosen Wahl des Präfixoperators mit den erforderlichen Trennzeichen gehen.

Ich verstehe jedoch die Vorschläge anderer, dass eine Rust Future nicht wie die meisten anderen Futures aus anderen Sprachen ist, da sie nicht sofort eine Aufgabe an einen Executor überträgt, sondern eher eine Kontrollflussstruktur ist, die den Kontext in das verwandelt, was ist im Wesentlichen homomorph zu einer Monaden-Anrufkette.

Ich bin eher anderer Meinung. Das von Ihnen erwähnte Verhalten ist keine Eigenschaft von await , sondern der umgebenden Funktion oder des Bereichs von async . Es ist nicht await , das die Ausführung des vorhergehenden Codes verzögert, sondern der Bereich, der diesen Code enthält.

Wahrscheinlich besteht das Problem mit dem seltsam aussehenden @ -Symbol darin, dass die Verwendung in diesem Kontext noch nie erwartet wurde, weshalb es in den meisten Schriftarten in einer für uns so unangenehmen Form bereitgestellt wird.

Wenn Sie dann eine bessere Glyphe und einige Ligaturen für gängige Programmierschriften (oder zumindest für Mozillas Fira Code ) bereitstellen, kann sich die Situation etwas verbessern.

In allen anderen Fällen sieht es für mich nicht so seltsam aus, dass @ so seltsam ist, dass beim Schreiben oder Verwalten von Code echte Probleme auftreten.


Der folgende Code verwendet beispielsweise ein anderes Symbol als das Symbol @ - :


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

Erweitern Sie zum Vergleich, wie es mit regulären ANSI @ aussieht


// A
if db.is_trusted_identity(recipient.clone(), message.key.clone())@? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key)@? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send()@?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()@?
    .error_for_status()?
    .json()@?;

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

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true)@?
        .res.json::<UserResponse>()@?
        .user
        .into();

    Ok(user)
}

@norcalli

Anfangs war ich stark auf der Seite einer durch Trennzeichen erforderlichen Präfixsyntax wie await(future) oder await{future} da diese so eindeutig und einfach visuell zu analysieren ist.

Dann möchten Sie wahrscheinlich eindeutige if { cond } , while { cond } und match { expr } ...

Ich verstehe jedoch die Vorschläge anderer, dass eine Rust Future nicht wie die meisten anderen Futures aus anderen Sprachen ist

Es ist nicht wahr. Es ist eigentlich wie die meisten anderen Futures aus anderen Sprachen. Der Unterschied zwischen "Lauf bis zum ersten Warten beim Abrufen" und "Lauf bis zum ersten Warten beim Abrufen" ist nicht so groß. Ich weiß, weil ich mit beiden gearbeitet habe. Wenn Sie tatsächlich denken "wenn dieser Unterschied zum Tragen kommt", werden Sie nur Eckfall finden. Beispielsweise wird beim Erstellen der Zukunft bei der ersten Umfrage ein Fehler angezeigt, anstatt beim Erstellen.

Sie können intern unterschiedlich sein, sind aber aus Anwendersicht ziemlich gleich, so dass es für mich keinen Sinn macht, hier eine Unterscheidung zu treffen.

@ Pixel

Der Unterschied zwischen "Lauf bis zum ersten Warten beim Abrufen" und "Lauf bis zum ersten Warten beim Abrufen" ist nicht so groß.

Das ist jedoch nicht der Unterschied, über den wir sprechen. Wir sprechen nicht von faul gegen eifrig, zuerst zu warten.

Wir sprechen hier davon, dass await die erwarteten Promise (JS) / Task (C #) auf dem Executor in anderen Sprachen verbindet, die bereits auf dem Executor platziert wurden Bei der Konstruktion (lief also bereits im "Hintergrund"), aber in Rust sind Future s träge Zustandsmaschinen, bis await! angetrieben werden.

Promise / Task ist ein Handle für eine laufende asynchrone Operation. Future ist eine verzögerte asynchrone Berechnung. Personen, einschließlich namhafter Namen in Rust, haben diesen Fehler bereits gemacht, und Beispiele wurden bereits in der Mitte dieser über 500 Kommentare verlinkt.

Persönlich denke ich, dass diese Nichtübereinstimmung der Semantik groß genug ist, um der Vertrautheit von await . Unsere Future erreichen das gleiche Ziel wie Promise / Task , jedoch mit einem anderen Mechanismus.


Anekdotisch war für mich, als ich zum ersten Mal async / await in JavaScript lernte, async "nur" etwas, das ich geschrieben habe, um die await Supermacht zu bekommen. Und die Art und Weise, wie mir beigebracht wurde, Parallelität zu erlangen, war a = fa(); b = fb(); /* later */ await [a, b]; (oder was auch immer es ist, es ist ein Alter her, seit ich JS schreiben musste). Mein Standpunkt ist, dass die Ansicht anderer Leute über async mit mir übereinstimmt, da Rusts Semantik nicht mit async (geben Sie await Supermacht), sondern mit Future nicht übereinstimmt. await! .


An diesem Punkt glaube ich, dass die Diskussion über die Unterschiede in Rusts async / Future / await Semantik ihren Lauf genommen hat und keine neuen Informationen präsentiert werden. Wenn Sie keine neue Position und / oder Einsicht haben, ist es wahrscheinlich am besten für den Thread, wenn wir diese Diskussion hier lassen. (Ich würde es gerne zu Internals und / oder Discord bringen.)

@ CAD97 ja, ich sehe deine Position, aber ich denke, die Unterscheidung ist nicht so groß.

Du hast mich, ich habe dich. Lassen Sie also die Diskussion fließen.

@ CAD97

Personen, einschließlich namhafter Namen in Rust, haben diesen Fehler bereits gemacht, und Beispiele wurden bereits in der Mitte dieser über 500 Kommentare verlinkt.

Wenn selbst Leute, die mit Rust vertraut sind, diesen Fehler machen, ist es wirklich ein Fehler?

Wir hatten also eine Reihe von Diskussionen über Async-Warten im Rust All Hands. Im Verlauf dieser Diskussionen wurden einige Dinge klar:

Erstens gibt es (noch) im lang-Team keinen Konsens über die Syntax zum Warten . Es gibt eindeutig viele Möglichkeiten und starke Argumente für alle. Wir haben lange nach Alternativen gesucht und eine Menge interessanter Hin- und Herbewegungen produziert. Ich denke, ein sofortiger nächster Schritt für diese Diskussion besteht darin, diese Notizen (zusammen mit anderen Kommentaren aus diesem Thread) in eine Art zusammenfassenden Kommentar umzuwandeln, der den Fall für jede Variante darlegt, und dann von dort aus fortzufahren. Ich arbeite mit @withoutboats und @cramertj daran.

Wenn wir uns von der Syntaxfrage zurückziehen, planen wir auch eine allgemeine Überprüfung des Status der Implementierung. Es gibt eine Reihe von aktuellen Einschränkungen (z. B. erfordert die Implementierung TLS unter der Haube, um Informationen über den Waker einzufädeln). Dies können Blocker für die Stabilisierung sein oder auch nicht, aber unabhängig davon handelt es sich um Probleme, die letztendlich angegangen werden müssen und die einige konzertierte Anstrengungen erfordern werden (teilweise vom Compilerteam). Ein weiterer nächster Schritt besteht darin, diese Triage durchzuführen und einen Bericht zu erstellen. Ich erwarte, dass wir diese Triage nächste Woche durchführen, und wir werden dann ein Update haben.

In der Zwischenzeit werde ich diesen Thread sperren, bis wir die Möglichkeit hatten, die oben genannten Berichte zu erstellen . Ich bin der Meinung, dass der Thread bereits seinen Zweck erfüllt hat, den möglichen Designraum eingehend zu untersuchen, und weitere Kommentare werden in dieser Phase nicht besonders hilfreich sein. Sobald wir die oben genannten Berichte zur Hand haben, werden wir auch die nächsten Schritte zur endgültigen Entscheidung darlegen.

(Um den letzten Absatz etwas zu erweitern, bin ich ziemlich daran interessiert, alternative Wege zu erkunden, um den Designraum über lange Diskussionsthreads hinaus zu erkunden. Dies ist ein viel größeres Thema, als ich in diesem Kommentar ansprechen kann, daher werde ich nicht auf Details eingehen , aber es reicht vorerst zu sagen, dass ich sehr daran interessiert bin, bessere Wege zu finden, um diese - und zukünftige! - Syntaxdebatten zu lösen.)

Async-Warten auf Statusbericht:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/

In Bezug auf meinen vorherigen Kommentar enthält dies die Ergebnisse der Triage und einige Gedanken zur Syntax (aber noch keine vollständige Syntaxbeschreibung).

Markieren Sie dieses Problem zumindest vorerst als Blockierung für die asynchrone Stabilisierung.

Vor einiger Zeit versprach Niko, dass wir eine Zusammenfassung der Diskussion im Sprachteam und in der Community über die endgültige Syntax für den erwarteten Operator schreiben würden. Wir entschuldigen uns für das lange Warten. Eine Beschreibung des Status der Diskussion finden Sie weiter unten. Lassen Sie mich vorher jedoch auch ein Update darüber geben, wo die Diskussion jetzt steht und wohin wir von hier aus gehen werden.

Kurze Zusammenfassung, wo async-await gerade steht

Zunächst hoffen wir, das asynchrone Warten in der Version 1.37 zu stabilisieren , die am 4. Juli 2019 verzweigt. Da wir das Makro await! nicht stabilisieren möchten, müssen wir die Syntaxfrage vorher lösen. Beachten Sie, dass diese Stabilisierung nicht das Ende der Straße darstellt, sondern eher den Anfang. Es bleibt noch viel zu tun (z. B. asynchrones Fn in Merkmalen) und auch implizite Arbeit (fortgesetzte Optimierung, Fehlerbehebung und dergleichen). Dennoch wird die Stabilisierung von Async / Warten ein wichtiger Meilenstein sein!

In Bezug auf die Syntax lautet der Plan für die Auflösung wie folgt:

  • Zu Beginn veröffentlichen wir eine Zusammenfassung der bisherigen Syntaxdebatte - bitte werfen Sie einen Blick darauf.
  • Wir möchten vorwärtskompatibel mit offensichtlichen zukünftigen Erweiterungen der Syntax sein: Verarbeitung von Streams insbesondere mit for for await -Schleifen (wie der erster Post hier und weitere in Zukunft).
  • Auf dem bevorstehenden Lang-Team-Meeting am 2. Mai planen wir, die Interaktion mit for-Schleifen zu diskutieren und einen Plan zu erstellen, um rechtzeitig eine endgültige Entscheidung über die Syntax zu treffen, um Async / Warten in 1.37 zu stabilisieren. Wir werden nach dem Meeting ein Update für diesen internen Thread veröffentlichen .

Die Zuschreibung

Die Beschreibung ist ein Dropbox-Papierdokument, das hier verfügbar ist . Wie Sie sehen werden, ist es ziemlich lang und enthält viele Argumente, die hin und her gehen. Wir würden uns über Feedback freuen. Anstatt diese Ausgabe erneut zu öffnen (die bereits mehr als 500 Kommentare enthält), habe ich zu diesem Zweck einen internen Thread erstellt .

Wie ich bereits sagte, planen wir, in naher Zukunft eine endgültige Entscheidung zu treffen. Wir sind auch der Meinung, dass die Diskussion weitgehend einen stabilen Zustand erreicht hat: Erwarten Sie, dass die nächsten Wochen die "letzte Kommentierungsphase" für diese Syntaxdiskussion sein werden. Nach dem Treffen werden wir hoffentlich einen detaillierteren Zeitplan haben, um zu teilen, wie diese Entscheidung getroffen wird.

Die Async / Await-Syntax ist wahrscheinlich das am heißesten erwartete Feature, das Rust seit 1.0 erhalten hat, und insbesondere die Syntax für das Warten war eine der Entscheidungen, zu denen wir die meisten Rückmeldungen erhalten haben. Vielen Dank an alle, die in den letzten Monaten an diesen Diskussionen teilgenommen haben! Dies ist eine Wahl, bei der viele Menschen stark unterschiedliche Gefühle haben. Wir möchten allen versichern, dass Ihr Feedback gehört wird und die endgültige Entscheidung nach gründlichen und sorgfältigen Überlegungen getroffen wird.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen