Rust: Problema de seguimiento para async / await (RFC 2394)

Creado en 8 may. 2018  ·  308Comentarios  ·  Fuente: rust-lang/rust

Este es el problema de seguimiento para RFC 2394 (rust-lang / rfcs # 2394), que agrega sintaxis async y await al idioma.

Dirigiré el trabajo de implementación de este RFC, pero agradecería la tutoría ya que tengo relativamente poca experiencia trabajando en rustc.

HACER:

Preguntas sin resolver:

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

Comentario más útil

Acerca de la sintaxis: Realmente me gustaría tener await como palabra clave simple. Por ejemplo, veamos una inquietud del blog:

No estamos exactamente seguros de qué sintaxis queremos para la palabra clave await. Si algo es un futuro de un Resultado, como lo será cualquier futuro IO, querrá poder esperarlo y luego aplicarle el operador ? . Pero el orden de precedencia para habilitar esto puede parecer sorprendente: await io_future? sería await primero y ? segundo, a pesar de que ? está léxicamente más ligado de lo esperado.

Estoy de acuerdo aquí, pero los frenillos son malos. Creo que es más fácil recordar que ? tiene una precedencia menor que await y termina con eso:

let foo = await future?

Es más fácil de leer, es más fácil de refactorizar. Creo que es el mejor enfoque.

let foo = await!(future)?

Permite comprender mejor el orden en el que se ejecutan las operaciones, pero en mi opinión es menos legible.

Creo que una vez que obtienes que await foo? ejecuta await primero, no tienes problemas con él. Probablemente esté más ligado léxicamente, pero await está en el lado izquierdo y ? está en el derecho. Por lo tanto, sigue siendo lo suficientemente lógico await primero y manejar Result después.


Si existe algún desacuerdo, expréselo para que podamos discutirlo. No entiendo lo que significa el voto negativo silencioso. Todos le deseamos lo mejor a Rust.

Todos 308 comentarios

La discusión aquí parece haberse calmado, así que vinculándola aquí como parte de la pregunta de sintaxis await : https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

La implementación está bloqueada en # 50307.

Acerca de la sintaxis: Realmente me gustaría tener await como palabra clave simple. Por ejemplo, veamos una inquietud del blog:

No estamos exactamente seguros de qué sintaxis queremos para la palabra clave await. Si algo es un futuro de un Resultado, como lo será cualquier futuro IO, querrá poder esperarlo y luego aplicarle el operador ? . Pero el orden de precedencia para habilitar esto puede parecer sorprendente: await io_future? sería await primero y ? segundo, a pesar de que ? está léxicamente más ligado de lo esperado.

Estoy de acuerdo aquí, pero los frenillos son malos. Creo que es más fácil recordar que ? tiene una precedencia menor que await y termina con eso:

let foo = await future?

Es más fácil de leer, es más fácil de refactorizar. Creo que es el mejor enfoque.

let foo = await!(future)?

Permite comprender mejor el orden en el que se ejecutan las operaciones, pero en mi opinión es menos legible.

Creo que una vez que obtienes que await foo? ejecuta await primero, no tienes problemas con él. Probablemente esté más ligado léxicamente, pero await está en el lado izquierdo y ? está en el derecho. Por lo tanto, sigue siendo lo suficientemente lógico await primero y manejar Result después.


Si existe algún desacuerdo, expréselo para que podamos discutirlo. No entiendo lo que significa el voto negativo silencioso. Todos le deseamos lo mejor a Rust.

Tengo opiniones encontradas sobre await como palabra clave, @Pzixel. Si bien ciertamente tiene un atractivo estético, y quizás sea más consistente, dado que async es una palabra clave, la "hinchazón de palabras clave" en cualquier idioma es una preocupación real. Dicho esto, ¿tener async sin await tiene algún sentido, en cuanto a características? Si es así, quizás podamos dejarlo como está. De lo contrario, me inclinaría por hacer await una palabra clave.

Creo que es más fácil recordar que ? tiene una precedencia menor que await y termina con él.

Podría ser posible aprender eso e internalizarlo, pero hay una fuerte intuición de que las cosas que se tocan están más unidas que las cosas que están separadas por espacios en blanco, por lo que creo que siempre se leería mal a primera vista en la práctica.

Tampoco ayuda en todos los casos, por ejemplo, una función que devuelve Result<impl Future, _> :

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

La preocupación aquí no es simplemente "¿puedes entender la precedencia de una única espera + ? ", Sino también "cómo se ve encadenar varias esperas". Entonces, incluso si solo seleccionáramos una precedencia, todavía tendríamos el problema de await (await (await first()?).second()?).third()? .

Un resumen de las opciones para la sintaxis await , algunas del RFC y el resto del hilo RFC:

  • Requiere delimitadores de algún tipo: await { future }? o await(future)? (esto es ruidoso).
  • Simplemente elija una precedencia, de modo que await future? o (await future)? haga lo que se espera (ambos se sienten sorprendentes).
  • Combine los dos operadores en algo como await? future (esto es inusual).
  • Haga await postfix de alguna manera, como en future await? o future.await? (esto no tiene precedentes).
  • Use un nuevo sello como lo hizo ? , como en future@? (esto es "ruido de línea").
  • No use sintaxis en absoluto, haciendo que await esté implícita (esto hace que los puntos de suspensión sean más difíciles de ver). Para que esto funcione, el acto de construir un futuro también debe hacerse explícito. Este es el tema del hilo interno que vinculé anteriormente .

Dicho esto, ¿tener async sin await tiene algún sentido, en cuanto a características?

@alexreg Lo hace. Kotlin funciona de esta manera, por ejemplo. Esta es la opción "espera implícita".

@rpjohnst Interesante. Bueno, generalmente estoy a favor de dejar async y await como características explícitas del lenguaje, ya que creo que eso está más en el espíritu de Rust, pero no soy un experto en programación asincrónica. ..

@alexreg async / @rpjohnst clasificó muy bien todas las posibilidades. Prefiero la segunda opción, estoy de acuerdo con otras consideraciones (ruidosas / inusuales / ...). He estado trabajando con código asincrónico / en espera durante los últimos 5 años o algo así, es realmente importante tener esas palabras clave marcadas.

@rpjohnst

Entonces, incluso si solo seleccionáramos una precedencia, todavía tendríamos el problema de await (await (await first ()?). Second ()?). Third () ?.

En mi práctica, nunca escribe dos await en una línea. En casos muy raros, cuando lo necesite, simplemente vuelva a escribirlo como then y no use await en absoluto. Puedes ver que es mucho más difícil de leer que

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

Así que creo que está bien si el lenguaje desalienta a escribir código de tal manera para que el caso principal sea más simple y mejor.

hero away future await? parece interesante aunque desconocido, pero no veo ningún contraargumento lógico en contra de eso.

En mi práctica, nunca escribe dos await en una línea.

Pero, ¿es esto porque es una mala idea independientemente de la sintaxis, o simplemente porque la sintaxis await de C # lo hace feo? La gente hizo argumentos similares en torno a try!() (el precursor de ? ).

El sufijo y las versiones implícitas son mucho menos feas:

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

Pero, ¿es esto porque es una mala idea independientemente de la sintaxis, o simplemente porque la sintaxis de espera existente de C # lo hace feo?

Creo que es una mala idea independientemente de la sintaxis porque tener una línea por operación async ya es lo suficientemente complejo de entender y difícil de depurar. Tenerlos encadenados en una sola declaración parece ser aún peor.

Por ejemplo, echemos un vistazo al código real (he tomado una pieza de mi proyecto):

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

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

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

    var statuses = await request.GetStatusesAsync();

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

Muestra que en la práctica no vale la pena encadenar await s incluso si la sintaxis lo permite, porque se volvería completamente ilegible await solo hace que un delineador sea aún más difícil de escribir y leer, pero lo hago creo que no es la única razón por la que es malo.

El sufijo y las versiones implícitas son mucho menos feos.

La posibilidad de distinguir el inicio de la tarea y la tarea en espera es realmente importante. Por ejemplo, a menudo escribo código como ese (nuevamente, un fragmento del proyecto):

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

    return await Task.WhenAll(tasks);
}

Aquí estamos creando solicitudes N async y luego las estamos esperando. No esperamos en cada iteración de bucle, pero primero creamos una matriz de solicitudes asíncronas y luego las esperamos todas a la vez.

No conozco a Kotlin, así que tal vez resuelvan esto de alguna manera. Pero no veo cómo se puede expresar si "ejecutar" y "esperar" la tarea es lo mismo.


Así que creo que la versión implícita es imposible incluso en lenguajes mucho más implícitos como C #.
En Rust con sus reglas que ni siquiera le permiten convertir implícitamente u8 a i32 , sería mucho más confuso.

@Pzixel Sí, la segunda opción parece una de las más preferibles. También he usado async/await en C #, pero no mucho, ya que no he programado principalmente en C # durante algunos años. En cuanto a la precedencia, await (future?) es más natural para mí.

@rpjohnst Me gusta la idea de un operador postfix, pero también me preocupa la legibilidad y las suposiciones que la gente hará; podría confundirse fácilmente con un miembro de un struct llamado await .

La posibilidad de distinguir el inicio de la tarea y la tarea en espera es realmente importante.

Por lo que vale, la versión implícita hace esto. Se discutió a muerte tanto en el hilo RFC como en el hilo interno, por lo que no entraré en muchos detalles aquí, pero la idea básica es solo que mueve lo explícito de la tarea pendiente a la construcción de la tarea, no lo hace. no introduce ninguna nueva implícita.

Tu ejemplo se vería así:

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

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

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

Esto es lo que quise decir con "para que esto funcione, el acto de construir un futuro también debe hacerse explícito". Es muy similar a trabajar con subprocesos en código sincronizado: llamar a una función siempre espera a que se complete antes de reanudar la llamada, y existen herramientas independientes para introducir la simultaneidad. Por ejemplo, los cierres y thread::spawn / join corresponden a bloques asincrónicos y join_all / select / etc.

Por lo que vale, la versión implícita hace esto. Se discutió a muerte tanto en el hilo RFC como en el hilo interno, por lo que no entraré en muchos detalles aquí, pero la idea básica es solo que mueve lo explícito de la tarea pendiente a la construcción de la tarea, no lo hace. No introduce ninguna nueva implícita.

Creo que sí. No puedo ver aquí cuál sería el flujo en esta función, dónde se encuentran los puntos donde la ejecución se interrumpe hasta que se completa la espera. Solo veo el bloque async que dice "hola, en algún lugar aquí hay funciones asíncronas, intenta averiguar cuáles, ¡te sorprenderás!".

Otro punto: el óxido tiende a ser un lenguaje en el que se puede expresar todo, cerca del metal desnudo, etc. Me gustaría proporcionar un código bastante artificial, pero creo que ilustra la idea:

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

El óxido siempre tiende a proporcionar un control total sobre lo que está sucediendo. await permiten especificar puntos donde continuar el proceso. También le permite unwrap un valor dentro del futuro. Si permite la conversión implícita en el lado del uso, tiene varias implicaciones:

  1. En primer lugar, debe escribir un código sucio para emular este comportamiento.
  2. Ahora, RLS e IDE deben esperar que nuestro valor sea Future<T> o esperar T . No es un problema con las palabras clave; si existe, el resultado es T , de lo contrario es Future<T>
  3. Hace que el código sea más difícil de entender. En su ejemplo, no veo por qué interrumpe la ejecución en la línea get_status_updates , pero no en get_status_update . Son bastante similares entre sí. Entonces, o no funciona de la forma en que lo era el código original o es tan complicado que no puedo verlo incluso cuando estoy bastante familiarizado con el tema. Ambas alternativas no hacen que esta opción sea un favor.

No puedo ver aquí cuál sería el flujo en esta función, dónde se encuentran los puntos donde la ejecución se interrumpe hasta que se completa la espera.

Sí, esto es lo que quise decir con "esto hace que los puntos de suspensión sean más difíciles de ver". Si lees el hilo interno vinculado, expuse un argumento de por qué esto no es un problema tan grande. No tiene que escribir ningún código nuevo, simplemente coloque las anotaciones en un lugar diferente ( async bloques en lugar de await ed expresiones). Los IDE no tienen problemas para saber cuál es el tipo (siempre es T para llamadas a funciones y Future<Output=T> para async bloques).

También señalaré que su comprensión probablemente sea incorrecta independientemente de la sintaxis. Las funciones async Rust no ejecutan ningún código hasta que se esperan de alguna manera, por lo que su verificación b.Status != TaskStatus.RanToCompletion siempre pasará. Esto también se discutió a muerte en el hilo de RFC, si está interesado en por qué funciona de esta manera.

En su ejemplo, no veo por qué interrumpe la ejecución en la línea get_status_updates , pero no en get_status_update . Son bastante similares entre sí.

Lo hace la ejecución de interrupción en ambos lugares. La clave es que los bloques async no se ejecutan hasta que se los espera, porque esto es cierto para todos los futuros en Rust, como describí anteriormente. En mi ejemplo, get_statuses llama (y por lo tanto espera) get_status_updates , luego en el ciclo construye (pero no espera) count futuros, luego llama (y por lo tanto espera ) join_all , momento en el que esos futuros llaman simultáneamente (y por lo tanto esperan) get_status_update .

La única diferencia con su ejemplo es cuando exactamente comienzan a correr los futuros; en el suyo, es durante el ciclo; en el mío, es durante join_all . Pero esta es una parte fundamental de cómo funcionan los futuros de Rust, no tiene nada que ver con la sintaxis implícita o incluso con async / await en absoluto.

También señalaré que su comprensión probablemente sea incorrecta independientemente de la sintaxis. Las funciones asíncronas de Rust no ejecutan ningún código hasta que se esperan de alguna manera, por lo que su verificación b.Status! = TaskStatus.RanToCompletion siempre pasará.

Sí, las tareas de C # se ejecutan de forma sincrónica hasta el primer punto de suspensión. Gracias por señalar eso.
Sin embargo, realmente no importa porque todavía debería poder ejecutar alguna tarea en segundo plano mientras ejecuto el resto del método y luego verificar si la tarea en segundo plano está terminada. Por ejemplo, podría ser

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

Tengo tu idea sobre los bloques async y, como veo, son la misma bestia, pero con más desventajas. En la propuesta original, cada tarea asincrónica está emparejada con await . Con los bloques async , cada tarea se emparejaría con el bloque async en el punto de construcción, por lo que estamos en casi la misma situación que antes (relación 1: 1), pero incluso un poco peor, porque se siente más antinatural y más difícil de entender, porque el comportamiento del sitio de llamadas depende del contexto. Con await puedo ver let a = foo() o let b = await foo() y sabría que esta tarea está recién construida o construida y esperada. Si veo let a = foo() con async bloques, tengo que mirar si hay algunos async arriba, si entiendo bien, porque en este caso

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

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

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

Estamos esperando todas las tareas a la vez mientras estamos aquí.

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

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

Los estamos ejecutando uno por uno.

Por lo tanto, no puedo decir cuál es el comportamiento exacto de esta parte:

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

Sin tener más contexto.

Y las cosas se ponen más raras con los bloques anidados. Sin mencionar las preguntas sobre herramientas, etc.

El comportamiento del sitio de llamadas depende del contexto.

Esto ya es cierto con los cierres y el código de sincronización normal. Por ejemplo:

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

vs

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

Otra cosa que debe tener en cuenta de la propuesta de espera implícita completa es que no puede llamar a async fn s desde contextos que no sean async . Esto significa que la sintaxis de llamada de función some_function(arg1, arg2, etc) siempre ejecuta el cuerpo de some_function hasta su finalización antes de que continúe la llamada, independientemente de si some_function es async . Entonces, la entrada en un contexto async siempre se marca explícitamente, y la sintaxis de llamada de función es en realidad más consistente.

Con respecto a la sintaxis de espera: ¿Qué pasa con una macro con sintaxis de método? No puedo encontrar un RFC real para permitir esto, pero encontré algunas discusiones ( 1 , 2 ) en reddit, por lo que la idea no tiene precedentes. Esto permitiría que await trabaje en la posición de postfijo sin convertirlo en una palabra clave / introducir una nueva sintaxis solo para esta función.

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

Hay una biblioteca de Scala-world que simplifica las composiciones de mónadas: http://monadless.io

Quizás algunas ideas sean interesantes para Rust.

cita de los documentos:

La mayoría de los lenguajes convencionales tienen soporte para programación asincrónica usando el modismo async / await o lo están implementando (por ejemplo, F #, C # / VB, Javascript, Python, Swift). Aunque es útil, async / await generalmente está vinculado a una mónada particular que representa cálculos asincrónicos (Tarea, Futuro, etc.).

Esta biblioteca implementa una solución similar a async / await pero generalizada a cualquier tipo de mónada. Esta generalización es un factor importante considerando que algunas bases de código usan otras mónadas como Task además de Future para cálculos asincrónicos.

Dada una mónada M , la generalización usa el concepto de elevar valores regulares a una mónada ( T => M[T] ) y anular valores de una instancia de mónada ( M[T] => T ). > Ejemplo de uso:

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

Tenga en cuenta que el levantamiento corresponde a asíncrono y un levantamiento para esperar.

Esto ya es cierto con los cierres y el código de sincronización normal. Por ejemplo:

Veo varias diferencias aquí:

  1. El contexto lambda es inevitable, pero no es para await . Con await no tenemos un contexto, con async tenemos que tener uno. El primero gana, porque proporciona las mismas características, pero requiere saber menos sobre el código.
  2. Las lambdas tienden a ser cortas, varias líneas como máximo, por lo que vemos todo el cuerpo a la vez, y simples. async funciones
  3. Las lambdas rara vez se anidan (excepto para las llamadas then , pero se propone await ), los bloques async se anidan con frecuencia.

Otra cosa que debe tener en cuenta de la propuesta de espera implícita completa es que no puede llamar a fns asíncronos desde contextos no asíncronos.

Hmm, no me di cuenta de eso. No suena bien, porque en mi práctica a menudo desea ejecutar async desde un contexto no async. En C # async es solo una palabra clave que permite al compilador reescribir el cuerpo de la función, no afecta la interfaz de la función de ninguna manera, por lo que async Task<Foo> y Task<Foo> son completamente intercambiables, y desacopla la implementación y la API.

A veces, es posible que desee bloquear la tarea async , por ejemplo, cuando desee llamar a alguna API de red desde main . Debe bloquear (de lo contrario, regresa al sistema operativo y el programa finaliza) pero debe ejecutar una solicitud HTTP asíncrona. No estoy seguro de qué solución podría haber aquí, excepto piratear main para permitir que sea asincrónico, así como lo hacemos con el tipo de retorno principal Result , si no puede llamarlo desde el main no asincrónico .

Otra consideración a favor del actual await es cómo funciona en otro lenguaje popular (como lo señala @fdietze ). Facilita la migración desde otros lenguajes como C # / TypeScript / JS / Python y, por lo tanto, es un mejor enfoque en términos de atraer a gente nueva.

Veo varias diferencias aqui

También debe darse cuenta de que el RFC principal ya tiene bloques async , con la misma semántica que la versión implícita, entonces.

No suena bien, porque en mi práctica a menudo desea ejecutar async desde un contexto no async.

Esto no es un problema. Todavía se puede utilizar async bloques en no async contextos (lo cual está bien, ya que sólo evalúan a un F: Future como siempre), y todavía se puede generar o bloque de futuros usando exactamente la misma API que antes.

Simplemente no puede llamar a async fn s, sino envolver la llamada en un bloque async como lo hace independientemente del contexto en el que se encuentre, si desea un F: Future fuera de eso.

async es solo una palabra clave que permite al compilador reescribir el cuerpo de la función, no afecta la interfaz de la función de ninguna manera

Sí, esta es una diferencia legítima entre las propuestas. También estaba cubierto con el hilo interno. Podría decirse que tener diferentes interfaces para los dos es útil porque le muestra que la versión async fn no ejecutará ningún código como parte de la construcción, mientras que la versión -> impl Future puede, por ejemplo, iniciar una solicitud antes de darle a F: Future . También hace que async fn s sean más consistentes con fn s normales, ya que llamar a algo declarado como -> T siempre le dará un T , independientemente de si es async .

(También debe tener en cuenta que en Rust todavía hay un gran salto entre async fn y Future versión -Volviendo, tal como se describe en el RFC. El async fn versión no hace mención Future en cualquier lugar de su firma; y la versión manual requiere impl Trait , lo que conlleva algunos problemas relacionados con la vida útil. Esto es, de hecho, parte de la motivación para async fn para empezar.)

Facilita la migración desde otros lenguajes como C # / TypeScript / JS / Python

Esta es una ventaja solo para la sintaxis literal await future , que es bastante problemática por sí sola en Rust. Cualquier otra cosa con la que podamos terminar también tiene una discrepancia con esos lenguajes, mientras que la espera implícita al menos tiene a) similitudes con Kotlin yb) similitudes con el código síncrono basado en subprocesos.

Sí, esta es una diferencia legítima entre las propuestas. También estaba cubierto con el hilo interno. Podría decirse que es útil tener diferentes interfaces para los dos

Yo diría que _ tener diferentes interfaces para los dos tiene algunas ventajas_, porque tener API dependiente de los detalles de implementación no me suena bien. Por ejemplo, está redactando un contrato que simplemente delega una llamada al futuro interno

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

