Rust: Resolver la sintaxis de "await"

Creado en 15 ene. 2019  Â·  512Comentarios  Â·  Fuente: rust-lang/rust

Antes de comentar en este hilo, verifique https://github.com/rust-lang/rust/issues/50547 e intente verificar que no está duplicando argumentos que ya se han hecho allí.


Notas de los pastores:

Si es nuevo en este hilo, considere comenzar desde https://github.com/rust-lang/rust/issues/57640#issuecomment -456617889, que fue seguido por tres excelentes comentarios resumidos, el último de los cuales fue https: //github.com/rust-lang/rust/issues/57640#issuecomment -457101180. (¡Gracias, @traviscross!)

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

Comentario más útil

Pensé que podría ser útil escribir cómo otros lenguajes manejan una construcción en espera.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Pitón

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Cortar a tajos

$result = await task;

Dardo

var result = await task;

Con todo eso, recordemos que las expresiones de Rust pueden resultar en varios métodos encadenados. La mayoría de los idiomas tienden a no hacer eso.

Todos 512 comentarios

Pensé que podría ser útil escribir cómo otros lenguajes manejan una construcción en espera.


Kotlin

val result = task.await()

C

var result = await task;

F

let! result = task()

Scala

val result = Await.result(task, timeout)

Pitón

result = await task

JavaScript

let result = await task;

C ++ (Coroutines TR)

auto result = co_await task;

Cortar a tajos

$result = await task;

Dardo

var result = await task;

Con todo eso, recordemos que las expresiones de Rust pueden resultar en varios métodos encadenados. La mayoría de los idiomas tienden a no hacer eso.

Con todo eso, recordemos que las expresiones de Rust pueden resultar en varios métodos encadenados. La mayoría de los idiomas tienden a no hacer eso.

Yo diría que los lenguajes que admiten métodos de extensión tienden a tenerlos. Estos incluirían Rust, Kotlin, C # (por ejemplo, LINQ de sintaxis de método y varios constructores) y F #, aunque este último usa mucho el operador de tubería para obtener el mismo efecto.

Puramente anecdótico de mi parte, pero regularmente me encuentro con una docena de expresiones encadenadas de métodos en el código Rust en la naturaleza y se lee y funciona bien. No he experimentado esto en ningún otro lugar.

Me gustaría ver que este problema se menciona en la publicación superior de # 50547 (al lado de la casilla de verificación "Sintaxis final para await").

Kotlin

val result = task.await()

La sintaxis de Kotlin es:

val result = doTask()

El await es solo un suspendable function , no una cosa de primera clase.

Gracias por mencionar eso. Kotlin se siente más implícito porque los futuros están ansiosos por defecto. Sin embargo, sigue siendo un patrón común en un bloque diferido utilizar ese método para esperar en otros bloques diferidos. Ciertamente lo he hecho varias veces.

@cramertj Dado que hay 276 comentarios en https://github.com/rust-lang/rust/issues/50547 , ¿podría resumir los argumentos que se hicieron allí para que sea más fácil no repetirlos aquí? (¿Quizás agregarlos al OP aquí?)

Kotlin se siente más implícito porque los futuros están ansiosos por defecto. Sin embargo, sigue siendo un patrón común en un bloque diferido utilizar ese método para esperar en otros bloques diferidos. Ciertamente lo he hecho varias veces.

tal vez debería agregar ambos casos de uso con un poco de contexto / descripción.

Además, ¿qué pasa con otros idiomas que utilizan esperas implícitas, como go-lang?

Una razón para estar a favor de una sintaxis posterior a la corrección es que, desde la perspectiva de las personas que llaman, una espera se comporta mucho como una llamada a una función: abandona el control de flujo y cuando lo recupera, un resultado está esperando en su pila. En cualquier caso, preferiría una sintaxis que adopte el comportamiento similar a una función al contener la función-paranthesis. Y hay buenas razones para querer dividir la construcción de corrutinas desde su primera ejecución para que este comportamiento sea consistente entre bloques sincronizados y asíncronos.

Pero si bien se ha debatido el estilo de corrutina implícita, y estoy del lado de la explícita, ¿ llamar a una corrutina no sería lo suficientemente explícito? Esto probablemente funcione mejor cuando la corrutina no se usa directamente donde se construyó (o podría funcionar con secuencias). En esencia, a diferencia de una llamada normal, esperamos que una corrutina tarde más de lo necesario en un orden de evaluación más relajado. Y .await!() es más o menos un intento de diferenciar entre llamadas normales y llamadas de rutina.

Entonces, después de haber brindado una visión algo nueva de por qué se podría preferir la corrección posterior, una propuesta humilde de sintaxis:

  • future(?)
  • o future(await) que viene con sus propias compensaciones, por supuesto, pero parece ser aceptado como menos confuso, ver la parte inferior de la publicación.

Adaptando un ejemplo bastante popular de otro hilo (asumiendo que logger.log también sea ​​una corrutina, para mostrar cómo se ve la llamada inmediata):

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

Y con la alternativa:

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

Para evitar el código ilegible y ayudar al análisis, solo permita espacios después del signo de interrogación, no entre este y el paran abierto. Entonces future(? ) es bueno mientras que future( ?) no lo sería. Este problema no surge en el caso de future(await) donde todo el token actual se puede usar como antes.

La interacción con otros operadores posteriores a la corrección (como el actual ? -try) también es como en las llamadas a funciones:

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

O

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

Algunas razones para que te guste esto:

  • Comparado con .await!() , no alude a un miembro que pueda tener otros usos.
  • Sigue la precedencia natural de las llamadas, como el encadenamiento y el uso de ? . Esto mantiene el número de clases de precedencia más bajo y ayuda con el aprendizaje. Y las llamadas a funciones siempre han sido algo especiales en el lenguaje (aunque tienen un rasgo), por lo que no se espera que el código de usuario pueda definir su propio my_await!() que tiene una sintaxis y un efecto muy similar.
  • Esto podría generalizarse a los generadores y arroyos, así como a los generadores que esperan que se proporcionen más argumentos en la reanudación. En esencia, esto se comporta como un FnOnce mientras que Streams se comportaría como un FnMut . También se pueden acomodar fácilmente argumentos adicionales.
  • Para aquellos que han usado el actual Futures antes, esto captura cómo un ? con Poll debería haber funcionado todo el tiempo (secuencia de estabilización desafortunada aquí). Como paso de aprendizaje, también es consistente con esperar que un operador basado en ? desvíe el flujo de control. (await) por otro lado no satisfaría esto, pero después de todo, la función siempre esperará reanudarse en el punto divergente.
  • Utiliza una sintaxis similar a una función, aunque este argumento solo es bueno si estás de acuerdo conmigo: smile:

Y razones para no gustarle esto:

  • ? parece ser un argumento pero ni siquiera se aplica a una expresión. Creo que esto podría resolverse mediante la enseñanza, ya que el símbolo al que parece aplicarse es la llamada de función en sí, que es la noción algo correcta. Esto también significa positivamente que la sintaxis es inequívoca, espero.
  • Más (y diferentes) mezclas de parántesis y ? pueden ser difíciles de analizar. Especialmente cuando tienes un futuro que devuelve el resultado de otro futuro: construct_future()(?)?(?)? . Pero podría hacer el mismo argumento para poder obtener un resultado de un objeto fn , lo que lleva a que se permita una expresión como esta: foobar()?()?()? . Dado que, sin embargo, nunca he visto este uso ni una queja, la división en declaraciones separadas en tales casos parece que rara vez se requiere. Este problema tampoco existe para construct_future()(await)?(await)? -
  • future(?) es mi mejor oportunidad de obtener una sintaxis escueta y algo concisa. Sin embargo, su razonamiento se basa en los detalles de implementación en corrutinas (que regresan temporalmente y se envían en el currículum), lo que puede hacer que no sea adecuado para una abstracción. future(await) sería una alternativa que aún podría explicarse después de que await se haya internalizado como palabra clave, pero la posición del argumento es un poco difícil de asimilar para mí. Podría estar bien, y ciertamente es más legible cuando la corrutina devuelve un resultado.
  • ¿Interferencia con otras propuestas de convocatorias de funciones?
  • ¿Tu propio? No es necesario que le guste, simplemente se sintió como un desperdicio no al menos proponer esta sintaxis escueta posterior a la corrección.

future(?)

No hay nada especial en Result : los futuros pueden devolver cualquier tipo de Rust. Da la casualidad de que algunos futuros devuelven Result

Entonces, ¿cómo funcionaría eso para los futuros que no devuelven Result ?

Parece que no estaba claro a qué me refería. future(?) es lo que se discutió anteriormente como future.await!() o similar. Bifurcarse en un futuro que también devuelve un resultado sería future(?)? (dos formas diferentes de cómo podemos renunciar al flujo de control temprano). Esto hace que el sondeo futuro (?) y el resultado de la prueba ? ortogonales. Editar: agregó un ejemplo adicional para esto.

Bifurcarse en un futuro que también devuelva un resultado sería future(?)?

Gracias por aclararlo. En ese caso, definitivamente no soy fanático de eso.

Eso significa que llamar a una función que devuelve Future<Output = Result<_, _>> se escribiría como foo()(?)?

Tiene mucha sintaxis y usa ? para dos propósitos completamente diferentes.

Si es específicamente la sugerencia para el operador ? que es pesada, uno podría, por supuesto, reemplazarla con la palabra clave recién reservada. Solo inicialmente había considerado que esto se parecía demasiado a un argumento real de tipo desconcertante, pero la compensación podría funcionar en términos de ayudar a analizar mentalmente la declaración. Entonces, la misma declaración para impl Future<Output = Result<_,_>> se convertiría en:

  • foo()(await)?

El mejor argumento de por qué ? es apropiado es que el mecanismo interno utilizado es algo similar (de lo contrario, no podríamos usar Poll en las bibliotecas actuales) pero esto puede perder el sentido de ser una buena abstracción.

Tiene mucha sintaxis

Pensé que ese es el objetivo de las esperas explícitas.

usa ? para dos propósitos completamente diferentes.

sí, entonces la sintaxis foo()(await) sería mucho mejor.

esta sintaxis es como llamar a una función que devuelve un cierre y luego llamar a ese cierre en JS.

Mi lectura de "sintaxis pesada" estaba más cerca de "sigil-heavy", ver una secuencia de ()(?)? es bastante discordante. Esto se mencionó en la publicación original:

Más (y diferentes) mezclas de parántesis y ? pueden ser difíciles de analizar. Especialmente cuando tiene un futuro que devuelve el resultado de otro futuro: construct_future()(?)?(?)?

Pero podría hacer el mismo argumento para poder obtener un resultado de un objeto fn , lo que lleva a que se permita una expresión como esta: foobar()?()?()? . Dado que, sin embargo, nunca he visto este uso ni una queja, la división en declaraciones separadas en tales casos parece que rara vez se requiere.

Creo que la refutación aquí es: ¿cuántas veces has visto -> impl Fn en estado salvaje (y mucho menos -> Result<impl Fn() -> Result<impl Fn() -> Result<_, _>, _>, _> )? ¿Cuántas veces espera ver -> impl Future<Output = Result<_, _>> en una base de código asincrónica? Tener que nombrar un valor de retorno raro de impl Fn para facilitar la lectura del código es muy diferente a tener que nombrar una fracción significativa de los valores de retorno temporales de impl Future .

Tener que nombrar un valor de retorno Fn implícito poco común para facilitar la lectura del código es muy diferente a tener que nombrar una fracción significativa de los valores de retorno futuros implícitos temporales.

No veo cómo esta elección de sintaxis influye en la cantidad de veces que debe nombrar explícitamente su tipo de resultado. No creo que no influya en la inferencia de tipos de manera diferente a await? future .

Sin embargo, todos ustedes hicieron muy buenos puntos aquí y cuantos más ejemplos consigo (edité la publicación original para que siempre contenga ambas versiones de sintaxis), más me inclino hacia future(await) . No es irrazonable escribir, y aún conserva toda la claridad de la sintaxis de llamada a función que se pretendía evocar.

¿Cuántas veces espera ver -> impl Future> en una base de código asincrónica?

Espero ver el tipo equivalente de esto (una fn asíncrona que devuelve un resultado) todo el tiempo , probablemente incluso la mayoría de todas las fns asíncronas, ya que si lo que está esperando es un IO par, es casi seguro que arroje Errores IO hacia arriba.


Enlace a mi publicación anterior sobre el problema de seguimiento y agregando algunas ideas más.

Creo que hay muy pocas posibilidades de que una sintaxis que no incluya la cadena de caracteres await sea ​​aceptada para esta sintaxis. Creo que en este punto, después de un año de trabajo en esta característica, sería más productivo intentar sopesar los pros y los contras de las alternativas viables conocidas para tratar de encontrar cuál es la mejor que proponer nuevas sintaxis. Las sintaxis creo que son viables, dadas mis publicaciones anteriores:

  • El prefijo aguarda con delimitadores obligatorios. Aquí, esta es también una decisión de qué delimitadores (ya sea llaves o parens o aceptar ambos; todos estos tienen sus propios pros y contras). Es decir, await(future) o await { future } . Esto resuelve completamente los problemas de precedencia, pero es sintácticamente ruidoso y ambas opciones del delimitador presentan posibles fuentes de confusión.
  • El prefijo aguarda con la precedencia "útil" con respecto a ? . (Es decir, ¿que aguardan ata más fuerte que?). Esto puede sorprender a algunos usuarios al leer el código, pero creo que las funciones que devuelven futuros de resultados serán abrumadoramente más comunes que las funciones que devuelven resultados de futuros.
  • El prefijo aguarda con la precedencia "obvia" con respecto a ? . (Es decir, que se une más fuerte de lo que espera). Azúcar de sintaxis adicional await? para una espera combinada y? operador. Creo que este azúcar de sintaxis es necesario para que este orden de precedencia sea viable, de lo contrario, todos escribirán (await future)? todo el tiempo, que es una variante peor de la primera opción que enumeré.
  • Postfix aguardar con el espacio de sintaxis aguardar. Esto resuelve el problema de la precedencia al tener un orden visual claro entre los dos operadores. Me siento incómodo con esta solución en muchos aspectos.

Mi propia clasificación entre estas opciones cambia cada vez que examino el tema. A partir de este momento, creo que usar la precedencia obvia con el azúcar parece el mejor equilibrio entre ergonomía, familiaridad y comprensión. Pero en el pasado he favorecido cualquiera de las otras dos sintaxis de prefijo.

En aras de la discusión, daré a estas cuatro opciones estos nombres:

Nombre | Futuro | Futuro del resultado | Resultado del futuro
--- | --- | --- | ---
Delimitadores obligatorios | await(future) o await { future } | await(future)? o await { future }? | await(future?) o await { future? }
Precedencia útil | await future | await future? | await (future?)
Precedencia obvia con azúcar | await future | await? future o (await future)? | await future?
Postfix palabra clave | future await | future await? | future? await

(He utilizado específicamente "palabra clave de sufijo" para distinguir esta opción de otras sintaxis de sufijo como "macro de sufijo").

Una de las deficiencias de 'bendecir' await future? en Precedencia útil, pero también otras que no funcionan como corrección posterior sería que los patrones habituales de conversión manual de expresiones con ? ya no se aplican, o requerir que Future repita explícitamente los métodos Result de una manera compatible. Encuentro esto sorprendente. Si se replican, de repente se vuelve tan confuso cuál de los combinadores trabaja en un futuro regresado y cuáles están ansiosos. En otras palabras, sería tan difícil decidir qué hace realmente un combinador como en el caso de la espera implícita. (Editar: en realidad, vea dos comentarios a continuación donde tengo una perspectiva más técnica de lo que quiero decir con el reemplazo sorprendente de ? )

Un ejemplo donde podemos recuperarnos de un caso de error:

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

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

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

Este problema no ocurre con los operadores reales posteriores a la reparación:

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

Estas son expresiones diferentes, el operador de punto tiene mayor precedencia que el operador de "precedencia obvia" await , por lo que el equivalente es:

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

Esto tiene exactamente la misma ambigüedad de si or_recover es asíncrono o no. (Lo que yo sostengo que no importa, usted sabe que la expresión en su conjunto es asíncrona y puede ver la definición de or_recover si por alguna razón necesita saber si esa parte específica es asíncrona).

Esto tiene exactamente la misma ambigüedad de si or_recover es asíncrono o no.

No es exactamente lo mismo. unwrap_or_else debe producir una corrutina porque se espera, por lo que la ambigüedad es si get_result es una corrutina (por lo que se construye un combinador) o un Result<impl Future, _> (y Ok ya contiene una corrutina, y Err construye una). Ambos no tienen las mismas preocupaciones de poder identificar de un vistazo la ganancia de eficiencia al mover un punto de secuencia await a un join , que es una de las principales preocupaciones de espera implícita. La razón es que, en cualquier caso, este cálculo intermedio debe estar sincronizado y debe haberse aplicado al tipo antes de esperar y debe haber resultado en la corrutina esperada. Hay una preocupación más grande aquí:

Estas son expresiones diferentes, el operador de punto tiene mayor precedencia que el operador de espera de "precedencia obvia", por lo que el equivalente es

Eso es parte de la confusión, reemplazar ? con una operación de recuperación cambió la posición de await fundamentalmente. En el contexto de la sintaxis ? , dada una expresión parcial expr de tipo T , espero la siguiente semántica de una transformación (suponiendo que exista T::unwrap_or_else ) :

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

Sin embargo, en 'Precedencia útil' y await expr? ( await expr produce T ) obtenemos

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

mientras que en la precedencia obvia esta transformación ya no se aplica en absoluto sin una parántesis adicional, pero al menos la intuición todavía funciona para 'Resultado del futuro'.

¿Y qué pasa con el caso aún más interesante en el que espera en dos puntos diferentes en una secuencia combinatoria? Con cualquier sintaxis de prefijo, esto, creo, requiere paréntesis. El resto del lenguaje Rust intenta evitar esto a fondo para hacer que funcionen las 'expresiones que se evalúan de izquierda a derecha', un ejemplo de esto es la magia de autorreferencia.

Ejemplo para mostrar que esto empeora para cadenas más largas con múltiples puntos de espera / intento / combinación.

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

Lo que me gustaría que se evitara es crear el análogo de Rust del análisis de tipo de C en el que saltas entre
lado izquierdo y derecho de la expresión para los combinadores 'puntero' y 'matriz'.

Entrada de tabla en el estilo de @withoutboats :

| Nombre | Futuro | Futuro del resultado | Resultado del futuro |
| - | - | - | - |
| Delimitadores obligatorios | await(future) | await(future)? | await(future?) |
| Precedencia útil | await future | await future? | await (future?) |
| Precedencia obvia | await future | await? future | await future? |
| Postfix Call | future(await) | future(await)? | future?(await) |

| Nombre | Encadenado |
| - | - |
| Delimitadores obligatorios | await(await(foo())?.bar())? |
| Precedencia útil | await(await foo()?).bar()? |
| Precedencia obvia | await? (await? foo()).bar() |
| Postfix Call | foo()(await)?.bar()(await) |

Estoy firmemente a favor de un sufijo en espera por varias razones, pero no me gusta la variante que muestra @withoutboats , principalmente parece por las mismas razones. P.ej. foo await.method() es confuso.

Primero veamos una tabla similar pero agregando un par de variantes de sufijo más:

| Nombre | Futuro | Futuro del resultado | Resultado del futuro |
| ---------------------- | -------------------- | ----- ---------------- | --------------------- |
| Delimitadores obligatorios | await { future } | await { future }? | await { future? } |
| Precedencia útil | await future | await future? | await (future?) |
| Precedencia obvia | await future | await? future | await future? |
| Postfix palabra clave | future await | future await? | future? await |
| Campo de sufijo | future.await | future.await? | future?.await |
| Método postfijo | future.await() | future.await()? | future?.await() |

Ahora veamos una expresión futura encadenada:

| Nombre | Futuros encadenados de resultados |
| ---------------------- | -------------------------- ----------- |
| Delimitadores obligatorios | await { await { foo() }?.bar() }? |
| Precedencia útil | await (await foo()?).bar()? |
| Precedencia obvia | await? (await? foo()).bar() |
| Postfix palabra clave | foo() await?.bar() await? |
| Campo de sufijo | foo().await?.bar().await? |
| Método postfix | foo().await()?.bar().await()? |

Y ahora, para un ejemplo del mundo real, desde reqwests , de dónde es posible que desee esperar un futuro encadenado de resultados (usando mi formulario de espera preferido).

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

En realidad, creo que cada separador se ve bien para la sintaxis de postfix, por ejemplo:
let res: MyResponse = client.get("https://my_api").send()/await?.json()/await?;
Pero no tengo una opinión sólida sobre cuál usar.

¿Podría la macro postfix (es decir, future.await!() ) seguir siendo una opción? Es claro, conciso e inequívoco:

| Futuro | Futuro del resultado | Resultado del futuro |
| --- | --- | --- |
| future.await! () | future.await! ()? | futuro? .¡espera! () |

Además, la macro de postfix requiere menos esfuerzo para implementarse y es fácil de entender y usar.

Además, la macro de postfix requiere menos esfuerzo para implementarse y es fácil de entender y usar.

También está usando una característica de idioma común (o al menos se vería como una macro postfix normal).

Una macro de postfix sería buena ya que combina la concisión y la capacidad de encadenamiento de postfix con las propiedades no mágicas y la presencia obvia de macros, y encajaría bien con macros de usuarios de terceros, como algunos .await_debug!() , .await_log!(WARN) o .await_trace!()

Una macro postfix sería buena ya que combina las [...] propiedades no mágicas de las macros.

@novacrazy el problema con este argumento es que cualquier await! macro _ sería mágico, está realizando una operación que no es posible en el código escrito por el usuario (actualmente, la implementación basada en el generador subyacente está algo expuesta, pero tengo entendido que antes de la estabilización, esto estará completamente oculto (y, de hecho, interactuar con él en este momento requiere el uso de algunas características nocturnas internas rustc todos modos)).

@ Nemo157 Hmm. No sabía que estaba destinado a ser tan opaco.

¿Es demasiado tarde para reconsiderar el uso de una macro de procedimiento como #[async] para hacer la transformación de la función "asíncrona" a la función del generador, en lugar de una palabra clave mágica? Son tres caracteres adicionales para escribir, y podrían marcarse en los documentos de la misma manera que #[must_use] o #[repr(C)] .

Realmente me disgusta la idea de ocultar tantas capas de abstracción que controlan directamente el flujo de ejecución. Se siente contrario a lo que es Rust. El usuario debe poder rastrear completamente el código y descubrir cómo funciona todo y hacia dónde va la ejecución. Se les debe animar a piratear cosas y engañar a los sistemas, y si usan el Rust seguro, debería ser seguro. Esto no mejora nada si perdemos el control de bajo nivel, y también puedo ceñirme a los futuros sin procesar.

Creo firmemente que Rust, el lenguaje (no std / core ), debería proporcionar abstracciones y sintaxis solo si son imposibles (o muy poco prácticos) de hacer por los usuarios o std . Todo este asunto de la asincronía se ha salido de control en ese sentido. ¿ Realmente necesitamos algo más que la API pin y los generadores en rustc ?

@novacrazy En general, estoy de acuerdo con el sentimiento pero no con la conclusión.

debe proporcionar abstracciones y sintaxis solo si son imposibles (o muy poco prácticos) de hacer para los usuarios o std.

¿Cuál es la razón de tener for -loops en el idioma cuando también podrían ser una macro que se convierte en loop con descansos? ¿Cuál es la razón de || closure cuando podría ser un rasgo dedicado y constructores de objetos? ¿Por qué presentamos ? cuando ya teníamos try!() ? La razón por la que no estoy de acuerdo con esas preguntas y sus conclusiones es la coherencia. El objetivo de estas abstracciones no es solo el comportamiento que encapsulan, sino también la accesibilidad del mismo. for -replacement se desglosa en mutabilidad, ruta de código principal y legibilidad. || -remplazo se desglosa en la verbosidad de la declaración, similar a Futures actualmente. try!() se desglosa en el orden esperado de expresiones y componibilidad.

Considere que async no solo es el decorador de una función, sino que también hay otros pensamientos de proporcionar patrones adicionales por aync-blocks y async || . Dado que se aplica a elementos de diferentes idiomas, la usabilidad de una macro parece subóptima. Ni siquiera pensar en la implementación si tiene que ser visible para el usuario.

El usuario debe poder rastrear completamente el código y descubrir cómo funciona todo y hacia dónde va la ejecución. Se les debe animar a piratear cosas y engañar a los sistemas, y si usan el Rust seguro, debería ser seguro.

No creo que este argumento se aplique porque la implementación de corrutinas usando completamente std api probablemente dependería en gran medida de unsafe . Y luego está el argumento inverso porque si bien es factible, y no se detendrá incluso si hay una forma sintáctica y semántica en el lenguaje para hacerlo, cualquier cambio correrá un gran riesgo de romper las suposiciones hechas en unsafe -code. Yo sostengo que Rust no debería hacer que parezca que está tratando de ofrecer una interfaz estándar para la implementación de bits que no tiene la intención de estabilizar pronto, incluidos los componentes internos de Coroutines. Un análogo a esto sería extern "rust-call" que sirve como la magia actual para dejar en claro que las llamadas de función no tienen tal garantía. Es posible que no deseemos nunca tener que return , aunque aún no se ha decidido el destino de las corrutinas apiladas. Es posible que queramos enganchar una optimización más profunda en el compilador.

Aparte: Hablando de lo cual, en teoría, no es una idea completamente seria, ¿podría denotarse la corrutina aguardar como un extern "await-call" fn () -> T hipotético? Si es así, esto permitiría en el preludio una

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

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

alias. future.await() en elementos documentados en un espacio de usuario. O para el caso, también podría ser posible otra sintaxis de operador.

@HeroicoKatora

¿Por qué introdujimos ? cuando ya teníamos try!()

Para ser justos, yo también estaba en contra de esto, aunque me ha crecido. Sería más aceptable si Try se estabilizara alguna vez, pero ese es otro tema.

El problema con los ejemplos de "azúcar" que da es que son azúcar muy, muy diluidos. Incluso impl MyStruct es más o menos azúcar por impl <anonymous trait> for MyStruct . Estos son azúcares de calidad de vida que añaden cero gastos generales.

Por el contrario, los generadores y las funciones asíncronas agregan una sobrecarga no del todo insignificante y una sobrecarga mental significativa. Específicamente, los generadores son muy difíciles de implementar como usuario, y podrían usarse de manera más efectiva y fácil como parte del lenguaje en sí, mientras que async podría implementarse además de eso con relativa facilidad.

Sin embargo, el punto sobre los bloqueos o cierres asíncronos es interesante, y reconozco que una palabra clave sería más útil allí, pero todavía me opongo a la imposibilidad de acceder a elementos de nivel inferior si es necesario.

Idealmente, sería maravilloso admitir la palabra clave async y un atributo / macro de procedimiento #[async] , y el primero permitiría un acceso de bajo nivel al generador generado (sin juego de palabras). Mientras tanto, yield debe permitirse en bloques o funciones usando async como palabra clave. Estoy seguro de que incluso podrían compartir el código de implementación.

En cuanto a await , si ambos de los anteriores son posibles, podríamos hacer algo similar y limitar la palabra clave await a async funciones / bloques de palabras clave y usar algún tipo de await!() macro en #[async] funciones.

Pseudocódigo:

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

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

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

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

    return Ok(item);
}

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

   let item = some_op() await?;

   return Ok(item);
}

Lo mejor de ambos mundos.

Esto se siente como una progresión más natural de la complejidad para presentar al usuario y se puede usar de manera más efectiva para más aplicaciones.

Recuerde que este problema es específicamente para discutir la sintaxis de await . Otras conversaciones sobre cómo se implementan las funciones y los bloques async están fuera de alcance, excepto con el propósito de recordarle a la gente que await! no es algo que pueda o podrá escribir en Rust's lenguaje superficial.

Me gustaría ponderar específicamente los pros y los contras de todas las propuestas de sintaxis posteriores a la corrección. Si una de las sintaxis se destaca con una pequeña cantidad de inconvenientes, tal vez deberíamos intentarlo. Sin embargo, si no hay ninguno, sería mejor admitir una sintaxis delimitada por prefijos de sintaxis que sea compatible con versiones posteriores a una corrección posterior aún por determinar si surge la necesidad. Como Postfix parece resonar como más conciso para algunos miembros, parece práctico evaluarlos primero antes de pasar a otros.

La comparación será syntax , example (el reqwest de @mehcode parece un punto de referencia del mundo real utilizable a este respecto), luego una tabla de ( concerns , y resolution opcional await se sienta ajena tanto a los recién llegados como a los usuarios experimentados, pero todos los que figuran actualmente la incluyen.

Ejemplo en una sintaxis de prefijo solo como referencia, no elimine esta parte en bicicleta, por favor:

let sent = (await client.get("https://my_api").send())?;
let res: MyResponse = (await sent.json())?;
  • Palabra clave de sufijo foo() await?

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

    • | Preocupación | Resolución |

      | - | - |

      | El encadenamiento sin ? puede resultar confuso o no permitido | |

  • Campo de sufijo foo().await?

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

    • | Preocupación | Resolución |

      | - | - |

      | Parece un campo | |

  • Método de sufijo foo().await()?

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

    • | Preocupación | Resolución |

      | - | - |

      | Parece un método o un rasgo | Puede estar documentado como ops:: rasgo? |

      | No es una llamada a función | |

  • Llamada postfix foo()(await)?

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

    • | Preocupación | Resolución |

      | - | - |

      | Puede confundirse con el argumento real | palabra clave + resaltado + no superpuesto |

  • Macro de sufijo foo().await!()?

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

    • | Preocupación | Resolución |

      | - | - |

      | En realidad, no será una macro… | |

      | … O, await ya no es una palabra clave | |

Un pensamiento adicional sobre post-fix vs. prefijo desde el punto de vista de posiblemente incorporar generadores: considerando valores, yield y await ocupan dos tipos opuestos de declaraciones. El primero da un valor de su función al exterior, el segundo acepta un valor.

Nota al margen: Bueno, Python tiene generadores interactivos donde yield puede devolver un valor. Simétricamente, las llamadas a dicho generador o flujo necesitan argumentos adicionales en una configuración de tipo fuerte. No intentemos generalizar demasiado, y veremos que el argumento probablemente se transfiera en cualquier caso.

Luego, sostengo que no es natural que estas declaraciones se parezcan. Aquí, de manera similar a las expresiones de asignación, tal vez deberíamos desviarnos de una norma establecida por otros lenguajes cuando esa norma es menos consistente y menos concisa para Rust. Como se expresa en otro lugar, siempre que incluyamos await y exista similitud con otras expresiones con el mismo orden de argumentos, no debería haber ningún obstáculo importante para la transición incluso desde otro modelo.

Dado que implícito parece fuera de la mesa.

Desde el uso de async / await en otros lenguajes y mirando las opciones aquí, nunca me ha parecido sintácticamente agradable encadenar futuros.

¿Hay una variante que no se pueda encadenar sobre la mesa?

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

Me gusta esto, ya que podrías argumentar que es parte del patrón.

El problema de hacer que await sea un enlace es que la historia de errores no es nada agradable.

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

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

Debemos tener en cuenta que casi todos los futuros disponibles en la comunidad para esperar son falibles y deben combinarse con ? .

Si realmente necesitamos el azúcar de sintaxis:

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

Tanto await como await? deberían agregarse como palabras clave o lo extendemos a let también, es decir, let? result = 1.divide(0);

Teniendo en cuenta la frecuencia con la que se usa el encadenamiento en el código de Rust, estoy completamente de acuerdo en que es importante que las esperas encadenadas sean lo más claras posible para el lector. En el caso de la variante postfix de await:

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

Esto generalmente se comporta de manera similar a cómo espero que se comporte el código de Rust. Tengo un problema con el hecho de que await() en este contexto se siente como una llamada de función, pero tiene un comportamiento mágico (sin función) en el contexto de la expresión.

La versión de macro postfix aclararía esto. La gente está acostumbrada a los signos de exclamación en óxido que significan "aquí hay magia" y ciertamente tengo una preferencia por esta versión por esa razón.

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

Dicho esto, vale la pena considerar que ya tenemos try!(expr) en el idioma y fue nuestro precursor de ? . Agregar una macro await!(expr) ahora sería totalmente coherente con la forma en que try!(expr) y ? se introdujeron en el lenguaje.

Con la versión await!(expr) de await, tenemos la opción de migrar a una macro postfix más tarde o agregar un nuevo operador con estilo ? modo que el encadenamiento sea fácil. Un ejemplo similar a ? pero para esperar:

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

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

Creo que deberíamos usar await!(expr) o await!{expr} por ahora, ya que es muy razonable y pragmático. Entonces podemos planear migrar a una versión postfix de await (es decir, .await! o .await!() ) más adelante si / una vez las macros postfix se convierten en algo. (O, eventualmente, seguir la ruta de agregar un operador de estilo ? adicional ... después de muchas discusiones sobre el tema: P)

FYI, la sintaxis de Scala no es Await.result ya que es una llamada de bloqueo. Los futuros de Scala son mónadas y, por lo tanto, usan llamadas a métodos normales o la comprensión de mónadas for :

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

Como resultado de esta horrible notación, se creó una biblioteca llamada scala-async con la sintaxis por la que estoy más a favor, que es la siguiente:

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

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

Esto refleja claramente cómo me gustaría que se viera el código rust, con el uso de delimitadores obligatorios y, como tal, me gustaría estar de acuerdo con los demás en mantener la sintaxis actual de await!() . Early Rust tenía muchos símbolos, y supongo que se alejó de él por una buena razón. El uso de azúcar sintáctico en forma de operador postfijo (o lo que sea) es, como siempre, compatible con versiones anteriores, y la claridad de await!(future) es inequívoca. También refleja la progresión que tuvimos con try! , como se mencionó anteriormente.

Una ventaja de mantenerlo como macro es que es más obvio de un vistazo que es una característica del lenguaje en lugar de una llamada de función normal. Sin la adición de ! , el resaltado de sintaxis del editor / visor sería la mejor manera de poder detectar las llamadas, y creo que confiar en esas implementaciones es una opción más débil.

Mis dos centavos (no soy un contribuyente habitual, fwiw) Soy más partidario de copiar el modelo de try! . Se ha hecho una vez antes, funcionó bien y después de que se hizo muy popular, hubo suficientes usuarios para considerar un operador postfix.

Entonces mi voto sería: estabilizar con await!(...) y apuntar a un operador postfix para un buen encadenamiento basado en una encuesta de desarrolladores de Rust. Await es una palabra clave, pero ! indica que es algo "mágico" para mí y los paréntesis lo mantienen inequívoco.

También una comparación:

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

Mi tercer centavo es que me gusta @ (para "aguardar") y # (para representar múltiples subprocesos / concurrencia).

¡También me gusta el sufijo @ ! Creo que en realidad no es una mala opción, aunque parece haber cierto sentimiento de que no es viable.

  • _ @ de await_ es un mnemónico agradable y fácil de recordar
  • ? y @ serían muy similares, por lo que aprender @ después de aprender ? no debería ser un salto tan grande
  • Ayuda a escanear una cadena de expresiones de izquierda a derecha, sin tener que escanear hacia adelante para encontrar un delimitador de cierre para entender una expresión.

Estoy muy a favor de la sintaxis await? foo , y creo que es similar a alguna sintaxis vista en matemáticas, donde por ejemplo. sin² x puede usarse para significar (sin x) ². Parece un poco incómodo al principio, pero creo que es muy fácil acostumbrarse.

Como se dijo anteriormente, estoy a favor de agregar await!() como macro, al igual que try!() , por ahora y eventualmente decidir cómo corregirlo. Si podemos tener en cuenta el soporte para un rustfix que convierte automáticamente las llamadas await!() al sufijo en espera, eso aún no se ha decidido, aún mejor.

La opción de palabra clave postfix es un claro ganador para mí.

  • No hay ningún problema de precedencia / orden, sin embargo, el orden podría hacerse explícito entre paréntesis. Pero sobre todo no hay necesidad de un anidamiento excesivo (argumento similar para preferir el sufijo '?' Como reemplazo de 'try ()!').

  • Se ve bien con el encadenamiento de varias líneas (consulte el comentario anterior de @earthengine) y, nuevamente, no hay confusión con respecto al pedido o lo que se espera. Y sin anidamiento / paréntesis adicionales para expresiones con múltiples usos de await:

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;
  • Se presta a una simple macro await! () (Vea el comentario anterior de @novacrazy):
macro_rules! await {
    ($e:expr) => {{$e await}}
}
  • Incluso una sola línea, desnuda (sin el '?'), Postfix aguardar encadenamiento de palabras clave no me molesta porque se lee de izquierda a derecha y estamos esperando la devolución de un valor en el que luego opera el método subsiguiente (aunque yo solo prefiera el código oxidado de varias líneas). El espacio rompe la línea y es suficiente indicador / señal visual de que está sucediendo la espera:
client.get("https://my_api").send() await.unwrap().json() await.unwrap()

Para sugerir otro candidato que no he visto presentado todavía (tal vez porque no sería analizable), ¿qué pasa con un divertido operador postfix de doble punto '..'? Me recuerda que estamos esperando algo (¡el resultado!) ...

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

¡También me gusta el sufijo @ ! Creo que en realidad no es una mala opción, aunque parece haber cierto sentimiento de que no es viable.

  • _ @ de await_ es un mnemónico agradable y fácil de recordar
  • ? y @ serían muy similares, por lo que aprender @ después de aprender ? no debería ser un salto tan grande
  • Ayuda a escanear una cadena de expresiones de izquierda a derecha, sin tener que escanear hacia adelante para encontrar un delimitador de cierre para entender una expresión.

No soy fanático de usar @ para esperar. Es incómodo escribir en un teclado de diseño fin / swe ya que tengo que presionar alt-gr con mi pulgar derecho y luego presionar la tecla 2 en la fila de números. Además, @ tiene un significado bien establecido (en), así que no veo por qué deberíamos combinar el significado.

Preferiría simplemente escribir await , es más rápido ya que no requiere ninguna acrobacia con el teclado.

Aquí está mi propia evaluación, muy subjetiva. También agregué future@await , lo que me parece interesante.

| sintaxis | notas |
| --- | --- |
| await { f } | fuerte:

  • muy sencillo
  • paralelos for , loop , async etc.
débiles:
  • muy detallado (5 letras, 2 llaves, 3 opcionales, pero probablemente espacios entre lindos)
  • el encadenamiento da como resultado muchas llaves anidadas ( await { await { foo() }?.bar() }? )
|
| await f | fuerte:
  • paralelos await sintaxis de Python, JS, C # y Dart
  • sencillo, corto
  • tanto precedencia útil vs comportan muy bien con prioridad obvia ? ( await fut? vs. await? fut )
débiles:
  • ambiguo: se debe aprender la precedencia útil frente a la obvia
  • el encadenamiento también es muy engorroso ( await (await foo()?).bar()? frente a await? (await? foo()).bar() )
|
| fut.await
fut.await()
fut.await!() | fuerte:
  • permite un encadenamiento muy fácil
  • corto
  • buena finalización de código
débiles:
  • engaña a los usuarios haciéndoles pensar que es un campo / función / macro definido en alguna parte. Editar: Estoy de acuerdo con @jplatte en que await!() siente menos mágico
|
| fut(await) | fuerte:
  • permite un encadenamiento muy fácil
  • corto
débiles:
  • engaña a los usuarios haciéndoles pensar que hay una variable await definida en algún lugar y que los futuros se pueden llamar como una función
|
| f await | fuerte:
  • permite un encadenamiento muy fácil
  • corto
débiles:
  • no se compara con nada en la sintaxis de Rust, no es obvio
  • mi cerebro agrupa los client.get("https://my_api").send() await.unwrap().json() await.unwrap() en client.get("https://my_api").send() , await.unwrap().json() y await.unwrap() (agrupados por primero, luego . ) que no es correcto
  • para Haskellers: parece curry pero no lo es
|
| f@ | fuerte:
  • permite un encadenamiento muy fácil
  • muy corto
débiles:
  • se ve un poco incómodo (al menos al principio)
  • consume @ que podría ser más adecuado para otra cosa
  • puede ser fácil de pasar por alto, especialmente en expresiones grandes
  • usa @ de una manera diferente a todos los demás idiomas
|
| f@await | fuerte:
  • permite un encadenamiento muy fácil
  • corto
  • buena finalización de código
  • await no necesita convertirse en una palabra clave
  • compatible con reenvíos: permite agregar nuevos operadores de sufijo en la forma @operator . Por ejemplo, ? podría haberse hecho como @try .
  • mi cerebro agrupa los client.get("https://my_api").send()@await.unwrap().json()@await.unwrap() en los grupos correctos (agrupados por . primero, luego @ )
débiles:
  • usa @ de una manera diferente a todos los demás idiomas
  • podría incentivar agregar demasiados operadores de sufijo innecesarios
|

Mis puntuaciones:

  • familiaridad (fam): qué tan cerca está esta sintaxis de las sintaxis conocidas (Rust y otras, como Python, JS, C #)
  • Evidencia (obv): Si leyeras esto en el código de otra persona por primera vez, ¿podrías adivinar el significado, la precedencia, etc.?
  • verbosidad (vrb): cuántos caracteres se necesitan para escribir
  • visibilidad (vis): lo fácil que es detectar (en lugar de pasar por alto) en el código
  • encadenamiento (cha): Qué fácil es encadenarlo con . y otros await s
  • agrupamiento (grp): si mi cerebro agrupa el código en los fragmentos correctos
  • Compatibilidad con versiones posteriores (fwd): si esto permite que se ajuste más tarde de manera ininterrumpida

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

Me parece que deberíamos reflejar la sintaxis de try!() en el primer corte y obtener un uso real del uso de await!(expr) antes de introducir alguna otra sintaxis.

Sin embargo, si / cuando construimos una sintaxis alternativa ...

Creo que @ ve feo, "at" para "async" no me parece tan intuitivo, y el símbolo ya se usa para la coincidencia de patrones.

async prefijo ? (que será a menudo).

Postfix .await!() encadena muy bien, se siente bastante obvio en su significado, incluye el ! para decirme que va a hacer magia y es menos novedoso sintácticamente, por lo que el "siguiente corte" se acerca a personalmente favorecería este. Dicho esto, para mí queda por ver cuánto mejoraría el código real con respecto al primer corte await! (expr) .

Prefiero el operador de prefijo para casos simples:
let result = await task;
Se siente mucho más natural teniendo en cuenta que no escribimos el tipo de resultado, por lo que la espera ayuda mentalmente cuando lee de izquierda a derecha para comprender que el resultado es una tarea con la espera.
Imagínelo así:
let result = somehowkindoflongtask await;
hasta que no llega al final de la tarea, no se da cuenta de que debe esperar al tipo que devuelve. Tenga en cuenta también (aunque esto está sujeto a cambios y no está directamente relacionado con el futuro del lenguaje) que IDE como Intellij en línea el tipo (sin ninguna personalización, si eso es posible) entre el nombre y los iguales.
Imagínelo así:
6voler6ykj

Eso no significa que mi opinión sea cien por ciento a favor del prefijo. Prefiero mucho la versión postfix de future cuando se trata de resultados, ya que se siente mucho más natural. Sin ningún contexto, puedo decir fácilmente cuál de estos significa qué:
future await?
future? await
En su lugar, mire este, cuál de estos dos es cierto, desde el punto de vista de un novato:
await future? === await (future?)
await future? === (await future)?

Estoy a favor de la palabra clave de prefijo: await future .

Es el que utilizan la mayoría de los lenguajes de programación que tienen async / await y, por lo tanto, es inmediatamente familiar para las personas que conocen uno de ellos.

En cuanto a la precedencia de await future? , ¿cuál es el caso común?

  • Una función que devuelve un Result<Future> que debe esperarse.
  • Un futuro que hay que esperar que devuelve un Result : Future<Result> .

Creo que el segundo caso es mucho más común cuando se trata de escenarios típicos, ya que las operaciones de E / S pueden fallar. Por lo tanto:

await future? <=> (await future)?

En el primer caso menos común, es aceptable tener paréntesis: await (future?) . Esto incluso podría ser un buen uso para la macro try! si no hubiera quedado obsoleta: await try!(future) . De esta forma, el operador await y el signo de interrogación no se encuentran en lados diferentes del futuro.

¿Por qué no tomar await como primer parámetro de función async ?

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

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

let output = future
    .run(await);

Aquí future.run(await) es una alternativa a await future .
Podría ser simplemente una función async normal que toma el futuro y simplemente ejecuta la macro await!() en ella.

C ++ (Concurrencia TR)

auto result = co_await task;

Esto está en Coroutines TS, no en concurrencia.

Otra opción podría ser usar la palabra clave become lugar de await :

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

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

let output = future.start(become);

become embargo,

Gracias @EyeOfPython por esa [descripción general]. Eso es especialmente útil para las personas que acaban de unirse al cobertizo para bicicletas.

Personalmente, espero que nos mantengamos alejados de f await , solo porque tiene una sintaxis muy poco rústica y lo hace parecer algo especial y mágico. Se convertiría en una de estas cosas por las que los usuarios nuevos en Rust se confundirán mucho y creo que no agrega tanta claridad, incluso para los veteranos de Rust, para que valga la pena.

@novacrazy el problema con este argumento es que cualquier await! macro _ sería_ mágica, está realizando una operación que no es posible en el código escrito por el usuario

@ Nemo157 Estoy de acuerdo en que una macro await! sería mágica, pero diría que esto no es un problema. Ya hay varias macros de este tipo en std , por ejemplo, compile_error! y no he visto a nadie quejarse de ellas. Creo que es normal usar una macro solo entendiendo lo que hace, no cómo lo hace.

Estoy de acuerdo con los comentaristas anteriores en que postfix sería el más ergonómico, pero preferiría comenzar con prefix-macro await!(expr) y potencialmente hacer la transición a postfix-macro una vez que sea una cosa en lugar de tener expr.await (campo mágico del compilador incorporado) o expr.await() (método mágico del compilador incorporado). Ambos introducirían una sintaxis completamente nueva exclusivamente para esta función, y en mi opinión, eso hace que el lenguaje se sienta inconsistente.

@EyeOfPython ¿Le importaría agregar future(await) a sus listas y tablas? Todos los aspectos positivos de su evaluación de future.await() parecen transferirse sin la debilidad

Dado que algunos han argumentado que a pesar del resaltado de sintaxis, foo.await parece demasiado a un acceso de campo, podríamos cambiar el token . a # y en su lugar escribir foo#await . Por ejemplo:

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

Para ilustrar cómo GitHub representaría esto con el resaltado de sintaxis, reemplacemos await con match ya que tienen la misma longitud:

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

Esto parece claro y ergonómico.

El fundamento de # lugar de otro token no es específico, pero el token es bastante visible, lo que ayuda.

Entonces, otro concepto: si el futuro fuera referencia :

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

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

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

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

let output = *future;

Esto sería realmente desagradable e inconsistente. Dejemos al menos desambiguar esa sintaxis:

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

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

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

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

let output = $future;

Mejor, pero aún feo (y aún peor, hace resaltar la sintaxis de github). Sin embargo, para lidiar con eso, introduzca la capacidad de retrasar el operador de prefijo :

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

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

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

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

let output = $future;

¡Esto es exactamente lo que quiero! No solo para await ( $ ) sino también para deref ( * ) y negate ( ! ).

Algunas advertencias en esta sintaxis:

  1. Precedencia del operador: para mí es obvio, pero ¿para otros usuarios?
  2. Interoperabilidad de macros: ¿el símbolo $ causaría problemas aquí?
  3. Inconsistencia de expresión: el operador de prefijo inicial se aplica a toda la expresión, mientras que el operador de prefijo retrasado se aplica solo a . expresión delimitada ( await_chain fn demuestre eso), ¿es confuso?
  4. Análisis e implementación: ¿esa sintaxis es válida en absoluto?

@Centril
IMO # está demasiado cerca de la sintaxis literal cruda r#"A String with "quotes""#

IMO # está demasiado cerca de la sintaxis literal cruda r#"A String with "quotes""#

Parece bastante claro por el contexto cuál es la diferencia en este caso.

@Centril
Lo hace, pero la sintaxis también es realmente ajena al estilo que Rust usa en mi humilde opinión. No se parece a ninguna sintaxis existente con una función similar.

@Laaas Tampoco ? cuando se presentó. Me encantaría ir con .await ; pero otros parecen descontentos con eso, así que estoy tratando de encontrar algo más que funcione (es decir, que sea claro, ergonómico, lo suficientemente fácil de escribir, encadenable, tenga buena precedencia) y foo#await parece satisfacer todo eso.

? embargo, .await , ¿por qué no .await! ?

@Centril Esa fue la razón principal detrás de future(await) , evitar el acceso al campo sin tener que agregar operadores adicionales o sintaxis foránea.

mientras que #await parece bastante arbitrario.

# es arbitrario, sí, ¿eso es importante o malo? Cuando se inventa una nueva sintaxis en algún momento, tiene que ser arbitraria porque alguien pensó que se veía bien o tenía sentido.

¿por qué no .await! ?

Eso es igualmente arbitrario, pero no me opongo.

@Centril Esa fue la razón principal detrás de future(await) , evitar el acceso al campo sin tener que agregar operadores adicionales o sintaxis foránea.

En cambio, parece una aplicación de función en la que está pasando await a future ; eso me parece más confuso que el "acceso de campo".

En su lugar, parece una aplicación de función en la que está pasando aguardar al futuro; eso me parece más confuso que el "acceso de campo".

Para mí eso era parte de la concisión. Await es en muchos aspectos como una llamada a una función (la llamada se ejecuta entre usted y su resultado), y pasar una palabra clave a esa función dejó en claro que este es un tipo diferente de llamada. Pero veo cómo puede resultar confuso que la palabra clave sea similar a cualquier otro argumento. (Menos el resaltado de sintaxis y los mensajes de error del compilador que intentan reforzar ese mensaje. Los futuros no se pueden llamar de otra forma que no sean los esperados y viceversa).

@Centril

Ser arbitrario no es algo negativo , pero tener semejanza con una sintaxis existente es algo positivo. .await! no es tan arbitrario, ya que no es una sintaxis completamente nueva; después de todo, es solo una macro posterior a la corrección llamada await .

Para aclarar / agregar al punto sobre la nueva sintaxis específicamente para await , lo que quiero decir con eso es introducir una nueva sintaxis que es diferente a la sintaxis existente. Hay muchas palabras clave de prefijo, algunas de las cuales se han agregado después de Rust 1.0 (por ejemplo, union ), pero AFAIK no es una sola palabra clave de prefijo (no importa si están separadas por un espacio, un punto u otra cosa) . Tampoco puedo pensar en ningún otro idioma que tenga palabras clave postfix.

En mi humilde opinión, la única sintaxis de sufijo que no aumenta significativamente la extrañeza de Rust es la macro de sufijo, porque refleja las llamadas a métodos, pero cualquiera que haya visto una macro de Rust antes puede identificarla claramente como una macro.

También me gustó la macro estándar await!() . Es simple y claro.
Prefiero que un acceso similar a un campo (o cualquier otro sufijo) coexista como awaited .

  • Await siente que estará esperando lo que viene a continuación. Esperado, a lo que vino antes / se fue.
  • Coherente con el método cloned() en iteradores.

Y también me gustó el @? como ? como azúcar.
Esto es personal, pero en realidad prefiero la combinación &? , porque @ tiende a ser demasiado alto y está por debajo de la línea, lo cual es muy distraído. & también es alto (bueno) pero no se queda corto (también es bueno). Desafortunadamente, & ya tiene un significado, aunque siempre iría seguido de un ? .. ¿Supongo?

P.ej.

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

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

Para mí, el @ siente como un carácter hinchado, distrae el flujo de lectura. Por otro lado, &? siente agradable, no distrae (mi) lectura y tiene un buen espacio superior entre & y ? .

Personalmente, creo que debería usarse una simple macro await! . Si las macros posteriores a la corrección entran en el idioma, entonces la macro podría simplemente expandirse, ¿no?

@Laaas Por mucho que me gustaría que hubiera, todavía no hay macros postfix en Rust, por lo que son una nueva sintaxis. También tenga en cuenta que foo.await!.bar y foo.await!().bar no son la misma sintaxis de superficie. En el último caso, habría un sufijo real y una macro incorporada (lo que implica renunciar a await como palabra clave).

@jplatte

Hay muchas palabras clave de prefijo, algunas de las cuales se agregaron después de Rust 1.0 (por ejemplo, union ),

union no es un operador de expresión unario, por lo que es irrelevante en esta comparación. Hay exactamente 3 operadores de prefijo unario estables en Rust ( return , break y continue ) y todos ellos se escriben en ! .

Hmm ... Con @ mi propuesta anterior se ve un poco mejor:

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

let output = @future;

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

@Centril Ese es mi punto, dado que aún no tenemos macros posteriores a la corrección, debería ser simplemente await!() . También quise decir .await!() FYI. No creo que await deba ser una palabra clave, aunque puede reservarla si la encuentra problemática.

union no es un operador de expresión unario, por lo que es irrelevante en esta comparación. Hay exactamente 3 operadores de prefijos unarios estables en Rust ( return , break y continue ) y todos ellos se escriben en ! .

Esto todavía significa que la variante prefijo-palabra clave encaja en el grupo de operadores de expresión unaria, incluso si se escribe de manera diferente. La variante de palabra clave postfix es una divergencia mucho mayor de la sintaxis existente, una que es demasiado grande en relación con la importancia de await en el idioma en mi opinión.

Con respecto a la macro de prefijo y la posibilidad de realizar la transición a una corrección posterior: await debe seguir siendo una palabra clave para que esto funcione. La macro sería entonces un elemento de idioma especial donde se le permite usar una palabra clave como nombre sin un r# adicional, y r#await!() podría pero probablemente no debería invocar la misma macro. Aparte de eso, parece la solución más pragmática para que esté disponible.

Es decir, mantenga await como palabra clave pero haga que await! resuelva en una macro lang-item.

@HeroicKatora, ¿

@Laaas Porque si queremos mantener abierta la posibilidad de transición, debemos permanecer abiertos a la futura sintaxis posterior a la corrección que la use como palabra clave. Por lo tanto, debemos mantener la reserva de palabras clave para que no necesitemos una interrupción de la edición para la transición.

@Centril

También tenga en cuenta que foo.await! .Bar y foo.await! (). Bar no tienen la misma sintaxis de superficie. En el último caso, habría un sufijo real y una macro incorporada (lo que implica renunciar a await como palabra clave).

¿No podría resolverse esto haciendo que la palabra clave await combinada con ! resuelva en una macro interna (que no se puede definir por medios normales)? Entonces sigue siendo una palabra clave, pero se resuelve como una macro en una sintaxis de macro.

@HeroicKatora ¿ await en x.await!() sería una palabra clave reservada?

No lo sería, pero si seguimos sin resolver el pos-arreglo, no es necesario que sea la solución a la que lleguemos en discusiones posteriores. Si esa fuera la mejor posibilidad única acordada, entonces deberíamos adoptar esta sintaxis exacta posterior a la corrección en primer lugar.

Esta es otra vez que nos encontramos con algo que funciona mucho mejor como operador de sufijo. El gran ejemplo de esto es try! que finalmente le dimos su propio símbolo ? . Sin embargo, creo que esta no es la última vez que un operador postfix es más óptimo y no podemos darle a todo su carácter especial. Así que creo que al menos no deberíamos empezar con @ . Sería mucho mejor si tuviéramos una forma de hacer este tipo de cosas. Es por eso que apoyo el estilo de macro postfix .await!() .

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

Pero para que esto tenga sentido, tendrían que introducirse las macros postfix. Por lo tanto, creo que sería mejor comenzar con una sintaxis de macro normal await!(foo) . Más tarde, podríamos expandir esto a foo.await!() o incluso foo@ si realmente creemos que esto es lo suficientemente importante como para garantizar su propio símbolo.
Que esta macro necesite un poco de magia no es nuevo en std y para mí no es un gran problema.
Como dijo @jplatte :

@ Nemo157 Estoy de acuerdo en que una macro await! sería mágica, pero diría que esto no es un problema. Ya hay varias macros de este tipo en std , por ejemplo, compile_error! y no he visto a nadie quejarse de ellas. Creo que es normal usar una macro solo entendiendo lo que hace, no cómo lo hace.

Un problema recurrente que veo discutido aquí es sobre el encadenamiento y cómo usar
aguardan encadenando expresiones. ¿Quizás podría haber otra solución?

Si queremos no usar await para encadenar y solo usar await para
asignaciones, podríamos tener algo como simplemente reemplazar let con await:
await foo = future

Luego, para el encadenamiento, podríamos imaginar algún tipo de operación como await res = fut1 -> fut2 o await res = fut1 >>= fut2 .

El único caso que falta es esperar y devolver el resultado, algún atajo
por await res = fut; res .
Esto se puede hacer fácilmente con un simple await fut

No creo haber visto otra propuesta como esta (al menos para el
encadenamiento), así que lo dejo aquí, ya que creo que sería bueno usarlo.

@HeroicKatora He añadido fut(await) a la lista y la he clasificado de acuerdo con mi opinión.

Si alguien siente que mi puntuación está mal, ¡dímelo!

En cuanto a la sintaxis, creo que .await!() es limpio y permite el encadenamiento sin agregar rarezas a la sintaxis.

Sin embargo, si alguna vez obtenemos macros de postfix "reales", será un poco extraño, porque presumiblemente .r#await!() podría ser sombreado, mientras que .await!() no podría.

Estoy bastante en contra de la opción "palabra clave postfix" (separada con un espacio como: foo() await?.bar() await? ), porque encuentro el hecho de que await está unido a la siguiente parte de la expresión, y no la parte en la que opera. Preferiría prácticamente cualquier símbolo que no sea el espacio en blanco aquí, e incluso preferiría la sintaxis de prefijo sobre esto a pesar de sus desventajas con cadenas largas.

Creo que la "precedencia obvia" (con el azúcar await? ) es claramente la mejor opción de prefijo, ya que los delimitadores obligatorios son un problema para el caso muy común de esperar una sola declaración, y la "precedencia útil" no lo es en absoluto intuitivo y, por tanto, confuso.

En cuanto a la sintaxis, creo que .await!() es limpio y permite el encadenamiento sin agregar rarezas a la sintaxis.

Sin embargo, si alguna vez obtenemos macros de postfix "reales", será un poco extraño, porque presumiblemente .r#await!() podría ser sombreado, mientras que .await!() no podría.

Si obtenemos macros postfix y usamos .await!() entonces podemos anular la reserva de await como palabra clave y convertirla en una macro postfix. La implementación de esta macro aún requeriría un poco de magia de cource, pero sería una macro real como lo es hoy compiler_error! .

@EyeOfPython ¿Podría explicar en detalle qué cambios considera en forward-compatibility ? No estoy seguro de qué manera fut@await calificaría más alto que fut.await!() y await { future } más bajo que await!(future) . La columna verbosity también parece un poco extraña, algunas expresiones son cortas pero tienen una calificación más baja, ¿considera declaraciones encadenadas, etc. Todo lo demás parece equilibrado y como la evaluación extensa mejor condensada hasta ahora?

Después de leer esta discusión, parece que mucha gente quiere agregar una macro await!() normal y descubrir la versión postfix más adelante. Eso es asumiendo que realmente queremos una versión postfix, que consideraré verdadera para el resto de este comentario.

Así que me gustaría sondear la opinión de todos los presentes: ¿Deberíamos estar todos de acuerdo con la sintaxis await!(future) POR AHORA? Las ventajas son que no hay ambigüedad de análisis con esa sintaxis, y también es una macro, por lo que no hay que cambiar la sintaxis del idioma para admitir este cambio. Las desventajas son que se verá feo para encadenar, pero eso no importa ya que esta sintaxis se puede reemplazar fácilmente con una versión postfix automáticamente.

Una vez que lo establezcamos y lo implementemos en el lenguaje, podemos continuar la discusión sobre la sintaxis de postfix aguardar con experiencias, ideas y posiblemente otras características del compilador más maduras.

@HeroicKatora Lo await para ser usado como identificador ordinario de forma natural y permitiría agregar otros operadores postfix, mientras que creo que fut.await!() sería mejor si await estaba reservado. Sin embargo, no estoy seguro de si esto es razonable, también parece definitivamente válido que fut.await!() podría ser más alto.

Para await { future } vs await!(future) , la última mantiene la opción abierta para cambiar a casi cualquiera de las otras opciones, mientras que la primera realmente solo permite cualquiera de las variantes await future ( como se establece en la entrada del blog de @withoutboats ). Así que creo que definitivamente debería ser fwd(await { future }) < fwd(await!(future)) .

En cuanto a la verbosidad, tienes razón, después de dividir los casos en subgrupos no reevalué la verbosidad, que debería ser la más objetiva de todas.

Lo editaré para tener en cuenta tus comentarios, ¡gracias!

Estabilizar await!(future) es la peor opción posible que puedo imaginar:

  1. Significa que tenemos que anular la reserva de await que además significa que el diseño del lenguaje futuro se vuelve más difícil.
  2. A sabiendas, está tomando la misma ruta que con try!(result) que desaprobamos (y que requiere escribir r#try!(result) en Rust 2018).

    • Si sabemos que await!(future) es una mala sintaxis que eventualmente queremos desaprobar, esto está creando una deuda técnica intencionalmente.

    • Además, try!(..) está definido en Rust, mientras que await!(future) no puede ser y sería magia de compilación.

    • Depreciar try!(..) no fue fácil y tuvo un costo social. Pasar por esa terrible experiencia nuevamente no parece atractivo.

  3. Esto usaría la sintaxis macro para una parte central e importante del lenguaje; que claramente no parece de primera clase.
  4. await!(future) es ruidoso ; a diferencia de await future , debe escribir !( ... ) .
  5. Las API de Rust, y especialmente la biblioteca estándar, se centran en la sintaxis de llamadas a métodos. Por ejemplo, es común encadenar métodos cuando se trabaja con Iterator s. Similar a await { future } y await future , la sintaxis await!(future) dificultará el encadenamiento de métodos e inducirá enlaces temporales let . Esto es malo para la ergonomía y, en mi opinión, para la legibilidad.

@Centril Me gustaría estar de acuerdo, pero hay algunas preguntas abiertas. ¿Estás seguro de 1. ? Si lo hacemos "mágico", ¿no podríamos hacerlo aún más mágico permitiendo que esto se refiera a una macro sin descartarla como palabra clave?

Me uní a Rust demasiado tarde para evaluar la perspectiva social de 2. . Repetir un error no suena atractivo, pero como parte del código aún no se convirtió de try! , debería ser evidente que era una solución. Eso plantea la cuestión de si pretendemos tener async/await como incentivo para migrar a la edición 2018 o más bien ser pacientes y no repetir esto.

Otras dos características centrales (en mi humilde opinión) muy parecidas a 3. : vec![] y format! / println! . El primero en gran medida porque no hay una construcción en caja estable afaik, el segundo debido a la construcción de cadenas de formato y no tener expresiones de tipo dependiente. Creo que estas comparaciones también ponen parcialmente 4. en otra perspectiva.

Me opongo a cualquier sintaxis que no se lea como el inglés. Es decir, "await x" dice algo como inglés. "x # !! @! &" no lo hace. "x.await" se lee tentadoramente como en inglés, pero no lo hará cuando x es una línea no trivial, como una llamada a una función miembro con nombres largos, o un montón de métodos iteradores encadenados, etc.

Más específicamente, apoyo la "palabra clave x", donde la palabra clave es probablemente await . Vengo de usar tanto las corrutinas TS de C ++ como las corrutinas c # de unity, que usan una sintaxis muy similar a esa. Y después de años de usarlos en código de producción, creo que saber dónde están sus puntos de rendimiento, de un vistazo, es absolutamente crítico . Cuando revisa la línea de sangría de su función, puede seleccionar cada co_await / yield return en una función de 200 líneas en cuestión de segundos, sin carga cognitiva.

No ocurre lo mismo con el operador de punto con await después, o con alguna otra sintaxis de "pila de símbolos" de sufijo.

Creo que await es una operación fundamental de control de flujo. Se le debe dar el mismo nivel de respeto que 'if , while , match y return . Imagínese si alguno de esos fueran operadores de posfijo: leer el código de Rust sería una pesadilla. Al igual que con mi argumento para await, tal como está, puede hojear la línea de sangría de cualquier función de Rust e inmediatamente seleccionar todo el flujo de control. Hay excepciones, pero son excepciones, y no es algo por lo que debamos esforzarnos.

Estoy de acuerdo con @ejmahler. No debemos olvidarnos del otro lado del desarrollo: la revisión del código. El archivo con código fuente se lee con mucha más frecuencia que se escribe, por lo que creo que debería ser más fácil de leer y comprender que escribir. Encontrar los puntos de rendimiento es realmente importante en la revisión del código. Y personalmente votaría por Useful precedence .
Creo que esto:

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

es más fácil de entender que esto:

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

@HeroicoKatora

@Centril Me gustaría estar de acuerdo, pero hay algunas preguntas abiertas. ¿Estás seguro de 1. ? Si lo hacemos "mágico", ¿no podríamos hacerlo aún más mágico permitiendo que esto se refiera a una macro sin descartarla como palabra clave?

¿Técnicamente? Posiblemente. Sin embargo, debería haber una fuerte justificación para casos especiales y en este caso tener magia sobre magia no parece justificado.

Eso plantea la cuestión de si pretendemos tener async/await como incentivo para migrar a la edición 2018 o más bien ser pacientes y no repetir esto.

No sé si alguna vez dijimos que teníamos la intención de un nuevo sistema de módulos, async / await y try { .. } como incentivos; pero independientemente de nuestra intención, lo son, y creo que eso es algo bueno. Queremos que la gente eventualmente comience a usar nuevas funciones de lenguaje para escribir bibliotecas mejores y más idiomáticas.

Otras dos características centrales (en mi humilde opinión) muy parecidas a 3. : vec![] y format! / println! . El primero mucho porque no hay una construcción en caja estable afaik,

El primero existe y está escrito vec![1, 2, 3, ..] , para imitar expresiones literales de matriz, por ejemplo, [1, 2, 3, ..] .

@ejmahler

"x.await" se lee tentadoramente como en inglés, pero no lo hará cuando x es una línea no trivial, como una llamada a una función miembro con nombres largos, o un montón de métodos iteradores encadenados, etc.

¿Qué pasa con un montón de métodos iteradores encadenados? Eso es claramente idiomático Rust.
La herramienta rustfmt también formateará cadenas de métodos en diferentes líneas para que obtenga (nuevamente usando match para mostrar el resaltado de sintaxis):

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

Si lee .await como "entonces aguarde", se lee perfectamente, al menos para mí.

Y después de años de usarlos en código de producción, creo que saber dónde están sus puntos de rendimiento, de un vistazo, es absolutamente crítico .

No veo cómo el postfix await niega eso, especialmente en el formato rustfmt anterior. Además, puedes escribir:

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

si te apetece.

en una función de 200 líneas en cuestión de segundos, sin carga cognitiva.

Sin saber demasiado sobre la función en particular, me parece que 200 LOC probablemente viola el principio de responsabilidad única y hace demasiado. La solución es hacer que haga menos y dividirlo. De hecho, creo que es lo más importante para la facilidad de mantenimiento y la legibilidad.

Creo que await es una operación fundamental de control de flujo.

También lo es ? . De hecho, await y ? son operaciones de flujo de control efectivas que dicen "extraer valor fuera de contexto". En otras palabras, en el contexto local, puede imaginar que estos operadores tienen el tipo await : impl Future<Output = T> -> T y ? : impl Try<Ok = T> -> T .

Hay excepciones, pero son excepciones, y no es algo por lo que debamos esforzarnos.

¿Y la excepción aquí es ? ?

@andreytkachenko

Estoy de acuerdo con @ejmahler. No debemos olvidarnos del otro lado del desarrollo: la revisión del código. El archivo con código fuente se lee con mucha más frecuencia que se escribe, por lo que creo que debería ser más fácil de leer y comprender que escribir.

El desacuerdo gira en torno a lo que sería mejor para la legibilidad y la ergonomía.

es más fácil de entender que esto:

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

No es así como se formateará; ejecutarlo a través de rustfmt da:

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

@ejmahler @andreytkachenko Estoy de acuerdo con @Centril aquí, el mayor cambio (algunos pueden decir una mejora, yo no lo haría) que obtienes de la sintaxis de prefijos es que los usuarios están incentivados a dividir sus declaraciones en varias líneas porque todo lo demás es ilegible. Eso no es Rust-y y las reglas de formato habituales lo compensan en la sintaxis posterior a la corrección. También considero que el punto de rendimiento es más oscuro en la sintaxis de prefijo porque await no se coloca realmente en el punto de código donde cede, sino que se opone a él.

Si va de esta manera, experimentemos por el simple hecho de deletrearlo, más bien con await como reemplazo de let en el espíritu de la idea de @Keruspe de hacer cumplir esto. Sin las otras extensiones de sintaxis porque parecen exageradas.

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

Pero para ninguno de estos veo suficientes beneficios para explicar su pérdida de componibilidad y complicaciones en la gramática.

Hm ... ¿es deseable tener tanto un prefijo como un sufijo de espera? ¿O simplemente la forma del sufijo?

Encadenar llamadas a métodos es idiomático específicamente cuando se habla de
iteradores y, en mucha menor medida, opciones, pero aparte de eso, el óxido es
primero un lenguaje imperativo y luego un lenguaje funcional.

No estoy argumentando que un sufijo es literalmente incomprensible, estoy haciendo un
argumento de carga cognitiva que oculta una operación de flujo de control de nivel superior,
con la misma importancia que el 'retorno', aumenta la carga cognitiva cuando
en comparación con ponerlo lo más cerca del principio de la línea, y estoy
haciendo este argumento basado en años de experiencia en producción.

El sábado 19 de enero de 2019 a las 11:59 a.m. Mazdak Farrokhzad [email protected]
escribió:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril Me gustaría estar de acuerdo pero hay
algunas preguntas abiertas. ¿Estás seguro de 1.? Si lo hacemos 'mágico' podría
no lo hacemos aún más mágico permitiendo que esto se refiera a una macro sin
¿Dejarlo como palabra clave?

¿Técnicamente? Posiblemente. Sin embargo, debería haber una fuerte justificación para
casos especiales y en este caso tener magia sobre magia no parece
justificado.

Eso plantea la cuestión de si pretendemos tener async / await como
un incentivo para migrar a la edición 2018 o más bien ser paciente y no
repita esto.

No sé si alguna vez dijimos que teníamos la intención de un nuevo sistema de módulos, async /
esperar y tratar de {..} ser incentivos; pero independientemente de nuestra intención
lo son, y creo que eso es algo bueno. Queremos que la gente eventualmente
empezar a usar nuevas funciones de lenguaje para escribir mejor y más idiomático
Bibliotecas.

Otras dos (en mi humilde opinión) características centrales muy parecidas a 3 .: vec! [] Y format! /
println !. El primero mucho porque no hay caja estable
construcción afaik,

El primero existe y se escribe vec! [1, 2, 3, ..], para imitar una matriz
expresiones literales, por ejemplo, [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" se lee tentadoramente como en inglés, pero no lo hará cuando x es un
línea no trivial, como una llamada de función miembro con nombres largos, o un montón
de métodos iteradores encadenados, etc.

¿Qué pasa con un montón de métodos iteradores encadenados? Eso es claramente
óxido idiomático.
La herramienta rustfmt también formateará cadenas de métodos en diferentes líneas para que
get (nuevamente usando match para mostrar el resaltado de sintaxis):

dejar foo = alpha (). coincidir? // o alpha() match? , alpha()#match? , alpha().match!()?
.beta
.algunos_otros_cosidos ().
.even_more_stuff (). coincidencia
.cosas_y_cosas ();

Si lee .await como "luego aguarde", se lee perfectamente, al menos para mí.

Y después de años de usarlos en código de producción, creo que saberdónde están sus puntos de rendimiento, de un vistazo, es absolutamente crítico .

No veo cómo postfix await niega eso, especialmente en el rustfmt
formato de arriba. Además, puedes escribir:

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

si te apetece.

en una función de 200 líneas en cuestión de segundos, sin carga cognitiva.

Sin saber demasiado sobre la función en particular, me parece
que 200 LOC probablemente viola el principio de responsabilidad única y no
demasiado. La solución es hacer que haga menos y dividirlo. en efecto, yo
Creo que eso es lo más importante para la facilidad de mantenimiento y la legibilidad.

Creo que await es una operación fundamental de control de flujo.

Asi es ?. De hecho, ¿esperar y? son operaciones de flujo de control efectivas
que dicen "extraer valor fuera de contexto". En otras palabras, en el local
contexto, puede imaginar que estos operadores tienen el tipo await: impl
Futuro-> T y? : impl Prueba-> T.

Hay excepciones, pero son excepciones y no es algo que debamos
luchar por.

¿Y la excepción aquí es? ?

@andreytkachenko https://github.com/andreytkachenko

Estoy de acuerdo con @ejmahler https://github.com/ejmahler . No deberíamos
Olvídese del otro lado del desarrollo: revisión del código. El archivo con código fuente es
mucho más a menudo se lee que se escribe, por lo que creo que debería ser más fácil
leer y comprender luego escribir.

El desacuerdo gira en torno a lo que sería mejor para la legibilidad y
ergonomía.

es más fácil de entender que esto:

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

No es así como se formateará; el formato idiomático rustfmt
es:

dejar cuerpo: MyResponse = cliente
.get ("https: // my_api")
.enviar()
.¿partido?
.into_json ()
.¿partido?;

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba
.

@ejmahler No estamos de acuerdo. "ocultación"; los mismos argumentos se hicieron wrt.

El operador ? es muy corto y ha sido criticado en el pasado por "ocultar" una devolución. La mayoría de las veces, el código se lee, no se escribe. Cambiarle el nombre a ?! hará que sea dos veces más largo y, por lo tanto, más difícil de pasar por alto.

Sin embargo, finalmente estabilizamos ? y desde entonces creo que la profecía no se ha materializado.
Para mí, el sufijo await sigue el orden de lectura natural (al menos para hablantes de idiomas de izquierda a derecha). En particular, sigue el orden de flujo de datos.

Sin mencionar el resaltado de sintaxis: todo lo relacionado con la espera se puede resaltar con un color brillante, por lo que se puede encontrar de un vistazo. Entonces, incluso si tuviéramos un símbolo en lugar de la palabra await real, aún sería muy legible y encontrable en el código resaltado de sintaxis. Dicho esto, sigo prefiriendo el uso de la palabra await solo por razones de grepping; es más fácil grepizar el código para cualquier cosa que se esté esperando si solo usamos la palabra await lugar de un símbolo como @ o #, cuyo significado depende de la gramática.

todos ustedes esto no es ciencia espacial

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

postfix ... es extremadamente legible, difícil de perder de un vistazo y súper intuitivo, ya que naturalmente lo lee como el código que se apaga mientras espera que el resultado del futuro esté disponible. no se necesitan travesuras de precedencia / macro y no hay ruido de línea adicional de sigilos desconocidos, ya que todos han visto elipses antes.

(disculpas a @solson)

@ ben0x539 ¿ future()....start ? ¿O esperar un resultado de rango como range()..... ? ¿Y a qué te refieres exactamente con no precedence/macro shenanigans necessary and ya que actualmente la elipsis .. requiere ser un operador binario o paranthesis a la derecha y esto es terriblemente cercano de un vistazo?

Sí el ? El operador existe. Ya reconocí que había
excepciones. Pero es una excepción. La gran mayoría del flujo de control en cualquier
El programa Rust se realiza mediante palabras clave de prefijo.

El sábado 19 de enero de 2019 a las 1:51 p.m. Benjamin Herr [email protected]
escribió:

todos ustedes esto no es ciencia espacial

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

postfix ... es extremadamente legible, difícil de perder de un vistazo y super
intuitivo ya que naturalmente lo lees como el código que se apaga
mientras espera que el resultado del futuro esté disponible. No
precedencia / macro chanchullos necesarios y sin ruido de línea adicional de
sigilos desconocidos, ya que todo el mundo ha visto elipses antes.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455818177 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba
.

@HeroicKatora Eso parece un poco artificial, pero sí, claro. Quise decir que, dado que es una operación de posfijo, como las otras soluciones de posfijo sugeridas, evita la necesidad de una precedencia contraria a la intuición para await x? , y no es una macro.

@ejmahler

Sí el ? El operador existe. Ya reconocí que había excepciones. Pero es una excepción.

Hay dos formas de expresión que se ajustan a keyword expr , a saber, return expr y break expr . El primero es más común que el segundo. La forma continue 'label realmente no cuenta ya que, si bien es una expresión, no tiene la forma keyword expr . Así que ahora tiene 2 formas de expresión unaria de palabra clave prefija completa y 1 forma de expresión unaria de sufijo. Antes incluso de tener en cuenta que ? y await son más similares que await y return , difícilmente llamaría return/break expr una regla para que ? sea ​​una excepción.

La gran mayoría del flujo de control en cualquier programa de Rust ocurre mediante palabras clave de prefijo.

Como se mencionó anteriormente, break expr no es tan común ( break; es más típico y return; son más típicos y no son formas de expresión unarias). Lo que queda son los primeros return expr; sy no me parece nada claro que esto sea mucho más común que match , ? , simplemente anidados if let s y else s, y for bucles. Una vez que try { .. } estabilice, esperaría que ? se use aún más.

@ ben0x539 Creo que deberíamos reservar ... para genéricos variadic, una vez que estemos listos para tenerlos

Realmente me gusta la idea de innovar con la sintaxis postfix aquí. Tiene mucho más sentido con el flujo y recuerdo cuánto mejor cambió el código cuando pasamos del prefijo try! al sufijo ? . Creo que hay mucha gente que hizo la experiencia en la comunidad de Rust de cuántas mejoras en el código hizo.

Si no nos gusta la idea de .await , estoy seguro de que se puede hacer algo de creatividad para encontrar un operador de sufijo real. Un ejemplo podría ser usar ++ o @ para await.

:( Simplemente no quiero esperar más.

Todos se sienten cómodos con la sintaxis macro, la mayoría de las personas en este hilo que comienzan con otras opiniones parecen terminar favoreciendo la sintaxis macro.

Seguro que será una "macro mágica", pero los usuarios rara vez se preocupan por cómo se ve la expansión de macro y para aquellos que lo hacen, es bastante fácil explicar los matices en los documentos.

La sintaxis de macros normal es como un pastel de manzana, es la segunda opción favorita de todos, pero como resultado la opción favorita de la familia [0]. Es importante destacar que, como con try! siempre podemos cambiarlo más tarde. Pero lo más importante es que cuanto antes estemos todos de acuerdo, antes podremos empezar a utilizarlo y ser productivos.

[0] (Referenciado en el primer minuto) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

Match, if, if let, while, while let y for son todos un flujo de control omnipresente
que usan prefijos. Fingir romper y continuar es el único flujo de control
palabras clave es frustrantemente engañoso.

El sábado 19 de enero de 2019 a las 3:37 p.m. Yazad Daruvala [email protected]
escribió:

:( Simplemente no quiero esperar más.

Todos se sienten cómodos con la sintaxis de macros, la mayoría de las personas en este hilo que
Empecemos por otras opiniones que parecen acabar favoreciendo la sintaxis macro.

Seguro que será una "macro mágica", pero a los usuarios rara vez les importa lo que la macro
la expansión parece y para aquellos que lo hacen, es bastante fácil de explicar el
matiz en los documentos.

La sintaxis de macros normal es como una tarta de manzana, es la segunda
opción favorita, pero como resultado la opción favorita de la familia [0].
Es importante destacar que, como con try! siempre podemos cambiarlo más tarde. Pero más
lo que es más importante, cuanto antes estemos todos de acuerdo, antes podremos empezar
¡usándolo y sea productivo!

[0] (referenciado en el primer minuto)
https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-455824275 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba
.

@mitsuhiko ¡ Estoy de acuerdo! Postfix se siente más rústico debido al encadenamiento. Creo que la sintaxis fut@await que propuse es otra opción interesante que no parece tener tantas desventajas como otras propuestas. Sin embargo, no estoy seguro si está demasiado lejos y sería preferible una versión más realista.

@ejmahler

match, if, if let, while, while let y for son todos flujos de control omnipresentes que usan prefijos. Fingir romper y continuar son las únicas palabras clave del flujo de control es frustrantemente engañoso.

No es engañoso en absoluto. La gramática relevante para estas construcciones es aproximadamente:

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

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

ElseExpr =
  | Block:Block
  | If:If
  ;

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

Aquí, los formularios son if/while expr block , for pat in expr block y match expr { pat0 => expr0, .., patn => exprn } . Hay una palabra clave que precede a lo que sigue en todas estas formas. Supongo que esto es lo que quieres decir con "usa prefijos". Sin embargo, todas estas son formas de bloque y no operadores de prefijo unarios. Por tanto, la comparación con await expr es engañosa, ya que no hay coherencia o regla de la que hablar. Si busca coherencia con los formularios en bloque, compárelo con await block , no con await expr .

@mitsuhiko ¡ Estoy de acuerdo! Postfix se siente más rústico debido al encadenamiento.

El óxido es dualista. Es compatible con enfoques tanto imperativos como funcionales. Y creo que está bien, porque en diferentes casos, cada uno de ellos puede ser más adecuado.

No lo sé. Parece que sería genial tener ambos:

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

Después de haber usado Scala durante un tiempo, también me gustaba mucho que muchas cosas funcionaran así. Especialmente match y if sería bueno tener en posiciones postfijas en Rust.

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

Resumen hasta ahora

Matrices de opciones:

Matriz de resumen de opciones (usando @ como sigilo, pero podría ser casi cualquier cosa):

| Nombre | Future<T> | Future<Result<T, E>> | Result<Future<T>, E> |
| --- | --- | --- | --- |
| PREFIJO | - | - | - |
| Macro de palabras clave | await!(fut) | await!(fut)? | await!(fut?) |
| Función de palabra clave | await(fut) | await(fut)? | await(fut?) |
| Precedencia útil | await fut | await fut? | await (fut?) |
| Prioridad obvia | await fut | await? fut | await fut? |
| POSTFIX | - | - | - |
| Fn con palabra clave | fut(await) | fut(await)? | fut?(await) |
| Campo de palabra clave | fut.await | fut.await? | fut?.await |
| Método de palabra clave | fut.await() | fut.await()? | fut?.await() |
| Macro de palabras clave Postfix | fut.await!() | fut.await!()? | fut?.await!() |
| Espacio palabra clave | fut await | fut await? | fut? await |
| Sigil Palabra clave | fut@await | fut@await? | fut?@await |
| Sigil | fut@ | fut@? | fut?@ |

El sigilo de "Sigil Keyword" _no puede_ ser # , ya que entonces no podrías hacerlo con un futuro llamado r . ... ya que el sigilo no tendría que cambiar la tokenización como mi primera preocupación .

Más uso en la vida real (PM me otros casos de uso _real_ con múltiples await en urlo y los

| Nombre | (reqwest) Client |> Client::get |> RequestBuilder::send |> await |> ? |> Response::json | > ? |
| --- | --- |
| PREFIJO | - |
| Macro de palabras clave | await!(client.get("url").send())?.json()? |
| Función de palabra clave | await(client.get("url").send())?.json()? |
| Precedencia útil | (await client.get("url").send()?).json()? |
| Prioridad obvia | (await? client.get("url").send()).json()? |
| POSTFIX | - |
| Fn con palabra clave | client.get("url").send()(await)?.json()? |
| Campo de palabra clave | client.get("url").send().await?.json()? |
| Método de palabra clave | client.get("url").send().await()?.json()? |
| Macro de palabras clave Postfix | client.get("url").send().await!()?.json()? |
| Espacio palabra clave | client.get("url").send() await?.json()? |
| Sigil Palabra clave | client.get("url").send()@await?.json()? |
| Sigil | client.get("url").send()@?.json()? |

NOTA DE EDICIÓN: Se me ha señalado que puede tener sentido que Response::json también devuelva un Future , donde send espera el IO saliente y json (u otra interpretación del resultado) espera el IO entrante. Sin embargo, dejaré este ejemplo como está, ya que creo que es significativo mostrar que el problema del encadenamiento se aplica incluso con un solo punto de espera IO en la expresión.

Parece haber un consenso aproximado de que de las opciones de prefijo, la precedencia obvia (junto con el azúcar await? ) es la más deseable. Sin embargo, muchas personas se han pronunciado a favor de una solución postfix, para facilitar el encadenamiento como el anterior. Aunque la elección del prefijo tiene un consenso aproximado, no parece haber consenso sobre qué solución de prefijo es la mejor. Todas las opciones propuestas conducen a una confusión fácil (mitigada por el resaltado de palabras clave):

  • Fn con palabra clave => llamando a fn con un argumento llamado await
  • Campo de palabra clave => acceso al campo
  • Método de palabra clave => llamada al método
  • Macro (prefijo o sufijo) => ¿ await una palabra clave o no?
  • Espacio palabra clave => divide la agrupación en una sola línea (¿mejor en varias líneas?)
  • Sigil => agrega nuevos sigilos a un lenguaje que ya se percibe como un sigil pesado

Otras sugerencias más drásticas:

  • Permita tanto el prefijo (precedencia obvia) como el sufijo "campo" (esto podría aplicarse a más palabras clave como match , if , etc.en el futuro para hacer de este un patrón generalizado, pero es innecesario adición a este debate) [[referencia] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455827164)]
  • await en patrones para resolver futuros (sin encadenamiento) [[referencia] (https://github.com/rust-lang/rust/issues/57640)]
  • Utilice un operador de prefijo pero permita retrasarlo [[referencia] (https://github.com/rust-lang/rust/issues/57640#issuecomment-455782394)]

La estabilización con una macro de palabras clave await!(fut) es, por supuesto, compatible en el futuro con básicamente todo lo anterior, aunque eso requiere hacer que la macro utilice una palabra clave en lugar de un identificador regular.

Si alguien tiene un ejemplo mayoritariamente real que usa dos await en una cadena, me encantaría verlo; nadie ha compartido uno hasta ahora. Sin embargo, el sufijo await también es útil incluso si no necesita await más de una vez en una cadena, como se muestra en el ejemplo reqwest.

Si me perdí algo notable arriba de este comentario de resumen, envíame un mensaje de correo electrónico a urlo e intentaré agregarlo (aunque requeriré que agregue los comentarios de otra persona para evitar el favoritismo en voz alta).

Personalmente, históricamente he estado a favor de la palabra clave de prefijo con la precedencia obvia. Sigo pensando que estabilizar con una macro de palabras clave await!(fut) sería útil para recopilar información del mundo real sobre dónde ocurre la espera en los casos de uso del mundo real, y aún nos permitiría agregar una opción de prefijo o sufijo no macro más adelante .

Sin embargo, en el proceso de redacción del resumen anterior, me empezó a gustar "Campo de palabras clave". "Palabra clave de espacio" se siente bien cuando se divide en varias líneas:

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

pero en una línea, produce una ruptura incómoda que agrupa mal la expresión: client.get("url").send() await?.json()? . Sin embargo, con el campo de palabra clave, se ve bien en ambas formas: client.get("url").send().await?.json()?

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

aunque supongo que un "método de palabra clave" fluiría mejor, ya que es una acción. Incluso podríamos convertirlo en un método "real" en Future si quisiéramos:

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

( extern "rust-await" , por supuesto, implicaría toda la magia necesaria para hacer la espera y en realidad no sería un fn real, principalmente estaría allí porque la sintaxis parece un método, si una palabra clave se utiliza el método.)

Permitir tanto el prefijo (precedencia obvia) como el sufijo "campo" ...

Si se selecciona cualquier sintaxis de sufijo (no importa si están juntos o en lugar del prefijo), definitivamente sería un argumento para una discusión futura: ahora tenemos palabras clave que funcionan tanto en notación de prefijo como de sufijo, precisamente porque a veces es preferible una sobre el otro, por lo que tal vez podríamos permitir ambos donde tenga sentido y aumentar la flexibilidad de la sintaxis, mientras unificamos las reglas. Tal vez sea una mala idea, tal vez sea rechazada, pero definitivamente es una discusión que se tendrá en el futuro, si se usa la notación postix para await .

Creo que hay muy pocas posibilidades de que una sintaxis que no incluya la cadena de caracteres await sea aceptada para esta sintaxis.

: +1:


Un pensamiento al azar que tuve después de ver un montón de ejemplos aquí ( como el de @mehcode ): Una de las quejas que recuerdo sobre .await es que es demasiado difícil de ver †, pero dado que las cosas que se esperan son típicamente falible, el hecho de que a menudo sea .await? ayuda a llamar la atención de todos modos.

† Si usa algo que no resalta las palabras clave


@ejmahler

Me opongo a cualquier sintaxis que no se lea como el inglés

Algo como request.get().await lee tan bien como algo como body.lines().collect() . En "un montón de métodos iteradores encadenados", creo que _prefix_ en realidad se lee peor, ya que debes recordar que dijeron "esperar" al principio, y nunca sabes cuándo escuchas algo si va a ser lo que estás esperando, algo así como una frase del camino del jardín .

Y después de años de usarlos en código de producción, creo que saber dónde están sus puntos de rendimiento, de un vistazo, es absolutamente crítico. Cuando revisa la línea de sangría de su función, puede seleccionar cada retorno de co_await / yield en una función de 200 líneas en cuestión de segundos, sin carga cognitiva.

Esto implica que nunca hay ninguna dentro de una expresión, lo cual es una restricción que absolutamente no apoyaría, dada la naturaleza orientada a la expresión de Rust. Y al menos con await C #, es absolutamente plausible tener CallSomething(argument, await whatever.Foo() .

Dado que async _will_ aparecerá en el medio de las expresiones, no entiendo por qué es más fácil de ver en el prefijo que en el sufijo.

Se le debe dar el mismo nivel de respeto que 'si, mientras, coinciden y regresan'. Imagínese si alguno de esos fueran operadores de posfijo: leer el código de Rust sería una pesadilla.

return (y continue y break ) y while son notables como _completamente_ inútiles para encadenar, ya que siempre devuelven ! y () . Y aunque por alguna razón omitió for , hemos visto código escrito muy bien usando .for_each() sin efectos negativos, particularmente en rayón .

Probablemente necesitemos hacer las paces con el hecho de que async/await será una característica importante del idioma. Aparecerá en todo tipo de código. Impregnará el ecosistema; en algunos lugares será tan común como ? . La gente tendrá que aprenderlo.

En consecuencia, es posible que deseemos centrarnos conscientemente en cómo se sentirá la elección de sintaxis después de usarla durante mucho tiempo en lugar de cómo se sentirá al principio.

También debemos entender que en lo que respecta a las construcciones de flujo de control, await es un tipo diferente de animal. Construcciones como return , break , continue , e incluso yield pueden entenderse intuitivamente en términos de jmp . Cuando los vemos, nuestros ojos rebotan en la pantalla porque el flujo de control que nos importa se mueve hacia otra parte. Sin embargo, aunque await afecta el flujo de control de la máquina, no mueve el flujo de control que es importante para nuestros ojos y para nuestra comprensión intuitiva del código.

No estamos tentados a encadenar llamadas incondicionales a return o break porque eso no tendría sentido. Por razones similares, no nos sentimos tentados a cambiar nuestras reglas de precedencia para adaptarlas. Estos operadores tienen poca precedencia. Toman todo a la derecha y lo devuelven a algún lugar, terminando la ejecución dentro de esa función o bloque. Sin embargo, el operador await quiere estar encadenado. Es una parte integral de una expresión, no el final de ella.

Habiendo considerado la discusión y los ejemplos en este hilo, me quedo con la sensación de que viviríamos para lamentar las sorprendentes reglas de precedencia.

El candidato del caballo acechador parece ir con await!(expr) por ahora y espera que algo mejor se resuelva más adelante. Antes de leer los comentarios de @Centril , probablemente habría apoyado esto con el interés de sacar esta importante característica con casi cualquier sintaxis. Sin embargo, sus argumentos me convencen de que esto simplemente estaría saliendo adelante. Sabemos que el encadenamiento de llamadas a métodos es importante en Rust. Eso impulsó la adopción de la palabra clave ? , que es muy popular y muy exitosa. Usar una sintaxis que sabemos que nos decepcionará es, de hecho, solo agregar deuda técnica.

Al principio de este hilo, @withoutboats indicó que solo cuatro opciones existentes parecen viables. De ellos, es probable que solo la sintaxis del sufijo expr await nos haga felices a largo plazo. Esta sintaxis no crea extrañas sorpresas de precedencia. No nos obliga a crear una versión de prefijo del operador ? . Funciona muy bien con el encadenamiento de métodos y no interrumpe el flujo de control de izquierda a derecha. Nuestro exitoso operador ? sirve como precedente para un operador postfijo, y await se parece más a ? en la práctica que a return , break , o yield . Si bien un operador sin símbolo de sufijo puede ser nuevo en Rust, el uso de async/await estará lo suficientemente extendido como para hacerlo familiar rápidamente.

Si bien todas las opciones para una sintaxis de sufijo parecen factibles, expr await tiene algunas ventajas. Esta sintaxis deja en claro que await es una palabra clave, lo que ayuda a enfatizar el flujo de control mágico. En comparación con expr.await , expr.await() expr.await! , expr.await!() , etc., esto evita tener que explicar que esto parece un campo / método / macro, pero realmente no es en este caso especial. Todos nos acostumbraríamos al separador de espacios aquí.

Deletrear await como @ o usar algún otro símbolo que no cause problemas de análisis es atractivo. Sin duda, es un operador lo suficientemente importante como para garantizarlo. Pero si, al final, tiene que escribirse await , estará bien. Siempre que esté en posición postfija.

Como alguien mencionó ejemplos de _real_ ... mantengo una base de código de óxido de 23,858 líneas (según tokei) que es muy asíncrona y usa futuros 0.1 await (altamente experimental, lo sé). Vamos (redactado) a espeleología (tenga en cuenta que todo se ha ejecutado a través de rustfmt):

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

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

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

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

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

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

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

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

    Ok(user)
}

Ahora transformemos esto en la variante de prefijo más popular, la precedencia obvia con el azúcar. Por razones obvias, esto no se ha ejecutado a través de rustfmt, así que me disculpo si hay una mejor manera de escribirlo.

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

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

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

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

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

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

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

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

    Ok(user)
}

Finalmente, transformemos esto en mi variante de sufijo favorito, "campo de sufijo".

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

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

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

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

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

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

    Ok(user)
}

Después de este ejercicio, encuentro varias cosas.

  • Ahora estoy fuertemente en contra de await? foo . Se lee bien para expresiones simples, pero ? siente perdido en expresiones complejas. Si debemos hacer un prefijo, prefiero tener una precedencia "útil".

  • El uso de la notación postfija me lleva a unir declaraciones y reducir los enlaces let innecesarios.

  • Usar la notación postfix _field_ me lleva a preferir fuertemente que .await? aparezca en la línea de lo que espera en lugar de en su propia línea en el lenguaje rustfmt.

Aprecio la notación de elipses postfijo "..." anterior, tanto por su concisión como simbólicamente en el idioma inglés que representa una pausa en anticipación de algo más. (¡Como funciona el comportamiento asíncrono!), También encadena bien.

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

Dudo que otras opciones sean tan concisas y visualmente significativas.

UN

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

segundo

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

¿Debería considerarse una buena forma tener largas cadenas de espera?

¿Por qué no usar simplemente combinadores Future regulares?

De hecho, algunas expresiones no se traducen bien en cadenas de espera si desea tener comportamientos de respaldo en caso de falla.

Personalmente creo esto:

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

value.unwrap()

lee mucho más naturalmente que esto:

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

value.unwrap()

Después de todo, todavía tenemos todo el poder de los futuros de costo cero en nuestras manos.

Considere eso.

@EyeOfPython Me gustaría @ en future@await . Podemos escribir future~await , donde ~ se trabaja como un semi guión, y funcionaría para cualquier posible operador de sufijo.

El guión - ya se usó como operador menos y operador negativo. Ya no es bueno. Pero ~ se usó para indicar objetos de montón en Rust y, por lo demás, apenas se usó en ningún lenguaje de programación. Debería generar menos confusiones para las personas de otros idiomas.

@earthengine Buena idea, pero tal vez solo use future~ que significa esperar un futuro, donde ~ funciona como la palabra clave await . (como ? símbolo

Futuro | Futuro del resultado | Resultado del futuro
- | - | -
futuro ~ | futuro ~? | futuro? ~

También futuros encadenados como:

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

Eché un vistazo a cómo Go implementa la programación asincrónica y encontré algo interesante allí. La alternativa más cercana a los futuros en Go son los canales. Y en lugar de await u otra sintaxis de gritos para esperar valores, los canales Go solo proporcionan el operador <- para ese propósito. Para mí, parece bastante limpio y sencillo. Anteriormente, a menudo he visto cómo la gente elogia a Go por su sintaxis simple y sus buenas instalaciones asíncronas, por lo que definitivamente es una buena idea aprender algo de su experiencia.

Desafortunadamente, no pudimos tener exactamente la misma sintaxis porque hay muchas más llaves angulares en el código fuente de Rust que en Go, principalmente debido a los genéricos. Esto hace que el operador <- realmente sutil y no sea agradable trabajar con él. Otra desventaja es que podría verse como opuesto a -> en la firma de la función y no hay razón para considerarlo así. Y otra desventaja más es que <- sigil estaba destinado a implementarse como placement new , por lo que la gente podría malinterpretarlo.

Entonces, después de algunos experimentos con la sintaxis, me detuve en <-- sigil:

let output = <-- future;

En el contexto async <-- es bastante sencillo, aunque menos de <- . Pero, en cambio, proporciona una gran ventaja sobre <- así como sobre el prefijo await : funciona bien con sangría.

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

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

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

Para mí, este operador parece aún más único y más fácil de detectar que await (que se parece más a cualquier otra palabra clave o variable en el contexto del código). Desafortunadamente, en Github parece más delgado que en mi editor de código, pero creo que eso no es fatal y podemos vivir con eso. Incluso si alguien se siente incómodo, el resaltado de sintaxis diferente o una mejor fuente (especialmente con ligaduras) resolverán todos los problemas.

Otra razón para que me guste esta sintaxis es porque conceptualmente podría expresarse como "algo que aún no está aquí". La dirección de derecha a izquierda de la flecha es opuesta a la dirección en la que leemos el texto, lo que nos permite describirlo como "algo que viene del futuro". La forma larga del operador <-- también sugiere que "comienza una operación duradera". Los corchetes angulares y dos guiones dirigidos desde future podrían simbolizar "sondeo continuo". Y todavía podemos leerlo como "esperando" como lo era antes.
No es una especie de iluminación, pero puede ser divertido.


Lo más importante de esta propuesta es que también sería posible el encadenamiento de métodos ergonómicos. La idea del operador de prefijo retrasado que propuse anteriormente encaja bien aquí. De esta manera tendríamos lo mejor de ambos mundos de prefijo y sufijo await sintaxis. Realmente espero que también se introduzcan algunos extras útiles que personalmente quería en muchas ocasiones antes: desreferenciación retardada y sintaxis de negación retardada .

No estoy seguro de si la palabra

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

La precedencia del operador parece bastante obvia: de izquierda a derecha.

Espero que esta sintaxis reduzca la necesidad de escribir funciones is_not_* cuyo propósito es solo negar y devolver una propiedad bool . Y las expresiones booleanas / de eliminación de referencias en algunos casos serán más limpias al usarlas.


Finalmente, lo he aplicado en ejemplos del mundo real publicados por @mehcode y me gusta cómo <-- hace el énfasis adecuado en la función async dentro de las cadenas de llamadas de métodos. Por el contrario, el sufijo await simplemente se ve como un acceso de campo normal o una llamada de función (dependiendo de la sintaxis) y es casi imposible distinguirlos sin un resaltado o formato de sintaxis especial.

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

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

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

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

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

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

    Ok(user)
}

Después de todo: esa es la sintaxis que quiero usar.

@novacrazy

¿Debería considerarse una buena forma tener largas cadenas de espera? ¿Por qué no utilizar simplemente combinadores Future regulares?

No estoy seguro si no te entendí mal, pero esos no son exclusivos, aún puedes usar combinadores también

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

Pero para que esto funcione, la cláusula and_then debe escribirse en FnOnce(T) -> impl Future<_> , no solo en FnOnce(T) -> U . Haciendo combinadores encadenados en el futuro y el resultado solo funciona limpiamente sin paranthesis en postfix:

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

En esta publicación me centraré en la cuestión de la precedencia del

  • Llamada al método ( future.await() )
  • Expresión de campo ( future.await )
  • Llamada a función ( future(await) )

Las diferencias en funcionalidad son bastante pequeñas pero existentes. Tenga en cuenta que aceptaría todos estos, esto es principalmente un ajuste fino. Para mostrarlos todos, necesitamos algunos tipos. Por favor, no comente sobre la artificiosidad del ejemplo, esta es la versión más comprimida que muestra todas las diferencias a la vez.

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

// What we want to do, in macro syntax:
let foo: Foo<_, _, _> = …;
(await!(foo.member))(42)
  • Llamada de método: foo.member.await()(42)
    Se une con más fuerza, por lo que no hay ninguna parátesis
  • Miembro: (foo.member.await)(42)
    Necesita paranttesis alrededor del resultado esperado cuando este es un invocable, esto es consistente con tener un invocable como miembro, de lo contrario confusión con la función de llamada a miembro. Esto también sugiere que uno podría desestructurar con patrones: let … { await: value } = foo.member; value(42) alguna manera?
  • Llamada a función: (foo.member)(await)(42)
    Necesita paranthesis para desestructurar (movemos miembro) ya que se comporta como llamada de función.

Todos ellos tienen el mismo aspecto cuando no desestructuramos una estructura de entrada mediante el movimiento de un miembro, ni llamamos al resultado como un invocable, ya que estas tres clases de precedencia vienen directamente una tras otra. ¿Cómo queremos que se comporten los futuros?

El mejor paralelo a la llamada al método debería ser simplemente otra llamada al método tomando self .

El mejor paralelo al miembro es una estructura que solo tiene el miembro implícito await y, por lo tanto, se desestructura al moverse desde esto, y este movimiento espera implícitamente el futuro. Este se siente como el menos obvio.

El paralelo a la función es llamada es el comportamiento de los cierres. Preferiría esta solución como la adición más limpia al corpus del lenguaje (ya que en la sintaxis se asemeja mejor a las posibilidades del tipo) pero hay algunos puntos positivos para la llamada al método y .await nunca es más largo que los demás.

@HeroicKatora Podríamos .await en libsyntax para permitir foo.await(42) pero esto sería inconsistente / ad-hoc. Sin embargo, (foo.await)(42) parece útil ya que, si bien existen cierres de salida de futuros, probablemente no sean tan comunes. Por lo tanto, si optimizamos para el caso común, no tener que agregar () a .await probablemente salga ganando.

@Centril Estoy de acuerdo, la coherencia es importante. Cuando veo un par de comportamientos que son similares, me gustaría inferir otros mediante síntesis. Pero aquí .await parece incómodo, especialmente con el ejemplo anterior que muestra claramente que es paralelo a la desestructuración implícita (a menos que pueda encontrar una sintaxis diferente donde ocurren estos efectos). Cuando veo la desestructuración, instantáneamente me pregunto si puedo usar esto con let-bindings, etc. Esto, sin embargo, no sería posible porque desestructuraríamos el tipo original que no tiene tal miembro o especialmente cuando el tipo es solo impl Future<Output=F> (Nota al margen mayoritariamente irrelevante: hacer que esto funcione nos devolvería a un prefijo alternativo await _ = en lugar de let _ = , curiosamente¹)

Eso no nos prohíbe usar la sintaxis en sí, creo que podría lidiar con aprenderla y si resulta ser la definitiva la usaré con vigor, pero parece una clara debilidad.


¹ Esto podría ser consistente y permitir ? al permitir ? detrás de los nombres en un patrón

  • await value? = failing_future();

para que coincida con la parte Ok de Result . Esto también parece interesante de explorar en otros contextos, pero más bien fuera de tema. También conduciría a una sintaxis de prefijo y sufijo coincidente para await al mismo tiempo.

Eso no nos prohíbe usar la sintaxis en sí, creo que podría lidiar con aprenderla y si resulta ser la definitiva la usaré con vigor, pero parece una clara debilidad.

Creo que cada solución tendrá algún inconveniente en alguna dimensión o caso. consistencia, ergonomía, encadenabilidad, legibilidad, ... Esto hace que sea una cuestión de grado, importancia de los casos, adecuación al código típico de Rust y APIs, etc.

En el caso de que un usuario escriba foo.await(42) ...

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

    foo.closure(42);
}

... ya proporcionamos buenos diagnósticos:

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

Ajustar esto para que se ajuste a foo.await(42) parece bastante alcanzable. De hecho, por lo que puedo ver, sabemos que el usuario tiene la intención de (foo.await)(42) cuando se escribe foo.await(42) por lo que puede ser cargo fix ed en un MachineApplicable conducta. De hecho, si estabilizamos foo.await pero no permitimos foo.await(42) , creo que incluso podemos cambiar la precedencia más adelante si es necesario, ya que foo.await(42) no será legal al principio.

Funcionarían más anidamientos (por ejemplo, futuro del resultado del cierre, aunque esto no será común)
`` óxido
estructura HasClosure fn _foo () -> Resultado <(), ()> {
let foo: HasClosure <_ i = "27"> = HasClosure {cierre: Ok (| x | {})};

foo.closure?(42);

Ok(())

}

por ejemplo, futuro del resultado del cierre - no es que esto sea común

El sufijo adicional ? operador hace que esto sea inequívoco sin modificaciones a la sintaxis, en cualquiera de los ejemplos posteriores a la corrección. No es necesario realizar ajustes . Los problemas son solo que .member explícitamente un campo y la necesidad de aplicar primero la desestructuración por movimiento. Y realmente no quiero decir que esto sea difícil de escribir. Principalmente quiero decir que esto parece inconsistente con otros usos de .member que, por ejemplo, pueden transformarse en coincidencias. La publicación original estaba sopesando aspectos positivos y negativos en ese sentido.

Editar: Ajustar para ajustar future.await(42) tiene el riesgo adicional, probablemente no intencionado, de hacer que esto a) sea inconsistente con cierres donde este no es el caso debido a métodos con el mismo nombre que el miembro permitido; b) inhibir desarrollos futuros donde nos gustaría dar argumentos a await . Pero, como mencionaste anteriormente, ajustar por Future devolver un cierre no debería ser el problema más urgente.

@novacrazy ¿

No estoy seguro de cuánta experiencia tiene con Futures 0.3, pero la expectativa general es que los combinadores no se usarán mucho y el uso primario / idiomático será async / await.

Async / await tiene varias ventajas sobre los combinadores, por ejemplo, admite préstamos en todos los puntos de rendimiento.

Los combinadores existían mucho antes de async / await, pero async / await se inventó de todos modos, ¡y por una buena razón!

Async / await llegó para quedarse, lo que significa que debe ser ergonómico (incluso con cadenas de métodos).

Por supuesto, las personas son libres de usar los combinadores si lo desean, pero no deberían ser necesarios para obtener una buena ergonomía.

Como dijo @cramertj , intentemos mantener la discusión enfocada en async / await, no en alternativas a async / await.

De hecho, algunas expresiones no se traducen bien en cadenas de espera si desea tener comportamientos de respaldo en caso de falla.

Su ejemplo se puede simplificar significativamente:

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

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

Esto deja en claro qué partes están manejando el error y qué partes están en el camino normal feliz.

Esta es una de las mejores cosas de async / await: funciona bien con otras partes del lenguaje, incluidos bucles, ramas, match , ? , try , etc.

De hecho, aparte de await , este es el mismo código que escribiría si no estuviera usando Futures.

Otra forma de escribirlo, si prefiere usar el combinador or_else :

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

value.unwrap()

Y lo mejor de todo es mover el código normal a una función separada, haciendo que el código de manejo de errores sea aún más claro:

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

value.unwrap()

(Esta es una respuesta al comentario de @joshtriplett ).

Para ser claros, no tienes que poner entre paréntesis, lo mencioné porque algunas personas dijeron que es muy difícil de leer sin los paréntesis. Entonces, los paréntesis son una opción estilística

Todas las sintaxis se benefician de los paréntesis en algunas situaciones, ninguna de las sintaxis es perfecta, es una cuestión de qué situaciones queremos optimizar.

Además, después de volver a leer su comentario, ¿tal vez pensó que estaba abogando por el prefijo await ? No lo estaba, mi ejemplo estaba usando postfix await . En general, me gusta el sufijo await , aunque también me gustan algunas de las otras sintaxis.

Estoy empezando a sentirme cómodo con fut.await , creo que la reacción inicial de la gente será "Espera, ¿así es como esperas? Extraño". pero luego les encantaría por la conveniencia. Por supuesto, lo mismo ocurre con @await , que se destaca mucho más que .await .

Con esa sintaxis, podemos omitir algunos de los permitidos en el ejemplo:

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

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

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

Esto también aclara lo que se desenvuelve con ? , por await some_op()? , no es obvio si some_op() se desenvuelve o el resultado esperado.

@Pauan

No estoy tratando de desviar el enfoque del tema aquí, estoy tratando de señalar que no existe en una burbuja. Tenemos que considerar cómo funcionan las cosas juntas .

Incluso si se eligiera la sintaxis ideal, todavía querría usar futuros personalizados y combinadores en algunas situaciones. La idea de que esos podrían ser obsoletos me hace cuestionar toda la dirección de Rust.

Los ejemplos que da todavía se ven terribles en comparación con los combinadores, y con la sobrecarga del generador probablemente serán un poco más lentos y producirán más código de máquina.

En lo que respecta a await, todo este prefijo / postfix sigil / palabra clave bikeshedding es maravilloso, pero tal vez deberíamos ser pragmáticos e ir con la opción más simple que sea más familiar para los usuarios que vienen a Rust. Es decir: palabra clave de prefijo

Este año va a pasar más rápido de lo que pensamos. Incluso enero está casi terminado. Si resulta que los usuarios no están contentos con una palabra clave de prefijo, se puede cambiar en una edición 2019/2020. Incluso podemos hacer una broma de "retrospectiva es 2020".

@novacrazy

El consenso general que he visto es que la _primera_ que queremos una tercera edición es 2022. Definitivamente no queremos planear otra edición; la edición de 2018 fue excelente, pero no exenta de costos. (Y uno de los puntos de la edición 2018 es hacer posible async / await, imagínese retirar eso y decir "no, ¡necesita actualizar a la edición 2020 ahora!")

En cualquier caso, no creo que una palabra clave de prefijo -> transición de palabra clave de postfijo sea posible en una edición, incluso si fuera deseable. La regla en torno a las ediciones es que debe haber una forma de escribir código idiomático en la edición X de manera que se compile sin advertencias y funcione igual en la edición X + 1.

Es la misma razón por la que preferimos no estabilizarnos con una macro de palabras clave si podemos generar consenso sobre una solución diferente; estabilizar deliberadamente una solución que sabemos que es indeseable es en sí mismo problemático.

Creo que hemos demostrado que una solución postfix es más óptima, incluso para expresiones con un solo punto de espera. Pero dudo que cualquiera de las soluciones postfijas propuestas sea obviamente mejor que todas las demás.

Solo mis dos centavos (no soy nadie, pero sigo la discusión durante bastante tiempo). Mi solución favorita sería la versión @await postfix. ¿Quizás podría considerar un sufijo !await , como una nueva sintaxis de macro de sufijo?

Ejemplo:

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

Después de algunas iteraciones de lenguaje, sería increíble poder implementar nuestras propias macros postfix.

... todas las nuevas propuestas de sygils

Rust ya tiene mucha sintaxis / sygil, tenemos await palabra clave reservada, stuff@await (o cualquier otro sygil) se ve raro / feo (subjetivo, lo sé), y es solo una sintaxis ad-hoc que no se integra con nada más en el idioma, lo cual es una gran bandera roja.

He echado un vistazo a cómo implementa Go
... <- ... propuesta

@ I60R : Go tiene una sintaxis terrible llena de soluciones ad-hoc, y es totalmente imperativo, muy diferente a Rust. Esta propuesta es nuevamente sygil / syntax y totalmente ad-hoc solo para esta característica en particular.

@ I60R : Go tiene una sintaxis terrible

Por favor, abstengámonos de atacar otros idiomas aquí. "X tiene una sintaxis terrible" no conduce a la iluminación ni al consenso.

Como usuario de Python / JavaScript / Rust y estudiante de informática, personalmente prefiero que el prefijo await + f.await() esté en ambos idiomas.

  1. Tanto Python como JavaScript tienen el prefijo await . Esperaría ver aparecer await al principio. Si tengo que leer profundamente para darme cuenta de que se trata de un código asincrónico, me siento muy incómodo. Con la capacidad WASM de Rust, podría atraer a muchos desarrolladores de JS. Creo que la familiaridad y la comodidad son realmente importantes, considerando que Rust ya tiene muchos otros conceptos nuevos.

  2. Postfix await parece conveniente en la configuración de encadenamiento. Sin embargo, no me gustan las soluciones como .await , @await , f await porque parecen una solución ad-hoc para la sintaxis await mientras que tiene sentido pensar en .await() como llamar a un método en el future .

Rust ya es una desviación de javascript y await no se parece en nada a la función de llamada (es decir, la funcionalidad no se puede emular a través de una función) el uso de funciones para indicar await hace que sea confuso para los principiantes que se introduzcan en async-await. Por lo tanto, creo que la sintaxis debería ser diferente.

Me he convencido de que .await() es probablemente significativamente más deseable que .await , aunque el resto de esta publicación cubre un poco esa posición.

La razón de esto es que await!(fut) tiene que _consumir fut por valor_. Hacer que parezca un acceso de campo es _bad_, porque eso no tiene la connotación de moverse de la forma en que lo hace una palabra clave de prefijo, o el potencial de moverse como lo hace una macro o una llamada a un método.

Curiosamente, la sintaxis del método de palabras clave hace que parezca casi un diseño en espera implícito . Desafortunadamente, "Explicit Async, Implicit Await" no es posible para Rust (por lo que _por favor_ no vuelva a litigar en este hilo) ya que queremos que async fn() -> T se use de manera idéntica a fn() -> Future<T> , en lugar de activar un comportamiento de "espera implícita".

El hecho de que una sintaxis .await() _ se vea_ como un sistema de espera implícito (como el que usa Kotlin) podría ser un detractor, y casi daría la sensación de un "Asíncrono implícito, Espera implícita" debido a la magia que rodea a la no- realmente-un-método-llamada .await() sintaxis. ¿Podrías usar eso como await(fut) con UFCS? ¿Sería Future::await(fut) para UFCS? Cualquier sintaxis que parezca otra dimensión del lenguaje plantea problemas a menos que se pueda unificar de alguna manera con él, al menos sintácticamente, aunque no funcionalmente.

Sigo siendo escéptico si los beneficios de cualquier solución de sufijo _individual_ superan los inconvenientes de esa misma solución, aunque el concepto de una solución de sufijo es más deseable que un prefijo en general.

Estoy un poco sorprendido de que este hilo esté lleno de sugerencias que parecen hechas porque son posibles, y no porque parezcan producir beneficios significativos sobre las sugerencias iniciales.
¿Podemos dejar de hablar de $ , # , @ , ! , ~ etc, sin plantear un argumento significativo sobre lo que está mal? con await , que se comprende bien y se ha probado en varios otros lenguajes de programación?

Creo que la publicación de https://github.com/rust-lang/rust/issues/57640#issuecomment -455361619 ya enumeró todas las buenas opciones.

De aquellos:

  • Los delimitadores obligatorios parecen estar bien, al menos es obvio cuál es la precedencia y podemos leer otros códigos sin más claridad. Y escribir dos paréntesis no es tan malo. Quizás el único inconveniente es que parece una llamada de función, aunque es una operación de flujo de control diferente.
  • La precedencia útil podría ser la opción preferible. Ese parece ser el camino que tomaron la mayoría de los otros idiomas, por lo que es familiar y está probado.
  • Personalmente, creo que la palabra clave postfix con espacios en blanco se ve extraña con múltiples esperas en una sola declaración:
    client.get("url").send() await?.json()? . Ese espacio en blanco en el medio parece fuera de lugar. Entre paréntesis tendría un poco más de sentido para mí: (client.get("url").send() await)?.json()?
    Pero encuentro que el flujo de control es aún más difícil de seguir que con la variante de prefijo.
  • No me gusta el campo postfix. await realiza una operación muy compleja: Rust no tiene propiedades computables y el acceso al campo es, por lo demás, una operación muy simple. Por tanto, parece dar una impresión errónea sobre la complejidad de esta operación.
  • El método Postfix podría estar bien. Podría alentar a algunas personas a escribir declaraciones muy largas con múltiples esperas en ellas, lo que podría ocultar más los puntos de rendimiento. También vuelve a hacer que las cosas parezcan una llamada a un método, aunque sea algo diferente.

Por estos motivos, prefiero la "precedencia útil" seguida de "delimitadores obligatorios".

Go tiene una sintaxis terrible llena de soluciones ad-hoc, y es totalmente imperativo, muy diferente a Rust. Esta propuesta es nuevamente sygil / syntax y totalmente ad-hoc solo para esta característica en particular.

@dpc , si lees la propuesta <-- completo, verás que esta sintaxis solo está inspirada en Go, sin embargo, es bastante diferente y utilizable tanto en imperativo como en contexto de encadenamiento de funciones. Tampoco veo cómo la sintaxis await no es una solución ad-hoc, para mí es mucho más específica y torpe que <-- . Es similar a tener deref reference / reference.deref / etc en lugar de *reference , o tener try result / result.try / etc en lugar de result? . Tampoco veo ninguna ventaja en el uso de la palabra clave await no sea la familiaridad con JS / Python / etc., que de todos modos debería ser menos significativa que tener una sintaxis coherente y componible. Y no veo ninguna desventaja de tener <-- sigil aparte de que no es un await que de todos modos no es tan simple como el inglés simple y los usuarios deben entender lo que hace primero.

Editar: esta también podría ser una buena respuesta a la publicación de @ Matthias247 , ya que proporciona algunos argumentos en contra de await y propone una posible alternativa no afectada por los mismos problemas


Es realmente interesante para mí leer críticas en contra de la sintaxis <-- , libre de argumentos que apelen a razones históricas y prejuiciosas.

Mencionemos detalles reales sobre la precedencia:

La tabla de precedencia tal como está hoy :

Operador / Expresión | Asociatividad
- | -
Caminos |
Llamadas a métodos |
Expresiones de campo | de izquierda a derecha
Llamadas a funciones, indexación de matrices |
? |
Unario - * ! & &mut |
as | de izquierda a derecha
* / % | de izquierda a derecha
+ - | de izquierda a derecha
<< >> | de izquierda a derecha
& | de izquierda a derecha
^ | de izquierda a derecha
\| | de izquierda a derecha
== != < > <= >= | Requerir paréntesis
&& | de izquierda a derecha
\|\| | de izquierda a derecha
.. ..= | Requerir paréntesis
= += -= *= /= %= &= \|= ^= <<= >>= | De derecha a izquierda
return break cierres |

La precedencia útil pone await antes de ? para que se una más estrechamente que ? . Por tanto, un ? en la cadena une un `await a todo lo anterior.

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

Sí, con precedencia útil, eso "simplemente funciona". ¿Sabes lo que hace eso de un vistazo? ¿Es ese mal estilo (probablemente)? Si es así, ¿puede rustfmt arreglarlo automáticamente?

La precedencia obvia pone await _somewhere_ debajo de ? . No estoy seguro de dónde exactamente, aunque esos detalles probablemente no importan demasiado.

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

¿Sabes lo que hace eso de un vistazo? ¿Puede rustfmt ponerlo en un estilo útil que no desperdicie demasiado espacio vertical y horizontal automáticamente?


¿Dónde caerían las palabras clave postfix en esto? Probablemente Método de palabra clave con llamadas de método y Campo de palabra clave con expresiones de campo, pero no estoy seguro de cómo deberían enlazarse los demás. ¿Qué opciones conducen a la menor cantidad de configuraciones posibles donde el await recibe un "argumento" sorprendente?

Para esta comparación, sospecho que los "delimitadores obligatorios" (lo que llamé Función de palabra clave en el resumen ) ganan fácilmente, ya que sería equivalente a una llamada de función normal.

@ CAD97 Para ser claros, recuerde que .json() también es un futuro (al menos en reqwests ).

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

Cuanto más juego con la conversión de expresiones de óxido complejas (incluso las que solo necesitan 1 espera, sin embargo, tenga en cuenta que en mi base de código futuro de más de 20,000 casi todas las expresiones asíncronas son una espera, seguida directamente de otra espera), más me disgusta prefijo para Rust.

Esto es _todo_ debido al operador ? . Ningún otro lenguaje tiene un operador de flujo de control de sufijo _y_ await que esencialmente siempre están emparejados en código real.


Mi preferencia sigue siendo el campo de posfijo . Como operador de control de postfix, creo que necesita la agrupación visual ajustada que future.await proporciona sobre future await . Y en comparación con .await()? , prefiero cómo se ve .await? _extraño_ por lo que será _advertido_ y los usuarios no asumirán que es una función simple (y por lo tanto no preguntarán por qué UFCS no funciona) .


Como un dato más a favor del postfijo, cuando este se estabilice, un rustfix para pasar de await!(...) a lo que decidamos sería muy apreciado. No veo cómo otra cosa que no sea la sintaxis del sufijo podría traducirse sin ambigüedades sin envolver cosas en ( ... ) innecesariamente.

Creo que primero deberíamos responder a la pregunta "¿queremos fomentar el uso de await en contextos de encadenamiento?". Creo que la respuesta predominante es "sí", por lo que se convierte en un fuerte argumento para las variantes de sufijo. Si bien await!(..) será el más fácil de agregar, creo que no deberíamos repetir la historia de try!(..) . Además, personalmente no estoy de acuerdo con el argumento de que "el encadenamiento oculta una operación potencialmente costosa", ya tenemos muchos métodos de encadenamiento que pueden ser _muy_ pesados, por lo que el encadenamiento no implica pereza.

Si bien la palabra clave del prefijo await será la más familiar para los usuarios que vienen de otros idiomas, no creo que debamos tomar nuestra decisión basándonos en ella, sino que debemos concentrarnos en un plazo más largo, es decir, usabilidad, conveniencia y legibilidad . @withoutboats habló sobre el "presupuesto de familiaridad", pero creo firmemente que no deberíamos introducir soluciones subóptimas solo por la familiaridad.

Ahora probablemente no queremos dos formas de hacer lo mismo, por lo que no deberíamos introducir variantes de prefijo y de sufijo. Entonces, digamos que hemos reducido nuestras opciones a variantes de postfijo.

Primero comencemos con fut await , esta variante no me gusta mucho, porque afectará seriamente la forma en que los humanos analizan el código y será una fuente constante de confusión al leer el código. (No olvide que el código es principalmente para leer)

Siguiente fut.await , fut.await() y fut.await!() . Creo que la variante más consistente y menos confusa será la macro de postfix. No creo que valga la pena introducir una nueva entidad de "función de palabra clave" o "método de palabra clave" solo para guardar un par de caracteres.

Por último, variantes basadas en sigilo: fut@await y fut@ . No me gusta la variante fut@await , si presentamos el sigilo, ¿por qué molestarse con la parte await ? ¿Tenemos planes para futuras extensiones fut@something ? Si no, simplemente se siente redundante. Así que me gusta la variante fut@ , resuelve los problemas de precedencia, el código se vuelve fácil de entender, escribir y leer. Los problemas de visibilidad se pueden resolver resaltando el código. No será difícil explicar esta función como "@ para esperar". Por supuesto, el mayor inconveniente es que pagaremos por esta función con el muy limitado "presupuesto sigil", pero considerando la importancia de la función y la frecuencia con la que se usará en bases de código asíncronas, creo que valdrá la pena a largo plazo. . Y, por supuesto, podemos establecer ciertos paralelismos con ? . (Aunque tendremos que estar preparados para las bromas de Perl de los críticos de Rust)

En conclusión: en mi opinión, si estamos dispuestos a sobrecargar el "presupuesto sigil" deberíamos ir con fut@ , y si no con fut.await!() .

Cuando se habla de familiaridad, no creo que debamos preocuparnos demasiado por la familiaridad con JS / Python / C #, ya que Rust está en un nicho diferente y ya se ve diferente en muchas cosas. Proporcionar una sintaxis similar a estos lenguajes es un objetivo a corto plazo y de baja recompensa. Nadie seleccionará Rust solo por usar una palabra clave familiar cuando bajo el capó funciona completamente diferente.

Pero la familiaridad con Go importa, ya que Rust se encuentra en un nicho similar e incluso por filosofía está más cerca de Go que de otros idiomas. Y a pesar de todo el odio prejuicioso, uno de los puntos más fuertes de ambos es que no copian a ciegas características, sino que implementan soluciones que realmente tienen razón.

En mi opinión, en este sentido la sintaxis <-- es más fuerte aquí

Con todo eso, recordemos que las expresiones de Rust pueden resultar en varios métodos encadenados. La mayoría de los idiomas tienden a no hacer eso.

Me gustaría recordar la experiencia del equipo de desarrollo de C #:

La principal consideración en contra de la sintaxis de C # es la precedencia del operador await foo?

Esto es algo sobre lo 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 que la gente realmente quisiera "encadenar" algo más allá de su llamada asíncrona.

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

y

Esta es también la razón por la que no usamos 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 funciones 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

Es un buen punto contra el sigilo en lugar de la palabra dedicada (clave).

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

Realmente deberías escuchar a los chicos con millones de usuarios.

Así que no quieres encadenar nada, solo quieres tener varios await , y mi experiencia es la misma. Escribiendo código async/await durante más de 6 años, y nunca quise tal característica. La sintaxis de postfix parece realmente extraña y se considera que resuelve una situación que probablemente nunca suceda. Async call es realmente algo audaz, por lo que varias esperas en una sola línea son demasiado pesadas.

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

Eso parece un análisis a posteriori. Quizás una de las razones por las que no continúan es porque es extremadamente incómodo hacerlo en la sintaxis de prefijo (comparable a no querer try! varias veces en una declaración porque sigue siendo legible a través del operador ? ). Lo anterior considera principalmente (por lo que puedo decir) la precedencia, no la posición. Y me gustaría recordarles que C # no es Rust, y los miembros del rasgo pueden cambiar bastante el deseo de llamar a métodos en los resultados.

@ I60R ,

  1. Creo que la familiaridad es importante. Rust es un idioma relativamente nuevo y la gente migrará de otros idiomas y si Rust les resulta familiar, será más fácil para ellos tomar la decisión de elegir Rust.
  2. No soy muy divertido con los métodos de encadenamiento: es mucho más difícil depurar cadenas largas y creo que el encadenamiento solo complica la legibilidad del código y se puede permitir solo como opción adicional (como macros .await!() ). La forma de prefijo obligará a los desarrolladores a extraer código en métodos en lugar de encadenar, como:
let resp = await client.get("http://api")?;
let body: MyResponse = await resp.into_json()?;

en algo como esto:

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

Eso parece un análisis a posteriori. Quizás una de las razones por las que no continúan es porque es extremadamente incómodo hacerlo en sintaxis de prefijo. Lo anterior solo considera la precedencia, no la posición. Y me gustaría recordarles que C # no es Rust, y los miembros del rasgo pueden cambiar bastante el deseo de llamar a métodos en los resultados.

No, se trata de experimentos internos del equipo de C # cuando tenía tanto prefijo / sufijo / formas implícitas. Y estoy hablando de mi experiencia, que no es solo un hábito en el que no puedo ver profesionales de formularios postfix.

@mehcode Tu ejemplo no me motiva. reqwest decide conscientemente hacer que el ciclo inicial de solicitud / respuesta y el manejo posterior de la respuesta (flujo del cuerpo) se separen de los procesos concurrentes, por lo que deben esperarse dos veces, como muestra

reqwest podría exponer totalmente las siguientes API:

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

o

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

(El último es un azúcar simple de más de and_then ).

Me parece preocupante que muchos de los ejemplos de postfix aquí usen esta cadena como ejemplo, ya que la mejor decisión de la API de reqwest sy hyper s es mantener estas cosas separadas.

Sinceramente, creo que la mayoría de los ejemplos de espera de postfix aquí deberían reescribirse usando combinadores (y azúcar si es necesario) o mantenerse en operaciones similares y esperar varias veces.

@andreytkachenko

La forma de prefijo obligará a los desarrolladores a extraer código en métodos en lugar de encadenar, como:

¿Eso es bueno o malo? Para los casos en los que hay N métodos, cada uno de los cuales puede dar lugar a M seguimientos, ¿se supone que el desarrollador debe proporcionar N * M métodos? Personalmente, me gustan las soluciones componibles, aunque sean un poco más largas.

en algo como esto:

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

@Pzixel

No creo que en C # obtengas tanto código encadenado / funcional como en Rust. ¿O me equivoco? Eso hace que las experiencias de los desarrolladores / usuarios de C # sean interesantes, pero no necesariamente aplicables a Rust. Ojalá pudiéramos contrastar con Ocaml o Haskell.

Creo que esa es la causa principal del desacuerdo es que algunos de nosotros disfrutamos del estilo imperativo y otros del funcional. Y eso es. Rust es compatible con ambos, y ambos lados del debate quieren que la asincrónica se ajuste bien a la forma en que normalmente escriben código.

@dpc

No creo que en C # obtengas tanto código encadenado / funcional como en Rust. ¿O me equivoco? Eso hace que las experiencias de los desarrolladores / usuarios de C # sean interesantes, pero no necesariamente aplicables a Rust. Ojalá pudiéramos contrastar con Ocaml o Haskell.

LINQ y el estilo funcional son bastante populares en C #.

@dpc ,
si se trata de legibilidad, creo que tanto código explícito como mejor, y mi experiencia dice que si los nombres de los métodos / funciones se autodescriben bien, no es necesario hacer los seguimientos.

si se trata de gastos generales, el compilador Rust es bastante inteligente para insertarlos (de todos modos siempre tenemos #[inline] ).

@dpc

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

Mi sensación es que esto básicamente repite el problema de la API de futuros: facilita el encadenamiento, pero el tipo de esa cadena se vuelve mucho más difícil de penetrar y depurar.

¿No se aplica ese mismo argumento a la? operador aunque? Permite más encadenamiento y, por lo tanto, dificulta la depuración. Pero, ¿por qué elegimos? probar más! ¿luego? ¿Y por qué Rust prefiere las API con patrón de construcción? Así que Rust ya tomó varias decisiones a favor del encadenamiento en las API. Y sí, eso puede dificultar la depuración, pero ¿no debería suceder eso a nivel de pelusa, tal vez una nueva pelusa para clippy para cadenas que son demasiado grandes? Todavía tengo que ver suficiente motivación en cuanto a qué tan diferente es la espera aquí.

¿Eso es bueno o malo? Para los casos en los que hay N métodos, cada uno de los cuales puede dar lugar a M seguimientos, ¿se supone que el desarrollador debe proporcionar N * M métodos? Personalmente, me gustan las soluciones componibles, aunque sean un poco más largas.

N y M no son necesariamente grandes, y tampoco todos pueden ser de interés / útiles de extraer.

@andreytkachenko ,

  1. No estoy de acuerdo, la familiaridad está sobrevalorada aquí. Al migrar de otro idioma, las personas buscarían primero la capacidad de hacer programación al estilo async-await pero no exactamente con la misma sintaxis. Si se implementara de manera diferente pero eso proporcionaría una mejor experiencia de programación, entonces se convertirá en otra ventaja más.

  2. La imposibilidad de depurar cadenas largas es una limitación de los depuradores y no del estilo del código. Acerca de la legibilidad, creo que depende, y hacer cumplir el estilo imperativo es innecesariamente restrictivo aquí. Si las llamadas a funciones async siguen siendo llamadas a funciones, entonces es sorprendente no admitir el encadenamiento. Y de todos modos, <-- es un operador de prefijo con capacidad para usarlo en cadenas de funciones como una opción adicional, como dijiste

@skade De acuerdo.

Todos los ejemplos con largas cadenas de esperas son un olor a código. Podrían resolverse de manera mucho más elegante con combinadores o API actualizadas, en lugar de crear estas máquinas generadoras de estados espaguetis bajo el capó. La sintaxis taquigráfica extrema es una receta para una depuración aún más difícil que los futuros actuales.

Todavía soy un fan de ambos prefijo de la palabra clave await y macro await!(...) / .await!() , en combinación con async y #[async] funciones del generador , como se describe en mi comentario aquí .

Si todo sale correctamente, probablemente se podría crear una sola macro para que await! maneje tanto el prefijo como el sufijo, y la palabra clave await solo sería una palabra clave dentro de las funciones async .

@skade ,
Para mí, una gran desventaja de usar combinadores en future es que ocultarán las llamadas de función async y sería imposible distinguirlas de las funciones regulares. Quiero ver todos los puntos que se pueden suspender, ya que es muy probable que se usen dentro de cadenas de estilo constructor.

@ I60R depurar cadenas de llamadas largas no es en absoluto un problema de depuración, porque el problema de escribir estas cadenas es obtener una inferencia de tipo correcta. Dado que muchos de esos métodos toman parámetros genéricos y distribuyen parámetros genéricos, potencialmente vinculados a un cierre, es un problema grave.

No estoy seguro de si hacer visibles todos los puntos de suspensión sea un objetivo de la función. Esto lo deciden los implementadores. Y es totalmente posible con todas las versiones propuestas de la sintaxis await .

@novacrazy buen punto, ahora que lo mencionas, como ex javascripter, NUNCA uso cadenas de espera, siempre usé bloques de entonces

Supongo que se vería como

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

que ni siquiera sé si es posible, necesito buscar futuros en Rust

@richardanaya esto es totalmente posible (sintaxis diferente).

Como la caja implícita:

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

Podemos ingresar una nueva palabra clave await y el rasgo Await con esta implícita:

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

Y use await como método y también como palabra clave:

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

@richardanaya De acuerdo. Fui uno de los primeros desarrolladores en adoptar async / await para mis cosas de webdev hace muchos años, y el poder real vino de combinar async / await con Promises / Futures existentes. Además, incluso su ejemplo probablemente podría simplificarse como:

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

let value = result.unwrap();

Si ha anidado futuros o resultados de futuros o futuros de resultados, el combinador .flatten() puede simplificar aún más eso drásticamente. Sería de mala educación desenvolver y esperar a cada uno manualmente.

@XX Eso es redundante e inválido. await solo existe en las funciones async , y solo puede funcionar en tipos que implementan Future / IntoFuture todos modos, por lo que no hay necesidad de una nueva rasgo.

@novacrazy, @richardanaya

podrían resolverse de manera mucho más elegante con combinadores o API actualizadas, en lugar de crear estas máquinas generadoras de estados espaguetis bajo el capó. La sintaxis taquigráfica extrema es una receta para una depuración aún más difícil que los futuros actuales.

@novacrazy buen punto, ahora que lo mencionas, como ex javascripter, NUNCA uso cadenas de espera, siempre usé bloques de entonces

Los combinadores tienen propiedades y poderes totalmente diferentes en Rusts async / await, por ejemplo, con respecto a los préstamos entre puntos de rendimiento. No puede escribir de forma segura combinadores que tengan las mismas capacidades que los bloques asíncronos. Dejemos la discusión del combinador fuera de este hilo, ya que no es útil.

@ I60R

Para mí, una gran desventaja de usar combinadores en el futuro es que ocultarán las llamadas a funciones asíncronas y sería imposible distinguirlas de las funciones regulares. Quiero ver todos los puntos que se pueden suspender, ya que es muy probable que se usen dentro de cadenas de estilo constructor.

No puede ocultar los puntos de suspensión. Lo único que permiten los combinadores es crear otros futuros. En algún momento habrá que esperarlos.

Tenga en cuenta que C # no tiene el problema de que la mayoría de los códigos que usan un prefijo await lo combinarán con el operador postfix (ya existente) ? . En particular, las preguntas sobre la precedencia y la incomodidad general de tener dos "decoradores" similares aparecen en lados opuestos de una expresión.

@ Matthias247 Si necesita pedir prestados datos, no dude en utilizar varias declaraciones await . Sin embargo, muchas veces simplemente necesita mover datos, y los combinadores son perfectamente válidos para eso y tienen el potencial de compilar hasta un código más eficiente. A veces se optimiza completamente.

Una vez más, el verdadero poder está en combinar las cosas. No existe una forma correcta de hacer las cosas tan complicadas como esta. En parte, es por eso que considero que toda esta sintaxis de eliminación de bicicletas es exactamente eso si no va a ayudar a los nuevos usuarios que vienen de otros lenguajes y a crear un código fácil de mantener, eficaz y legible . El 80% de las veces dudo que siquiera toque async / await sin importar cuál sea la sintaxis, solo para proporcionar las API más estables y de rendimiento utilizando futuros simples.

En ese sentido, la palabra clave de prefijo await y / o la macro mixta await!(...) / .await!() son las opciones más legibles, familiares y fáciles de depurar.

@andreytkachenko

Vi tu publicación y estoy totalmente de acuerdo, de hecho, ahora que reflexiono sobre la "familiaridad", creo que hay dos tipos de familiaridad:

  1. Haciendo que la sintaxis de await parezca lenguajes similares que usan mucho await. Javascript es bastante grande aquí y muy relevante para nuestras capacidades WASM como comunidad.

  2. El segundo tipo de familiaridad es hacer que el código parezca familiar para los desarrolladores que solo trabajan con código síncrono. Creo que el mayor aspecto de await es hacer que el código asincrónico LOOK sea sincrónico. Esto es realmente una cosa que javascript realmente hace mal es que se normaliza mucho tiempo que las cadenas que parecen completamente ajenas a los desarrolladores principalmente sincrónicos.

Una cosa que ofrecería desde mis días en javascript async, lo que fue mucho más útil para mí como desarrollador en retrospectiva fue la capacidad de agrupar promesas / futuros juntos. Promise.all (p1 (), p2 ()) para poder paralizar fácilmente el trabajo. El encadenamiento de then () siempre fue solo un eco de la promesa pasada de Javascript, pero bastante arcaico e innecesario ahora que lo pienso.

Quizás ofrecería esta idea de esperar. "Intente que las diferencias entre el código asíncrono y el código de sincronización sean lo más mínimas posible"

@novacrazy La función asíncrona devuelve un tipo impl Future , ¿verdad? ¿Qué nos impide agregar el método de espera al rasgo Future ? Así :

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

@XX Según tengo entendido, las funciones async en Rust se transforman en máquinas de estado usando generadores. Este artículo es una buena explicación. Entonces await necesita una función async para trabajar, por lo que el compilador puede transformarlos correctamente. await no puede funcionar sin la parte async .

Future tiene un wait método, que es similar a lo que usted sugiere, pero bloquea el flujo actual.

@skade ,

Pero, ¿cómo podría afectar el estilo del código a la inferencia de tipos? No puedo ver la diferencia en el mismo código que está escrito en un estilo imperativo y encadenado. Los tipos deben ser exactamente iguales. Si el depurador no puede resolverlos, definitivamente es un problema en el depurador, no en el código.

@skade , @ Matthias247 , @XX

Con los combinadores tendrá exactamente un punto de suspensión marcado al comienzo de la cadena de funciones. Todo lo demás estaría implícito en el interior. Ese es exactamente el mismo problema que tomar implícitamente mut , que personalmente para mí fue uno de los mayores puntos de confusión en Rust. Algunas API devolverían futuros mientras que otras devolverían resultados de futuros; no quiero eso. Los puntos de suspensión deben ser explícitos si es posible y la sintaxis componible correctamente alentaría

@ I60R let enlaces son puntos de unión para escribir inferencia y ayudar en el informe de errores.

@novacrazy

Así que await necesita una función asincrónica para trabajar

¿Puede expresarse en tipo de devolución? Por ejemplo, que el tipo de retorno es impl Future + Async , en lugar de impl Future .

@skade Siempre he pensado que . en la sintaxis de llamada al método tiene exactamente el mismo propósito

@dpc

No creo que en C # obtengas tanto código encadenado / funcional como en Rust. ¿O me equivoco? Eso hace que las experiencias de los desarrolladores / usuarios de C # sean interesantes, pero no necesariamente aplicables a Rust. Ojalá pudiéramos contrastar con Ocaml o Haskell.

obtienes tanto como Rust. Mire cualquier código LINQ y lo verá.

El código asincrónico típico se ve así:

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

O, más general

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

No encadena llamadas, lo asigna a una variable y luego lo usa de alguna manera. Es especialmente cierto cuando se trata de un comprobador de préstamos.


Estoy de acuerdo en que puede encadenar futuros en una sola situación. Cuando desee manejar un error que podría producirse durante la llamada. por ejemplo, let a = await!(service_a)? . Esta es la única situación en la que la alternativa postfix es mejor. Podría ver que se beneficia aquí, pero no creo que supere todas las desventajas.

Otra razón: ¿qué pasa con impl Add for MyFuture { ... } y let a = await a + b; ?

Me gustaría recordar sobre RFC de asignación de tipo generalizado en el contexto de la palabra clave de espera de postfix. Permitiría el siguiente código:

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

Muy parecido a la palabra clave de espera de sufijo:

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

Con las mismas desventajas cuando está mal formateado:

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

Creo que si nos tragamos la píldora de RFC de asignación de tipo, deberíamos tragar fut await por coherencia.

Por cierto, ¿sabías que esta es una sintaxis válida?

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

Sin embargo, vi cero ocurrencias de esto en código real.

Por favor, permítame desviarme un poco. Creo que deberíamos darle a las macros postfix, como sugiere @BenoitZugmeyer , otro pensamiento. Esos podrían hacerse como expr!macro o como expr@macro , o tal vez incluso expr.macro!() . Creo que sería preferible la primera opción. No estoy seguro de si son una buena idea, pero si queremos extraer un concepto general y no una solución ad-hoc sin dejar de esperar un postfix , al menos deberíamos pensar en las macros postfix como una posible solución.

Tenga en cuenta que incluso si hiciéramos de await una macro postfix, aún sería mágica (como compile_error! ). Sin embargo, @jplatte y otros ya han establecido que eso no es un problema.

Cómo lo haría si tuviéramos que seguir esa ruta, primero establecería exactamente cómo funcionarían las macros postfix, luego solo permitiría await como una macro postfix mágica y luego permitiría macros postfix definidas propias.

Lexing

En cuanto a lexing / parsing, esto podría ser un problema. Si miramos expr!macro , el compilador podría pensar que hay una macro llamada expr! y luego hay algunas letras inválidas macro después de eso. Sin embargo, expr!macro debería ser posible lex por un lookahead de uno, y algo se convierte en una macro postfix cuando hay un expr seguido de un ! , seguido directamente de identifier . No soy un desarrollador de idiomas y no estoy seguro de si esto hace que el lexing sea demasiado complejo. Asumiré que las macros postfix pueden tomar la forma de expr!macro .

¿Serían útiles las macros postfix?

En la parte superior de mi cabeza, se me han ocurrido estos otros casos de uso para macros postfix. Sin embargo, no creo que todos puedan implementarse mediante macros personalizadas, así que no estoy seguro de si esta lista es muy útil.

  • stream!await_all : para esperar transmisiones
  • option!or_continue : cuando option es Ninguno, continúa el ciclo
  • monad!bind : por hacer =<< sin vincularlo a un nombre

stream!await_all

Esto nos permite no solo esperar futuros, sino también corrientes.

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

sería equivalente a (en un bloque async -stream-esque):

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

UpdateTickerAction::new(exchange_rate.value)

option!or_continue

Esto nos permite desenvolver una opción, y si es None , continuar el ciclo.

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

sería equivalente a:

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

monad!bind

Este nos permitiría obtener mónadas de una manera bastante rústica (es decir, centrada en la expresión). Rust todavía no tiene nada parecido a las mónadas y no estoy convencido de que se agreguen a Rust. No obstante, si se añadieran, esta sintaxis podría resultar útil.

Tomé lo siguiente de aquí .

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

sería equivalente a:

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

O, más en línea, con menos let s:

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

Evaluación

Estoy muy dividido en esto. La parte matemática de mi cerebro quiere encontrar una solución generalizada para await , y las macros postfix podrían ser una forma de lograrlas. La parte pragmática de mi cerebro piensa: para qué molestarse en hacer un lenguaje cuyo macro sistema ya nadie entiende aún más complicado.

Sin embargo, desde ? y await , tenemos dos ejemplos de operadores postfijos súper útiles. ¿Qué pasa si encontramos otros que queremos agregar en el futuro, tal vez similares a los que mencioné? Si los hemos generalizado, podemos agregarlos a Rust de forma natural. Si no lo hemos hecho, tendríamos que idear otra sintaxis cada vez, lo que probablemente inflaría el lenguaje más de lo que lo harían las macros postfix.

¿Qué piensan ustedes?

@OjoPython

Se me ocurrió una idea muy similar, y estoy seguro de que las macros de postfix en Rust es cuestión de tiempo.
Estoy pensando en eso cada vez que trato con el corte de ndarray :

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

pero mucho mejor seria

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

y muchos combinadores pueden desaparecer, como los que tienen _with o _else postfijos:

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

@EyeOfPython @andreytkachenko Las macros postfix no son actualmente una característica en Rust y en mi humilde opinión necesitaría una fase de implementación RFC + FCP + completa.

Esta no es una discusión de RFC, sino una discusión de una RFC aceptada que debe implementarse.

Por esa razón, no creo que sea práctico discutirlos aquí o proponerlos para la sintaxis asíncrona. Retrasaría aún más la función de forma masiva.

Para frenar esta discusión ya masiva, creo que _no_ es útil discutirlos aquí, pueden verse como fuera del tema discutido.

Solo un pensamiento: si bien este hilo es específicamente para await , creo que tendremos la misma discusión para las expresiones yield en el futuro, que también podrían encadenarse. En la medida en que prefiero ver una sintaxis que se pueda generalizar, como se vea.

Mis razones para no usar macros aquí:

  1. Las macros se proporcionan para cosas específicas del dominio y usarlas de cualquier manera que cambie el flujo de control de los programas o emule las características del lenguaje principal es una exageración
  2. Las macros de Postfix se abusarían de inmediato para implementar operadores personalizados u otras características de lenguaje esotérico que conducen a un código incorrecto
  3. Desalentarían el desarrollo de características lingüísticas adecuadas:

    • stream.await_all es un caso de uso perfecto para combinador

    • option.or_continue y reemplazar _else combinadores es un caso de uso perfecto para el operador coalescente nulo

    • monad.bind es un caso de uso perfecto para cadenas de if-let

    • ndarray slicing es un caso de uso perfecto para const genéricos

  4. Pondrían en duda el operador ? ya implementado

@collinanderson

que igualmente podría estar encadenado

¿Por qué demonios crees que podrían estar encadenados? cero ocurrencias en código real, como el ejemplo anterior de println .

@Pzixel Es probable que eventualmente Generator::resume tome un valor y, por lo tanto, yield expr tenga un tipo que no sea () .

@vlaff No veo claramente un argumento para que await tenga que ser coherente con la asignación de tipo. Son cosas muy distintas.

Además, Type Ascription es otra de esas características que se prueban repetidamente, no hay garantía de que _este_ lo haga. Si bien no quiero oponerme a esto, TA es un RFC futuro y esta es una discusión sobre una característica aceptada con una sintaxis ya propuesta.

Al leer los muchos comentarios, para agregar un resumen adicional sobre por qué esperar! (...) aparece un camino muy ideal:

  1. parece el más familiar para el código existente porque las macros son familiares
  2. ¡hay trabajo existente que usa espera! (...) https://github.com/alexcrichton/futures-await y podría ayudar a reducir la reescritura de código
  3. dado que las macros postfix no están sobre la mesa, es posible que nunca formen parte del lenguaje y "¡aguardan!" en un contexto no estándar, no parece que sea una posibilidad según las RFC
  4. le da a la comunidad más tiempo para considerar la dirección a largo plazo después de un uso estable y generalizado, al tiempo que proporciona algo que no está totalmente fuera de la norma (p.
  5. ¡podría usarse como un patrón similar para el rendimiento próximo! () hasta que descubramos una ruta oficial que podría satisfacer la espera y el rendimiento.
  6. el encadenamiento puede no ser tan valioso como se esperaba y la claridad probablemente se mejore al tener múltiples esperas en múltiples líneas
  7. no tendrían que ocurrir cambios para los resaltadores de sintaxis IDE
  8. las personas de otros idiomas probablemente no se desanimen por una macro espera! (...) después de ver otros usos de macro (menos sobrecarga cognitiva)
  9. es probablemente el camino de esfuerzo más mínimo de todos los caminos para avanzar hacia la estabilización

¡esperar! macro ya está aquí, la pregunta es sobre el encadenamiento.

Cuanto más lo pienso, más me parece bien el postfix. Por ejemplo:

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

Permite el anidamiento de cualquier nivel y es legible. Funciona a la perfección con ? y posibles operadores futuros. Parece un poco extraño para los recién llegados, pero el beneficio gramatical puede superarlo.

La razón principal es que await debería ser una palabra clave separada, no una llamada a función. Es demasiado importante, por lo que debería tener su propio resaltado y lugar en el texto.

@Pzixel , @HeroicKatora , @skade

Vea, por ejemplo, Python yield y yield from expresiones: allí, la función externa puede proporcionar un valor que se convertirá en el resultado de yield cuando se reanude el generador. Esto es lo que quiso decir Por lo tanto, yield también tendrá un tipo distinto de ! o () .

Desde el punto de la corrutina, tanto yield como await suspenden y pueden (eventualmente) devolver un valor. En la medida en que son simplemente 2 caras de la misma moneda.

Y para descartar otra posibilidad de sintaxis, _square-brackets-with-keyword_, en parte para resaltar esto (usando yield para el resaltado de sintaxis):

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

"palabra clave postfix" tiene más sentido para mí. sufijo con un carácter que no sea un espacio en blanco que separa la expresión-futuro y await también tiene sentido para mí pero no '.' ya que eso es para métodos y await no es un método. Sin embargo, si desea await como palabra clave de prefijo, me gusta la sugerencia de @XX de un rasgo o método que solo llama await self para aquellos que quieren encadenar un montón de cosas no podría nombrar el método await ; creo que solo wait serviría). Personalmente, probablemente terminaría haciendo una espera por línea y no encadenando tanto porque encuentro que las cadenas largas son menos legibles, por lo que prefijo o postfijo funcionaría para mí.

[editar] Olvidé que wait ya existe y bloquea el futuro, así que tacha ese pensamiento.

@roland Me refiero específicamente a esto, que analiza las adscripciones de tipo: https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@rolandsteiner para que escribas

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

Cuando lo escribiría como:

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

@skade Oh, quisiste decir un comentario diferente al que pensaba, lo siento. : Lengua_salida_atascada:

@Pzixel Es probable que eventualmente Generator::resume tome un valor y, por lo tanto, yield expr tenga un tipo que no sea () .

Por lo tanto, yield también tendrá un tipo distinto de ! o () .

@valff , @rolandsteiner Me parece poco probable que yield devuelva el valor de reanudación, que es difícil de encajar en un lenguaje escrito estáticamente sin hacer que la sintaxis del generador y / o el rasgo sean molestos para trabajar. El prototipo original con argumentos de reanudación usaba la palabra clave gen arg para referirse a este argumento, algo así es mucho más probable que funcione bien en mi opinión. Desde ese punto de vista, yield todavía devolverá () por lo que no debería afectar mucho la discusión de await .

Creo que await debería ser un operador de prefijo, porque async es (y espero que yield sea ​​un prefijo). O téngalo como un método regular, usado en posición de sufijo, entonces.

Sin embargo, no obtengo todo el derramamiento de bicicletas aquí: ¿por qué incluso considerar una palabra clave postfix donde ningún otro Rust la tiene? Haría que el lenguaje fuera tan extraño.

Además, encadenando futuros, entiendo por qué podría ser interesante. ¿Pero el encadenamiento aguarda? ¿Qué se supone que significa eso? ¿Un futuro que devuelve un nuevo futuro? ¿Es realmente un idioma tan común que queremos que Rust tenga ese idioma como ciudadano de primera clase? Si realmente nos importa eso, creo que deberíamos:

  1. Vaya por el método (es decir, foo.await() ). No introduce una palabra clave postfix extraña y todos sabemos lo que significa. También podemos encadenar con eso.
  2. Si realmente queremos una palabra clave / loloperator, podemos resolver esa cuestión más tarde.

Además, por el simple hecho de dar mi opinión personal, odio el await en la posición de la palabra clave postfix.

El prefijo await se usa en otros idiomas por una razón: coincide con el lenguaje natural. No dices "Esperaré tu llegada". Aunque haya escrito JavaScript, puedo estar un poco sesgado.

También diré que considero que el encadenamiento de await está sobrevalorado. El valor de async/await sobre futuros combinadores es que permite escribir código asincrónico secuencialmente, utilizando las construcciones del lenguaje estándar como if , match , etcétera. Vas a querer romper tus esperas de todos modos.

No inventemos demasiada sintaxis mágica por async/await , es solo una pequeña parte del lenguaje. La sintaxis del método propuesto (es decir, foo.await() ) es, en mi opinión, demasiado ortogonal a las llamadas a métodos normales y resulta demasiado mágica. La maquinaria proc-macro está en su lugar, ¿por qué no disfrazarla detrás de eso?

¿Qué tal usar la palabra clave await en lugar de async en la defensa de la función? Por ejemplo:

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future
}

Este await fn solo se puede llamar en el contexto async :

async {
    let n = foo(bar());
}

Y sin azúcar a let n = (await foo(bar())); .
Luego, además de la palabra clave await , el rasgo Future podría implementar el método await para usar la lógica await en la posición de sufijo, por ejemplo:

async {
    let n = bar().awaited();
}

Además, ¿alguien puede explicarme la relación con los generadores? Me sorprende que async / await se implementen antes de que los generadores estén (incluso estabilizados).

Los generadores async / await . No hay planes actuales para estabilizarlos y aún requieren un RFC no experimental antes de que puedan serlo. (Personalmente, me encantaría que se estabilizaran, pero parece probable que estén al menos a uno o dos años de distancia, probablemente no serán RFCed hasta después de que async / await esté estable y ha habido algo de experiencia con él).

@XX Creo que estás rompiendo la semántica de async si usas await en la definición de la función. Sigamos con los estándares, ¿no?

@phaazon ¿Puede especificar con más detalle cómo se rompe la semántica de async ?

La mayoría de los lenguajes usan async para introducir algo que será asincrónico y await espera. Entonces, tener await lugar de async es un poco extraño para mí.

@phaazon No, no en cambio, sino además. Ejemplo completo:

async fn bar() -> i32 {
    5 // will be "converted" to impl Future<Output = i32>
}

await fn foo(future: impl Future<Output = i32>) -> i32 {
    future // will be "converted" to i32 in async context
}

async {
    let a = await bar(); // correct, a == 5
    let b = foo(bar()); // correct, b == 5
}

let c = foo(bar()); // error, can't call desugaring statement `await foo(bar())`

si es válido, entonces será posible implementar el método await para usarlo en una cadena:

async {
    let n = first().awaited()?.second().awaited()?;
    // let n = (await (await first())?.second())?;
}

En mi opinión, no importa si postfix await "se siente" demasiado mágico y no familiar para el idioma. Si adoptamos la sintaxis del método .await() o .await!() entonces solo es cuestión de explicar que Future s tienen un método .await() o .await!() que también puedes usar para esperarlos en los casos que tengan sentido. No es tan difícil de entender si pasas 5 minutos pensando en ello, incluso si nunca lo has visto antes.

Además, si vamos con la palabra clave prefijo, ¿qué nos impide escribir una caja con lo siguiente (pseudocódigo incompleto porque no tengo la infraestructura para lidiar con los detalles en este momento):

trait AwaitChainable {
    fn await(self) -> impl Future;
}

impl<T: Future> AwaitChainable for T {
    fn await(self) -> impl Future {
        await self
    }
}

De esa manera, podemos obtener postfix await si queremos. Quiero decir, incluso si eso de alguna manera no es posible y tenemos que implementar postfix await mágicamente por el compilador, todavía podemos usar algo como el ejemplo anterior para explicar cómo funciona principalmente. No sería difícil de aprender.

@ivandardi Yo pensé lo mismo. Pero esta definicion

fn await(self) -> impl Future {
    await self
}

no dice que la función se pueda llamar solo en async -context.

@XX Sí, como dije, ignore el pseudocódigo roto: PI actualmente no tiene la infraestructura para buscar cómo se deben escribir allí la sintaxis y los rasgos correctos :( Solo imagina que escribí lo que solo lo hace invocable en async contextos.

@XX , @ivandardi Por definición, await solo funciona en un contexto async . Por tanto, esto es ilegal:

fn await(self) -> impl Future {
    await self
}

Debe ser

async fn await(self) -> impl Future {
    await self
}

Que solo se puede llamar en un contexto async como este:

await future.await()

Lo que frustra todo el propósito.

La única forma de hacer que esto funcione es cambiar async completo, lo cual está fuera de la mesa.

@ivandardi Esta evolución de mi versión:

fn await(self) -> T { // How to indicate using in async-context only?
    await self
}

`` óxido
await fn await (self) -> T {// Porque async ya se tomó
aguardarme
}

```rust
await fn await(self) -> T {
    self // remove excess await
}

@CryZe @XX ¿ me abucheas ? Estoy en lo cierto.

@XX Lo que está tratando de hacer es alterar el significado de async y esperar completamente (por ejemplo, crear un contexto await que sea de alguna manera diferente de un contexto async ). No creo que reciba mucho apoyo de los desarrolladores del lenguaje.

@EyeOfPython Esta definición no funciona como se esperaba:

async fn await(self) -> impl Future {
    await self
}

Más como:

#[call_only_in_async_context_with_derived_await_prefix]
fn await(self) -> impl Future {
    self
}

Estoy votando negativamente porque ignoraste que se trata de una pseudo sintaxis hipotética. Esencialmente, su punto es que Rust podría agregar un rasgo especial como Drop and Copy que el lenguaje comprende, lo que agrega la capacidad de llamar .await () en el tipo que luego interactúa con el tipo como lo haría una palabra clave await. Además, la votación se considera irrelevante para los RFC de todos modos, ya que el punto es encontrar una solución objetiva, no una basada en sentimientos subjetivos como los visualizados por votos a favor / en contra.

No entiendo qué debería hacer este código. Pero esto no tiene nada que ver con el funcionamiento actual de async await. Si le importa, manténgalo fuera de este hilo y escriba un RFC dedicado para él.

@CryZe Ese tipo está disponible. Se llama Future. Y se acordó hace mucho tiempo que async / await es un mecanismo que SÓLO está dirigido a transformar el código asincrónico. No pretende ser una notación de vinculación general.

@ivandari su postfix aguardar no espera pero crea un nuevo futuro. Es esencialmente una función de identidad. No puede esperar implícitamente, porque no es una función regular.

@ Matthias247 Lo que estaban tratando de sugerir era una forma de que postfix await tuviera la sintaxis de una llamada a un método. De esa forma, Rust no necesita introducir nuevos símbolos arbitrarios como #, @ o ... como hicimos con? operador, y todavía se ve bastante natural:

let result = some_operation().await()?.some_method().await()?;

Entonces, la idea sería de alguna manera esperar que sea un tipo especial de método en el rasgo Future que el compilador ve de la misma manera que una palabra clave de espera normal (que no es necesario tener en absoluto en este caso) y transformar el async código en un generador desde allí. Entonces tiene el flujo de control lógico adecuado de izquierda a derecha en lugar de izquierda derecha izquierda con await some_future()? y no necesita introducir símbolos nuevos extraños.

(Entonces tl; dr: parece una llamada a un método pero en realidad es solo un postfix en espera)

Algo que no he visto mencionado explícitamente en consideración de una sintaxis de sufijo es la prevalencia de los combinadores de error y opción. Este es el lugar donde rust es más diferente de todos los idiomas que nos esperan: en lugar de los rastreos automáticos, tenemos .map_err()? .

En particular cosas como:

let result = await reqwest::get(..).send();
let response = result.map_err(|e| add_context(e))?;
let parser = await response.json();
let parsed = parser.map_err(|e| add_context(e))?;

es menos legible que (no me importa qué sintaxis de postfijo se considere):

let parsed = reqwest::get(..)
    .send() await
    .map_err(add_context)?
    .json() await
    .map_err(add_context)?;

_aunque_ este formato predeterminado tiene más líneas que el enfoque de múltiples variables. Las expresiones que no tienen múltiples esperas parecen ocupar menos espacio vertical con postfix-await.

No escribo código asincrónico en este momento, por lo que hablo por ignorancia, lamento continuar; si esto no es un problema en la práctica, entonces eso es excelente. Simplemente no quiero que el manejo adecuado de errores sea menos ergonómico solo para ser más similar a otros lenguajes que manejan errores de manera diferente.

Sí, eso es lo que quiero decir. Sería lo mejor de ambos mundos, permitiendo a las personas elegir si usar prefijo o sufijo aguardar según la situación.

@XX Si eso no se puede expresar en el código de usuario, entonces tendremos que recurrir al compilador para implementar esa funcionalidad. Pero en términos de entender cómo funciona el sufijo, la explicación que publiqué anteriormente todavía funciona, aunque sea sólo una comprensión superficial.

@CryZe , @XX ,

A primera vista parece suave, pero no coincide en absoluto con la filosofía de Rust. Incluso el método sort en iteradores no devuelve self y rompe la cadena de llamadas del método para hacer la asignación explícita. Pero espera que algo no menos impactante se implemente de una manera completamente implícita indistinguible de las llamadas a funciones regulares. En mi opinión, no hay posibilidades.

Sí, eso es lo que quiero decir. Sería lo mejor de ambos mundos, permitiendo a las personas elegir si usar prefijo o sufijo aguardar según la situación.

¿Por qué debería ser ese un objetivo? Rust no admite esto para todos los demás flujos de control (por ejemplo, break , continue , if , etc. tampoco. No hay una razón en particular, aparte de "esto podría verse mejor en la opinión particular de uno.

En general, me gustaría recordarles a todos que await es bastante especial y no una invocación de método normal:

  • Los depuradores pueden comportarse de manera extraña al pasar por encima de los esperas, porque la pila podría desenrollarse y restablecerse. Por lo que recuerdo, tomó mucho tiempo hacer que la depuración asíncrona funcionara en C # y Javascript. ¡Y esos tienen equipos pagados que trabajan en depuradores!
  • Los objetos locales que no cruzan los puntos de espera se pueden almacenar en la pila del sistema operativo real. Los que no lo son deben trasladarse al futuro generado, lo que al final marcará la diferencia en la memoria del montón requerida (que es donde vivirá el futuro).
  • Los préstamos en los puntos de espera son la razón por la que los futuros generados deben ser !Unpin , y causan muchos inconvenientes con algunos de los combinadores y mecanismos existentes. Puede ser que sea potencialmente posible generar Unpin de futuros de async métodos que no toman prestado a través de espera en el futuro. Sin embargo, si await s son invisibles, eso nunca sucederá.
  • Puede haber otros problemas inesperados del comprobador de préstamos, que no están cubiertos por el estado actual de async / await.

@Pzixel @lnicola

Los ejemplos que da para C # son totalmente imperativos, y nada como las cadenas largas de estilo funcional que vemos a menudo en Rust. Esa sintaxis LINQ es solo un DSL, y no se parece en nada a foo().bar().x().wih_boo(x).camboom().space_flight(); . He buscado en Google un poco, y los ejemplos de código C # parecen 100% imperativos, al igual que la mayoría de los lenguajes de programación populares. Es por eso que no podemos simplemente tomar lo que hizo langue X, porque simplemente no es lo mismo.

La notación de prefijo IMO encaja perfectamente en el estilo imperativo de codificación. Pero Rust admite ambos estilos.

@skade

No estoy de acuerdo con algunos de tus comentarios (y otros) sobre problemas con el estilo funcional. Para ser breve, simplemente acuerdemos que hay personas con una fuerte preferencia por uno u otro.

@ Matthias247 No, hay una razón en particular además de lo bonito que se ve. Es porque Rust fomenta el encadenamiento. Y podría decir "oh, otros mecanismos de flujo de control no tienen una sintaxis de sufijo, así que ¿por qué esperar ser especial?". Bueno, esa es una evaluación incorrecta. Tenemos un flujo de control postfix. Son simplemente internos a los métodos que llamamos. Option::unwrap_or es como hacer una declaración de coincidencia de postfijo. Iterator::filter es como hacer una sentencia if de sufijo. El hecho de que el flujo de control no sea parte de la sintaxis de encadenamiento en sí, no significa que no tengamos ya un flujo de control postfix. En ese punto de vista, agregar un sufijo await en realidad sería consistente con lo que tenemos. En este caso, incluso podríamos tener algo más similar a cómo funciona Iterator y en lugar de tener un sufijo sin procesar en espera, podríamos tener algunos combinadores en espera, como Future::await_or . De cualquier manera, tener postfix esperando no es solo una cuestión de apariencia, es una cuestión de funcionalidad y calidad de vida. De lo contrario, todavía estaríamos usando la macro try!() , ¿verdad?

@dpc

Los ejemplos que da para C # son totalmente imperativos, y nada como las cadenas largas de estilo funcional que vemos a menudo en Rust. Esa sintaxis de LINQ es solo un DSL, y no se parece en nada a foo (). Bar (). X (). Wih_boo (x) .camboom (). Space_flight (); He buscado en Google un poco, y los ejemplos de código C # parecen 100% imperativos, al igual que la mayoría de los lenguajes de programación populares. Es por eso que no podemos simplemente tomar lo que hizo langue X, porque simplemente no es lo mismo.

No es cierto (puedes comprobar cualquier marco, por ejemplo, polly ), pero no discutiré sobre ello. Veo tantos códigos encadenados en ambos idiomas. Sin embargo, en realidad es una cosa que hace que todo sea diferente. Y se llama rasgo Try . C # no tiene nada similar, por lo que se supone que no debe hacer nada más allá de await point. Si obtiene una excepción, se ajustará y generará automáticamente para la persona que llama.

No es el caso de Rust, donde tienes que usar manualmente el operador ? .

Si tiene que tener algo más allá de await punto, debe crear el operador "combinado" await? , extender las reglas del idioma, etc., etc. O puede simplemente hacer un sufijo de espera y las cosas se vuelven más naturales un poco de sintaxis alienígena, pero ¿a quién le importa?).

Así que actualmente veo que postfix aguarda como una solución más viable. La única sugerencia que tengo debería ser una palabra clave dedicada separada por espacios, no await() o await!() .

Creo que se me ocurrió una buena razón para justificar un método .await () normal. Si lo piensas, no es diferente a cualquier otra forma de bloqueo. Usted llama al método .await (), la ejecución del hilo (posiblemente verde) se detiene y, en algún momento, el tiempo de ejecución en ejecución reanuda la ejecución del hilo en algún momento. Entonces, para todos los efectos, ya sea que tenga un canal Mutex o estándar como este:

let result = my_channel().recv()?.iter().map(...).collect();

o un futuro como este:

let result = my_future().await()?.iter().map(...).collect();

no es diferente. Ambos bloquean la ejecución en sus respectivos recv () / await () y ambos encadenan de la misma manera. La única diferencia es que await () posiblemente se esté ejecutando en un ejecutor diferente que no es el subproceso pesado del sistema operativo (pero puede muy bien ser en el caso de un ejecutor de un solo subproceso). Por lo tanto, desalentar grandes cantidades de encadenamiento afecta a ambos exactamente de la misma manera y probablemente debería conducir a una pelusa real.

Sin embargo, mi punto aquí es que, desde el punto de vista de la persona que escribe este código, no hay mucha diferencia en .recv () o .await (), ambos son métodos que bloquean la ejecución y regresan una vez que el resultado está disponible . Entonces, para todos los efectos, puede ser prácticamente un método normal y no requiere una palabra clave completa. Tampoco tenemos una palabra clave recv o lock para Mutex y el canal std.

Sin embargo, obviamente, rustc quiere convertir todo el código en un generador. Pero, ¿es eso realmente semánticamente necesario desde el punto de vista del lenguaje? Estoy bastante seguro de que se podría escribir un compilador alternativo a rustc que implemente aguardar mediante el bloqueo de un subproceso del sistema operativo real (ingresar un fn asíncrono necesitaría generar un subproceso y luego esperar bloquearía ese subproceso). Si bien esa sería una implementación extremadamente ingenua y lenta, se comportaría semánticamente exactamente de la misma manera. Por lo tanto, el hecho de que rustc se convierta en await en un generador no es semánticamente necesario. Entonces, en realidad, diría que rustc convertir la llamada al método .await () en un generador puede verse como un detalle de implementación de optimización de rustc. De esa manera, puede justificar que .await () es una especie de método completo y no una palabra clave completa, pero también puede hacer que rustc convierta todo en un generador.

@CryZe a diferencia de .recv() , podemos interrumpir con seguridad await desde cualquier otro lugar del programa y luego el código junto a await no se ejecutaría. Esa es una gran diferencia y esa es la razón más valiosa por la que no deberíamos hacer await implícitamente.

@ I60R No es menos explícito que aguardar como palabra clave. Aún puede resaltarlo de la misma manera en el IDE si lo desea. Simplemente mueve la palabra clave a la posición postfix, donde yo diría que en realidad es menos fácil pasarla por alto (ya que está exactamente en la posición donde se detiene la ejecución).

Idea loca: espera implícita . Me he movido a un hilo separado, porque este ya está demasiado ocupado con la discusión de postfijo vs prefijo.

@CryZe

Creo que se me ocurrió una buena razón para justificar un método .await () normal. Si lo piensas, no es diferente a cualquier otra forma de bloqueo.

Vuelva a leer https://github.com/rust-lang/rust/issues/57640#issuecomment -456147515

@ Matthias247 Estos son muy buenos puntos, parece que los extrañé.

Toda esta charla sobre hacer await en una función, miembro o implícita de otra manera es fundamentalmente inválida. No es una acción, es una transformación.

El compilador, en un nivel alto, ya sea a través de una palabra clave o macro, transforma las expresiones de espera en expresiones de rendimiento del generador especial, en el lugar, contabilizando los préstamos y otras cosas en el camino.

Esto debería ser explícito.

Debería ser lo más aparente posible como palabra clave o, obviamente, generar código con una macro.

Los métodos mágicos, los rasgos, los miembros, etc. son demasiado fáciles de interpretar y malinterpretar, especialmente para los nuevos usuarios.

La palabra clave de prefijo await o la macro de prefijo await parecen la forma más aceptable de hacer esto, lo cual tiene sentido ya que muchos otros lenguajes lo hacen de esa manera. No tenemos que ser especiales para hacerlo bien.

@novacrazy

Los métodos mágicos, los rasgos, los miembros, etc. son demasiado fáciles de interpretar y malinterpretar, especialmente para los nuevos usuarios.

¿Se puede ampliar al respecto? Más específicamente en la variante .await!() , si es posible.

Tener un foo.await!() no es difícil de leer, en mi opinión, ESPECIALMENTE con el resaltado de sintaxis adecuado, que no debe ignorarse.

¿Entendiste mal el significado de eso? Lo que hace es esencialmente lo siguiente (ignore el tipo mystakes):

trait Future {
    fn await!(self) -> Self::Item {
        await self
    }
}

AKA await this_foo y this_foo.await!() son exactamente iguales. ¿Qué es fácil de malinterpretar sobre eso?

Y sobre el tema de los nuevos usuarios: ¿nuevos usuarios a qué? ¿Programación en general, o nuevos usuarios de Rust como lenguaje pero con experiencia en lenguaje de programación? Porque si es lo primero, dudo que incursionen en la programación asincrónica de inmediato. Y si es lo último, entonces es más fácil explicar la semántica de postfix await (como se explicó anteriormente) sin confusión.

Alternativamente, si solo se agrega el prefijo await, nada de lo que yo sepa detiene la creación de un programa que toma como entrada el código de forma de Rust

foo.bar().baz().quux().await!().melo().await!()

y lo transforma en

await (await foo.bar().baz().quux()).melo()

@ivandardi

.await!() es bueno para algunos casos, y probablemente funcionaría junto con await!(...) como:

macro_rules! await {
    // prefix
    ($fut:expr) => {...}

    // postfix
    ($self:Self) => { await!($self) }
}

Sin embargo, las macros del método postfix no existen en este momento y es posible que nunca existan.

Si pensamos que es una posibilidad en el futuro, deberíamos ir con la macro await!(...) por ahora y simplemente agregar el sufijo en el futuro cuando se implemente.

Tener ambas macros sería ideal, pero si no hay intención de implementar macros postfix en el futuro, la palabra clave de prefijo await es probablemente la mejor opción.

@novacrazy Estoy de acuerdo con eso y es mi propuesta original. Deberíamos agregar await!() por ahora y encontrar un formulario de sufijo a medida que avanzamos. Potencialmente, también discutiremos la posibilidad de macros postfix y si pudiéramos ad-hoc un postfix .await!() en el idioma antes de tener soporte completo de macros postfix en el idioma. Un poco como lo que sucedió con ? y el rasgo Try : se agregó primero como un caso especial y luego se expandió a un caso más general. Lo único que tendríamos que tener cuidado al decidir es cómo se vería la sintaxis de la macro de sufijo general, que podría merecer una discusión por separado.

La palabra clave de prefijo await o la macro de prefijo await parecen la forma más aceptable de hacer esto, lo cual tiene sentido ya que muchos otros lenguajes lo hacen de esa manera.

Es obvio que es viable, pero solo recuerdo dos argumentos a favor y no los encuentro convincentes:

  • Así es como lo hacen otros idiomas

    • Eso está bien, pero ya estamos haciendo las cosas de manera diferente, como no-ejecutar-a menos que- poll ed en lugar de ejecutar-hasta-el-primero- await , porque el óxido es fundamentalmente diferente idioma

  • A la gente le gusta ver await al principio de la línea

    • Pero no siempre está ahí, así que si ese es el único lugar donde uno mira, tendrá problemas

    • Es tan fácil ver los await s en foo(aFuture.await, bFuture.await) como si fueran un prefijo

    • En rust, uno ya está escaneando el _end_ de la línea para ? si observa el flujo de control

¿Me he perdido algo?

Si el debate fuera "bueno, son todos iguales", estaría absolutamente de acuerdo con "bueno, si realmente no nos importa, podríamos hacer lo que hacen los demás". Pero no creo que sea ahí donde estamos.

@scottmcm Sí a "meh, son todos iguales". Al menos desde la perspectiva del usuario.

Por tanto, deberíamos encontrar un equilibrio decente entre legibilidad, familiaridad, facilidad de mantenimiento y rendimiento. Por lo tanto, mi declaración en mi último comentario es cierta, con macro await!() si pretendemos agregar macros de método postfix ( .await!() ) en el futuro, o simplemente una palabra clave de prefijo aburrida await contrario.

Digo aburrido, porque aburrido es bueno. Queremos mantener nuestra mente alejada de la sintaxis misma al escribir código con estos.

Si f.await() no sería una buena idea, prefiero la sintaxis de prefijo.

  1. Como usuario, espero que el lenguaje que uso tenga solo unas pocas reglas de sintaxis, y al usar estas reglas, puedo inferir de manera confiable lo que podría estar haciendo. NO excepciones aquí y allá. En Rust, async está al principio, await al principio no será una excepción. Sin embargo, f await , el formulario de palabra clave de publicación sería. f.await parece un acceso de campo, una excepción . f.await!() tiene macro de sufijo nunca apareció en el idioma, y ​​no sé para qué otros casos sería bueno, una excepción . No tenemos respuesta sobre cómo estas sintaxis se convertirían en reglas, no en excepciones de una sola vez .

  2. Cuando se hace una excepción, espero que tenga sentido intuitivo. Tome ? como ejemplo, que puede verse como una excepción porque no es frecuente en otros idiomas. f()?.map() lee casi como calcular f () y ¿este resultado es bueno? El ? aquí se explica a sí mismo. Pero por f await pregunto por qué es postfix ?, por f.await pregunto es await un campo, f.await!() pregunto por qué aparecen las macros en esa posición? No me proporcionan un sentido convincente / intuitivo, al menos a primera vista.

  3. Ampliando el primer punto, a Rust le gustaría más ser un lenguaje de sistemas. Los jugadores principales aquí, C / C ++ / Go / Java son todos algo imperativos. También creo que la mayoría de los chicos comienzan su carrera con un lenguaje imperativo, C / Python / Java, no Haskell, etc. Supongo que para persuadir a los desarrolladores de sistemas y a los desarrolladores de la próxima generación de que adopten Rust, Rust primero debe hacerlo bien en estilo imperativo y luego funcional, no ser muy funcional pero no tener un sentimiento imperativo familiar.

  4. Creo que no hay nada de malo en dividir una cadena y escribirla en varias líneas. No será prolijo. Es simplemente explícito .

Al ver cómo la discusión se mueve continuamente de una dirección a otra (prefijo vs postfijo) desarrollé una fuerte opinión de que hay algo mal con la palabra clave await y debemos retirarnos de ella por completo. En cambio, propongo la siguiente sintaxis que creo que será un muy buen compromiso entre todas las opiniones que se publicaron en el hilo actual:

// syntax below is exactly the same as with prefix `await`
let response = go client.get("https://my_api").send();
let body: MyResponse = go response.into_json();

En el primer paso, lo implementaríamos como un operador de prefijo regular sin ningún error en el manejo de superestructuras específicas:

// code below don't compiles because `?` takes precedence over `go`
let response = go client.get("https://my_api").send()?;
let body: MyResponse = go response.into_json()?;

En el segundo paso, implementaríamos una sintaxis de operador de prefijo diferido que también permitiría el manejo adecuado de errores.

// now `go` takes precedence over `?` if present
let response = client.get("https://my_api").go send()?;
let body: MyResponse = response.go into_json()?;

Eso es todo.


Ahora veamos algunos ejemplos adicionales que brindan más contexto:


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

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

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

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

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

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

    Ok(user)
}

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

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

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

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

Hay una versión oculta debajo del spoiler con el resaltado de sintaxis habilitado


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

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

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

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

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

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

    Ok(user)
}

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

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

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

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


En mi opinión, esta sintaxis es mejor que las alternativas en todos los aspectos:

✓ Consistencia: se ve muy orgánico dentro del código de Rust y no requiere romper el estilo del código
✓ Capacidad de composición: se integra bien con otras características de Rust como encadenamiento y manejo de errores
✓ Simplicidad: es breve, descriptiva, fácil de entender y fácil de trabajar.
✓ Reutilización: la sintaxis del operador de prefijo diferido también sería útil en otros contextos
✓ Documentación: implica que los despachos del lado de la llamada fluyen en algún lugar y espera hasta que regresa
✓ Familiaridad: proporciona un patrón ya familiar pero lo hace con menos compensaciones
✓ Legibilidad: se lee en inglés sencillo y no distorsiona el significado de las palabras
✓ Visibilidad: su posición hace que sea muy difícil disfrazarse en algún lugar del código
✓ Accesibilidad: se puede buscar fácilmente en Google
✓ Bien probado: golang es popular hoy en día con una sintaxis de apariencia similar
✓ Sorpresas: es difícil malinterpretar y abusar de esa sintaxis
✓ Gratificación: después de una pequeña cantidad de aprendizaje, cada usuario estará satisfecho con el resultado.


Editar: como @ivandardi señaló en el comentario a continuación, algunas cosas deben aclararse:

1. Sí, esta sintaxis es un poco difícil de analizar a simple vista, pero al mismo tiempo es imposible inventar una sintaxis aquí que no tenga problemas de legibilidad. go sintaxis de await , y en la posición diferida es IMO más legible que el sufijo await p.ej:

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

donde not await parece ambiguo y tiene una asociatividad diferente a todas las palabras clave existentes, pero también podría convertirse en un bloqueador si decidimos implementar bloques await en el futuro.

2. Requerirá la creación de RFC, la implementación y estabilización de la sintaxis de "operador de prefijo diferido". Esta sintaxis no sería específica del código asíncrono, sin embargo, usarla en modo asíncrono sería la motivación principal para ello.

3. En la sintaxis de "operador de prefijo diferido", las palabras clave son importantes porque:

  • la palabra clave larga aumentaría el espacio entre la variable y el método, lo que es malo para la legibilidad
  • la palabra clave larga es una elección extraña para el operador de prefijo
  • la palabra clave larga es innecesariamente detallada cuando queremos que el código asíncrono se vea como sincronizado

De todos modos, es muy posible comenzar a usar await lugar de go y luego cambiarle el nombre en la edición 2022. Hasta ese momento es muy probable que se inventen diferentes palabras clave u operadores. E incluso podemos decidir que no se requiere ningún cambio de nombre. Descubrí que await es legible.

Hay una versión oculta debajo del spoiler con await usado en lugar de go


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

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

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

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

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

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

    Ok(user)
}

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

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

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

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


Si está aquí para votar en contra, asegúrese de:

  • que lo entendiste correctamente, ya que podría no ser el mejor orador para explicar cosas nuevas
  • que tu opinión no es parcial, ya que hay muchos prejuicios de dónde se origina esa sintaxis
  • que pondrás una razón válida a continuación, ya que los votos negativos silenciosos son extremadamente tóxicos
  • que tu razón no se trata de sentimientos inmediatos, ya que he intentado diseñar una función a largo plazo aquí

@ I60R

Tengo algunas quejas con eso.

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

Eso es un poco difícil de analizar a simple vista. A primera vista, pensarías que coincidiríamos en db.go , pero luego hay algo más y dices "oooh, ok, load es un método de db". En mi opinión, la sintaxis no funcionará principalmente porque los métodos siempre deben estar cerca del objeto al que pertenecen, sin espacios ni interrupciones de palabras clave entre ellos.

En segundo lugar, su propuesta requiere definir qué es la "sintaxis de operador de prefijo diferido" y, posiblemente, generalizarla en el idioma primero. De lo contrario, será algo que solo se usará para código asincrónico, y volvemos a la discusión de "¿por qué no postfix macros?" también.

En tercer lugar, no creo que la palabra clave importe en absoluto. Sin embargo, deberíamos favorecer await ya que la palabra clave se ha reservado para la edición 2018, mientras que la palabra clave go no lo ha hecho y debería someterse a una búsqueda más profunda en crates.io antes de ser propuesta.

Al ver cómo la discusión se mueve continuamente de una dirección a otra (prefijo vs postfijo) desarrollé una fuerte opinión de que hay algo mal con la palabra clave await y debemos apartarnos de ella por completo.

Podríamos tener una espera (semi-) implícita y todo estaría bien, pero será difícil de vender en la comunidad de Rust, incluso a pesar del hecho de que el objetivo de las funciones asíncronas es ocultar los detalles de implementación y hacer que se vean bien. como bloquear io one.

@dpc Quiero decir, la mejor forma en que puedo pensar que funciona como un compromiso entre la espera semi-implícita y el deseo de que las secciones de código se esperen explícitamente es algo similar a unsafe .

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

Donde todo dentro del bloque await se esperaría automáticamente. Eso resuelve la necesidad de posfijo en espera de encadenamiento, ya que en su lugar puede esperar toda la cadena. Pero al mismo tiempo, no estoy seguro de si esa forma sería deseable. Tal vez alguien más pueda encontrar una buena razón en contra de esto.

@dpc El objetivo de las funciones asíncronas no es ocultar los detalles, es hacerlas más accesibles y fáciles de usar. Nunca deberíamos ocultar operaciones potencialmente costosas y deberíamos aprovechar los futuros explícitos siempre que sea posible.

Nadie confunde las funciones asíncronas con las síncronas, en ningún idioma. Es simplemente para organizar secuencias de operaciones en una forma más legible y fácil de mantener, en lugar de escribir a mano una máquina de estado.

Además, las esperas implícitas como se muestra en el ejemplo de Future , que, como ya he mostrado, siguen siendo increíblemente poderosos. No podemos simplemente tomar cualquier Future y esperarlo, eso sería muy ineficiente.

Explícito, familiar, legible, mantenible y eficaz: palabra clave de prefijo await o macro await!() , con la posible macro de sufijo .await!() en el futuro.

@novacrazy Incluso si se

Por supuesto, puede seguir utilizando combinadores, nada está en desuso.

Es la misma situación que ? : el código nuevo usualmente usará ? (porque es mejor que los combinadores), pero los combinadores Opción / Resultado aún se pueden usar (y los más divertidos como or_else todavía se usan regularmente).

En particular, async / await puede reemplazar completamente map y and_then , pero los combinadores Future más funky pueden (y serán) todavía usados.

Los ejemplos que da todavía se ven terribles en comparación con los combinadores,

No lo creo, creo que son mucho más claros, porque usan características estándar de Rust.

La claridad rara vez se trata de la cantidad de personajes, pero tiene mucho que ver con conceptos mentales.

Con el sufijo await se ve aún mejor:

some_op().await?
    .another_op().await?
    .final_op().await

Puedes comparar eso con tu original:

some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())

y con la sobrecarga del generador probablemente será un poco más lento y producirá más código de máquina.

¿Por qué afirma que habrá una sobrecarga adicional de async / await? Tanto los generadores como async / await están diseñados específicamente para ser de costo cero y muy optimizados.

En particular, async / await se compila en una máquina de estado altamente optimizada, al igual que los combinadores Future.

en lugar de crear estas máquinas generadoras de estado espagueti bajo el capó.

Los combinadores del futuro también crean una "máquina de estado espagueti" debajo del capó, que es exactamente para lo que fueron diseñados.

No son fundamentalmente diferentes de async / await.

[Los combinadores futuros] tienen el potencial de compilar hasta un código más eficiente.

Deje de difundir información errónea. En particular, async / await tiene la posibilidad de ser más rápido que los combinadores Future.

Si resulta que los usuarios no están contentos con una palabra clave de prefijo, se puede cambiar en una edición 2019/2020.

Como han dicho otros, las ediciones no son algo arbitrario que simplemente hacemos cuando nos da la gana, están específicamente diseñadas para suceder solo cada pocos años (como muy pronto).

Y como señaló @Centril , crear una mala sintaxis solo para reemplazarla más tarde es una forma muy ineficiente de hacer las cosas.

El ritmo de la discusión es bastante alto en este momento; así que en un esfuerzo por ralentizar las cosas y permitir que la gente se ponga al día, bloqueé el problema temporalmente . Se desbloqueará en un día. En ese momento, trate de ser lo más constructivo posible para avanzar.

Desbloquear el problema un día después ... Por favor, considere no hacer puntos ya hechos y mantenga los comentarios sobre el tema (por ejemplo, este no es el lugar para considerar la espera implícita u otras cosas fuera de alcance ...).

Como este ha sido un hilo largo, destaquemos algunos de los comentarios más interesantes enterrados en él.

@mehcode nos proporcionó un extenso código del mundo real con await en las posiciones de prefijo y postfijo: https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

@Centril argumentó de manera persuasiva que estabilizar await!(expr) equivale a agregar deuda técnica a sabiendas: https://github.com/rust-lang/rust/issues/57640#issuecomment -455806584

@valff nos recordó que la sintaxis de la palabra clave expr await postfix encajaría perfectamente con la asignación de tipo generalizada : https://github.com/rust-lang/rust/issues/57640#issuecomment -456023146

@quodlibetor destacó que el efecto sintáctico de los combinadores Error y Option es exclusivo de Rust y argumenta a favor de la sintaxis postfix: https://github.com/rust-lang/rust/issues/57640#issuecomment -456143523

Al final del día, el equipo de lang tendrá que hacer una llamada aquí. Con el fin de identificar puntos en común que puedan conducir a un consenso, quizás sería útil resumir las posiciones declaradas de los miembros del equipo de lang sobre el tema clave de la sintaxis postfix:

  • @Centril expresó su apoyo a la sintaxis postfix y exploró una serie de variaciones en este ticket. Centril particularmente no quiere estabilizar await!(expr) .

  • @cramertj expresó su apoyo a la sintaxis de postfix y específicamente a la sintaxis de palabras clave de postfix expr await .

  • @joshtriplett expresó su apoyo a la sintaxis de sufijo y sugirió que también deberíamos proporcionar una versión de prefijo.

  • @scottmcm expresó su apoyo a la sintaxis postfix.

  • @withoutboats no quiere estabilizar una sintaxis macro. Aunque preocupado por agotar nuestro "presupuesto de desconocidos", sin barcos considera que la sintaxis de la palabra clave expr await postfix tiene "una oportunidad real".

  • @aturon , @eddyb , @nikomatsakis y @pnkfelix aún no han expresado ninguna posición con respecto a la sintaxis en los números # 57640 o # 50547.

Entonces, las cosas que necesitamos para encontrar una respuesta sí / no a:

  • ¿Deberíamos tener sintaxis de prefijo?
  • ¿Deberíamos tener sintaxis postfix?
  • ¿Deberíamos tener sintaxis de prefijo ahora y averiguar la sintaxis de sufijo más tarde?

Si optamos por la sintaxis de prefijo, hay dos contendientes principales que creo que la gente acepta más: Útil y Obvio, como se ve en este comentario . Los argumentos en contra de await!() son bastante fuertes, por lo que lo considero descartado. Así que tenemos que averiguar si queremos una sintaxis útil u obvia. En mi opinión, deberíamos elegir la sintaxis que utilice menos paréntesis en general.

Y en cuanto a la sintaxis del sufijo, es más complicado. También hay argumentos sólidos para la palabra clave postfix await con espacio. Pero eso depende mucho de cómo se formatee el código. Si el código está mal formateado, el sufijo await palabra clave con espacio puede verse realmente mal. Entonces, primero, tendríamos que asumir que todo el código de Rust se formateará correctamente con rustfmt, y que rustfmt siempre dividirá las esperas encadenadas en diferentes líneas, incluso si caben en una sola línea. Si podemos hacer eso, entonces esa sintaxis está muy bien, ya que resuelve el problema de legibilidad y confusión con espacios en el medio de una cadena de una sola línea.

Y finalmente, si optamos tanto por el prefijo como por el sufijo, tenemos que averiguar y escribir la semántica de ambas sintaxis para ver cómo interactúan entre sí y cómo se convertiría una en la otra. Debería ser posible hacerlo, ya que elegir entre sufijo o prefijo await no debería cambiar la forma en que se ejecuta el código.

Para aclarar mi proceso de pensamiento y cómo abordo y evalúo propuestas de sintaxis de superficie wrt. await ,
aquí están los objetivos que tengo (sin ningún orden de importancia):

  1. await debe seguir siendo una palabra clave para permitir el diseño de idiomas en el futuro.
  2. La sintaxis debe sentirse de primera clase.
  3. La espera debe poder encadenarse para componer bien con ? y métodos en general, ya que son frecuentes en Rust. No se debe obligar a uno a realizar enlaces temporales let que pueden no ser subdivisiones significativas.
  4. Debería ser fácil grep para los puntos de espera.
  5. Debería ser posible ver los puntos de espera de un vistazo.
  6. La precedencia de la sintaxis debe ser intuitiva.
  7. La sintaxis debe componerse bien con IDE y memoria muscular.
  8. La sintaxis debería ser fácil de aprender.
  9. La sintaxis debe ser ergonómica para escribir.

Habiendo definido (y probablemente olvidado ...) algunos de mis objetivos, aquí hay un resumen y mi valoración de algunas propuestas con respecto a ellos:

  1. Mantener await como palabra clave dificulta el uso de una sintaxis basada en macros, ya sea await!(expr) o expr.await!() . Para usar una sintaxis de macro, await como macro se codifica y no se integra con la resolución de nombres (es decir, use core::await as foo; vuelve imposible), o await se renuncia por completo como palabra clave.

  2. Creo que las macros tienen un sentimiento que no es de primera clase sobre ellas, ya que están destinadas a inventar sintaxis en el espacio del usuario. Aunque no es un punto técnico, el uso de la sintaxis macro en una construcción tan central del lenguaje da una impresión sin pulir. Podría decirse que las sintaxis .await y .await() tampoco son las más de primera clase; pero no tan de segunda clase como se sentirían las macros.

  3. El deseo de facilitar el encadenamiento hace que cualquier sintaxis de prefijo funcione mal, mientras que las sintaxis de postfijo se componen naturalmente con cadenas de métodos y ? en particular.

  4. Grepping es más fácil cuando se usa la palabra clave await ya sea sufijo, prefijo, macro, etc. Cuando se usa una sintaxis basada en sigilo, por ejemplo, # , @ , ~ , grepping se vuelve más difícil. La diferencia no es grande, pero .await es un poco más fácil de grep por await y await , ya que cualquiera de ellos podría incluirse como palabras en los comentarios, mientras que es más improbable que .await .

  5. Es probable que los sellos sean más difíciles de detectar de un vistazo, mientras que await es más fácil de ver y, especialmente, la sintaxis está resaltada. Se ha sugerido que .await u otras sintaxis de sufijo son más difíciles de detectar de un vistazo o que el sufijo await ofrece poca legibilidad. Sin embargo, en mi opinión, @scottmcm señala correctamente que los futuros de resultados son comunes y que .await? ayuda a llamar la atención sobre sí mismo. Además, Scott señala que el prefijo await conduce a oraciones de ruta de jardín y que el prefijo aguarda en medio de las expresiones no es más legible que el sufijo. Con la llegada del operador ? , los programadores de Rust ya necesitan escanear el final de las líneas para buscar el flujo de control .

  6. La precedencia de .await y .await() se destaca por ser completamente predecible; funcionan como sus contrapartes de acceso de campo y llamada de método. Una macro de sufijo presumiblemente tendría la misma precedencia que una llamada a un método. Mientras tanto, el prefijo await tiene una precedencia consistente, predecible y no útil en relación con ? (es decir, await (expr?) ), o la precedencia es inconsistente y útil (es decir, (await expr)? ). A un sigilo se le puede dar la precedencia deseada (por ejemplo, tomando su intuición de ? ).

  7. En 2012, @nikomatsakis señaló que Simon Peyton Jones señaló una vez (p. 56) que siente envidia del "poder del punto" y cómo proporciona la magia IDE mediante la cual se puede reducir la función o el campo al que se refiere. Dado que el poder del punto existe en muchos lenguajes populares (por ejemplo, Java, C #, C ++, ..), esto ha llevado a que "alcanzar el punto" esté arraigado en la memoria muscular. Para ilustrar lo poderoso que es este hábito, aquí hay una captura de pantalla, debida a @scottmcm , de Visual Studio con Re # er:
    Re#er intellisense

    C # no tiene posfijo en espera. No obstante, esto es tan útil que se muestra en una lista de autocompletar sin ser una sintaxis válida. Sin embargo, puede que a muchos, incluido yo mismo, no se les ocurra probar .aw cuando .await no es la sintaxis superficial. Considere el beneficio de la experiencia IDE si lo fuera. Las sintaxis await expr y expr await no ofrecen ese beneficio.

  8. Los sigilos probablemente ofrecerían poca familiaridad. El prefijo await tiene el beneficio de estar familiarizado con C #, JS, etc. Sin embargo, estos no separan await y ? en operaciones distintas, mientras que Rust sí. Tampoco es exagerado pasar de await expr a expr.await ; el cambio no es tan radical como ir con un sigilo. Además, debido a la potencia de puntos antes mencionada, es probable que se aprenda .await escribiendo expr. y viendo await como la primera opción en la ventana emergente de autocompletar.

  9. Los sellos son fáciles de escribir y ofrecerían una buena ergonomía. Sin embargo, aunque .await es más largo de escribir, también viene con dot-powers que pueden hacer que la escritura fluya aún mejor. No tener que romper en declaraciones de let también facilita una mejor ergonomía. El prefijo await , o peor aún, await!(..) , carece de poderes de puntos y habilidades de encadenamiento. Al comparar .await , .await() y .await!() , las primeras ofertas son las más concisas.

Dado que ninguna sintaxis es la mejor para lograr todos los objetivos al mismo tiempo, se debe hacer una compensación. Para mí, la sintaxis con más beneficios y menos inconvenientes es .await . En particular, se puede encadenar, conserva await como palabra clave, es greppable, tiene poderes de punto y, por lo tanto, se puede aprender y es ergonómico, tiene una precedencia obvia y, finalmente, es legible (especialmente con un buen formato y resaltado).

@Centril Solo para aclarar algunas preguntas. En primer lugar, me gustaría confirmar que solo desea que el postfix espere, ¿verdad? En segundo lugar, ¿cómo abordar la dualidad de que .await sea ​​un acceso de campo? ¿Estaría bien tener .await como tal, o debería favorecerse .await() con el paréntesis para implicar que algún tipo de operación está sucediendo allí? Y en tercer lugar, con la sintaxis .await , ¿esperaría ser la palabra clave o simplemente un identificador de "acceso al campo"?

En primer lugar, me gustaría confirmar que solo desea que el postfix espere, ¿verdad?

Sí. Ahora mismo al menos.

En segundo lugar, ¿cómo abordar la dualidad de que .await sea ​​un acceso de campo?

Es un pequeño inconveniente; aunque hay una gran ventaja en el poder del punto. La distinción es algo que creo que los usuarios aprenderán rápidamente, especialmente porque está resaltada por palabras clave y la construcción se usará con frecuencia. Además, como señaló Scott, .await? será el más común, lo que debería aliviar aún más la situación. También debería ser fácil agregar una nueva entrada de palabra clave para await en rustdoc, al igual que lo hemos hecho para, por ejemplo, fn .

¿Estaría bien tener .await como tal, o debería favorecerse .await() con el paréntesis para implicar que algún tipo de operación está sucediendo allí?

Prefiero .await sin cola () ; tener () al final parece una sal innecesaria en la mayoría de los casos y, supongo, haría que los usuarios se inclinaran más a buscar el método.

Y en tercer lugar, con la sintaxis .await , ¿esperaría ser la palabra clave o simplemente un identificador de "acceso al campo"?

await seguiría siendo una palabra clave. Presumiblemente, cambiaría libsyntax de tal manera que no se produjera un error al encontrar await después de . y luego lo representaría de manera diferente en AST o al bajar a HIR ... pero eso es principalmente un detalle de implementación .

¡Gracias por aclarar todo eso! 👍

En este punto, la cuestión clave parece ser si se debe seguir una sintaxis de sufijo. Si esa es la dirección, entonces seguramente podemos estrecharnos en cuál. Al leer la sala, la mayoría de los partidarios de la sintaxis de sufijo aceptarían cualquier sintaxis de sufijo razonable sobre la sintaxis de prefijo.

Al entrar en este hilo, la sintaxis postfix parecía ser la más débil. Sin embargo, ha atraído el claro apoyo de cuatro miembros del equipo de lang y la apertura de un quinto (los otros cuatro han estado en silencio hasta ahora). Además, ha atraído un apoyo considerable de la comunidad en general que comenta aquí.

Además, los partidarios de la sintaxis postfix parecen tener una clara convicción de que es el mejor camino para Rust. Parece que sería necesario algún problema profundo o refutaciones convincentes a los argumentos presentados hasta ahora para rechazar este apoyo.

Dado esto, parece que necesitamos escuchar a @withoutboats , que seguramente ha estado siguiendo este hilo de cerca, y a los otros cuatro miembros del equipo de lang. Sus opiniones probablemente impulsarán este hilo. Si tienen preocupaciones, discutirlas sería la prioridad. Si están convencidos del caso de la sintaxis de sufijo, podemos pasar a encontrar consenso para cuál.

Vaya, acabo de notar que @Centril ya había comentado, así que ocultaré mi publicación por limpieza.

@ivandardi Dado el objetivo (1) de la publicación, tengo entendido que await seguiría siendo una palabra clave, por lo que nunca sería un acceso de campo, de la misma manera que loop {} nunca es una expresión literal de estructura. Y esperaría que se resalte para hacer lo más obvio (como https://github.com/rust-lang/rust-enhanced/issues/333 está esperando hacer en Sublime).

Mi única preocupación es que hacer que la sintaxis de espera parezca un acceso diferente podría causar confusión en una base de código de la edición 2015 sin darme cuenta. (2015 es el valor predeterminado cuando no se especifica, abre el proyecto de otra persona, etc.) Siempre que la edición de 2015 tenga un error claro cuando no hay un campo await (y una advertencia si lo hay), creo eso (junto con el maravilloso argumento de Centril) elimina mis preocupaciones personales sobre un campo de palabra clave (o método).

Ah, y una cosa que también deberíamos decidir mientras estamos en eso es cómo rustfmt terminaría formateándolo.

let val = await future;
let val = await returns_future();
let res = client.get("https://my_api").await send()?.await json()?;
  1. Utiliza await - cheque
  2. Primera clase - comprobar
  3. Encadenamiento - comprobar
  4. Grepping - comprobar
  5. Sin sigilo - comprobar
  6. Precedencia - ¹check
  7. Potencia de punto - ²check
  8. Fácil de aprender: ³ comprobar
  9. Ergonómico - comprobar

¹Precedencia: el espacio después de await hace que no sea obvio en la posición diferida. Sin embargo, es exactamente lo mismo que en el siguiente código: client.get("https://my_api").await_send()?.await_json()? . Para todos los angloparlantes es incluso más natural que con todas las demás propuestas.

²Dot power: requeriría soporte adicional para que IDE mueva await a la izquierda de la llamada al método después de que se escriba . , ? o ; continuación. No parece ser demasiado difícil de implementar.

³Fácil de aprender: en la posición de prefijo ya es familiar para los programadores. En posición diferida sería obvio después de que se estabilizara la sintaxis


Pero el punto más fuerte de esta sintaxis es que será muy consistente:

  • No se puede confundir con el acceso a la propiedad.
  • Tiene una precedencia obvia con ?
  • Siempre tendrá el mismo formato
  • Coincide con el lenguaje humano
  • No se ve afectado por la ocurrencia horizontal variable en la cadena de llamadas al método *

* la explicación está oculta debajo del spoiler


Con la variante de sufijo es difícil predecir qué función es async y dónde estaría la próxima aparición de await en el eje horizontal:

let res = client.get("https://my_api")
    .very_long_method_name(param, param, param).await?
    .short().await?;

Con la variante diferida sería casi siempre lo mismo:

let res = client.get("https://my_api")
    .await very_long_method_name(param, param, param)?
    .await short()?;

¿No habría un. entre la espera y la siguiente llamada al método? Me gusta
get().await?.json() .

El miércoles 23 de enero de 2019 a las 05:06 I60R < [email protected] escribió:

deje val = esperar el futuro;
let res = client.get ("https: // my_api") .await send () ?. await json () ?;

  1. Usos aguardan - comprobar
  2. Primera clase - comprobar
  3. Encadenamiento - comprobar
  4. Grepping - comprobar
  5. Sin sigilo - comprobar
  6. Precedencia - ¹check
  7. Potencia de punto - ²check
  8. Fácil de aprender: ³ comprobar
  9. Ergonómico - comprobar

¹Precedencia: el espacio hace que no sea obvio en posición diferida. Sin embargo es
exactamente igual que en el siguiente código: client.get ("https: // my_api
") .await_send () ?. await_json () ?. Para todos los angloparlantes es aún más
natural que con todas las demás propuestas

²Dot power: requeriría soporte adicional para que IDE se mueva en espera a
la izquierda de la llamada al método después. o se escribe a continuación. No parece ser demasiado
difícil de implementar

³Fácil de aprender: en la posición de prefijo ya es familiar para los programadores,
y en posicin diferida sera obvio despus de que la sintaxis fuera

estabilizado

Pero el punto más fuerte de esta sintaxis es que será muy
consistente:

  • No se puede confundir con el acceso a la propiedad.
  • Tiene precedencia obvia con?
  • Siempre tendrá el mismo formato
  • Coincide con el lenguaje humano
  • No se ve afectado con la ocurrencia horizontal variable en la llamada al método
    cadena*

* la explicación está oculta debajo del spoiler

Con la variante postfix es difícil predecir qué función es asincrónica y
donde estaría la próxima aparición de await en el eje horizontal:

let res = client.get ("https: // my_api")

.very_long_method_name(param, param, param).await?

.short().await?;

Con la variante diferida sería casi siempre lo mismo:

let res = client.get ("https: // my_api")

.await very_long_method_name(param, param, param)?

.await short()?;

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456693759 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AIA8Ogyjc8LW6teZnXyzOCo31-0GPohTks5vGAoDgaJpZM4aBlba
.

También deberíamos decidir, mientras estamos en eso, cómo terminará rustfmt formateándolo

Ese es el dominio de https://github.com/rust-dev-tools/fmt-rfcs , por lo que diría que está fuera de tema para este problema. Estoy seguro de que habrá algo que sea consistente con cualquier corrección y precedencia que se elija.

@ I60R

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

¿Cómo se verían las funciones de funciones libres? Si lo estoy leyendo bien, en realidad es un prefijo await .

Creo que esto demuestra que no deberíamos apresurarnos a desacreditar la experiencia del equipo de lenguaje C #. Microsoft piensa mucho en los estudios de usabilidad, y aquí tomamos una decisión basada en una sola línea de código y la premisa de que Rust es lo suficientemente especial como para tener una sintaxis diferente de todos los demás lenguajes. No estoy de acuerdo con esa premisa: mencioné anteriormente que LINQ y los métodos de extensión son omnipresentes en C #.

Sin embargo, puede que a muchos, incluido yo mismo, no se les ocurra probar .aw cuando .await no es la sintaxis superficial.

~ No creo que se le ocurra a nadie familiarizado con los otros idiomas. ~ Aparte de eso, ¿esperaría que la ventana emergente de finalización incluya los métodos await y Future ?

[...] y aquí estamos tomando una decisión basada en una sola línea de código y la premisa de que Rust es lo suficientemente especial como para tener una sintaxis diferente de todos los demás lenguajes. No estoy de acuerdo con esa premisa [...]

@lnicola Solo quiero señalar en caso de que usted y otros se lo hayan perdido (es fácil perderse en la avalancha de comentarios): https://github.com/rust-lang/rust/issues/57640#issuecomment -455846086

Varios ejemplos reales del código de producción real.

Dado el comentario de @Centril anterior , parece que las opciones de sufijo en la contención más fuerte son expr await sintaxis de palabra clave de postfijo y sintaxis de campo de postfijo expr.await (y quizás la sintaxis del método de postfijo aún no está disponible).

En comparación con la palabra clave postfix, @Centril sostiene que el campo postfix se beneficia del "poder del punto", que los IDE pueden sugerirlo mejor como un autocompletado, y que las instancias de su uso pueden ser más fáciles de encontrar con grep porque el punto inicial proporciona una desambiguación del uso de la palabra en los comentarios.

Por el contrario, @cramertj ha argumentado a favor de la sintaxis de palabras clave postfix basándose en que dicha sintaxis deja en claro que no es una llamada a un método o un acceso a un campo. @withoutboats ha argumentado que la palabra clave postfix es la más familiar de las opciones de postfix.

Con respecto a "el poder del punto", debemos considerar que un IDE aún podría ofrecer await como finalización después de un punto y luego simplemente eliminar el punto cuando se selecciona la finalización. Un IDE suficientemente inteligente (por ejemplo, con RLS) podría incluso advertir el futuro y ofrecer await después de un espacio.

El campo de sufijo y las opciones de método parecen ofrecer la mayor superficie para las objeciones, debido a la ambigüedad, en comparación con la palabra clave de sufijo. Como parte del soporte para el campo / método postfix sobre la palabra clave postfix parece provenir de un ligero malestar con la presencia del espacio en el encadenamiento de métodos, deberíamos revisar y comprometernos con la observación de @valff de que la palabra clave postfix ( foo.bar() await ) no parecerá más sorprendente que la adscripción de tipo generalizada ( foo.bar() : Result<Vec<_>, _> ). Para ambos, el encadenamiento continúa después de este interludio separado por espacios.

@ivandardi , @lnicola ,

Actualicé mi comentario reciente con un ejemplo faltante para una llamada de función gratuita. Si quieres más ejemplos puedes consultar el último spoiler en mi comentario anterior.

Con el interés de debatir future.await? vs future await? ...

Algo que no se discute demasiado es la agrupación visual o grepping de una cadena de métodos.

considere ( match lugar de await para resaltado de sintaxis)

post(url).multipart(form).send().match?.error_for_status()?.json().match?

en comparación con

post(url).multipart(form).send() match?.error_for_status()?.json() match?

Cuando escaneo visualmente la primera cadena en busca de await , identifico clara y rápidamente send().await? y veo que estamos esperando el resultado del send() .

Sin embargo, cuando escaneo visualmente la segunda cadena en busca de await , primero veo await?.error_for_status() y tengo que ir, no, hacer una copia de seguridad y luego conectar send() await juntos.


Sí, algunos de estos mismos argumentos se aplican a la adscripción de tipo generalizada, pero esa no es una característica aceptada todavía. Si bien me gusta la adscripción de tipos en cierto sentido, creo que en un contexto de expresión _general_ (vago, lo sé) debería ser necesario que esté envuelto en parens. Sin embargo, todo eso está fuera del tema de esta discusión. También tenga en cuenta que la cantidad de await en una base de código tiene un gran cambio de ser significativamente mayor que la cantidad de adscripción de tipo.

La atribución de tipo generalizada también vincula el tipo a la expresión atribuida mediante el uso de un operador distinto : . Al igual que cualquier otro operador binario, leerlo deja (visualmente) claro que las dos expresiones son un solo árbol. Mientras que future await no tiene operador y podría confundirse fácilmente con future_await fuera del hábito de ver rara vez dos nombres no separados por un operador, excepto si el primero es una palabra clave (se aplican excepciones, que no es para digo que prefiero la sintaxis de prefijo, no lo hago).

Compare esto con el inglés si lo desea, donde se usa un guión ( - ) para agrupar visualmente palabras que de otra manera se interpretarían fácilmente como separadas.

Creo que esto demuestra que no deberíamos apresurarnos a desacreditar la experiencia del equipo de lenguaje C #.

No creo que "tan rápido" y "descrédito" sean justos aquí. Creo que aquí está sucediendo lo mismo que sucedió en el RFC async / await: considerar cuidadosamente por qué se hizo de una manera y averiguar si el equilibrio sale de la misma manera para nosotros. El equipo de C # incluso se menciona explícitamente en uno:

Pensé que la idea de await era que realmente querías el resultado, no el futuro
así que tal vez la sintaxis de espera debería ser más como la sintaxis 'ref'

let future = task()
pero
let await result = task()

así que para encadenar deberías hacer

task().chained_method(|future| { /* do something with future */ })

pero

task().chained_method(|await result| { /* I've got the result */ })
- foo.await             // NOT a real field
- foo.await()           // NOT a real method
- foo.await!()          // NOT a real macro

Todos funcionan bien con el encadenamiento, y todos tienen inconvenientes que no son un campo / método / macro real.
Pero dado que await , como palabra clave, ya es algo especial, no necesitamos hacerlo más especial.
Deberíamos seleccionar el más simple, foo.await . Tanto () como !() son redundantes aquí.

@liigo

- foo await        // IS neither field/method/macro, 
                   // and clearly seen as awaited thing. May be easily chained. 
                   // Allow you to easily spot all async spots.

@mehcode

Cuando escaneo visualmente la primera cadena en espera, identifico clara y rápidamente send (). y ver que estamos esperando el resultado de send ().

Me gusta más la versión espaciada, ya que es mucho más fácil ver dónde se construye el futuro y dónde se espera. Es mucho más fácil ver lo que pasa, en mi humilde opinión

image

Con la separación de puntos se parece muchísimo a una llamada a un método. El resaltado de código no ayudará mucho y no siempre está disponible.

Finalmente, creo que el encadenamiento no es el caso de uso principal (excepto ? , pero foo await? es lo más claro posible), y con single await se convierte en

post(url).multipart(form).send().match?

vs

post(url).multipart(form).send() match?

Donde este último parece mucho más delgado en mi opinión.

Entonces, si nos hemos reducido a future await y future.await , ¿podemos hacer que el "punto" mágico sea opcional? Para que la gente pueda elegir cuál quiere y, lo más importante, ¡dejar de discutir y seguir adelante!

No es perjudicial tener una sintaxis opcional, Rust tiene muchos ejemplos de este tipo. el más conocido es el último sperator ( ; o , o lo que sea, después del último elemento, como en (A,B,) ) son en su mayoría opcionales. No vi una razón sólida de por qué no podemos hacer que el punto sea opcional.

Creo que sería un buen momento para hacer esto en Nightly y dejar que el ecosistema decida cuál es el mejor para ellos. Podemos tener pelusas para hacer cumplir el estilo preferido, lo hace personalizable.

Luego, antes de aterrizar en Estable, revisamos el uso de la comunidad y decidimos si debemos elegir uno o simplemente dejar ambos.

Estoy completamente en desacuerdo con la idea de que las macros no se sentirían lo suficientemente de primera clase para esta característica (no la consideraría una característica tan central en primer lugar) y me gustaría volver a resaltar mi comentario anterior sobre await!() (no importa si prefijo o sufijo) no es una "macro real", pero supongo que al final mucha gente quiere que esta función se estabilice lo antes posible, en lugar de bloquearla en macros de postfijo y el prefijo en espera no es realmente un buen ajuste para Rust.

En cualquier caso, bloquear este hilo por un día solo ha ayudado mucho y ahora me daré de baja. No dude en mencionarme si responde directamente a uno de mis puntos.

Mi argumento en contra de la sintaxis con . es que await no es un campo y, por lo tanto, no debería verse como un campo. Incluso con el resaltado de sintaxis y la fuente en negrita, se verá como un campo de alguna manera . seguido de una palabra está fuertemente asociado con el acceso al campo.

Tampoco hay razón para usar la sintaxis con . para una mejor integración IDE, ya que podríamos potenciar cualquier sintaxis diferente con la finalización . usando algo como la función de finalización de postfijo disponible en Intellij IDEA.

Necesitamos percibir await como palabra clave de flujo de control. Si decidimos encarecidamente optar por el sufijo await , entonces deberíamos considerar la variante sin . y propongo usar el siguiente formato junto para enfatizar mejor los puntos de bloqueo con una posible interrupción:

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

Más ejemplos

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

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

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

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

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

Con resaltado de sintaxis

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

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

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

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

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

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


Esto también podría ser la respuesta al punto de @ivandardi sobre el formato

Encuentro que todos estos ejemplos de postfix esperan estar más en contra que a favor. Las largas cadenas de esperas se sienten mal, y el encadenamiento debe mantenerse para futuros combinadores.

Compare (con lo match como await para resaltar la sintaxis):

post(url)?.multipart(form).send()?.match?.error_for_status()?.json().match?

a

let value = await post(url)?.multipart(form).send()?.error_for_status()
                 .and_then(|resp| resp.json()) // and then parse JSON

// now handle errors

¿Notas la falta de esperas en el medio? Porque el encadenamiento generalmente se puede resolver con un mejor diseño de API. Si send() devuelve un futuro personalizado con métodos adicionales, por ejemplo, para convertir mensajes de estado que no sean 200 en errores graves, entonces no hay necesidad de esperas adicionales. En este momento, al menos en reqwest , parece ejecutarse en el lugar y producir un Result simple en lugar de un nuevo futuro, que es de donde viene el problema aquí.

Encadenar espera es un fuerte olor a código en mi opinión, y .await o .await() confundirá a tantos usuarios nuevos que ni siquiera es gracioso. .await siente especialmente como algo salido de Python o PHP, donde los accesos de miembros variables pueden mágicamente tener un comportamiento especial.

await debe ser al principio y obvio, como diciendo, "antes de continuar, esperamos este" no "y luego, esperamos este". Este último es literalmente el combinador and_then .

Otra forma potencial de pensarlo es considerar las expresiones y futuros async / await como iteradores. Ambos son perezosos, pueden tener muchos estados y pueden terminarse con sintaxis ( for-in para iteradores, await para futuros). No propondríamos una extensión de sintaxis para que los iteradores los encadenen como expresiones regulares como estamos aquí, ¿verdad? Usan combinadores para el encadenamiento, y las operaciones asincrónicas / futuros deberían hacer lo mismo, solo terminando una vez. Funciona bastante bien.

Finalmente, y lo más importante, quiero poder hojear verticalmente las primeras sangrías de una función y ver claramente dónde ocurren las operaciones asíncronas. Mi visión no es tan buena, y tener esperas al final de la línea o atascado en el medio haría las cosas mucho más difíciles que podría quedarme con futuros crudos por completo.

Todos, por favor, lean esta publicación del blog antes de comentar.

Async / await no se trata de conveniencia. No se trata de evitar los combinadores.

Se trata de permitir cosas nuevas que no se pueden hacer con combinadores.

Entonces, cualquier sugerencia de "usemos combinadores en su lugar" está fuera de tema y debe discutirse en otro lugar.

¿Notas la falta de esperas en el medio? Porque el encadenamiento generalmente se puede resolver con un mejor diseño de API.

No obtendrá una cadena bonita en un ejemplo asincrónico real. Ejemplo real:

req.into_body().concat2().and_then(move |chunk| {
    from_slice::<Update>(chunk.as_ref())
        .into_future()
        .map_err(|_| {
            ...
        })
        .and_then(|update| {
            ...
        })
        .and_then(move |(user, file_id, chat_id, message_id)| {
            do_thing(&file_id)
                .and_then(move |file| {
                    if some_cond {
                        Either::A(do_yet_another-thing.and_then(move |bytes| {
                            ...

                            if another_cond {
                                ...
                                Either::A(
                                    do_other_thing()
                                        .then(move |res| {
                                            ...
                                        }),
                                )
                            } else {
                                Either::B(future::ok(()))
                            }
                        }))
                    } else {
                        Either::B(future::ok(()))
                    }
                })
                .map_err(|e| {
                    ...
                })
        })
        // ...and here we unify both paths
        .map(|_| {
            Response::new(Body::empty())
        })
        .or_else(Ok)
})

Los combinadores no ayudan aquí. El anidamiento se puede eliminar, pero no es fácil (lo intenté dos veces y fallé). Tienes toneladas de Either:A(Either:A(Either:A(...))) al final.

OTOH se resuelve fácilmente con async / await .

@Pauan lo ha escrito antes de terminar mi publicación. Que así sea. No menciones los combinadores, async/await no deberían verse similares. Está bien si lo hace, hay cosas más importantes a considerar.


Rechazar mi comentario no hará que los combinadores sean más convenientes.

Al leer el comentario de adscripción de tipo , estaba pensando en cómo se vería combinado con await . Dada la reserva que algunas personas tienen con .await o .await() como campos de miembros confusos / acceso al método, me pregunto si una sintaxis de "adscripción" general podría ser una opción, (es decir, "dame lo que await devuelve ").

Usando yield por await para resaltar la sintaxis

let result = connection.fetch("url"):yield?.collect_async():yield?;

Combinado con la adscripción de tipo, por ejemplo:

let result = connection.fetch("url") : yield?
    .collect_async() : yield Vec<u64>?;

Ventajas: todavía se ve bien (en mi humilde opinión), sintaxis diferente del acceso al campo / método.
Desventajas: posibles conflictos con la adscripción de tipo (?), Múltiples adscripciones teóricamente posibles:

foo():yield:yield

Editar: Se me ocurrió que usar 2 adscripciones sería más lógico en el ejemplo combinado:

let result = connection.fetch("url") : yield?
    .collect_async() : yield : Vec<u64>?;

@Pauan sería inapropiado sobreaplicar esa publicación de blog. Los combinadores se pueden aplicar junto con la sintaxis de espera para evitar los problemas que describe.

El problema principal siempre fue que un futuro generable debe ser 'static , pero si capturaba variables por referencia en esos combinadores, terminaba con un futuro que no era 'static . Pero esperar un futuro no 'static no hace que la fn asincrónica en la que se encuentra no sea 'static . Entonces puedes hacer cosas como esta:

`` óxido
// corte: & [T]

deje x = esperar futuro. y_entonces (| i | si rebanada.contiene (i) {async_fn (rebanada)});

Entonces, estaba planeando no comentar en absoluto, pero quería agregar algunas notas de alguien que no escribe mucho código asincrónico, pero aún así tendrá que leerlo:

  • El prefijo await es mucho más fácil de notar. Esto es tanto cuando lo está esperando (dentro de un async fn) como fuera (en la expresión de algún cuerpo de macro).
  • El sufijo await como palabra clave puede destacar bien en cadenas con pocas o ninguna otra palabra clave. Pero tan pronto como tenga cierres o similar con otras estructuras de control, su awaits se volverá mucho menos notorio y más fácil de pasar por alto.
  • Encuentro .await como un pseudocampo muy extraño. No es como cualquier otro campo que me importe.
  • No estoy seguro de si confiar en la detección de IDE y el resaltado de sintaxis es una buena idea. Es valioso la facilidad de lectura cuando no se dispone de resaltado de sintaxis. Además, con las discusiones recientes sobre la complicación de la gramática y los problemas que causará para las herramientas, puede que no sea una buena idea confiar en que los IDE / editores hagan las cosas bien.

No comparto el sentimiento de que await!() como macro "no se siente de primera clase". Las macros son un ciudadano de primera clase en Rust. Tampoco están allí solo para "inventar sintaxis en el código de usuario". Muchas macros "integradas" no están definidas por el usuario y no están ahí por meras razones sintácticas. format!() está ahí para darle al compilador el poder de las cadenas de formato de verificación de tipo estáticamente. include_bytes!() tampoco está definido por el usuario y no está ahí por meras razones sintácticas, es una macro porque necesita ser una extensión de sintaxis ya que, nuevamente, hace cosas especiales dentro del compilador y no podría razonablemente estar escrito de otra manera sin agregarlo como una característica principal del lenguaje.

Y este es mi punto: las macros son una manera perfecta de introducir y ocultar la magia del compilador de una manera uniforme y nada sorprendente. Await es un buen ejemplo de esto: necesita un manejo especial, pero su entrada es solo una expresión y, como tal, es ideal para la realización usando una macro.

Entonces, realmente no veo la necesidad de una palabra clave especial en absoluto. Dicho esto, si va a ser una palabra clave, entonces debería ser solo eso, una simple palabra clave; realmente no entiendo la motivación de disfrazarlo como un campo. Eso simplemente parece incorrecto, no es un acceso de campo, ni siquiera remotamente como un acceso de campo (a diferencia de, digamos, los métodos getter / setter con sugar), por lo que tratarlo como uno es inconsistente con cómo funcionan los campos hoy en el idioma.

@ H2CO3 await! macro es buena en todas las formas deseables, excepto por dos propiedades:

  1. Los frenillos extra son realmente molestos cuando escribes otros miles de esperas. Rust eliminó las llaves de los bloques if/while/... por (creo) la misma razón. Puede parecer un cajero automático sin importancia, pero es realmente el caso
  2. Asimetría con async como palabra clave. Async debería hacer algo más grande que permitir una macro en el cuerpo del método. De lo contrario, podría ser el atributo #[async] en la parte superior de la función. Entiendo que ese atributo no le permitirá usarlo en bloques, etc., pero proporciona cierta expectativa de encontrar la palabra clave await . Esta expectativa puede ser causada por la experiencia en otros idiomas, quién sabe, pero creo firmemente que existe. Puede que no se aborde, pero debe tenerse en cuenta.

De lo contrario, podría ser el atributo #[async] en la parte superior de la función.

Durante mucho tiempo lo fue, y lamento que se vaya. Estamos introduciendo otra palabra clave mientras que el atributo #[async] funcionó perfectamente bien, y ahora con macros de procedimiento similares a atributos estables, incluso sería consistente con el resto del lenguaje tanto sintáctica como semánticamente, incluso si todavía se conociera (y manejado especialmente) por el compilador.

@ H2CO3 ¿cuál es el propósito o la ventaja de tener await como macro desde la perspectiva del usuario ? ¿Hay macros integradas que cambien el flujo de control de programas bajo el capó?

@ I60R La ventaja es la uniformidad con el resto del lenguaje y, por lo tanto, no tener que preocuparse por otra palabra clave más, con todo el equipaje que traería consigo, por ejemplo, la precedencia y la agrupación son obvias en una macro invocación.

No veo por qué la segunda parte es relevante: ¿no puede haber una macro incorporada que haga algo nuevo? De hecho, creo que casi todas las macros integradas hacen algo "extraño" que no se puede lograr (de manera razonable / eficiente / etc.) sin el soporte del compilador. await!() no sería un nuevo intruso en este sentido.

Anteriormente sugerí una espera implícita y, más recientemente, sugerí una espera no encadenable .

La comunidad parece estar muy a favor de la espera explícita (me encantaría que eso cambiara, pero…). Sin embargo, la granularidad de lo explícito es lo que crea un texto estándar (es decir, necesitarlo muchas veces en la misma expresión parece crear un texto estándar).

[ Advertencia : la siguiente sugerencia será controvertida, pero déle una oportunidad sincera]

Tengo curiosidad por saber si la mayoría de la comunidad se comprometería y permitiría una "espera implícita parcial". ¿Quizás leer esperar una vez por cadena de expresión sea lo suficientemente explícito?

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

Esto es algo parecido a la desestructuración de valores, a lo que llamo desestructuración de expresiones.

// Value de-structuring
let (a, b, c) = ...;

// De-sugars to:
let _abc = ...;
let a = _abc.0;
let b = _abc.1;
let c = _abc.2;
// Expression de-structuring
await response = client.get("https://my_api").send()?.json()?;

// De-sugards to:
// Using: prefix await with explicit precedence
// Since send() and json() return impl Future but ? does not expect a Future, de-structure the expression between those sub-expressions.
let response = await (client.get("https://my_api").send());
let response = await (response?.json());
let response = response?;



md5-eeacf588eb86592ac280cf8c372ef434



```rust
// If needed, given that the compiler knows these expressions creating a, b are independent,
// each of the expressions would be de-structured independently.
await (a, b) = (
    client.get("https://my_api_a").send()?.json()?,
    client.get("https://my_api_b").send()?.json()?,
);
// De-sugars to:
// Not using await syntax like the other examples since await can't currently let us do concurrent polling.
let a: impl Future = client.get("https://my_api_a").send();
let b: impl Future = client.get("https://my_api_b").send();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;

let a: impl Future = a?.json();
let b: impl Future = b?.json();
let (a, b) = (a.poll(), b.poll());
// return if either a or b is NotReady;
let (a, b) = (a?, b?);

También estaría contento con otras implementaciones de la idea central, "la lectura en espera una vez por cadena de expresión es probablemente lo suficientemente explícita". Porque reduce el texto estándar, pero también hace posible el encadenamiento con una sintaxis basada en prefijos que es más familiar para todos.

@yazaddaruvala Había una advertencia arriba, así que ten cuidado.

Tengo curiosidad por saber si la mayoría de la comunidad se comprometería y permitiría una "espera implícita parcial". ¿Quizás leer esperar una vez por cadena de expresión sea lo suficientemente explícito?

  1. No creo que la mayoría de la comunidad quiera algo implícito parcial. El enfoque nativo de Rust es más como "explícito todo". Incluso los lanzamientos u8 -> u32 se hacen explícitamente.
  2. No funciona bien para escenarios más complicados. ¿Qué debería hacer el compilador con Vec<Future> ?
await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

¿Debería realizarse await para cada llamada anidada? Bien, probablemente podamos hacer que await funcione para una línea de código (por lo que no se esperará mapFunc anidado), pero es muy fácil romper este comportamiento. Si await funciona para una línea de código, cualquier refactorización como "extraer valor" lo rompe.

@yazaddaruvala

¿Entiendo que tiene razón en lo que está proponiendo? El prefijo await tiene prioridad sobre ? con la posibilidad adicional de usar await en lugar de la palabra clave let para cambiar el contexto para esparcir todo impl Future <_ i = "7"> vuelve con await?

Lo que significa que estas tres declaraciones a continuación son equivalentes:

// foo.bar() -> Result<impl Future<_>, _>>
let a = await foo.bar()?;
let a = await (foo.bar()?);
await a = foo.bar()?;

Y estas dos declaraciones a continuación son equivalentes:

// foo.bar() -> impl Future<Result<_, _>>
await a = foo.bar()?;
let a = await (foo.bar())?;

No estoy seguro de que la sintaxis sea ideal, pero quizás tener una palabra clave que cambie a un "contexto implícito de espera" hace que los problemas de la palabra clave de prefijo sean menos problemáticos. Y al mismo tiempo, permite usar un control más detallado sobre dónde se usa await cuando sea necesario.

En mi opinión, para abreviar el prefijo await tiene el beneficio de similitud con muchos otros lenguajes de programación y con muchos lenguajes humanos . Si al final se decide que await solo debe ser un sufijo, espero que a veces termine usando una macro Await!() (o cualquier forma similar que no entre en conflicto con las palabras clave), cuando sienta que me apoya en La familiaridad profundamente arraigada con la estructura de las oraciones en inglés ayudará a reducir la sobrecarga mental de leer / escribir código. Hay un valor definido en las formas de sufijo para expresiones encadenadas más largas, pero mi punto de vista explícitamente no basado en hechos objetivos es que, con la complejidad humana del prefijo aguardar subsidiado por el lenguaje hablado, el beneficio de la simplicidad de tener solo una forma sí lo hace. no superan claramente la claridad potencial del código de tener ambos. Suponiendo que confía en que el programador elija el que sea más apropiado para el fragmento de código actual, al menos.

@Pzixel

No creo que la mayoría de la comunidad quiera algo implícito parcial. El enfoque nativo de Rust es más como "explícito todo".

Yo no lo veo de la misma manera. Lo veo siempre siendo un compromiso de explícito e implícito. Por ejemplo:

  • Se requieren tipos de variables en el alcance de la función

    • Los tipos de variables pueden estar implícitos dentro de un ámbito local.

    • Los contextos de clausura son implícitos

    • Dado que ya podemos razonar sobre las variables locales.

  • Se requieren de por vida para ayudar al verificador de préstamos.

    • Pero dentro de un ámbito local se pueden inferir.

    • En el nivel de función, la vida útil no necesita ser explícita, cuando todas tienen el mismo valor.

  • Seguro que hay más.

Usar await solo una vez por expresión / declaración, con todos los beneficios del encadenamiento, es un compromiso muy razonable entre explícito y reductor.

No funciona bien para escenarios más complicados. ¿Qué debería hacer el compilador con Vec??

await response = client.get("https://my_api").send()?.json()?.parse_as::<Vec<String>>()?.map(|x| client.get(x))?;

Yo diría que el compilador debería tener un error. Hay una gran diferencia de tipos que no se puede inferir. Mientras tanto, el ejemplo con Vec<impl Future> no se resuelve con ninguna de las sintaxis de este hilo.

// Given that json() returns a Vec<imple Future>,
// do I use .await on the `Vec`? That seems odd.
// Given that the client.get() returns an `impl Future`,
// do I .await inside the .map? That wont work, given Iterator methods are not `async`
client.get("https://my_api").send().await?.json()[UNCLEAR]?.parse_as::<Vec<String>>()?.map(|x| client.get(x)[UNCLEAR])?;

Yo diría que Vec<impl Future> es un mal ejemplo, o deberíamos mantener cada sintaxis propuesta con el mismo estándar. Mientras tanto, la sintaxis de "espera implícita parcial" que propuse funciona mejor que las propuestas actuales para escenarios más complicados como await (a, b) = (client.get("a")?, client.get("b")?); usando un sufijo normal o un prefijo aguardar let (a, b) = (client.get("a") await?, client.get("b") await?); resulta en operaciones de red secuenciales, donde el -La versión implícita puede ser ejecutada simultáneamente por el compilador desugardard apropiadamente (como lo he mostrado en mi publicación original).

En 2011, cuando la función async en C # todavía estaba en prueba, pregunté si await era un operador de prefijo y obtuve una respuesta del administrador de proyectos del lenguaje C #. Dado que existe una discusión sobre si Rust debería usar un operador de prefijo, pensé que podría ser valioso publicar esa respuesta aquí. De ¿ Por

Como puede imaginar, la sintaxis de las expresiones de espera fue un gran punto de discusión antes de que nos decidiéramos por lo que hay ahora :-). Sin embargo, no consideramos mucho a los operadores postfix. Hay algo acerca de postfix (cf. calificadores HP y el lenguaje de programación Forth) que hace las cosas más simples en principio y menos accesibles o legibles en la práctica. Tal vez sea la forma en que la notación matemática nos lava el cerebro cuando éramos niños ...

Definitivamente descubrimos que el operador de prefijo (con su sabor literalmente imperativo - "¡haz esto!") Era, con mucho, el más intuitivo. La mayoría de nuestros operadores unarios ya son prefijos, y await parece encajar con eso. Sí, se ahoga en expresiones complejas, pero también lo hace el orden de evaluación en general, debido al hecho de que, por ejemplo, la aplicación de la función tampoco es postfix. Primero evalúa los argumentos (que están a la derecha) y luego llama a la función (que está a la izquierda). Sí, hay una diferencia en la sintaxis de la aplicación de la función en C # que viene con paréntesis ya integrados, mientras que para la espera normalmente puede eliminarlos.

Lo que veo ampliamente (y yo mismo lo he adoptado) en espera es un estilo que usa muchas variables temporales. Tiendo a preferir

var bResult = await A().BAsync();
var dResult = await bResult.C().DAsync();
dResult.E()

o algo así. En general, evitaría tener más de un await en todas las expresiones menos las más simples (es decir, todos son argumentos para la misma función u operador; probablemente estén bien) y evitaría poner entre paréntesis las expresiones de espera, prefiriendo usar locales adicionales para ambos trabajos .

¿Tener sentido?

Mads Torgersen, administrador de proyectos en lenguaje C #


Mi experiencia personal como desarrollador de C # es que la sintaxis me impulsa a usar ese estilo de declaraciones múltiples, y que esto hace que el código asincrónico sea mucho más incómodo de leer y escribir que el equivalente sincrónico.

@yazaddaruvala

Yo no lo veo de la misma manera. Lo veo siempre como un compromiso de explícito e implícito

Todavía es explícito. Puedo vincular un buen artículo sobre el tema por cierto .

Yo diría que el compilador debería tener un error. Hay una gran diferencia de tipos que no se puede inferir. Mientras tanto, el ejemplo con Vecno se resuelve con ninguna de las sintaxis de este hilo.

¿Por qué debería ser un error? Estoy feliz de tener Vec<impl future> , que luego podría unir. Propones limitaciones adicionales sin ningún motivo.

Yo diría que el Veces un mal ejemplo, o deberíamos mantener todas las sintaxis propuestas con el mismo estándar.

Deberíamos examinar cada caso. No podemos simplemente ignorar los casos extremos porque "meh, ya sabes, nadie escribe esto de todos modos". El diseño del lenguaje se trata principalmente de casos extremos.

@chescock Estoy de acuerdo contigo, pero dicho antes, Rust tiene una gran diferencia con C # aquí: en C # nunca escribes (await FooAsync()).Bar() . Pero en Rust lo haces. Y estoy hablando de ? . En C # tiene una excepción implícita que se propaga a través de llamadas asíncronas. Pero en rust tienes que ser explícito y escribir ? en el resultado de la función después de que se esperaba.

Por supuesto, puede pedir a los usuarios que escriban

let foo = await bar();
let bar = await foo?.baz();
let bar = bar?;

pero es mucho más extraño que

let foo = await? bar();
let bar = await? foo.baz();

Esto se ve mucho mejor, pero requiere la introducción de una nueva combinación de await y ? doint (await expr)? . Además, si tenemos otros operadores, podríamos escribir await?&@# y hacer que todas las combinaciones funcionen por completo ... Parece un poco complicado.

Pero luego podemos colocar await como un sufijo y, naturalmente, encajará en el idioma actual:

let foo = bar() await?;
let bar = foo.baz() await?;

ahora await? son dos tokens separados, pero funcionan en conjunto. Puede tener await?&@# y no se molestará en hackearlo en el compilador.

Por supuesto, parece un poco más extraño que el modo de prefijo, pero dicho esto, se trata de la diferencia Rust / C #. Podemos usar la experiencia de C # para asegurarnos de que necesitamos una sintaxis de espera explícita que probablemente tenga una palabra clave separada, pero no debemos seguir ciegamente el mismo camino e ignorar las diferencias de Rust / C #.

Yo mismo fui partidario del prefijo await durante mucho tiempo e incluso invité a desarrolladores de lenguaje C # al tema (así que tenemos información más reciente que en 2011 😄), pero ahora creo que el sufijo await palabra clave es el mejor enfoque.

No olvides que hay una noche, para que podamos volver a trabajar más tarde si encontramos una mejor manera.

Se me ha ocurrido que es posible que podamos obtener lo mejor de ambos mundos con una combinación de:

  • un prefijo async palabra clave
  • una característica de macro de sufijo general

Lo que en conjunto permitiría implementar una macro postfix .await!() sin la magia del compilador.

En este momento, una macro postfix .await!() requeriría magia de compilador. Sin embargo, si se estabilizara una variante de prefijo de la palabra clave await , entonces esto ya no sería cierto: la macro de sufijo .await!() podría implementarse trivialmente como una macro que antepusiera su argumento (una expresión) con la palabra clave await y envuelto todo entre corchetes.

Ventajas de este enfoque:

  • El prefijo await palabra clave está disponible y accesible para usuarios familiarizados con esa construcción en otros idiomas.
  • La palabra clave prefijo asíncrono se puede usar cuando sea deseable hacer que la naturaleza asíncrona de una expresión "destaque".
  • Habría una variante de sufijo .await!() , que podría usarse en situaciones de encadenamiento, o en cualquier otra situación en la que la sintaxis del prefijo sea incómoda.
  • No habría necesidad de magia de compilador (potencialmente inesperada) para la variante postfix (ya que de lo contrario sería un problema para las opciones de campo, método o macro de postfix).
  • Las macros de postfix también estarían disponibles para otras situaciones (como .or_else!(continue) ), que casualmente necesitan la sintaxis de macros de postfix por razones similares (de lo contrario, requieren que la expresión anterior se ajuste de una manera incómoda e ilegible).
  • La palabra clave prefijo await podría estabilizarse relativamente rápido (permitiendo que el ecosistema se desarrolle) sin que tengamos que esperar a la implementación de macros postfix. Pero a medio y largo plazo todavía dejaríamos abierta la posibilidad de una sintaxis de sufijo para la espera.

@nicoburns , @ H2CO3 ,

No deberíamos implementar operadores de flujo de control como await!() y .or_else!(continue) como macros porque desde la perspectiva del usuario no hay una razón significativa para hacerlo. De todos modos, en ambas formas, los usuarios tendrían exactamente las mismas funciones y deberían aprender las dos, sin embargo, si estas funciones se implementaran como macros, además, los usuarios se preocuparían de por qué se implementan exactamente de esta manera. Es imposible no darse cuenta de eso, porque habría una diferencia extraña y artificial entre los operadores regulares de primera clase y los operadores regulares implementados como macros (ya que por sí mismos las macros son de primera clase, pero las cosas implementadas por ellos no lo son).

Es exactamente la misma respuesta que para . adicionales antes de await : no lo necesitamos. Las macros no pueden imitar el flujo de control de primera clase: tienen una forma diferente, tienen diferentes casos de uso, tienen diferentes resaltados, funcionan de manera diferente y se perciben de manera completamente diferente.

Para las funciones .await!() y .or_else!(continue) existen soluciones adecuadas y ergonómicas con hermosas sintaxis: await palabra clave y none -operador coalescente. Deberíamos preferir implementarlos en lugar de algo genérico, de uso poco frecuente y feo como macros postfix.

No hay razón para usar macros, ya que no hay algo complicado como format!() , no hay algo que rara vez se usa como include_bytes!() , no hay un DSL personalizado, no se elimina la duplicación en el código de usuario, hay no se requiere sintaxis similar a vararg, no hay necesidad de que algo se vea diferente, y no podemos usarlo de esta manera solo porque es posible.

No deberíamos implementar operadores de flujo de control como macros porque desde la perspectiva del usuario no hay ninguna razón significativa para hacerlo.

try!() se implementó como una macro. Había una razón perfectamente válida para ello.

Ya he descrito cuál sería la razón para hacer de await una macro: no hay necesidad de un nuevo elemento de lenguaje si una función existente también puede lograr el objetivo.

no hay algo complicado como format!()

Ruego diferir: format!() no se trata de complicaciones, se trata de verificar en tiempo de compilación. Si no fuera por la verificación en tiempo de compilación, podría ser una función.

Por cierto, no sugerí que debería ser una macro postfix. (Creo que no debería.)

Para ambas características existe una solución adecuada y ergonómica con hermosas sintaxis

No deberíamos darle a cada llamada de función, expresión de control de flujo o característica de conveniencia menor su propia sintaxis muy especial. Eso solo da como resultado un lenguaje inflado, lleno de sintaxis sin ninguna razón, y es exactamente lo opuesto a "hermoso".

(He dicho lo que pude; no voy a responder más a este aspecto de la pregunta).

@nicoburns

Se me ha ocurrido que es posible que podamos obtener lo mejor de ambos mundos con una combinación de:

  • un prefijo await palabra clave
  • una característica de macro de sufijo general

Esto implica hacer de await una palabra clave contextual en lugar de la palabra clave real que es actualmente. No creo que debamos hacer eso.

Lo que en conjunto permitiría implementar una macro postfix .await!() sin la magia del compilador.

Esta es una solución más compleja en cuanto a implementación que foo await o foo.await (especialmente el último). Todavía hay "magia", en la misma medida; acaba de hacer una maniobra contable .

Ventajas de este enfoque:

  • El prefijo await palabra clave está disponible y es accesible para usuarios familiarizados con esa construcción en otros idiomas.

Si vamos a agregar .await!() más tarde, solo les estamos dando a los usuarios más para aprender (tanto await foo como foo.await!() ) y ahora los usuarios preguntarán "cuál debería usar cuando..". Esto parece consumir más del presupuesto de complejidad que foo await o foo.await como lo harían las soluciones individuales.

  • Las macros de postfix también estarían disponibles para otras situaciones (como .or_else!(continue) ), que coincidentemente necesitan la sintaxis de macros de postfix por razones similares (de lo contrario, requieren que la expresión anterior se ajuste de una manera incómoda e ilegible).

Creo que las macros postfix tienen valor y deben agregarse al lenguaje. Sin embargo, eso no significa que deban usarse por await ing; Como mencionas, hay .or_else!(continue) y muchos otros lugares donde las macros postfix serían útiles.

  • La palabra clave prefijo await podría estabilizarse relativamente rápido (permitiendo que el ecosistema se desarrolle) sin que tengamos que esperar a la implementación de macros postfix. Pero a medio y largo plazo todavía dejaríamos abierta la posibilidad de una sintaxis de sufijo para la espera.

No veo ningún valor en "dejar posibilidades abiertas"; el valor de postfix para composición, experiencia IDE, etc. se conoce hoy. No estoy interesado en "estabilizar await foo hoy y espero que podamos llegar a un consenso sobre foo.await!() mañana".


@ H2CO3

try!() se implementó como una macro. Había una razón perfectamente válida para ello.

try!(..) quedó obsoleto, lo que significa que lo consideramos inadecuado para el idioma, en particular porque no era postfix. Usarlo como argumento, aparte de lo que no deberíamos hacer, parece extraño. Además, try!(..) se define sin ningún soporte del compilador .

Por cierto, no sugerí que debería ser una macro postfix. (Creo que no debería.)

await no es "todos y cada uno ..." - en particular, no es una característica de conveniencia menor ; más bien, es una característica importante del lenguaje que se está agregando.

@phaylon

Además, con las discusiones recientes sobre la complicación de la gramática y los problemas que causará para las herramientas, puede que no sea una buena idea confiar en que los IDE / editores hagan las cosas bien.

La sintaxis foo.await está destinada a reducir los problemas de herramientas, ya que cualquier editor con cualquier apariencia de buen soporte para Rust ya entenderá la forma .ident . Lo que el editor debe hacer es agregar await a la lista de palabras clave. Además, los buenos IDE ya tienen finalización de código basada en . , por lo que parece más sencillo extender RLS (o equivalentes ...) para proporcionar await como primera sugerencia cuando el usuario escribe . después de my_future .

En cuanto a complicar la gramática, .await es en realidad menos probable que complique la gramática dado que apoyar .await en la sintaxis es esencialmente analizar .$ident y luego no generar errores en ident == keywords::Await.name() .

La sintaxis foo.await está destinada a reducir los problemas de las herramientas, ya que cualquier editor con una apariencia de buen soporte para Rust ya comprenderá la forma .ident. Lo que el editor debe hacer es agregar await a la lista de palabras clave. Además, los buenos IDE ya tienen finalización de código basada en. - por lo que parece más sencillo extender RLS (o equivalentes ...) para proporcionar await como primera sugerencia cuando el usuario escribe. después de my_future.

Simplemente encuentro esto en desacuerdo con la discusión gramatical. Y el problema no es solo ahora, sino dentro de 5, 10, 20 años, después de un par de ediciones. Pero como espero que haya notado, también menciono que incluso si una palabra clave está resaltada, los puntos de espera pueden pasar desapercibidos si hay otras palabras clave involucradas.

En cuanto a complicar la gramática, .await es en realidad menos probable que complique la gramática dado que admitir .await en la sintaxis es esencialmente analizar. $ Ident y luego no generar errores en ident == palabras clave :: Await.name ().

Creo que el honor pertenece a await!(future) , ya que la gramática ya lo apoya completamente.

@Centril try! eventualmente se volvió redundante porque el operador ? puede hacer estrictamente más. No es "inadecuado para el idioma". Sin embargo, puede que no te guste , lo cual acepto. Pero para mí es una de las mejores cosas que inventó Rust, y fue uno de los puntos de venta. Y sé que está implementado sin el soporte del compilador, pero no veo cómo eso es relevante cuando se habla de si realiza o no el flujo de control. Lo hace, independientemente.

esperar no es "todos y cada uno ..."

Pero otros mencionados aquí (por ejemplo, or_else ) sí lo son, y mi punto todavía se aplica a las características principales de todos modos. Agregar sintaxis solo por agregar sintaxis no es una ventaja, por lo que siempre que haya algo que ya funcione en un caso más general, debería preferirse a inventar una nueva notación. (Sé que el otro argumento en contra de las macros es que "no son sufijos". Simplemente no creo que los beneficios de que await sea su propio operador posfijo sean lo suficientemente altos como para justificar el costo. Hemos sobrevivido a las llamadas de funciones anidadas. estará igualmente bien después de haber escrito un par de macros anidadas).

El miércoles 23 de enero de 2019 a las 09:59:36 p.m. + 0000, Mazdak Farrokhzad escribió:

  • Las macros de postfix también estarían disponibles para otras situaciones (como .or_else!(continue) ), que casualmente necesitan la sintaxis de macros de postfix por razones similares (de lo contrario, requieren que la expresión anterior se ajuste de una manera incómoda e ilegible).

Creo que las macros postfix tienen valor y deben agregarse al lenguaje. Sin embargo, eso no significa que deban usarse para await ing; Como mencionas, hay .or_else!(continue) y muchos otros lugares donde las macros postfix serían útiles.

La razón principal para usar .await!() es que parecerse a una macro
está claro que puede afectar el flujo de control.

.await parece un acceso de campo, .await() parece una función
llamada, y ni los accesos a campos ni las llamadas a funciones pueden afectar el control
fluir. .await!() parece una macro y las macros pueden afectar el control
fluir.

@joshtriplett I no afecta el flujo de control en el sentido estándar. Este es el hecho básico para justificar que el verificador de préstamos debería trabajar en futuros como se define (y pin es el razonamiento de cómo ). Desde el punto de vista de la ejecución de la función local, la ejecución de await es como la mayoría de las otras llamadas a funciones. Continúa donde lo dejó y tiene un valor de retorno en la pila.

Simplemente encuentro esto en desacuerdo con la discusión gramatical. Y el problema no es solo ahora, sino dentro de 5, 10, 20 años, después de un par de ediciones.

No tengo idea de lo que quieres decir con esto.

Creo que el honor pertenece a await!(future) , ya que la gramática ya lo apoya completamente.

Entiendo, pero en este punto, debido a los numerosos otros inconvenientes que tiene esta sintaxis, creo que está efectivamente descartado.

@Centril try! eventualmente se volvió redundante porque el operador ? puede hacer estrictamente más. No es "inadecuado para el idioma". Sin embargo, puede que no te guste, lo cual acepto.

Está explícitamente desaprobado y, además, es un error grave escribir try!(expr) en Rust 2018. Decidimos colectivamente hacerlo así y, por lo tanto, se consideró inadecuado.

La razón principal para usar .await!() es que el aspecto de una macro deja en claro que puede afectar el flujo de control.

.await parece un acceso de campo, .await() parece una llamada de función y ni los accesos de campo ni las llamadas de función pueden afectar el flujo de control.

Creo que la distinción es algo que los usuarios aprenderán con relativa facilidad, especialmente porque .await generalmente será seguido por ? (que es el flujo de control local de funciones). En cuanto a las llamadas a funciones, y como se mencionó anteriormente, creo que es justo decir que los métodos iteradores son una forma de flujo de control. Además, el hecho de que una macro pueda afectar el flujo de control no significa que lo hará. Muchas macros no lo hacen (por ejemplo, dbg! , format! , ...). El entendimiento de que .await!() o .await afectará el flujo de control (aunque en un sentido mucho más débil que ? , según la nota de @HeroicKatora ) fluye de la palabra await sí mismo.

@Centril

Esto implica hacer esperar una palabra clave contextual en lugar de la palabra clave real que es actualmente. No creo que debamos hacer eso.

Eek. Eso es un poco doloroso. Quizás la macro podría tener un nombre diferente como .wait!() o .awaited!() (este último es bastante bueno ya que deja en claro que se aplica a la expresión anterior).

"Lo que en conjunto permitiría implementar una macro postfix .await! () Sin la magia del compilador".
Esta es una solución más compleja en cuanto a implementación que foo await o foo.await (especialmente el último). Todavía hay "magia", en la misma medida; acaba de hacer una maniobra contable.

Además, try! (..) se define sin ningún soporte del compilador.

Y si tuviéramos un prefijo await palabra clave y macros postfix, entonces .await!() (quizás con un nombre diferente) también podría implementarse sin soporte de compilador, ¿verdad? Por supuesto, la palabra clave await sí misma aún implicaría una cantidad significativa de magia del compilador, pero eso simplemente se aplicaría al resultado de una macro, y no es diferente de la relación de try!() con la return palabra clave.

Si vamos a agregar .await! () Más adelante, solo estamos dando a los usuarios más para aprender (tanto await foo como foo.await! ()) Y ahora los usuarios preguntarán "¿cuál debo usar cuando ...". Esto parece consumir más del presupuesto de complejidad de lo que foo await o foo.await como lo harían las soluciones individuales.

Creo que la complejidad que esto agrega al usuario es mínima (la complejidad de la implementación puede ser otro asunto, pero si queremos macros postfix de todos modos ...). Ambas formas se leen intuitivamente, de modo que al leer cualquiera de las dos, debería ser obvio lo que está sucediendo. En cuanto a qué elegir al escribir código, los documentos podrían simplemente decir algo como:

"Hay dos formas de esperar un futuro en Rust: await foo.bar(); y foo.bar().await!() . Cuál usar es una preferencia estilística y no hace ninguna diferencia en el flujo de ejecución"

No veo ningún valor en "dejar posibilidades abiertas"; el valor de postfix para composición, experiencia IDE, etc. se conoce hoy.

Eso es verdad. Supongo que en mi opinión se trata más de asegurarnos de que nuestra implementación no cierre la posibilidad de tener un lenguaje más unificado en el futuro.

No tengo idea de lo que quieres decir con esto.

No importa mucho. Pero en general también lo prefiero cuando la sintaxis es obvia sin un buen resaltado. Entonces, las cosas se destacan en git diff y otros contextos similares.

En teoría sí, y para programas triviales sí, pero en realidad programadores
necesitan saber dónde están sus puntos de suspensión.

Para un ejemplo simple, puede sostener una RefCell a través de un punto de suspensión y
el comportamiento de su programa será diferente que si el RefCell fuera
liberado antes del punto de suspensión. En programas grandes habrá
innumerables sutilezas como ésta, donde el hecho de que la función actual
está suspendiendo es información importante.

El miércoles 23 de enero de 2019 a las 2:21 p. M. HeroicKatora [email protected]
escribió:

@joshtriplett https://github.com/joshtriplett No afecta el control
Fluir en el sentido estándar. Este es el hecho básico para justificar que la
El comprobador de préstamos debería funcionar en futuros según lo definido. Desde el punto de vista del
ejecución de función local, la ejecución de await es como cualquier otra llamada de función.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456989262 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABGmeqcr2g4olxQhH9Fb9r6g3XRaFUu2ks5vGOBkgaJpZM4aBlba
.

El miércoles 23 de enero de 2019 a las 02:26:07 PM -0800, Mazdak Farrokhzad escribió:

La razón principal para usar .await!() es que el aspecto de una macro deja en claro que puede afectar el flujo de control.

.await parece un acceso a un campo, .await() parece una llamada a una función y ni los accesos a los campos ni las llamadas a las funciones pueden afectar el flujo de control.

Creo que la distinción es algo que los usuarios aprenderán con relativa facilidad.

¿Por qué deberían tener que hacerlo?

Creo que la distinción es algo que los usuarios aprenderán con relativa facilidad, especialmente porque .await suele ir seguido de? (que es el flujo de control local de función).

A menudo sí, pero no siempre, y la sintaxis definitivamente debería funcionar para los casos en los que este no es el caso.

En cuanto a las llamadas a funciones, y como se mencionó anteriormente, creo que es justo decir que los métodos iteradores son una forma de flujo de control.

No de la misma manera que return o ? (o incluso break ). Sería muy sorprendente si una llamada de función pudiera regresar dos niveles más allá de la función que la llamó (una de las razones por las que Rust no tiene excepciones es precisamente porque esto es sorprendente), o romper un bucle en la llamada función.

Además, el hecho de que una macro pueda afectar el flujo de control no significa que lo hará. Muchas macros no lo hacen (por ejemplo, dbg !, ¡formato !, ...). La comprensión de que .await! () O .await afectará el flujo de control (aunque en un sentido mucho más débil que?, Según la nota de

No me gusta esto en absoluto. Efectuar el control del flujo es un efecto secundario realmente importante . Debería tener una forma sintáctica diferente a las construcciones que normalmente no pueden hacer esto. Las construcciones que pueden hacer esto en Rust son: palabras clave ( return , break , continue ), operadores ( ? ) y macros (evaluando a uno de las otras formas). ¿Por qué enturbiar las aguas agregando una excepción?

@ejmahler No veo cómo mantener una RefCell es diferente para la comparación con una llamada de función normal. También podría ser que la función interna quiera adquirir el mismo RefCell. Pero nadie parece tener un problema importante si no capta de inmediato el gráfico de llamadas potenciales completo. Del mismo modo, los problemas con el agotamiento de la pila no son motivo de especial preocupación por el hecho de que deba anotar el espacio de pila proporcionado. ¡Aquí la comparación sería favorable para las corrutinas! ¿Deberíamos hacer que las llamadas a funciones normales sean más visibles si no están alineadas? ¿Necesitamos llamadas de cola explícitas para controlar este problema cada vez más difícil con las bibliotecas?

La respuesta es, en mi humilde opinión, que el mayor riesgo al usar RefCell y await es la falta de familiaridad. Cuando los otros problemas anteriores se pueden abstraer del hardware, creo que nosotros, como programadores, también podemos aprender a no aferrarnos a RefCell, etc., en todos los puntos de rendimiento, excepto cuando se revise.

El miércoles 23 de enero de 2019 a las 10:30:10 p.m. + 0000, Elliott Mahler escribió:

En teoría sí, y para programas triviales sí, pero en realidad programadores
necesitan saber dónde están sus puntos de suspensión.

: +1:

@HeroicoKatora

Una diferencia fundamental es la cantidad de código que debe inspeccionar, cuántos
posibilidades que tienes que tener en cuenta. Los programadores ya están acostumbrados a
asegurándose de que nada de lo que llamen también use una RefCell que hayan verificado
afuera. Pero con await, en lugar de inspeccionar una sola pila de llamadas, no tenemos
control sobre lo que se ejecuta durante la espera, literalmente, cualquier línea del
puede que se ejecute el programa. Mi intuición me dice que el programador promedio es
mucho menos equipados para hacer frente a esta explosión de posibilidades, ya que
tengo que lidiar con ello con mucha menos frecuencia.

Para otro ejemplo de puntos de suspensión que son información crítica, en el juego
programación, normalmente escribimos corrutinas que están destinadas a reanudarse
una vez por cuadro, y cambiamos el estado del juego en cada currículum. Si pasamos por alto un
punto de suspensión en una de estas corrutinas, es posible que dejemos el juego en un
estado para varios marcos.

Todos estos pueden tratarse diciendo "sea más inteligente y haga menos
errores ”por supuesto, pero ese enfoque históricamente no funciona.

El miércoles 23 de enero de 2019 a las 2:44 p.m. Josh Triplett [email protected]
escribió:

El miércoles 23 de enero de 2019 a las 10:30:10 p.m. + 0000, Elliott Mahler escribió:

En teoría sí, y para programas triviales sí, pero en realidad programadores
necesitan saber dónde están sus puntos de suspensión.

: +1:

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/57640#issuecomment-456995913 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABGmep05nhZizdE5xil4FsnpONJCxqn-ks5vGOXhgaJpZM4aBlba
.

Para otro ejemplo de puntos de suspensión que son información crítica, en la programación de juegos, normalmente escribimos corrutinas que están destinadas a reanudarse una vez por cuadro, y cambiamos el estado del juego en cada currículum. Si pasamos por alto un punto de suspensión en una de estas corrutinas, es posible que dejemos el juego en un estado interrumpido durante varios fotogramas.

Si suelta la clave en un SlotMap, pierde memoria. El lenguaje en sí no te ayuda con esto de forma inherente. Lo que quiero decir es que sus ejemplos de cómo await es vagamente no lo suficientemente visible (siempre que sea una palabra clave, su ocurrencia será única) no parecen generalizarse a problemas que no han ocurrido para otras características. Creo que debería evaluar aguardar menos sobre qué característica le brinda el lenguaje de inmediato y más sobre cómo le permite expresar sus propias características. Evalúe sus herramientas y luego aplíquelas. Sugerir una solución para el estado del juego, y cómo se aborda esto lo suficientemente bien: escriba un envoltorio que afirme al regresar que el estado del juego de hecho marcó la cantidad requerida, async le permite tomar prestado su propio estado para esa causa. Prohibir otros espera en ese código compartimentado.

La explosión de estado en RefCell no proviene de que usted no verifique que no se usa en otro lugar, sino de otros puntos de código sin saber que confió en ese invariante y lo agregó en silencio. Estos se abordan de la misma forma, documentación, comentarios, envoltura correcta.

Mantener RefCell no es muy diferente a agrupar Mutex al bloquear io. Puede llegar a un punto muerto, que es peor y más difícil de depurar que el pánico. Y, sin embargo, no anotamos operaciones de bloqueo con la palabra clave explícita block . :).

Compartir RefCell entre funciones asíncronas limitará en gran medida su usabilidad general ( !Send ), por lo que no creo que sea común. Y RefCell generalmente se deben evitar y, cuando se usan, se deben pedir prestado durante el menor tiempo posible.

Por lo tanto, no creo que acumular RefCell accidentalmente sobre un punto de rendimiento sea un gran problema.

Si pasamos por alto un punto de suspensión en una de estas corrutinas, es posible que dejemos el juego en un estado interrumpido durante varios fotogramas.

Las corrutinas y las funciones asíncronas son algo diferente. No estamos tratando de hacer implícito yield .

Para cualquiera que esté a favor de la sintaxis de macros en lugar de la sintaxis de palabras clave aquí: Describa de qué se trata await que significa que debería ser una macro y en qué se diferencia de, digamos, while , que _podría_ tener solo ha sido una macro while! ( incluso usando solo macro_rules! ; sin carcasa especial en absoluto).

Editar: Esto no es retórico. Estoy genuinamente interesado en tal cosa, de la misma manera que pensé que quería correr hasta la primera espera (como C #), pero el RFC me convenció de lo contrario.

Para cualquiera que esté a favor de la sintaxis de macros en lugar de la sintaxis de palabras clave aquí: Por favor, describa de qué se trata await que significa que debería ser una macro y en qué se diferencia de, digamos, while, ¡que podría haber sido un tiempo! macro (¡incluso usando solo macro_rules !; sin ninguna funda especial).

En general, estoy de acuerdo con este razonamiento y quiero usar una palabra clave para la sintaxis de prefijo.

Sin embargo, como se describe anteriormente (https://github.com/rust-lang/rust/issues/57640#issuecomment-456990831), estoy a favor de tener también una macro de sufijo para la sintaxis de sufijo (quizás .awaited!() para evitar conflictos de nombres con la palabra clave del prefijo). Mi razonamiento es en parte que simplifica las cosas tener una palabra clave que solo se puede usar de una manera, y guarda más variaciones para macros, pero principalmente es que, por lo que puedo ver, nadie ha podido encontrar un sufijo sintaxis de palabras clave que consideraría aceptable:

  • foo.bar().await y foo.bar().await() técnicamente serían palabras clave, pero se ven como un acceso a un campo o una llamada a un método, y no creo que una construcción de modificación del flujo de control deba ocultarse así.
  • foo.bar() await es confuso, especialmente con más encadenamientos como foo.bar() await.qux() . Nunca he visto una palabra clave utilizada como un sufijo como ese (estoy seguro de que existen, pero en realidad no puedo pensar en un solo ejemplo en ningún idioma que conozca de palabras clave que funcionen así). Y creo que se lee de manera muy confusa, como si la espera se aplicara al qux() no al foo.bar() .
  • foo.bar()@await o algún otro signo de puntuación podría funcionar. Pero no es particularmente agradable y se siente bastante ad-hoc. De hecho, no creo que haya problemas importantes con esta sintaxis (a diferencia de las opciones anteriores). Siento que una vez que comenzamos a agregar sigilos, el equilibrio comienza a inclinarse hacia el abandono de la sintaxis personalizada y hacia que sea una macro.

Reiteraré una idea anterior una vez más, aunque modificada con nuevos pensamientos y consideraciones, luego intentaré permanecer en silencio.

await como palabra clave solo es válida dentro de una función async todos modos, por lo que deberíamos permitir el identificador await otro lugar. Si una variable se llama await en un ámbito externo, no se puede acceder a ella desde un bloque async menos que se utilice como un identificador sin formato. O, por ejemplo, la macro await! .

Además, adaptar la palabra clave de esa manera permitiría mi punto principal: deberíamos permitir la mezcla y combinación de generadores y funciones asíncronas, así:

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

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

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

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

    return Ok(item);
}

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

   let item = await? some_op();

   return Ok(item);
}

Creo que esto es lo suficientemente transparente para los usuarios extremadamente avanzados que quieren hacer cosas divertidas, pero permite que los usuarios nuevos y moderados usen solo lo necesario. 2 y 3, si se usan sin declaraciones yield , son efectivamente idénticas.

También creo que el prefijo await? es una excelente forma abreviada de agregar ? al resultado de la operación asíncrona, pero eso no es estrictamente necesario.

Si las macros de postfix se convierten en algo oficial (que espero que lo hagan), tanto await!(...) como .await!() podrían existir juntos, lo que permite tres formas equivalentes de hacer esperas, para casos de uso y estilos específicos. . No creo que esto agregue ninguna sobrecarga cognitiva, pero permitiría una mayor flexibilidad.

Idealmente, async fn y #[async] fn* podrían incluso compartir el código de implementación para transformar el generador subyacente en una máquina de estado asíncrona.

Estos problemas han dejado en claro que no existe un estilo realmente preferido, por lo que lo mejor que puedo esperar es proporcionar niveles de complejidad limpios, flexibles, legibles y fáciles de abordar. Creo que el esquema anterior es un buen compromiso para el futuro.

await como palabra clave solo es válida dentro de una función async todos modos, por lo que debemos permitir que el identificador espere en otro lugar.

No sé si eso es práctico en Rust, dadas las macros higiénicas. Si llamo a foo!(x.await) o foo!(await { x }) cuando quiere un expr , creo que debería ser inequívoco que se desea la palabra clave await , no la await campo o la expresión literal de estructura await con taquigrafía de inicio de campo, incluso en un método síncrono.

permitiendo tres formas equivalentes de hacer espera

Por favor no. (Al menos en core . Obviamente, las personas pueden crear macros en su propio código. Si lo desean)

En este momento, una macro postfix .await! () Requeriría magia del compilador. Sin embargo, si se estabilizara una variante de prefijo de la palabra clave await, entonces esto ya no sería cierto: la macro postfix .await! () Podría implementarse trivialmente como una macro que antepusiera su argumento (una expresión) con la palabra clave await y se envolviera todo entre paréntesis.

Notaré que esto es igualmente cierto, ¡pero más fácil! - en la otra dirección: si estabilizamos la sintaxis de la palabra clave .await , la gente ya puede hacer una macro de prefijo awaited!() si no les gusta lo suficiente el sufijo.

No me gusta la idea de agregar múltiples variantes permitidas (como prefijo y sufijo) pero por una razón ligeramente diferente a la que los usuarios preguntarán. Trabajo en una empresa bastante grande y las peleas por el estilo del código son reales. Me gustaría tener solo una forma obvia y correcta para usar. Después de todo, el Zen de Python podría tener razón con respecto a este punto.

No me gusta la idea de agregar múltiples variantes permitidas (como prefijo y sufijo) pero por una razón ligeramente diferente a la que los usuarios preguntarán. Trabajo en una empresa bastante grande y las peleas por el estilo del código son reales. Me gustaría tener solo una forma obvia y correcta para usar. Después de todo, el Zen de Python podría tener razón con respecto a este punto.

Yo sé lo que quieres decir. Sin embargo, no hay forma de evitar que los programadores definan sus propias macros haciendo cosas como await!() . Las estructuras simulares siempre serán posibles. Así que de todas formas habrá diferentes variantes.

Bueno, no await!(...) al menos ya que eso sería un error; pero si un usuario define macro_rules! wait { ($e:expr) => { e.await }; } y luego lo usa como wait!(expr) entonces se verá claramente unidiomático y probablemente pasará de moda rápidamente. Eso disminuye significativamente la probabilidad de variación en el ecosistema y permite a los usuarios aprender menos estilos. Por lo tanto, creo que el punto de @yasammez es adecuado.

@Centril Si alguien quiere hacer cosas malas, difícilmente se le puede detener. ¿Qué pasa con _await!() o awai!() ?

o, cuando el identificador Unicode está habilitado, algo como àwait!() .

...

@earthengine El objetivo es establecer normas comunitarias (similar a lo que hacemos con lints de estilo y rustfmt ), no para evitar que varias personas hagan cosas raras a propósito. Aquí estamos tratando con probabilidades , no con garantías absolutas de no ver _await!() .

Resumamos y revisemos los argumentos para cada sintaxis de sufijo:

  • __ expr await (palabra clave postfix) __: La sintaxis de la palabra clave postfix usa la palabra clave await que ya hemos reservado. Await es una transformación mágica, y el uso de una palabra clave ayuda a que se destaque de forma adecuada. No parece un acceso de campo, un método o una llamada de macro, y no tendremos que explicar esto como una excepción. Encaja bien con las reglas de análisis actuales y con el operador ? . Podría decirse que el espacio en el encadenamiento de métodos no es peor que con la adscripción de tipo generalizada , una RFC que parece probable que se acepte de alguna forma. En el lado negativo, los IDE pueden necesitar hacer más magia para proporcionar un autocompletado por await . Las personas pueden encontrar demasiado molesto el espacio en el encadenamiento de métodos (aunque @withoutboats ha argumentado que eso podría ser una ventaja), particularmente si el código no está formateado de manera que await termine cada línea. Utiliza una palabra clave que quizás podríamos evitar usar si adoptamos otro enfoque.

  • __ expr.await (campo postfijo) __: La sintaxis del campo postfijo aprovecha el "poder del punto" : se ve y se siente natural en el encadenamiento y permite que los IDE autocompleten await sin realizar otras operaciones (como como eliminar automáticamente el punto ). Es tan conciso como la palabra clave postfix. El punto delante de await puede facilitar su búsqueda con grep. En el lado negativo, parece un acceso de campo. Como un acceso de campo aparente, no hay una señal visual de que "esto hace algo".

  • __ expr.await() (método postfix) __: La sintaxis del método postfix es similar al campo postfix. Por el lado positivo, la sintaxis de llamada () indica al lector, "esto hace algo". Visto localmente, casi tiene sentido de la misma manera que una llamada a un método de bloqueo produciría la ejecución en un programa multiproceso. En el lado negativo, es un poco más largo y ruidoso, y disfrazar el comportamiento mágico de await como método puede resultar confuso. Podríamos decir que await es un método en el mismo sentido limitado que call/cc en Scheme es una función. Como método, deberíamos considerar si Future::await(expr) debería funcionar de forma coherente con UFCS .

  • __ expr.await!() (macro de sufijo) __: La sintaxis de macro de sufijo aprovecha de manera similar el "poder del punto". La macro ! bang indica "esto podría hacer algo mágico". En el lado negativo, esto es aún más ruidoso, y aunque las macros hacen magia, normalmente no hacen magia al código circundante como lo hace await . También en el lado negativo, asumiendo que estandarizamos una sintaxis de macro de postfix de propósito general , puede haber problemas al continuar tratando await como una palabra clave.

  • __ expr@ , expr# , expr~ y otros símbolos de un solo carácter__: El uso de un solo carácter como lo hacemos con ? maximiza la concisión y quizás crea una sintaxis de sufijo parece más natural. Al igual que con ? , podemos encontrar que apreciamos esta concisión si await comienza a invadir nuestro código. En el lado negativo, hasta y a menos que el dolor de tener nuestro código lleno de await convierta en un problema, es difícil ver que se forme un consenso en torno a las compensaciones inherentes a la adopción de tal sintaxis.

¡Quería tomar una publicación para agradecer a @traviscross por estas publicaciones resumidas! Han sido consistentemente bien redactados y bien reticulados. Es muy apreciado. :corazón:

Tengo la idea de que al agregar "operadores de tubería" como F #, los usuarios pueden usar prefijo o sufijo (con una diferencia de sintaxis explícita).

// use `|>` for instance, Rust can choose other sigils if there are conflicts with current syntax
await expr
expr |> await

// and we can use this operator on normal function calls too
f(g(h(x))) 
x |> h |> g |> f
// this is more convenient than "postfix macro"
x.h!().g!().f!()

@traviscross Excelente resumen. También ha habido una discusión sobre la combinación de sigilo y palabra clave, por ejemplo, fut@await , así que agregaré esto aquí para las personas que vienen a este hilo.

He enumerado los pros y los contras de esta sintaxis aquí . @earthengine dice que son posibles otros sellos distintos de @ , como ~ . @BenoitZugmeyer favorece a @await , y pregunta si las macros postfix ala expr!await serían una buena idea. @dpc argumenta que @await es demasiado ad-hoc y no se integra bien con lo que ya tenemos, además, que Rust ya tiene mucho sigilo; @cenwangumass está de acuerdo en que es demasiado ad-hoc. @newpavlov dice que la parte await siente redundante, especialmente si no agregamos otras palabras clave similares en el futuro. @nicoburns dice que la sintaxis podría funcionar y que no hay muchos problemas con ella, pero que es una solución demasiado ad-hoc.

@traviscross gran resumen!

Mis 0.02 $ en el orden de peor a mejor en mi opinión:

  • 3 es definitivamente un no-go porque ni siquiera es una llamada a un método.
  • 2 no es un campo, es muy confuso, especialmente para los recién llegados. tener await en la lista de finalización no es realmente útil. Después de escribir toneladas de ellos, simplemente escríbalos como fn o extern . La finalización adicional es incluso peor que nada porque, en lugar de métodos / campos útiles, una palabra clave me sugerirá.
  • 4 Macro es algo que encaja aquí, pero no me sienta bien. Me refiero a la asimetría con async como palabra clave, como mencioné anteriormente
  • 5 Sigil puede ser demasiado conciso y más difícil de detectar, pero es una entidad separada y puede ser tratado así. No se parece a ninguna otra cosa, por lo que no genera confusión para los usuarios.
  • 1 En mi humilde opinión, el mejor enfoque, es solo un sigilo tan fácil de detectar y que ya es una palabra clave reservada. La separación del espacio, como se mencionó anteriormente, es una ventaja, no un defecto. Está bien existir cuando no se realiza el formateo, pero con rustfmt es aún menos significativo.

Como alguien que ha estado esperando ansiosamente que llegue este momento, aquí están mis $ 0.02 también.

En su mayor parte, estoy de acuerdo con @Pzixel , excepto en los dos últimos puntos, en el sentido de que los try!() / ? par. Todavía he visto a personas argumentar a favor de try!() sobre ? por motivos de claridad, y creo que tener algo de agencia sobre el aspecto de su código en este caso particular es un pro más que un contra , incluso a costa de tener dos sintaxis diferentes.

En particular, una palabra clave await postfix es excelente si escribe su código asincrónico como una secuencia explícita de pasos, por ejemplo

let val1 = my_async() await;
...
let val2 = another_async(val1) await;
...
let val3 = yet_another_async(val2) await;

Por otro lado, es posible que prefiera hacerlo algo más complicado, en el típico estilo de encadenamiento del método Rust-y, por ejemplo

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s))~);

Creo que en este caso ya está bastante claro por el contexto que estamos lidiando con un futuro, y la verbosidad del teclado await es redundante. Comparar:

let my_final_value = commit(get_some_data()
                        .and_then(|s| get_another_data(s))
                        .or_else(|s| report_error(s)) await);

Otra cosa que me gusta de la sintaxis del sufijo de un solo símbolo es que deja en claro que el símbolo pertenece a la expresión , mientras que (para mí, personalmente) el await independiente parece un poco ... perdido ? :)

Supongo que lo que estoy tratando de decir es que await ve mejor en declaraciones , mientras que un sufijo de un solo símbolo se ve mejor en expresiones .

De todos modos, estos son solo mis pensamientos.

Dado que esta ya es la madre de todos los hilos de eliminación de bicicletas, quería agregar otro sigilo que no se ha mencionado hasta ahora AFAICT: -> .

La idea es que refleja el -> de la declaración de tipo de retorno de la función que sigue, que - siendo la función async - es el tipo de retorno esperado.

async fn send() -> Result<Response, HttpError> {...}
async fn into_json() -> Result<Json, EncodingError> {...}

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

En lo anterior, lo que obtiene de send()-> es un Result<Response, HttpError> , tal como está escrito en la declaración de la función.

Estos son mis $ 0.02 después de leer la mayor parte de la discusión anterior y reflexionar sobre las opciones propuestas durante unos días. No hay ninguna razón para dar mi opinión en absoluto, solo soy una persona cualquiera en Internet. Probablemente no comentaré mucho más, ya que soy cauteloso de no agregar más ruido a la discusión.


Me gusta un sigilo postfix. Hay precedencia para los sigilos postfix y creo que se sentiría consistente con el resto del lenguaje. No tengo ninguna preferencia particular sobre qué sigilo en particular se usa, ninguno es convincente, pero creo que se desvanecerá con la familiaridad. Un sigilo introduce el menor ruido en comparación con otras opciones de sufijo.

No me importa la idea de reemplazar . (al encadenar) con -> para esperar. Sigue siendo conciso y poco ambiguo, pero preferiría algo que se pueda usar en los mismos contextos que ? .

No me gustan mucho las otras opciones de sufijo que se han presentado. await no es un campo ni un método y, por lo tanto, se siente completamente inconsistente con el resto del lenguaje para que una construcción de flujo de control se presente como tal. No hay macros de postfix o palabras clave en el idioma, por lo que también parece inconsistente.

Si fuera nuevo en el idioma, sin saberlo completamente, entonces asumiría que .await o .await() no son características especiales del idioma y son campos o métodos en un tipo, como es el caso de todos los demás campos y métodos en el idioma que verá el usuario. Una vez que haya adquirido un poco más de experiencia, si se eligió .await!() , el usuario podría intentar aprender a definir sus propias macros de postfijo (al igual que aprendió a definir sus propias macros de prefijo); no puede. Si ese usuario vio un sello, es posible que necesite buscar documentación (como ? ), pero no lo confundiría con nada más y no perdería el tiempo tratando de encontrar la definición de .await() o documentación para el campo .await .

Me gusta un prefijo await { .. } . Este enfoque tiene una clara precedencia, pero tiene problemas (que ya se han discutido en detalle). A pesar de esto, creo que sería beneficioso para quienes prefieran utilizar combinadores. No me gustaría que esta sea la única opción implementada, ya que no es ergonómico con el método de encadenamiento, pero creo que complementaría muy bien un sigilo postfix.

No me gustan otras opciones de prefijos, no se sienten tan consistentes con otras construcciones de flujo de control en el lenguaje. De manera similar a un método de sufijo, una función de prefijo es inconsistente, no hay otras funciones globales integradas en el lenguaje usado para controlar el flujo. Tampoco se utilizan macros para controlar el flujo (con la excepción de try!(..) pero eso está en desuso porque tenemos una solución mejor: un sigilo postfix).


Casi todas mis preferencias se reducen a lo que se siente natural y consistente (para mí). Cualquiera que sea la solución que se elija, debe tener tiempo para la experimentación antes de la estabilización; la experiencia práctica será un juez mucho mejor de qué opción es mejor que la especulación.

También vale la pena considerar que puede haber una mayoría silenciosa que podría tener opiniones completamente diferentes: aquellos que participan en estas discusiones (incluyéndome a mí mismo) no son necesariamente representativos de todos los usuarios de Rust (esto no significa un desaire o ser de cualquier forma ofensiva).

tl; dr Los sigilos de Postfix son una forma natural de expresar await (debido a la precedencia) y es un enfoque conciso y consistente. Agregaría un prefijo await { .. } y un sufijo @ (que se puede usar en los contextos como ? ). Para mí es más importante que Rust se mantenga internamente consistente.

@SamuelMoriarty

Creo que en este caso ya está bastante claro por el contexto que estamos tratando con un futuro, y la verbosidad del teclado en espera es redundante. Comparar:

Lo siento, pero ni siquiera vi ~ a simple vista. Releí todo el comentario y mi segunda lectura fue más exitosa. Es una gran explicación de por qué creo que el sigilo es peor. Es solo una opinión, pero se confirmó una vez más.

Otro punto en contra de los sigilos es que Rust se convierte en J. Por ejemplo:

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

?@? significa "función que puede devolver un error o un futuro, que debe ser esperado, con error, si lo hay, propagado a una persona que llama"

Me gustaria mas tener

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

@rolandsteiner

Dado que esta ya es la madre de todos los hilos de eliminación de bicicletas, quería agregar otro sigilo que no se ha mencionado hasta ahora AFAICT: ->.

Hacer que la gramática dependa del contexto no mejora las cosas, solo empeora. Conduce a peores errores y tiempos de compilación más lentos.

Tenemos un significado separado para -> , no tiene nada que ver con async/await .

Preferiría el prefijo await { .. } y, si es posible, un sufijo ! sigil.
Un signo de exclamación enfatizaría sutilmente la pereza de los futuros. No se ejecutarán hasta que se les dé un comando con un signo de exclamación.

El ejemplo anterior se vería así:

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

Lo siento si mi opinión no es relevante, ya que tengo poca experiencia asíncrona y ninguna experiencia con el ecosistema de funciones asíncronas de Rust.

Sin embargo, mirando .send()?!?.json()?!?; y otras combinaciones como esa, entiendo las razones básicas por las que la propuesta basada en sigilo me parece incorrecta.

Primero, siento que los sellos encadenados se vuelven ilegibles rápidamente, donde sea que sea ?!? o ?~? o ?->? . Esta será una cosa más con la que se encontrarán los principiantes, adivinando si es un operador o varios. La información está demasiado compacta.

En segundo lugar, en general, me parece que los puntos de espera son menos comunes que los puntos de propagación de errores y más significativos. Los puntos de espera son lo suficientemente importantes como para merecer ser una etapa en la cadena por sí mismos, no una "transformación menor" adjunta a otra etapa (y especialmente no "sólo una de las transformaciones menores"). Probablemente estaría bien incluso solo con la forma de la palabra clave de prefijo (que casi obliga a esperar a romper la cadena), pero en general eso se siente demasiado restrictivo. Mi opción ideal probablemente sería una macro tipo método .await!() con la posibilidad futura de expandir el sistema de macros para permitir macros de usuario tipo método.

La línea de base mínima para la estabilización, en mi opinión, es el operador de prefijo await my_future . Todo lo demás puede seguir.

expr....await reflejaría el contexto de algo que está sucediendo hasta la espera y coherente con los operadores rustlang. También async await es un patrón paralelo ... espera no se puede expresar como método o propiedad similar

Tengo mi inclinación por await!(foo) , pero como otros han señalado que hacerlo nos obligaría a anular la reserva y, por lo tanto, excluiría su uso futuro como operador hasta 2021, iré con await { foo } como mi preferencia. En cuanto a postfix aguardar, no tengo una opinión en particular.

Sé que puede que no sea el mejor lugar para hablar sobre la espera implícita, pero ¿funcionaría algo así como una espera implícita explícita? Entonces, primero tenemos el prefijo de espera de aterrizaje:

await { future }?

Y luego agregamos algo similar a

let result = implicit await { client.get("https://my_api").send()?.json()?; }

o

let result = auto await { client.get("https://my_api").send()?.json()?; }

Al elegir el modo implícito, todo entre {} se espera automáticamente.

Esto ha unificado la sintaxis await y equilibraría la necesidad de un prefijo de espera, encadenamiento y ser lo más explícito posible.

Decidí rg --type csharp '[^ ]await' para estudiar ejemplos de lugares donde el prefijo era subóptimo. Es posible que no todos sean perfectos, pero son códigos reales que se revisaron. (Ejemplos ligeramente desinfectados para eliminar ciertas cosas del modelo de dominio).

(await response.Content.ReadAsStringAsync()).Should().Be(text);

Usar FluentAssertions como una forma más agradable de hacer las cosas que el MSTest normal assert_eq! Assert.Equal .

var previous = (await branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Esta cosa general de "mira, realmente solo necesitaba una cosa de eso" es un montón de ellos.

id = id ?? (await this.storageCoordinator.GetDefaultWidgetAsync(cancellationToken)).Identity;

Otro "Solo necesitaba una propiedad". (Aparte: "hombre, me alegro de que Rust no necesite CancellationToken s).

var pending = (await transaction.Connection.QueryAsync<EventView>(command)).ToList();

El mismo .collect() que la gente ha mencionado en Rust.

foreach (var key in changes.Keys.Intersect((await neededChangesTask).Keys))

Había estado pensando en que tal vez me gustara la palabra clave postfix, con nuevas líneas rustfmt después de ella (y después de ? , si está presente), pero creo que esto me hace pensar que la nueva línea no es buena en general.

else if (!await container.ExistsAsync())

Uno de los raros en los que el prefijo era realmente útil.

var response = (HttpWebResponse)await request.GetResponseAsync();

Hubo algunos moldes, aunque, por supuesto, los moldes son otro lugar donde Rust es posfijo pero C # es prefijo.

using (var response = await this.httpClient.SendAsync(requestMsg))

Este prefijo vs postfijo no importa, pero creo que es otra diferencia interesante: debido a que C # no tiene Drop , un montón de cosas terminan _necesitando_ ir en variables y no encadenadas.

algunos de @scottmcm 's ejemplos rustified en los diferentes postfix variantes:

// keyword
response.content.read_as_string()) await?.should().be(text);
// field
response.content.read_as_string()).await?.should().be(text);
// function
response.content.read_as_string()).await()?.should().be(text);
// macro
response.content.read_as_string()).await!()?.should().be(text);
// sigil
response.content.read_as_string())@?.should().be(text);
// sigil + keyword
response.content.read_as_string())@await?.should().be(text);
// keyword
let previous = branch.list_history(timestamp_utc, None, 1) await?.history_entries.single_or_default();
// field
let previous = branch.list_history(timestamp_utc, None, 1).await?.history_entries.single_or_default();
// function
let previous = branch.list_history(timestamp_utc, None, 1).await()?.history_entries.single_or_default();
// macro
let previous = branch.list_history(timestamp_utc, None, 1).await!()?.history_entries.single_or_default();
// sigil
let previous = branch.list_history(timestamp_utc, None, 1)@?.history_entries.single_or_default();
// sigil + keyword
let previous = branch.list_history(timestamp_utc, None, 1)@await?.history_entries.single_or_default();
// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;
// field
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await?.identity).await?;
// function
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await()?.identity).await()?;
// macro
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async().await!()?.identity).await!()?;
// sigil
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@?.identity)@?;
// sigil + keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async()@await?.identity)@await?;

(gracias a @ Nemo157 por las correcciones)

// keyword
let pending = transaction.connection.query(command) await.into_iter().collect::<Vec<EventView>>();
// field
let pending = transaction.connection.query(command).await.into_iter().collect::<Vec<EventView>>();
// function
let pending = transaction.connection.query(command).await().into_iter().collect::<Vec<EventView>>();
// macro
let pending = transaction.connection.query(command).await!().into_iter().collect::<Vec<EventView>>();
// sigil
let pending = transaction.connection.query(command)@.into_iter().collect::<Vec<EventView>>();
// sigil + keyword
let pending = transaction.connection.query(command)@await.into_iter().collect::<Vec<EventView>>();

Para mí, después de leer esto, @ sigil está fuera de la mesa, ya que es invisible especialmente frente a ? .

No he visto a nadie en este hilo discutir la variante de espera para Stream. Sé que está fuera de alcance, pero ¿deberíamos pensar en ello?

Sería una pena si tomáramos una decisión que resultó ser un bloqueador de espera en Streams.

// keyword
id = id.or_else(|| self.storage_coordinator.get_default_widget_async() await?.identity);

No puede usar await dentro de un cierre como este, debería haber un método de extensión adicional en Option que tome un cierre asíncrono y devuelva un Future sí mismo (en en el momento en que creo que la firma del método de extensión es imposible de especificar, pero con suerte obtendremos alguna forma de hacer que los cierres asíncronos se puedan usar en algún momento) .

// keyword
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

(o una traducción más directa del código usando if let lugar de un combinador)

@ Nemo157 Yeal, pero probablemente podamos hacerlo sin funciones adicionales:

id = ok(id).transpose().or_else(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Pero el enfoque if let parece más natural.

Para mí, después de leer esto, @ sigil está fuera de la mesa, ya que es invisible, especialmente frente a?.

No se olvide de los esquemas de resaltado de código alternativo, para mí este código se ve así:
1

Personalmente, no creo que la "invisibilidad" aquí sea un problema mayor que para el ? independiente.

Y un esquema de color inteligente puede hacerlo aún más notable (por ejemplo, usando un color diferente al de ? ).

@newpavlov no puede elegir el esquema de color en herramientas externas, por ejemplo, en las pestañas de revisión de gitlab / github.

Dicho esto, no es una buena práctica confiar solo en resaltar. Otros pueden tener otras preferencias.

Hola chicos, soy un nuevo alumno de Rust que viene de C ++. Solo quiero comentar que no sería algo como

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

o

id = id.or_else_async(async || { 
    self.storage_coordinator.get_default_widget_async() await?.identity 
}) await?;

ser básicamente incomprensible? El await se empuja hacia el final de la línea, mientras que nuestro enfoque se concentra principalmente al principio (que es como cuando usas un motor de búsqueda).
Algo como

id =  await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

o

id = auto await {
    id.or_else_async(async || { self.storage_coordinator.get_default_widget_async()?.identity })
}?;

sugerido anteriormente me parece mucho mejor.

Estoy de acuerdo. Las primeras palabras son lo primero que uno mira al escanear código, resultados de búsqueda, párrafos de texto, etc. Esto inmediatamente pone en desventaja a cualquier tipo de sufijo.

Para la ubicación de ? , estoy convencido de que await? foo es lo suficientemente distinto y fácil de aprender.

Si estabilizamos esto ahora y después de un año de dos de uso decidimos que realmente queremos algo mejor para el encadenamiento, podemos considerar las macros postfix como una característica general.

Me gustaría proponer una variación sobre la idea de que @nicoburns aguarde tanto el prefijo como el sufijo.
Podríamos tener 2 cosas:

  • un prefijo await palabra clave (por ejemplo, el sabor con un enlace más fuerte que ? , pero esto es menos importante)
  • un nuevo método en std::Future , por ejemplo fn awaited(self) -> Self::Output { await self } . Su nombre también podría ser block_on , o blocking , o algo mejor que se le ocurra a otra persona.

Esto permitiría tanto el uso del prefijo "simple" como también permitir el encadenamiento, evitando tener que esperar una palabra clave contextual.

Técnicamente, la segunda viñeta también se puede lograr con una macro de sufijo, en cuyo caso escribiríamos .awaited!() .

Esto permitiría un código que se parece a esto:

let done = await delayed;

let value = await delayed_result?;

let value2 = await some.thing()?;

let value3 = some.other().thing().awaited()?;

let value4 = promise
        .awaited()
        .map_err(|e| e.into())?
        .obtain_other_future()
        .awaited();

Dejando a un lado otros problemas, el punto principal de esta propuesta es tener await como el bloque de construcción básico del mecanismo, al igual que match , y luego tener combinadores para ahorrarnos tener que escribir mucho palabras clave y llaves de todo tipo. Creo que esto implica que se pueden enseñar de la misma manera: primero el simple await , luego, para evitar demasiados paréntesis, se puede usar .awaited() y encadenar.


Alternativamente, una versión más mágica podría eliminar por completo la palabra clave await y confiar en un método mágico .awaited() en std :: Future, que no pueden implementar otros que escriban sus propios futuros, pero creo que esto sería bastante contrario a la intuición y demasiado de un caso especial.

un nuevo método en std::Future , por ejemplo fn awaited(self) -> Self::Output { await self }

Estoy bastante seguro de que eso es imposible (sin hacer que la función sea mágica), porque para esperar dentro de ella tendría que ser async fn awaited(self) -> Self::Output { await self } , que aún así necesitaría ser await ed. Y si estamos contemplando hacer que la función sea mágica, también podría ser la palabra clave, IMO.

Ya existe Future::wait (¿aunque aparentemente 0.3 aún no lo tiene?), Lo que ejecuta un futuro bloqueando el hilo.

El problema es que _ el objetivo de await es _no_ bloquear el hilo_. Si la sintaxis a esperar es prefijo y queremos una opción de sufijo, debe ser una macro de sufijo y no un método.

Y si va a decir que use un método mágico, simplemente llámelo .await() , que ya se ha discutido varias veces en el hilo, tanto como un método de palabra clave como un método mágico extern "rust-await-magic" "real "fn.

EDITAR: scottmcm ninja me y GitHub no me informaron (¿porque el móvil?), Sin embargo, todavía voy a dejar esto.

@scottmcm ¿También podría analizar el recuento de frecuencias en las que await parecían estar bien en comparación con los subóptimos? Creo que una encuesta para el recuento de frecuencias de prefijo vs postfijo podría ayudar a responder varias preguntas.

  1. Tengo la impresión de que hasta ahora el mejor caso de uso de postfix await ha sido
client.get("https://my_api").send() await?.json() await?

tantas publicaciones usan como ejemplo. Tal vez me perdí algo, pero ¿hay otros casos? ¿No sería bueno que extrajera esta línea en una función si aparece con frecuencia en todo el código base?

  1. Como mencioné anteriormente, si algunas personas escriben
id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

lo que hacen es ocultar visualmente exactamente await lugar de hacerlo explícito para que todos los puntos de rendimiento puedan verse claramente .

  1. La palabra clave Postfix requeriría inventar algo que no existe en otros lenguajes convencionales. Si no ofrece un resultado significativamente mejor, inventarlo no valdría la pena.

La espera se empuja hacia el final de la línea, mientras que nuestro enfoque se concentra principalmente al principio (que es como cuando usas un motor de búsqueda).

Obviamente, no puedo refutar que la gente escanee contenido web usando un patrón en forma de F , @ dowchris97.

Sin embargo, no es un hecho que usen el mismo patrón para el código. Por ejemplo, este otro en la página parece que podría coincidir mejor con la forma en que la gente busca ? y, por lo tanto, podría buscar await :

El patrón manchado consiste en omitir grandes trozos de texto y escanear como si buscara algo específico, como un enlace, dígitos, una palabra en particular o un conjunto de palabras con una forma distintiva (como una dirección o una firma).

A modo de comparación, tomemos el mismo ejemplo si devolvió Result lugar de impl Future :

let id = id.try_or_else(|| Ok(self.storage_coordinator.try_get_default_widget()?.identity))?;

Creo que es bastante claro que un patrón de lectura en forma de F para un código como ese no ayuda a encontrar los ? s, ni ayuda si se divide en varias líneas como

let id = id.try_or_else(|| {
    let widget = self.storage_coordinator.try_get_default_widget()?;
    Ok(widget.identity)
})?;

Creo que se podría usar una descripción análoga a la suya para argumentar que la posición del sufijo "oculta visualmente exactamente el ? lugar de hacerlo explícito para que se pueda ver claramente el punto de retorno", que estoy de acuerdo fue uno de las preocupaciones originales acerca de ? , pero parece que no ha sido un problema en la práctica.

Entonces, en general, creo que colocarlo donde las personas ya hayan sido capacitadas para buscar ? es la mejor manera de asegurarse de que la gente lo vea. Definitivamente prefiero que no tengan que usar dos escaneos diferentes simultáneamente.

@scottmcm

Sin embargo, no es un hecho que usen el mismo patrón para el código. Por ejemplo, este otro en la página parece que podría coincidir mejor con la forma en que la gente busca ? y, por lo tanto, podría buscar await :

El patrón manchado consiste en omitir grandes trozos de texto y escanear como si buscara algo específico, como un enlace, dígitos, una palabra en particular o un conjunto de palabras con una forma distintiva (como una dirección o una firma).

Ciertamente, tampoco hay evidencia de que el uso de patrones manchados ayude a comprender el código mejor / más rápido. Especialmente cuando los seres humanos usan más comúnmente el patrón en forma de F, que mencionaré más adelante.

Tome esta línea como ejemplo, cuando comience a usar el patrón manchado, suponga que nunca ha leído esto antes.

id = id.or_else_async(async || self.storage_coordinator.get_default_widget_async() await?.identity) await?;

Para mí, comencé cuando vi async , luego fui primero a await? , luego a self.storage_coordinator.get_default_widget_async() , luego a .identity , y finalmente me di cuenta de toda la línea es asincrónico. Yo diría que definitivamente esta no es la experiencia de lectura que me gusta. Una razón es que nuestro sistema de lenguaje escrito no tiene este tipo de salto hacia adelante y hacia atrás intercalado . Cuando saltas, interrumpe la construcción del modelo mental de lo que está haciendo esta línea.

A modo de comparación, ¿qué está haciendo esta línea? ¿Cómo lo sabes?

id = await? id.or_else_async(async || {
    let widget = await? self.storage_coordinator.get_default_widget_async();
    widget.identity
});

Tan pronto como llegué a await? , inmediatamente tuve un aviso de que esto es asincrónico. Luego leí let widget = await? , nuevamente, sin ninguna dificultad, sabía que esto es asincrónico, algo está sucediendo. Siento que sigo el patrón en forma de F. De acuerdo con https://thenextweb.com/dd/2015/04/10/how-to-design-websites-that-mirror-how-our-eyes-work/ , la forma de F es el patrón más utilizado. Entonces, ¿vamos a diseñar un sistema que se ajuste a la naturaleza humana o inventar algún sistema que necesite educación especial y trabaje en contra de nuestra naturaleza ? Prefiero el primero. La diferencia sería aún mayor cuando las líneas asíncronas y normales se intercalan de esta manera

await? id.or_else_async(async || {
    let widget1 = await? self.storage_coordinator.get_default_widget_async();
    let result1 = do_some_wierd_computation_on(widget1.identity);
    let widget2 = await? self.network_coordinator.get_default_widget_async();
    let result2 = do_some_strange_computation_on(widget2.identity);
});

¿Se supone que debo buscar await? todas las líneas?

let id = id.try_or_else (|| Ok (self.storage_coordinator.try_get_default_widget () ?. identidad)) ?;

Por eso, no creo que la comparación sea buena. Primero, ? no cambia tanto el modelo mental. En segundo lugar, el éxito de ? radica en el hecho de que no requiere que usted salte hacia adelante y hacia atrás entre las palabras. La sensación que tengo cuando leo ? en .try_get_default_widget()? es, "ok, obtienes un buen resultado". Eso es. No tengo que retroceder para leer algo más para entender esta línea.

Por lo tanto, mi conclusión general es que postfix puede proporcionar una comodidad

Cómo se vería esto en realidad con un rustfmt estilo y resaltado de sintaxis propuesto ( while -> async , match -> await )

while fn foo() {
    identity = identity
        .or_else_async(while || {
            self.storage_coordinator
                .get_default_widget_async().match?
                .identity
        }).match?;
}

No sé ustedes, pero veo los match es instantáneamente.

(¡Hola @ CAD97 , lo arreglé!)

@ dowchris97

Si vas a argumentar que ? no cambia el modelo mental, ¿qué hay de malo en mi argumento de que await no debería cambiar tu modelo mental del código? (Esta es la razón por la que he favorecido las opciones de espera implícitas anteriormente, aunque estoy convencido de que no son las adecuadas para Rust).

Específicamente, lee async al _comienzo_ del encabezado de la función. Si la función es tan grande que no cabe en una pantalla, es casi seguro que es demasiado grande y tiene otros problemas de legibilidad mayores que encontrar los puntos await .

Una vez que esté async , debe mantener ese contexto. Pero en ese punto, todo lo que espera es una transformación que convierte un cálculo diferido Future en el resultado Output al estacionar el tren de ejecución actual.

No debería importar que esto signifique una complicada transformación de la máquina de estado. Ese es un detalle de implementación. La única diferencia con los subprocesos del sistema operativo y el bloqueo es que otro código puede ejecutarse en el subproceso actual mientras espera el cálculo diferido, por lo que Sync no lo protege tanto. (En todo caso, lo leería como un requisito para que async fn sea Send + Sync lugar de inferirse y no ser seguro para subprocesos).

En un estilo mucho más orientado a las funciones, sus widgets y resultados se verían así (sí, sé que esto no está usando una Monad y pureza real, etc., perdóneme):

    let widget1 = await(get_default_widget_async(storage_coordinator(self)));
    let result1 = do_some_wierd_computation_on(identity(widget1));
    let widget2 = await(get_default_widget_async(network_coordinator(self)));
    let result2 = do_some_strange_computation_on(identity(widget2));

Pero debido a que es un orden inverso al de la canalización del proceso, la multitud funcional inventó el operador de "canalización", |> :

    let widget1 = self |> storage_coordinator |> get_default_widget_async |> await;
    let result1 = widget1 |> identity |> do_some_wierd_computation_on;
    let widget2 = self |> network_coordinator |> get_default_widget_async |> await;
    let result2 = widget2 |> identity |> do_some_strange_computation_on;

Y en Rust, ese operador de canalización es . , que proporciona un alcance de lo que se puede canalizar y buscar por tipo a través de la aplicación del método:

    let widget1 = self.storage_coordinator.get_default_widget_async().await();
    let result1 = widget1.identity.do_some_wierd_computation_on();
    let widget2 = self.network_coordinator.get_default_widget_async().await;
    let result2 = widget2.identity.do_some_strange_computation_on();

Cuando piensa en . como canalización de datos de la misma manera que lo hace |> , las cadenas más largas que se ven a menudo en Rust comienzan a tener más sentido, y cuando se formatea bien (como en el ejemplo de Centril) no No se pierda la legibilidad porque solo tiene una canalización vertical de transformaciones en los datos.

await no te dice "oye, esto es asincrónico". async hace. await es cómo se estaciona y espera el cálculo diferido, y tiene mucho sentido ponerlo a disposición del operador de canalización de Rust.

(Oye @Centril , olvidaste hacer que sea un async fn (o while fn ), lo que diluye mi punto un poco 😛

si podríamos o no redefinir la invocación macro

m!(item1, item2)

es lo mismo que

item1.m!(item2)

para que podamos usar el estilo await tanto de prefijo como de postfijo

await!(future)

y

future.await!()

@ CAD97

await no te dice "oye, esto es asincrónico". async hace. await es cómo se estaciona y espera el cálculo diferido, y tiene mucho sentido ponerlo a disposición del operador de tuberías de Rust.
Sí, supongo que lo he entendido bien, pero no escribí con rigor.

También entiendo tu punto. Pero no estoy convencido, sigo pensando que poner await al frente será tremendamente mejor.

Sé que |> se usa en otros idiomas para significar algo más , pero se ve bastante bien y extremadamente claro para mí en Rust en lugar del prefijo await :

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

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

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

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

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

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

    Ok(user)
}

El argumento sobre el orden de lectura se aplicaría igualmente bien al operador ? que reemplaza try!() . Después de todo, "oye, esto podría ceder" es importante, pero "oye, esto podría regresar antes" también es importante, si no más. Y, de hecho, las preocupaciones sobre la visibilidad se plantearon repetidamente en la discusión de bikeshed sobre ? (incluido este hilo interno y este problema de GitHub ). Pero la comunidad finalmente decidió aprobarlo y la gente se acostumbró. Sería extraño terminar ahora con los modificadores ? y await apareciendo en lados opuestos de una expresión, solo porque, básicamente, la comunidad cambió de opinión sobre la importancia de la visibilidad.

El argumento sobre el orden de lectura se aplicaría igualmente bien al operador ? que reemplaza try!() . Después de todo, "oye, esto podría ceder" es importante, pero "oye, esto podría regresar antes" también es importante, si no más. Y, de hecho, las preocupaciones sobre la visibilidad se plantearon repetidamente en la discusión de bikeshed sobre ? (incluido este hilo interno y este problema de GitHub ). Pero la comunidad finalmente decidió aprobarlo y la gente se acostumbró. Sería extraño terminar ahora con los modificadores ? y await apareciendo en lados opuestos de una expresión, solo porque, básicamente, la comunidad cambió de opinión sobre la importancia de la visibilidad.

Realmente no se trata de lo que está haciendo tu código. Se trata de su modelo mental de lo que está haciendo su código, si el modelo se puede construir fácilmente o no, si hay choques o no, si es intuitivo o no. Estos pueden diferir en gran medida de unos a otros.

No quiero debatir más sobre esto. Creo que la comunidad está lejos de conformarse con una solución postfix, aunque muchas personas aquí pueden apoyarla. Pero creo que podría haber una solución:

Mozilla construye Firefox, ¿verdad? ¡Se trata de UI / UX! ¿Qué tal una investigación seria de HCI sobre este problema? Así que realmente podemos convencernos unos a otros utilizando datos, no conjeturas.

@ dowchris97 Ha habido (solo) dos casos de comparación de código del mundo real en este hilo: una comparación de ~ 24k líneas con bases de datos y reqwest y una comparación que ejemplifica por qué C # no es una comparación precisa con la sintaxis base de Rust . Ambos resultan que en el código Rust del mundo real, await in postfix parece más natural, y no sufre los problemas que tienen otros lenguajes sin semántica de movimiento de valor natural. A menos que aparezca alguien con otro ejemplo del mundo real de tamaño decente que tiende a mostrar lo contrario, estoy bastante convencido de que la sintaxis de prefijos es una necesidad impuesta por otros lenguajes sobre sí mismos porque carecen de la semántica de valor clara de la canalización de Rust (donde leer código idiomático de izquierda a derecha casi siempre hace precisamente lo que sugeriría el modelo mental).

Editar: solo si no es lo suficientemente claro, ninguno de C #, Python, C ++, Javascript tiene métodos de miembros que toman self por valor en lugar de referencia. C ++ tiene las referencias más cercanas a rvalue, pero el orden del destructor sigue siendo confuso en comparación con Rust.

Creo que el argumento de que await es mejor como prefijo no se deriva de cómo tienes que cambiar tu modelo mental del código, sino más bien de cómo en rust tenemos palabras clave de prefijo, pero no tenemos palabras clave de sufijo (por postfixes, rust usa sigilos como lo ejemplifica ? ) Es por eso que await foo() siente más fácil de leer que foo() await y por qué algunas personas quieren @ al final de una declaración y no les gusta tener await allí.

Por una razón similar, .await siente extraño de usar: el operador de punto se usa para acceder a los campos y métodos de una estructura (que también es la razón por la que no se puede ver como un operador de canalización puro), por lo que tener .await es como decir "el punto se usa para acceder a campos y métodos de una estructura o para acceder a await , que no es ni un campo ni una función".

Personalmente, me gustaría ver el prefijo await o un sigilo postfix (no tiene que ser @ ).

Ambos resultan que en el código de Rust del mundo real, esperar en postfix parece más natural

Esa es una declaración polémica. El ejemplo de reqwest solo presenta una versión de la sintaxis de sufijo.

En una nota diferente, si esta discusión se reduce a una votación de a quién le gusta qué más, por favor menciónelo en reddit para que la gente no se queje como lo hizo con impl Trait en argumentos de función.

@ eugene2k Para la discusión fundamental de si postfix se ajusta al modelo mental de los programadores de Rust, la mayoría o todas las sintaxis de postfix están aproximadamente en la misma escala en comparación con el prefijo. No creo que haya tanta diferencia de legibilidad significativa entre las variantes de sufijo como entre prefijo y sufijo. Vea también mi comparación de bajo nivel de precedencia de operadores que concluye que su semántica es igual en la mayoría de los usos, por lo que es una cuestión de qué operador transmite mejor el significado (actualmente prefiero una sintaxis de llamada de función real, pero no tienen una fuerte preferencia sobre los demás tampoco).

@ eugene2k Las decisiones en Rust nunca se toman mediante votación. Rust no es una democracia, es una meritocracia.

Los equipos core / lang analizan todos los diversos argumentos y perspectivas y luego deciden. Esta decisión se toma por consenso (entre los miembros del equipo), no por votación.

Aunque los equipos de Rust tienen absolutamente en cuenta los deseos generales de la comunidad, en última instancia, deciden basándose en lo que creen que es mejor para Rust a largo plazo.

La mejor manera de influir en Rust es presentar nueva información, hacer nuevos argumentos o mostrar nuevas perspectivas.

Repetir argumentos existentes o decir "yo también" (o algo similar) no aumenta las posibilidades de que una propuesta sea aceptada. Nunca se aceptan propuestas basadas en la popularidad.

Eso también significa que los diversos votos a favor / en contra en este hilo no importan en absoluto para qué propuesta se acepta.

(No me refiero a usted específicamente, estoy explicando cómo funcionan las cosas por el bien de todos en este hilo).

@Pauan Se ha dicho antes que los equipos core / lang

Entonces, cuando se han analizado todos estos contextos y a los tomadores de decisiones del equipo les gusta un enfoque, mientras que la mayoría de los otros usuarios, que no están en el equipo, prefieren otro enfoque, ¿cuál debería ser la decisión final?

La decisión siempre la toma el equipo. Período. Así es como se diseñaron intencionalmente las reglas.

Y las funciones suelen ser implementadas por miembros del equipo. Y los miembros del equipo también han establecido confianza dentro de la comunidad. Así que tienen autoridad tanto de jure como de facto.

Si la situación cambia (quizás en base a la retroalimentación) y los miembros del equipo cambian de opinión, entonces pueden cambiar su decisión. Pero incluso entonces, la decisión siempre la toman los miembros del equipo.

Como dices, las decisiones a menudo implican cierta subjetividad, por lo que es imposible complacer a todos, pero se debe tomar una decisión. Para tomar una decisión, el sistema que usa Rust se basa en que los miembros del equipo lleguen a un consenso.

Cualquier discusión sobre si Rust debe ser gobernado de manera diferente está fuera de tema y debe discutirse en otro lugar.

(PD: no estoy en los equipos core o lang, así que no tengo ninguna autoridad en esta decisión, así que tengo que ceder ante ellos, al igual que tú)

@HeroicoKatora

No creo que haya tanta diferencia de legibilidad significativa entre las variantes de postfijo

Estoy en desacuerdo. Encuentro que foo().await()?.bar().await()? es más fácil de leer que foo() await?.bar() await? o incluso foo()@?.bar()@? A pesar de eso, siento que tener métodos que en realidad no son métodos sienta un mal precedente.

Me gustaría proponer otra idea. Estoy de acuerdo en que el prefijo await no es fácil de encadenar junto con otras funciones. ¿Qué tal esta sintaxis de sufijo: foo(){await}?.bar()?{await} ? No se puede confundir con las llamadas a funciones y me parece que es bastante fácil de leer en cadena.

Y otra propuesta más escrita por mí. Consideremos la siguiente sintaxis de llamada al método:

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

    Ok(user)
}

Lo que lo hace único entre las demás propuestas:

  • Los corchetes hacen que la prioridad y el alcance sean mucho más limpios.
  • La sintaxis es lo suficientemente extensible como para permitir la eliminación de enlaces temporales en otros contextos también.

Creo que la extensibilidad es la mayor ventaja aquí. Esta sintaxis permitiría implementar características de un lenguaje que en su forma común no son posibles en Rust debido a la alta relación complejidad / utilidad. La lista de posibles construcciones de lenguaje se proporciona a continuación:

  1. Aplazamiento de todos los operadores de prefijo (incluido await , así es como se supone que funciona):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
  1. Funcionalidad del operador de oleoductos:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
  1. Retorno de la función invalidante:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
  1. Funcionalidad Wither:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
  1. División de cadena:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
  1. Macros de Postfix:
let x = long().method().[dbg!(it)].chain();

Creo que la introducción de un nuevo tipo de sintaxis (campos mágicos, macros postfix, corchetes) tiene un mayor impacto en el lenguaje que esta característica por sí sola, y debería requerir un RFC.

¿Hay algún repositorio que ya use mucho await ? Ofrecería reescribir una parte más grande en cada estilo propuesto, para que podamos tener una mejor idea de cómo se ven y cuán comprensible es el código.

Reescribo en delimitadores obligatorios:

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

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

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

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

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

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

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

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

    Ok(user)
}

Es casi idéntico a await!() . Entonces, ¡se ve hermoso! Después de haber usado await!() durante un año o dos, ¿por qué inventaría de repente el postfix await que no aparece en ninguna parte de la historia del lenguaje de programación?

¿Eh? La sintaxis expr.[await it.foo()] @ I60R con la palabra clave contextual it es bastante clara. No esperaba que me gustara ninguna propuesta de sintaxis nueva y elegante, pero eso es realmente bastante bueno, es un uso inteligente del espacio de sintaxis (porque IIRC .[ no es actualmente una sintaxis válida en ninguna parte), y resolvería mucho más problemas que solo esperar.

Estuvo de acuerdo en que definitivamente requeriría un RFC, y puede que no resulte ser la mejor opción. Pero creo que es otro punto en el lado de conformarse con una sintaxis de prefijo para await por el momento, sabiendo que hay una serie de opciones para resolver el problema de "los operadores de prefijo son incómodos para encadenar" de una manera más general que beneficia más que async / await en el futuro.

Y otra propuesta más escrita por mí. Consideremos la siguiente sintaxis de llamada al método:

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

    Ok(user)
}

Lo que lo hace único entre las demás propuestas:

* Square brackets makes precedence and scoping much cleaner.

* Syntax is extensible enough to allow temporary bindings removal in other contexts as well.

Creo que la extensibilidad es la mayor ventaja aquí. Esta sintaxis permitiría implementar características de un lenguaje que en su forma común no son posibles en Rust debido a su alta relación complejidad / utilidad. La lista de posibles construcciones de lenguaje se proporciona a continuación:

1. Deferring of all prefix operators (including `await` - it's how it supposed to work):
let result = api.method().[await returns_future()];
let cond = long.method().chain().[!is_empty()];
let val = something.[*returns_ref()];
1. Pipeline operator functionality:
// from https://users.rust-lang.org/t/pipe-results-like-elixir/11175/19
let deserialized: DataType =
    Path::new("path/to/file.json")
        .[File::open(&it)].expect("file not found")
        .[serde_json::from_reader(it)].expect("error while reading json");
1. Overriding function return:
let sorted_vec = iter
    .map(mapper)
    .collect::<Vec<_>>()
    .[sort(),];
1. Wither functionality:
consume(&HashMap::new(). [
    insert("key1", val1),
    insert("key2", val2),
]);
1. Chain splitting:
let sf = surface(). [
    draw_circle(ci_dimens).draw_rectangle(rect_dimens).finish()?,
    draw_something_custom().finish()?,
];
1. Postfix macros:
let x = long().method().[dbg!(it)].chain();

Verdaderamente incomprensible.

@tajimaha Mirando tu ejemplo, creo que await {} podría ser mucho mejor que await!() si vamos con delimitadores obligatorios, porque evita el problema de "demasiados corchetes" que puede causar legibilidad problemas con la sintaxis await!() .

Comparar:
`` c #
espera {foo.bar (url, false, qux.clone ())};

with

```c#
await!(foo.bar(url, false, qux.clone()));

(ps Puede obtener el resaltado de sintaxis de async y await para ejemplos simples configurando el idioma en c #).

@nicoburns Puede usar cualquiera de () , {} o [] con la macro.

@sgrif Ese es un buen punto. Y como este es el caso, sugeriría que "prefijo palabra clave con delimitadores obligatorios" tiene muy poco sentido como opción. Ya que rompe la consistencia con otras macros básicamente sin ningún beneficio.

(FWIW, sigo pensando que "prefijo sin delimitadores" con una solución general como macros postfix o la sugerencia de @ I60R para postfix tiene más sentido. Pero la opción "simplemente seguir con la macro existente" está creciendo en mí ...)

Sugeriría que "prefijo palabra clave con delimitadores obligatorios" tiene muy poco sentido como opción. Ya que rompe la consistencia con otras macros básicamente sin ningún beneficio.

¿Por qué tiene poco sentido y por qué una palabra clave que no tiene la misma sintaxis que una macro es un problema?

@tajimaha

Sugeriría que "prefijo palabra clave con delimitadores obligatorios" tiene muy poco sentido como opción. Ya que rompe la consistencia con otras macros básicamente sin ningún beneficio.

¿Por qué tiene poco sentido y por qué una palabra clave que no tiene la misma sintaxis que una macro es un problema?

Bueno, si await fuera una macro, eso tendría la ventaja de no agregar ninguna sintaxis adicional al lenguaje. Reduciendo así la complejidad del idioma. Sin embargo, existe un buen argumento para usar una palabra clave: return , break , continue , y otras construcciones de modificación del flujo de control también son palabras clave. Pero todos estos funcionan sin delimitadores, por lo que para ser coherente con estas construcciones, await también debería funcionar sin delimitadores.

Si tiene delimitadores de espera con obligatorios, entonces tiene:

// Macros using `macro!(foo);` syntax 
format!("{}", foo);
println!("hello world");

// Normal keywords using `keyword foo;`
continue foo;
return foo;

// *and* the await keyword which is kind of in between the other two syntaxes:
await(foo);
await{foo};

Esto es potencialmente confuso, ya que ahora debe recordar 3 formas de sintaxis en lugar de 2. Y dado que la palabra clave con delimitadores obligatorios no ofrece ningún beneficio sobre la sintaxis macro, creo que sería preferible seguir con el estándar. macro sintaxis si deseamos aplicar delimitadores (que no estoy del todo convencido de que debamos).

Una pregunta para aquellos que usan mucho async / await en Rust hoy: ¿con qué frecuencia esperan funciones / métodos frente a variables / campo?

Contexto:

Sé que en C # es común hacer cosas que se reducen a este patrón:

var fooTask = this.FooAsync();
var bar = await this.BarAsync();
var foo = await fooTask;

De esa forma ambos corren en paralelo. (Algunos dirán que aquí se debe usar Task.WhenAll , pero la diferencia de rendimiento es pequeña y hace que el código sea más complicado ya que requiere pasar por índices de matriz).

Pero tengo entendido que, en Rust, eso en realidad no se ejecutará en paralelo, ya que poll para fooTask no se llamaría hasta que bar obtuviera su valor, y _necesita_ usar un combinador, tal vez

let (foo, bar) = when_all!(
    self.foo_async(),
    self.bar_async(),
).await;

Entonces, dado eso, tengo curiosidad por saber si uno regularmente termina teniendo el futuro en una variable o campo que debe esperar, o si casi siempre está esperando expresiones de llamada. Porque si es lo último, hay una pequeña variante de formato de la palabra clave postfix que realmente no hemos discutido: palabra clave postfix _no-space_.

No he pensado mucho en si es bueno, pero sería posible escribir código como

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

(En realidad, no quiero tener una discusión sobre rustfmt, como he dicho, pero recuerdo que una de las razones por las que no me gusta la palabra clave postfix _ era_ el espacio que divide el fragmento visual).

Si vamos con eso, también podríamos ir con la sintaxis .await para aprovechar
el poder del punto, ¿no?

Una pregunta para aquellos que usan mucho async / await en Rust hoy: ¿con qué frecuencia esperan funciones / métodos frente a variables / campo?

Pero tengo entendido que, en Rust, eso en realidad no se ejecutará en paralelo en absoluto [...]

Correcto. De la misma base de código que antes, aquí está este ejemplo:

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(move |id| {
        self__.request(URL, Method::GET, vec![("info".into(), id.into())])
            .and_then(|mut response| response.json().from_err())
    });

    await!(futures_unordered(futures).collect())?
};

Si tuviera que reescribir el cierre con un cierre async :

let self__ = self_.clone();
let responses: Vec<Response> = {
    let futures = all_ids.into_iter().map(async move |id| {
        let mut res =
            await!(self__.request(URL, Method::GET, vec![("info".into(), id.into())]))?;

        Ok(await!(res.json())?)
    });

    await!(futures_unordered(futures).collect())?
};

Si tuviera que cambiar a la sintaxis .await (y encadenarlo):

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

¿Hay algún repositorio que ya tenga un uso intensivo en espera? Ofrecería reescribir una parte más grande en cada estilo propuesto

@gralpli Lamentablemente, nada de lo que pueda abrir en código abierto usa mucho await! . Definitivamente se presta más al código de la aplicación en este momento (especialmente por ser tan inestable).

let self__ = self_.clone();
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Estas líneas muestran exactamente cómo el código se estropea por el uso excesivo de postfix y encadenamiento .

Veamos la versión del prefijo:

let func = async move |id| {
    let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
    Ok(await(req.json())?)
}
let responses: Vec<Response> = await {
    futures_unordered(all_ids.into_iter().map(func)).collect()
}?;

Las dos versiones usan 7 líneas, pero en mi opinión, la segunda es más limpia. También hay dos conclusiones para el uso de delimitadores obligatorios:

  1. La await { future }? no parece ruidosa si la parte future es larga. Ver let req = await { self.request(URL, Method::GET, vec![("info".into(), id.into())]) }?;
  2. Cuando la línea es corta, usar await(future) podría ser mejor. Ver Ok(await(req.json())?)

En mi opinión, al cambiar entre las dos variantes, la legibilidad de este código es mucho mejor que antes.

El primer ejemplo está mal formateado. No creo que rustfmt lo formatee como
ese. ¿Podría ejecutar rustfmt y publicarlo de nuevo aquí?

@ivandardi @mehcode ¿Podrías hacer eso? No sé cómo puedo formatear la sintaxis .await . Acabo de copiar el código. ¡Gracias!

Me gustaría agregar que este ejemplo muestra:

  1. El código de producción no solo será cadenas simples y agradables como:
client.get("https://my_api").send().await?.json().await?
  1. Las personas pueden abusar o abusar del encadenamiento.
let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Aquí, el cierre asíncrono maneja cada ID, no tiene nada que ver con el control de nivel superior futures_unordered . Ponerlos juntos reduce significativamente su capacidad para comprenderlo.

Todo _was_ pasó por rustfmt desde mi publicación (con algunas modificaciones menores para que se compile). Aún no se ha decidido dónde se coloca el .await? y actualmente lo coloco al final de la línea que se espera.


Ahora estoy de acuerdo en que todo parece bastante horrible. Este es un código escrito en una fecha límite y es probable que las cosas se vean horribles cuando tienes una sala de cocineros tratando de sacar algo.

Quiero señalar (desde su punto) que abusar del prefijo puede verse mucho peor (en mi opinión, por supuesto):

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

Ahora divirtámonos y hagámoslo mucho mejor usando retrospectiva y algunos nuevos adaptadores en futuros v0.3

Prefijo con precedencia "obvia" (y azúcar)
let responses: Vec<Response> = await? stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect();
Prefijo con precedencia "útil"
let responses: Vec<Response> = await stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect()?;
Prefijo con delimitadores obligatorios
let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;
Campo de sufijo
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect().await?;
Palabra clave de sufijo
let responses: Vec<Response> = stream::iter(all_ids)
    .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
    .and_then(|mut res| res.json().err_into())
    .try_buffer_unordered(10)
    .try_collect() await?;

Liendres menores aquí. No hay TryStreamExt::and_then , probablemente debería haberlo. Suena como un PR fácil para cualquiera que tenga tiempo y quiera contribuir.


  • Quiero, nuevamente, expresar mi fuerte disgusto por await? En cadenas largas pierdo por completo la pista de los ? que he llegado a buscar al final de las expresiones para significar que esta expresión es falible y puede _salir de la función_.

  • Además, quiero expresar mi creciente aversión por await .... ? (precedencia útil) y considerar lo que sucedería si tuviéramos un fn foo() -> Result<impl Future<Output = Result<_>>>

    // Is this an error? Does`await .. ?` bind outer-most to inner?
    await foo()??
    

Quiero señalar (desde su punto) que abusar del prefijo puede verse mucho peor (en mi opinión, por supuesto):

let responses: Vec<Response> = await!(futures_unordered(all_ids.into_iter().map(async move |id| {
    Ok(await!(await!(self__
        .request(URL, Method::GET, vec![("info".into(), id.into())]))?
        .json())?)
}))
.collect())?;

Esto no es realmente una preocupación porque en Python javascript, es más probable que las personas los escriban en líneas separadas. En realidad, no he visto await (await f) en Python.

Prefijo con delimitadores obligatorios

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

Esto también parece volver al uso de combinadores. Mientras que el objetivo de introducir async / await es reducir su uso cuando sea apropiado.

Bueno, ese es el objetivo de tener postfix esperando. Dado que se trata de Rust, es menos probable que las personas los escriban en líneas separadas, ya que Rust fomenta el encadenamiento. Y para eso, la sintaxis postfix es esencialmente obligatoria para que el flujo de instrucciones siga el mismo flujo de lectura de línea. Si no tenemos la sintaxis postfix, entonces habrá mucho código con temporales que también están encadenados, mientras que si tuviéramos postfix await, todo podría reducirse a una sola cadena.

@ivandardi @mehcode Copiando de Rust RFC en async / await:

Después de ganar experiencia y comentarios de los usuarios con el ecosistema basado en el futuro, descubrimos ciertos desafíos ergonómicos. Usar el estado que debe compartirse entre los puntos de espera era extremadamente poco ergonómico, ya que requería arcos o encadenamiento de uniones, y aunque los combinadores a menudo eran más ergonómicos que escribir un futuro manualmente, a menudo conducían a conjuntos desordenados de devoluciones de llamada encadenadas y anidadas.

... use con un azúcar sintáctico que se ha vuelto común en muchos idiomas con async IO: las palabras clave async y await.

Desde la perspectiva del usuario, pueden usar async / await como si fuera un código sincrónico, y solo necesitan anotar sus funciones y llamadas.

Por lo tanto, el objetivo de introducir async await es reducir el encadenamiento y hacer que el código async sea como si estuvieran sincronizados . El encadenamiento solo se menciona dos veces en este RFC, "requiriendo arcos o encadenamiento de unión" y "todavía a menudo conduce a conjuntos desordenados de devoluciones de llamada encadenadas y anidadas". No me suena demasiado positivo.

Argumentar a favor del encadenamiento y, por lo tanto, la palabra clave postfix posiblemente necesite una reescritura importante de este RFC.

@tajimaha No ha entendido bien el RFC. Se trata específicamente de los combinadores futuros ( map , and_then , etc.), no se habla de encadenamiento en general (por ejemplo, métodos que devuelven impl Future ).

Creo que es seguro decir que los métodos async serán bastante comunes, por lo que el encadenamiento es bastante importante.

Además, malinterpreta el proceso: no es necesario reescribir el RFC. El RFC es un punto de partida, pero no es una especificación. Ninguno de los RFC está escrito en piedra (¡ni debería serlo!)

El proceso RFC es fluido, no es tan rígido como está insinuando. Nada en el RFC nos impide discutir el sufijo await .

Además, los cambios se incluirán en el RFC de estabilización , no en el RFC original (que ya ha sido aceptado y, por lo tanto, no se modificará).

Argumentar el encadenamiento y, por lo tanto, la palabra clave postfix posiblemente necesite una reescritura importante de este RFC.

Estoy bromeando aquí.

Una cosa es probablemente cierta al escribir el RFC. La gente quería específicamente una nueva herramienta "que pueda usar async / await como si fuera un código sincrónico". La tradición siguiendo la sintaxis cumpliría mejor esta promesa.
Y aquí lo ven, no son los futuros combinadores,

let responses: Vec<Response> =
    futures_unordered(all_ids.into_iter().map(async move |id| {
        Ok(self__
            .request(URL, Method::GET, vec![("info".into(), id.into())]).await?
            .json().await?)
    }))
    .collect().await?;

Sin embargo, "a menudo conducían a conjuntos desordenados de devoluciones de llamada encadenadas y anidadas".

La gente quería específicamente una nueva herramienta "que pueda usar async / await como si fuera un código sincrónico". La tradición siguiendo la sintaxis cumpliría mejor esta promesa.

No veo cómo eso es cierto: tanto el prefijo como el sufijo await cumplen ese deseo.

De hecho, postfix await probablemente cumpla mejor ese deseo, porque es muy natural con cadenas de métodos (que son muy comunes en el código Rust sincrónico).

El uso del prefijo await fomenta en gran medida muchas variables temporales, que a menudo no es el estilo idiomático de Rust.

Sin embargo, "a menudo conducían a conjuntos desordenados de devoluciones de llamada encadenadas y anidadas".

Veo exactamente un cierre, y no es una devolución de llamada, solo está llamando map en un Iterator (¡nada en absoluto que ver con Futuros!)

Por favor, no intente torcer las palabras del RFC para justificar el prefijo await .

Usar el RFC para justificar el prefijo await es muy extraño, porque el RFC mismo dice que la sintaxis no es definitiva y se decidirá más adelante. El momento de esa decisión es ahora.

La decisión se tomará en base a los méritos de las diversas propuestas, el RFC original es completamente irrelevante (excepto como una referencia histórica útil).

Tenga en cuenta que el último ejemplo de @ mehcode utiliza principalmente combinadores _stream_ (y el único combinador futuro podría ser reemplazado trivialmente por un bloque asincrónico). Esto es equivalente a usar combinadores de iteradores en código síncrono, por lo que se puede usar en algunas situaciones cuando son más apropiados que los bucles.

Esto es fuera de tema, pero la mayor parte de la conversación aquí la tienen una docena de comentaristas. De los 383 comentarios en el momento en que eliminé este problema, solo había 88 carteles únicos. En un esfuerzo por evitar quemar / sobrecargar a cualquiera que tenga que ir a leer estos comentarios, recomendaría ser lo más completo posible en sus comentarios y asegurarse de que no sea una reiteración de un punto anterior.


Histograma de los comentarios

HeroicKatora:(32)********************************
Centril:(22)**********************
ivandardi:(21)*********************
I60R:(21)*********************
Pzixel:(16)****************
novacrazy:(15)***************
scottmcm:(13)*************
EyeOfPython:(11)***********
mehcode:(11)***********
Pauan:(10)**********
XX:(9)*********
nicoburns:(9)*********
tajimaha:(9)*********
skade:(8)********
CAD97:(8)********
Laaas:(8)********
dpc:(8)********
ejmahler:(7)*******
Nemo157:(7)*******
yazaddaruvala:(6)******
traviscross:(6)******
CryZe:(6)******
Matthias247:(5)*****
dowchris97:(5)*****
rolandsteiner:(5)*****
earthengine:(5)*****
H2CO3:(5)*****
eugene2k:(5)*****
jplatte:(4)****
lnicola:(4)****
andreytkachenko:(4)****
cenwangumass:(4)****
richardanaya:(4)****
chpio:(3)***
joshtriplett:(3)***
phaylon:(3)***
phaazon:(3)***
ben0x539:(2)**
newpavlov:(2)**
comex:(2)**
DDOtten:(2)**
withoutboats:(2)**
valff:(2)**
darkwater:(2)**
tanriol:(1)*
liigo:(1)*
yasammez:(1)*
mitsuhiko:(1)*
mokeyish:(1)*
unraised:(1)*
mzji:(1)*
swfsql:(1)*
spacekookie:(1)*
sgrif:(1)*
nikonthethird:(1)*
edwin-durai:(1)*
norcalli:(1)*
quodlibetor:(1)*
chescock:(1)*
BenoitZugmeyer:(1)*
F001:(1)*
FuGangqiang:(1)*
Keruspe:(1)*
LegNeato:(1)*
MSleepyPanda:(1)*
SamuelMoriarty:(1)*
Swoorup:(1)*
Uristqwerty:(1)*
alexmaco:(1)*
arabidopsis:(1)*
arielb1:(1)*
axelf4:(1)*
casey:(1)*
lholden:(1)*
cramertj:(1)*
crlf0710:(1)*
davidtwco:(1)*
dyxushuai:(1)*
eaglgenes101:(1)*
AaronFriel:(1)*
gralpli:(1)*
huxi:(1)*
ian-p-cooke:(1)*
jonimake:(1)*
josalhor:(1)*
jsdw:(1)*
kjetilkjeka:(1)*
kvinwang:(1)*

Tenga en cuenta que el último ejemplo de @ mehcode utiliza principalmente combinadores _stream_ (y el único combinador futuro podría ser reemplazado trivialmente por un bloque asincrónico). Esto es equivalente a usar combinadores de iteradores en código síncrono, por lo que se puede usar en algunas situaciones cuando son más apropiados que los bucles.

Lo mismo se puede argumentar aquí que puedo / debería usar el prefijo await donde sea más apropiado que el encadenamiento.

@Pauan Aparentemente no se trata solo de torcer palabras. Estoy mostrando un problema de código real, escrito por un partidario de sintaxis postfix. Y como dije, el código de estilo de prefijo ilustra mejor su intención, aunque no necesariamente tiene muchos temporales, como se quejan los partidarios de postfix (al menos en este caso). también, suponga que su código tiene una cadena de una línea con dos esperas, ¿cómo puedo depurar la primera? (esta es una pregunta verdadera y no lo sé).
En segundo lugar, la comunidad de rust se está volviendo más grande, las personas de diversos orígenes (como yo, soy el que más uso python / c / java) no estarán de acuerdo en que las cadenas de métodos son las mejores formas de hacer las cosas. Espero que al tomar una decisión, no se base (no debería ser) solo en el punto de vista de los primeros usuarios.

@tajimaha El mayor cambio de claridad de la corrección posterior al prefijo parece ser el uso de un cierre local para eliminar algunos argumentos de función anidados. Esto no me parece exclusivo del prefijo, son bastante ortogonales. Puedes hacer lo mismo con postfix, y creo que es aún más claro. Estoy de acuerdo en que tal vez sea un mal uso del encadenamiento para algunas bases de código, pero no veo cómo este mal uso es único o está conectado a postfix de una manera importante.

let get_one_id = async move |id| {
    self.request(URL, Method::GET, vec![("info".into(), id.into())])
        .await?
        .json().await
};

let responses: Vec<Response> = futures_unordered(all_ids.into_iter().map(get_one_id))
    .collect().await?;

Pero, en el sufijo, el let -binding y el Ok en el resultado pueden eliminarse juntos los últimos ? para proporcionar directamente el resultado y luego el bloque de código también es innecesario dependiendo por gusto personal. Esto no funciona bien en el prefijo debido a dos esperas en la misma declaración.

No entiendo el sentimiento expresado regularmente que permite que los enlaces sean unidiomáticos en el código de Rust. Son bastante frecuentes y comunes en los ejemplos de código, especialmente en el manejo de resultados. Rara vez veo más de 2 ? en el código con el que trato.

Además, lo que es idiomatic cambia a lo largo de la vida útil de un idioma, por lo que tendría mucho cuidado al usarlo como argumento.

No sé si se ha sugerido algo como esto antes, pero ¿podría aplicarse una palabra clave del prefijo await a una expresión completa? Tomando el ejemplo que se ha mencionado antes:

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

donde get , send y json son asíncronos.

Para mí (tengo poca experiencia asíncrona en otros lenguajes de programación) el sufijo expr await parece natural: "Haz esto, _entonces_ espera el resultado"

Hubo algunas preocupaciones de que los siguientes ejemplos parezcan extraños:

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

Sin embargo, yo diría que esto debería dividirse en varias líneas:

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

Esto es mucho más claro y tiene la ventaja adicional de que el await es fácil de detectar, si siempre está al final de la línea.

En un IDE, esta sintaxis carece del "poder del punto", pero sigue siendo mejor que la versión de prefijo: cuando escribe el punto y luego nota que necesita await , solo tiene que eliminar el punto y escriba " await ". Es decir, si el IDE no ofrece autocompletar para palabras clave.

La sintaxis de punto expr.await es confusa porque ninguna otra palabra clave de flujo de control utiliza un punto.

Creo que el problema es que aunque tenemos encadenamiento, que a veces puede ser bonito, no deberíamos ir al extremo diciendo que todo debería hacerse encadenando. También deberíamos proporcionar herramientas para la programación de estilo C o Python. Aunque Python casi no tiene ningún componente de encadenamiento allí, su código a menudo se elogia por ser legible. Los programadores de Python tampoco se quejan de que tenemos demasiadas variables temporales.

¿Qué tal un sufijo then ?

No entiendo el sentimiento expresado regularmente que permite que los enlaces sean unidiomáticos en el código de Rust. Son bastante frecuentes y comunes en los ejemplos de código, especialmente en el manejo de resultados. Rara vez veo más de 2 ? en el código con el que trato.

Además, lo que es idiomatic cambia a lo largo de la vida útil de un idioma, por lo que tendría mucho cuidado al usarlo como argumento.

Esto me inspiró a examinar el código actual de Rust donde hay dos o más ? en una línea (puede ser que alguien pueda examinar el uso de varias líneas). Entrevisté a xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, tejo, Rocket, exa, iron, parity-ethereum, tikv. Estos son proyectos de Rust con la mayoría de las estrellas.

Lo que encuentro es que aproximadamente solo 40 líneas de un total de ? en una línea. Eso es 0,006% .

También quiero señalar que el estudio de los patrones de uso de código existentes no revelará la experiencia del usuario al escribir código nuevo.

Suponga que le dan un trabajo para interactuar con una nueva API o es nuevo en el uso de solicitudes. ¿Es probable que escribas

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

en una sola toma ? Si es nuevo en la API, dudo que desee asegurarse de construir la solicitud correctamente, ver el estado de devolución, verificar su suposición sobre lo que devuelve esta API o simplemente jugar con la API de esta manera:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

Una API de red no se parece en nada a los datos de la memoria, no sabes qué hay allí. Esto es bastante natural para la creación de prototipos. Y, cuando está creando prototipos, se preocupa de que todo salga bien, NO de muchas variables temporales . Puede decir que podemos usar la sintaxis postfix como:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = request.send().await?;
dbg!(response);
let data = response.json().await?;
dbg!(data);

Pero, si ya tiene esto:

let request = client.get("https://my_api").header("k", "v");
dbg!(request);
let response = await(request.send())?;
dbg!(response);
let data = await(response.json())?;
dbg!(data);

Todo lo que tiene que hacer es probablemente envolverlo en una función y su trabajo está hecho, el encadenamiento ni siquiera surge en este proceso.

Lo que encuentro es que aproximadamente solo alrededor de 40 líneas de un total de 585562 líneas usan dos o más. en una línea.

Me gustaría sugerir que esta no es una medida útil. Lo que realmente importa es más de un operador de flujo de control _ por expresión_. Según el estilo típico (rustfmt), casi siempre terminarán en diferentes líneas en el archivo aunque pertenezcan a la misma expresión y, por lo tanto, estarían encadenadas en la cantidad que el sufijo (teóricamente) importa por await .

no debemos ir al extremo diciendo que todo debe hacerse encadenando

¿Alguien ha dicho que todo _debería_ hacerse con encadenamiento? Todo lo que he visto hasta ahora es que debería ser _ergonómico_ encadenar en los casos en que tenga sentido, lo mismo que sucede en el código síncrono.

sólo alrededor de 40 líneas de 585562 líneas totales utilizan dos o más? en una línea.

No estoy seguro de que sea relevante para prefijo vs postfijo. Notaré que _ninguno_ de mis ejemplos de C # de querer sufijo incluyó múltiples await s en una línea, ni siquiera múltiples await s en una declaración. Y el ejemplo de @Centril de diseño de postfijo potencial tampoco puso múltiples await s en una línea.

Una mejor comparación podría ser con cosas encadenadas de ? , como estos ejemplos del compilador:

Ok(&self.get_bytes(cx, ptr, size_with_null)?[..size])
self.try_to_scalar()?.to_ptr().ok()
let idx = decoder.read_u32()? as usize;
.extend(self.at(cause, param_env).eq(v1, v2)?.into_obligations());
for line in BufReader::new(File::open(path)?).lines() {

Editar: Parece que me ganaste esta vez , @ CAD97 :

Esto es sorprendentemente similar al código de promesa de javascipt con una gran cantidad de then s. Yo no llamaría a esto sincrónico. Es casi seguro que el encadenamiento tenga un await y pretenda ser sincrónico.

Prefijo con delimitadores obligatorios

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

@ CAD97 @scottmcm Buen punto. Sabía que hay una limitación para lo que estoy midiendo:

(puede ser que alguien pueda evaluar el uso de varias líneas)

Estoy haciendo esto porque @skade mencionó la similitud entre await y ? , así que hice un análisis rápido. Estoy proporcionando una idea sin hacer una investigación seria. Creo que un análisis más detallado sería difícil de hacer mirando el código, ¿verdad? Es posible que necesite analizar e identificar expresiones, con las que no estoy familiarizado. Espero que alguien pueda hacer este análisis.

¿Alguien ha dicho que todo debería hacerse con encadenamiento? Todo lo que he visto hasta ahora es que debería ser ergonómico encadenar en los casos en que tenga sentido, lo mismo que sucede en el código síncrono.

Lo que estoy diciendo es que si solo se agrega postfix, no sería ergonómico cuando el estilo C / Python se ve bien (por supuesto, en mi opinión). También me refiero a que el encadenamiento puede no ser necesario cuando crea un prototipo .

Tengo la sensación de que la dirección de este hilo está demasiado centrada en el encadenamiento excesivo y en cómo hacer que el código de espera sea lo más conciso posible.

En su lugar, quiero animar a todos a que vean en qué se diferencia el código asíncrono de las variantes sincrónicas y cómo esto influirá en el uso y la utilización de recursos. Menos de 5 comentarios en los 400 en este hilo mencionan esas diferencias. Si no está al tanto de estas diferencias, tome la versión nocturna actual e intente escribir una parte decente de código asíncrono. Incluyendo el intento de obtener una versión asincrónica / en espera idiomática (no combinatoria) del fragmento de compilación del código que se analiza en las últimas 20 publicaciones.

Marcará la diferencia si las cosas existen puramente entre los puntos de rendimiento / espera, si las referencias persisten en los puntos de espera y, a menudo, algunos requisitos peculiares sobre los futuros hacen que no sea posible escribir código tan conciso como se imagina en este hilo. Por ejemplo, no podemos poner funciones asíncronas arbitrarias en combinadores arbitrarios, porque es posible que no funcionen con los tipos !Unpin . Si creamos futuros a partir de bloques asíncronos, es posible que no sean directamente compatibles con combinadores como join! o select! , porque necesitan tipos fijados y fusionados, por lo que llamadas adicionales a pin_mut! y .fuse() puede ser necesario dentro de.

Además, para trabajar con bloques asíncronos, las nuevas utilidades basadas en macros funcionan join! y select! funcionan mucho mejor que las antiguas variantes del combinador. Y estos son de la manera excesiva que a menudo se proporciona aquí como ejemplos.

No sé cómo postfix await puede funcionar con .unwrap() en este ejemplo de tokio más simple

let response = await!({
    client.get(uri)
        .timeout(Duration::from_secs(10))
}).unwrap();

Si se adopta el prefijo, se convertirá en

let response = await {
    client.get(uri).timeout(Duration::from_secs(10))
}.unwrap();

Pero si se adopta el sufijo,

client.get(uri).timeout(Duration::from_secs(10)).await.unwrap()
client.get(uri).timeout(Duration::from_secs(10)) await.unwrap()

¿Existe alguna explicación intuitiva que podamos dar a los usuarios? Entra en conflicto con las reglas existentes. await es un campo? o await es un enlace que tiene un método llamado unwrap() ? ¡DEMASIADO! Desenvolvemos mucho cuando iniciamos un proyecto. Violaron múltiples reglas de diseño en The Zen of Python.

Los casos especiales no son lo suficientemente especiales como para romper las reglas.
Si la implementación es difícil de explicar, es una mala idea.
Ante la ambigüedad, rechace la tentación de adivinar.

¿Existe alguna explicación intuitiva que podamos dar a los usuarios? Entra en conflicto con las reglas existentes. await es un campo? o await es un enlace que tiene un método llamado unwrap() ? ¡DEMASIADO! Desenvolvemos mucho cuando iniciamos un proyecto. Violaron múltiples reglas de diseño en The Zen of Python.

Yo diría que, aunque hay demasiados documentos en docs.rs llaman unwrap , unwrap deben reemplazarse con ? en muchos casos del mundo real. Al menos, esta es mi práctica.

Lo que encuentro es que aproximadamente solo alrededor de 40 líneas de un total de 585562 líneas usan dos o más. en una línea.

Me gustaría sugerir que esta no es una medida útil. Lo que realmente importa es más de un operador de flujo de control _ por expresión_. Según el estilo típico (rustfmt), casi siempre terminarán en diferentes líneas en el archivo aunque pertenezcan a la misma expresión y, por lo tanto, estarían encadenadas en la cantidad que el sufijo (teóricamente) importa por await .

Renuncio a que pueda haber un límite para este enfoque.

Volví a realizar una encuesta para xi-editor, alacritty, ripgrep, bat, xray, fd, firecracker, tejo, Rocket, exa, iron, parity-ethereum, tikv. Estos son proyectos de Rust con la mayoría de las estrellas. Esta vez busqué patrón:

xxx
  .f1()
  .f2()
  .f3()
  ...

y si hay varios operadores de flujo de control en estas expresiones.

Identifiqué que SOLO 15 de las 7066 cadenas tienen múltiples operadores de flujo de control. Eso es 0,2% . Estas líneas abarcan 167 de 585562 líneas de código. Eso es 0.03% .

@cenwangumass Gracias por tomarse el tiempo y cuantificar. :corazón:

Una consideración es que dado que Rust tiene un enlace variable con let, puede ser un argumento convincente para el prefijo await , ya que si se usa de manera consistente, tendría una línea de código separada para cada await await -punto. La ventaja es doble: los seguimientos de pila, ya que el número de línea brinda un mayor contexto sobre dónde ocurrió el problema, y ​​la facilidad de depuración del punto de interrupción, ya que es común desear establecer un punto de interrupción en cada punto de espera separado para inspeccionar variables, puede superar la brevedad de una sola línea de código.

Personalmente, estoy dividido entre el estilo de prefijo y el sigilo de postfijo, aunque después de leer https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 Probablemente estoy mayormente a favor de un sigilo de postfijo.

Estilo de prefijo renderizado:

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

    Ok(user)
}

Estilo postfix renderizado:

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

    Ok(user)
}

Sigilo postfijo renderizado @:

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

    Ok(user)
}

Sigilo posfijo renderizado #:

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

    Ok(user)
}

No he visto suficiente gente hablar de await por Stream s. Si bien está fuera de alcance, tomar la decisión alrededor de await con un poco de previsión de Stream podría valer la pena.

Probablemente necesitemos algo como esto:
Sintaxis de prefijo

for await response in stream {
    let response = response?;
    ...
}

// In which case an `await?` variant might be beneficial
for await? response in stream {
    ...
}

Sintaxis de sufijo

for response in stream await {
    let response = response?;
    ...
}
for response in stream.await!() {
    let response = response?;
    ...
}

// Or a specialized variant of `await` and `?`
//     Note (Not Obvious): The `?` actually applies to each response of `await`
for response in stream await? {
    ...
}
for response in stream.await!()? {
    ...
}

Posiblemente la sintaxis más ergonómica / consistente

let results: Vec<Result<_, _>> = ...;
for value in? results {
    ...
}
for response await? stream {
    ...
}

Solo quiero asegurarme de que estos ejemplos se discutan al menos un poco, porque si bien postfix es bueno para encadenar Future , a primera vista parece el menos intuitivo para Stream . No estoy seguro de cuál es la solución correcta aquí. ¿Quizás postfix await por Future y un estilo diferente de await por Stream ? Pero para diferentes sintaxis necesitaríamos asegurarnos de que el análisis no sea ambiguo.

Todos estos son solo mis pensamientos iniciales, podría valer la pena pensar más en el caso de uso Stream desde una perspectiva postfix. ¿Alguien tiene alguna idea sobre cómo postfix podría funcionar bien con Stream , o si deberíamos tener dos sintaxis?

La sintaxis para la iteración de la transmisión no es algo que suceda durante bastante tiempo, me imagino. Sin embargo, es bastante fácil usar un bucle while let y hacerlo manualmente:

Campo de sufijo
while let Some(value) = stream.try_next().await? {
}
Prefijo con precedencia "consistente" y azúcar
while let Some(value) = await? stream.try_next() {
}
Prefijo con delimitadores obligatorios
while let Some(value) = await { stream.try_next() }? {
}

Sería la forma actual de hacer las cosas (más await ).


Notaré que este ejemplo hace que el "prefijo con delimitadores" se vea especialmente mal para mí. Si bien await(...)? podría verse un poco mejor, si tuviéramos que hacer un "prefijo con delimitadores", espero que solo permitamos _un_ tipo de delimitador (como try { ... } ).

Tangente rápida, pero "esperar con delimitadores" no sería simplemente ... ¿prefijo normal aguardar? await espera una expresión, por lo que tener await expr y await { expr } son esencialmente lo mismo. No tendría sentido esperar solo con delimitadores y no tenerlo sin ellos también, especialmente porque cualquier expresión puede estar rodeada por {} y seguir siendo la misma expresión.

La pregunta sobre los flujos me llevó a pensar que para Rust podría ser bastante natural tener esperando disponible no solo como operador en expresiones, sino también como modificador en patrones:

// These two lines mean the same - both await the future
let x = await my_future;
let async x = my_future;

que luego puede funcionar naturalmente con for

for async x in my_stream { ... }

@ivandardi

El problema que resuelve el "prefijo en espera con delimitadores obligatorios" es la pregunta de precedencia en torno a ? . Con los delimitadores _ mandatorios_, no hay preguntas de precedencia.

Vea try { .... } para la sintaxis _similar_ (estable). Try tiene delimitadores obligatorios por la misma razón, cómo interactúa con ? , ya que un ? dentro de las llaves es muy diferente a uno exterior.

@yazaddaruvala No creo que deba haber una forma asincrónica específica de manejar un bucle for con resultados más agradables, que debería provenir de alguna característica genérica que también se ocupa de Iterator<Item = Result<...>> .

// postfix syntax
for response in stream await {
    ...
}

Esto implica que stream: impl Future<Output = Iterator> y está esperando que el iterador esté disponible, no cada elemento. No veo muchas posibilidades de algo mejor que async for item in stream (sin una característica más general como menciona @tanriol ).


@ivandardi, la diferencia es la precedencia una vez que comienzas a encadenar al final de los delimitadores, aquí hay dos ejemplos que analizan de manera diferente con "prefijo de precedencia obvio", "prefijo de precedencia útil" (al menos mi comprensión de lo que es la precedencia "útil") y "prefijo de delimitadores obligatorios".

await (foo.bar()).baz()?;
await { let foo = quux(); foo.bar() }.baz()?;

que analizar (con suficientes delimitadores para que no sean ambiguos para las tres variantes)

// obvious precedence prefix
await ((foo.bar()).baz()?);
await ({ let foo = quux(); foo.bar() }.baz()?);

// useful precedence prefix
(await ((foo.bar()).baz()))?;
(await ({ let foo = quux(); foo.bar() }.baz())?;

// mandatory delimiters prefix
(await (foo.bar())).baz()?;
(await { let foo = quux(); foo.bar() }).baz()?;

En mi opinión, los ejemplos de @scottmcm de C # en https://github.com/rust-lang/rust/issues/57640#issuecomment -457457727 se ven bien con los delimitadores movidos de la (await foo).bar a la await(foo).bar :

await(response.Content.ReadAsStringAsync()).Should().Be(text);
var previous = await(branch.ListHistoryAsync(timestampUtc, null, cancellationToken, 1)).HistoryEntries.SingleOrDefault();

Etcétera. Este diseño es familiar para las llamadas a funciones normales, para el flujo de control existente basado en palabras clave y para otros lenguajes (incluido C #). Esto evita romper el presupuesto de extrañeza y no parece causar ningún problema para el encadenamiento posterior a await .

Aunque es cierto que tiene el mismo problema que try! para el encadenamiento múltiple de await , ¿no parece ser tan importante como lo fue para Result ? Preferiría limitar la extrañeza de la sintaxis aquí que acomodar un patrón relativamente poco común y posiblemente ilegible.

y familiarizado con otros lenguajes (incluido C #)

Así no es como funciona la precedencia en C # (o Javascript o Python)

await(response.Content.ReadAsStringAsync()).Should().Be(text);

es equivalente a

var future = (response.Content.ReadAsStringAsync()).Should().Be(text);
await future;

(el operador de punto tiene una precedencia más alta que await , por lo que se vincula con más fuerza incluso si intenta hacer que await parezca una llamada de función).

Lo sé, esa no es la afirmación que estaba haciendo. Simplemente que el orden sigue siendo el mismo, por lo que lo único que las personas necesitan agregar son paréntesis, y que (por separado) la sintaxis de llamada de función existente tiene la precedencia correcta.

Este diseño es [...] familiar del flujo de control existente basado en palabras clave

Estoy en desacuerdo. De hecho, advertimos contra el uso de ese diseño en el flujo de control existente basado en palabras clave:

warning: unnecessary parentheses around `return` value
 --> src/lib.rs:2:9
  |
2 |   return(4);
  |         ^^^ help: remove these parentheses
  |
  = note: #[warn(unused_parens)] on by default

Y rustfmt cambia eso a return (4); , _agregando en_ un espacio.

Eso realmente no afecta el punto que estoy tratando de hacer, y se siente como si estuviera partiéndose los pelos. return (4) , return(4) , no es más que un problema de estilo.

Leí todo el número y comencé a sentir que {} llaves y prefijo están bien, pero luego desaparecieron

Veamos:

let foo = await some_future();
let bar = await {other_future()}?.bar

Se ve bien, ¿no? Pero, ¿qué pasa si queremos encadenar más await en una cadena?

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

En mi humilde opinión, se ve mucho peor que

let foo = some_future() await;
let bar = other_future() await?
           .bar_async() await?;

Sin embargo, dicho esto, no creo en el encadenamiento asíncrono, como está escrito en la publicación de ejemplo de uso de async/await en hiper servidor, por favor, muestre dónde podría ser útil el encadenamiento usando este ejemplo concreto. Estoy realmente tentado, sin bromas.


Acerca de los ejemplos anteriores, la mayoría de ellos están "corrompidos" de alguna manera por los combinadores. Casi nunca los necesitas ya que te esperan. P.ej

let responses: Vec<Response> = await {
    stream::iter(all_ids)
        .then(|id| self.request(URL, Method::GET, vec![("info".into(), id.into())]))
        .and_then(|mut res| res.json().err_into())
        .try_buffer_unordered(10)
        .try_collect()
}?;

es solo

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())]) await?;
      Ok(res.json() await?)
   })
   .join_all() await
   .collect()?

si los futuros pueden devolver un error es tan simple como:

let responses: Vec<Response> = all_ids
   .map(async |id|  {
      let response = self.request(URL, Method::GET, vec![("info".into(), id.into())])? await?;
      Ok(res.json()? await?)
   })
   .join_all()? await
   .collect()?

@lnicola , @nicoburns ,

He creado un hilo Pre-RFC para la sintaxis val.[await future] en https://internals.rust-lang.org/t/pre-rfc-extended-dot-operator-as-possible-syntax-for-await -encadenamiento / 9304

Las preguntas de procedimiento de https://internals.rust-lang.org

Creo que tenemos dos campos en esta discusión: las personas a las que les gustaría enfatizar los puntos de rendimiento frente a las personas a las que les gustaría restarles importancia.

Async en Rust, alrededor de 2018 contiene la siguiente propaganda:

La notación asincrónica / en espera es una forma de hacer que la programación asincrónica se parezca más a la programación sincrónica.

Tomando el ejemplo simple de tokio de arriba (cambiando unwrap() a ? ):

let response = await!({
    client.get(uri).timeout(Duration::from_secs(10))
})?;

y la aplicación del sigilo postfix conduce a

let response = client.get(uri).timeout(Duration::from_secs(10))!?

que se parece mucho a la programación síncrona y carece de la notación de prefijo y sufijo await intercalados que distraigan.

Usé ! como sigilo postfix en este ejemplo, aunque esa sugerencia me dio algunos votos negativos en un comentario anterior. Lo hice porque el signo de exclamación tiene un significado inherente para mí en este contexto que tanto @ (que mi cerebro lee como "en") y # carecen. Pero eso es simplemente una cuestión de gustos y no es mi punto.

Simplemente preferiría cualquier sigilo de postfijo de un solo carácter sobre todas las demás alternativas precisamente porque es muy discreto, lo que facilita el control de lo que el código que está leyendo está haciendo en realidad frente a si es o no asincrónico, lo cual Consideraría un detalle de implementación. No diría que no es importante en absoluto, pero diría que es mucho menos importante para el flujo del código que la devolución anticipada en el caso de ? .

Para decirlo de otra manera: usted se preocupa principalmente por los puntos de rendimiento cuando escribe código asincrónico, no tanto mientras lo lee. Dado que el código se lee con más frecuencia que se escribe, una sintaxis discreta para await sería útil.

Me gustaría recordar la experiencia del equipo de C # (los énfasis son míos):

Esta es también la razón por la que no usamos 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 nos lamentamos muchos años después de que algo sea excesivamente detallado . Algunas funciones 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

Los personajes de Sigil son casi invisibles sin el resaltado adecuado y son menos amigables para los recién llegados (como probablemente dije antes).


Pero hablando de sigilos, @ probablemente no confundirá a los que no hablan inglés (por ejemplo, a mí) porque no lo leemos como at . Tenemos un nombre completamente diferente para él que no se activa automáticamente cuando lee el código, por lo que simplemente lo entiende como un jeroglífico completo, con su propio significado.

@huxi Tengo la sensación de que los sigilos ya están bastante descartados. Consulte la publicación de Centril nuevamente para conocer las razones por las que deberíamos usar la palabra await .


Además, para todos aquellos a los que no les guste el postfix, espere mucho, y use el argumento pragmático de "bueno, no hay mucho código de la vida real que pueda encadenarse, por lo que no se debe agregar postfix await", aquí hay una pequeña anécdota (que probablemente mataré por mala memoria):

En la Segunda Guerra Mundial, una de las naciones en lucha estaba perdiendo muchos aviones. Tuvieron que reforzar sus aviones de alguna manera. Entonces, la forma obvia de saber dónde deben enfocarse es mirar los aviones que regresaron y ver dónde impactan más las balas. Resulta que en un avión, un promedio del 70% de los agujeros estaban en las alas, el 10% en el área del motor y el 20% en otras áreas del avión. Entonces, con esas estadísticas, tendría sentido reforzar las alas, ¿verdad? ¡Incorrecto! La razón de eso es que solo estás mirando los aviones que regresaron. Y en esos aviones, parece que el daño de las balas en las alas no es tan malo. Sin embargo, todos los aviones que regresaron solo sufrieron daños menores en el área del motor, lo que puede llevar a la conclusión de que un daño importante en el área del motor es fatal. Por tanto, la zona del motor debería reforzarse.

Mi punto con esta anécdota es: tal vez no hay muchos ejemplos de código de la vida real que puedan aprovechar postfix await porque no hay postfix await en otros idiomas. Así que todo el mundo está acostumbrado a escribir código de espera de prefijo y está muy acostumbrado a él, pero nunca sabremos si la gente empezaría a encadenar espera más si tuviéramos postfix espera.

Así que creo que el mejor curso de acción sería aprovechar la flexibilidad que nos brindan las compilaciones nocturnas y elegir una sintaxis de prefijo y una sintaxis de postfijo para agregar al lenguaje. Voto por el prefijo "Precedencia obvia" aguarda, y el sufijo .await aguarda. Estas no son las opciones de sintaxis finales, pero debemos elegir una para empezar, y creo que esas dos opciones de sintaxis proporcionarían una experiencia más nueva en comparación con otras opciones de sintaxis. Después de implementarlas todas las noches, podemos obtener estadísticas de uso y opiniones sobre cómo trabajar con código real usando ambas opciones y luego podemos continuar con la discusión de la sintaxis, esta vez respaldada con un argumento mejor pragmático.

@ivandardi La anécdota es bastante contundente y en mi humilde opinión no encaja: es una historia motivadora al comienzo de un viaje, para recordarle a la gente que busque lo que no es obvio y cubra todos los ángulos, no es una para usar contra la oposición en una discusión. Era apropiado para Rust 2018, donde se planteó y no estaba vinculado a un problema específico. Usarlo contra otro lado en un debate es descortés, necesitarías afirmar la posición de la persona que ve más o tiene más visión para que funcione. No creo que esto sea lo que quieres. Además, permaneciendo en la imagen, tal vez postfix no esté en ninguno de los idiomas porque postfix nunca llegó a casa;).

La gente realmente ha medido y mirado: tenemos un operador encadenable en Rust ( ? ) que rara vez se usa para encadenar. https://github.com/rust-lang/rust/issues/57640#issuecomment -458022676

También se ha cubierto muy bien que esto es _sólo_ una medida del estado actual, como @cenwangumass ha puesto muy bien aquí: https://github.com/rust-lang/rust/issues/57640#issuecomment -457962730. Así que no es como si la gente usara esto como números finales.

Sin embargo, quiero una historia en la que el encadenamiento se convierta en un estilo medio dominante si el postfix es realmente el camino a seguir. Sin embargo, no estoy convencido de eso. También mencioné anteriormente que el ejemplo dominante que se usa aquí ( reqwest ) solo requiere 2 esperas porque la API así lo eligió y una API de cadena conveniente sin la necesidad de 2 esperas se puede construir fácilmente hoy.

Si bien aprecio la necesidad de una fase de investigación, me gustaría señalar que la espera ya se ha retrasado gravemente, cualquier fase de investigación lo empeorará. Tampoco tenemos forma de recopilar estadísticas en muchas bases de código, tendríamos que armar un conjunto de habla nosotros mismos. Me encantaría más investigación de usuarios aquí, pero eso lleva tiempo, una configuración que aún no existe y gente para ejecutarla.

@Pzixel debido a dejar

let foo = await some_future();
let bar = await {
                await {other_future()}?.bar_async()
          }?;

como

''
dejemos que foo = espere un_futuro ();
let bar = await {other_future ()} ?. bar_async ();
let bar = await {bar} ?;

@Pzixel ¿Tiene alguna fuente para esa cita de experiencia del equipo de C #? Porque el único que pude encontrar fue este comentario . Esto no pretende ser una acusación o algo así. Solo me gustaría leer el texto completo.

Mi cerebro traduce @ a "at" debido a su uso en direcciones de correo electrónico. Ese símbolo se llama "Klammeraffe" en mi idioma nativo, que se traduce aproximadamente como "mono aferrado". De hecho, aprecio que mi cerebro se haya conformado con "en" en su lugar.

Mis 2 centavos, como usuario relativamente nuevo de Rust (con experiencia en C ++, pero eso realmente no ayuda).

Algunos de ustedes mencionaron a los recién llegados, esto es lo que pienso:

  • En primer lugar, una macro await!( ... ) me parece indispensable, ya que es muy fácil de usar y no se puede malinterpretar. Parece similar a println!() , panic!() , ... y eso es lo que sucedió con try! después de todo.
  • Los delimitadores obligatorios también son simples e inequívocos.
  • una versión postfix, ya sea de campo, función o macro, no sería difícil de leer o escribir en mi humilde opinión, ya que los recién llegados simplemente dirían "ok, así es como lo haces". Este argumento también es válido para la palabra clave postfix, eso es "inusual" pero "por qué no".
  • con respecto a la notación de prefijo con precedencia útil, creo que parecería confuso. El hecho de que await vincule más estrechamente dejará boquiabiertos a algunos y creo que algunos usuarios simplemente preferirán poner muchos paréntesis para dejarlo claro (encadenado o no).
  • La precedencia obvia sin azúcar es fácil de entender y enseñar. Luego, para introducir el azúcar a su alrededor, simplemente llame a await? una palabra clave útil. Útil porque elimina la necesidad de engorrosos paréntesis:
    `` c# let response = (await http::get("https://www.rust-lang.org/"))?; // see kids? aguardan ... unwraps the future, so you have to use ? to unwrap the Result // but there is some sugar if you want, thanks to the aguardar? `Operador
    dejar respuesta = esperar? http :: get ("https://www.rust-lang.org/");
    // pero no deberías encadenar, porque esta sintaxis no conduce a un código encadenado legible
- sigils can be understood quite easily *if the chosen character makes sense* if it is introduced to be "the `?` for futures".

That being said, since no agreement seems to be reached, I think it would be reasonable to ship `await!()` to stable Rust. Then this discussion can be extended without blocking the whole process. Same that what happened for `try!()`/`?`, so again newcomers won't be lost. And if [Simple postfix macros](https://github.com/rust-lang/rfcs/pull/2442) get accepted, the problem will disappear since we'll get postfix macro for "free".

---

Just a thought, what about a postfix keyword, but which can be put as prefix as well, similar in some ways to the `const` keyword of C++? (I don't know if that was already proposed) In prefix position, it behaves like "prefix `await` with obvious precedence and optional sugar":
```c#
// preferred without chaining:
let response = await? http::get("https://www.rust-lang.org/");

// but also possible: (rustfmt warning)
let response = http::get("https://www.rust-lang.org/") await?;
let response = (http::get("https://www.rust-lang.org/") await)?;
let response = (await http::get("https://www.rust-lang.org/"))?;

// chains well
let matches = http::get("https://www.rust-lang.org/") await?
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;

// any of these are also allowed, but arguably ugly (rustfmt warning again)
let matches = await ((http::get("https://www.rust-lang.org/") await?)
    .body?
    .async_regex_search("(?=(\d+))\w+\1"));
let matches = (await? http::get("https://www.rust-lang.org/"))
    .body?
    .async_regex_search("(?=(\d+))\w+\1") await;
let matches = await http::get("https://www.rust-lang.org/") await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1");
let matches = await (await http::get("https://www.rust-lang.org/"))?
    .body?
    .async_regex_search("(?=(\d+))\w+\1");
let matches = await!(
    http::get("https://www.rust-lang.org/")) await?
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
);
let matches = await { // <-- parenthesis or braces optional here, but they clarify
    (await? http::get("https://www.rust-lang.org/"))
        .body?
        .async_regex_search("(?=(\d+))\w+\1")
};

Cómo enseñar eso:

  • ( await!() macro posible)
  • prefijo recomendado cuando no ocurre ningún encadenamiento, con azúcar (ver arriba)
  • postfix recomendado con encadenamiento
  • es posible mezclarlos, pero no se recomienda
  • posible usar prefijo al encadenar con combinadores

Por mi propia experiencia personal, diría que el prefijo await no es un problema para encadenar.
El encadenamiento se agrega mucho en el código Javascript con prefijo en espera y combinadores como f.then(x => ...) sin perder legibilidad en mi opinión y no parecen sentir ninguna necesidad de intercambiar combinadores por postfijo en espera.

Que hacer:

let ret = response.await!().json().await!().to_string();

es lo mismo que:

let ret = await future.then(|x| x.json()).map(|x| x.to_string());

Realmente no veo que los beneficios de postfix esperen por encima de las cadenas de combinador.
Me resulta más fácil entender lo que sucede en el segundo ejemplo.

No veo ningún problema de legibilidad, encadenamiento o precedencia en el siguiente código:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Yo estaría a favor de este prefijo aguardar porque:

  • de la familiaridad con otros idiomas.
  • de la alineación con las demás palabras clave (volver, romper, continuar, rendimiento, etc ...).
  • todavía se siente como Rust (con combinadores como lo hacemos con iteradores).

Async / await será una gran adición al lenguaje y creo que más personas usarán más cosas relacionadas con el futuro después de esta adición y su estabilización.
Algunos incluso podrían descubrir la programación asincrónica por primera vez con Rust.
Por lo tanto, podría ser beneficioso mantener baja la complejidad del lenguaje, mientras que los conceptos detrás de async pueden ser difíciles de aprender.

Y no creo que enviar tanto el prefijo como el postfijo sea una buena idea, pero esto es solo una opinión personal.

@huxi Sí, ya lo https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886

Mi cerebro traduce @ a "en" debido a su uso en direcciones de correo electrónico. Ese símbolo se llama "Klammeraffe" en mi idioma nativo, que se traduce aproximadamente como "mono aferrado". De hecho, aprecio que mi cerebro se haya conformado con "en" en su lugar.

Tengo una historia similar: en mi idioma es "perro", pero no afecta la lectura del correo electrónico.
Sin embargo, es bueno ver tu experiencia.

@llambda la pregunta era sobre encadenamiento. Por supuesto, podría simplemente introducir variables adicionales. Pero de todos modos, este await { foo }? lugar de await? foo o foo await? parece horrible.

@totorigolo
Buen post. Pero no creo que su segunda sugerencia sea un buen camino a seguir. Cuando introduce dos formas de hacer algo, solo produce confusión y problemas, por ejemplo, necesita la opción rustfmt o su código se vuelve un desastre.

Se supone que @Hirevo async/await eliminan la necesidad en los combinadores. No volvamos a "pero puedes hacerlo solo con combinadores". Tu código es el mismo que

future.then(|x| x.json()).map(|x| x.to_string()).map(|ret| ... );

Entonces, ¿eliminemos async/await juntos?

Dicho esto, los combinadores son menos expresivos, menos convenientes y, a veces, simplemente no puedes expresar lo que await podría, por ejemplo, pedir prestado entre puntos de espera (para qué se diseñó Pin ).

No veo ningún problema de legibilidad, encadenamiento o precedencia en el siguiente código

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {

    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| serde_json::from_str::<User>(x?))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str()))
        .map(|x| serde_json::from_str::<Vec<Permission>>(x?));

    Ok(user)
}

Hago:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = serde_json::from_str(user);
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = serde_json::from_str(permissions );
    Ok(user)
}

¿Algo raro esta pasando? No hay problema:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = dbg!(fetch(format!("/user/{0}", name).as_str()) await?);
    let user: User = dbg!(serde_json::from_str(user));
    let permissions = dbg!(fetch(format!("/permissions/{0}", x.id).as_str()) await?);
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions));
    Ok(user)
}

Es más engorroso hacerlo funcionar con combinadores. Sin mencionar que sus funciones ? en then / map no funcionarán como se esperaba, y su código no funcionará sin into_future() y alguna otra cosas raras que no necesitas en async/await flow.

Estoy de acuerdo en que mi propuesta no es óptima porque introduce mucha sintaxis legal usando la palabra clave async . Pero creo que las reglas son fáciles de entender y que satisfacen todos nuestros casos de uso.

Pero nuevamente, eso significaría muchas variaciones permitidas:

  • await!(...) , para mantener la coherencia con try!() y macros conocidas como println!()
  • await , await(...) , await { ... } , es decir. sintaxis de prefijo sin azúcar
  • await? , await?() , await? {} , es decir. prefijo sintaxis con azúcar
  • ... await , es decir. sintaxis postfix (más ... await? , pero que no es azúcar)
  • todas las combinaciones de esos, pero que no se recomiendan (ver mi publicación anterior )

Pero en la práctica solo esperamos ver:

  • prefijo sin Result s: await , o await { ... } para aclarar con expresiones largas
  • prefijo con Result s: await? , o await? {} para aclarar con expresiones largas
  • sufijo al encadenar: ... await , ... await?

+1 al envío en espera! () Macro a estable. Mañana, si es posible 😄

Hay mucha especulación sobre cómo se usará este patrón y preocupaciones sobre la ergonomía en tales casos. Aprecio estas preocupaciones, pero no veo una razón convincente por la que esto no pueda ser un cambio iterativo. Esto permitirá la generación de métricas de uso reales que luego puedan informar la optimización (asumiendo que sea necesario).

Si la adopción de async Rust es un esfuerzo mayor para los principales proyectos / cajas que cualquier esfuerzo posterior de refactorización _opcional_ para pasar a una sintaxis más concisa / expresiva, entonces recomiendo encarecidamente que permitamos que ese esfuerzo de adopción comience ahora mismo. La continua incertidumbre está causando dolor.

@Pzixel
(Primero, cometí un error al llamar a la función fetch_user, lo solucionaré en esta publicación)

No estoy diciendo que async / await sea inútil y que debamos eliminarlo para los combinadores.
Await permite vincular un valor de un futuro a una variable dentro del mismo alcance después de que se resuelve, lo cual es realmente útil y ni siquiera es posible solo con combinadores.
Eliminar la espera no era mi punto.
Acabo de decir que los combinadores pueden funcionar muy bien con await, y que el problema del encadenamiento se puede abordar esperando solo la expresión completa creada por los combinadores (eliminando la necesidad de await (await fetch("test")).json() o await { await { fetch("test") }.json() } ).

En mi ejemplo de código, ? se comporta según lo previsto por cortocircuito, lo que hace que el cierre devuelva un Err(...) , no toda la función (esa parte es manejada por el await? en toda la cadena).

Reescribiste efectivamente mi ejemplo de código, pero eliminaste la parte de encadenamiento (haciendo enlaces).
Por ejemplo, para la parte de depuración, lo siguiente tiene exactamente el mismo comportamiento con solo el dbg necesario. y nada más (ni siquiera paréntesis extra):

async fn fetch_permissions(name: &str) -> Result<Vec<Permission>, Error> {
    let user = await? fetch(format!("/user/{0}", name).as_str())
        .map(|x| dbg!(serde_json::from_str::<User>(dbg!(x)?)))
        .then(|x| fetch(format!("/permissions/{0}", x?.id).as_str())))
        .map(|x| dbg!(serde_json::from_str::<Vec<Permission>>(dbg!(x)?)));
    Ok(user)
}

No sé cómo hacer lo mismo sin combinadores y sin enlaces o paréntesis adicionales.
Algunas personas hacen encadenamiento para evitar llenar el alcance con variables temporales.
Así que solo quería decir que los combinadores pueden ser útiles y no deben ignorarse al tomar una decisión sobre una sintaxis en particular con respecto a su capacidad de encadenamiento.

Y, por último, por curiosidad, ¿por qué el código no funcionaría sin .into_future() , no son ya futuros (no soy un experto en esto, pero esperaría que ya fueran futuros)?

Me gustaría resaltar un problema significativo con fut await : afecta seriamente la forma en que la gente lee el código. Existe un cierto valor en que las expresiones de programación sean similares a las frases utilizadas en un lenguaje natural, esta es una de las razones por las que tenemos construcciones como for value in collection {..} , por qué en la mayoría de los lenguajes escribimos a + b ("a más b") en lugar de a b + , y escribir / leer "esperar algo" es mucho más natural en inglés (y otros idiomas SVO ) que "algo en espera". Imagínense que en lugar de ? hubiéramos usado un sufijo try palabra clave: let val = foo() try; .

fut.await!() y fut.await() no tienen este problema porque parecen llamadas de bloqueo familiares (pero "macro" además enfatiza la "magia" asociada), por lo que se percibirán de manera diferente desde un espacio separado palabra clave. Sigil también se percibirá de manera diferente, de una manera más abstracta, sin ningún paralelismo directo con las frases en lenguaje natural, y por esta razón creo que es irrelevante cómo la gente lee los sigilos propuestos.

En conclusión: si se decide seguir con la palabra clave, creo firmemente que deberíamos elegir solo entre las variantes de prefijo.

@rpjohnst No estoy seguro de cuál es tu punto, entonces. actual_fun(a + b)? y break (a + b)? me importan debido a la diferente precedencia, así que no sé qué se supone que es await(a + b)? .

He estado siguiendo esta discusión desde lejos y tengo algunas preguntas y comentarios.

Mi primer comentario es que creo que la macro .await!() postfix satisface todos los objetivos principales de Centril con la excepción del primer punto:

" await debe seguir siendo una palabra clave para permitir el diseño del lenguaje futuro".

¿Qué otros usos de la palabra clave await vemos en el futuro?


Editar : entendí mal cómo funciona exactamente await . Tacheé mis declaraciones incorrectas y actualicé los ejemplos.

Mi segundo comentario es que la palabra clave await en Rust hace cosas completamente diferentes de await en otro idioma y esto tiene el potencial de causar confusión y condiciones de carrera inesperadas.

async function waitFor6SecondThenReturn6(){
  let result1 = await waitFor1SecondThenReturn1(); // executes first
  let result2 = await waitFor2SecondThenReturn2(); // executes second
  let result3 = await waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

Creo que un prefijo async hace un trabajo mucho más claro al indicar que los valores asíncronos son piezas de una máquina de estado construida por el compilador:

async function waitFor6SecondThenReturn6(){
  let async result1 = waitFor1SecondThenReturn1(); // executes first
  let async result2 = waitFor2SecondThenReturn2(); // executes second
  let async result3 = waitFor3SecondThenReturn3(); // executes third
  return result1 + result2 + result3;
}

Es intuitivo que las funciones async permiten usar valores async . Esto también crea la intuición de que hay maquinaria asincrónica en funcionamiento en segundo plano y cómo podría estar funcionando.

Esta sintaxis tiene algunas preguntas sin resolver y problemas de componibilidad claros, pero deja en claro dónde está sucediendo el trabajo asincrónico y sirve como un complemento de estilo sincrónico para el .await!() más componible y encadenable.

Tuve dificultades para notar el sufijo await al final de algunas expresiones en ejemplos anteriores de estilo procedimental, así que así es como se vería con esta sintaxis propuesta:

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let async user = dbg!(fetch(format!("/user/{0}", name).as_str()));
    let user: User = dbg!(serde_json::from_str(user?));
    let async permissions = dbg!(fetch(format!("/permissions/{0}", user.id).as_str()));
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(permissions?));
    Ok(user)
}

(También existe el argumento a favor de una macro .dbg!() fácilmente componible y encadenable, pero eso es para un foro diferente).

El martes 29 de enero de 2019 a las 11:31:32 p.m. -0800, Sphericon escribió:

He estado siguiendo esta discusión desde lejos y tengo algunas preguntas y comentarios.

Mi primer comentario es que creo que la macro .await!() postfix satisface todos los objetivos principales de Centril con la excepción del primer punto:

" await debe seguir siendo una palabra clave para permitir el diseño del lenguaje futuro".

Como uno de los principales proponentes de la sintaxis .await!() , yo
Pienso absolutamente que await debería seguir siendo una palabra clave. Dado que nosotros
Necesito que .await!() se integre en el compilador de todos modos, eso parece
trivial.

@Hirevo

Reescribiste efectivamente mi ejemplo de código, pero eliminaste la parte de encadenamiento (haciendo enlaces).

No entiendo este enfoque de "encadenamiento por encadenamiento". El encadenamiento es bueno cuando es bueno, por ejemplo, no crea variables temporales inútiles. Dice "eliminó el encadenamiento", pero puede ver que no proporciona ningún valor aquí.

Por ejemplo, para la parte de depuración, lo siguiente tiene exactamente el mismo comportamiento con solo el dbg necesario. y nada más (ni siquiera paréntesis extra)

No, tu hiciste esto

async fn fetch_user(name: &str) -> Result<Vec<Permission>, Error> {
    let user = fetch(format!("/user/{0}", name).as_str()) await?;
    let user: User = dbg!(serde_json::from_str(dbg!(user)));
    let permissions =  fetch(format!("/permissions/{0}", x.id).as_str()) await?;
    let permissions: Vec<Permission> = dbg!(serde_json::from_str(dbg!(permissions));
    Ok(user)
}

No es lo mismo.

Acabo de decir que los combinadores pueden jugar muy bien con await, y que el problema del encadenamiento se puede abordar esperando solo la expresión completa construida por los combinadores (eliminando la necesidad de await (await fetch ("prueba")). Json () o await {await {fetch ("prueba")} .json ()}).

Es solo un problema para el prefijo await , no existe para otras formas.

No sé cómo hacer lo mismo sin combinadores y sin enlaces o paréntesis adicionales.

¿Por qué no crear estas uniones? Los enlaces siempre son mejores para el lector, por ejemplo, y si tiene un depurador que podría imprimir un valor de enlace, pero que no pudo evaluar una expresión.

Finalmente, en realidad no eliminó ningún enlace. Acaba de usar la sintaxis labmda |user| donde hice let user = ... . No te salvó nada, pero ahora este código es más difícil de leer, más difícil de depurar y no tiene acceso al padre (por ejemplo, tienes que envolver los errores externamente en la cadena de métodos en lugar de hacerlo, se llama a sí mismo, probablemente).


En pocas palabras: el encadenamiento no proporciona valor por sí solo. Puede ser útil en algunos escenarios, pero no es uno de ellos. Y como escribo código asíncrono / en espera durante más de seis años, creo que no querrás escribir código asíncrono encadenado nunca . No porque no puedas, sino porque es inconveniente y el enfoque vinculante siempre es más agradable de leer y, a menudo, de escribir. Por no decir que no necesitas combinadores en absoluto. Si tiene async/await , no necesita estos cientos de métodos en futures / streams , solo necesita dos: join y select . Todo lo demás se puede hacer a través de iteradores / enlaces / ..., es decir, herramientas de lenguaje comunes, que no te obligan a aprender otra infraestructura.

La comunidad await como palabra clave o sigilo, así que en mi humilde opinión, sus ideas sobre async requieren otro RFC.

Mi segundo comentario es que la palabra clave await en Rust hace cosas completamente diferentes a las de await en otros idiomas y esto tiene el potencial de causar confusión y condiciones de carrera inesperadas.

¿Puede ser más detallado? No vi ninguna diferencia entre JS / C # await's, excepto que los futuros se basan en encuestas, pero realmente no tienen nada que hacer con async/await .

Con respecto a la sintaxis del prefijo await? propuesta en varios lugares aquí:
`` C #
deja foo = esperar? bar_async ();

How would this look with ~~futures of futures~~ result of results *) ? I.e., would it be arbitrarily extensible:
```C#
let foo = await?? double_trouble();

IOW, el prefijo await? parece una sintaxis que es demasiado especial para mí.

) * editado.

¿Cómo se vería esto con los futuros de futuros? Es decir, sería arbitrariamente extensible:

let foo = await?? double_trouble();

IOW, await? parece una sintaxis que es demasiado especial para mí.

@rolandsteiner por "futuros de futuros" ¿te refieres a impl Future<Output = Result<Result<_, _>, _>> (uno await + dos ? implica "desenvolver" un solo futuro y dos resultados para mí, sin esperar futuros anidados ).

await? _es_ un caso especial, pero es un caso especial que probablemente se aplicará al 90% + usos de await . El objetivo de los futuros es una forma de esperar en operaciones _IO_ asíncronas, IO es falible, por lo que el 90% + de async fn probablemente devolverá io::Result<_> (o algún otro tipo de error que incluya una variante de IO ). Las funciones que devuelven Result<Result<_, _>, _> son bastante raras actualmente, por lo que no esperaría que requieran una sintaxis de casos especiales.

@ Nemo157 Tienes razón, por supuesto: Resultado de Resultados. Actualicé mi comentario.

Hoy escribimos

  1. await!(future?) por future: Result<Future<Output=T>,E>
  2. await!(future)? por future: Future<Output=Result<T,E>>

Y si escribimos await future? tenemos que averiguar cuál significa.

Pero, ¿es cierto que el caso 1 siempre puede convertirse en el caso 2? En el caso 1, la expresión produce un futuro o un error. Pero el error puede retrasarse y trasladarse al futuro. Así que podemos manejar el caso 2 y hacer una conversión automática aquí.

En el punto de vista del programador, Result<Future<Output=T>,E> garantiza un retorno temprano para el caso de error, pero excepto que los dos tienen la misma sementic. Puedo imaginar que el compilador puede funcionar y evitar la llamada adicional poll si el caso de error es inmediato.

Entonces la propuesta es:

await exp? puede interpretarse como await (exp?) si exp es Result<Future<Output=T>,E> , e interpretarse como (await exp)? si exp es Future<Output=Result<T,E>> . En ambos casos, regresará temprano por error y se resolverá con el resultado verdadero si se ejecuta correctamente.

Para casos más complicados, podemos aplicar algo como el método automático de desreferencia del receptor:

> Al interpelar await exp???? primero verificamos exp y si es Result , try y continuamos cuando el resultado sigue siendo Result hasta quedarse sin ? o tener algo que no sea Result . Entonces tiene que ser un futuro y nosotros await en él y aplicamos el resto ? s.

Yo era un partidario de la palabra clave postfix / sigil y todavía lo soy. Sin embargo, solo quiero mostrar que la precedencia del prefijo puede no ser un gran problema en la práctica y tiene soluciones.

Sé que a los miembros del equipo de Rust no les gustan las cosas implícitas, pero en tal caso, hay muy poca diferencia entre las posibles sementics y tenemos una buena manera de asegurarnos de que hacemos lo correcto.

await? es un caso especial, pero es un caso especial que probablemente se aplicará al 90% + usos de await . El objetivo de los futuros es una forma de esperar las operaciones de IO asíncronas, IO es falible, por lo que el 90% + de async fn probablemente devolverá io::Result<_> (o algún otro tipo de error que incluya una variante de IO ). Las funciones que devuelven Result<Result<_, _>, _> son bastante raras actualmente, por lo que no esperaría que requieran una sintaxis de casos especiales.

Los casos especiales son malos para componer, expandir o aprender, y eventualmente se convierten en equipaje. No es un buen compromiso hacer excepciones a las reglas del lenguaje para un solo caso de uso teórico de usabilidad.

¿Sería posible implementar Future por Result<T, E> where T: Future ? De esa manera, podría simplemente await result_of_future sin necesidad de desenvolverlo con ? . Y eso, por supuesto, devolvería un Resultado, por lo que lo llamaría await result_of_future , lo que significaría (await result_of_future)? . De esa manera no necesitaríamos la sintaxis await? y la sintaxis del prefijo sería un poco más consistente. Avísame si hay algún problema con esto.

Los argumentos adicionales para await con delimitadores obligatorios incluyen (personalmente, no estoy seguro de qué sintaxis me gusta más en general):

  • Sin carcasa especial del operador ? , sin await? o await??
  • Congruente con los operadores de flujo de control existentes como loop , while y for , que también requieren delimitadores obligatorios
  • Se siente más en casa con construcciones Rust existentes similares
  • La eliminación de la carcasa especial ayuda a evitar problemas al escribir macros
  • No usa sigilos ni sufijos, evitando gastos del presupuesto de extrañeza

Ejemplo:

let p = if y > 0 { op1() } else { op2() };
let p = await { p }?;

Sin embargo, después de jugar con esto en un editor, todavía se siente incómodo. Creo que preferiría tener await y await? sin delimitadores, como con break y return .

¿Sería posible implementar Future for Result?donde T: Futuro?

Querrías lo inverso. El tipo de espera más común es un futuro donde su tipo de salida es un resultado.

Luego está el argumento explícito en contra de ocultar o absorber el ? en espera. ¿Qué pasa si quieres igualar el resultado, etc.?

Si tiene un Result<Future<Result<T, E2>>, E1> , esperarlo devolvería un Result<Result<T, E2>, E1> .

Si tiene un Future<Result<T, E1>> , esperarlo simplemente devolvería el Result<T, E1> .

No hay que esconder ni absorber los ? en espera, y puede hacer lo que sea necesario con el resultado después.

Oh. Debo haberte entendido mal entonces. Sin embargo, no veo cómo eso ayuda, ya que todavía necesitamos combinar ? con await el 99% del tiempo.


Oh. Se supone que la sintaxis await? implica (await future)? que sería el caso común.

Exactamente. Así que haríamos que el enlace await más estrecho en await expr? , y si esa expresión es Result<Future<Result<T, E2>>, E1> entonces evaluaría algo del tipo Result<T, E2> . Significaría que no hay una carcasa especial para esperar en los tipos de resultados. Simplemente sigue las implementaciones de rasgos normales.

@ivandardi ¿qué pasa con Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

@Pzixel Tenga en cuenta que esa forma de futuro se ha ido. Ahora hay un solo tipo asociado, Salida (que probablemente será un Resultado).


@ivandardi Está bien. Ya lo veo. Lo único que tendrías en tu contra es que la precedencia es algo extraño que tendrías que aprender allí, ya que es una desviación, pero también lo es casi cualquier cosa con await , supongo.

Aunque un resultado que devuelve un futuro es tan poco común, no he encontrado un caso aparte de algo en el núcleo de tokio que se eliminó, por lo que no creo que necesitemos ninguna implicación de azúcar / rasgo para ayudar en ese caso.

@ivandardi ¿qué pasa con Result<Future<Item=i32, Error=SomeError>, FutCreationError> ?

Bueno, supongo que eso no es posible, ya que el rasgo Future solo tiene un tipo asociado Output .


@mehcode Bueno,

Bueno, supongo que eso no es posible, ya que el rasgo Futuro solo tiene un tipo asociado de Salida.

¿Por qué no?

fn probably_get_future(val: u32) -> Result<impl Future<Item=i32, Error=u32>, &'static str> {
    match val {
        0 => Ok(ok(15)),
        1 => Ok(err(100500)),
        _ => Err("Coulnd't create a future"),
    }
}

@Pzixel Ver https://doc.rust-lang.org/std/future/trait.Future.html

Estás hablando del antiguo rasgo que estaba en la caja futures .

Honestamente, no creo que tener una palabra clave en la posición de prefijo como se suponía que era:

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

tiene una gran ventaja sobre tener un sigilo en la misma posición:

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

Creo que solo se ve de manera inconsistente con otros operadores de prefijos, agrega espacios en blanco redundantes antes de la expresión y cambia el código al lado derecho a una distancia notable.


Podemos intentar usar sigil con sintaxis de puntos extendida ( Pre-RFC ) que resuelve problemas con ámbitos profundamente anidados:

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

además de agregar posibilidad a los métodos de cadena:

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

Y obviamente, sustituya * con @ que tiene más sentido aquí:

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

Lo que me gusta aquí es que @ refleja en await que se coloca en el LHS de la declaración de función, mientras que ? refleja en Result<User> que se coloca en el RHS de declaración de función. Esto hace que @ extremadamente consistente con ? .


Tiene alguna idea sobre esto?

Tiene alguna idea sobre esto?

Cualquier frenillo adicional es simplemente imposible

@mehcode Sí, no me di cuenta de que los futuros ahora carecen del tipo Error . Pero el punto sigue siendo válido: puede tener una función, que probablemente devuelva una función, que, cuando se complete, puede devolver un resultado o error.

Tiene alguna idea sobre esto?

¡Si! Según el comentario de Centril , los sigilos no son muy grepparables. Solo comenzaría a considerar los sigilos si uno puede crear una expresión regular que identifique todos los puntos de espera.

En cuanto a tu propuesta de sintaxis de puntos extendida, tendrías que explicarla mucho más profundamente, proporcionando la semántica y el des azucar de cada uso. Actualmente, no puedo entender el significado de los fragmentos de código que publicó con esa sintaxis.


Pero el punto sigue siendo válido: puede tener una función, que probablemente devuelva una función, que, cuando se complete, puede devolver un resultado o error.

Entonces tendrías un Future<Item=Result<T, E>> , ¿verdad? Bueno, en ese caso, solo ... esperas el futuro y lidias con el resultado 😐

Como en, suponga que foo es de tipo Future<Item=Result<T, E>> . Entonces await foo será un Result<T, E> y puede usar lo siguiente para solucionar los errores:

await foo?;
await foo.unwrap();
match await foo { ... }
await foo.and_then(|x| x.bar())

@melodicstream

Entonces tendrías un futuro>, ¿verdad? Bueno, en ese caso, solo ... esperas el futuro y lidias con el resultado 😐

No, me refiero a Result<Future<Item=Result<T, E>>, OtherE>

Con la variante postfix, simplemente lo hace

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

@melodicstream Actualicé mi publicación y agregué un enlace a pre-RFC para esa función

Dios mío ... acabo de volver a leer todos los comentarios por tercera vez ...

El único sentimiento consistente es mi disgusto por la notación de postfijo .await en todas sus variaciones porque seriamente esperaría que await sea ​​parte de Future con esa sintaxis. El "poder del punto" puede funcionar para un IDE pero no funciona para mí en absoluto. Ciertamente podría adaptarme a él si fuera la sintaxis estabilizada, pero dudo que alguna vez me sienta realmente "bien".

Yo optaría por la notación de prefijo await súper básica sin corchetes obligatorios, principalmente debido al principio KISS y porque hacerlo de manera similar a la mayoría de los otros idiomas vale mucho.

Quitar el azúcar de await? future a (await future)? estaría bien y se lo agradecería, pero cualquier cosa más allá de eso me parece cada vez más una solución sin problemas. El simple cambio de enlace de let mejoró la legibilidad del código en la mayoría de los ejemplos y yo, personalmente, probablemente seguiría esa ruta mientras escribía el código, incluso si el encadenamiento fácil fuera una opción.

Habiendo dicho eso, estoy bastante feliz de no estar en condiciones de decidir sobre esto.

Ahora dejaré este problema solo en lugar de agregar más ruido y esperaré (juego de palabras) el veredicto final.

... al menos, honestamente intentaré hacer eso ...

@Pzixel Suponiendo que foo es del tipo Result<Future<Item=Result<T, E1>>, E2> , entonces await foo sería del tipo Result<Result<T, E1>, E2> y luego puede manejar ese Resultado en consecuencia.

await foo?;
await foo.and_then(|x| x.and_then(|y| y.bar()));
await foo.unwrap().unwrap();

@melodicstream no, no lo hará. No puedes esperar Result, puedes esperar Future. So you have to do foo ()? to unwrap Futuro from Resultado , then do aguardar to get a result, then again ? `Para desenvolver el resultado del futuro.

En forma de sufijo será foo? await? , en prefijo ... no estoy seguro.

Entonces tus ejemplos simplemente no funcionan, especialmente el último, porque debería ser

(await foo.unwrap()).unwrap()

Sin embargo, @huxi puede tener razón, estamos resolviendo el problema que probablemente no existe. La mejor manera de averiguarlo es permitir la macro postfix y ver la base de código real después de la adopción básica async/await .

@Pzixel Por eso hice la propuesta de implementar Future en todos los tipos de Result<Future<Item=T>, E> . Hacer eso permitiría lo que estoy diciendo.

Aunque estoy bien con await foo?? por Result<Future<Output=Result<T, E1>>, E2> , NO estoy satisfecho con await foo.unwrap().unwrap() . En mi primer modelo cerebral esto tiene que ser

(await foo.unwrap()).unwrap()

De lo contrario, seré realmente confuso. La razón es que ? es un operador general y unwrap es un método. El compilador puede hacer algo especial para operadores como . , pero si es un método normal, asumiré que siempre se relaciona con la expresión más cercana en su lado izquierdo, únicamente.

La sintaxis del sufijo, foo.unwrap() await.unwrap() , también está bien para mí, ya que sé que await es solo una palabra clave, no un objeto, por lo que debe ser parte de la expresión antes de unwrap() .

La macro de estilo postfix resuelve muy bien muchos de estos problemas, pero solo la pregunta de si queremos mantener la familiaridad con los lenguajes existentes y mantenerlo prefijado. Votaría por el estilo postfix.

Tengo razón en que el siguiente código no es igual:

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      do_async_call().map(|_| 10)
   }
}

y

async fn foo(n: u32) -> u32 {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      await!(do_async_call());
      10
   }
}

El primero llena el pánico al intentar crear un futuro, mientras que el segundo entra en pánico en la primera encuesta. Si es así, ¿deberíamos hacer algo con él? Podría ser una solución alternativa como

fn foo(n: u32) -> impl Future<Item = u32> {
   if n == 0 {
      panic!("Can't be zero");
   } else {
      async {
         await!(do_async_call());
         10 
      }
   }
}

Pero es mucho menos conveniente.

@Pzixel : esta es una decisión que se ha tomado. async fn son completamente perezosos y no se ejecutan en absoluto hasta que son encuestados, por lo que capturan todas las vidas de entrada para toda la vida futura. La solución alternativa es una función de ajuste que devuelve impl Future siendo explícito y utiliza un bloque async (o fn) para el cálculo diferido. Esto es _requerido_ para apilar Future combinadores sin asignación entre cada uno, ya que después de la primera encuesta no se pueden mover.

Esta es una decisión tomada y por qué los sistemas de espera implícitos no funcionan (bien) para Rust.

Desarrollador de C # aquí, y voy a abogar por el estilo de prefijo, así que prepárate para votar.

La sintaxis de sufijo con un punto ( read().await o read().await() ) es muy engañosa y sugiere un acceso a un campo o una invocación de método, y await no es ninguna de esas cosas; es una característica del idioma. No soy un rustáceo experimentado de ninguna manera, pero no tengo conocimiento de ningún otro caso .something que sea efectivamente una palabra clave que será reescrita por el compilador. Lo más cercano que puedo pensar es el sufijo ? .

Habiendo tenido async / await en C # durante algunos años, la sintaxis estándar de prefijo con un espacio ( await foo() ) no me ha causado ningún problema en todo ese tiempo. En los casos en los que se requiere un await anidado, no es oneroso usar parens, que es la sintaxis estándar para todas las precedencias de operadores explícitos y, por lo tanto, es fácil de asimilar, por ejemplo

`` c #
var list = (aguarda GetListAsync ()). ToList ();


`await` is essentially a unary operator, so treating it as such makes sense.

In C# 8.0, currently in preview, we have async enumerables (iterators), which comes with an `IAsyncEnumerable<T>` interface and `await foreach (var x in QueryAsync())` syntax. The lack of async enumerables has been an issue since async was added in C# 5; for example, we have ORMs that return a `Task<IEnumerable>` which is only half asynchronous; we have to block on calls to `MoveNext()`. Having proper async enumerables is a big deal.

If a postfix `await` is used and similar async it