Y luego solo desea agregar algunos registros

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

Y se convierte en un cambio rotundo. Whoa?

Esta es una ventaja solo para la sintaxis literal de espera futura, que es bastante problemática por sí sola en Rust. Cualquier otra cosa con la que podamos terminar también tiene una discrepancia con esos lenguajes, mientras que la espera implícita al menos tiene a) similitudes con Kotlin yb) similitudes con el código síncrono basado en subprocesos.

Es una ventaja para cualquier sintaxis await , await foo / foo await / foo@ / foo.await / ... una vez que entienda que es el Lo mismo, la única diferencia es que lo colocas antes / después o tienes un sigilo en lugar de una palabra clave.

También debe tener en cuenta que en Rust todavía hay un gran salto entre async fn y la versión que regresa al futuro, como se describe en el RFC

Lo sé y me inquieta mucho.

Y se convierte en un cambio rotundo.

Puede evitarlo devolviendo un bloque async . Bajo la propuesta de espera implícita, su ejemplo se ve así:

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

Y con la tala:

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

El mayor problema de tener esta distinción surge durante la transición del ecosistema de futuras implementaciones manuales y combinadores (la única forma en la actualidad) a async / await. Pero incluso entonces, la propuesta le permite mantener la interfaz anterior y proporcionar una nueva asincrónica junto con ella. C # está lleno de ese patrón, por ejemplo.

Bueno, eso suena razonable.

Sin embargo, creo que tal implícita (no vemos si foo() aquí es una función asíncrona o de sincronización) conduce a los mismos problemas que surgieron en protocolos como COM + y fue una razón para que WCF se implementara como estaba. . La gente tenía problemas cuando las solicitudes remotas asíncronas parecían llamadas a métodos simples.

Este código se ve perfectamente bien, excepto que no puedo ver si alguna solicitud es asíncrona o sincronizada. Creo que es información importante. Por ejemplo:

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

Es crucial saber si bar es una función de sincronización o asíncrona. A menudo veo await en el ciclo como un marcador de que este código debe cambiarse para lograr un mejor rendimiento durante la carga y el rendimiento. Este es un código que revisé ayer (el código es subóptimo, pero es una de las iteraciones de revisión):

image

Como puede ver, noté fácilmente que tenemos un bucle en espera aquí y pedí cambiarlo. Cuando se confirmó el cambio, obtuvimos una aceleración de carga de página 3x. Sin await fácilmente podría pasar por alto este mal comportamiento.

Admito que no he usado Kotlin, pero la última vez que miré ese lenguaje, parecía ser principalmente una variante de Java con menos sintaxis, hasta el punto en que era fácil traducir mecánicamente uno al otro. También puedo imaginar por qué sería de agrado en el mundo de Java (que tiende a ser un poco pesado en sintaxis), y soy consciente de que recientemente obtuvo un impulso en popularidad específicamente debido a que no es Java (la situación de Oracle vs. ).

Sin embargo, si decidimos tener en cuenta la popularidad y la familiaridad, es posible que deseemos echar un vistazo a lo que hace JavaScript, que también es explícito await .

Dicho esto, C # introdujo await en los lenguajes convencionales, que es quizás uno de los idiomas en los que se consideraba que la usabilidad era de suma importancia . En C #, las llamadas asincrónicas se indican no solo por la palabra clave await , sino también por el sufijo Async de las llamadas al método. La otra característica del lenguaje que se comparte más con await , yield return también es visible en el código.

¿Porqué es eso? Mi opinión es que los generadores y las llamadas asincrónicas son construcciones demasiado poderosas para dejarlas pasar desapercibidas en el código. Existe una jerarquía de operadores de flujo de control:

  • ejecución secuencial de declaraciones (implícita)
  • llamadas a función / método (bastante evidente, compárese con, por ejemplo, Pascal donde no hay diferencia en el sitio de llamada entre una función nula y una variable)
  • goto (está bien, no es una jerarquía estricta)
  • generadores ( yield return tiende a destacarse)
  • await + Async sufijo

Fíjate cómo también van de menos a más verbosos, según su expresividad o potencia.

Por supuesto, otros idiomas adoptaron enfoques diferentes. Las continuaciones de esquema (como en call/cc , que no es muy diferente de await ) o las macros no tienen sintaxis para mostrar lo que está llamando. Para las macros, Rust adoptó el enfoque de facilitar su visualización.

Entonces, yo diría que tener menos sintaxis no es deseable en sí mismo (hay lenguajes como APL o Perl para eso), y que la sintaxis no tiene que ser simplemente repetitiva y tiene un papel importante en la legibilidad.

También hay un argumento paralelo (lo siento, no recuerdo la fuente, pero podría provenir de alguien del equipo de idiomas) de que las personas se sienten más cómodas con la sintaxis ruidosa de las nuevas funciones cuando son nuevas, pero luego están bien con una menos detallado una vez que terminan siendo de uso común.


En cuanto a la pregunta de await!(foo)? frente a await foo? , estoy en el campamento anterior. Puede internalizar prácticamente cualquier sintaxis, sin embargo, estamos demasiado acostumbrados a tomar señales del espaciado y la proximidad. Con await foo? existe una gran posibilidad de que uno se cuestione sobre la precedencia de los dos operadores, mientras que las llaves dejan en claro lo que está sucediendo. No vale la pena salvar a tres personajes. Y en cuanto a la práctica de encadenar await! s, si bien puede ser un modismo popular en algunos idiomas, creo que tiene demasiados inconvenientes, como la escasa legibilidad y la interacción con los depuradores, para que valga la pena optimizarlo.

No vale la pena salvar a tres personajes.

En mi experiencia anecdótica, los caracteres adicionales (por ejemplo, nombres más largos) no son un gran problema, pero los tokens adicionales pueden ser realmente molestos. En términos de una analogía de la CPU, un nombre largo es un código de línea recta con una buena localidad (puedo escribirlo desde la memoria muscular) mientras que el mismo número de caracteres cuando involucra múltiples tokens (por ejemplo, puntuación) es ramificado y lleno de errores de caché.

(Estoy totalmente de acuerdo en que await foo? sería muy poco obvio y deberíamos evitarlo, y que tener que escribir más tokens sería preferible; mi observación es solo que no todos los personajes son creados iguales).


@rpjohnst Creo que su propuesta alternativa podría tener una recepción ligeramente mejor si se presentara como "asincrónica explícita" en lugar de "espera implícita" :-)

Es crucial saber si bar es una función de sincronización o asíncrona.

No estoy seguro de que esto sea realmente diferente de saber si alguna función es barata o cara, o si hace IO o no, o si toca algún estado global o no. (Esto también se aplica a la jerarquía de @lnicola : si las llamadas asíncronas se ejecutan hasta su finalización al igual que las llamadas sincronizadas, ¡entonces no son diferentes en términos de potencia!)

Por ejemplo, el hecho de que la llamada estuviera en un bucle es tan importante, si no más, que el hecho de que fuera asincrónica. Y en Rust, donde la paralelización es mucho más fácil de hacer bien, también podría sugerir que los bucles síncronos de aspecto costoso se cambien a iteradores de rayón.

Así que no creo que exigir await sea ​​tan importante para captar estas optimizaciones. Los bucles siempre son buenos lugares para buscar optimización, y async fn s ya son un buen indicador de que puede obtener alguna concurrencia IO barata. Si se pierde esas oportunidades, incluso podría escribir un lint de Clippy para "llamada asíncrona en un bucle" que ejecuta ocasionalmente. ¡Sería genial tener una pelusa similar para el código síncrono también!

La motivación para la "asincrónica explícita" no es simplemente "menos sintaxis", como implica @lnicola . Es para hacer que el comportamiento de la sintaxis de llamada a la función sea más consistente, de modo que foo() siempre ejecute el cuerpo de foo hasta su finalización. Según esta propuesta, omitir una anotación solo le da un código menos concurrente, que es cómo prácticamente todo el código ya se comporta. En "espera explícita", omitir una anotación introduce simultaneidad accidental, o al menos entrelazado accidental, lo cual es problemático .

Creo que su propuesta alternativa podría tener una recepción ligeramente mejor si se presentara como "asincrónica explícita" en lugar de "espera implícita" :-)

El hilo se denomina "construcción futura explícita, espera implícita", pero parece que el último nombre se ha quedado. :PAGS

No estoy seguro de que esto sea realmente diferente de saber si alguna función es barata o cara, o si hace IO o no, o si toca algún estado global o no. (Esto también se aplica a la jerarquía de @lnicola : si las llamadas asíncronas se ejecutan hasta su finalización al igual que las llamadas sincronizadas, ¡entonces no son diferentes en términos de potencia!)

Creo que esto es tan importante como saber que la función cambia algún estado, y ya tenemos una palabra clave mut tanto en el lado de la llamada como en el lado de la persona que llama.

La motivación para la "asincrónica explícita" no es simplemente "menos sintaxis", como implica @lnicola . Es para hacer que el comportamiento de la sintaxis de la llamada a la función sea más consistente, de modo que foo () siempre ejecute el cuerpo de foo hasta su finalización.

Por un lado, es una buena consideración. En el otro, puede separar fácilmente la creación futura y la ejecución futura. Quiero decir, si foo te devuelve una abstracción que te permite llamar a run y obtener algún resultado, no hace que foo basura inútil que no hace nada, hace una muy Cosa útil: construye algún objeto al que puedes llamar métodos más adelante. No lo hace diferente. El método foo que llamamos es solo una caja negra y vemos su firma Future<Output=T> y en realidad devuelve un futuro. Así que explícitamente lo await cuando queremos hacerlo.

El hilo se denomina "construcción futura explícita, espera implícita", pero parece que el último nombre se ha quedado. :PAGS

Personalmente, creo que la mejor alternativa es "espera explícita asincrónica explícita" :)


PD

Esta noche también me asaltó un pensamiento: ¿trataste de comunicarte con C # LDM? Por ejemplo, tipos como @HaloFour , @gafter o @CyrusNajmabadi . Puede ser muy buena idea preguntarles por qué tomaron la sintaxis que tomaron. Propondría preguntarle a chicos de otros idiomas también, pero simplemente no los conozco :) Estoy seguro de que tuvieron múltiples debates sobre la sintaxis existente y ya podrían discutirlo mucho y pueden tener algunas ideas útiles.

No significa que Rust tenga que tener esta sintaxis porque C # la tiene, pero solo permite tomar decisiones más ponderadas.

Personalmente, creo que la mejor alternativa es "espera explícita asincrónica explícita" :)

Sin embargo, la propuesta principal no es "asincrónica explícita", por eso elegí el nombre. Es "asincronía implícita ", porque no se puede decir de un vistazo dónde se está introduciendo la asincronía. Cualquier llamada de función no anotada podría estar construyendo un futuro sin esperarlo, aunque Future no aparezca en ninguna parte de su firma.

Por si sirve de algo, la rosca internos incluye un "esperan ser explícita asíncrono explícita" alternativa, porque eso es con una u otra alternativa principal con capacidad para un futuro. (Vea la sección final de la primera publicación).

¿Intentaste comunicarte con C # LDM?

El autor del RFC principal lo hizo. El punto principal que surgió de esto, por lo que recuerdo, fue la decisión de no incluir Future en la firma de async fn s. En C #, puede reemplazar Task con otros tipos para tener cierto control sobre cómo se maneja la función. Pero en Rust, no tenemos (y no tendremos) ningún mecanismo de este tipo; todos los futuros pasarán por un solo rasgo, por lo que no es necesario escribir ese rasgo cada vez.

También nos comunicamos con los diseñadores de lenguajes de Dart, y esa fue una gran parte de mi motivación para redactar la propuesta de "asincrónica explícita". Dart 1 tuvo un problema porque las funciones no se ejecutaron hasta su primera espera cuando se llamaron (no es exactamente igual a cómo funciona Rust, pero similar), y eso causó una confusión tan masiva que en Dart 2 cambiaron, por lo que las funciones se ejecutaron por primera vez. esperar cuando se llame. Rust no puede hacer eso por otras razones, pero podría ejecutar la función completa cuando se llama, lo que también evitaría esa confusión.

También nos comunicamos con los diseñadores de lenguajes de Dart, y esa fue una gran parte de mi motivación para redactar la propuesta de "asincrónica explícita". Dart 1 tuvo un problema porque las funciones no se ejecutaron hasta su primera espera cuando se llamaron (no es exactamente igual a cómo funciona Rust, pero similar), y eso causó una confusión tan masiva que en Dart 2 cambiaron, por lo que las funciones se ejecutaron por primera vez. esperar cuando se llame. Rust no puede hacer eso por otras razones, pero podría ejecutar la función completa cuando se llama, lo que también evitaría esa confusión.

Gran experiencia, no me di cuenta. Es bueno saber que has hecho un trabajo tan enorme. Bien hecho 👍

Esta noche también me asaltó un pensamiento: ¿trataste de comunicarte con C # LDM? Por ejemplo, tipos como @HaloFour , @gafter o @CyrusNajmabadi . Puede ser muy buena idea preguntarles por qué tomaron la sintaxis que tomaron.

Me complace brindarle cualquier información que le interese. Sin embargo, solo la he revisado. ¿Sería posible condensar algunas preguntas específicas que tenga actualmente?

Con respecto a la sintaxis de await (esto podría ser completamente estúpido, no dude en gritarme; soy un novato en programación asíncrona y no tengo idea de lo que estoy hablando):

En lugar de usar la palabra "aguardar", ¿no podemos introducir un símbolo / operador, similar a ? . Por ejemplo, podría ser # o @ o cualquier otra cosa que no se utilice actualmente.

Por ejemplo, si fuera un operador de sufijo:

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

Es muy conciso y se lee naturalmente de izquierda a derecha: primero espere ( # ), luego maneje los errores ( ? ). No tiene el problema que tiene la palabra clave de espera de sufijo, donde .await parece un miembro de estructura. # es claramente un operador.

No estoy seguro de si postfix es el lugar correcto para hacerlo, pero se sintió así debido a la precedencia. Como prefijo:

let stuff = #func()?;

O diablos incluso:

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

¿Se ha hablado de esto alguna vez?

(Me doy cuenta de que esto comienza a acercarse a la sintaxis de "combinación aleatoria de símbolos en el teclado" por la que Perl es infame ... :-D)

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

@CyrusNajmabadi gracias por venir. La pregunta principal es ¿qué opción de las enumeradas cree que se adapta mejor al lenguaje actual de Rust tal como está, o tal vez hay alguna otra alternativa? Este tema no es muy largo, por lo que puede desplazarse fácilmente de arriba hacia abajo rápidamente. La pregunta principal: ¿debería Rust seguir la forma actual de C # / TS / ... await o tal vez debería implementar la suya propia? ¿Es la sintaxis actual algún tipo de "legado" que le gustaría cambiar de alguna manera o se ajusta mejor a C # y también es la mejor opción para los nuevos idiomas?

La consideración principal contra la sintaxis de C # es la precedencia del operador await foo? debe esperar primero y luego evaluar el operador ? , así como la diferencia de que, a diferencia de C #, la ejecución no se ejecuta en el hilo de llamada hasta el primer await , pero no comienza en absoluto, de la misma manera que el fragmento de código actual no ejecuta verificaciones de negatividad hasta que GetEnumerator se llama por primera vez:

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

Más detallado en mi primer comentario y discusión posterior.

@Pzixel Oh, supongo que me perdí ese cuando estaba hojeando este hilo antes ...

En cualquier caso, no he visto mucha discusión sobre esto, aparte de esa breve mención.

¿Hay buenos argumentos a favor / en contra?

@rayvector Argumenté un poco aquí a favor de una sintaxis más detallada. Una de las razones es la que mencionas:

la sintaxis de "combinación aleatoria de símbolos en el teclado" por la que Perl es famoso

Para aclarar, no creo que await!(f)? esté realmente en ejecución para la sintaxis final, se eligió específicamente porque es una forma sólida de no comprometerse con ninguna elección en particular. Aquí hay sintaxis (incluido el operador ? ) que creo que todavía están "en ejecución":

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

O posiblemente alguna combinación de estos. El punto es que varios de ellos contienen llaves para ser más claros sobre la precedencia y hay muchas opciones aquí, pero la intención es que await sea ​​un operador de palabra clave, no una macro, en la versión final ( salvo algún cambio importante como el que ha propuesto rpjohnst).

Voto por un operador simple de espera de sufijo (por ejemplo, ~ ) o la palabra clave sin parens y con la mayor precedencia.

He estado leyendo este hilo y me gustaría proponer lo siguiente:

  • await f? evalúa primero el operador ? y luego espera el futuro resultante.
  • (await f)? espera el futuro primero, y luego evalúa el operador ? contra el resultado (debido a la precedencia del operador Rust ordinario)
  • await? f está disponible como azúcar sintáctico para `(await f) ?. Creo que "el futuro devuelve un resultado" será un caso muy común, por lo que una sintaxis dedicada tiene mucho sentido.

Estoy de acuerdo con otros comentaristas en que await debe ser explícito. Es bastante sencillo hacer esto en JavaScript, y realmente aprecio la claridad y la legibilidad del código Rust, y siento que hacer async implícito arruinaría esto para el código async.

Se me ocurrió que el "bloque asíncrono implícito" debería ser implementable como un proc_macro, que simplemente inserta una palabra clave await antes de cualquier futuro.

La pregunta principal es qué opción de las enumeradas cree que se adapta mejor al lenguaje actual de Rust tal como está,

Preguntarle a un diseñador de C # qué se adapta mejor al lenguaje rust es ... interesante :)

No me siento calificado para tomar tal determinación. Me gusta el óxido y incursiono en él. Pero no es un idioma que uso todos los días. Tampoco lo he arraigado profundamente en mi psique. Como tal, no creo que esté calificado para hacer ninguna afirmación sobre cuáles son las opciones adecuadas para este idioma aquí. Quiere preguntarme acerca de Go / TypeScript / C # / VB / C ++. Seguro, me sentiría mucho más cómodo. Pero el óxido está demasiado fuera de mi ámbito de experiencia como para sentirme cómodo con esos pensamientos.

La consideración principal contra la sintaxis de C # es la precedencia de los operadores await foo?

Esto es algo que siento que puedo comentar. Pensamos mucho en la precedencia con 'await' y probamos muchas formas antes de establecer la forma que queríamos. Una de las cosas principales que encontramos fue que para nosotros y los clientes (internos y externos) que querían usar esta función, rara vez era el caso de que la gente realmente quisiera "encadenar" algo más allá de su llamada asíncrona. En otras palabras, las personas parecían gravitar fuertemente hacia que "esperar" fuera la parte más importante de cualquier expresión completa y, por lo tanto, estar cerca de la cima. Nota: por 'expresión completa' me refiero a cosas como la expresión que obtiene en la parte superior de una declaración de expresión, o la expresión a la derecha de una asignación de nivel superior, o la expresión que pasa como un 'argumento' a algo.

La tendencia de la gente a querer "continuar" con la "espera" dentro de un expr era rara. Ocasionalmente vemos cosas como (await expr).M() , pero parecen menos comunes y menos deseables que la cantidad de personas que hacen await expr.M() .

Esta es también la razón por la que no utilizamos ninguna forma "implícita" para "esperar". En la práctica, era algo en lo que la gente quería pensar con mucha claridad y en lo que querían estar al frente y al centro en su código para poder prestarle atención. Curiosamente, incluso años después, esta tendencia se ha mantenido. es decir, a veces, muchos años después, lamentamos que algo sea excesivamente detallado. Algunas características son buenas de esa manera desde el principio, pero una vez que las personas se sienten cómodas con ellas, se adaptan mejor a algo más terso. Ese no ha sido el caso con 'await'. A la gente todavía parece gustarle mucho la naturaleza pesada de esa palabra clave y la precedencia que elegimos.

Hasta ahora, estamos muy contentos con la elección de precedencia para nuestra audiencia. En el futuro, podríamos hacer algunos cambios aquí. Pero, en general, no existe una fuerte presión para hacerlo.

-

además de la diferencia de que, a diferencia de la ejecución de C #, no se ejecuta en el hilo de la persona que llama hasta la primera espera, pero no comienza en absoluto, el fragmento de código actual de la misma manera no ejecuta verificaciones de negatividad hasta que se llama a GetEnumerator por primera vez:

En mi opinión, la forma en que hicimos los enumeradores fue un error y ha generado mucha confusión a lo largo de los años. Ha sido especialmente malo debido a la propensión a tener que escribir una gran cantidad de código de esta manera:

`` c #
anular SomeEnumerator (argumentos X)
{
// Validar Args, hacer trabajo sincrónico.
return SomeEnumeratorImpl (args);
}

anular SomeEnumeratorImpl (X argumentos)
{
// ...
producir
// ...
}

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

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

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

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

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

Puede llamar totalmente a este método y nunca esperar. si "la ejecución no se ejecuta en el hilo del llamador hasta que la primera espera", ¿qué sucede realmente aquí?

¿esperar? f está disponible como azúcar sintáctico para `(aguardar f) ?. Creo que "el futuro devuelve un resultado" será un caso muy común, por lo que una sintaxis dedicada tiene mucho sentido.

Esto me resuena más. Permite que "esperar" sea el concepto más importante, pero también permite un manejo sencillo de los tipos de resultados.

Una cosa que sabemos de C # es que la intuición de las personas sobre la precedencia está ligada al espacio en blanco. Entonces, si tiene "aguardar x?" entonces inmediatamente se siente como await tiene menos precedencia que ? porque el ? linda con la expresión. Si lo anterior realmente se analiza como (await x)? , sería sorprendente para nuestra audiencia.

Analizarlo como await (x?) se sentiría más natural solo por la sintaxis, y se ajustaría a la necesidad de obtener un 'Resultado' de un futuro / tarea de regreso, y querer 'esperar' eso si realmente recibió un valor . Si eso luego devolvió un Resultado, se siente apropiado combinarlo con el 'esperar' para indicar que sucederá después. por lo que await? x? cada ? une estrechamente a la parte del código con la que se relaciona más naturalmente. El primer ? relaciona con el await (y específicamente el resultado del mismo), y el segundo se relaciona con el x .

si "la ejecución no se ejecuta en el hilo del llamador hasta que la primera espera", ¿qué sucede realmente aquí?

No sucede nada hasta que la persona que llama espera el valor de retorno de FooAsync , momento en el que el cuerpo de FooAsync ejecuta hasta que un await o vuelve.

Funciona de esta manera porque Rust Future s son controlados por encuestas, asignados a la pila e inamovibles después de la primera llamada a poll . La persona que llama debe tener la oportunidad de moverlos a su lugar: en el montón para el nivel superior Future s, o por valor dentro de un padre Future , a menudo en el "marco de pila" de una llamada async fn ejecute cualquier código.

Esto significa que estamos atascados con a) semántica similar a un generador de C #, donde no se ejecuta ningún código en la invocación, ob) semántica similar a una corutina de Kotlin, donde la llamada a la función también la espera inmediata e implícitamente (con un cierre async { .. } bloques para cuando necesite ejecución concurrente).

Estoy a favor de este último, porque evita el problema que mencionas con los generadores de C # y también evita por completo la pregunta sobre la precedencia del operador.

@CyrusNajmabadi En Rust, Future generalmente no funciona hasta que se genera como Task (es mucho más similar a F # Async ):

let bar = foo();

En este caso, foo() devuelve Future , pero probablemente no haga nada. Tienes que generarlo manualmente (que también es similar a F # Async ):

tokio::run(bar);

Cuando se genera, ejecutará Future . Dado que este es el comportamiento predeterminado de Future , sería más coherente que async / await en Rust no ejecute ningún código hasta que se genere.

Obviamente, la situación es diferente en C #, porque en C # cuando llamas a foo() inmediatamente comienza a ejecutar Task , por lo que tiene sentido en C # ejecutar código hasta el primer await .

Además ... ¿cómo funcionan las cosas si tienes algo como esto? [...] Puedes llamar totalmente a este método y nunca esperar. si "la ejecución no se ejecuta en el hilo del llamador hasta que la primera espera", ¿qué sucede realmente aquí?

Si llama a FooAsync() entonces no hace nada, no se ejecuta ningún código. Luego, cuando lo genere, ejecutará el código sincrónicamente, await nunca se ejecutará, por lo que inmediatamente devuelve () (que es la versión de Rust de void )

En otras palabras, no es "la ejecución no se ejecuta en el hilo de la persona que llama hasta que la primera espera", es "la ejecución no se ejecuta hasta que se genera explícitamente (como con tokio::run )"

No sucede nada hasta que la persona que llama espera el valor de retorno de FooAsync, momento en el que el cuerpo de FooAsync se ejecuta hasta que espera o regresa.

Ick. Eso parece lamentable. Hay muchas ocasiones en las que es posible que nunca llegue a esperar algo (a menudo debido a la cancelación y la composición de las tareas). Como desarrollador, aún apreciaría recibir errores tempranos (que es una de las razones más comunes por las que la gente quiere que la ejecución se ejecute a la espera).

Esto significa que estamos atrapados con a) semántica similar a un generador de C #, donde no se ejecuta código en la invocación, ob) semántica similar a una corutina de Kotlin, donde la llamada a la función también la espera inmediata e implícitamente (con async {. .} bloques para cuando necesite ejecución concurrente).

Teniendo en cuenta estos, preferiría lo primero que lo segundo. Aunque solo mi preferencia personal. Si el enfoque de kotlin se siente más natural para su dominio, ¡hágalo!

@CyrusNajmabadi Ick. Eso parece lamentable. Hay muchas ocasiones en las que es posible que nunca llegue a esperar algo (a menudo debido a la cancelación y la composición de las tareas). Como desarrollador, aún apreciaría recibir errores tempranos (que es una de las razones más comunes por las que la gente quiere que la ejecución se ejecute a la espera).

Siento exactamente lo contrario. En mi experiencia con JavaScript, es muy común olvidar usar await . En ese caso, el Promise aún se ejecutará, pero los errores se tragarán (o sucederán otras cosas raras).

Con el estilo Rust / Haskell / F #, se ejecuta Future (con el manejo correcto de errores) o no se ejecuta en absoluto. Entonces notas que no se está ejecutando, así que investigas y lo arreglas. Creo que esto da como resultado un código más robusto.

@Pauan @rpjohnst Gracias por las explicaciones. Esos fueron enfoques que también consideramos. Pero resultó no ser tan deseable en la práctica.

En los casos en los que no querías que "realmente hiciera nada. Tienes que generarlo manualmente", encontramos que es más limpio modelar eso como devolver algo que genera tareas bajo demanda. es decir, algo tan simple como Func<Task> .

Siento exactamente lo contrario. En mi experiencia con JavaScript, es muy común olvidar usar await.

C # funciona para tratar de asegurarse de que esperó o utilizó la tarea con sensatez.

pero los errores serán tragados

Eso es lo contrario de lo que estoy diciendo. Estoy diciendo que quiero que el código se ejecute con entusiasmo para que los errores sean cosas que golpeo de inmediato, incluso en el caso de que nunca termine ejecutando el código en la tarea. Esto es lo mismo con los iteradores. Preferiría saber que lo estaba creando incorrectamente en el momento en que llamo a la función en lugar de potencialmente mucho más adelante en la línea si / cuando el iterador se transmite.

Entonces notas que no se está ejecutando, así que investigas y lo arreglas.

En los escenarios a los que me refiero, "no correr" es completamente razonable. Después de todo, mi aplicación puede decidir en cualquier momento que no necesita ejecutar la tarea. Ese no es el error que estoy describiendo. El error que estoy describiendo es que no pasé la validación, y quiero averiguarlo tan cerca del punto en el que lógicamente creé el trabajo en lugar del punto en el que el trabajo realmente necesita ejecutarse. Dado que estos son modelos para describir el procesamiento asíncrono, a menudo es bueno que estén muy lejos unos de otros. Por lo tanto, tener la información sobre los problemas lo antes posible es valioso.

Como se mencionó, esto tampoco es hipotético. Algo similar sucede con los flujos / iteradores. Las personas a menudo los crean, pero luego no se dan cuenta de ellos hasta más tarde. Ha sido una carga adicional para las personas tener que rastrear estas cosas hasta su origen. Esta es la razón por la que tantas API (incluido el BCL) ahora tienen que dividir entre el trabajo síncrono / temprano y el trabajo diferido / perezoso real.

Eso es lo contrario de lo que estoy diciendo. Estoy diciendo que quiero que el código se ejecute con entusiasmo para que los errores sean cosas que golpeo de inmediato, incluso en el caso de que nunca termine ejecutando el código en la tarea.

Puedo entender el deseo de errores tempranos, pero estoy confundido: ¿en qué situación alguna vez "terminarías sin llegar a generar el Future "?

La forma en que Future s funcionan en Rust es que compones Future s juntos de varias formas (incluyendo async / Future fusionado simple que contiene todos los Future s. Y luego, en el nivel superior de su programa ( main ), usa tokio::run (o similar) para generarlo.

Aparte de esa única llamada tokio::run en main , por lo general no generará Future s manualmente, sino que simplemente los compondrá. Y la composición maneja naturalmente el desove / manejo de errores / cancelación / etc. correctamente.

También quiero dejar algo claro. Cuando digo algo como:

Pero resultó no ser tan deseable en la práctica.

Estoy hablando muy específicamente de cosas con nuestro idioma / plataforma. Solo puedo dar una idea de las decisiones que tenían sentido para C # /. Net / CoreFx, etc. Puede ser completamente el caso de que su situación sea diferente y lo que desea optimizar y los tipos de enfoques que debe tomar van en una completa dirección diferente.

Puedo entender el deseo de cometer errores tempranos, pero estoy confundido: ¿en qué situación "terminarías sin llegar a generar el futuro"?

Todo el tiempo :)

Considere cómo se escribe Roslyn (el compilador C # / VB / base de código IDE). Es muy asincrónico e interactivo . es decir, el caso de uso principal debe usarse de manera compartida con muchos clientes que acceden a él. Los servicios de Cliest son comunes al interactuar con el usuario a través de una gran cantidad de funciones, muchas de las cuales deciden que ya no necesitan hacer el trabajo que originalmente pensaron que era importante, debido a que el usuario realiza una serie de acciones. Por ejemplo, mientras el usuario escribe, estamos haciendo toneladas de composiciones y manipulaciones de tareas, y podemos terminar decidiendo ni siquiera llegar a ejecutarlas porque otro evento llegó unos ms después.

Por ejemplo, mientras el usuario escribe, estamos haciendo toneladas de composiciones y manipulaciones de tareas, y podemos terminar decidiendo ni siquiera llegar a ejecutarlas porque otro evento llegó unos ms después.

Sin embargo, ¿no se trata solo de la cancelación?

Y la composición maneja naturalmente el desove / manejo de errores / cancelación / etc. correctamente.

Simplemente parece que tenemos dos modelos muy diferentes para representar las cosas. Está bien :) Mis explicaciones deben tomarse en el contexto del modelo que elegimos. Es posible que no tengan sentido para el modelo que está eligiendo.

Simplemente parece que tenemos dos modelos muy diferentes para representar las cosas. Está bien :) Mis explicaciones deben tomarse en el contexto del modelo que elegimos. Es posible que no tengan sentido para el modelo que está eligiendo.

Absolutamente, solo intento comprender su perspectiva y también explicar nuestra perspectiva. Gracias por tomarse el tiempo de explicar las cosas.

Sin embargo, ¿no se trata solo de la cancelación?

La cancelación es un concepto ortogonal a la asincronía (para nosotros). Se usan comúnmente juntos. Pero ninguno necesita al otro.

Podría tener un sistema completamente sin cancelación, y puede ser simplemente el caso de que nunca llegue a ejecutar el código que 'espera' las tareas que ha compuesto. es decir, por una razón lógica, su código puede simplemente decir "no necesito esperar a 't', solo voy a hacer otra cosa". Nada acerca de las tareas (en nuestro mundo) dicta o requiere que se espere que esa tarea sea esperada. En tal sistema, me gustaría obtener una validación temprana.

Nota: esto es similar al problema del iterador. Usted puede llamar a alguien para conseguir los resultados que va a utilizar más adelante en el código. Sin embargo, por diversas razones, es posible que no tenga que utilizar los resultados. Mi deseo personal aún sería obtener los resultados de la validación temprano, incluso si técnicamente no podría haberlos obtenido y mi programa tuvo éxito.

Creo que hay argumentos razonables para ambas direcciones. Pero mi opinión es que el enfoque sincrónico ha tenido más ventajas que desventajas. Por supuesto, si el enfoque sincrónico literalmente no encaja debido a cómo su implicación real quiere funcionar, entonces eso parece responder a la pregunta sobre lo que debe hacer: D

En otras palabras, no creo que su enfoque sea malo aquí. Y si tiene grandes beneficios en torno a este modelo que crees que es adecuado para Rust, entonces definitivamente hazlo :)

Podría tener un sistema completamente sin cancelación, y puede ser simplemente el caso de que nunca llegue a ejecutar el código que 'espera' las tareas que ha compuesto. es decir, por una razón lógica, su código puede simplemente decir "no necesito esperar a 't', solo voy a hacer otra cosa".

Personalmente, creo que eso se maneja mejor con la lógica if/then/else habitual:

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

Pero como dices, es una perspectiva muy diferente a la de C #.

Personalmente, creo que eso se maneja mejor con la lógica habitual if / then / else:

Si. eso estaría bien si la verificación de la condición pudiera realizarse en el mismo punto en que se crea la tarea (y muchos casos son así). Pero en nuestro mundo comúnmente no es el caso que las cosas estén tan bien conectadas de esa manera. Después de todo, queremos hacer un trabajo asincrónico en respuesta a los usuarios (para que los resultados estén listos cuando sea necesario), pero más adelante podemos decidir que ya no nos importa.

En nuestros dominios, la 'espera' ocurre en el punto en que la persona "necesita el valor", que es una determinación / componente / etc. diferente. de la decisión sobre "¿debería empezar a trabajar en el valor?"

En cierto sentido, están muy desacoplados y eso se ve como una virtud. El productor y el consumidor pueden tener políticas completamente diferentes, pero pueden comunicarse de manera efectiva sobre el trabajo asincrónico que se realiza a través de la agradable abstracción de la 'Tarea'.

De todos modos, me retiraré de la opinión de sincronización / asincronía. Claramente, hay modelos muy diferentes en juego aquí. :)

En términos de precedencia, he proporcionado información sobre cómo piensa C # las cosas. Espero que sea útil. Avísame si quieres más información allí.

@CyrusNajmabadi Sí, sus ideas fueron muy útiles. Personalmente, estoy de acuerdo con usted en que await? foo es el camino a seguir (aunque también me gusta la propuesta "explícita async ").

Por cierto, si desea una de las mejores opiniones de expertos sobre todas las complejidades del modelo .net en torno al trabajo de modelado asíncrono / sincronizado, y todos los pros / contras de ese sistema, entonces @stephentoub sería la persona con quien hablar. Sería 100 veces mejor que yo para explicar las cosas, aclarar los pros y los contras y, probablemente, poder profundizar en los modelos en ambos lados. Él está íntimamente familiarizado con el enfoque de .net aquí (incluidas las opciones tomadas y las opciones rechazadas), y cómo ha tenido que evolucionar desde el principio. También es dolorosamente consciente de los costos de rendimiento de los enfoques que ha adoptado .net (que es una de las razones por las que ValueTask ahora existe), lo que imagino que sería algo en lo que ustedes están pensando en primer lugar con su deseo de cero / bajo. -Abstracciones de costes.

Por lo que recuerdo, pensamientos similares sobre estas divisiones se pusieron en el enfoque de .net en los primeros días, y creo que él podía hablar muy bien sobre las decisiones finales que se tomaron y cuán apropiadas han sido.

Seguiría votando a favor de await? future incluso si parece un poco desconocido. ¿Hay inconvenientes reales en componerlos?

Aquí hay otro análisis exhaustivo de los pros y los contras de los asíncronos fríos (F #) vs calientes (C #, JS): http://tomasp.net/blog/async-csharp-differences.aspx

Ahora hay un nuevo RFC para macros postfix que permitiría experimentar con postfix await sin un cambio de sintaxis dedicado: https://github.com/rust-lang/rfcs/pull/2442

await {} es mi favorito aquí, que recuerda a unsafe {} además muestra precedencia.

let value = await { future }?;

@seunlanlege
sí, es remeniscente, por lo que la gente tiene la falsa suposición de que pueden escribir código como este

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

Pero no pueden.

@Pzixel
si te entiendo correctamente, ¿estás asumiendo que la gente asumiría que los futuros se esperan implícitamente dentro de un bloque await {} ? No estoy de acuerdo con eso. await {} solo esperaría en la expresión a la que se evalúa el bloque.

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

Y debe ser un patrón que se desaconseja

simplificado

let value = await { create_future() };

Propone una declaración en la que más de una expresión "debería desalentarse". ¿No ves nada malo en eso?

¿Es favorable hacer un patrón await (aparte de ref etc.)?
Algo como:

let await n = bar();

Prefiero llamar a eso un patrón async que await , aunque no veo muchas ventajas en convertirlo en una sintaxis de patrón. Las sintaxis de patrones generalmente funcionan de forma dual con respecto a sus contrapartes de expresión.

Según la página actual de https://doc.rust-lang.org/nightly/std/task/index.html , el mod de tarea consiste en reexportaciones de libcore y reexportaciones para liballoc, lo que hace que el resultado sea un poco ... subóptimo. Espero que esto se aborde de alguna manera antes de que se estabilice.

Eché un vistazo al código. Y tengo algunas sugerencias:

  • [x] El rasgo UnsafePoll y Poll enum tienen nombres muy similares, pero no están relacionados. Sugiero cambiar el nombre de UnsafePoll , por ejemplo, a UnsafeTask .
  • [x] En la caja de futuros, el código se dividió en diferentes submódulos. Ahora, la mayor parte del código está agrupado en task.rs lo que dificulta la navegación. Sugiero dividirlo de nuevo.
  • [x] TaskObj#from_poll_task() tiene un nombre extraño. Sugiero nombrarlo new() lugar
  • [x] TaskObj#poll_task podría ser poll() . El campo llamado poll podría llamarse poll_fn lo que también sugeriría que es un puntero de función
  • Waker podría usar la misma estrategia que TaskObj y poner la vtable en la pila. Solo una idea, no sé si queremos esto. ¿Sería más rápido porque es un poco menos indirecto?
  • [] dyn ahora es estable en beta. El código probablemente debería usar dyn donde corresponda

También puedo proporcionar un PR para estas cosas. @cramertj @aturon no dude en comunicarse conmigo a través de Discord para discutir los detalles.

¿Qué tal si agregas un método await() para todos los Future ?

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

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

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

@zengsai El problema es que await no funciona como un método normal. De hecho, considere lo que haría el método await cuando no esté en un bloque / función async . Los métodos no saben en qué contexto se ejecutan, por lo que no pueden causar un error de compilación.

@xfix esto no es cierto en general. El compilador puede hacer lo que quiera y podría manejar la llamada al método especialmente en este caso. La llamada al estilo del método resuelve el problema de las preferencias, pero es inesperado (await no funciona de esta manera en otros lenguajes) y probablemente sería un truco feo en el compilador.

@elszben Que el compilador pueda hacer lo que quiera no significa que deba hacer lo que quiera.

future.await() suena como una llamada de función normal, mientras que no lo es. Si desea ir de esta manera, la sintaxis future.await!() propuesta en algún lugar anterior permitiría la misma semántica y marcaría claramente con una macro "Algo extraño está sucediendo aquí, lo sé".

Editar: publicación eliminada

Moví esta publicación al RFC de futuros. Enlace

¿Alguien ha mirado la interacción entre async fn y #[must_use] ?

Si tiene un async fn , llamarlo directamente no ejecuta ningún código y devuelve un Future ; parece que todo async fn debe tener una inherente #[must_use] en el "exterior" impl Future tipo, por lo que no se les puede llamar sin tener que hacer algo con el Future .

Además de eso, si adjunta un #[must_use] al async fn usted mismo, parece que eso debería aplicarse al retorno de la función interna . Entonces, si escribe #[must_use] async fn foo() -> T { ... } , entonces no puede escribir await!(foo()) sin hacer algo con el resultado de await.

¿Alguien ha mirado la interacción entre async fn y # [must_use]?

Para otros interesados ​​en esta discusión, consulte https://github.com/rust-lang/rust/issues/51560.

Estaba pensando en cómo se implementan las funciones asincrónicas y me di cuenta de que estas funciones no admiten la recursividad ni la recursividad mutua.

para la sintaxis de espera, personalmente estoy a favor de las macros posteriores a la corrección, sin un enfoque de espera implícito, por su fácil encadenamiento, y que también se puede usar como una llamada a un método

@ warlord500 , está ignorando por completo la experiencia de millones de desarrolladores descritos anteriormente. No desea encadenar await .

@Pzixel, por favor, no
Sé que es posible que algunos contribuyentes no quieran permitir el encadenamiento de esperas, pero hay algunos de nosotros
desarrolladores que lo hacen. No estoy seguro de dónde sacaste la noción de que estaba ignorando
opiniones de los desarrolladores, mi comentario solo especificaba la opinión de un miembro de la comunidad y mis razones para tener esa opinión.

EDITAR : si tiene una opinión diferente, ¡compártala! Tengo curiosidad por saber por qué dices
¿No deberíamos permitir el encadenamiento de esperas a través de un método como la sintaxis?

@ warlord500 porque el equipo de MS compartió su experiencia con miles de clientes y millones de desarrolladores. Yo mismo lo sé porque escribo código asincrónico / en espera día a día, y nunca quieres encadenarlos. Aquí hay una cita exacta, si lo desea:

Pensamos mucho en la precedencia con 'await' y probamos muchas formas antes de establecer la forma que queríamos. Una de las cosas principales que encontramos fue que para nosotros y los clientes (internos y externos) que querían usar esta función, rara vez era el caso de que la gente realmente quisiera "encadenar" algo más allá de su llamada asíncrona. En otras palabras, las personas parecían gravitar fuertemente hacia que "esperar" fuera la parte más importante de cualquier expresión completa y, por lo tanto, estar cerca de la cima. Nota: por 'expresión completa' me refiero a cosas como la expresión que obtiene en la parte superior de una declaración de expresión, o la expresión a la derecha de una asignación de nivel superior, o la expresión que pasa como un 'argumento' a algo.

La tendencia de la gente a querer "continuar" con la "espera" dentro de un expr era rara. Ocasionalmente vemos cosas como (await expr).M() , pero parecen menos comunes y menos deseables que la cantidad de personas que hacen await expr.M() .

Ahora estoy bastante confundido, si te entiendo correctamente, no deberíamos apoyar
¿Esperas el estilo de cadena fácil posterior a la reparación porque no se usa comúnmente? ves a la espera como la parte más importante de una expresión.
Solo presumo en este caso para asegurarme de entenderte correctamente.
Si me equivoco, no dudes en corregirme.

Además, puede publicar el enlace donde obtuvo la cotización,
gracias.

Mi contraataque a los dos puntos anteriores es solo porque no usas algo comúnmente, no necesariamente significa que admitirlo sería dañino para el caso en que hace que el código sea más limpio.

a veces aguardan en la parte más importante de una expresión, si el futuro que genera la expresión es
la parte más importante y le gustaría ponerla hacia la parte superior, aún puede hacerlo si permitimos un estilo de macro postfix además del estilo de macro normal

Además, puede publicar el enlace donde obtuvo la cotización,
gracias.

Pero ... pero dijiste que habías leído todo el hilo ... 😃

Pero no tengo ningún problema en compartirlo: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886. Te sugiero que leas todas las publicaciones de Cyrus, es realmente una experiencia de todo el ecosistema de C # /. Net, es una experiencia invaluable que puede ser reutilizada por Rust.

a veces aguardan inst la parte más importante de una expresión

La cita dice claramente lo contrario 😄 Y ya sabes, yo mismo tengo la misma sensación, escribiendo async / await en el día a día.

¿Tiene alguna experiencia con async / await? ¿Puedes compartirlo entonces, por favor?

Vaya, no puedo creer que me perdí eso. Gracias por tomarse el tiempo de su día para vincular eso.
No tengo ninguna experiencia, así que supongo que, en el gran esquema, mi opinión no importa tanto.

@Pzixel Le agradezco que comparta información sobre su experiencia y la de los demás con async / await , pero sea respetuoso con los demás colaboradores. No es necesario criticar los niveles de experiencia de los demás para hacer que se escuchen sus propios puntos técnicos.

Nota del moderador: @Pzixel No se permiten ataques personales a miembros de la comunidad. Lo eliminé de tu comentario. No lo hagas de nuevo. Si tiene preguntas sobre nuestra política de moderación, comuníquese con nosotros en [email protected].

@crabtw No

Pregunté acerca de la experiencia una vez cuando quise entender si la persona tiene una necesidad real de encadenar "espera" o si es su extrapolación de las características actuales. No quería apelar a la autoridad, es solo un montón de información útil en la que puedo decir "tienes que probarlo tú mismo y darte cuenta de esta verdad por ti mismo". Nada ofensivo aquí.

No se permiten ataques personales a miembros de la comunidad. Lo eliminé de tu comentario.

Sin ataques personales. Como puedo ver, comentaste mi referencia sobre votos negativos. Bueno, fue solo mi reactor en mi voto negativo, nada especial. Como se eliminó, también es razonable eliminar esa referencia (incluso puede ser confuso para otros lectores), así que gracias por eliminarlo.

Gracias por la referencia. Quería mencionar que no debes tomar nada de lo que digo como 'evangelio' :) Rust y C # son lenguajes diferentes con comunidades, paradigmas y modismos diferentes. Definitivamente debe tomar las mejores decisiones para su idioma. Espero que mis palabras sean útiles y puedan darnos una idea. Pero siempre esté abierto a diferentes formas de hacer las cosas.

Espero que se te ocurra algo asombroso para Rust. Luego podemos ver lo que hiciste y robarlo amablemente y adoptarlo para C # :)

Por lo que puedo decir, el argumento vinculado habla principalmente sobre la precedencia de await y, en particular, sostiene que tiene sentido analizar await x.y() como await (x.y()) lugar de (await x).y() porque el usuario querrá y esperará más a menudo la interpretación anterior (y el espacio también sugiere esa interpretación). Y tendería a estar de acuerdo, aunque también observaría que una sintaxis como await!(x.y()) elimina la ambigüedad.

Sin embargo, no creo que eso sugiera una respuesta en particular con respecto al valor de encadenar como x.y().await!().z() .

El comentario citado es interesante en parte porque hay una gran diferencia en Rust, que ha sido uno de los grandes factores que han retrasado nuestro descubrimiento de la sintaxis de espera final: C # no tiene operador ? , por lo que no tienen código que tendría que escribirse (await expr)? . Describen (await expr).M() como realmente poco común, y tiendo a pensar que eso también sería cierto en Rust, pero la única excepción a eso, desde mi perspectiva, es ? , que será muy común porque muchos futuros evaluarán los resultados ( todos los que existen en este momento lo hacen, por ejemplo).

@withoutboats sí, así es. Me gustaría citar esta parte una vez más:

la única excepción a eso, desde mi perspectiva, es?

Si solo hay una excepción, entonces parece razonable crear await? foo como un atajo para (await foo)? y tener lo mejor de ambos mundos.

En este momento, al menos, la sintaxis propuesta de await!() permitirá el uso inequívoco de ? . Podemos preocuparnos por una sintaxis más corta para la combinación de await y ? si decidimos cambiar la sintaxis base por await . (Y dependiendo de lo que lo cambiamos a, puede que no tengamos un problema en absoluto.)

@joshtriplett estas llaves adicionales eliminan la ambigüedad, pero son realmente muy pesadas. Por ejemplo, buscar en mi proyecto actual:

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

Tengo 139 espera en 2743 sloc. Tal vez no sea gran cosa, pero creo que deberíamos considerar la alternativa sin corsé como una mejor y más limpia. Dicho esto, ? es la única excepción, por lo que podríamos usar fácilmente await foo sin llaves e introducir una sintaxis especial solo para este caso especial. No es gran cosa, pero podría ahorrar algunos frenos para un proyecto LISP.

Creé una publicación de blog sobre por qué creo que las funciones asíncronas deberían usar el enfoque de tipo de retorno externo para su firma. ¡Disfruta leyendo!

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

No he seguido todas las discusiones, así que no dudes en indicarme dónde ya se habría hablado de esto si me lo hubiera perdido.

Aquí hay una preocupación adicional sobre el enfoque del tipo de retorno interno: ¿cómo se vería la sintaxis para Stream s, cuando se especificará? Creo que async fn foo() -> impl Stream<Item = T> se vería bien y sería consistente con async fn foo() -> impl Future<Output = T> , pero no funcionaría con el enfoque de tipo de retorno interno. Y no creo que queramos introducir una palabra clave async_stream .

@Ekleog Stream necesitaría usar una palabra clave diferente. No puede usar async porque impl Trait funciona al revés. Solo puede garantizar que se implementen ciertos rasgos, pero los rasgos mismos deben estar ya implementados en el tipo concreto subyacente.

Sin embargo, el enfoque del tipo de retorno externo sería útil si algún día quisiéramos agregar funciones de generador asíncrono:

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

Stream podría implementarse para todos los generadores asíncronos con Return = () . Esto hace que esto sea posible:

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

Nota: Los generadores ya están disponibles por la noche, pero no usan esta sintaxis. Actualmente, tampoco son conscientes de la fijación a diferencia de Stream en futuros 0.3.

Editar: este código usó anteriormente un Generator . Perdí una diferencia entre Stream y Generator . Las transmisiones son asincrónicas. Esto significa que pueden, pero no tienen que ceder un valor. Pueden responder con Poll::Ready o Poll::Pending . Un Generator por otro lado siempre tiene que ceder o completarse sincrónicamente. Ahora lo he cambiado a AsyncGenerator para reflejar esto.

Edit2: @Ekleog La implementación actual de generadores usa una sintaxis sin marcador y parece detectar que debería ser un generador buscando un yield dentro del cuerpo. Esto significa que estaría en lo correcto al decir que async podría reutilizarse. Sin embargo, si ese enfoque es sensato es otra cuestión. Pero supongo que eso es para otro tema ^^ '

De hecho, estaba pensando que async podría reutilizarse, ¿sería solo porque async , según este RFC, solo se permitiría con Future s, y por lo tanto podría detectar está generando un Stream al observar el tipo de retorno (que debe ser Future o Stream ).

La razón por la que planteo esto ahora es porque si queremos tener la misma palabra clave async para generar tanto Future sy Stream s, entonces creo que el retorno externo El enfoque de tipo sería mucho más limpio, porque sería explícito, y no creo que nadie espere que un async fn foo() -> i32 produzca un flujo de i32 (que sería posible si el cuerpo contuviera a yield y se eligió el enfoque de tipo de retorno interno).

Podríamos tener una segunda palabra clave para generadores (por ejemplo, gen fn ), y luego crear flujos simplemente aplicando ambos (por ejemplo, async gen fn ). El tipo de retorno externo no necesita entrar en esto en absoluto.

@rpjohnst Lo

No queremos establecer dos tipos asociados. Una transmisión sigue siendo un solo tipo, no impl Iterator<Item=impl Future>> ni nada por el estilo.

@rpjohnst Me refiero a los tipos asociados Yield y Return de generadores (asíncronos)

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

Este era mi boceto original, pero creo que hablar de generadores es adelantarnos demasiado, al menos en lo que respecta al tema del seguimiento:

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

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

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

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

De manera más general, creo que deberíamos tener más experiencia con la implementación antes de revisar las decisiones tomadas en el RFC. Estamos dando vueltas alrededor de los mismos argumentos que ya hemos hecho, necesitamos experiencia con la función propuesta por el RFC para ver si se necesita una nueva ponderación.

Me gustaría estar completamente de acuerdo con usted, pero me pregunto: si leo correctamente su comentario, la estabilización de la sintaxis async / await esperará una sintaxis e implementación decentes para las transmisiones async y ganar experiencia con las dos. (ya que no sería posible cambiar entre los tipos de devolución externos y los tipos de devolución internos una vez que se estabilice)

Pensé que se esperaba async / await para Rust 2018 y no esperaría que los generadores async estuvieran listos para entonces, pero ...?

(Además, mi comentario fue pensado solo como un argumento adicional a la publicación del blog de @MajorBreakfast , sin embargo, parece haber borrado por completo la discusión sobre este tema ... ese no era en absoluto mi objetivo, y supongo que el debate debería volver a centrarse en esta publicación de blog?)

El caso de uso limitado de la palabra clave await todavía me confunde. (Esp. Futuro vs Stream vs Generador)

¿No sería suficiente una palabra clave de rendimiento para todos los casos de uso? Como en

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

Lo que mantiene el tipo de retorno explícito y, por lo tanto, solo se necesita una palabra clave para toda la semántica basada en la "continuación" sin fusionar la palabra clave y la biblioteca con demasiada fuerza.

(Hicimos esto en el idioma de la arcilla por cierto)

@aep await no genera un futuro del generador; detiene la ejecución del Future y devuelve el control al llamador.

@cramertj bueno, podría haber hecho exactamente eso aunque (devolver un futuro que contiene la continuación después de la palabra clave yield), que es un caso de uso mucho más amplio.
pero supongo que llegué un poco tarde a la fiesta para esa discusión. :)

@aep El razonamiento para un await específica de la palabra clave es para componibilidad con un futuro generador específico yield palabra clave. Queremos admitir generadores asincrónicos y eso significa dos "alcances" de continuación independientes.

Además, no puede devolver un futuro que contenga la continuación, porque los futuros de Rust se basan en encuestas, no en devoluciones de llamada, al menos parcialmente por razones de administración de memoria. Es mucho más fácil para poll mutar un solo objeto que para yield lanzar referencias a él.

Creo que async / await no debería ser una palabra clave que contamine el lenguaje en sí, porque async es solo una característica, no la interna del lenguaje.

@sackery Es parte de los

así que hazlo como palabra clave como lo hace nim, ¡c #!

Pregunta: ¿cuál debería ser la firma de los cierres sin movimiento async que capturan valores por referencia mutable? Actualmente están simplemente prohibidos. Parece que queremos algún tipo de enfoque GAT que permita que el préstamo del cierre dure hasta que el futuro esté muerto, por ejemplo:

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

@cramertj hay un problema general aquí con la devolución de referencias mutables al entorno capturado de un cierre. ¿Posiblemente la solución no necesita estar vinculada a async fn?

@withoutboats right, va a ser mucho más común en situaciones async de lo que probablemente sería en otros lugares.

¿Qué tal fn async lugar de async fn ?
Me gusta más let mut que mut let .

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

Una vez que busque pub fn , aún puede encontrar todas las funciones públicas en el código fuente.pero actualmente la sintaxis no lo es.

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

Esta propuesta no es muy importante, es una cuestión de gusto personal.
Así que respeto la opinión de todos ustedes :)

Creo que todos los modificadores deben ir antes de fn. Está claro y cómo se hace en otros idiomas. Es solo sentido común.

@Pzixel Sé que los modificadores de acceso deben ir antes de fn porque es importante.
pero creo que async probablemente no lo sea.

@xmeta No he visto esta idea propuesta antes. Probablemente queramos poner async delante de fn para ser coherentes, pero creo que es importante considerar todas las opciones. ¡Gracias por publicar!

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

@MajorBreakfast Gracias por tu respuesta, pensé así.

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

@xmeta @MajorBreakfast

async fn es indivisible, representa una función asincrónica。

async fn es un todo.

Busca pub fn , eso significa que está buscando una función de sincronización pública.
De la misma manera, busca pub async fn , eso significa que está buscando una función asincrónica pública.

@ZhangHanDong

  • async fn define una función normal que devuelve un futuro. Todas las funciones que devuelven un futuro se consideran "asincrónicas". Los punteros de función de async fn sy otras funciones que devuelven un futuro son idénticos °. Aquí hay un ejemplo de patio de recreo . Una búsqueda de "async fn" solo puede encontrar las funciones que usan la notación, no encontrará todas las funciones asincrónicas.
  • Una búsqueda de pub fn no encontrará las funciones unsafe o const .

° El tipo concreto devuelto por un async fn es, por supuesto, anónimo. Quiero decir que ambos devuelven un tipo que implementa Future

@xmeta tenga en cuenta que mut no "va tras let", o más bien, que mut no modifica let . let toma un patrón, es decir

let PATTERN = EXPRESSION;

mut es parte del PATTERN , no del let sí mismo. Por ejemplo:

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

@steveklabnik, lo entiendo. Solo quería mostrar la asociación entre la estructura jerárquica y el orden de las palabras. Gracias

¿Qué piensan las personas sobre el comportamiento deseado de return y break dentro de los bloques async ? Actualmente return regresa del bloque asincrónico; si permitimos return , esta es realmente la única opción posible. Podríamos prohibir completamente return y usar algo como 'label: async { .... break 'label x; } para regresar de un bloque asincrónico. Esto también se relaciona con la conversación sobre si usar la palabra clave break o return para la función de ruptura de bloques (https://github.com/rust-lang/rust/issues/ 48594).

Estoy a favor de permitir return . La principal preocupación para prohibirlo es que podría ser confuso porque no regresa de la función actual, sino del bloque asincrónico. Sin embargo, dudo que sea confuso. Los cierres permiten return ya y nunca lo encontré confuso. Aprender que return aplica a los bloques asíncronos es, en mi opinión, fácil y, en mi opinión, permitirlo es bastante valioso.

@cramertj return siempre debe salir de la función contenedora, nunca de un bloque interno; si no tiene sentido que eso funcione, que parece que no es así, entonces return no debería funcionar en absoluto.

Usar break para esto parece desafortunado, pero dado que desafortunadamente tenemos label-break-value, entonces al menos es consistente con eso.

¿Están todavía planeados los movimientos y cierres asíncronos? Lo siguiente es del RFC:

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

y mas abajo en la pagina

async { /* body */ }

// is equivalent to

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

lo que hace que return esté alineado con los cierres, y parece bastante fácil de entender y explicar.

¿Está planeando el rfc romper para bloquear permitir saltar de un cierre interno con una etiqueta? Si no es así (y no estoy sugiriendo que debería permitirlo), sería muy desafortunado no permitir el comportamiento consistente de returns , luego use una alternativa que también sea inconsistente con el rfc de break-to-blocks.

@memoryruins async || { ... return x; ... } absolutamente debería funcionar. Estoy diciendo que async { ... return x; ... } no debería, precisamente porque async no es un cierre. return tiene un significado muy específico: "retorno de la función contenedora". Los cierres son una función. los bloques asincrónicos no lo son.

@memoryruins Ambos ya están implementados.

@joshtriplett

los bloques asincrónicos no lo son.

Supongo que todavía pienso en ellos como funciones en el sentido de que son un cuerpo con un contexto de ejecución definido por separado del bloque que los contiene, por lo que tiene sentido para mí que return sea ​​interno al async bloque. La confusión aquí parece mayormente sintáctica, en el sentido de que los bloques suelen ser simplemente envoltorios de una expresión en lugar de cosas que llevan el código a un nuevo contexto de ejecución como || y async do.

Sin embargo, @cramertj "sintáctico" es importante.

Piensa en ello de esta manera. Si tiene algo que no parece una función (o un cierre, y está acostumbrado a reconocer los cierres como funciones) y ve un return , ¿dónde cree que va su analizador mental?

Cualquier cosa que se apropie de return hace que sea más confuso leer el código de otra persona. Las personas están al menos acostumbradas a la idea de que break regresa a algún bloque principal y tendrán que leer el contexto para saber qué bloque. return siempre ha sido el martillo más grande que regresa de toda la función.

Si no se les trata de manera similar a los cierres evaluados inmediatamente, estoy de acuerdo en que la devolución sería inconsistente, especialmente sintácticamente. Si ya se ha decidido ? en bloques asíncronos (el RFC todavía dice que estaba indeciso), entonces imagino que estaría alineado con eso.

@joshtriplett me parece arbitrario decir que puede reconocer funciones y cierres (que son sintácticamente muy diferentes) como "ámbitos de retorno", pero los bloques asíncronos no pueden reconocerse en las mismas líneas. ¿Por qué son aceptables dos formas sintácticas distintas, pero no tres?

Hubo alguna discusión previa sobre este tema en el RFC . Como dije allí, estoy a favor de los bloques asíncronos usando break _sin tener que proporcionar una etiqueta (no hay forma de salir del bloque asíncrono a un bucle externo para que no pierda expresividad).

@withoutboats Un cierre es solo otro tipo de función; una vez que aprende "un cierre es una función", puede aplicar todo lo que sabe sobre las funciones a los cierres, incluido " return siempre regresa de la función contenedora".

@ Nemo157 Incluso si tiene break sin etiquetar async , tendría que proporcionar un mecanismo (como 'label: async ) para regresar antes de un bucle dentro de un bloque asincrónico .

@joshtriplett

Un cierre es solo otro tipo de función; una vez que aprende "un cierre es una función", puede aplicar todo lo que sabe sobre funciones a los cierres, incluido "devolver siempre devuelve de la función contenedora".

Creo que los bloques async también son una especie de "función", una sin argumentos que se pueden ejecutar hasta su finalización de forma asincrónica. Son un caso especial de cierres de async que no tienen argumentos y se han aplicado previamente.

@cramertj sí, estaba asumiendo que cualquier punto de ruptura implícito también se puede etiquetar si es necesario (como creo que todos pueden

Cualquier cosa que haga que el flujo de control sea más difícil de seguir y, en particular, redefine lo que significa return , ejerce una gran presión sobre la capacidad de leer el código sin problemas.

En la misma línea, la guía estándar en C es "no escribir macros que regresen desde el medio de la macro". O, como un caso menos común pero aún problemático: si escribe una macro que parece un bucle, break y continue deberían funcionar desde dentro. He visto a personas escribir macros en bucle que en realidad incorporan dos bucles, por lo que break no funciona como se esperaba, y eso es extremadamente confuso.

Creo que los bloques asíncronos también son una especie de "función"

Creo que esa es una perspectiva basada en conocer los aspectos internos de la implementación.

No los veo como funciones en absoluto.

No los veo como funciones en absoluto.

@joshtriplett

Mi sospecha es que habría hecho el mismo argumento al llegar a un lenguaje con cierres por primera vez: que return no debería funcionar dentro del cierre, sino dentro de la función de definición. Y de hecho, hay lenguajes que toman esta interpretación, como Scala.

@cramertj No lo haría, no; para lambdas y / o funciones definidas dentro de una función, se siente completamente natural que sean una función. (Mi primera exposición a ellos fue en Python, FWIW, donde las lambdas no pueden usar return y en funciones anidadas return retorna de la función que contiene return .)

Creo que una vez que uno sabe lo que hace un bloque asincrónico, queda intuitivamente claro cómo debe comportarse return . Una vez que sepa que representa una ejecución retrasada, está claro que return no se puede aplicar a la función. Está claro que la función ya habrá regresado cuando se ejecute el bloque. En mi opinión, aprender esto no debería ser un gran desafío. Al menos deberíamos probarlo y ver.

Este RFC no propone cómo las construcciones ? -operator y control-flow como return , break y continue deberían funcionar dentro de bloques asincrónicos.

¿Sería mejor no permitir ningún operador de flujo de control o posponer bloques hasta que se escriba un RFC dedicado? Se mencionaron otras características deseadas que se discutirán más adelante. Mientras tanto, tendremos funciones asíncronas, cierres y await! :)

Estoy de acuerdo con @memoryruins aquí, creo que valdría la pena crear otro RFC para discutir esos detalles con más detalle.

¿Qué opinas de una función que nos permite acceder al contexto desde dentro de una fn asíncrona, quizás llamada core::task::context() ? Simplemente entraría en pánico si se llamara desde fuera de una fn asincrónica. Creo que sería muy útil, por ejemplo, para acceder al ejecutor para generar algo.

@MajorBreakfast esa función se llama lazy

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

Para algo más específico, como el desove, es probable que haya funciones auxiliares que lo hagan más ergonómico.

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

@ Nemo157 En realidad, spawn_with_handle es donde me gustaría usar esto. Al convertir el código a 0.3, noté que spawn_with_handle es en realidad solo un futuro porque necesita acceso al contexto ( ver código ). Lo que me gustaría hacer es agregar un método spawn_with_handle a ContextExt y hacer que spawn_with_handle una función gratuita que solo funcione dentro de funciones asíncronas:

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

Esto eliminaría todas las tonterías de doble espera que tenemos actualmente.

Ahora que lo pienso, el método debería llamarse core::task::with_current_context() y funcionar de forma un poco diferente porque debe ser imposible almacenar una referencia.

Editar: esta función ya existe con el nombre get_task_cx . Actualmente se encuentra en libstd por razones técnicas. Propongo convertirlo en API pública una vez que se pueda poner en libcore.

Dudo que sea posible tener una función que se pueda llamar desde una función que no sea async que pueda brindarle el contexto de alguna función principal async una vez que se haya movido fuera de TLS. En ese punto, el contexto probablemente se tratará como una variable local oculta dentro de la función async , por lo que podría tener una macro que acceda al contexto directamente en esa función, pero no habría forma de tener spawn_with_handle mágicamente saca el contexto de su interlocutor.

Entonces, potencialmente algo como

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

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

@ Nemo157 Creo que tienes razón: es probable que una función como la que propongo no funcione si no se llama directamente desde dentro de async fn. Quizás la mejor manera es hacer de spawn_with_handle una macro que use await internamente (como select! y join! ):

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

Esto se ve bien y se puede implementar fácilmente a través de await!(lazy(|ctx| { ... })) dentro de la macro.

async_context!() es problemático porque no puede evitar que almacene la referencia de contexto en los puntos de espera.

async_context!() es problemático porque no puede evitar que almacene la referencia de contexto en los puntos de espera.

Dependiendo de la implementación, puede. Si se resucitaran los argumentos del generador completo, sería necesario limitarlos de modo que no se pueda mantener una referencia en el punto de rendimiento de todos modos, el valor detrás del argumento tendría una vida útil que solo se ejecuta hasta el punto de rendimiento. Async / await simplemente heredaría esa limitación.

@ Nemo157 ¿ Te refieres a algo como esto?

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

@Pzixel Lamento despertar una _posiblemente_ vieja discusión, pero me gustaría agregar mis pensamientos.

Sí, me gusta que la sintaxis await!() elimina la ambigüedad al combinarla con cosas como ? , pero también estoy de acuerdo en que esta sintaxis es molesta de escribir mil veces en un solo proyecto. También creo que es ruidoso y un código limpio es importante.

Es por eso que me pregunto cuál es el argumento real contra un símbolo con sufijo (que se ha mencionado varias veces antes), como something_async()@ comparación con algo con await , tal vez porque await es una palabra clave conocida en otros idiomas? El @ podría ser divertido, ya que se asemeja a un a de await, pero puede ser cualquier símbolo que encaje bien.

Yo diría que tal elección de sintaxis sería lógica, ya que algo similar sucedió con try!() que básicamente se convirtió en un sufijo ? (sé que esto no es exactamente lo mismo). Es conciso, fácil de recordar y fácil de escribir.

Otra cosa asombrosa acerca de tal sintaxis es que el comportamiento es inmediatamente claro cuando se combina con el símbolo ? (al menos creo que lo sería). Eche un vistazo a lo siguiente:

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

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

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

Esto no tiene el problema como lo hace await future? cuando no está claro a primera vista lo que sucederá a menos que sepa sobre tal situación. Y, sin embargo, su implementación es consistente con ? .

Ahora, hay algunas cosas _ menores_ en las que puedo pensar que contrarrestarían esta idea:

  • tal vez sea _demasiado_ conciso y menos visible / detallado a diferencia de algo con await , lo que hace que sea _difícil_ detectar puntos de suspensión en una función.
  • tal vez sea asimétrico con la palabra clave async , donde una es una palabra clave y la otra un símbolo. Aunque, await!() sufre el mismo problema que es un keywore versus una macro.
  • elegir un símbolo agrega otro elemento sintáctico y algo que aprender. Pero, asumiendo que esto podría convertirse en algo de uso común, no creo que esto sea un problema.

@phaux también mencionó el uso del símbolo ~ . Sin embargo, creo que este personaje es divertido de escribir en bastantes diseños de teclado, por lo que recomendaría dejar esa idea.

¿Qué piensan chicos? ¿Estás de acuerdo en que de alguna manera es similar a cómo try!() _se convirtió_ ? ? ¿Prefieres await o un símbolo, y por qué? ¿Estoy loco por discutir esto, o tal vez me estoy perdiendo algo?

Perdón por la terminología incorrecta que pueda haber usado.

La mayor preocupación que tengo con la sintaxis basada en sigilos es que se puede convertir fácilmente en una sopa de glifos, como amablemente demostró. Cualquiera que esté familiarizado con Perl (pre-6) entenderá a dónde voy con esto. Evitar el ruido de la línea es la mejor forma de avanzar.

Dicho esto, tal vez la mejor manera de hacerlo sea exactamente como con try! ? Es decir, comience con una macro explícita async!(foo) y, si surge la necesidad, agregue algún sigilo que sería azúcar para ella. Claro, eso es posponer el problema para más adelante, pero async!(foo) es suficiente para una primera iteración de async / await, con la ventaja de ser relativamente poco controvertido. (y de tener el precedente de try! / ? debería surgir de un sigilo)

@withoutboats No he leído este hilo completo, pero ¿alguien está ayudando con la implementación? ¿Dónde está tu rama de desarrollo?

Y, con respecto a las consideraciones restantes sin resolver, ¿alguien ha pedido ayuda a expertos fuera de la comunidad de Rust? Joe Duffy sabe y se preocupa mucho por la concurrencia y comprende los detalles complicados bastante bien , y ha dado una charla en RustConf , por lo que sospecho que puede estar dispuesto a recibir una solicitud de orientación, si tal solicitud está justificada.

@BatmanAoD se aterrizó una implementación inicial en https://github.com/rust-lang/rust/pull/51580

El hilo de RFC original tuvo comentarios de varios expertos en el espacio PLT, incluso fuera de Rust :)

Me gustaría sugerir el símbolo '$' para esperar futuros, porque el tiempo es dinero, y quiero recordarle esto al compilador.

Solo bromeaba. No creo que tener un símbolo de espera sea una buena idea. Rust se trata de ser explícito y permitir que las personas escriban código de bajo nivel en un lenguaje poderoso que no te permita dispararte en el pie. Un símbolo es mucho más vago que una macro await! , y permite que las personas se disparen en el pie de una manera diferente escribiendo un código difícil de leer. Ya diría que ? es un paso demasiado lejos.

Tampoco estoy de acuerdo con que haya una palabra clave async que se utilizará en la forma async fn . Implica algún tipo de "sesgo" hacia async. ¿Por qué async merece una palabra clave? El código asincrónico es solo otra abstracción para nosotros que no siempre es necesaria. Creo que un atributo async actúa más como una "extensión" del dialecto base de Rust que nos permite escribir código más poderoso.

No soy un arquitecto de idiomas, pero tengo un poco de experiencia escribiendo código asíncrono con Promises en JavaScript, y creo que la forma en que se hace allí hace que escribir código asíncrono sea un placer.

@steveklabnik Ah, está bien, gracias. ¿Podemos (/ debería yo) actualizar la descripción del problema? ¿Quizás el ítem de viñeta "implementación inicial" debería dividirse en "implementación sin move soporte" e "implementación completa"?

¿Se está trabajando en la próxima iteración de implementación en alguna bifurcación / rama pública? ¿O no puede continuar hasta que se acepte la RFC 2418?

¿Por qué el problema de sintaxis async / await se discute aquí en lugar de en un RFC?

@ c-edw Creo que la pregunta sobre la palabra clave async responde ¿De qué color es su función?

@parasyte Se me ha sugerido que esa publicación es, de hecho, un argumento en contra de la idea completa de las funciones asíncronas sin la concurrencia de estilo de hilo verde administrada automáticamente.

No estoy de acuerdo con esta posición, porque los subprocesos verdes no se pueden implementar de forma transparente sin un tiempo de ejecución (administrado), y hay una buena razón para que Rust admita código asincrónico sin requerirlo.

Pero parece que su lectura de la publicación es que la semántica async / await está bien, pero ¿hay una conclusión que extraer sobre la palabra clave? ¿Le importaría ampliar eso?

También estoy de acuerdo con su punto de vista. Estaba comentando que la palabra clave async es necesaria, y el artículo expone el razonamiento detrás de ella. Las conclusiones extraídas por el autor son un asunto diferente.

@parasyte Ah, está bien. Me alegro de haber preguntado, debido a la aversión del autor a las dicotomías rojo / azul, pensé que estabas diciendo lo contrario.

Me gustaría aclarar más, ya que siento que no le hice justicia.

La dicotomía es ineludible . Algunos proyectos han intentado borrarlo haciendo que todas las llamadas a funciones sean asincrónicas, lo que obliga a que las llamadas a funciones de sincronización no existan. Midori es un ejemplo obvio. Y otros proyectos han intentado borrar la dicotomía ocultando las funciones asíncronas detrás de la fachada de las funciones de sincronización. gevent es un ejemplo de este tipo.

Ambos tienen el mismo problema; ¡todavía necesitan la dicotomía para distinguir entre esperar a que se complete una tarea asincrónica y

  • Midori introdujo no solo la palabra clave await , sino también una palabra clave async en el sitio de llamadas a funciones .
  • gevent proporciona gevent.spawn además de la espera implícita de las llamadas a funciones de aspecto normal.

Esa fue la razón por la que mencioné el artículo sobre el color de una función, ya que responde a la pregunta: "¿Por qué async merece una palabra clave?"

Bueno, incluso el código síncrono basado en subprocesos puede distinguir "esperar a que se complete una tarea" (unirse) y "iniciar una tarea" (generar). Podrías imaginar un lenguaje donde todo es asincrónico (en cuanto a implementación), pero no hay una anotación en await (porque es el comportamiento predeterminado), y el async Midori es, en cambio, un cierre pasado a un spawn API. Esto coloca a todo-asíncrono exactamente en la misma base sintáctica / color de función que todo-sincronizado.

Entonces, aunque estoy de acuerdo en que async merece una palabra clave, me parece que eso se debe más a que Rust se preocupa por el mecanismo de implementación en este nivel y, por esa razón, necesita proporcionar ambos colores.

@rpjohnst Sí, he leído sus propuestas. Es conceptualmente lo mismo que ocultar los colores a la gevent. Lo cual critiqué en el foro rust en el mismo hilo; cada llamada de función parece sincrónica, lo que es un peligro particular cuando una función es sincrónica y bloqueada en una canalización asincrónica. Este tipo de error es impredecible y es un verdadero desastre solucionarlo.

No hablo de mi propuesta en particular, hablo de un lenguaje donde todo es asincrónico. Se puede escapar de la dicotomía que manera- mi propuesta no trata de eso.

IIUC eso es exactamente lo que intentó Midori. En ese punto, palabras clave vs cierres es solo una discusión semántica.

El jueves 12 de julio de 2018 a las 3:01 p.m., Russell Johnston [email protected]
escribió:

Usó la presencia de palabras clave como argumento de por qué la dicotomía
todavía existe en Midori. Si los elimina, ¿dónde está la dicotomía? El
la sintaxis es idéntica al código de sincronización total, pero con las capacidades de async
código.

Porque cuando llamas a una función asíncrona sin esperar su resultado,
devuelve sincrónicamente una promesa. Que se puede esperar más tarde. 😐

_Vaya, ¿alguien sabe algo sobre Midori? Siempre pensé que es un proyecto cerrado en el que casi ninguna criatura viva trabaja en él. Sería interesante si alguno de ustedes hubiera escrito sobre esto con más detalles.

/sin relación

@Pzixel Ninguna criatura viviente todavía está trabajando en él, porque el proyecto se cerró. Pero el blog de Joe Duffy tiene muchos detalles interesantes. Vea mis enlaces arriba.

Nos hemos descarrilado aquí y siento que me estoy repitiendo, pero eso es parte de "la presencia de palabras clave": la palabra clave await . Si reemplaza las palabras clave con API como spawn y join , puede ser completamente asincrónico (como Midori) pero sin dicotomía (a diferencia de Midori).

O en otras palabras, como dije antes, no es fundamental, solo lo tenemos porque queremos la opción.

@CyrusNajmabadi lamento haberte aquí hay información adicional sobre la toma de decisiones.

Si no quieres que te mencionen de nuevo, dímelo, por favor. Simplemente pensé que podría estar interesado.

Desde el canal de discordia # wg-net :

@cramertj
alimento para el pensamiento: a menudo escribo Ok::<(), MyErrorType>(()) al final de los bloques async { ... } . ¿Quizás hay algo que podamos pensar para facilitar la restricción del tipo de error?

@sin barcos
[...] posiblemente queremos que sea coherente con [ try ]?

(Recuerdo una discusión relativamente reciente sobre cómo los bloques try podrían declarar su tipo de retorno, pero no puedo encontrarlo ahora ...)

matices mencionados:

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

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

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

Una cosa que puede hacer try y que es menos ergonómica con async es usar un enlace variable o una adscripción de tipo, p. Ej.

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

Anteriormente había lanzado la idea de permitir una sintaxis similar a fn para el rasgo Future , por ejemplo, Future -> io::Result<()> . Eso haría que la opción de suministro de tipo manual se vea un poco mejor, aunque todavía tiene muchos caracteres:

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

sería mi elección.

Es similar a la sintaxis de cierre existente:

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

Editar: Y eventualmente cuando sea posible que TryFuture implemente Future :

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

Edición 2: Para ser precisos, lo anterior funcionaría con las definiciones de rasgos actuales. Es solo que un tipo TryFuture no es tan útil hoy porque actualmente no implementa Future

@MajorBreakfast ¿ -> impl Future<Output = io::Result<()>> lugar de -> io::Result<()> ? Ya hacemos el tipo de devolución de azúcar por async fn foo() -> io::Result<()> , por lo que en mi opinión, si usamos una sintaxis basada en -> , parece claro que querríamos el mismo azúcar aquí.

@cramertj Sí, debería ser coherente. Mi publicación anterior asume que puedo convencerlos a todos de que el enfoque del tipo de retorno externo es superior 😁

En caso de que vayamos con async -> R { .. } entonces presumiblemente también deberíamos ir con try -> R { .. } además de usar expr -> TheType en general para la adscripción de tipo. En otras palabras, la sintaxis de adscripción de tipo que usamos debe aplicarse uniformemente en todas partes.

@Centril estoy de acuerdo. Debería ser utilizable en todas partes. Ya no estoy seguro de si -> es realmente la elección correcta. Asocio -> con ser invocable. Y los bloques asincrónicos no lo son.

@MajorBreakfast Básicamente estoy de acuerdo; Creo que deberíamos usar : para la asignación de tipo, entonces async : Type { .. } , try : Type { .. } y expr : Type . Hemos discutido las posibles ambigüedades en Discord y creo que encontramos un camino a seguir con : que tiene sentido ...

Otra pregunta es acerca de Either enum. Ya tenemos Either en futures caja. También es confuso porque se ve como Either de either caja cuando no lo es.

Debido a que Futures parece estar fusionado en std (al menos partes muy básicas), ¿podríamos incluir también Either allí? Es crucial tenerlos para poder devolver impl Future de la función.

Por ejemplo, a menudo escribo código como el siguiente:

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

Me gustaría poder escribirlo así:

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

Pero según tengo entendido, cuando se expande async se requiere que se inserte aquí Either , porque o entramos en condición o no.

_Puede encontrar el código real aquí si lo desea.

@Pzixel no necesitará Either dentro de async funciones, siempre y cuando await los futuros, entonces la transformación de código que async hace ocultará esos dos tipos internamente y presentan un solo tipo de retorno al compilador.

@Pzixel Además, (personalmente) espero que Either no se presente con este RFC, porque eso sería introducir una versión restringida de https://github.com/rust-lang/rfcs/issues / 2414 (que funciona solo con 2 tipos y solo con Future s), por lo que es probable que agregue API cruft si alguna vez se fusiona una solución general, y como @ Nemo157 mencionó, no parece ser una emergencia para tienes Either ahora mismo :)

@Ekleog seguro, me golpeó la idea de que en realidad tengo toneladas de either en mi código asíncrono existente y realmente me gustaría deshacerme de ellos. Luego recordé mi confusión cuando pasé ~ media hora hasta que me di cuenta de que no se compila porque tenía either crate en dependencias (los errores futuros son bastante difíciles de entender, por lo que tomó bastante tiempo). Por eso escribí el comentario, solo para asegurarme de que este problema se resuelva de alguna manera.

Por supuesto, esto no está relacionado solo con async/await , es algo más genérico, por lo que merece su propio RFC. Solo quería enfatizar que futures debería saber acerca de either o viceversa (para implementar IntoFuture correctamente).

@Pzixel El Either exportado por la caja de futuros es una reexportación de la caja either . El futures crate 0.3 no puede implementar Future por Either debido a las reglas huérfanas. Es muy probable que también eliminemos las impls Stream y Sink de Either por coherencia y ofrezcamos una alternativa en su lugar (discutida aquí ). Además, la caja either podría implementar Future , Stream y Sink , probablemente bajo una marca de función.

Dicho esto, como ya mencionó @ Nemo157 , cuando se trabaja con futuros, es mejor usar funciones asíncronas en lugar de Either .

Las cosas async : Type { .. } ahora se proponen en https://github.com/rust-lang/rfcs/pull/2522.

¿Las funciones async / await implementan automáticamente Send ya implementadas?

Parece que la siguiente función asíncrona no es (¿todavía?) Send :

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

    await!(chan.next());
}

El enlace al reproductor completo (que no se compila en el patio de recreo por falta de futures-0.3 , supongo) está aquí .

Además, al investigar este problema, encontré https://github.com/rust-lang/rust/issues/53249, que supongo que debería agregarse a la lista de seguimiento de la publicación más alta :)

Aquí hay un patio de recreo que muestra que las funciones asíncronas / en espera que implementan Send _should_ funcionan. Descomentar la versión Rc detecta correctamente que la función no es Send . Puedo echar un vistazo a su ejemplo específico en un momento (no hay compilador Rust en esta máquina: mild_frowning_face :) para tratar de averiguar por qué no funciona.

@Ekleog std::mpsc::Receiver no es Sync , y el async fn que escribiste tiene una referencia a él. Las referencias a !Sync elementos son !Send .

@cramertj Hmm… pero, ¿no tengo un mpsc::Receiver propiedad, que debería ser Send si su tipo genérico es Send ? (además, no es un std::mpsc::Receiver sino un futures::channel::mpsc::Receiver , que también es Sync si el tipo es Send , perdón por no notar el mpsc::Receiver alias era ambiguo!)

@ Nemo157 ¡Gracias! Abrí https://github.com/rust-lang/rust/issues/53259 para evitar demasiado ruido en este tema :)

La cuestión de si y cómo los bloques async permiten ? y otro flujo de control podría justificar alguna interacción con los bloques try (por ejemplo, try async { .. } para permitir ? sin una confusión similar a return ?).

Esto significa que el mecanismo para especificar un tipo de bloque async puede necesitar interactuar con el mecanismo para especificar un tipo de bloque try . Dejé un comentario sobre la sintaxis de adscripción RFC: https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Simplemente haga clic en lo que al principio pensé que era un problema de futures-rs , pero resulta que en realidad puede ser un problema asincrónico / en espera, así que aquí está: https://github.com/rust-lang-nursery/ futuros-rs / issues / 1199 # issuecomment -413089012

Como se discutió hace unos días sobre la discordia, await aún no se ha reservado como palabra clave. Es bastante importante obtener esta reserva (y agregarla a la lint de palabras clave de la edición 2018) antes del lanzamiento de 2018. Es una reserva un poco complicada ya que queremos seguir usando la sintaxis de macros por ahora.

¿La API Future / Task tendrá una forma de generar futuros locales?
Veo que hay SpawnLocalObjError , pero parece que no se utiliza.

@panicbit En el grupo de trabajo que estamos discutiendo actualmente si tiene sentido incluir la funcionalidad de generación en el contexto. https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError no está completamente sin usar: LocalPool de la caja de futuros lo usa. Sin embargo, tiene razón en que nada en libcore lo usa)

@withoutboats Noté que algunos de los enlaces en la descripción del problema están desactualizados. Específicamente, https://github.com/rust-lang/rfcs/pull/2418 está cerrado y https://github.com/rust-lang-nursery/futures-rs/issues/1199 se ha movido a https: / /github.com/rust-lang/rust/issues/53548

NÓTESE BIEN. El nombre de este problema de seguimiento es async / await, ¡pero también está asignado a la API de tareas! La API de tareas tiene actualmente pendiente una RFC de estabilización: https://github.com/rust-lang/rfcs/pull/2592

¿Alguna posibilidad de hacer que las palabras clave sean reutilizables para implementaciones asíncronas alternativas? actualmente crea un futuro, pero es una especie de oportunidad perdida para hacer que la sincronización basada en push sea más utilizable.

@aep Es posible convertir fácilmente de un sistema basado en push al sistema Future basado en pull usando oneshot::channel .

Como ejemplo, JavaScript Promises se basa en push, por lo que stdweb usa oneshot::channel para convertir JavaScript Promises en Rust Futures . También usa oneshot::channel para algunas otras API de devolución de llamada basadas en push, como setTimeout .

Debido al modelo de memoria de Rust, los futuros basados ​​en push tienen costos de rendimiento adicionales en comparación con pull . Por lo tanto, es mejor pagar ese costo de rendimiento solo cuando sea necesario (por ejemplo, usando oneshot::channel ), en lugar de tener todo el sistema Future basado en push.

Habiendo dicho eso, no soy parte de los equipos core o lang, así que nada de lo que digo tiene autoridad. Es solo mi opinión personal.

en realidad es al revés en el código con recursos limitados. los modelos de extracción tienen una penalización porque necesita el recurso dentro de la cosa que se está extrayendo en lugar de alimentar el siguiente valor listo a través de una pila de funciones en espera. El diseño de futures.rs es simplemente demasiado caro para cualquier cosa cercana al hardware, como conmutadores de red (mi caso de uso) o renderizadores de juegos.

Sin embargo, en este caso, todo lo que necesitaríamos aquí es hacer que async emita algo como lo hace Generator. Como dije antes, creo que async y generators son en realidad lo mismo si lo abstrae lo suficiente en lugar de vincular dos palabras clave a una sola biblioteca.

Sin embargo, en este caso, todo lo que necesitaríamos aquí es hacer que async emita algo como lo hace Generator.

async en este punto es literalmente una envoltura mínima alrededor de un literal de generador. Tengo dificultades para ver cómo los generadores ayudan con la E / S asíncrona basada en push, ¿no necesitas una transformación CPS para esos?

¿Podría ser más específico sobre lo que quiere decir con "necesita los recursos dentro de la cosa que está siendo extraída"? No estoy seguro de por qué necesitaría eso, o cómo "alimentar el próximo valor listo a través de una pila de funciones en espera" es diferente de poll() .

Tenía la impresión de que los futuros basados ​​en push eran más caros (y, por lo tanto, más difíciles de usar en entornos restringidos). Permitir que se adjunten devoluciones de llamada arbitrarias a un futuro requiere alguna forma de direccionamiento indirecto, generalmente asignación de montón, por lo que en lugar de asignar una vez con el futuro raíz, asigna en cada combinador. Y la cancelación también se vuelve más compleja debido a problemas de seguridad de subprocesos, por lo que no la admite o requiere que todas las devoluciones de llamada utilicen operaciones atómicas para evitar carreras. Todo eso se suma a un marco mucho más difícil de optimizar, por lo que puedo decir.

¿No prefieres una transformación CPS para esos?

sí, la sintaxis del generador actual no funciona para eso porque no tiene argumentos para la continuación, por lo que esperaba que async ofreciera formas de hacerlo.

¿Necesita los recursos dentro de la cosa que está siendo extraída?

esa es mi forma terrible de decir que invertir el orden async funciona dos veces tiene un costo. Es decir, una vez del hardware a los futuros y viceversa utilizando canales. Necesita llevar un montón de cosas que no tienen beneficios en el código cercano al hardware.

Un ejemplo común sería que no puede simplemente invocar la pila futura cuando sabe que un descriptor de archivo de un socket está listo, sino que debe implementar toda la lógica de ejecución para asignar eventos del mundo real a futuros, lo que tiene un costo externo como el bloqueo, tamaño del código y, lo más importante, complejidad del código.

Permitir que se adjunten devoluciones de llamada arbitrarias a un futuro requiere alguna forma de indirección

sí, entiendo que las devoluciones de llamada son costosas en algunos entornos (no en el mío, donde la velocidad de ejecución es irrelevante, pero tengo 1 MB de memoria total, por lo que futures.rs ni siquiera cabe en flash), sin embargo, no necesita envío dinámico en absoluto cuando lo tiene algo así como continuaciones (que el concepto de generador actual medio implementa).

Y la cancelación también se vuelve más compleja debido a la seguridad de los subprocesos.

Creo que estamos confundiendo las cosas aquí. No estoy abogando por devoluciones de llamada. Las continuaciones pueden ser pilas estáticas muy bien. Por ejemplo, lo que implementamos en el lenguaje de la arcilla es solo un patrón generador que puede usar para empujar o tirar. es decir:

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

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

Supongo que puedo seguir haciendo eso con una macro, pero siento que es una oportunidad perdida aquí desperdiciar una palabra clave de lenguaje en un concepto específico.

no en el mío, donde la velocidad de ejecución es irrelevante, pero tengo 1 MB de memoria total, por lo que futures.rs ni siquiera cabe en flash

Estoy bastante seguro de que los futuros actuales están destinados a ejecutarse en entornos con limitaciones de memoria. ¿Qué es exactamente lo que ocupa tanto espacio?

Editar: este programa ocupa 295 KB de espacio en el disco cuando se compila; lanzamiento en mi macbook (el hola mundo básico ocupa 273 KB):

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

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

no en el mío, donde la velocidad de ejecución es irrelevante, pero tengo 1 MB de memoria total, por lo que futures.rs ni siquiera cabe en flash

Estoy bastante seguro de que los futuros actuales están destinados a ejecutarse en entornos con limitaciones de memoria. ¿Qué es exactamente lo que ocupa tanto espacio?

Además, ¿a qué te refieres con memoria? He ejecutado código actual basado en async / await en dispositivos con 128 kB flash / 16 kB ram. Definitivamente hay problemas de uso de memoria con async / await actualmente, pero en su mayoría son problemas de implementación y se pueden mejorar agregando algunas optimizaciones adicionales (por ejemplo, https://github.com/rust-lang/rust/issues/52924).

Un ejemplo común sería que no puede simplemente invocar la pila futura cuando sabe que un descriptor de archivo de un socket está listo, sino que debe implementar toda la lógica de ejecución para asignar eventos del mundo real a futuros, lo que tiene un costo externo como el bloqueo, tamaño del código y, lo más importante, complejidad del código.

¿Por qué? Esto todavía no parece nada a lo que el futuro te obligue a hacer. Puede llamar a poll tan fácilmente como lo haría con un mecanismo basado en push.

Además, ¿a qué te refieres con memoria?

No creo que esto sea relevante. Toda esta discusión se ha detallado para invalidar un punto que ni siquiera tenía la intención de hacer. No estoy aquí para criticar el futuro más allá de decir que atornillar su diseño al lenguaje central es un error.

Mi punto es que la palabra clave async se puede hacer a prueba de futuro si se hace correctamente. Continuaciones es lo que quiero, pero tal vez a otras personas se les ocurran ideas aún mejores.

Puede llamar a la encuesta con la misma facilidad que lo haría con un mecanismo basado en push.

Sí, eso tendría sentido si Future: poll tuviera argumentos de llamada. No puede tenerlos porque la encuesta debe ser abstracta. En su lugar, propongo emitir una continuación de la palabra clave async e impl Future para cualquier continuación con cero argumentos.

Es un cambio simple y de bajo esfuerzo que no agrega ningún costo a los futuros, pero permite la reutilización de palabras clave que actualmente son exclusivamente para una biblioteca.

Pero, por supuesto, las continuaciones también se pueden implementar con un preprocesador, que es lo que vamos a hacer. Desafortunadamente, el desugar solo puede ser un cierre, que es más caro que una continuación adecuada.

@aep ¿Cómo haríamos posible reutilizar las palabras clave ( async y await )?

@Centril, mi solución rápida ingenua sería reducir un async a un generador, no a un futuro. Eso dará tiempo para hacer que el generador sea útil para continuaciones adecuadas en lugar de ser un backend exclusivo para futuros.

Es como un PR de 10 líneas tal vez. Pero no tengo la paciencia para luchar contra una colmena por eso, así que simplemente construiré un proceso previo para desugar una palabra clave diferente.

No he estado siguiendo las cosas async, así que disculpas si esto se ha discutido antes / en otro lugar, pero ¿cuál es el plan (de implementación) para admitir async / await en no_std ?

AFAICT, la implementación actual usa TLS para pasar un Waker, pero no hay soporte TLS (o hilo) en no_std / core . Escuché de @alexcrichton que podría ser posible deshacerse de TLS si / cuando Generator.resume obtenga soporte para argumentos.

¿Se está implementando el plan para bloquear la estabilización de async / await en el soporte de no_std ? ¿O estamos seguros de que se puede agregar no_std soporte sin cambiar ninguna de las piezas que se estabilizarán para enviar std async / await en estable?

@japaric poll ahora toma el contexto explícitamente. AFAIK, TLS ya no debería ser necesario.

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

Editar: no es relevante para async / await, solo para futuros.

[...] ¿estamos seguros de que se puede agregar el soporte de no_std sin cambiar ninguna de las piezas que se estabilizarán para enviar std async / await en estable?

Eso creo. Las piezas relevantes son las funciones en std::future , todas ellas están ocultas detrás de una característica inestable adicional gen_future que nunca se estabilizará. La transformación async usa set_task_waker para almacenar el waker en TLS y luego await! usa poll_with_tls_waker para acceder a él. Si los generadores obtienen soporte para el argumento de reanudación, entonces la transformación async puede pasar el despertador como argumento de reanudación y await! puede leerlo fuera del argumento.

EDITAR: Incluso sin argumentos del generador, creo que esto también podría hacerse con un código un poco más complicado en la transformación asíncrona. Personalmente, me gustaría ver los argumentos del generador agregados para otros casos de uso, pero estoy bastante seguro de que será posible eliminar el requisito de TLS con / sin ellos.

@japaric Mismo barco. Incluso si alguien hizo que los futuros funcionen en sistemas integrados, es muy arriesgado ya que es todo Tier3.

Descubrí un truco feo que requiere mucho menos trabajo que arreglar async: weave in an Arca través de una pila de generadores.

  1. ver el argumento "Encuesta" https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 es un arco
  2. registrando algo en la encuesta en la línea 87
  3. ceda para generar un punto de continuación en la línea 92
  4. llamar a un generador desde un generador para crear una pila de nivel superior en la línea 207
  5. finalmente ejecutando toda la pila pasando un tiempo de ejecución en la línea 215

Idealmente, simplemente reducirían el async a una pila de cierre "pura" en lugar de un futuro, por lo que no necesitaría ninguna suposición de tiempo de ejecución y luego podría insertar el entorno impuro como un argumento en la raíz.

Estaba a medio camino de implementar eso

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

pero es un poco inútil ir hasta el final si soy el único que lo quiere.

Y no podía dejar de pensar en si es posible la asincronización / espera sin TLS sin argumentos del generador, así que implementé un par de macros no_std proc-macro basado en async_block! / await! usando solo variables locales.

Definitivamente requiere garantías de seguridad mucho más sutiles que la solución actual basada en TLS o una solución basada en argumentos del generador (al menos cuando asume que los argumentos del generador subyacentes son sólidos), pero estoy bastante seguro de que es sólido (siempre que nadie usa el agujero de higiene bastante grande que no pude encontrar, esto no sería un problema para una implementación en el compilador, ya que puede usar identificadores gensym innombrables para comunicarse entre la transformación asíncrona y la macro de espera).

Me acabo de dar cuenta de que no se menciona mover await! de std a core en el OP, tal vez # 56767 podría agregarse a la lista de problemas para resolver antes de la estabilización para rastrear esta.

@ Nemo157 Como no se espera que await! se estabilice, no es un bloqueador de todos modos.

@Centril No sé quién te dijo que await! no se espera que se estabilice ...: guiño:

@cramertj Se refería a la versión macro, no a la versión de palabra clave, creo ...

@ crlf0710 ¿qué pasa con la versión implícita de espera / explícita async-block ?

@ crlf0710 Yo

@cramertj ¿No queremos eliminar la macro porque actualmente hay un truco feo en el compilador que hace posible la existencia de await y await! ? Si estabilizamos la macro, nunca podremos eliminarla.

@stjepang Realmente no me importa demasiado en ninguna dirección la sintaxis de await! , aparte de una preferencia general por las notaciones postfijas y una aversión por la ambigüedad y los símbolos impronunciables / no aptos para Google. Hasta donde yo sé, las sugerencias actuales (con ? para aclarar la precedencia) son:

  • await!(x)? (lo que tenemos hoy)
  • await x? ( await enlaza más estrechamente que ? , todavía notación de prefijo, necesita métodos parens para encadenar)
  • await {x}? (igual que el anterior, pero requiere temporalmente {} para eliminar la ambigüedad)
  • await? x ( await enlaza menos estrechamente, aún con notación de prefijo, necesita métodos parens para encadenar)
  • x.await? (parece un acceso de campo)
  • x# / x~ / etc. (algún símbolo)
  • x.await!()? (postfix-macro-style, @withoutboats y creo que quizás otros no son fanáticos de postfix-macros porque esperan que . permita el envío basado en tipos, lo que no sucedería con las macros postfix )

Creo que la mejor ruta para el envío es conseguir await!(x) , un-keyword-ify await , y eventualmente algún día venderle a la gente la bondad de las macros postfix, permitiéndonos agregar x.await!() . Otras personas tienen opiniones diferentes;)

Sigo este tema muy libremente, pero aquí está mi opinión:

Personalmente, me gusta la macro await! tal como está y como se describe aquí: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

No es ningún tipo de magia o sintaxis nueva, solo una macro normal. Menos es más, después de todo.

Por otra parte, también preferí try! , ya que Try todavía no está estabilizado. Sin embargo, await!(x)? es un compromiso decente entre el azúcar y las acciones con nombre obvio, y creo que funciona bien. Además, podría ser potencialmente reemplazado por alguna otra macro en una biblioteca de terceros para manejar funciones adicionales, como el seguimiento de depuración.

Mientras tanto, async / yield es "simplemente" azúcar sintáctico para los generadores. Me recuerda los días en los que JavaScript recibía soporte asincrónico / en espera y tenías proyectos como Babel y Regenerator que transpilaban código asincrónico para usar generadores y Promesas / Futuros para operaciones asíncronas, esencialmente como lo estamos haciendo nosotros.

Tenga en cuenta que eventualmente querremos que async y generators sean características distintas, potencialmente incluso componibles entre sí (produciendo un Stream ). Dejar await! como una macro que simplemente baja a yield no es una solución permanente.

Dejando espera! como macro que simplemente reduce el rendimiento no es una solución permanente.

No puede ser visible para el usuario permanentemente si baja a yield , pero ciertamente puede continuar implementándose de esa manera. Incluso cuando tiene async + generators = Stream , aún puede usar, por ejemplo, yield Poll::Pending; frente a yield Poll::Ready(next_value) .

Tenga en cuenta que eventualmente querremos que async y generators sean características distintas

¿Async y generators no son características distintas? Relacionado, por supuesto, pero comparando esto nuevamente con cómo lo hizo JavaScript, siempre pensé que async se construiría sobre generadores; que la única diferencia al ser una función asíncrona devolvería y produciría Future s en contraposición a cualquier valor regular. Se necesitaría un ejecutor para evaluar y esperar a que se ejecute la función asíncrona. Además de algunas cosas extra de por vida, no estoy seguro.

De hecho, una vez escribí una biblioteca sobre esto exactamente, evaluando de forma recursiva tanto las funciones asíncronas como las funciones generadoras que devolvían Promises / Futures.

@cramertj No se puede implementar de esa manera si los dos son "efectos" distintos. Hay algo de discusión sobre esto aquí: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. No queremos yield Poll::Ready(next_value) , queremos yield next_value , y tener await s en otra parte de la misma función.

@rpjohnst

No queremos generar Poll :: Ready (next_value), queremos generar next_value y tenemos esperas en otra parte de la misma función.

Sí, por supuesto, eso es lo que le parecería al usuario, pero en términos de desugaring, solo tiene que envolver yield s en Poll::Ready y agregar un Poll::Pending a el yield generado a partir de await! . Sintácticamente para los usuarios finales, aparecen como características separadas, pero aún pueden compartir una implementación en el compilador.

@cramertj También este:

  • await? x

@novacrazy Sí, son características distintas, pero deberían poder componerse juntas.

Y de hecho en JavaScript son componibles:

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

“Los iteradores y generadores asíncronos son lo que obtienes cuando combinas una función asíncrona y un iterador, por lo que es como un generador asíncrono en el que puedes esperar o una función asíncrona desde la que puedes ceder”, explicó. Anteriormente, ECMAScript le permitía escribir una función en la que podía ceder o esperar, pero no ambas. "Esto es realmente conveniente para consumir transmisiones que se están convirtiendo cada vez más en parte de la plataforma web, especialmente con el objeto Fetch que expone las transmisiones".

El iterador asíncrono es similar al patrón Observable, pero más flexible. “Un Observable es un modelo de empuje; una vez que te suscribes, te bombardean con eventos y notificaciones a toda velocidad, ya sea que estés listo o no, por lo que tienes que implementar estrategias de almacenamiento en búfer o de muestreo para lidiar con la charla ”, explicó Terlson. El iterador asíncrono es un modelo push-pull: usted solicita un valor y se lo envía, lo que funciona mejor para cosas como primitivas de E / S de red.

@Centril ok, abierto # 56974, ¿es eso lo suficientemente correcto como para agregarlo como una pregunta sin resolver al OP?


Realmente no quiero volver a entrar en el ciclo de sintaxis await , pero tengo que responder al menos a un punto:

Personalmente, me gusta la macro await! tal como está y como se describe aquí: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Tenga en cuenta que también dije que no creo que la macro pueda permanecer como una macro implementada en la biblioteca (ignorando si seguirá apareciendo o no como una macro para los usuarios), para ampliar las razones:

  1. Ocultar la implementación subyacente, como dice uno de los problemas no resueltos, actualmente puede crear un generador usando || await!() .
  2. Soportar generadores asincrónicos, como @cramertj menciona, requiere diferenciar entre los yield s agregados por await y otros yield s escritos por el usuario. Esto _ podría_ hacerse como una etapa de pre-macro-expansión, _si_ los usuarios nunca quisieron yield dentro de macros, pero hay construcciones muy útiles yield -in-macro como yield_from! . Con la restricción de que yield s en macros deben ser compatibles, esto requiere que await! sea ​​una macro incorporada al menos (si no es la sintaxis real).
  3. Apoyando async fn en no_std . Conozco dos formas de implementar esto, ambas requieren async fn -created- Future y await para compartir un identificador en el que se almacena el waker. La única forma en que Puede ver que hay un identificador higiénicamente seguro compartido entre estos dos lugares si ambos están implementados en el compilador.

Creo que hay un poco de confusión aquí: nunca fue la intención que await! se expandiera públicamente de manera visible a un envoltorio alrededor de las llamadas a yield . Cualquier futuro para la sintaxis similar a una macro await! dependerá de una implementación similar a la de la actual compile_error! , assert! , format_args! etc. y podría desugar a código diferente dependiendo del contexto.

Lo único importante que hay que entender aquí es que no hay una diferencia semántica significativa entre ninguna de las sintaxis propuestas, son solo sintaxis superficial.

Escribiría una alternativa para resolver la sintaxis await .

En primer lugar, me gusta la idea de poner await como operador de sufijo. Pero expression.await se parece demasiado a un campo, como ya se señaló.

Entonces mi propuesta es expression awaited . La desventaja aquí es que awaited aún no se conserva como palabra clave, pero es más natural en inglés y, sin embargo, no existen tales expresiones (quiero decir, formas gramaticales como expression [token] ) son válidas en Rust ahora mismo, para que esto pueda justificarse.

Entonces podemos escribir expression? awaited para esperar un Result<Future,_> , y expression awaited? para esperar un Future<Item=Result<_,_>> .

@earthengine

Si bien no estoy convencido de la palabra clave awaited , creo que estás en algo.

La información clave aquí es: yield y await son como return y ? .

return x devuelve valor x , mientras que x? desenvuelve el resultado x , regresando antes si es Err .
yield x rinde valor x , mientras que x awaited espera x futuros, regresando temprano si es Pending .

Tiene una bonita simetría. Quizás await realmente debería ser un operador de sufijo.

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

No soy fanático de una sintaxis esperada de postfix por la razón exacta que @cramertj acaba de mostrar. Reduce la legibilidad general, especialmente para expresiones largas o expresiones encadenadas. No da ningún sentido de anidamiento como lo haría await! / await . No tiene la simplicidad de ? , y nos estamos quedando sin símbolos para usar en un operador de sufijo ...

Personalmente, todavía estoy a favor de await! por las razones que describí anteriormente. Se siente oxidado y serio.

Reduce la legibilidad general, especialmente para expresiones largas o expresiones encadenadas.

En los estándares Rustfmt, el ejemplo se escribirá

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

Apenas puedo ver cómo esto afecta la legibilidad.

También me gusta postfix await. Creo que usar un espacio sería inusual y tendería a romper la agrupación mental. Sin embargo, creo que .await!() emparejaría muy bien, con ? montaje, ya sea antes o después, y el ! permitiría la interacción del flujo de control.

(Eso no requiere un mecanismo de macro de postfix completamente general; el compilador podría usar el caso especial .await!() ).

Comencé con un gran desagrado por el sufijo await (sin . o () ) ya que se ve bastante extraño: la gente que viene de otros idiomas se reirá de nuestra gasto seguro. Ese es un costo que debemos tomarnos en serio. Sin embargo, x await claramente no es una llamada de función o un acceso de campo ( x.await / x.await() / await(x) todos tienen este problema) y hay menos funky cuestiones de precedencia. Esta sintaxis resolvería claramente ? y la precedencia de acceso al método, por ejemplo, foo await? y foo? await ambos tienen un orden de precedencia claro para mí, al igual que foo await?.x y foo await?.y (sin negar que se vean extraños, solo argumentando que la precedencia es clara).

yo tambien pienso eso

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

se lee mejor que

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

Considerándolo todo, apoyaría esto.

@joshtriplett RE .await!() deberíamos hablar por separado-- Inicialmente yo también estaba a favor de esto, pero no creo que debamos aterrizar esto si no podemos obtener macros postfix en general, y creo que hay una gran oposición permanente hacia ellos (con una razón bastante buena, aunque desafortunada), y realmente me gustaría que eso no bloqueara la estabilización de await .

¿Por qué no los dos?

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

Veo más el atractivo de postfix ahora, y casi me gusta más en algunos escenarios. Especialmente con el truco anterior, que es tan simple que ni siquiera es necesario que lo proporcione Rust.

Entonces, +1 para postfix.

Creo que también deberíamos tener una función de prefijo, además de la versión de sufijo.

En cuanto a los detalles de la sintaxis de sufijo, no estoy tratando de decir que .await!() es la única sintaxis de sufijo viable; Simplemente no soy fanático del postfix await con un espacio inicial.

Parece aceptable (aunque todavía inusual) cuando lo formatea con una declaración por línea, pero mucho menos razonable cuando formatea declaraciones simples en una línea.

Para aquellos que no les gustan los operadores de palabras clave postfijas, podemos definir un operador simbólico adecuado para await .

En este momento, nos quedamos sin operadores en caracteres ASCII simples para el operador de sufijo. Sin embargo, ¿qué tal

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

Si realmente necesitamos ASCII simple, se me ocurrió (inspirado en la forma de arriba)

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

o (forma simular en posición horizontal)

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

Otra idea es convertir la estructura await un corchete.

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

Todas esas soluciones ASCII comparten el mismo problema de aprobación de que <..> ya se usa demasiado y tenemos problemas de análisis con < y > . Sin embargo, >< o >=< podrían ser mejores para esto, ya que no requieren espacio dentro del operador, ni < s abiertos en la posición actual.


Para aquellos que simplemente no les gusta el espacio en el medio, pero está bien para los operadores de palabras clave postfix, ¿qué tal si usamos guiones?

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

Acerca de tener muchas formas diferentes de escribir el mismo código, no, personalmente, no me gusta. La razón principal por la que es mucho más difícil para las personas que son nuevas comprender adecuadamente cuál es la forma correcta o el punto de tenerlo. La segunda razón es que tendremos muchos proyectos diferentes que usan una sintaxis diferente y sería más difícil saltar entre ellos y leerlos (especialmente para los recién llegados al rust). Creo que se debe implementar una sintaxis diferente solo si realmente hay una diferencia y ofrece algunas ventajas. Una gran cantidad de azúcar en el código solo hace que sea mucho más difícil aprender y trabajar con el lenguaje.

@goffrie Sí, estoy de acuerdo en que no deberíamos tener muchas formas diferentes de hacer lo mismo. Sin embargo, solo estaba proponiendo diferentes alternativas, la comunidad solo necesita elegir una. Por lo tanto, esto no es realmente una preocupación.

Además, en términos de la macro await! no hay forma de evitar que el usuario invente sus propias macros para hacerlo de manera diferente, y Rust tiene la intención de habilitar esto. Por lo tanto, "tener muchas formas diferentes de hacer lo mismo" es inevitable.

Creo que esa simple macro tonta que mostré demuestra que no importa lo que hagamos, el usuario hará lo que quiera de todos modos. Una palabra clave, ya sea prefijo o sufijo, se puede convertir en una macro de prefijo de función, o presumiblemente en una macro de método de sufijo, siempre que existan. Incluso si elegimos macros de función o método para await , podrían invertirse con otra macro. Realmente no importa.

Por lo tanto, debemos centrarnos en la flexibilidad y el formato. Proporcione una solución que llene más fácilmente todas esas posibilidades.

Además, aunque en este corto tiempo me he apegado a la sintaxis de la palabra clave postfix, await debería reflejar lo que se decida para yield con generadores, que probablemente sea una palabra clave de prefijo. Para los usuarios que deseen una solución postfix, es probable que eventualmente existan macros similares a métodos.

Mi conclusión es que una palabra clave de prefijo await es la mejor sintaxis predeterminada por ahora, tal vez con una caja regular que proporciona a los usuarios una macro de función await! , y en el futuro un método de postfix similar .await!() macro.

@novacrazy

Además, aunque en este corto tiempo me he apegado a la sintaxis de la palabra clave postfix, await debería reflejar lo que se decida por yield con generadores, que probablemente sea una palabra clave de prefijo.

La expresión yield 42 está en el tipo ! , mientras que foo.await está en el tipo T donde foo: impl Future<Output = T> . @stjepang hace la analogía correcta con ? y return aquí. await no es como yield .

¿Por qué no los dos?

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

Deberá nombrar la macro de otra manera porque await debe seguir siendo una palabra clave verdadera.


Por una variedad de razones, me opongo al prefijo await y aún más a la forma de bloque await { ... } .

Primero están los problemas de precedencia con await expr? donde la precedencia consistente es await (expr?) pero usted quiere (await expr)? . Como solución a los problemas de precedencia, algunos han sugerido await? expr además de await expr . Esto implica await? como unidad y carcasa especial; eso parece injustificado, una pérdida de nuestro presupuesto de complejidad y una indicación de que await expr tiene serios problemas.

Más importante aún, el código de Rust, y en particular la biblioteca estándar, se centra en gran medida en el poder de la sintaxis de llamada de método y punto. Cuando await es el prefijo, anima al usuario a inventar enlaces temporales temporales en lugar de simplemente encadenar métodos. Esta es la razón por la que ? es posfijo, y por la misma razón, await también debería ser posfijo.

Aún peor sería await { ... } . Esta sintaxis, si se formatea consistentemente de acuerdo con rustfmt , se convertiría en:

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

Esto no sería ergonómico y aumentaría significativamente la longitud vertical de las funciones.


En cambio, creo que la espera, como ? , debería ser postfix, ya que encaja con el ecosistema de Rust que se centra en el encadenamiento de métodos. Se han mencionado varias sintaxis de sufijo; Voy a repasar algunos de ellos:

  1. foo.await!() - Esta es la solución de macro postfix . Si bien estoy firmemente a favor de las macros de postfix, estoy de acuerdo con @cramertj en https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040 que no deberíamos hacer esto a menos que también nos comprometamos a postfix macros en general. También creo que usar una macro postfix de esta manera da una sensación que no es de primera clase; deberíamos evitar hacer que una construcción de lenguaje use sintaxis de macros.

  2. foo await - Esto no es tan malo, realmente funciona como un operador postfix ( expr op ) pero siento que falta algo con este formato (es decir, se siente "vacío"); En contraste, expr? adjunta ? directamente sobre expr ; aquí no hay espacio. Esto hace que ? vea visualmente atractivo.

  3. foo.await - Esto ha sido criticado por parecer un acceso de campo; y eso es cierto. Sin embargo, debemos recordar que await es una palabra clave y, por lo tanto, se resaltará la sintaxis como tal. Si lee el código de Rust en su IDE o de manera equivalente en GitHub, await estará en un color o negrita diferente al de foo . Usando una palabra clave diferente podemos demostrar esto:

    let x = foo.match?;
    

    Habitualmente, los campos también son sustantivos, mientras que await es un verbo.

    Si bien hay un factor de burla inicial sobre foo.await , creo que se debe considerar seriamente como una sintaxis visualmente atractiva y legible.

    Como beneficio adicional, usar .await le da el poder del punto y el autocompletado que normalmente tiene el punto en los IDE (consulte la página 56). Por ejemplo, puede escribir foo. y si foo resulta ser un futuro, await se mostrará como la primera opción. Esto facilita tanto la ergonomía como la productividad del desarrollador, ya que alcanzar el punto es algo que muchos desarrolladores han entrenado en la memoria muscular.

    En todas las posibles sintaxis postfijas, a pesar de las críticas sobre su apariencia de acceso al campo, esta sigue siendo mi sintaxis favorita.

  4. foo# - Esto usa el sigilo # para esperar en foo . Creo que considerar un sigilo es una buena idea dado que ? también es un sigilo y porque hace que la espera sea liviana. Combinado con ? se vería como foo#? - eso se ve bien. Sin embargo, # no tiene una justificación específica. Más bien, es simplemente un sigilo que todavía está disponible.

  5. foo@ - Otro sigilo es @ . Cuando se combina con ? , obtenemos foo@? . Una justificación para este sello específico es que se ve a -ish ( @wait ).

  6. foo! - Finalmente, hay ! . Cuando se combina con ? , obtenemos foo!? . Desafortunadamente, esto tiene una cierta sensación de WTF. Sin embargo, ! parece forzar el valor, que se ajusta a "aguardar". Hay un inconveniente en que foo!() ya es una invocación de macro legal, por lo que esperar y llamar a una función debería escribirse (foo)!() . Usar foo! como sintaxis también nos quitaría la oportunidad de tener macros de palabras clave (por ejemplo, foo! expr ).

Otro sello único es foo~ . La onda puede entenderse como "eco" o "lleva tiempo". Sin embargo, no se usa en ninguna parte del idioma Rust.

Tilde ~ se usaba en los viejos tiempos para el tipo de montón asignado: https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

¿Se puede reutilizar ? ? ¿O es demasiada magia? ¿Cómo se vería impl Try for T: Future ?

@parasyte Sí, lo recuerdo. Pero aún así se había ido.

@jethrogb no hay forma de que pueda ver impl Try trabajando directamente, ? explícitamente return s el resultado de Try de la función actual mientras que await necesita yield .

Tal vez ? podría estar en mayúsculas especiales para hacer otra cosa en el contexto de un generador de modo que pueda yield o return dependiendo del tipo de expresión a la que se aplica , pero no estoy seguro de cuán comprensible sería. Además, ¿cómo interactuaría eso con Future<Output=Result<...>> , tendrías que let foo = bar()??; para ambos hacer la "espera" y luego obtener la variante Ok de la Result ( ¿o ? en generadores se basarían en un rasgo triestado que puede yield , return o resolver a un valor con una sola aplicación)?

Ese último comentario entre paréntesis en realidad me hace pensar que podría ser viable, haga clic para ver un boceto rápido
enum GenOp<T, U, E> { Break(T), Yield(U), Error(E) }

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

    fn into_result(self) -> GenOp<Self::Ok, Self::Yield, Self::Error>;
}
con `foo?` dentro de un generador que se expande a algo como (aunque esto tiene un problema de propiedad, y también necesita apilar el resultado de `foo`)
loop {
    match TryGen::into_result(foo) {
        GenOp::Break(val) => break val,
        GenOp::Yield(val) => yield val,
        GenOp::Return(val) => return Try::from_error(val.into()),
    }
}

Desafortunadamente, no veo cómo manejar la variable de contexto de waker en un esquema como este, tal vez si ? estuvieran en una caja especial para async lugar de generadores, pero si va a ser especial -encasillado aquí sería bueno si fuera utilizable para otros casos de uso de generadores.

Tuve el mismo pensamiento con respecto a la reutilización de ? como @jethrogb.

@ Nemo157

no hay forma de que pueda ver impl Try trabajando directamente, ? explícitamente return s el resultado de Try de la función actual mientras aguarda necesita yield .

Quizás me falten algunos detalles sobre ? y el rasgo Try , pero ¿dónde / por qué es eso explícito? ¿Y no es un return en un cierre asincrónico esencialmente lo mismo que yield todos modos, solo una transición de estado diferente?

Tal vez ? podría tener un formato especial para hacer otra cosa en el contexto de un generador de modo que pueda yield o return dependiendo del tipo de expresión a la que se aplica , pero no estoy seguro de cuán comprensible sería.

No veo por qué eso debería ser confuso. Si piensa en ? como "continuar o divergir", entonces parece natural, en mi humilde opinión. Por supuesto, sería útil cambiar el rasgo Try para usar nombres diferentes para los tipos de devolución asociados.

Además, ¿cómo interactuaría eso con Future<Output=Result<...>> , tendrías que let foo = bar()?? ;

Si desea esperar el resultado y luego salir temprano en un resultado de Error, entonces esa sería la expresión lógica, sí. No creo que un triestado especial TryGen sea ​​necesario en absoluto.

Desafortunadamente, no veo cómo manejar la variable de contexto de waker en un esquema como este, ¿tal vez si? tenían una carcasa especial para asíncronos en lugar de generadores, pero si va a tener una carcasa especial aquí, sería bueno si se pudiera usar para otros casos de uso de generadores.

No entiendo esta parte. ¿Podría darme más detalles?

@jethrogb @rolandsteiner Una estructura podría implementar tanto Try como Future . En este caso, ¿cuál debería ? desenvolver?

@jethrogb @rolandsteiner Una estructura podría implementar tanto Try como Future. En este caso, ¿cuál debería? ¿desenvolver?

No, no podría debido a la implícita Try for T: Future.

¿Por qué nadie habla de la construcción explícita y la propuesta

pero todo es solo sombreado de bicicletas, creo que deberíamos conformarnos con la sintaxis de macro simple await!(my_future) al menos por ahora

pero todo es solo sombreado de bicicletas, creo que deberíamos conformarnos con la sintaxis de macro simple await!(my_future) al menos por ahora

No, no es "solo" deshacerse de las bicicletas como si fuera algo banal e insignificante. El hecho de que await se escriba como prefijo o sufijo impacta fundamentalmente en cómo se escribe el código asíncrono wrt. encadenamiento de métodos y lo componible que se siente. La estabilización en await!(future) también implica que se abandone await como palabra clave, lo que hace imposible el uso futuro de await como palabra clave. "Al menos por ahora" sugiere que podemos encontrar una mejor sintaxis más adelante y desconoce la deuda técnica que esto conlleva. Me opongo a introducir la deuda a sabiendas por una sintaxis que debe reemplazarse más adelante.

Estabilizar en await! (Futuro) también implica que se renuncia a await como palabra clave, lo que hace imposible el uso futuro de await como palabra clave.

podríamos convertirlo en una palabra clave en la próxima época, requiriendo la sintaxis de identificación sin formato para la macro, tal como hicimos con try .

@rolandsteiner

¿Y no es un return en un cierre asíncrono esencialmente lo mismo que yield todos modos, solo una transición de estado diferente?

yield no existe en un cierre asíncrono, es una operación introducida durante la reducción de la sintaxis async / await a generators / yield . En la sintaxis del generador actual, yield es bastante diferente a return , si la expansión ? se realiza antes de la transformación del generador, entonces no sé cómo sabría cuándo insertar un return o un yield .

Si desea esperar el resultado y no salir antes de un resultado de Error, entonces esa sería la expresión lógica, sí.

Puede ser lógico, pero me parece una desventaja que muchos (¿la mayoría?) De los casos en los que escribe funciones asíncronas se completarán con el doble de ?? para solucionar los errores de E / S.

Desafortunadamente, no veo cómo manejar la variable de contexto de waker ...

No entiendo esta parte. ¿Podría darme más detalles?

La transformación asíncrona toma una variable de activación en la función Future::poll generada, que luego debe pasarse a la operación de espera transformada. Actualmente, esto se maneja con una variable TLS proporcionada por std que ambas transforman la referencia, si ? se manejaran como un punto de retorno _en el nivel de los generadores_ entonces la transformación asíncrona pierde en cierto modo para insertar esta referencia de variable.

Escribí una publicación de blog sobre la sintaxis de await describe mi preferencia hace dos meses. Sin embargo, básicamente asumió una sintaxis de prefijo y solo consideró el tema de la precedencia desde esa perspectiva. Aquí hay algunos pensamientos adicionales ahora:

  • Mi opinión general es que Rust ya ha estirado mucho su presupuesto de desconocidos. Sería ideal que la sintaxis async / await a nivel de superficie sea lo más familiar posible para alguien que provenga de JavaScript, Python o C #. Sería ideal desde esta perspectiva divergir sólo en formas menores de la norma. Las sintaxis de sufijos varían según el grado de divergencia (por ejemplo, foo await es una divergencia menor que algunos sigilos como foo@ ), pero todos son más divergentes de lo que el prefijo aguarda.
  • También prefiero estabilizar una sintaxis que no use ! . Todos los usuarios que se ocupan de async / await se preguntarán por qué await es una macro en lugar de una construcción de flujo de control normal, y creo que la historia aquí será esencialmente "bueno, no pudimos encontrar una buena sintaxis, así que nos decidimos por haciendo que parezca una macro ". Ésta no es una respuesta convincente. No creo que la asociación entre ! y el flujo de control sea realmente suficiente para justificar esta sintaxis: creo que ! tiene una macroexpensión bastante específica, que no lo es.
  • Tengo algunas dudas sobre el beneficio de postfix await en general (no del todo, solo una especie de ). Siento que el saldo es un poco diferente de ? , porque esperar es una operación más costosa (cede en un ciclo hasta que esté listo, en lugar de simplemente ramificarse y regresar una vez). Sospecho un poco del código que esperaría dos o tres veces en una sola expresión; Me parece bien decir que estos deben sacarse en sus propias ataduras. Así que la compensación try! vs ? no me atrae tan fuertemente aquí. Pero también, estaría abierto a muestras de código que la gente cree que no deberían ser extraídas de las listas y que son más claras como cadenas de métodos.

Dicho esto, foo await es la sintaxis postfix más viable que he visto hasta ahora:

  • Es relativamente familiar para la sintaxis postfix. Todo lo que tienes que aprender es que await va después de la expresión en lugar de antes de ella en Rust, en lugar de una sintaxis significativamente diferente.
  • Claramente resuelve la cuestión de la precedencia de la que se ha tratado todo esto.
  • El hecho de que no funcione bien con el encadenamiento de métodos me parece casi una ventaja, más que una desventaja, por las razones a las que aludí anteriormente. Me sentiría más obligado si tuviéramos algunas reglas gramaticales que impidieran foo await.method() solo porque realmente siento que el método se está aplicando (sin sentido) a await , no a foo (mientras que, curiosamente, No siento eso con foo await? ).

Todavía me estoy inclinando hacia una sintaxis de prefijo, pero creo que await es la primera sintaxis de sufijo que siento que tiene una oportunidad real para mí.

Nota al margen: siempre es posible usar parens para aclarar la precedencia:

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

Esto no es exactamente lo ideal, pero considerando que está tratando de agrupar mucho en una sola línea, creo que es razonable.

Y como @earthengine mencionó antes, la versión multilínea es muy razonable (sin parens adicionales):

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

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Sería ideal que la sintaxis async / await a nivel de superficie sea lo más familiar posible para alguien que provenga de JavaScript, Python o C #.

En el caso de try { .. } , tomamos en cuenta la familiaridad con otros idiomas. Sin embargo, también fue el diseño correcto desde un punto de vista de consistencia interna con Rust. Entonces, con el debido respeto a esos otros lenguajes, la consistencia interna en Rust parece más importante y no creo que la sintaxis de prefijos se ajuste a Rust ni en términos de precedencia ni en cómo están estructuradas las API.

  • También prefiero estabilizar una sintaxis que no use ! . Todos los usuarios que se ocupan de async / await se preguntarán por qué await es una macro en lugar de una construcción de flujo de control normal, y creo que la historia aquí será esencialmente "bueno, no pudimos encontrar una buena sintaxis, así que nos decidimos por haciendo que parezca una macro ". Ésta no es una respuesta convincente.

Estoy de acuerdo con este sentimiento, .await!() no se verá lo suficientemente de primera clase.

  • Tengo algunas dudas sobre el beneficio de postfix await en general (no del todo, solo una especie de). Siento que el saldo es un poco diferente de ? , porque esperar es una operación más costosa (cede en un ciclo hasta que esté listo, en lugar de simplemente ramificarse y regresar una vez).

No veo qué tiene que ver lo caro con extraer cosas en enlaces let . Las cadenas de métodos pueden ser costosas y, a veces, también. El beneficio de los enlaces let es a) dar un nombre a piezas suficientemente grandes donde tenga sentido para mejorar la legibilidad, b) poder hacer referencia al mismo valor calculado más de una vez (por ejemplo, por &x o cuando se puede copiar un tipo).

Sospecho un poco del código que esperaría dos o tres veces en una sola expresión; Me parece bien decir que estos deben sacarse en sus propias ataduras.

Si cree que deberían sacarse a sus propios enlaces let , aún puede hacer esa elección con el sufijo await :

let temporary = some_computation() await?;

Para aquellos que no están de acuerdo y prefieren el método de encadenamiento, el sufijo await da la posibilidad de elegir. También creo que postfix sigue mejor la lectura de izquierda a derecha y el orden de flujo de datos aquí, por lo que incluso si extrae a let enlaces, todavía prefiero postfix.

Tampoco creo que debas esperar dos o tres veces para que el sufijo await sea ​​útil. Considere, por ejemplo (este es el resultado de rustfmt ):

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

Pero también, estaría abierto a muestras de código que la gente cree que no deberían ser extraídas de las listas y que son más claras como cadenas de métodos.

La mayor parte del código fucsia que leí se sintió antinatural cuando se extrajo en enlaces let y con let binding = await!(...)?; .

  • Es relativamente familiar para la sintaxis postfix. Todo lo que tienes que aprender es que await va después de la expresión en lugar de antes de ella en Rust, en lugar de una sintaxis significativamente diferente.

Mi preferencia por foo.await aquí es principalmente porque obtienes un buen autocompletado y el poder del punto. Tampoco se siente tan radicalmente diferente. Escribir foo.await.method() también aclara que .method() se aplica a foo.await . Entonces resuelve esa preocupación.

  • Claramente resuelve la cuestión de la precedencia de la que se ha tratado todo esto.

No, no se trata solo de precedencia. Las cadenas de métodos son igualmente importantes.

  • El hecho de que no funcione bien con el encadenamiento de métodos me parece casi una ventaja, más que una desventaja, por las razones a las que aludí anteriormente.

No estoy seguro de por qué no funciona bien con el encadenamiento de métodos.

Me sentiría más obligado si tuviéramos algunas reglas gramaticales que impidieran foo await.method() solo porque realmente siento que el método se está aplicando (sin sentido) a await , no a foo (mientras que, curiosamente, No siento eso con foo await? ).

Mientras que no estaría obligado a ir con foo await si introdujéramos un corte de papel de diseño intencional y evitáramos el encadenamiento de métodos con la sintaxis de sufijo await .

Concediendo que cada opción tiene una desventaja, y que una de ellas debería terminar siendo elegida ... una cosa que me molesta de foo.await es que, incluso si asumimos que no se confundirá literalmente con un campo de estructura, todavía parece acceder a un campo de estructura. La connotación del acceso al campo es que no está sucediendo nada particularmente impactante: es una de las operaciones menos efectivas en Rust. Mientras tanto, la espera tiene un gran impacto, una de las operaciones con más efectos secundarios (realiza las operaciones de E / S creadas en el futuro y tiene efectos de flujo de control). Entonces, cuando leo foo.await.method() , mi cerebro me dice que me salte el .await porque es relativamente poco interesante, y tengo que poner atención y esfuerzo para anular manualmente ese instinto.

todavía parece que está accediendo a un campo de estructura.

@glaebhoerl Tiene buenos puntos; Sin embargo, ¿el resaltado de sintaxis tiene un impacto nulo o insuficiente en su apariencia y en la forma en que su cerebro procesa las cosas? Al menos para mí, el color y la audacia son muy importantes al leer el código, por lo que no me saltearía .await que tiene un color diferente al resto de las cosas.

La connotación del acceso al campo es que no está sucediendo nada particularmente impactante: es una de las operaciones menos efectivas en Rust. Mientras tanto, la espera tiene un gran impacto, una de las operaciones con más efectos secundarios (realiza las operaciones de E / S creadas en el futuro y tiene efectos de flujo de control).

Estoy totalmente de acuerdo con esto. await es una operación de flujo de control como break o return , y debe ser explícita. La notación postfija propuesta se siente antinatural, como if Python: compare if c { e1 } else { e2 } con e1 if c else e2 . Ver el operador al final te hace pensar dos veces, independientemente de cualquier resaltado de sintaxis.

Tampoco veo cómo e.await es más consistente con la sintaxis de Rust que await!(e) o await e . No hay otra palabra clave postfix, y dado que una de las ideas era aplicar un caso especial en el analizador, no creo que eso sea una prueba de ser coherente.

También se menciona el problema de familiaridad @withoutboats . Podemos elegir una sintaxis extraña y maravillosa si tiene algunos beneficios maravillosos. Sin embargo, ¿los tiene un sufijo await ?

¿El resaltado de sintaxis tiene un impacto nulo o insuficiente en su apariencia y en la forma en que su cerebro procesa las cosas?

(Buena pregunta, estoy seguro de que tendría algún impacto, pero es difícil adivinar cuánto sin intentarlo realmente (y sustituir una palabra clave diferente solo llega hasta cierto punto). Ya que estamos en el tema ... mucho tiempo Hace mencioné que creo que el resaltado de sintaxis debería resaltar todos los operadores con efectos de flujo de control ( return , break , continue , ? ... y ahora await ) en un color especial extra-distintivo, pero no estoy a cargo del resaltado de sintaxis de nada y no sé si alguien realmente hace esto).

Estoy totalmente de acuerdo con esto. await es una operación de flujo de control como break o return , y debe ser explícita.

Estamos de acuerdo. Las notaciones foo.await , foo await , foo# , ... son explícitas . No hay ninguna espera implícita.

Tampoco veo cómo e.await es más consistente con la sintaxis de Rust que await!(e) o await e .

La sintaxis e.await per se no es consistente con la sintaxis de Rust, pero postfix generalmente se adapta mejor a ? y cómo están estructuradas las API de Rust (se prefieren los métodos a las funciones gratuitas).

La sintaxis await e? , si está asociada como (await e)? es completamente inconsistente con la forma en que break y return asocian. await!(e) también es inconsistente ya que no tenemos macros para el flujo de control y también tiene el mismo problema que otros métodos de prefijo.

No hay otra palabra clave postfix, y dado que una de las ideas era aplicar un caso especial en el analizador, no creo que eso sea una prueba de ser coherente.

No creo que realmente necesite cambiar libsyntax en absoluto por .await ya que ya debería manejarse como una operación de campo. La lógica preferiría ser tratada en resolve o HIR donde la traduces a una construcción especial.

Podemos elegir una sintaxis extraña y maravillosa si tiene algunos beneficios maravillosos. Sin embargo, ¿un sufijo await tiene?

Como se mencionó anteriormente, sostengo que lo hace debido al encadenamiento de métodos y la preferencia de Rust por las llamadas a métodos.

No creo que realmente necesite cambiar libsyntax en absoluto para .await, ya que ya debería manejarse como una operación de campo.

Esto es divertido.
Entonces, la idea es reutilizar el enfoque de self / super ..., pero para campos en lugar de para segmentos de ruta.

Sin embargo, esto hace que await una palabra clave de segmento de ruta (ya que pasa por resolución), por lo que es posible que desee prohibir los identificadores sin procesar para ella.

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

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

No hay ninguna espera implícita.

La idea surgió un par de veces en este hilo (la propuesta de "espera implícita").

no tenemos macros para controlar el flujo

Hay try! (que cumplió bastante bien su propósito) y posiblemente el select! desuso. Tenga en cuenta que await es "más fuerte" que return , por lo que no es descabellado esperar que sea más visible en el código que ? return .

Sostengo que lo hace debido al encadenamiento de métodos y la preferencia de Rust por las llamadas a métodos.

También tiene una preferencia (más notable) por los operadores de flujo de control de prefijo.

La espera e? sintaxis, si se asocia como (aguardar e)? es completamente inconsistente con cómo se asocian romper y devolver.

Prefiero await!(e)? , await { e }? o tal vez incluso { await e }? ; no creo que haya visto este último discutido, y no estoy seguro de si funciona.


Admito que podría tener un sesgo de izquierda a derecha. _Nota_

Mi opinión sobre esto parece cambiar cada vez que miro el tema, como si me hiciera el papel de abogado del diablo. Parte de eso se debe a que estoy tan acostumbrado a escribir mis propios futuros y máquinas de estado. Un futuro personalizado con poll es totalmente normal.

Quizás esto debería pensarse de otra manera.

Para mí, las abstracciones de costo cero en Rust se refieren a dos cosas: costo cero en tiempo de ejecución y, lo que es más importante, costo cero mentalmente.

Puedo razonar muy fácilmente sobre la mayoría de las abstracciones en Rust, incluidos los futuros, porque son solo máquinas de estado.

Con este fin, debe existir una solución simple que introduzca la menor cantidad de magia al usuario. Los sellos especialmente son una mala idea, ya que se sienten innecesariamente mágicos. Esto incluye .await campos mágicos.

Quizás la mejor solución sea la más sencilla, la macro original await! .

Entonces, con el debido respeto a esos otros lenguajes, la consistencia interna en Rust parece más importante y no creo que la sintaxis de prefijos se ajuste a Rust ni en términos de precedencia ni en cómo están estructuradas las API.

¿No veo cómo ...? await(foo)? / await { foo }? parece totalmente correcto en términos de precedencia de operadores y cómo se estructuran las API en Rust; su desventaja es la palabrería de los padres y (según su perspectiva) el encadenamiento, sin romper el precedente o ser confuso.

Hay try! (que cumplió bastante bien su propósito) y posiblemente el select! desuso.

Creo que la palabra operativa aquí está desaprobada . El uso de try!(...) es un error grave en Rust 2018. Ahora es un error grave porque introdujimos una sintaxis mejor, de primera clase y postfix.

Tenga en cuenta que await es "más fuerte" que return , por lo que no es descabellado esperar que sea más visible en el código que ? return .

El operador ? también puede tener efectos secundarios (a través de otras implementaciones que no sean Result ) y realiza un flujo de control, por lo que también es bastante "fuerte". Cuando se discutió, ? fue acusado de "ocultar una devolución" y de ser fácil de pasar por alto. Creo que esa predicción no se cumplió por completo. La situación re. await parece bastante similar.

También tiene una preferencia (más notable) por los operadores de flujo de control de prefijo.

Esos operadores de flujo de control de prefijo se escriben en ! type. Mientras tanto, el otro operador de flujo de control ? que toma un contexto impl Try<Ok = T, ...> y le da un T es sufijo.

¿No veo cómo ...? await(foo)? / await { foo }? parece totalmente correcto en términos de precedencia de operadores y cómo se estructuran las API en Rust-

La sintaxis await(foo) no es la misma que await foo si se requiere paréntesis para el primero y no para el segundo. El primero no tiene precedentes, el segundo tiene problemas de precedencia wrt. ? como hemos comentado aquí, en la publicación del blog de Boat y en Discord. La sintaxis await { foo } es problemática por otras razones (consulte https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

su desventaja es la palabrería de los padres y (dependiendo de su perspectiva) el encadenamiento, no romper precedentes o ser confuso.

Esto es lo que quiero decir con "las API están estructuradas". Creo que los métodos y el encadenamiento de métodos son comunes e idiomáticos en Rust. Las sintaxis de prefijo y bloque se componen mal con esos y con ? .

Puedo estar en minoría con esta opinión, y si es así, ignórame:

¿Sería justo mover la discusión de prefijo vs postfijo a un hilo de Internals, y luego simplemente volver aquí con el resultado? ¿De esa manera podemos mantener el problema de seguimiento para

@seanmonstar Sí, simpatizo mucho con el deseo de limitar la discusión sobre los problemas de seguimiento y tener problemas que en realidad son solo actualizaciones de estado. Este es uno de los problemas que espero que podamos abordar con algunas revisiones de cómo gestionamos el proceso de RFC y los problemas en general. Por ahora, he abierto un nuevo número aquí para que lo usemos para debatir.

IMPORTANTE PARA TODOS: más discusión sobre la sintaxis await debe ir aquí .

Bloquear temporalmente durante un día para garantizar que se produzca una discusión futura sobre la sintaxis await sobre el tema correspondiente.

El martes 15 de enero de 2019 a las 07:10:32 AM -0800, Pauan escribió:

Nota al margen: siempre es posible usar parens para aclarar la precedencia:

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

Eso anula el beneficio principal de postfix await: "solo mantén
escritura / lectura ". Postfix await, como postfix ? , permite el flujo de control
para seguir moviéndose de izquierda a derecha:

foo().await!()?.bar().await!()

Si await! fuera el prefijo, o cuando try! era el prefijo, o si tiene
para poner entre paréntesis, entonces tienes que saltar de nuevo al lado izquierdo de
la expresión al escribirla o leerla.

EDITAR: Estaba leyendo comentarios de principio a fin por correo electrónico y no vi los comentarios de "mover la conversación al otro tema" hasta después de enviar este correo.

Informe de estado de espera asincrónica:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Quería publicar una actualización rápida sobre el estado de async-await
esfuerzo. La versión corta es que estamos en la recta final de
algún tipo de estabilización, pero quedan algunos
preguntas a superar.

Anuncio del grupo de trabajo de implementación

Como parte de este impulso, me complace anunciar que hemos formado un
grupo de trabajo de implementación async-await . Este grupo de trabajo
es parte de todo el esfuerzo async-await, pero se centra en el
implementación y es parte del equipo del compilador. Si tu quisieras
ayude a obtener async-await sobre la línea de meta, tenemos una lista de problemas
donde definitivamente nos gustaría recibir ayuda (sigue leyendo).

Si está interesado en participar, disponemos de un "horario de oficina"programado para el martes (consulte el [calendario del equipo del compilador]) , si
puede aparecer luego en [Zulip], ¡sería ideal! (Pero si no, simplemente introduzca cualquier
hora.)

...

¿Cuándo estará std::future::Future estable? ¿Tiene que esperar a que async espere? Creo que es un diseño muy agradable y me gustaría comenzar a portarlo. (¿Hay una cuña para usarlo en el establo?)

@ry vea el nuevo problema de seguimiento: https://github.com/rust-lang/rust/issues/59113

Otro problema del compilador para async / await: https://github.com/rust-lang/rust/issues/59245

También tenga en cuenta que https://github.com/rust-lang-nursery/futures-rs/issues/1199 en la publicación superior se puede marcar, ya que ahora está arreglado.

Parece que hay un problema con HRLB y cierres asíncronos: https://github.com/rust-lang/rust/issues/59337. (Aunque, al volver a leer el RFC, en realidad no especifica que los cierres asíncronos estén sujetos al mismo argumento de captura de por vida que tiene la función asíncrona).

Sí, los cierres asíncronos tienen muchos problemas y no deberían incluirse en la ronda inicial de estabilización. El comportamiento actual se puede emular con un cierre + bloque asíncrono, y en el futuro me encantaría ver una versión que permitiera hacer referencia a las variables ascendentes del cierre del futuro devuelto.

Acabo de notar que actualmente await!(fut) requiere que fut sea Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

¿Es eso esperado? No parece estar en el RFC.

@Ekleog que no es await! da el error, await! apila conceptualmente el futuro pasado para permitir que se usen futuros de !Unpin ( ejemplo de patio de recreo rápido ). El error proviene de la restricción en impl Future for Box<impl Future + Unpin> , que requiere que el futuro sea Unpin para evitar que hagas algo como:

// where Foo: Future + !Unpin
let mut foo: Box<Foo> = ...;
Pin::new(&mut foo).poll(cx);
let mut foo = Box::new(*foo);
Pin::new(&mut foo).poll(cx);

Debido a que Box es Unpin y permite mover el valor de él, puede sondear el futuro una vez en una ubicación de montón, luego sacar el futuro de la caja y colocarlo en una nueva ubicación de montón y sondear de nuevo.

await posiblemente debería estar en mayúsculas y minúsculas para permitir Box<dyn Future> ya que consume el futuro

¿Quizás el rasgo IntoFuture debería resucitar por await! ? Box<dyn Future> puede implementar eso convirtiéndolo en Pin<Box<dyn Future>> .

Aquí viene mi siguiente error con async / await: parece que se usa un tipo asociado a un parámetro de tipo en el tipo de retorno de una inferencia de rupturas de async fn : https://github.com/rust-lang/rust/ cuestiones / 60414

Además de agregar potencialmente # 60414 a la lista de la publicación superior (no sé si todavía se está usando, ¿tal vez sería mejor señalar la etiqueta github?), Creo que la “Resolución de rust-lang / rfcs # 2418 ”se puede marcar, ya que IIRC el rasgo Future se ha estabilizado recientemente.

Vengo de una publicación de Reddit y debo decir que no me gusta la sintaxis postfix en absoluto. Y parece que a la mayoría de Reddit tampoco le gusta.

Prefiero escribir

let x = (await future)?

que aceptar esa extraña sintaxis.

En cuanto al encadenamiento, puedo refactorizar mi código para evitar tener más de 1 await .

Además, JavaScript en el futuro puede hacer esto ( propuesta de canalización inteligente ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Si se implementa el prefijo await , eso no significa que await no se pueda encadenar.

@KSXGitHub, este no es realmente el lugar para esta discusión, pero la lógica se describe aquí y hay muy buenas razones para ello que muchas personas han pensado durante muchos meses https://boats.gitlab.io/blog/post / esperar-decisión /

@KSXGitHub Si bien tampoco me gusta la sintaxis final, se ha discutido ampliamente en # 57640, https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https: //internals.rust- lang.org/t/a-final-proposal-for-await-syntax/ , y en varios otros lugares. Mucha gente ha expresado su preferencia allí y no está aportando ningún argumento nuevo al tema.

No discuta las decisiones de diseño aquí, hay un hilo para este propósito explícito

Si planea comentar allí, tenga en cuenta que la discusión ya se ha desarrollado bastante: asegúrese de tener algo sustancial que decir y asegúrese de que no se haya dicho antes en el hilo.

@withoutboats, a mi entender, la sintaxis final ya está acordada, ¿tal vez es hora de marcarlo como Hecho? :rubor:

¿La intención es estabilizarse a tiempo para la próxima versión beta del 4 de julio, o los errores de bloqueo requerirán otro ciclo para resolverse? Hay muchos problemas abiertos bajo la etiqueta A-async-await, aunque no estoy seguro de cuántos de ellos son críticos.

Ajá, ignore eso, acabo de descubrir la etiqueta AsyncAwait-Blocking .

¡Hola! ¿Cuándo tenemos que esperar el lanzamiento estable de esta función? ¿Y cómo puedo usar eso en compilaciones nocturnas?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 contiene información sobre la fecha de lanzamiento prevista y más

¿Existe un plan para implementar automáticamente Unpin para los futuros generados por async fn ?

Específicamente, me pregunto si Unpin no está disponible automáticamente debido al código Future generado en sí mismo, o si podemos usar referencias como argumentos.

@DoumanAsh Supongo que si un fn asíncrono nunca tiene autorreferencias activas en los puntos de rendimiento, entonces el futuro generado podría implementar Unpin, ¿tal vez?

Creo que debería ir acompañado de algunos mensajes de error bastante útiles que digan "no Unpin debido a _este_ préstamo" + una pista de "alternativamente, puede enmarcar este futuro"

El PR de estabilización en # 63209 señala que "Todos los bloqueadores ahora están cerrados". y aterrizó en todas las noches el 20 de agosto, por lo que se dirigirá al corte beta a finales de esta semana. Merece la pena señalar que desde el 20 de agosto se han presentado algunos nuevos problemas de bloqueo (según el seguimiento de la etiqueta AsyncAwait-Blocking). Dos de estos (# 63710, # 64130) parecen ser agradables que en realidad no impedirían la estabilización, sin embargo, hay otros tres problemas (# 64391, # 64433, # 64477) que vale la pena discutir. Estos últimos tres problemas están relacionados, y todos surgen debido al PR # 64292, que a su vez se envió para abordar el problema de AsyncAwait-Blocking # 63832. Un PR, # 64584, ya ha aterrizado en un intento de abordar la mayor parte de los problemas, pero los tres temas permanecen abiertos por ahora.

El lado positivo es que los tres bloqueadores abiertos serios parecen estar relacionados con el código que debería compilarse, pero que actualmente no se compila. En ese sentido, sería compatible con versiones anteriores de arreglos terrestres posteriores sin impedir la beta-ización y la eventual estabilización de async / await. Sin embargo, me pregunto si alguien del equipo de lang piensa que algo aquí es lo suficientemente preocupante como para sugerir que async / await debería activarse todas las noches durante otro ciclo (que, por desagradable que parezca, es el punto del programa de lanzamiento rápido después de todo).

@bstrie Estamos reutilizando "AsyncAwait-Blocking" por falta de una etiqueta mejor para señalarlos como "alta prioridad", en realidad no están bloqueando. Deberíamos renovar pronto el sistema de etiquetado para que sea menos confuso, cc @nikomatsakis.

... No es bueno ... nos perdimos async-await en el esperado 1.38. Tener que esperar 1.39, solo porque algunos "problemas" que no contaban ...

@earthengine No creo que sea una evaluación justa de la situación. Ha valido la pena tomar en serio todos los problemas que han surgido. No sería bueno aterrizar async en espera solo para que las personas se encuentren con estos problemas al intentar usarlo en la práctica :)

¿Fue útil esta página
0 / 5 - 0 calificaciones