Rust: Problema de seguimiento para `impl Trait` (RFC 1522, RFC 1951, RFC 2071)

Creado en 27 jun. 2016  ·  417Comentarios  ·  Fuente: rust-lang/rust

NUEVO PROBLEMA DE SEGUIMIENTO = https://github.com/rust-lang/rust/issues/63066

Estado de implementación

Se implementa la función básica tal como se especifica en RFC 1522 , sin embargo, ha habido revisiones que aún necesitan trabajo:

RFC

Ha habido una serie de RFC con respecto al rasgo impl, todos los cuales son rastreados por este problema de seguimiento central.

Preguntas sin resolver

La implementación también ha planteado una serie de preguntas interesantes:

  • [x] ¿Cuál es la precedencia de la palabra clave impl al analizar tipos? Discusión: 1
  • [ ] ¿Deberíamos permitir impl Trait después de -> en fn tipos o paréntesis azúcar? #45994
  • [ ] ¿Tenemos que imponer un DAG en todas las funciones para permitir la fuga de seguridad automática, o podemos usar algún tipo de aplazamiento? Discusión: 1

    • Semántica del presente: DAG.

  • [x] ¿Cómo deberíamos integrar el rasgo impl en regionck? Discusión: 1 , 2
  • [ ] ¿Deberíamos permitir especificar tipos si algunos parámetros son implícitos y otros explícitos? por ejemplo, fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Algunas preocupaciones sobre el uso de rasgos impl anidados] (https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] ¿Debería ser la sintaxis en un impl existential type Foo: Bar o type Foo = impl Bar ? ( ver aquí para la discusión )
  • [ ] ¿Debería el conjunto de "usos definidores" para un existential type en un impl ser solo elementos del impl, o incluir elementos anidados dentro de las funciones del impl, etc.? ( ver aquí por ejemplo )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Comentario más útil

Dado que esta es la última oportunidad antes de que FCP cierre, me gustaría presentar un último argumento en contra de los rasgos automáticos automáticos. Me doy cuenta de que esto es un poco de última hora, así que como máximo me gustaría abordar este problema formalmente antes de comprometernos con la implementación actual.

Para aclarar a cualquiera que no haya estado siguiendo impl Trait , este es el problema que estoy presentando. Un tipo representado por tipos impl X actualmente implementa automáticamente rasgos automáticos si y solo si el tipo concreto detrás de ellos implementa dichos rasgos automáticos. Concretamente, si se realiza el siguiente cambio de código, la función continuará compilando, pero cualquier uso de la función que dependa del hecho de que el tipo que devuelve implementa Send fallará.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(ejemplo más simple: trabajando , los cambios internos causan fallas )

Este problema no está claro. Hubo una decisión muy deliberada de tener "fugas" de características automáticas: si no lo hiciéramos, tendríamos que poner + !Send + !Sync en cada función que devuelva algo que no sea Enviar o Sincronizar, y tiene una historia poco clara con posibles otros rasgos automáticos personalizados que simplemente no podrían implementarse en el tipo concreto que devuelve la función. Estos son dos problemas que abordaré más adelante.

Primero, me gustaría simplemente expresar mi objeción al problema: esto permite cambiar el cuerpo de una función para cambiar la API pública. Esto reduce directamente la mantenibilidad del código.

A lo largo del desarrollo de rust, se han tomado decisiones que se equivocan en el lado de la verbosidad sobre la usabilidad. Cuando los recién llegados ven esto, piensan que es verbosidad por verbosidad, pero este no es el caso. Cada decisión, ya sea que las estructuras no implementen Copia automáticamente o que todos los tipos sean explícitos en las firmas de funciones, es por el bien de la mantenibilidad.

Cuando presento a la gente Rust, claro, puedo mostrarles velocidad, productividad, seguridad de la memoria. Pero ir tiene velocidad. Ada tiene seguridad en la memoria. Python tiene productividad. Lo que Rust tiene supera a todos estos, tiene mantenibilidad. Cuando el autor de una biblioteca quiere cambiar un algoritmo para que sea más eficiente, o cuando quiere rehacer la estructura de una caja, tiene una fuerte garantía del compilador de que les avisará cuando cometan errores. En rust, puedo estar seguro de que mi código seguirá funcionando no solo en términos de seguridad de la memoria, sino también en términos de lógica e interfaz. _Cada interfaz de función en Rust se puede representar completamente mediante la declaración de tipo de la función_.

Estabilizar impl Trait tal como está tiene una gran posibilidad de ir en contra de esta creencia. Claro, es extremadamente agradable por el hecho de escribir código rápidamente, pero si quiero hacer un prototipo, usaré Python. Rust es el lenguaje de elección cuando se necesita capacidad de mantenimiento a largo plazo, no código de solo escritura a corto plazo.


Digo que solo hay una "gran posibilidad" de que esto sea malo aquí porque, nuevamente, el problema no está claro. La idea completa de 'rasgos automáticos' en primer lugar no es explícita. Send y Sync se implementan en función del contenido de una estructura, no de la declaración pública. Dado que esta decisión funcionó para el óxido, impl Trait actuando de manera similar también podría funcionar bien.

Sin embargo, las funciones y las estructuras se usan de manera diferente en una base de código y estos no son los mismos problemas.

Al modificar los campos de una estructura, incluso los campos privados, inmediatamente queda claro que se está cambiando el contenido real de la misma. Las estructuras con campos que no son de envío o sincronización tomaron esa decisión, y los mantenedores de bibliotecas saben que deben verificar dos veces cuando un RP cambia los campos de una estructura.

Al modificar las partes internas de una función, definitivamente está claro que uno puede afectar tanto el rendimiento como la corrección. Sin embargo, en Rust, no necesitamos comprobar que estamos devolviendo el tipo correcto. Las declaraciones de funciones son un contrato estricto que debemos cumplir y rustc cuida las espaldas. Hay una delgada línea entre los rasgos automáticos en las estructuras y los retornos de las funciones, pero cambiar las partes internas de una función es mucho más rutinario. Una vez que tengamos Future alimentados por generador completo, será aún más rutinario modificar las funciones que devuelven -> impl Future . Todos estos serán cambios que los autores deben buscar implementaciones modificadas de Enviar/Sincronizar si el compilador no las detecta.

Para resolver esto, podríamos decidir que esta es una carga de mantenimiento aceptable, como lo hizo la discusión original de RFC . Esta sección en el RFC de rasgo impl conservador presenta los argumentos más importantes para filtrar rasgos automáticos ("OIBIT" es el nombre antiguo para rasgos automáticos).

Ya expliqué mi respuesta principal a esto, pero aquí hay una última nota. Cambiar el diseño de una estructura no es tan común; se puede prevenir. La carga de mantenimiento para garantizar que las funciones continúen implementando los mismos rasgos automáticos es mayor que la de las estructuras simplemente porque las funciones cambian mucho más.


Como nota final, me gustaría decir que los rasgos automáticos automáticos no son la única opción. Es la opción que elegimos, pero la alternativa de los rasgos automáticos de exclusión voluntaria sigue siendo una alternativa.

Podríamos requerir funciones que devuelvan elementos que no sean de envío/sincronización para indicar + !Send + !Sync o para devolver un rasgo (¿posiblemente alias?) que tenga esos límites. Esta no sería una buena decisión, pero podría ser mejor que la que estamos eligiendo actualmente.

En cuanto a la preocupación con respecto a los rasgos automáticos personalizados, diría que cualquier rasgo automático nuevo no debe implementarse solo para los nuevos tipos introducidos después del rasgo automático. Esto podría proporcionar más problemas de los que puedo abordar ahora, pero no es uno que no podamos abordar con más diseño.


Esto es muy tardío y muy extenso, y estoy seguro de haber planteado estas objeciones antes. Me alegra poder comentar por última vez y asegurarme de que estamos totalmente de acuerdo con la decisión que estamos tomando.

Gracias por leer, y espero que la decisión final coloque a Rust en la mejor dirección posible.

Todos 417 comentarios

@aturon ¿Podemos realmente poner el RFC en el repositorio? ( @mbrubeck comentó allí que esto era un problema).

Hecho.

El primer intento de implementación es el #35091 (segundo, si cuenta mi sucursal del año pasado).

Un problema con el que me encontré es con las vidas. A la inferencia de tipos le gusta poner variables de región _en todas partes_ y sin ningún cambio de verificación de región, esas variables no infieren nada más que ámbitos locales.
Sin embargo, el tipo concreto _debe_ ser exportable, por lo que lo restringí a 'static y nombré explícitamente los parámetros de vida útil de enlace temprano, pero _nunca_ ninguno de esos si hay alguna función involucrada, incluso un literal de cadena no infiere a 'static , es casi completamente inútil.

Una cosa en la que pensé, que no tendría ningún impacto en la verificación de la región en sí misma, es borrar vidas:

  • nada que exponga el tipo concreto de un impl Trait debería preocuparse por la vida útil: una búsqueda rápida de Reveal::All sugiere que ese ya es el caso en el compilador
  • se debe colocar un límite en todos los tipos concretos de impl Trait en el tipo de retorno de una función, que sobrevive a la llamada de esa función; esto significa que cualquier tiempo de vida es, por necesidad, 'static o uno de los parámetros de por vida de la función - _incluso_ si no podemos saber cuál (por ejemplo, "el más corto de 'a y 'b ")
  • debemos elegir una varianza para el parametrismo de por vida implícito de impl Trait (es decir, en todos los parámetros de por vida en el alcance, igual que con los parámetros de tipo): la invariancia es más fácil y le da más control a la persona que llama, mientras que la contravarianza le permite a la persona que llama hacer más y requeriría verificar que cada vida útil en el tipo de retorno esté en una posición contravariante (lo mismo con el parametrismo de tipo covariante en lugar de invariante)
  • el mecanismo de filtración automática de rasgos requiere que se pueda colocar un límite de rasgos en el tipo concreto, en otra función; dado que hemos borrado las vidas y no tenemos idea de qué vida va a dónde, cada vida borrada en el tipo concreto tendrá que ser sustituida con una nueva variable de inferencia que se garantiza que no será más corta que la vida útil más corta de todos los parámetros de vida útil reales; el problema radica en el hecho de que las impls de rasgos pueden terminar requiriendo relaciones de por vida más sólidas (por ejemplo, X<'a, 'a> o X<'static> ), que deben detectarse y cometerse un error, ya que no se pueden probar para esos vidas

Ese último punto sobre la fuga automática de rasgos es mi única preocupación, todo lo demás parece sencillo.
No está del todo claro en este punto cuánto de la verificación de regiones podemos reutilizar tal como está. Ojalá todos.

cc @rust-lang/lang

@eddyb

Pero las vidas _son_ importantes con impl Trait - por ejemplo

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

Lo mencioné varias veces en los hilos de RFC

versión sin objeto de rasgo:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

Esto es UB o no, según la definición de as_debug .

@arielb1 Ah, cierto, olvidé que una de las razones por las que hice lo que hice fue capturar solo los parámetros de por vida, no los anónimos enlazados en tiempo de ejecución, excepto que realmente no funciona.

@arielb1 ¿Tenemos una relación de vida útil estricta que podamos poner entre la vida útil que se encuentra en el borrado previo del tipo concreto y la vida útil enlazada en tiempo de ejecución en la firma? De lo contrario, puede que no sea una mala idea mirar las relaciones de por vida y fallar instantáneamente cualquier 'a outlives 'b directo o _indirecto_ donde 'a es _cualquier cosa_ que no sea 'static o un parámetro de por vida y 'b aparece en el tipo concreto de un impl Trait .

Perdón por tomarme un tiempo para escribir de nuevo aquí. Así que he estado pensando
sobre este problema Mi sensación es que, en última instancia, tenemos que (y
quiero) extender regionck con un nuevo tipo de restricción -- lo llamaré
una restricción \in , porque le permite decir algo como '0 \in {'a, 'b, 'c} , lo que significa que la región utilizada para '0 debe ser
ya sea 'a , 'b , o 'c . No estoy seguro de cuál es la mejor manera de integrar
esto para que se resuelva solo, ciertamente si el conjunto \in es un singleton
conjunto, es solo una relación de igualdad (que actualmente no tenemos como
cosa de primera clase, pero que se puede componer a partir de dos límites), pero
de lo contrario, complica las cosas.

Todo esto se relaciona con mi deseo de hacer que el conjunto de restricciones regionales
más expresivo que lo que tenemos hoy. Ciertamente uno podría componer un
\in restricción de OR y == restricciones. Pero por supuesto más
las restricciones expresivas son más difíciles de resolver y \in no es diferente.

De todos modos, permítanme exponer un poco de mi pensamiento aquí. Trabajemos con esto
ejemplo:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Creo que la eliminación de azúcar más precisa para un impl Trait es probablemente un
nuevo tipo:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Ahora el impl Iterator<Item=u32> en foo debería comportarse igual que
FooReturn<'a,'b> se comportaría. Aunque no es una combinación perfecta. Una
la diferencia, por ejemplo, es la varianza, como mencionó Eddyb: estoy
asumiendo que haremos impl Foo -tipos invariantes sobre el tipo
parámetros de foo . Sin embargo, el comportamiento del rasgo automático funciona.
(Otra área en la que la coincidencia podría no ser ideal es si alguna vez añadimos el
capacidad de "perforar" la abstracción impl Iterator , por lo que el código
"dentro" la abstracción conoce el tipo preciso, entonces ordenaría
de tener una operación implícita de "desenvolver".)

De alguna manera, una mejor coincidencia es considerar un tipo de rasgo sintético:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Ahora podríamos considerar que el tipo impl Iterator es como <() as FooReturn<'a,'b>>::Type . Esto tampoco es una combinación perfecta, porque
normalmente lo normalizaría. Podrías imaginarte usando la especialización
para evitar eso sin embargo:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

En este caso, <() as FooReturn<'a,'b>>::Type no se normalizaría,
y tenemos un partido mucho más cercano. La varianza, en particular, se comporta
derecho; si alguna vez quisiéramos tener algún tipo que esté "dentro" del
abstracción, serían lo mismo pero se les permite
normalizar. Sin embargo, hay una trampa: el rasgo automático no
bastante trabajo (Es posible que queramos considerar armonizar las cosas aquí,
Realmente.)

De todos modos, mi objetivo al explorar estos potenciales desazúcar no es
sugerimos que implementemos "Impl Rasgo" como un _real_ desugaring
(aunque podría ser bueno...) sino para dar una intuición para nuestro trabajo. I
creo que el segundo desazúcar -en términos de proyecciones- es un
muy útil para guiarnos hacia adelante.

Un lugar donde esta proyección de eliminación de azúcar es una guía realmente útil es
la relación "sobrevive". Si quisiéramos verificar si <() as FooReturn<'a,'b>>::Type: 'x , RFC 1214 nos dice que podemos probar esto
siempre y cuando se mantengan 'a: 'x _y_ 'b: 'x . Esto es lo que creo que queremos
para manejar las cosas para el rasgo impl también.

En tiempo trans, y para auto-rasgos, tendremos que saber qué XXX
es, por supuesto. La idea básica aquí, supongo, es crear un tipo
variable para XXX y compruebe que los valores reales que se devuelven
todo se puede unificar con XXX . Esa variable de tipo debería, en teoría,
dinos nuestra respuesta. Pero claro, el problema es que este tipo
La variable puede referirse a muchas regiones que no están dentro del alcance del
Firma fn: por ejemplo, las regiones del cuerpo fn. (Este mismo problema
no ocurre con los tipos; aunque, técnicamente, podrías poner
por ejemplo, una declaración de estructura en el cuerpo fn y sería innombrable,
eso es una especie de restricción artificial, uno podría moverse
la estructura fuera de la fn.)

Si nos fijamos tanto en la estructura desazucarada como en la impl, hay una
(implícita en la estructura léxica de Rust) restricción de que XXX puede
solo nombre 'static o vidas como 'a y 'b , que
aparecen en la firma de la función. Eso es lo que no somos
modelando aquí. No estoy seguro de cuál es la mejor manera de hacerlo: algún tipo
los esquemas de inferencia tienen una representación más directa del alcance, y
Siempre quise agregar eso a Rust, para ayudarnos con los cierres. Pero
pensemos primero en deltas más pequeños, supongo.

Aquí es donde viene la restricción \in . Uno puede imaginar agregar
una regla de verificación de tipos que (básicamente) FR(XXX) \subset {'a, 'b} --
lo que significa que las "regiones libres" que aparecen en XXX solo pueden ser 'a y
'b . Esto terminaría traduciéndose en \in requisitos para el
varias regiones que aparecen en XXX .

Veamos un ejemplo real:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Aquí, el tipo si condition es verdadero sería algo como
Cloned<SliceIter<'a, i32>> . Pero si condition es falso, lo haríamos
quiero Cloned<SliceIter<'b, i32>> . Por supuesto, en ambos casos lo haríamos
termine con algo como (usando números para las variables de tipo/región):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Si luego instanciamos la variable 0 a Cloned<SliceIter<'2, i32>> ,
tenemos '0: '2 y '1: '2 , o un conjunto total de relaciones de región
me gusta:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Entonces, ¿qué valor deberíamos usar para '2 ? También tenemos el adicional
restricción que '2 in {'a, 'b} . Con el fn tal como está escrito, creo que
tendría que reportar un error, ya que ni 'a ni 'b es un
elección correcta Curiosamente, sin embargo, si agregamos la restricción 'a: 'b , entonces habría un valor correcto ( 'b ).

Tenga en cuenta que si solo ejecutamos el algoritmo _normal_, terminaríamos con
'2 siendo 'body . No estoy seguro de cómo manejar las relaciones \in
a excepción de una búsqueda exhaustiva (aunque puedo imaginar algunos especiales
casos).

OK, eso es todo lo que he conseguido. =)

En el PR #35091, @arielb1 escribió:

No me gusta el enfoque de "capturar todas las vidas en el rasgo impl" y preferiría algo más como la elisión de por vida.

Pensé que tendría más sentido discutir aquí. @arielb1 , ¿puedes dar más detalles sobre lo que tienes en mente? En términos de las analogías que hice anteriormente, supongo que básicamente estás hablando de "podar" el conjunto de tiempos de vida que aparecerían como parámetros en el nuevo tipo o en la proyección (es decir, <() as FooReturn<'a>>::Type lugar de <() as FooReturn<'a,'b>>::Type o algo?

No creo que las reglas de elisión de por vida, tal como existen, sean una buena guía a este respecto: si solo elegimos la vida útil de &self para incluir solo, entonces no necesariamente podríamos incluir el escriba los parámetros de la estructura Self , ni escriba los parámetros del método, ya que pueden tener condiciones WF que requieren que nombremos algunas de las otras vidas.

De todos modos, sería genial ver algunos ejemplos que ilustren las reglas que tiene en mente, y quizás alguna de las ventajas de las mismas. :) (Además, supongo que necesitaríamos algo de sintaxis para anular la elección). En igualdad de condiciones, si podemos evitar tener que elegir entre N vidas, preferiría eso.

No he visto interacciones de impl Trait con la privacidad discutidas en ninguna parte.
Ahora fn f() -> impl Trait puede devolver un tipo privado S: Trait manera similar a los objetos de rasgos fn f() -> Box<Trait> . Es decir, los objetos de tipo privado pueden caminar libremente fuera de su módulo en forma anónima.
Esto parece razonable y deseable: el tipo en sí es un detalle de implementación, solo su interfaz, disponible a través de un rasgo público Trait es público.
Sin embargo, hay una diferencia entre los objetos de rasgos y impl Trait . Con los objetos de rasgo solo, todos los métodos de rasgo de tipos privados pueden obtener un enlace interno, aún se podrán llamar a través de punteros de función. Con impl Trait s, los métodos de rasgos de tipos privados se pueden llamar directamente desde otras unidades de traducción. El algoritmo que realiza la "internalización" de símbolos tendrá que esforzarse más para internalizar métodos solo para tipos no anónimos con impl Trait , o ser muy pesimista.

@nikomatsakis

La forma "explícita" de escribir foo sería

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Aquí no hay duda sobre el límite de por vida. Obviamente, tener que escribir el límite de por vida cada vez sería bastante repetitivo. Sin embargo, la forma en que lidiamos con ese tipo de repetición es generalmente a través de la elisión de por vida. En el caso de foo , la elisión fallaría y obligaría al programador a especificar explícitamente la vida útil.

Me opongo a agregar la elisión de por vida sensible a la explicitación como lo hizo impl Trait y no de otra manera.

@ arielb1 hmm, no estoy 100% seguro de cómo pensar sobre esta sintaxis propuesta en términos de "desazúcar" que discutí. Le permite especificar lo que parece ser un límite de por vida, pero lo que estamos tratando de inferir es principalmente qué vidas aparecen en el tipo oculto. ¿Sugiere esto que a lo sumo una vida podría estar "oculta" (y que tendría que especificarse exactamente?)

Parece que no siempre es el caso de que un "parámetro único de por vida" sea suficiente:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

En este caso, el tipo de iterador oculto se refiere tanto a 'a como a 'b (aunque _es_ variante en ambos; pero supongo que podríamos encontrar un ejemplo que sea invariable).

Así que @aturon y yo discutimos un poco este tema y quería compartirlo. Realmente hay un par de preguntas ortogonales aquí y quiero separarlas. La primera pregunta es "¿qué parámetros de tipo/vida útil se pueden usar potencialmente en el tipo oculto?" En términos de (casi-)desazúcar en un default type , esto se reduce a "qué tipo de parámetros aparecen en el rasgo que introducimos". Entonces, por ejemplo, si esta función:

fn foo<'a, 'b, T>() -> impl Trait { ... }

sería desazucarado a algo como:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

entonces esta pregunta se reduce a "¿qué tipo de parámetros aparecen en el rasgo Foo y su impl"? Básicamente, los ... aquí. Claramente, esta inclusión incluye el conjunto de parámetros de tipo que aparecen y son utilizados por el mismo Trait , pero ¿qué parámetros de tipo adicionales? (Como señalé antes, esta eliminación de azúcar es 100 % fiel excepto por la filtración de rasgos automáticos, y diría que también deberíamos filtrar rasgos automáticos para impls especializables).

La respuesta predeterminada que hemos estado usando es "todos ellos", por lo que aquí ... sería 'a, 'b, T (junto con cualquier parámetro anónimo que pueda aparecer). Este _puede_ ser un valor predeterminado razonable, pero no es _necesariamente_ el mejor valor predeterminado. (Como señaló @arielb1 ).

Esto tiene un efecto en la relación de sobrevidas, ya que, para determinar que <() as Foo<...>>::Type (refiriéndose a alguna instanciación particular y opaca de impl Trait ) sobrevive a 'x , debemos mostrar efectivamente que ...: 'x (es decir, cada parámetro de tiempo de vida y de tipo).

Es por eso que digo que no es suficiente considerar los parámetros de por vida: imagina que tenemos alguna llamada a foo como foo::<'a0, 'b0, &'c0 i32> . Esto implica que las tres vidas, '[abc]0 , deben sobrevivir a 'x -- en otras palabras, mientras el valor de retorno esté en uso, esto prolongará los préstamos de todos los datos proporcionados a la función . Pero, como señaló @arielb1 , la elisión sugiere que esto generalmente será más largo de lo necesario.

Así que me imagino que lo que necesitamos es:

  • establecer un incumplimiento razonable, tal vez usando la intuición de la elisión;
  • tener una sintaxis explícita para cuando el valor predeterminado no sea apropiado.

@aturon espetó algo como impl<...> Trait como sintaxis explícita, lo que parece razonable. Por lo tanto, se podría escribir:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

para indicar que el tipo oculto no se refiere de hecho a 'a o 'b sino solo a T . O se podría escribir impl<'a> Trait para indicar que no se capturan ni 'b ni T .

En cuanto a los valores predeterminados, parece que tener más datos sería bastante útil, pero la lógica general de elisión sugiere que haríamos bien en capturar todos los parámetros nombrados en el tipo de self , cuando corresponda. Por ejemplo, si tiene fn foo<'a,'b>(&'a self, v: &'b [u8]) y el tipo es Bar<'c, X> , entonces el tipo de self sería &'a Bar<'c, X> y, por lo tanto, capturaríamos 'a , 'c y X por defecto, pero no 'b .


Otra nota relacionada es cuál es el significado de un límite de por vida. Creo que los límites de duración del sonido tienen un significado existente que no debe cambiarse: si escribimos impl (Trait+'a) eso significa que el tipo oculto T sobrevive a 'a . De manera similar, se puede escribir impl (Trait+'static) para indicar que no hay punteros prestados presentes (incluso si se capturan algunas vidas). Al inferir el tipo oculto T , esto implicaría un límite de por vida como $T: 'static , donde $T es la variable de inferencia que creamos para el tipo oculto. Esto se manejaría de la manera habitual. Desde la perspectiva de la persona que llama, donde el tipo oculto está, bueno, oculto, el límite de 'static nos permitiría concluir que impl (Trait+'static) sobrevive a 'static incluso si hay parámetros de vida capturados.

Aquí simplemente se comporta exactamente como se comportaría la eliminación de azúcar:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

Todo esto es ortogonal por inferencia. Todavía queremos (creo) agregar la noción de una restricción de "elegir de" y modificar la inferencia con algunas heurísticas y, posiblemente, una búsqueda exhaustiva (la experiencia de RFC 1214 sugiere que las heurísticas con un respaldo conservador pueden llevarnos muy lejos; No estoy al tanto de personas que se encuentren con limitaciones a este respecto, aunque probablemente haya un problema en alguna parte). Ciertamente, agregar límites de por vida como 'static o 'a' puede influir en la inferencia y, por lo tanto, ser útil, pero esa no es una solución perfecta: por un lado, son visibles para la persona que llama y se convierten en parte de la API, que no se puede desear.

Posibles opciones:

Límite de vida explícito con elisión de parámetro de salida

Al igual que los objetos de rasgo actuales, los objetos impl Trait tienen un único parámetro de límite de duración, que se deduce mediante las reglas de elisión.

Desventaja: poco ergonómico
ventaja: claro

Límites explícitos de por vida con elisión "todo genérico"

Al igual que los objetos de rasgos de hoy, los objetos impl Trait tienen un solo parámetro de límite de por vida.

Sin embargo, la elisión crea nuevos parámetros de límite anticipado que sobreviven a todos los parámetros explícitos:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Desventaja: agrega un parámetro de límite anticipado

más.

Me encontré con este problema con impl Trait +'a y préstamo: https://github.com/rust-lang/rust/issues/37790

Si estoy entendiendo este cambio correctamente (¡y la posibilidad de que eso sea probablemente baja!), entonces creo que este código de zona de juegos debería funcionar:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Tanto ThingOne como ThingTwo implementan el rasgo Thing . build dice que devolverá algo que implementa Thing , lo cual hace. Sin embargo, no compila. Así que claramente estoy malinterpretando algo.

Ese "algo" debe tener un tipo, pero en tu caso tienes dos tipos en conflicto. @nikomatsakis sugirió anteriormente hacer que esto funcione en general creando, por ejemplo, ThingOne | ThingTwo medida que aparecen las discrepancias de tipo.

@eddyb, ¿ podría dar más detalles sobre ThingOne | ThingTwo ? ¿No necesita tener Box si solo conocemos el tipo en tiempo de ejecución? ¿O es una especie de enum ?

Sí, podría ser un tipo similar a enum ad-hoc que delegue las llamadas al método de rasgos, cuando sea posible, a sus variantes.

Yo también he querido ese tipo de cosas antes. Las enumeraciones anónimas RFC: https://github.com/rust-lang/rfcs/pull/1154

Es un caso raro de algo que funciona mejor si está basado en inferencias, porque si solo crea estos tipos en una falta de coincidencia, las variantes son diferentes (lo cual es un problema con la forma generalizada).
También puede obtener algo de no tener coincidencia de patrones (¿excepto en casos obviamente inconexos?).
Pero el azúcar de delegación de la OMI "simplemente funcionaría" en todos los casos relevantes, incluso si logra obtener un T | T .

¿Podría deletrear las otras mitades implícitas de esas oraciones? No entiendo la mayor parte, y sospecho que me falta algo de contexto. ¿Estabas respondiendo implícitamente a los problemas con los tipos de unión? Ese RFC es simplemente enumeraciones anónimas, no tipos de unión: (T|T) sería exactamente tan problemático como Result<T, T> .

Oh, no importa, confundí las propuestas (también me quedé atascado en el móvil hasta que resuelvo mi disco duro defectuoso, así que disculpas por sonar como en Twitter).

Encuentro intrigantes las enumeraciones anónimas (posicionales, es decir, T|U != U|T ), y creo que podrían experimentarse con ellas en una biblioteca si tuviéramos genéricos variados (puede eludir esto usando hlist ) y const genéricos (ídem, con números peano).

Pero, al mismo tiempo, si tuviéramos soporte de lenguaje para algo, serían tipos de unión, no enumeraciones anónimas. Por ejemplo, no Result sino tipos de error (para evitar el tedio de los contenedores con nombre para ellos).

No estoy seguro de si este es el lugar correcto para preguntar, pero ¿por qué se necesita una palabra clave como impl ? No pude encontrar una discusión (podría ser mi culpa).

Si una función devuelve Trait impl, su cuerpo puede devolver valores de cualquier tipo que implemente Trait

Ya que

fn bar(a: &Foo) {
  ...
}

significa "aceptar una referencia a un tipo que implementa el rasgo Foo " que esperaría

fn bar() -> Foo {
  ...
}

para significar "devolver un tipo que implementa el rasgo Foo ". ¿Es esto imposible?

@kud1ing el motivo es no eliminar la posibilidad de tener una función que devuelva el tipo de tamaño dinámico Trait si se agrega soporte para valores de retorno de tamaño dinámico en el futuro. Actualmente, Trait ya es un horario de verano válido, simplemente no es posible devolver un horario de verano, por lo que debe encasillarlo para convertirlo en un tipo de tamaño.

EDITAR: Hay alguna discusión sobre esto en el hilo RFC vinculado.

Bueno, por un lado, independientemente de si se agregarán valores de retorno de tamaño dinámico, prefiero la sintaxis actual. A diferencia de lo que sucede con los objetos de rasgos, esto no es un borrado de tipo, y cualquier coincidencia como "el parámetro f: &Foo toma algo que implica Foo , mientras que esto devuelve algo que implica Foo " podría ser engañoso.

Deduje de la discusión de RFC que en este momento impl es una implementación de marcador de posición, y no se desea mucho impl . ¿Hay alguna razón para _no_ querer un rasgo impl si el valor de retorno no es DST?

Creo que la técnica impl actual para manejar la "fuga automática de rasgos" es problemática. En su lugar, deberíamos hacer cumplir un pedido DAG de modo que si define un fn fn foo() -> impl Iterator , y tiene una persona que llama fn bar() { ... foo() ... } , entonces tenemos que verificar el tipo de foo() antes de bar() (para que sepamos cuál es el tipo oculto). Si resulta un ciclo, informaríamos un error. Esta es una postura conservadora, probablemente podamos hacerlo mejor, pero creo que la técnica actual, en la que recopilamos obligaciones de rasgos automáticos y las verificamos al final, no funciona en general. Por ejemplo, no funcionaría bien con la especialización.

(Otra posibilidad que podría ser más permisiva que requerir un DAG estricto es verificar el tipo de ambos fns "juntos" hasta cierto punto. Creo que eso es algo a considerar solo después de que hayamos reestructurado un poco el sistema de rasgos).

@Nercury No entiendo. ¿Está preguntando si hay razones para no querer que fn foo() -> Trait signifique -> impl Trait ?

@nikomatsakis Sí, estaba preguntando precisamente eso, perdón por el lenguaje convulso :). Pensé que hacer esto sin la palabra clave impl sería más simple, porque este comportamiento es exactamente lo que uno esperaría (cuando se devuelve un tipo concreto en lugar del tipo de retorno de rasgo). Sin embargo, podría estar perdiendo algo, por eso estaba preguntando.

La diferencia es que las funciones que devuelven impl Trait siempre devuelven el mismo tipo, es básicamente una inferencia de tipo de retorno. IIUC, las funciones que devuelven solo Trait podrían devolver cualquier implementación de ese rasgo de forma dinámica, pero la persona que llama debería estar preparada para asignar espacio para el valor devuelto a través de algo como box foo() .

@Nercury La razón simple es que la sintaxis -> Trait ya tiene un significado, por lo que tenemos que usar algo más para esta función.

De hecho, he visto a personas que esperan ambos tipos de comportamiento de forma predeterminada, y este tipo de confusión surge con bastante frecuencia. Honestamente, prefiero que fn foo() -> Trait no signifique nada (o sea una advertencia de forma predeterminada) palabras clave tanto para el caso de "algún tipo conocido en tiempo de compilación que puedo elegir pero la persona que llama no ve" como para el caso de "objeto de rasgo que podría enviarse dinámicamente a cualquier tipo que implemente el rasgo", por ejemplo, fn foo() -> impl Trait frente a fn foo() -> dyn Trait . Pero obviamente esos barcos han zarpado.

¿Por qué el compilador no genera una enumeración que contiene todos los diferentes tipos de retorno de la función, implementa el rasgo pasando los argumentos a cada variante y devuelve eso en su lugar?

Eso pasaría por alto la regla del único tipo de retorno permitido.

@NeoLegends Hacer esto manualmente es bastante común, y un poco de azúcar podría ser bueno y se ha propuesto en el pasado, pero es un tercer conjunto de semántica completamente diferente de devolver impl Trait o un objeto de rasgo, por lo que no es realmente relevante para esta discusión.

@Ixrec Sí, sé que esto se está haciendo manualmente, pero el caso de uso real de las enumeraciones anónimas como tipos de retorno generados por el compilador son tipos que no se pueden deletrear, como largas cadenas de iteradores o futuros adaptadores.

¿Cómo es esta semántica diferente? Las enumeraciones anónimas (en la medida en que el compilador las genera, no según las enumeraciones anónimas RFC) como valores de retorno solo tienen sentido si hay una API común como un rasgo que abstrae las diferentes variantes. Estoy sugiriendo una función que todavía se ve y se comporta como el rasgo impl normal, solo con el límite de un tipo eliminado a través de una enumeración generada por el compilador que el consumidor de la API nunca verá directamente. El consumidor siempre debe ver solo "Rasgo impl".

Las enumeraciones anónimas generadas automáticamente le dan a impl Trait un costo oculto que es fácil pasar por alto, por lo que es algo a considerar.

Sospecho que el "paso de enumeración automática" solo tiene sentido para los rasgos seguros de objetos. ¿Es lo mismo cierto para impl Trait sí mismo?

@rpjohnst A menos que esta sea la variante del método real en los metadatos del cajón y monomorfizada en el sitio de la llamada. Por supuesto, esto requiere que el cambio de una variante a otra no rompa a la persona que llama. Y esto podría ser demasiado mágico.

@glaebhoerl

Sospecho que el "paso de enumeración automática" solo tiene sentido para los rasgos seguros de objetos. ¿Es lo mismo cierto para el Rasgo impl en sí mismo?

¡Este es un punto interesante! He estado debatiendo cuál es la forma correcta de "desengañar" el rasgo impl, y en realidad estaba a punto de sugerir que tal vez queríamos pensar en ello más como una "estructura con un campo privado" en lugar de la "proyección de tipo abstracto". " interpretación. Sin embargo, eso parece implicar algo muy parecido a la derivación generalizada de nuevos tipos, que por supuesto se descubrió que no era F<T> de un impl por T .

@nikomatsakis

El problema es, en términos de Rust

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

El tl;dr es que la derivación generalizada de nuevos tipos se implementó (y se implementa) simplemente transmute ing la vtable; después de todo, una vtable consta de funciones en el tipo, y un tipo y su nuevo tipo tienen la misma representación , así que debería estar bien, ¿verdad? Pero se rompe si esas funciones también usan tipos que están determinados por la bifurcación de nivel de tipo en la identidad (en lugar de la representación) del tipo dado, por ejemplo, usando funciones de tipo o tipos asociados (o en Haskell, GADT). Porque no hay garantía de que las representaciones de esos tipos también sean compatibles.

Tenga en cuenta que este problema solo es posible debido al uso de transmutación insegura. Si, en cambio, solo generara el aburrido código repetitivo para envolver/desenvolver el nuevo tipo en todas partes y enviar todos los métodos a su implementación desde el tipo base (¿como algunas de las propuestas de delegación automática para Rust IIRC?), Entonces el peor resultado posible sería un tipo error o tal vez un ICE. Después de todo, por construcción, si no usa un código inseguro, no puede tener un resultado inseguro. Del mismo modo, si generamos código para algún tipo de "transmisión de enumeración automática", pero no usamos ninguna primitiva unsafe para hacerlo, no habría ningún peligro.

(No estoy seguro de si esto se relaciona o cómo se relaciona con mi pregunta original de si los rasgos utilizados con impl Trait , y/o el paso automático de enumeración, ¿por necesidad tendrían que ser seguros para los objetos?)

@rpjohnst Uno podría hacer que el caso de enumeración opte por marcar el costo:

fn foo() -> enum impl Trait { ... }

Sin embargo, es casi seguro que es alimento para un RFC diferente.

@glaebhoerl, sí, pasé un tiempo investigando el problema y me sentí bastante convencido de que no sería un problema aquí, al menos.

Disculpas si es algo obvio, pero estoy tratando de entender las razones por las que impl Trait no puede aparecer en los tipos de retorno de los métodos de rasgos, o si tiene algún sentido en primer lugar. P.ej:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Tiene mucho sentido, y AFAIK la intención es hacer que funcione, pero aún no se ha implementado.

De alguna manera tiene sentido, pero no es misma característica subyacente (esto se ha discutido mucho por cierto):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Específicamente, impl Trait en los RHS de los tipos asociados de impl Trait for Type sería similar a la función implementada hoy, en el sentido de que no se puede eliminar a Rust estable, mientras que en los trait puede ser.

Sé que es probable que sea demasiado tarde y, en su mayoría, se deshaga de las bicicletas, pero ¿se ha documentado en alguna parte por qué se introdujo la palabra clave impl ? Me parece que ya tenemos una forma en el código Rust actual de decir "el compilador descubre qué tipo va aquí", a saber, _ . ¿No podríamos reutilizar esto aquí para dar la sintaxis:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Eso no es lo que hace la función, el tipo no es el que devuelve la función, sino un "envoltorio semántico" que oculta todo excepto las API elegidas (y OIBIT porque son una molestia).

Podríamos permitir que algunas funciones infieran tipos en sus firmas al forzar un DAG, pero tal característica nunca ha sido aprobada y es poco probable que alguna vez se agregue a Rust, ya que tocaría la "inferencia global".

Sugiera el uso de la sintaxis @Trait para reemplazar impl Trait , como se menciona aquí .

Es más fácil extenderlo a otras posiciones de tipo y composición como Box<@MyTrait> o &@MyTrait .

@Trait por any T where T: Trait y ~Trait por some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

En fn func(t: T) -> V , no es necesario distinguir ninguna t o alguna v, por lo que es un rasgo.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

todavía funciona.

@JF-Liu Personalmente me opongo a tener any y some combinados en una palabra clave/sígilo, pero técnicamente tiene razón en que podríamos tener un solo sigilo y usarlo como el impl Trait RFC de

@JF-Liu @eddyb Había una razón por la que se eliminaron los sigilos del lenguaje. ¿Por qué esa razón no se aplicaría a este caso?

@ también se usa en la coincidencia de patrones, no se elimina del idioma.

Lo que tenía en mente es que los sigilos AFAIK se usaron en exceso.

Sintaxis bikesheding: Estoy profundamente descontento con la notación impl Trait , porque usar una palabra clave (fuente en negrita en un editor) para nombrar un tipo es demasiado ruidoso. ¿Recuerda la observación de sintaxis en voz alta de C struct y Stroustroup (diapositiva 14)?

En https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 , @konstin sugirió la <Trait> . Se ve muy bien, especialmente en las posiciones de entrada:

fn take_iterator(iterator: <Iterator<Item=i32>>)

Veo que de alguna manera entrará en conflicto con UFCS, pero tal vez esto se pueda resolver.

También creo que usar paréntesis angulares en lugar de rasgo impl es una mejor opción, al menos en la posición de tipo de retorno, por ejemplo:

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait> conflictos de sintaxis con genéricos, considere:

Vec<<FnOnce() -> bool>> frente a Vec<@FnOnce() -> bool>

Si se permite Vec<FnOnce() -> bool> , entonces <Trait> es una buena idea, significa la equivalencia con el parámetro de tipo genérico. Pero dado que Box<Trait> es diferente a Box<@Trait> , debe renunciar a la sintaxis de <Trait> .

Prefiero la sintaxis de la palabra clave impl porque cuando lee la documentación rápidamente, esto permite menos formas de leer mal los prototipos.
Qué piensas ?

Me estoy dando cuenta de que propuse un superconjunto para este rfc en el hilo interno (Gracias por @matklad por señalarme aquí):

Permita que se utilicen rasgos en los parámetros de función y tipos de devolución rodeándolos con corchetes angulares como en el siguiente ejemplo:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

Luego, el compilador monomorfizaría el parámetro utilizando las mismas reglas que se aplican actualmente a los genéricos. El tipo de retorno podría, por ejemplo, derivarse de la implementación de funciones. Esto significa que no puede simplemente llamar a este método en un Box<Trait_with_transform> o usarlo en objetos enviados dinámicamente en general, pero aun así haría que las reglas fueran más permisivas. No he leído toda la discusión de RFC, por lo que tal vez ya haya una solución mejor que me he perdido.

Prefiero la sintaxis de palabras clave impl porque cuando lee la documentación rápidamente, esto permite menos formas de leer mal los prototipos.

Un color diferente en el resaltado de sintaxis debería ser suficiente.

Este artículo de Stroustrup analiza elecciones sintácticas similares para conceptos de C++ en la sección 7: http://www.stroustrup.com/good_concepts.pdf

No utilice la misma sintaxis para genéricos y existenciales. No són la misma cosa. Los genéricos permiten que la persona que llama decida cuál es el tipo concreto, mientras que (este subconjunto restringido de) existenciales permite que la función que se llama decida cuál es el tipo concreto. Este ejemplo:

fn transform(iter: <Iterator>) -> <Iterator>

debería ser equivalente a esto

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

o debería ser equivalente a esto

fn transform(iter: impl Iterator) -> impl Iterator

El último ejemplo no se compilará correctamente, ni siquiera todas las noches, y en realidad no se puede llamar con el rasgo iterador, pero un rasgo como FromIter permitiría a la persona que llama construir una instancia y pasarla a la función sin poder para determinar el tipo concreto de lo que están pasando.

Tal vez la sintaxis debería ser similar, pero no debería ser la misma.

No es necesario distinguir ninguno de (genéricos) o algunos de (existenciales) en el nombre del tipo, depende de dónde se use el tipo. Cuando se usa en variables, argumentos y campos de estructura, siempre acepta cualquiera de T, cuando se usa en el tipo de retorno fn, siempre obtiene algo de T.

  • use Type , &Type , Box<Type> para tipos de datos concretos, despacho estático
  • use @Trait , &@Trait , Box<@Trait> y el parámetro de tipo genérico para el tipo de datos abstractos, despacho estático
  • use &Trait , Box<Trait> para tipos de datos abstractos, despacho dinámico

fn func(x: @Trait) es equivalente a fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) se puede escribir simplemente como fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T parámetro fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } es equivalente a struct Foo<T: Trait> { field: T } .

Cuando se usa en variables, argumentos y campos de estructura, siempre acepta cualquiera de T, cuando se usa en el tipo de retorno fn, siempre obtiene algo de T.

Puede devolver cualquiera de los rasgos, ahora mismo, en Rust estable, utilizando la sintaxis genérica existente. Es una característica muy utilizada. serde_json::de::from_slice toma &[u8] como parámetro y devuelve T where T: Deserialize .

También puede devolver significativamente algo de Rasgo, y esa es la característica que estamos discutiendo. No puede usar existenciales para la función de deserializar, al igual que no puede usar genéricos para devolver cierres sin caja. Son características diferentes.

Para un ejemplo más familiar, Iterator::collect puede devolver cualquier T where T: FromIterator<Self::Item> , lo que implica mi notación preferida: fn collect(self) -> any FromIterator<Self::Item> .

¿Qué hay de la sintaxis?
fn foo () -> _ : Trait { ... }
para valores de retorno y
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
para parámetros?

Para mí, ninguna de las nuevas sugerencias se acerca a impl Trait en su elegancia. impl es una palabra clave ya conocida por todos los programadores de rust y, dado que se usa para implementar características, en realidad sugiere lo que la función está haciendo por sí sola.

Sí, seguir con las palabras clave existentes me parece ideal; Me gustaría ver impl para existenciales y for para universales.

Personalmente, me opongo a tener any y some combinados en una palabra clave/sígilo

@eddyb No lo consideraría una fusión. Se sigue naturalmente de la regla:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Editar: es unidireccional, no un isomorfismo.


Sin relación: ¿Hay alguna propuesta relacionada para permitir también impl Trait en otras posiciones covariantes como

~ óxidofn-foo(devolución de llamada: F) -> Rdonde F: FnOnce(impl AlgúnRasgo) -> R {devolución de llamada (crear_algo())}~

En este momento , esta no es una característica necesaria, ya que siempre puede poner un tiempo concreto para impl SomeTrait , lo que perjudica la legibilidad pero, por lo demás, no es gran cosa.

Pero si la función RFC 1522 se estabiliza, entonces sería imposible asignar una firma de tipo a programas como los anteriores si create_something da como resultado impl SomeTrait (al menos sin incluirlo). Creo que esto es problemático.

@Rufflewind En el mundo real, las cosas no son tan claras, y esta característica es una marca muy específica de existenciales (Rust tiene varios ahora).

Pero incluso entonces, todo lo que tiene es el uso de la covarianza para determinar qué significa impl Trait dentro y fuera de los argumentos de la función.

Eso no es suficiente para:

  • usando el opuesto del predeterminado
  • desambiguación dentro del tipo de un campo (donde tanto any como some son igualmente deseables)

@Rufflewind Eso parece un corchete incorrecto para lo que es impl Trait . Sé que Haskell explota esta relación para usar solo la palabra clave forall para representar tanto los universales como los existenciales, pero no funciona en el contexto que estamos discutiendo.

Tome esta definición, por ejemplo:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Si usamos la regla de que " impl en argumentos es universal, impl en tipos de retorno es existencial", entonces el tipo de elemento de función foo es lógicamente este (en notación de tipos inventados):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

Tratar ingenuamente impl como técnicamente solo significa universal o solo significa existencial y dejar que la lógica funcione por sí sola no funciona. Obtendrías esto:

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

O esto:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Y ninguno de estos se reduce a lo que queremos por reglas lógicas. Así que en última instancia any / some captan una distinción importante que no se pueden capturar con una sola palabra clave. Incluso hay ejemplos razonables en std en los que desea universales en la posición de retorno. Por ejemplo, este método Iterator :

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

Y no hay forma de escribirlo con impl y la regla de argumento/retorno.

tl; dr tener impl contextualmente denota universal o existencial realmente le da dos significados distintos.


Como referencia, en mi notación, la relación forall/exists mencionada por @Rufflewind se ve así:

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

Lo cual está relacionado con el concepto de que los objetos de rasgos (existenciales) son equivalentes a los genéricos (universales), pero no con esta pregunta de impl Trait .

Dicho esto, ya no estoy muy a favor de any / some . Quería ser preciso sobre lo que estamos hablando, y any / some tienen esta amabilidad teórica y visual, pero estaría bien usando impl con el contexto regla. Creo que cubre todos los casos comunes, evita problemas de gramática de palabras clave contextuales y podemos usar parámetros de tipo con nombre para el resto.

En ese sentido, para que coincida con la generalidad completa de los universales, creo que eventualmente necesitaremos una sintaxis para los existenciales con nombre, que habilite las cláusulas where arbitrarias y la capacidad de usar el mismo existencial en varios lugares de la firma.

En resumen, estaría feliz con:

  • impl Trait como la forma abreviada de universales y existenciales (contextualmente).
  • Parámetros de tipo con nombre como la mano larga completamente general para universales y existenciales. (Menos comúnmente necesario.)

Tratar ingenuamente a impl como técnicamente solo significa universal o solo significa existencial y dejar que la lógica funcione por sí sola en realidad no funciona. Obtendrías esto:

@solson Para mí, una traducción "ingenua" daría como resultado los cuantificadores existenciales justo al lado del tipo que se cuantifica. Por eso

~ óxido(impl MyTrait)~

es solo azúcar sintáctico para

~ óxido(existeT)~

que es una transformación local simple. Por lo tanto, una traducción ingenua que obedezca la regla " impl es siempre existencial" daría como resultado:

~ óxidofn(existeT) -> (existeR)~

Entonces, si saca el cuantificador del argumento de la función, se convierte en

~ óxidoporfn(T) -> (existeR)~

Entonces, aunque T siempre es existencial en relación a sí mismo, aparece como universal en relación con todo el tipo de función.


En mi opinión, creo que impl podría convertirse en la palabra clave de facto para los tipos existenciales. En el futuro, quizás uno podría construir tipos existenciales más complicados como:

~~oxido(imp.(Vec, T))~ ~

en analogía con los tipos universales (a través de HRTB)

~ óxido(para<'a> FnOnce(&'a T))~

@Rufflewind Esa vista no funciona porque fn(T) -> (exists<R: ReturnTrait>(R)) no es lógicamente equivalente a exists<R: ReturnTrait>(fn(T) -> R) , que es lo que realmente significa el tipo de retorno impl Trait .

(Al menos no en la lógica constructiva que generalmente se aplica a los sistemas de tipos, donde el testigo específico elegido para un existencial es relevante. El primero implica que la función podría elegir diferentes tipos para devolver en función, por ejemplo, de los argumentos, mientras que el último implica que hay un tipo específico para todas las invocaciones de la función, como es el caso en impl Trait .)

Siento que también nos estamos alejando un poco. Creo que impl contextual es un buen compromiso, y no creo que buscar este tipo de justificación sea necesario o particularmente útil (ciertamente no enseñaríamos la regla en términos de este tipo de conexión lógica ).

@solson Sí, tienes razón: los existenciales no se pueden flotar. Este no se sostiene en general:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

mientras que estos se mantienen en general:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

El último es responsable de la reinterpretación de los existenciales en los argumentos como genéricos.

Editar: Vaya, (∀A. g(A) → T) → (∃A. g(A)) → T se mantiene.

He publicado un RFC con una propuesta detallada para expandir y estabilizar impl Trait . Se basa en gran parte de la discusión sobre este y los hilos anteriores.

Vale la pena señalar que se ha aceptado https://github.com/rust-lang/rfcs/pull/1951 .

¿Cuál es el estado de esto actualmente? Tenemos un RFC que aterrizó, tenemos personas que usan la implementación inicial, pero no tengo claro qué elementos se deben hacer.

Se encontró en #43869 que la función -> impl Trait no admite un cuerpo puramente divergente:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

¿Se espera esto (dado que ! no implica Iterator ), o se considera un error?

¿Qué pasa con la definición de tipos inferidos, que no solo podrían usarse como valores de retorno, sino como cualquier cosa (supongo) para la que se puede usar un tipo actualmente?
Algo como:
type Foo: FnOnce() -> f32 = #[infer];
O con una palabra clave:
infer Foo: FnOnce() -> f32;

El tipo Foo podría usarse como tipo de devolución, tipo de parámetro o cualquier otra cosa para la que se pueda usar un tipo, pero sería ilegal usarlo en dos lugares diferentes que requieren un tipo diferente, incluso si eso type implementa FnOnce() -> f32 en ambos casos. Por ejemplo, lo siguiente no compilaría:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Esto no debería compilarse porque aunque los tipos de devolución de return_closure y return_closure2 son ambos FnOnce() -> f32 , sus tipos son en realidad diferentes, porque no hay dos cierres que tengan el mismo tipo en Rust . Para compilar lo anterior, necesitaría definir dos tipos inferidos diferentes:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Creo que lo que está sucediendo aquí es bastante obvio después de ver el código, incluso si no sabía de antemano qué hace la palabra clave inferir, y es muy flexible.

La palabra clave inferir (o macro) esencialmente le diría al compilador que averigüe cuál es el tipo, según dónde se use. Si el compilador no puede inferir el tipo, arrojaría un error, esto podría suceder cuando no hay suficiente información para acotar qué tipo debe ser (si el tipo inferido no se usa en ninguna parte, por ejemplo, aunque tal vez sea mejor hacer que ese caso específico sea una advertencia), o cuando es imposible encontrar un tipo que se ajuste a todos los lugares donde se usa (como en el ejemplo anterior).

@cramertj Ahh, por eso este problema se ha vuelto tan silencioso.

Entonces, @cramertj me preguntaba cómo pensaba que sería mejor resolver el problema de las regiones con límite tardío que encontraron en su PR. Mi opinión es que probablemente queramos "rediseñar" un poco nuestra implementación para tratar de esperar el modelo anonymous type Foo .

Por contexto, la idea es más o menos que

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

sería (más o menos) desazucarado a algo como esto

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Tenga en cuenta que, en este formulario, puede ver qué parámetros genéricos se capturan porque aparecen como argumentos para Foo ; en particular, 'b no se captura, porque no aparece en la referencia del rasgo en de todos modos, pero los parámetros de tipo T y U siempre lo son.

De todos modos, en la actualidad en el compilador, cuando tiene una referencia impl Debug , creamos una identificación definida que, efectivamente, representa este tipo anónimo. Luego tenemos la consulta generics_of , que calcula sus parámetros genéricos. En este momento, esto devuelve lo mismo que el contexto "encerrado", es decir, la función foo . Esto es lo que queremos cambiar.

En el "otro lado", es decir, en la firma de foo , representamos impl Foo como TyAnon . Esto es básicamente correcto: el TyAnon representa la referencia a Foo que vemos en la eliminación de azúcar anterior. Pero la forma en que obtenemos los "sustancias" para este tipo es usar la función "identidad" , que claramente es incorrecta, o al menos no generaliza.

Entonces, en particular, aquí se está produciendo una especie de "violación del espacio de nombres". Cuando generamos las sustituciones de "identidad" para un elemento, eso normalmente nos da las sustituciones que usaríamos al verificar el tipo de ese elemento, es decir, con todos sus parámetros genéricos en el alcance. Pero en este caso, creamos la referencia a Foo que aparece dentro de la función foo() , por lo que queremos que los parámetros genéricos de foo() aparezcan en Substs , no los de Foo . Esto funciona porque en este momento son lo mismo, pero en realidad no es correcto .

Creo que lo que deberíamos estar haciendo es algo como esto:

Primero, cuando calculamos los parámetros de tipo genérico de Foo (es decir, el tipo anónimo en sí), comenzaríamos a construir un nuevo conjunto de genéricos. Naturalmente incluiría los tipos. Pero durante toda la vida, caminaríamos sobre los límites de los rasgos e identificaríamos cada una de las regiones que aparecen dentro. Eso es muy similar a este código existente que escribió cramertj , excepto que no queremos acumular def-ids, porque no todas las regiones en el alcance tienen def-ids.

Creo que lo que queremos hacer es acumular el conjunto de regiones que aparecen y ponerlas en algún orden, y también rastrear los valores de esas regiones desde el punto de vista de foo() . Es un poco molesto hacer esto, porque no tenemos una estructura de datos uniforme que represente una región lógica. (Solíamos tener la noción de FreeRegion , que casi habría funcionado, pero ya no usamos FreeRegion para material enlazado en tiempo de ejecución, solo para material enlazado en tiempo de ejecución).

Quizás la mejor y más fácil opción sería simplemente usar un Region<'tcx> , pero tendría que cambiar las profundidades del índice de debruijn a medida que avanza para "cancelar" cualquier carpeta que se haya introducido. Sin embargo, esta es quizás la mejor opción.

Entonces, básicamente, a medida que recibimos devoluciones de llamada en visit_lifetime , las transformaríamos en Region<'tcx> expresadas en la profundidad inicial (tendremos que realizar un seguimiento a medida que pasamos por las carpetas). Los acumularemos en un vector, eliminando los duplicados.

Cuando hayamos terminado, tenemos dos cosas:

  • Primero, para cada región del vector, necesitamos crear un parámetro de región genérico. Todos pueden tener nombres anónimos o lo que sea, no importa mucho (aunque tal vez necesitemos que tengan identificadores definidos o algo...? Tengo que mirar las estructuras de datos de RegionParameterDef ...) .
  • Segundo, las regiones en el vector también son las cosas que queremos usar para los "sustancias".

OK, lo siento si eso es un críptico. No puedo entender cómo decirlo más claramente. Sin embargo, algo de lo que no estoy seguro: en este momento, siento que nuestro manejo de las regiones es bastante complejo, así que tal vez haya una manera de refactorizar las cosas para que sea más uniforme. Apostaría $10 a que @eddyb tiene algunas ideas aquí. ;)

@nikomatsakis Creo que mucho de eso es similar a lo que le dije a @cramertj , ¡pero más desarrollado!

Estuve pensando en impl Trait existenciales y encontré un caso curioso en el que creo que debemos proceder con precaución. Considere esta función:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Como puede validar en el juego , este código se compila hoy. Sin embargo, si profundizamos en lo que está sucediendo, destaca algo que tiene un peligro de "compatibilidad hacia adelante" que me preocupa.

Específicamente, está claro cómo deducimos el tipo que se devuelve aquí ( () ). Está menos claro cómo deducimos el tipo del parámetro impl Debug . Es decir, puede pensar en este valor de retorno como -> ?T donde ?T: Foo<?U> . Tenemos que deducir los valores de ?T y ?U basándonos solo en el hecho de que ?T = () .

En este momento, hacemos esto aprovechando el hecho de que solo existe un impl. Sin embargo, esta es una propiedad frágil. Si se agrega un nuevo impl, el código ya no se compilará , porque ahora no podemos determinar de manera única qué debe ser ?U .

Esto puede suceder en muchos escenarios en Rust, lo cual es bastante preocupante, pero ortogonal, pero hay algo diferente en el caso de impl Trait . En el caso de impl Trait , no tenemos una forma para que el usuario agregue anotaciones de tipo para guiar la inferencia. Tampoco tenemos realmente un plan para tal manera. La única solución es cambiar la interfaz fn a impl Foo<()> o algo más explícito.

En el futuro, usando abstract type , uno podría imaginar permitir a los usuarios dar explícitamente el valor oculto (o tal vez solo sugerencias incompletas, usando _ ), lo que luego podría ayudar a inferir, manteniendo aproximadamente el misma interfaz pública

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Aún así, creo que sería prudente evitar estabilizar los usos "anidados" de Trait impl existencial, excepto en enlaces de tipos asociados (por ejemplo, impl Iterator<Item = impl Debug> no sufre de estas ambigüedades).

En el caso de Rasgo impl, no tenemos una forma para que el usuario agregue anotaciones de tipo para guiar la inferencia. Tampoco tenemos realmente un plan para tal manera.

¿Quizás podría parecerse a UFCS? por ejemplo, <() as Foo<()>> -- sin cambiar el tipo como un simple as , simplemente desambiguándolo. Actualmente, esta es una sintaxis no válida, ya que espera que le sigan :: y más.

Acabo de encontrar un caso interesante con respecto a la inferencia de tipo con rasgo impl para Fn :
El siguiente código compila muy bien :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

Si comentamos la Sublínea, se arroja un error de compilación :

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Esto no parece estar relacionado con impl Trait , es cierto para la inferencia en general. Pruebe esta ligera modificación de su ejemplo:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

Parece que esto está cerrado ahora:

ICE al interactuar con elision

Una cosa que no veo enumerada en este problema o en la discusión es la capacidad de almacenar cierres y generadores, que no son proporcionados por la persona que llama, en campos de estructura. En este momento, esto es posible pero se ve feo: debe agregar un parámetro de tipo a la estructura para cada campo de cierre/generador y luego, en la firma de la función constructora, reemplace ese parámetro de tipo con impl FnMut/impl Generator . Aquí hay un ejemplo , y funciona, ¡lo cual es genial! Pero deja mucho que desear. Sería mucho mejor si pudiera deshacerse del parámetro de tipo:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait puede no ser la forma correcta de hacer esto, probablemente tipos abstractos, si he leído y entendido RFC 2071 correctamente. Lo que necesitamos es algo que podamos escribir en la definición de la estructura para poder inferir el tipo real ( [generator@src/main.rs:15:17: 21:10 _] ).

Los tipos abstractos de

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

¿Hay una ruta alternativa si es el impl Generator de otra persona el que quiero poner en mi estructura, pero no crearon un abstract type para que yo lo use?

@scottmcm Aún puede declarar su propio abstract type :

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Espera, ¿los tipos abstractos ya están disponibles todas las noches? ¿Dónde está el PR?

@alexreg No, no lo son.

Editar: ¡ Saludos, visitantes del futuro! El problema a continuación ha sido resuelto.


Me gustaría llamar la atención sobre este caso de uso extremo funky que aparece en #47348

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

¿Debería permitirse devolver una proyección en impl Trait como esta? (porque actualmente, __es.__)

No pude encontrar ninguna discusión sobre el uso como este, ni pude encontrar ningún caso de prueba para ello.

@ExpHP Hmmm . Parece problemático, por la misma razón que impl Foo<impl Bar> es problemático. Básicamente, no tenemos ninguna restricción real sobre el tipo en cuestión, solo sobre las cosas que se proyectan de él.

Creo que queremos reutilizar la lógica en torno a los "parámetros de tipo restringido" de impls. En resumen, especificar el tipo de devolución debería "restringir" el impl Sub . La función a la que me refiero es esta:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Un poco de clasificación para las personas a las que les gustan las casillas de verificación:

  • #46464 está hecho -> casilla de verificación
  • #48072 está hecho -> casilla de verificación

fusión @rfcbot fcp

Propongo que estabilicemos las funciones conservative_impl_trait y universal_impl_trait , con un cambio pendiente (una corrección de https://github.com/rust-lang/rust/issues/46541).

Pruebas que documentan la semántica actual

Las pruebas para estas características se pueden encontrar en los siguientes directorios:

run-pass/impl-trait
ui/impl-trait
compilar-fallar/impl-trait

Preguntas resueltas durante la implementación

Los detalles del análisis de impl Trait se resolvieron en RFC 2250 y se implementaron en https://github.com/rust-lang/rust/pull/45294.

impl Trait ha sido prohibido en la posición de tipo no asociado anidado y en ciertas posiciones de ruta calificada para evitar la ambigüedad. Esto se implementó en https://github.com/rust-lang/rust/pull/48084.

Características inestables restantes

Después de esta estabilización, será posible usar impl Trait en la posición del argumento y en la posición de retorno de las funciones que no son rasgos. Sin embargo, el uso de impl Trait en cualquier parte de la sintaxis de Fn todavía no está permitido para permitir futuras iteraciones de diseño. Además, no se permite especificar manualmente los parámetros de tipo de las funciones que usan impl Trait en la posición del argumento.

El miembro del equipo @cramertj ha propuesto fusionar esto. El siguiente paso es la revisión por parte del resto de equipos etiquetados:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sinbarcos

No hay preocupaciones actualmente en la lista.

Una vez que la mayoría de los revisores lo aprueben (y ninguno se oponga), entrará en su período final de comentarios. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡dígalo!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

Después de esta estabilización, será posible usar impl Trait en posición de argumento y posición de retorno de funciones que no son de rasgo. Sin embargo, el uso de Trait impl en cualquier parte de la sintaxis Fn todavía no está permitido para permitir futuras iteraciones de diseño. Además, no se permite especificar manualmente los parámetros de tipo de las funciones que usan Trait impl en la posición del argumento.

¿Cuál es el estado de usar impl Trait en posiciones de argumento/retorno en funciones de rasgos, o en sintaxis Fn, para el caso?

@alexreg La posición de retorno impl Trait en rasgos está bloqueada en un RFC, aunque RFC 2071 permitirá una funcionalidad similar una vez implementada. La posición de argumento impl Trait en las características no está bloqueada en ninguna característica técnica que yo sepa, pero no se permitió explícitamente en el RFC, por lo que se ha omitido por el momento.

impl Trait sintaxis de Fn está bloqueada en HRTB de nivel de tipo, ya que algunas personas piensan que T: Fn(impl Trait) debería reducir el azúcar a T: for<X: Trait> Fn(X) . impl Trait sintaxis de Fn no está bloqueada por ningún motivo técnico que yo sepa, pero no se permitió en el RFC a la espera de más trabajo de diseño. vea otro RFC o al menos un FCP separado antes de estabilizar esto.

@cramertj Bien, gracias por la actualización. Con suerte, podemos ver que estas dos funciones que no están bloqueadas en nada obtengan el visto bueno pronto, después de algunas discusiones. La eliminación de azúcar tiene sentido, en la posición del argumento, un argumento foo: T donde T: Trait es equivalente a foo: impl Trait , a menos que me equivoque.

Preocupación: https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 sigue siendo el mismo. ¿Es posible permitir lo siguiente?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm No, eso no es posible en este momento. Esa función devuelve ! , que no implementa el rasgo que proporcionó, ni tenemos un mecanismo para convertirlo a un tipo apropiado. Esto es desafortunado, pero no hay una manera fácil de solucionarlo en este momento (aparte de implementar más funciones para ! ). También es compatible con versiones anteriores para corregirlo en el futuro, ya que hacer que funcione permitiría compilar estrictamente más código.

La cuestión del turbopez solo se ha resuelto a medias. Al menos deberíamos advertir sobre impl Trait en argumentos de funciones efectivamente públicas, considerando impl Trait en argumentos como un tipo privado para el nuevo cheque privado en público .

La motivación es evitar que las bibliotecas rompan los turbofishes de los usuarios cambiando un argumento de genérico explícito a impl Trait . Todavía no tenemos una buena guía de referencia para que libs sepa qué es y qué no es un cambio importante y es muy poco probable que las pruebas lo detecten. Este tema no se discutió lo suficiente, si deseamos estabilizarnos antes de decidir por completo, deberíamos al menos apuntar el arma lejos del pie de los autores de lib.

La motivación es evitar que las bibliotecas rompan los turbofishes de los usuarios cambiando un argumento de genérico explícito a impl Trait .

Espero que cuando esto comience a suceder y la gente comience a quejarse, las personas del equipo de lang que actualmente tienen dudas se convencerán de que impl Trait debería admitir el suministro explícito de argumentos de tipo con turbofish.

@leodasvacas

La cuestión del turbopez solo se ha resuelto a medias. Al menos deberíamos advertir sobre Rasgo impl en argumentos de funciones efectivamente públicas, considerando Rasgo impl en argumentos como un tipo privado para el nuevo cheque privado en público.

No estoy de acuerdo, esto se ha resuelto. Por el momento, no estamos permitiendo turbofish para estas funciones por completo. Cambiar la firma de una función pública para usar impl Trait lugar de parámetros genéricos explícitos es un cambio importante.

Si permitimos turbofish para estas funciones en el futuro, probablemente solo permitirá especificar parámetros que no sean de tipo impl Trait .

:bell: Esto ahora está entrando en su período final de comentarios , según la revisión anterior . :campana:

Debo agregar que no quiero estabilizarme hasta que https://github.com/rust-lang/rust/pull/49041 aterrice. (Pero espero que eso sea pronto).

Así que #49041 contiene una solución para #46541, pero esa solución tiene más impacto de lo que anticipé, por ejemplo, el compilador no arranca ahora, y me está dando una medida de pausa sobre el curso correcto aquí. El problema en #49041 es que accidentalmente podemos permitir que se filtren vidas que no deberíamos. Así es como esto se manifiesta en el compilador. Podríamos tener un método como este:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

La clave aquí es que TyCtxt es invariable con respecto a 'tcx y 'gcx , por lo que deben aparecer en el tipo de devolución. Y, sin embargo, solo 'cx y 'tcx aparecen en los límites del rasgo impl, por lo que se supone que solo se "capturan" esas dos vidas. El antiguo compilador aceptaba esto porque 'gcx: 'cx , pero eso no es realmente correcto si piensas en la eliminación de azúcar que tenemos en mente. Esa eliminación de azúcar crearía un tipo abstracto como este:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

y, sin embargo, el valor para este tipo abstracto sería Baz<'cx, 'gcx, 'tcx> -- ¡pero 'gcx no está dentro del alcance!

La solución aquí es que tenemos que nombrar 'gcx en los límites. Esto es un poco molesto de hacer; no podemos usar 'cx + 'gcx . Supongo que podemos hacer un rasgo ficticio:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

y luego devolver algo como esto impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Algo que olvidé tener en cuenta: si el tipo de retorno declarado fuera dyn Iterator<Item = &'tcx Foo> + 'cx , estaría bien, porque se espera que los tipos dyn borren vidas. Por lo tanto, no creo que sea posible ninguna falta de solidez aquí, suponiendo que no pueda hacer nada problemático con un impl Trait que no sería posible con un dyn Trait .

Uno podría imaginar vagamente la idea de que el valor del tipo abstracto es un existencial similar: exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

Sin embargo, me parece bien estabilizar un subconjunto conservador (que descarta el fns anterior) y revisar esto como una posible expansión más adelante, una vez que hayamos decidido cómo queremos pensar al respecto.

ACTUALIZACIÓN: Para aclarar mi significado sobre los rasgos de dyn : digo que pueden "ocultar" toda la vida como 'gcx siempre que el límite ( 'cx , aquí) asegure que 'gcx seguirá activo donde sea que se use dyn Trait .

@nikomatsakis Ese es un ejemplo interesante, pero no creo que cambie el cálculo básico aquí, es decir, queremos que todas las vidas relevantes queden claras solo con el tipo de devolución.

El rasgo Captures parece un enfoque bueno y ligero para esta situación. ¿Parece que podría entrar en std::marker como inestable por el momento?

@nikomatsakis Su comentario de seguimiento me hizo darme cuenta de que no había ensamblado todas las piezas aquí para comprender por qué podría esperar eludir el 'gcx en este caso, es decir, 'gcx no es un "vida útil relevante" desde el punto de vista del cliente. En cualquier caso, empezar de forma conservadora parece correcto.

Mi opinión personal es que https://github.com/rust-lang/rust/issues/46541 no es realmente un error, es el comportamiento que esperaría, no veo cómo podría hacerse incorrecto, y es un dolor trabajar alrededor. En mi opinión, debería ser posible devolver un tipo que implemente Trait y sobreviva el tiempo 'a vida impl Trait + 'a , sin importar qué otros tiempos de vida contenga. Sin embargo, me parece bien estabilizar un enfoque más conservador para comenzar si eso es lo que prefiere @rust-lang/lang.

(Otra cosa para aclarar: la única vez que obtendrá errores con la solución en #49041 es cuando el tipo oculto es invariable con respecto a la vida útil faltante 'gcx , por lo que esto probablemente ocurre relativamente raramente).

@cramertj

Mi opinión personal es que #46541 no es realmente un error, es el comportamiento que esperaría, no veo cómo podría fallar, y es una molestia solucionarlo.

Simpatizo con ese punto de vista, pero me resisto a estabilizar algo para lo cual no entendemos cómo quitarle el azúcar (por ejemplo, porque parece depender de una vaga noción de vidas existenciales).

@rfcbot se refiere a sitios de devolución múltiple

Me gustaría registrar una preocupación final con el rasgo impl existencial. Una fracción sustancial de las veces que quiero usar el rasgo impl, en realidad quiero devolver más de un tipo. Por ejemplo:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Por supuesto, esto no funciona hoy en día, y definitivamente está fuera de alcance para que funcione. Sin embargo, la forma en que funciona ahora el rasgo impl, estamos cerrando efectivamente la puerta para que funcione alguna vez (con esa sintaxis). Esto se debe a que, actualmente, puede acumular restricciones de varios sitios de devolución:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Aquí, el tipo inferido es (i32, bool) , donde el primer return restringe la parte i32 y el segundo return restringe la parte bool .

Esto implica que nunca podríamos admitir casos en los que las dos declaraciones return no se unifican (como en mi primer ejemplo), o de lo contrario sería muy molesto hacerlo.

Me pregunto si deberíamos poner una restricción que requiera que cada return (en general, cada fuente de una restricción) debe especificarse completamente de forma independiente. (¿Y los unificamos después del hecho?)

Esto haría que mi segundo ejemplo fuera ilegal y nos dejaría espacio para apoyar potencialmente el primer caso en algún momento en el futuro.

@rfcbot resuelve múltiples sitios de retorno

Así que hablé un poco con @cramertj en #rust-lang . Estábamos discutiendo la idea de hacer que el "retorno anticipado" sea inestable para impl Trait , para que eventualmente podamos cambiarlo.

Argumentaron que sería mejor tener una aceptación explícita para este tipo de sintaxis, específicamente porque hay otros casos (p. ej., let x: impl Trait = if { ... } else { ... } ) en los que uno lo querría, y no podemos esperar para manejarlos todos implícitamente (definitivamente no).

Encuentro esto bastante persuasivo. Antes de esto, supuse que tendríamos alguna sintaxis de aceptación aquí de todos modos, pero solo quería asegurarme de que no cerráramos ninguna puerta prematuramente. Después de todo, explicar cuándo necesita insertar el "calce dinámico" es un poco complicado.

@nikomatsakis Solo mi opinión posiblemente menos informada: si bien habilitar una función para devolver uno de los múltiples tipos posibles en tiempo de ejecución puede ser útil, sería reacio a tener la misma sintaxis para la inferencia de tipo de retorno estático a un solo tipo, y permitiendo situaciones en las que se requiere internamente alguna decisión en tiempo de ejecución (lo que acaba de llamar el "ajuste dinámico").

Ese primer ejemplo de foo , hasta donde entendí el problema, podría resolverse en (1) un Iterator<Item = u32> encuadrado + tipo borrado, o (2) un tipo de suma de std::option::Iter o std::slice::Iter , que a su vez derivaría en una implementación de Iterator . Tratando de ser breve, ya que hubo algunas actualizaciones en la discusión (es decir, ahora leo los registros de IRC) y se está volviendo más difícil de entender: ciertamente estaría de acuerdo con una sintaxis similar a dyn para el shim dinámico, aunque también comprenda que llamarlo dyn podría no ser lo ideal.

Enchufe desvergonzado y una pequeña nota solo para el registro: puede obtener tipos de sumas y productos "anónimos" fácilmente con:

@Centril Sí, esas cosas de frunk son geniales. Sin embargo, tenga en cuenta que para que CoprodInjector::inject funcione, el tipo resultante tiene que ser inferible, lo que normalmente es imposible sin nombrar el tipo resultante (por ejemplo, -> Coprod!(A, B, C) ). A menudo sucede que está trabajando con tipos sin nombre, por lo que necesitaría -> Coprod!(impl Trait, impl Trait, impl Trait) , que fallará en la inferencia porque no sabe qué variante debe contener qué tipo de impl Trait .

@cramertj Muy cierto ( Map<Namable, Unnameable> ).

La idea de enum impl Trait se ha discutido anteriormente en https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695

@Centril Sí, eso es cierto. Estoy pensando específicamente en futuros, donde a menudo escribo cosas como

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj No diría que la enumeración anónima es similar a enum impl Trait , porque no podemos concluir X: Tr && Y: Tr(X|Y): Tr (contraejemplo: Default rasgo impl Future for (X|Y|Z|...) manualmente.

@kennytm Presumiblemente, nos gustaría generar automáticamente algunas impls de rasgos para enumeraciones anónimas, por lo que parece básicamente la misma característica.

@cramertj Dado que se puede nombrar una enumeración anónima (heh), si se genera una Default para (i32|String) , podríamos escribir <(i32|String)>::default() . OTOH <enum impl Default>::default() simplemente no se compilará, por lo que no importa lo que generemos automáticamente, seguirá siendo seguro ya que no se puede invocar en absoluto.

Sin embargo, hay algunos casos en los que la generación automática aún puede causar problemas con enum impl Trait . Considerar

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

Es perfectamente normal que, si tenemos mut rng: (XorShiftRng|IsaacRng) podamos calcular rng.next_u32() o rng.gen::<u64>() . Sin embargo, rng.gen_iter::<u16>() no se puede construir porque la generación automática solo puede producir (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) , mientras que lo que realmente queremos es Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Tal vez el compilador pueda rechazar automáticamente una llamada insegura de delegación al igual que la verificación de Sized ).

FWIW, esta característica me parece más cercana en espíritu a los cierres que a las tuplas (que son, por supuesto, la contraparte anónima struct de los hipotéticos enum anónimos). Las formas en que estas cosas son "anónimas" son diferentes.

Para los struct anónimos y los enum (tuplas y "separaciones"), el "anónimo" tiene el sentido de tipos "estructurales" (a diferencia de los "nominales"): están integrados, son completamente genéricos sobre sus tipos de componentes y no son una declaración con nombre en ningún archivo fuente. Pero el programador aún los escribe y los usa como cualquier otro tipo, las implementaciones de rasgos para ellos se escriben explícitamente como de costumbre, y no son particularmente mágicos (aparte de tener una sintaxis incorporada y ser 'variadic', que otros tipos todavía no puede ser). En cierto sentido, tienen un nombre, pero en lugar de ser alfanuméricos, su 'nombre' es la sintaxis utilizada para escribirlos (paréntesis y comas).

Los cierres, en cambio, son anónimos en el sentido de que su nombre es secreto . El compilador genera un tipo nuevo con un nombre nuevo cada vez que escribe uno, y no hay forma de averiguar cuál es ese nombre o referirse a él, incluso si quisiera. El compilador implementa uno o dos rasgos para este tipo de secreto, y la única forma en que puede interactuar con él es a través de estos rasgos.

Ser capaz de devolver diferentes tipos de diferentes ramas de un if , detrás de un impl Trait , parece más cercano a lo último: el compilador genera implícitamente un tipo para contener las diferentes ramas, implementa el rasgo solicitado en él para enviarlo al apropiado, y el programador nunca escribe o ve cuál es ese tipo, y no puede referirse a él ni tiene ninguna razón real para querer hacerlo.

(De hecho, esta característica se siente un poco relacionada con los "objetos literales" hipotéticos, que serían para otras características la sintaxis de cierre existente para Fn . Es decir, en lugar de una sola expresión lambda, usted implementaría cada método del rasgo dado (con self implícito) usando las variables en el alcance, y el compilador generaría un tipo anónimo para mantener las upvars e implementaría el rasgo dado para él, sería tener un modo opcional move de la misma manera, y así sucesivamente. De todos modos, sospecho que una forma diferente de expresar if foo() { (some future) } else { (other future) } sería object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (bueno, también necesitarías para elevar el resultado de foo() a let para que solo se ejecute una vez). Eso es bastante menos ergonómico y probablemente no debería considerarse como una *alternativa real a la otra característica, pero sugiere que hay una relación. Tal vez la primera podría convertirse en la segunda, o algo así).

@glaebhoerl ¡ esa es una idea muy interesante! También hay algo de la técnica anterior de Java aquí.

Algunos pensamientos en la parte superior de mi cabeza (así que no muy horneados):

  1. [bikeshed] el prefijo object sugiere que este es un objeto de rasgo en lugar de solo existencial, pero no lo es.

Una posible sintaxis alternativa:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [macros/sugar] puede proporcionar algo de azúcar sintáctico trivial para que obtenga:
future!(if foo() { a.poll() } else { b.poll() })

Sí, la pregunta de sintaxis es un desastre porque no está claro si quieres inspirarte en struct literales, cierres o impl bloques :) Acabo de elegir uno de los que tengo en la cabeza, por ejemplo motivo. (De todos modos, mi punto principal no era que deberíamos ir y agregar objetos literales [aunque deberíamos] sino que creo que los enum anónimos son una pista falsa aquí [aunque también deberíamos agregarlos]).

Ser capaz de devolver diferentes tipos de diferentes ramas de un if, detrás de un rasgo impl, parece más cercano a este último: el compilador genera implícitamente un tipo para contener las diferentes ramas, implementa el rasgo solicitado para enviarlo al adecuado, y el programador nunca escribe o ve cuál es ese tipo, y no puede referirse a él ni tiene ninguna razón real para querer hacerlo.

Mmm. Así que asumí que en lugar de generar "nombres nuevos" para los tipos de enumeración, aprovecharíamos los tipos | , correspondientes a impls como este:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Obviamente, habría problemas de coherencia con esto, en el sentido de que múltiples funciones generarán impls idénticos. Pero incluso dejando eso de lado, ahora me doy cuenta de que esta idea puede no funcionar por otras razones, por ejemplo, si hay varios tipos asociados, en algunos contextos pueden tener que ser iguales, pero en otros se les permite ser diferentes. Por ejemplo, tal vez volvamos:

-> impl IntoIterator<Item = Y>

pero en otro lugar lo hacemos

-> impl IntoIterator<IntoIter = X, Item = Y>

Supongo que serían dos impls superpuestos que no se pueden "unir"; bueno, tal vez con especialización.

De todos modos, la noción de "enumeraciones secretas" parece más limpia, supongo.

Me gustaría registrar una preocupación final con el rasgo impl existencial. Una fracción sustancial de las veces que quiero usar el rasgo impl, en realidad quiero devolver más de un tipo.

@nikomatsakis : ¿Es justo decir que, en este caso, lo que se devuelve está más cerca de un dyn Trait que de un impl Trait , porque el valor de retorno sintético/anónimo implementa algo parecido al envío dinámico?

cc https://github.com/rust-lang/rust/issues/49288 , un problema al que me he enfrentado mucho últimamente trabajando con Future s y Future - métodos de características de retorno.

Dado que esta es la última oportunidad antes de que FCP cierre, me gustaría presentar un último argumento en contra de los rasgos automáticos automáticos. Me doy cuenta de que esto es un poco de última hora, así que como máximo me gustaría abordar este problema formalmente antes de comprometernos con la implementación actual.

Para aclarar a cualquiera que no haya estado siguiendo impl Trait , este es el problema que estoy presentando. Un tipo representado por tipos impl X actualmente implementa automáticamente rasgos automáticos si y solo si el tipo concreto detrás de ellos implementa dichos rasgos automáticos. Concretamente, si se realiza el siguiente cambio de código, la función continuará compilando, pero cualquier uso de la función que dependa del hecho de que el tipo que devuelve implementa Send fallará.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(ejemplo más simple: trabajando , los cambios internos causan fallas )

Este problema no está claro. Hubo una decisión muy deliberada de tener "fugas" de características automáticas: si no lo hiciéramos, tendríamos que poner + !Send + !Sync en cada función que devuelva algo que no sea Enviar o Sincronizar, y tiene una historia poco clara con posibles otros rasgos automáticos personalizados que simplemente no podrían implementarse en el tipo concreto que devuelve la función. Estos son dos problemas que abordaré más adelante.

Primero, me gustaría simplemente expresar mi objeción al problema: esto permite cambiar el cuerpo de una función para cambiar la API pública. Esto reduce directamente la mantenibilidad del código.

A lo largo del desarrollo de rust, se han tomado decisiones que se equivocan en el lado de la verbosidad sobre la usabilidad. Cuando los recién llegados ven esto, piensan que es verbosidad por verbosidad, pero este no es el caso. Cada decisión, ya sea que las estructuras no implementen Copia automáticamente o que todos los tipos sean explícitos en las firmas de funciones, es por el bien de la mantenibilidad.

Cuando presento a la gente Rust, claro, puedo mostrarles velocidad, productividad, seguridad de la memoria. Pero ir tiene velocidad. Ada tiene seguridad en la memoria. Python tiene productividad. Lo que Rust tiene supera a todos estos, tiene mantenibilidad. Cuando el autor de una biblioteca quiere cambiar un algoritmo para que sea más eficiente, o cuando quiere rehacer la estructura de una caja, tiene una fuerte garantía del compilador de que les avisará cuando cometan errores. En rust, puedo estar seguro de que mi código seguirá funcionando no solo en términos de seguridad de la memoria, sino también en términos de lógica e interfaz. _Cada interfaz de función en Rust se puede representar completamente mediante la declaración de tipo de la función_.

Estabilizar impl Trait tal como está tiene una gran posibilidad de ir en contra de esta creencia. Claro, es extremadamente agradable por el hecho de escribir código rápidamente, pero si quiero hacer un prototipo, usaré Python. Rust es el lenguaje de elección cuando se necesita capacidad de mantenimiento a largo plazo, no código de solo escritura a corto plazo.


Digo que solo hay una "gran posibilidad" de que esto sea malo aquí porque, nuevamente, el problema no está claro. La idea completa de 'rasgos automáticos' en primer lugar no es explícita. Send y Sync se implementan en función del contenido de una estructura, no de la declaración pública. Dado que esta decisión funcionó para el óxido, impl Trait actuando de manera similar también podría funcionar bien.

Sin embargo, las funciones y las estructuras se usan de manera diferente en una base de código y estos no son los mismos problemas.

Al modificar los campos de una estructura, incluso los campos privados, inmediatamente queda claro que se está cambiando el contenido real de la misma. Las estructuras con campos que no son de envío o sincronización tomaron esa decisión, y los mantenedores de bibliotecas saben que deben verificar dos veces cuando un RP cambia los campos de una estructura.

Al modificar las partes internas de una función, definitivamente está claro que uno puede afectar tanto el rendimiento como la corrección. Sin embargo, en Rust, no necesitamos comprobar que estamos devolviendo el tipo correcto. Las declaraciones de funciones son un contrato estricto que debemos cumplir y rustc cuida las espaldas. Hay una delgada línea entre los rasgos automáticos en las estructuras y los retornos de las funciones, pero cambiar las partes internas de una función es mucho más rutinario. Una vez que tengamos Future alimentados por generador completo, será aún más rutinario modificar las funciones que devuelven -> impl Future . Todos estos serán cambios que los autores deben buscar implementaciones modificadas de Enviar/Sincronizar si el compilador no las detecta.

Para resolver esto, podríamos decidir que esta es una carga de mantenimiento aceptable, como lo hizo la discusión original de RFC . Esta sección en el RFC de rasgo impl conservador presenta los argumentos más importantes para filtrar rasgos automáticos ("OIBIT" es el nombre antiguo para rasgos automáticos).

Ya expliqué mi respuesta principal a esto, pero aquí hay una última nota. Cambiar el diseño de una estructura no es tan común; se puede prevenir. La carga de mantenimiento para garantizar que las funciones continúen implementando los mismos rasgos automáticos es mayor que la de las estructuras simplemente porque las funciones cambian mucho más.


Como nota final, me gustaría decir que los rasgos automáticos automáticos no son la única opción. Es la opción que elegimos, pero la alternativa de los rasgos automáticos de exclusión voluntaria sigue siendo una alternativa.

Podríamos requerir funciones que devuelvan elementos que no sean de envío/sincronización para indicar + !Send + !Sync o para devolver un rasgo (¿posiblemente alias?) que tenga esos límites. Esta no sería una buena decisión, pero podría ser mejor que la que estamos eligiendo actualmente.

En cuanto a la preocupación con respecto a los rasgos automáticos personalizados, diría que cualquier rasgo automático nuevo no debe implementarse solo para los nuevos tipos introducidos después del rasgo automático. Esto podría proporcionar más problemas de los que puedo abordar ahora, pero no es uno que no podamos abordar con más diseño.


Esto es muy tardío y muy extenso, y estoy seguro de haber planteado estas objeciones antes. Me alegra poder comentar por última vez y asegurarme de que estamos totalmente de acuerdo con la decisión que estamos tomando.

Gracias por leer, y espero que la decisión final coloque a Rust en la mejor dirección posible.

Ampliando la reseña de

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Esto no es tan malo, tendrías que pensar en un buen nombre (que FutureNSS no lo es). El principal beneficio es que reduce el corte de papel incurrido por la repetición de los límites.

¿No sería posible estabilizar esta función con los requisitos para indicar los rasgos automáticos de forma explícita y más tarde tal vez eliminar esos requisitos una vez que encontremos una solución adecuada para ese problema de mantenimiento o una vez que estemos lo suficientemente seguros de que, de hecho, no hay cargas de mantenimiento por la decisión de levantar los requisitos?

¿Qué hay de requerir Send menos que esté marcado como !Send , pero no proporcionar Sync menos que esté marcado como Sync? ¿No se supone que Enviar es más común en comparación con Sync?

Como esto:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

¿Hay alguna incoherencia entre impl Trait en la posición del argumento y la posición de retorno wrt. caracteristicas de los autos?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Esto tiene sentido para la posición del argumento porque si se le permitiera asumir Enviar aquí, obtendría errores de monomorfización posteriores. Por supuesto, no se requiere que las reglas para la posición de retorno y la posición del argumento coincidan aquí, pero presenta un problema en términos de capacidad de aprendizaje.

En cuanto a la preocupación con respecto a los rasgos automáticos personalizados, diría que cualquier rasgo automático nuevo no debe implementarse solo para los nuevos tipos introducidos después del rasgo automático.

Bueno, esto es cierto para el próximo rasgo automático Unpin (solo no está implementado para los generadores autorreferenciales), pero... ¿eso parece ser pura suerte? ¿Es esta una limitación con la que realmente podemos vivir? No puedo creer que no habrá algo en el futuro que deba desactivarse para, por ejemplo, &mut o Rc ...

Creo que esto se ha discutido y, por supuesto, es muy tarde, pero todavía no estoy satisfecho con impl Trait en la posición de argumento.

Las habilidades para a) trabajar con cierres/futuros por valor, yb) tratar algunos tipos como "salidas" y, por lo tanto, detalles de implementación, son idiomáticas y lo han sido desde antes de 1.0, porque respaldan directamente los valores fundamentales de Rust de rendimiento, estabilidad, y seguridad.

-> impl Trait simplemente cumple una promesa hecha por 1.0, o elimina un caso límite, o generaliza características existentes: agrega tipos de salida a funciones, tomando el mismo mecanismo que siempre se ha usado para manejar tipos anónimos y aplicándolo en más casos. Puede haber sido más basado en principios comenzar con abstract type , es decir, tipos de salida para módulos, pero dado que Rust no tiene un sistema de módulos ML, el orden no es gran cosa.

fn f(t: impl Trait) cambio, parece que se agregó "solo porque podemos", lo que hace que el idioma sea más grande y extraño sin devolver lo suficiente a cambio. He luchado y no he podido encontrar un marco existente para encajarlo. Entiendo el argumento sobre la concisión de fn f(f: impl Fn(...) -> ...) , y la justificación de que los límites ya pueden estar en las cláusulas <T: Trait> y where , pero se sienten vacíos. No niegan las desventajas:

  • Ahora tiene que aprender dos sintaxis para los límites: al menos <> / where comparten una sola sintaxis.

    • Esto también crea un acantilado de aprendizaje y oscurece la idea de usar el mismo tipo genérico en varios lugares.

    • La nueva sintaxis hace que sea más difícil saber sobre qué es genérica una función: debe escanear toda la lista de argumentos.

  • Ahora, lo que debería ser el detalle de implementación de una función (cómo declara sus parámetros de tipo) se convierte en parte de su interfaz, ¡porque no puede escribir su tipo!

    • Esto también se relaciona con las complicaciones de rasgos automáticos que se están discutiendo actualmente: más confusión sobre qué es la interfaz pública de una función y qué no lo es.

  • La analogía con dyn Trait es, sinceramente, falsa:

    • dyn Trait siempre significa lo mismo y no "infecta" las declaraciones que lo rodean más que a través del mecanismo de rasgo automático existente.

    • dyn Trait se puede usar en estructuras de datos, y este es realmente uno de sus principales casos de uso. impl Trait en estructuras de datos no tiene sentido sin mirar todos los usos de la estructura de datos.

    • Parte de lo que significa dyn Trait es el borrado de tipo, pero impl Trait no implica nada sobre su implementación.

    • El punto anterior será aún más confuso si introducimos genéricos no monomorfizados. De hecho, en tal situación, es probable que fn f(t: impl Trait) a) no funcionen con la nueva característica, y/o b) requieran aún más abogados de casos extremos como el problema con las características automáticas. ¡Imagina fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :grito:

Entonces, para mí, todo se reduce a que impl Trait en la posición de argumento agrega casos extremos, usa más el presupuesto de extrañeza, hace que el lenguaje se sienta más grande, etc. mientras que impl Trait en la posición de retorno unifica, simplifica y hace que el lenguaje se mantenga más unido.

¿Qué hay de solicitar Enviar a menos que esté marcado como !Enviar, pero no proporcionar Sincronización a menos que esté marcado como Sincronización? ¿No se supone que Enviar es más común en comparación con Sync?

Eso se siente muy... arbitrario y ad-hoc. Tal vez sea menos escribir, pero más recordar y más caos.

Idea de cobertizo para bicicletas aquí para no distraerme de mis puntos anteriores: en lugar de impl , use type ? Esa es la palabra clave utilizada para los tipos asociados, es probable que (una de) las palabras clave utilizadas para abstract type , sigue siendo bastante natural y sugiere más la idea de "tipos de salida para funciones":

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Gracias por leer, y espero que la decisión final coloque a Rust en la mejor dirección posible.

Agradezco la objeción bien redactada. Como señaló, los rasgos automáticos siempre han sido una elección deliberada para "exponer" algunos detalles de implementación que uno podría haber esperado que permanecieran ocultos. Creo que, hasta ahora, esa elección ha funcionado bastante bien, pero confieso que estoy constantemente nervioso al respecto.

Me parece que la pregunta importante es hasta qué punto las funciones son realmente diferentes de las estructuras:

Cambiar el diseño de una estructura no es tan común; se puede prevenir. La carga de mantenimiento para garantizar que las funciones continúen implementando los mismos rasgos automáticos es mayor que la de las estructuras simplemente porque las funciones cambian mucho más.

Es muy difícil saber qué tan cierto será esto. Parece que la regla general será que la introducción de Rc es algo que debe hacerse con precaución, no es tanto una cuestión de dónde lo almacena. (En realidad, el caso en el que realmente trabajo no es Rc sino la introducción de dyn Trait , ya que puede ser menos obvio).

Sospecho firmemente que en el código que devuelve futuros, será raro trabajar con tipos que no sean seguros para subprocesos, etc. Tenderá a evitar ese tipo de bibliotecas. (Además, por supuesto, siempre vale la pena tener pruebas que ejerciten su código en escenarios realistas).

En cualquier caso, esto es frustrante porque es el tipo de cosas que es difícil saber de antemano, sin importar cuánto tiempo de estabilización le demos.

Como nota final, me gustaría decir que los rasgos automáticos automáticos no son la única opción. Es la opción que elegimos, pero la alternativa de los rasgos automáticos de exclusión voluntaria sigue siendo una alternativa.

Cierto, aunque definitivamente me pone nervioso la idea de "destacar" rasgos automáticos específicos como Send . También se tiene en cuenta que hay otros casos de uso para el rasgo impl además de los futuros. Por ejemplo, devolver iteradores o cierres, y en esos casos, no es obvio que desee enviar o sincronizar de forma predeterminada. En cualquier caso, lo que realmente querría, y lo que estamos tratando de diferir =), es una especie de límite "condicional" (Enviar si T es Enviar). Esto es precisamente lo que te dan los rasgos automáticos.

@rpjohnst

Creo que esto se ha discutido.

De hecho, lo ha hecho :) desde el primer impl Trait RFC lo hace muchos años. (Woah, 2014. Me siento viejo.)

He luchado y no he podido encontrar un marco existente para encajarlo.

Yo siento todo lo contrario. Para mí, sin impl Trait en posición de argumento, impl Trait en posición de retorno se destaca aún más. El hilo unificador que veo es:

  • impl Trait -- donde aparece, indica que habrá "algún tipo monomorfizado que implemente Trait ". (La cuestión de quién especifica ese tipo, la persona que llama o la persona que recibe la llamada, depende de dónde aparezca el impl Trait ).
  • dyn Trait -- donde aparece, indica que habrá algún tipo que implemente Trait , pero que la elección del tipo se haga dinámicamente.

También hay planes para expandir el conjunto de lugares donde puede aparecer impl Trait , basándose en esa intuición. Por ejemplo, https://github.com/rust-lang/rfcs/pull/2071 permisos

let x: impl Trait = ...;

Se aplica el mismo principio: la elección del tipo se conoce estáticamente. De manera similar, el mismo RFC introduce abstract type (para lo cual impl Trait puede entenderse como una especie de azúcar sintáctico), que puede aparecer en impls de rasgos e incluso como miembros en módulos.

Idea de cobertizo para bicicletas aquí para no distraerme de mis puntos anteriores: en lugar de impl , use type ?

Personalmente, no me inclino a restablecer un cobertizo para bicicletas aquí. Pasamos bastante tiempo discutiendo la sintaxis en https://github.com/rust-lang/rfcs/pull/2071 y en otros lugares. No parece haber una "palabra clave perfecta", pero leer impl como "algún tipo que implementa" funciona bastante bien.

Permítanme agregar un poco más sobre la fuga automática de rasgos:

En primer lugar, en última instancia, creo que la fuga automática de rasgos es en realidad lo correcto aquí, precisamente porque es coherente con el resto del lenguaje. Los rasgos automáticos fueron, como dije antes, siempre una apuesta, pero parece haber sido uno que básicamente ha valido la pena. Simplemente no veo que impl Trait sea ​​tan diferente.

Pero también, estoy bastante nervioso por retrasarme aquí. Estoy de acuerdo en que hay otros puntos interesantes en el espacio de diseño y no estoy 100% seguro de que hayamos dado en el lugar correcto, pero no sé si alguna vez estaremos seguros de eso. Estoy bastante preocupado si nos retrasamos ahora, tendremos dificultades para cumplir con nuestra hoja de ruta para el año.

Finalmente, consideremos las implicaciones si me equivoco: Básicamente, de lo que estamos hablando aquí es de que semver se vuelve aún más sutil para juzgar. Esta es una preocupación, creo, pero que se puede mitigar de varias maneras. Por ejemplo, podemos usar lints que avise cuando se introduzcan los tipos !Send o !Sync . Durante mucho tiempo hemos hablado sobre la introducción de un verificador de semver que lo ayude a evitar violaciones accidentales de semver; este parece ser otro caso en el que eso ayudaría. En resumen, un problema, pero no creo que uno crítico.

Entonces, al menos a partir de este momento, todavía me siento inclinado a continuar el camino actual.

Personalmente, no me inclino a restablecer un cobertizo para bicicletas aquí.

Tampoco estoy muy interesado en eso; fue una ocurrencia tardía basada en mi impresión de que impl Trait en posición de argumento parece estar motivado por "rellenar agujeros" sintácticamente en lugar de semánticamente , lo que parece ser correcto dada su respuesta. :)

Para mí, sin impl Trait en posición de argumento, impl Trait en posición de retorno se destaca aún más.

Dada la analogía con los tipos asociados, esto se parece mucho a "sin type T en la posición del argumento, los tipos asociados se destacan aún más". Sospecho que esa objeción en particular no ha surgido porque la sintaxis que hemos elegido hace que parezca absurda: la sintaxis existente allí es lo suficientemente buena como para que nadie sienta la necesidad de un azúcar sintáctico como trait Trait<type SomeAssociatedType> .

Ya tenemos sintaxis para "algún tipo monomorfizado que implemente Trait ". En el caso de los rasgos, tenemos variantes especificadas tanto para el "llamador" como para el "llamado". En el caso de las funciones, solo tenemos la variante especificada por la persona que llama, por lo que necesitamos la nueva sintaxis para la variante especificada por la persona que llama.

La expansión de esta nueva sintaxis a las variables locales podría estar justificada, porque también es una situación similar a un tipo asociado: es una forma de ocultar + nombrar el tipo de salida de una expresión y es útil para reenviar los tipos de salida de las funciones de destinatario.

Como mencioné en mi comentario anterior, también soy fanático de abstract type . Es, nuevamente, simplemente una expansión del concepto de "tipo de salida" a los módulos. Y aplicar el uso de la inferencia de -> impl Trait , let x: impl Trait y abstract type los tipos asociados de los impls de rasgos también es excelente.

Lo que no me gusta es específicamente el concepto de agregar esta nueva sintaxis para argumentos de funciones . No hace lo mismo que ninguna de las otras funciones con las que se integra. Que hace lo mismo que la sintaxis que ya tenemos, sólo que con más casos de borde y menos aplicabilidad. :/

@nikomatsakis

Es muy difícil saber qué tan cierto será esto.

Me parece que deberíamos errar por el lado de ser conservadores entonces. ¿Podemos ganar más confianza en el diseño con más tiempo (permitiendo que la fuga automática de características se realice en una puerta de funciones separada y solo por la noche mientras estabilizamos el resto de impl Trait )? Siempre podemos agregar soporte para la fuga automática de rasgos más adelante si no la filtramos ahora.

Pero también, estoy bastante nervioso por retrasarme aquí. [..] Estoy bastante preocupado si nos retrasamos ahora, tendremos dificultades para cumplir con nuestra hoja de ruta para el año.

¡Comprensible! Sin embargo, y como estoy seguro de que ha considerado, las decisiones aquí vivirán con nosotros durante muchos años por venir.

Por ejemplo, podemos usar lints que avise cuando se introduzcan los tipos !Send o !Sync . Durante mucho tiempo hemos hablado sobre la introducción de un verificador de semver que lo ayude a evitar violaciones accidentales de semver; este parece ser otro caso en el que eso ayudaría. En resumen, un problema, pero no creo que uno crítico.

¡Es bueno escuchar esto! 🎉 Y creo que esto apacigua principalmente mis preocupaciones.

Cierto, aunque definitivamente me pone nervioso la idea de "destacar" rasgos automáticos específicos como Send .

Estoy muy de acuerdo con este sentimiento 👍.

En cualquier caso, lo que realmente querría, y lo que estamos tratando de diferir =), es una especie de límite "condicional" (Enviar si T es Enviar). Esto es precisamente lo que te dan los rasgos automáticos.

Siento que T: Send => Foo<T>: Send se entendería mejor si el código lo indicara explícitamente.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Aunque, como discutimos en WG-Traits, es posible que no obtenga ninguna inferencia aquí, por lo que siempre debe especificar Extra si desea algo que no sea Send , lo que sería un fastidio total .

@rpjohnst

La analogía con dyn Trait es, sinceramente, falsa:

Con respecto a impl Trait en posición de argumento es falso, pero no así con -> impl Trait ya que ambos son tipos existenciales.

  • Ahora, lo que debería ser el detalle de implementación de una función (cómo declara sus parámetros de tipo) se convierte en parte de su interfaz, ¡porque no puede escribir su tipo!

Me gustaría señalar que el orden de los parámetros de tipo nunca ha sido un detalle de implementación debido a turbofish, y en este sentido, creo que impl Trait puede ayudar ya que le permite dejar ciertos argumentos de tipo sin especificar en turbofish .

[..] la sintaxis existente allí es lo suficientemente buena como para que nadie sienta la necesidad de un azúcar sintáctico como rasgo Trait.

¿Nunca digas nunca? https://github.com/rust-lang/rfcs/issues/2274

Al igual que @nikomatsakis , realmente aprecio el cuidado puesto en estos comentarios de última hora; ¡Sé que puede sentirse como intentar arrojarse frente a un tren de carga, especialmente para una característica tan deseada como esta!


@daboross , quería profundizar un poco más en la idea de exclusión. A primera vista parece prometedor, porque nos permitiría declarar la firma en su totalidad, pero con valores predeterminados que hacen que el caso común sea conciso.

Desafortunadamente, sin embargo, se encuentra con algunos problemas una vez que comienza a ver el panorama general:

  • Si las características automáticas se trataron como exclusión voluntaria para impl Trait , también deberían serlo para dyn Trait .
  • Esto, por supuesto, se aplica incluso cuando estas construcciones se utilizan en posición de argumento.
  • Pero entonces, sería bastante extraño que los genéricos se comportaran de manera diferente. En otras palabras, para fn foo<T>(t: T) , razonablemente podría esperar T: Send por defecto.
  • Por supuesto, tenemos un mecanismo para esto, actualmente aplicado solo a Sized ; es un rasgo que se asume de forma predeterminada en todas partes, y por el cual se excluye escribiendo ?Sized

El mecanismo ?Sized sigue siendo uno de los aspectos más oscuros y difíciles de enseñar de Rust y, en general, hemos sido extremadamente reacios a expandirlo a otros conceptos. Usarlo para un concepto tan central como Send parece arriesgado, sin mencionar, por supuesto, que sería un gran cambio.

Sin embargo, lo que es más: realmente no queremos incluir una suposición de rasgo automático para los genéricos, porque parte de la belleza de los genéricos hoy en día es que puede ser genérico sobre si un tipo implementa un rasgo automático, y tener esa información solo "fluir a través". Por ejemplo, considere fn f<T>(t: T) -> Option<T> . Podemos pasar T independientemente de si es Send , y la salida será Send si es que T fuera. Esta es una parte muy importante de la historia de los genéricos en Rust.

También hay problemas con dyn Trait . En particular, debido a la compilación separada, tendríamos que restringir esta naturaleza de "exclusión voluntaria" únicamente a rasgos automáticos "bien conocidos" como Send y Sync ; probablemente significaría nunca estabilizar auto trait para uso externo.

Finalmente, vale la pena reiterar que el diseño de "fugas" se modeló explícitamente a partir de lo que sucede hoy en día cuando se crea un contenedor de tipo nuevo para devolver un tipo opaco. Fundamentalmente, creo que la "fuga" es un aspecto inherente de los rasgos automáticos en primer lugar; tiene ventajas y desventajas, pero es la función principal, y creo que deberíamos esforzarnos para que las nuevas funciones interactúen con ella en consecuencia.


@rpjohnst

No tengo mucho que agregar sobre el tema de la posición del argumento después de las extensas discusiones sobre el RFC y el comentario resumido de @nikomatsakis anterior.

Ahora, lo que debería ser el detalle de implementación de una función (cómo declara sus parámetros de tipo) se convierte en parte de su interfaz, ¡porque no puede escribir su tipo!

No entiendo lo que quieres decir con esto. ¿Puedes ampliar?

También quiero señalar que frases como:

fn f (t: rasgo impl) en cambio, parece que se agregó "solo porque podemos"

socavar la discusión de buena fe (lo llamo porque es un patrón repetido). El RFC hace todo lo posible para motivar la función y refutar algunos de los argumentos que está haciendo aquí, sin mencionar la discusión en el hilo, por supuesto, y en iteraciones anteriores del RFC, etc.

Las compensaciones existen, de hecho hay desventajas, pero no nos ayuda a llegar a una conclusión razonada para caricaturizar "el otro lado" del debate.

¡Gracias a todos por sus detallados comentarios! Estoy realmente muy emocionado de finalmente enviar impl Trait en la versión estable, por lo que estoy muy sesgado hacia la implementación actual y las decisiones de diseño que condujeron a ella. Dicho esto, haré todo lo posible para responder de la manera más imparcial posible y considerar las cosas como si estuviéramos comenzando desde cero:

auto Trait Fuga

La idea de la fuga de auto Trait me molestó durante mucho tiempo; de alguna manera, puede parecer antitético para muchos de los objetivos de diseño de Rust. En comparación con sus ancestros, como C++ o la familia ML, Rust es inusual porque requiere que los límites genéricos se establezcan explícitamente en las declaraciones de funciones. En mi opinión, esto hace que las funciones genéricas de Rust sean más fáciles de leer y comprender, y deja relativamente claro cuándo se está realizando un cambio incompatible con versiones anteriores. Continuamos con este patrón en nuestro enfoque de const fn , requiriendo que las funciones se especifiquen explícitamente como const lugar de inferir const de los cuerpos de las funciones. Al igual que los límites de rasgos explícitos, esto hace que sea más fácil saber qué funciones se pueden usar de qué manera, y brinda a los autores de bibliotecas la confianza de que los pequeños cambios de implementación no afectarán a los usuarios.

Dicho esto, he usado la posición de retorno impl Trait ampliamente en mis propios proyectos, incluido mi trabajo en el sistema operativo Fuchsia, y creo que la fuga automática de rasgos es el valor predeterminado correcto aquí. En la práctica, la consecuencia de eliminar la fuga sería que tengo que volver atrás y agregar + Send básicamente a todas las funciones de uso de impl Trait que he escrito. Los límites negativos (que requieren + !Send ) son una idea interesante para mí, pero entonces estaría escribiendo + !Unpin en casi todas las mismas funciones. Lo explícito es útil cuando informa las decisiones de los usuarios o hace que el código sea más comprensible. En este caso, creo que no serviría de nada.

Send y Sync son "contextos" en los que los usuarios programan: es extremadamente raro que escriba una aplicación o biblioteca que use ambos tipos Send y !Send (especialmente cuando se escribe código asíncrono para ejecutarlo en un ejecutor central, que es multiproceso o no). La elección de ser seguro para subprocesos o no es una de las primeras elecciones que se deben hacer al escribir una aplicación y, a partir de ahí, elegir ser seguro para subprocesos significa que todos mis tipos deben ser Send . Para las bibliotecas, casi siempre prefiero los tipos Send , ya que no usarlos generalmente significa que mi biblioteca no se puede usar (o requiere la creación de un subproceso dedicado) cuando se usa dentro de un contexto subproceso. Un parking_lot::Mutex indiscutible tendrá un rendimiento casi idéntico al de RefCell cuando se use en CPU modernas, por lo que no veo ninguna motivación para impulsar a los usuarios a especializarse en la funcionalidad de la biblioteca para el uso de !Send casos. Por estas razones, no creo que sea importante poder discernir entre los tipos Send y !Send en el nivel de firma de función, y no creo que sea un lugar común para autores de bibliotecas introduzcan accidentalmente tipos !Send en tipos impl Trait que anteriormente eran Send . Es cierto que esta elección tiene un costo de legibilidad y claridad, pero creo que la compensación bien vale la pena por los beneficios ergonómicos y de usabilidad.

Argumento-posición impl Trait

No tengo mucho que decir aquí, excepto que cada vez que alcancé la posición de argumento impl Trait , descubrí que aumentó en gran medida la legibilidad y el placer general de las firmas de mi función. Es cierto que no agrega una capacidad novedosa que no es posible en Rust de hoy, pero es una gran mejora de la calidad de vida para firmas de funciones complicadas, se combina bien conceptualmente con la posición de retorno impl Trait , y facilita la transición de los programadores OOP a felices Rustaceans. Actualmente, hay mucha redundancia en torno a tener que introducir un tipo genérico con nombre solo para proporcionar un límite (por ejemplo, F en fn foo<F>(x: F) where F: FnOnce() frente a fn foo(x: impl FnOnce()) ). Este cambio resuelve ese problema y da como resultado firmas de funciones que son más fáciles de leer y escribir, y IMO se siente como un ajuste natural junto con -> impl Trait .

TL; DR: Creo que nuestras decisiones originales fueron las correctas, aunque sin duda vienen con compensaciones.
Realmente aprecio que todos hayan hablado y hayan dedicado tanto tiempo y esfuerzo para asegurarse de que Rust sea el mejor lenguaje posible.

@centril

Con respecto a Rasgo impl en posición de argumento es falso, pero no así con -> Rasgo impl ya que ambos son tipos existenciales.

Sí, eso es lo que quise decir.

@aturon

frases como ... socavan la discusión de buena fe

Tienes razón, disculpa por eso. Creo que hice mi punto mejor en otro lugar.

Ahora, lo que debería ser el detalle de implementación de una función (cómo declara sus parámetros de tipo) se convierte en parte de su interfaz, ¡porque no puede escribir su tipo!

No entiendo lo que quieres decir con esto. ¿Puedes ampliar?

Con soporte para impl Trait en posición de argumento, puede escribir esta función de dos maneras:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

La elección de la forma determina si el consumidor de la API puede incluso escribir el nombre de cualquier instancia en particular (por ejemplo, para tomar su dirección). La variante impl Trait no le permite hacer eso, y esto no siempre se puede solucionar sin volver a escribir la firma para usar la sintaxis <T> . Además, pasar a la sintaxis <T> es un cambio radical.

A riesgo de una mayor caricatura, la motivación para esto es que es más fácil de enseñar, aprender y usar. Sin embargo, debido a que la elección entre los dos también es una parte importante de la interfaz de la función, al igual que el orden de los parámetros de tipo, no siento que esto se haya abordado adecuadamente; en realidad, no estoy en desacuerdo con que sea más fácil de usar o que da como resultado firmas de funciones más agradables.

No estoy seguro de que ninguno de nuestros otros cambios "simples, pero limitados -> complejos, pero generales", motivados por la capacidad de aprendizaje/ergonomía, involucren cambios que rompan la interfaz de esta manera. O el equivalente complejo de la forma simple se comporta de manera idéntica y solo necesita cambiar cuando ya está cambiando la interfaz o el comportamiento (por ejemplo, elisión de por vida, ergonomía de coincidencia, -> impl Trait ), o el cambio es igual de general y tiene la intención de aplicarse universalmente (por ejemplo, módulos/rutas, tiempos de vida en banda, dyn Trait ).

Para ser más concretos, me preocupa que empecemos a encontrar este problema en las bibliotecas, y será muy parecido a "todos deben recordar derivar Copy / Clone ", pero peor porque a) eso será un cambio radical, yb) siempre habrá una tensión para retroceder, ¡ específicamente porque para eso se diseñó la función!

@cramertj En cuanto a la redundancia de la firma de la función... ¿podríamos deshacernos de ella de alguna otra manera? Los tiempos de vida dentro de la banda pudieron salirse con la suya sin retroreferencias; tal vez podríamos hacer el equivalente moral de "parámetros de tipo en banda" de alguna manera. O en otras palabras, "el cambio es igual de general y está destinado a ser aplicado universalmente".

@rpjohnst

Además, pasar a la sintaxis <T> es un cambio radical.

No necesariamente, con https://github.com/rust-lang/rfcs/pull/2176 podría agregar un parámetro de tipo adicional T: Trait al final y turbofish seguiría funcionando (a menos que se refiera a la rotura por algún otro medio que no sea la rotura del turbopez).

La variante Trait impl no le permite hacer eso, y esto no siempre se puede solucionar sin volver a escribir la firma para usar la sintaxis <T> . Además, pasar a la sintaxis <T> es un cambio radical.

Además, creo que quiere decir que pasar de la sintaxis <T> es un cambio importante (porque las personas que llaman ya no pueden especificar el valor de T explícitamente usando turbofish).

ACTUALIZACIÓN: tenga en cuenta que si una función usa Trait impl, actualmente no permitimos usar turbofish en absoluto, incluso si tiene algunos parámetros genéricos normales.

@nikomatsakis Pasar a la sintaxis explícita también puede ser un cambio importante, si la firma anterior tenía una combinación de parámetros de tipo explícitos e implícitos: cualquiera que haya proporcionado parámetros de tipo n ahora deberá proporcionar n + 1 lugar. Ese fue uno de los casos que el RFC de @Centril pretendía resolver.

ACTUALIZACIÓN: tenga en cuenta que si una función usa Trait impl, actualmente no permitimos usar turbofish en absoluto, incluso si tiene algunos parámetros genéricos normales.

Esto técnicamente reduce la cantidad de casos de ruptura, pero por otro lado aumenta la cantidad de casos en los que no puede nombrar una instancia específica. :(

@nikomatsakis

Gracias por abordar esta inquietud con sinceridad.

Todavía dudo en decir que la filtración automática de rasgos es la solución _correcta_, pero estoy de acuerdo en que realmente no podemos saber qué es lo mejor hasta después del hecho.

Principalmente consideré el caso de uso de Futuros, pero ese no es el único. Sin filtrar Enviar/Sincronizar de los tipos locales, no hay realmente una buena historia para usar impl Trait en muchos contextos diferentes. Dado esto, y dados los rasgos automáticos adicionales, mi sugerencia no es realmente viable.

No quería destacar Sync y Send y _solo_ asumirlos, ya que eso es un poco arbitrario y solo es mejor para _un_ caso de uso. Sin embargo, la alternativa de asumir todos los rasgos automáticos tampoco sería buena. + !Unpin + !... en cada tipo no parece una solución viable.

Si tuviéramos otros cinco años de diseño de lenguajes para llegar a un sistema de efectos y otras ideas de las que no tengo ni idea ahora, podríamos llegar a algo mejor. Pero por ahora, y para Rust, parece que tener rasgos automáticos 100% "automáticos" es el mejor camino a seguir.

@lfairy

Pasar a la sintaxis explícita también puede ser un cambio importante, si la firma anterior tenía una combinación de parámetros de tipo explícitos e implícitos: cualquiera que haya proporcionado n parámetros de tipo ahora deberá proporcionar n + 1 lugar.

Eso no está permitido actualmente. Si usa impl Trait , no obtiene turbofish para ningún parámetro (como señalé). Sin embargo, esto no pretende ser una solución a largo plazo, sino más bien un paso conservador para evitar el desacuerdo sobre cómo proceder hasta que tengamos tiempo de idear un diseño redondeado. (Y, como señaló @rpjohnst , tiene sus propias desventajas ).

El diseño que me gustaría ver es (a) sí para aceptar el RFC de @centril o algo parecido y (b) decir que puede usar turbofish para parámetros explícitos (pero no los tipos impl Trait ). Sin embargo, no hicimos eso, en parte porque nos preguntábamos si podría haber una historia que permitiera la migración de un parámetro explícito a un rasgo impl.

@lfairy

Ese fue uno de los casos que el RFC de @Centril pretendía resolver.

_[Curiosidades]_ Por cierto, en realidad fue @nikomatsakis quien me hizo turbofish parcial podría facilitar las rupturas entre <T: Trait> y impl Trait ;) No era un objetivo del RFC en todo desde el principio, pero fue una agradable sorpresa. 😄

Con suerte, una vez que ganemos más confianza en torno a la inferencia, los valores predeterminados, los parámetros con nombre, etc., también podremos tener turbofish parciales, Eventualmente™.

El período final de comentarios ya está completo.

Si esto se envía en 1.26, entonces https://github.com/rust-lang/rust/issues/49373 me parece muy importante, Future y Iterator son dos de los principales usos -casos y ambos dependen mucho de conocer los tipos asociados.

Hice una búsqueda rápida en el rastreador de problemas, y #47715 es un ICE que aún debe repararse. ¿Podemos conseguir esto antes de que entre en el establo?

Algo con lo que me encontré con impl Trait hoy:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Parece que impl Trait es incompatible con unimplemented!() . ¿Es un problema conocido?

sí, vea #36375 y #44923

Acabo de darme cuenta de que la suposición 2 de RFC 1951 se enfrenta a algunos de mis usos planificados de impl Trait con bloques asíncronos. Específicamente, si toma un parámetro genérico AsRef o Into para tener una API más ergonómica, luego lo transforma en algún tipo de propiedad antes de devolver un bloque async , aún obtiene el devuelto impl Trait está vinculado por cualquier vida útil en ese parámetro, por ejemplo

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

Con esto obtendrá un error[E0597]: `url` does not live long enough porque get incluye el tiempo de vida de la referencia temporal en los impl Future devueltos. Este ejemplo es un poco artificial en el sentido de que podría pasar la URL por valor a get , pero es casi seguro que habrá casos similares en el código real.

Por lo que puedo decir, la solución esperada para esto son los tipos abstractos, específicamente

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

Al agregar la capa de direccionamiento indirecto, debe pasar explícitamente qué tipo genérico y parámetros de duración se requieren para el tipo abstracto.

Me pregunto si existe una forma potencialmente más sucinta de escribir esto, o esto simplemente terminará con tipos abstractos que se usan para casi todas las funciones y nunca el tipo de retorno impl Trait .

Entonces, si entiendo el comentario de @cramertj sobre ese problema, obtendrá un error en la definición de HttpClient::get algo así como `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Porque el RFC especifica explícitamente que impl Trait captura _todos_ los parámetros de tipo genérico, y es un error que se le permita capturar un tipo que puede contener una vida útil más corta que la declarada explícitamente).

A partir de esto, la única solución parece ser declarar un tipo abstracto nominal para permitir declarar explícitamente qué parámetros de tipo se capturan.

En realidad, parece que sería un cambio radical. Entonces, si se va a agregar un error en ese caso, mejor que sea pronto.

EDITAR: Y al volver a leer el comentario, no creo que eso sea lo que dice, por lo que todavía estoy confundido sobre si existe una forma potencial de evitar esto sin usar tipos abstractos o no.

@ Nemo157 Sí, corregir #42940 solucionaría su problema de por vida, ya que puede especificar que el tipo de devolución debe vivir tanto como el préstamo propio, independientemente de la vida útil de Url . Definitivamente, este es un cambio que queremos hacer, pero creo que es compatible con versiones anteriores: no permite que el tipo de devolución tenga una vida útil más corta, está restringiendo en exceso las formas en que se puede usar el tipo de devolución.

Por ejemplo, los siguientes errores con "el parámetro Iter puede no durar lo suficiente":

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Solo tener Iter en los genéricos de la función no es suficiente para permitir que esté presente en el tipo de retorno, pero actualmente las personas que llaman a la función asumen incorrectamente que sí lo está. Esto definitivamente es un error y debe corregirse, pero creo que puede corregirse de manera compatible con versiones anteriores y no debería bloquear la estabilización.

Parece que #46541 está listo. ¿Alguien puede actualizar el OP?

¿Hay alguna razón por la que se eligió la sintaxis abstract type Foo = ...; sobre type Foo = impl ...; ? Preferí mucho más este último, por la consistencia de la sintaxis, y recuerdo una discusión sobre esto hace un tiempo, pero parece que no puedo encontrarlo.

Soy partidario de type Foo = impl ...; o type Foo: ...; , abstract parece un bicho raro innecesario.

Si no recuerdo mal, una de las principales preocupaciones era que la gente aprendió a interpretar type X = Y como una sustitución textual ("reemplazar X con Y cuando corresponda"). Esto no funciona para type X = impl Y .

Yo prefiero type X = impl Y porque mi intuición es que type funciona como let , pero...

@alexreg Hay mucha discusión sobre el tema en RFC 2071 . TL;DR: type Foo = impl Trait; rompe la capacidad de desazúcar impl Trait en una forma "más explícita", y rompe las intuiciones de las personas sobre los alias de tipo que funcionan como una sustitución sintáctica un poco más inteligente.

Soy partidario de escribir Foo = impl ...; o escriba Foo: ...;, abstracto parece un bicho raro innecesario

Deberías unirte a mi campamento de exists type Foo: Trait; :wink:

@cramertj Hmm. Acabo de refrescarme en algo de eso, y si soy honesto, no puedo decir que entiendo el razonamiento de @withoutboats . Me parece tanto el más intuitivo (¿tienes un contraejemplo?) como la parte sobre la eliminación de azúcar que simplemente no entiendo. Supongo que mi intuición funciona como @lnicola. También creo que esta sintaxis es la mejor para hacer cosas como https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123: ¿se puede hacer esto con la sintaxis actual?

exists type Foo: Trait; es una ligera mejora, aunque aún eliminaría la palabra clave exists . type Foo: Trait; no me molestaría lo suficiente como para quejarme. 😉 abstract es simplemente superfluo/bicho raro, como dice @eddyb .

@alexreg

¿Se puede hacer esto en la sintaxis actual?

Sí, pero es mucho más incómodo. Esta fue mi razón principal para preferir la sintaxis = impl Trait (módulo de la palabra clave abstract ).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Editar: exists type Foo: (Foo1, Foo2); arriba debería haber sido type Foo = (Foo1, Foo2); . Perdón por la confusion.

@cramertj La sintaxis parece buena. ¿Debe exists ser una palabra clave adecuada?

@cramertj Correcto, estaba pensando que tendrías que hacer algo así... ¡una buena razón para preferir = impl Trait , creo! Honestamente, si la gente piensa que la intuición sobre la sustitución falla lo suficiente para los tipos existenciales aquí (en comparación con los alias de tipo simple), ¿por qué no el siguiente compromiso?

exists type Foo = (impl Bar, impl Baz);

(Sin embargo, sinceramente, preferiría tener la consistencia de usar la palabra clave única type para todo).

Encuentro:

exists type Foo: (Foo1, Foo2);

profundamente extraño. Usar Foo: (Foo1, Foo2) donde el RHS no es un límite no es consistente con la forma en que se usa Ty: Bound en otras partes del idioma.

Las siguientes formas me parecen bien:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

También prefiero no usar abstract como palabra aquí.

Encuentro exists type Foo: (Foo1, Foo2); profundamente extraño

Eso ciertamente me parece un error, y creo que debería decir type Foo = (Foo1, Foo2); .

Si estamos cambiando abstract type frente a exists type aquí, definitivamente apoyaría lo primero. Principalmente porque "abstracto" funciona como adjetivo. Fácilmente podría llamar a algo un "tipo abstracto" en una conversación, mientras que se siente extraño decir que estamos haciendo un "tipo existente".

También preferiría : Foo + Bar a : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar o = (impl Foo, impl Bar . El uso de + funciona bien con todos los demás lugares donde pueden estar los límites, y la falta de = realmente significa que no podemos escribir el tipo completo. No estamos haciendo un alias de tipo aquí, estamos haciendo un nombre para algo que garantizamos que tiene ciertos límites, pero que no podemos nombrar explícitamente.


También me sigue gustando la sugerencia de sintaxis de https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 de:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Aunque esto es, como se menciona en ese hilo, una diferencia demasiado pequeña y no muy explícita.

Debo estar interpretando (impl Foo, impl Bar) muy diferente a algunos de ustedes... para mí, esto significa que el tipo es una tupla de 2 de algunos tipos existenciales, y es completamente diferente de impl Foo + Bar .

@alexreg Si esa fuera la intención de @cramertj , todavía me : :

exists type Foo: (Foo1, Foo2);

Todavía parece muy poco claro lo que está haciendo: los límites generalmente no especifican una tupla de tipos posibles en cualquier caso, y podría confundirse fácilmente con el significado de la sintaxis Foo: Foo1 + Foo2 .

= (impl Foo, impl Bar) es una idea interesante: sería interesante permitir la creación de tuplas existenciales con tipos que no se conocen en sí mismos. Sin embargo, no creo que _necesitemos_ apoyarlos, ya que podemos introducir dos tipos existenciales para impl Foo y impl Bar luego un tercer tipo de alias para la tupla.

@daboross Bueno, estás haciendo un "tipo existencial" , no un "tipo existente" ; que es como se llama en la teoría de tipos. Pero creo que la frase "existe un tipo Foo que..." funciona muy bien tanto con el modelo mental como desde una perspectiva teórica tipo.

Sin embargo, no creo que debamos admitirlos, ya que podemos introducir dos tipos existenciales para impl Foo y impl Bar luego un tercer tipo de alias para la tupla.

Eso parece poco ergonómico... los temporales no son tan buenos en mi opinión.

@alexreg Nota: no quise decir que impl Bar + Baz; es lo mismo que (impl Foo, impl Bar) , este último es obviamente el 2-tuple.

@daboross

Si esa fuera la intención de @cramertj , todavía me

exists type Foo: (Foo1, Foo2);

Todavía parece muy poco claro lo que está haciendo: los límites generalmente no especifican una tupla de tipos posibles en cualquier caso, y podría confundirse fácilmente con el significado de la sintaxis Foo: Foo1 + Foo2.

Tal vez sea un poco confuso (no tan explícito como (impl Foo, impl Bar) , que lo entendería intuitivamente de inmediato), pero personalmente no creo que lo confunda con Foo1 + Foo2 .

= (impl Foo, impl Bar) es una idea interesante: sería interesante permitir la creación de tuplas existenciales con tipos que no se conocen en sí mismos. Sin embargo, no creo que debamos admitirlos, ya que podemos introducir dos tipos existenciales para impl Foo e impl Bar y luego un tercer tipo de alias para la tupla.

Sí, esa fue una propuesta temprana, y todavía me gusta bastante. Se ha notado que esto se puede hacer de todos modos usando la sintaxis actual, pero requiere 3 líneas de código, lo cual no es muy ergonómico. También mantengo que alguna sintaxis como ... = (impl Foo, impl Bar) es la más clara para el usuario, pero sé que aquí hay controversia.

@Centril No lo pensé al principio, pero era un poco ambiguo, y luego @daboross pareció interpretarlo de esa manera, ja. De todos modos, me alegro de que hayamos aclarado eso.

Vaya, vea mi edición en https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); debería haber sido type Foo = (Foo1, Foo2); .

@cramertj Ah, eso tiene más sentido ahora. De todos modos, ¿no crees que poder hacer lo siguiente es lo más ergonómico? Incluso navegando por ese otro hilo, realmente no he visto un buen argumento en contra.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Sí, yo creo que es la sintaxis más ergonómica.

Usando RFC https://github.com/rust-lang/rfcs/pull/2289 , así es como reescribiría el fragmento de código de

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

Sin embargo, creo que para los alias de tipo, no introducir exists ayudaría a retener el poder expresivo sin hacer innecesariamente más compleja la sintaxis del lenguaje; por lo tanto, desde un punto de vista del presupuesto de complejidad, impl Iterator parece mejor que exists . Sin embargo, la última alternativa realmente no introduce una nueva sintaxis y también es la más breve, aunque clara.

En resumen, creo que se deberían permitir las dos formas siguientes (porque funcionan tanto en impl Trait como en los límites de las sintaxis de tipos asociados que ya tenemos):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDITAR: ¿Qué sintaxis se debe usar? En mi opinión, clippy debería preferir sin ambigüedades la sintaxis Type: Bound cuando sea posible usarla, ya que es más ergonómica y directa.

Prefiero mucho más la variante type Foo: Trait variante type Foo = impl Trait . Coincide con la sintaxis del tipo asociado, lo cual es bueno porque también es un "tipo de salida" del módulo que lo contiene.

La sintaxis impl Trait se usa para los tipos de entrada y salida, lo que significa que corre el riesgo de dar la impresión de módulos polimórficos. :(

Si impl Trait se usara únicamente para los tipos de salida, entonces podría preferir la variante type Foo = impl Trait porque la sintaxis de tipo asociada es más para características (que se corresponden aproximadamente con las firmas de ML) mientras que type Foo = .. sintaxis de

@rpjohnst

Prefiero mucho más la variante type Foo: Trait variante type Foo = impl Trait .

Estoy de acuerdo, debe usarse siempre que sea posible; pero ¿qué pasa con el caso de (impl T, impl U) donde la sintaxis enlazada no se puede usar directamente? Me parece que la introducción de alias de tipo temporal perjudica la legibilidad.

Usar solo type Name: Bound parece confuso cuando se usa dentro de bloques de impl :

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

Tanto para esa sintaxis como para el plan actual (?) de prefijo de palabra clave, el costo de introducir alias de tipo temporales para usar en bloques impl también es mucho mayor, estos alias de tipo ahora deben exportarse a nivel de módulo ( y dado un nombre semánticamente significativo...), que bloquea un patrón relativamente común (al menos para mí) de definir implementaciones de rasgos dentro de módulos privados.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

contra

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 ¿

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

y:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

No veo por qué es necesario que haya dos sintaxis para la misma característica, aún sería posible usar solo la sintaxis type Name = impl Bound; que proporciona explícitamente nombres para las dos partes:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Estoy de acuerdo en que no es necesario (y no debería) haber dos sintaxis diferentes. Sin embargo, debo decir que no encuentro type (sin palabra clave de prefijo) confuso en absoluto.

@rpjohnst ¿Qué diablos es un módulo polimórfico? :-) De todos modos, no veo por qué deberíamos modelar la sintaxis después de las definiciones de tipo asociadas, que colocan límites de características en un tipo. Esto no tiene nada que ver con los límites .

@alexreg Un módulo polimórfico es uno que tiene parámetros de tipo, de la misma manera que fn foo(x: impl Trait) . No es algo que exista, así que no quiero que la gente piense que existe.

abstract type ( editar: para nombrar la función, no para sugerir el uso de la palabra clave) tiene mucho que ver con los límites. Los límites son lo único que sabes sobre el tipo. La única diferencia entre ellos y los tipos asociados es que se infieren, porque normalmente no se les puede nombrar.

@ Nemo157, la Foo: Bar ya es más familiar en otros contextos (límites en tipos asociados y en parámetros de tipo) y es más ergonómica y (en mi opinión) clara cuando se puede usar sin introducir temporales.

Escribiendo:

type IterDisplay: Iterator<Item: Display>;

parece mucho más directo wrt. lo que quiero decir, en comparación con

type IterDisplay = impl Iterator<Item = impl Display>;

Creo que esto es solo aplicar de manera consistente la sintaxis que ya tenemos; así que no es realmente nuevo.

EDIT2: La primera sintaxis también es cómo me gustaría que se representara en rustdoc.

Pasar de un rasgo que requiere algo en un tipo asociado a un impl también se vuelve muy fácil:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

La sintaxis impl Bar parece mejor cuando, de lo contrario, tendría que introducir temporales, pero también aplica la sintaxis de manera constante en todo momento.

Ser capaz de usar ambas sintaxis realmente no sería muy diferente de poder usar impl Trait en posición de argumento, además de tener un parámetro de tipo explícito T: Trait que luego es usado por un argumento.

EDIT1: De hecho, tener solo una sintaxis sería una carcasa especial, no al revés.

@rpjohnst Discrepo , aunque debería haber dicho que no tiene nada que ver explícitamente con los límites.

De todos modos, no estoy en contra de la sintaxis type Foo: Bar; , pero por el amor de Dios, deshagámonos de la palabra clave abstract . type por sí mismo es bastante claro, en cualquier circunstancia.

Personalmente, creo que usar = y impl es una buena pista visual de que se está produciendo una inferencia. También facilita la detección de esos lugares al ojear un archivo más grande.

Además, suponiendo que vea type Iter: Iterator<Item = Foo> , tendré que encontrar Foo y averiguar si es un tipo o un rasgo antes de saber qué está pasando.

Y, por último, creo que la pista visual de los puntos de inferencia también ayudará a depurar errores de inferencia e interpretar los mensajes de error de inferencia.

Así que creo que la variante = / impl resuelve un poco más los recortes de papel.

@phaylon

Además, suponiendo que vea el tipo Iter: Iterator<Item = Foo> , tendré que encontrar Foo y averiguar si es un tipo o un rasgo antes de saber qué está pasando.

Esto no lo entiendo; Item = Foo siempre debería ser un tipo hoy en día dado que dyn Foo es estable (y el rasgo básico se está eliminando gradualmente...)?

@centril

Esto no lo entiendo; Item = Foo siempre debería ser un tipo hoy en día dado que dyn Foo es estable (y el rasgo básico se está eliminando gradualmente...)?

Sí, pero en la variante impl menos propuesta, podría ser un tipo inferido con un límite o un tipo concreto. Por ejemplo, Iterator<Item = String> frente a Iterator<Item = Display> . Tengo que conocer los rasgos para saber si la inferencia está ocurriendo.

Editar: Ah, no noté que uno usó : . Un poco a lo que me refiero con fácil de pasar por alto :) Pero tienes razón en que son diferentes.

Edición 2: creo que este problema se mantendría fuera de los tipos asociados. Dados type Foo: (Bar, Baz) , necesitaría conocer a Bar y Baz para saber dónde ocurre la inferencia.

@centril

EDIT1: De hecho, tener solo una sintaxis sería una carcasa especial, no al revés.

Actualmente solo hay una forma de declarar tipos _existenciales_, -> impl Trait . Hay dos formas de declarar tipos _universales_ ( T: Trait y : impl Trait en una lista de argumentos).

Si tuviéramos módulos polimórficos que aceptaran tipos universales, podría ver algunos argumentos al respecto, pero creo que el uso actual de type Name = Type; tanto en módulos como en definiciones de rasgos es como un parámetro de tipo de salida, que debería ser un parámetro existencial. escribe.


@phaylon

Sí, pero en la variante impl menos propuesta, podría ser un tipo inferido con un límite o un tipo concreto. Por ejemplo, Iterator<Item = String> frente a Iterator<Item = Display> . Tengo que conocer los rasgos para saber si la inferencia está ocurriendo.

Creo que la variante impl menos usa : Bound en todos los casos para tipos existenciales, por lo que podría tener Iterator<Item = String> o Iterator<Item: Display> como límites de rasgos, pero Iterator<Item = Display> sería una declaración inválida.

@Nemo157
Tienes razón con respecto al tipo de caso asociado, mi error allí. Pero (como se señaló en mi edición) creo que todavía hay un problema con type Foo: (A, B) . Como A o B podría ser un tipo o rasgo.

Creo que esta también es una buena razón para optar por = . El : solo te dice que se infieren algunas cosas, pero no te dice cuáles. type Foo = (A, impl B) parece más claro.

También asumo que leer y proporcionar fragmentos de código es más fácil con impl , ya que nunca es necesario proporcionar un contexto adicional sobre lo que es un rasgo y lo que no lo es.

Editar: algunos créditos: mi argumento es básicamente el mismo que el de @alexreg aquí , solo quería ampliar el motivo por el que creo que impl es preferible.

Actualmente solo hay una forma de declarar tipos existenciales, -> impl Trait . Hay dos formas de declarar tipos universales ( T: Trait y : impl Trait en una lista de argumentos).

Eso es lo que estoy diciendo: PI ¿Por qué la cuantificación universal debería tener dos formas pero existencial solo una (ignorando dyn Trait ) en otros lugares ?

Me parece igualmente probable que un usuario vaya y escriba type Foo: Bound; y type Foo = impl Bound; habiendo aprendido diferentes partes del lenguaje, y no puedo decir que una sintaxis sea claramente mejor en todos los casos; Me queda claro que una sintaxis es mejor para algunas cosas y otra para cosas diferentes.

@phaylon

Creo que esta también es una buena razón para ir con =. El : solo te dice que se infieren algunas cosas, pero no te dice cuáles. type Foo = (A, impl B) me parece más claro.

Sí, esta es probablemente otra buena razón. Realmente se necesita un poco de desempaque para descubrir qué se está cuantificando existencialmente, saltando de una definición a otra.

Otra cosa es: ¿se permitiría incluso : dentro de un enlace de tipo asociado, bajo esa sintaxis? Me parecería un caso especial un poco extraño, dado que los tipos existenciales no se pueden componer/combinar de ninguna otra manera en esta sintaxis propuesta. Me imagino que el siguiente sería el enfoque más consistente usando esa sintaxis:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Usando la sintaxis que yo (y algunos otros aquí) preferimos, podríamos escribir todo esto en una sola línea y, además, queda inmediatamente claro dónde está ocurriendo la cuantificación.

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Sin relación con lo anterior: ¿cuándo jugamos a implementar let x: impl Trait en nightly? He estado perdiendo esta característica por un tiempo ahora.

@alexreg

Otra cosa es: ¿se permitiría incluso : dentro de un enlace de tipo asociado, bajo esa sintaxis?

Si por qué no; Este sería un efecto natural de rust-lang/rfcs#2289 + type Foo: Bound .

También podrías hacer:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Creo que es una mala idea permitir dos sintaxis alternativas. Huele a síndrome de "no pudimos decidir, así que apoyaremos a ambos". ¡Ver el código que los mezcla y los combina será una verdadera monstruosidad!

@Centril Estoy un poco con @nikomatsakis en ese RFC tuyo Por cierto, lo siento. Preferiría escribir impl Iterator<Item = impl Baz> . Bonito y explícito.

@alexreg Esto es justo;

Pero (des)afortunadamente (dependiendo de su punto de vista), ya comenzamos "permitir dos sintaxis alternativas" con impl Trait en posición de argumento, de modo que tenemos Foo: Bar y impl Bar trabajando para significar lo mismo;

Es para la cuantificación universal, pero a la notación impl Trait realmente no le importa en qué lado de la dualidad está; después de todo, no optamos por any Trait y some Trait .

Dado que ya elegimos "no pudimos decidir" y "el lado de la dualidad no importa sintácticamente" , me parece coherente aplicar "no podemos decidir" en todas partes para que los usuarios no se queden en "pero podría escribirlo así por allá, ¿por qué no aquí?" ;)


PD:

Re. impl Iterator<Item = impl Baz> no funciona como límite en una cláusula where; así que tendrías que mezclarlo como Iter: Iterator<Item = impl Baz> . Tendría que permitir: Iter = impl Iterator<Item = impl Baz> para que funcione uniformemente (¿quizás deberíamos hacerlo?).

Usar : Bound también es en lugar de = impl Bound también es explícito, solo que más corto ^,-
Creo que la diferencia de espaciado entre X = Ty y X: Ty hace que la sintaxis sea legible.

Habiendo ignorado mi propio consejo, continuemos esta conversación en el RFC ;)

Pero (de)afortunadamente (dependiendo de su punto de vista), ya comenzamos "permitir dos sintaxis alternativas" con Trait impl en posición de argumento, de modo que tenemos Foo: Bar e impl Bar trabajando para significar lo mismo;

Lo hicimos, pero creo que la elección se hizo más desde el punto de vista de la simetría/consistencia. Los argumentos de tipo genérico son estrictamente más poderosos que los de tipo universal ( impl Trait ). Pero estábamos introduciendo impl Trait en la posición de retorno, tenía sentido introducirlo en la posición del argumento.

Dado que ya elegimos "no pudimos decidir" y "el lado de la dualidad no importa sintácticamente", me parece consistente aplicar "no podemos decidir" en todas partes para que los usuarios no se queden en "pero podría escribirlo así por allá, ¿por qué no aquí?" ;)

No estoy seguro de que sea en el punto en que debamos levantar los brazos y decir "vamos a implementar todo". No hay un argumento tan claro aquí en cuanto a la ganancia.

PD:

Re. impl Iterator<Item = impl Baz> no funciona como límite en una cláusula where; así que tendrías que mezclarlo como Iter: Iterator<Item = impl Baz> . Tendría que permitir: Iter = impl Iterator<Item = impl Baz> para que funcione uniformemente (¿quizás deberíamos?).

Diría que solo admitimos where Iter: Iterator<Item = T>, T: Baz (como lo hemos hecho ahora) o vamos hasta el final con Iter = impl Iterator<Item = impl Baz> (como sugeriste). Permitir solo la casa de transición parece una especie de evasión.

Usar : Bound también es en lugar de = impl Bound también es explícito, solo que más corto ^,-
Creo que la diferencia de espaciado entre X = Ty y X: Ty hace que la sintaxis sea legible.

Es legible, pero no creo que sea tan claro/explícito que se está utilizando un tipo existencial. Esto se agrava cuando la definición debe dividirse en varias líneas debido a la limitación de esta sintaxis.

Habiendo ignorado mi propio consejo, continuemos esta conversación en el RFC ;)

Espera, ¿te refieres a tu RFC? Creo que es relevante tanto para eso como para este, por lo que puedo decir. :-)

Espera, ¿te refieres a tu RFC? Creo que es relevante tanto para eso como para este, por lo que puedo decir. :-)

OK; Sigamos aquí entonces;

Lo hicimos, pero creo que la elección se hizo más desde el punto de vista de la simetría/consistencia. Los argumentos de tipo genérico son estrictamente más poderosos que los de tipo universal ( impl Trait ). Pero estábamos introduciendo impl Trait en la posición de retorno, tenía sentido introducirlo en la posición del argumento.

Todo mi punto es sobre la consistencia y la simetría. =P
Si se le permite escribir impl Trait tanto para la cuantificación existencial como para la universal, para mí tiene sentido que también se le permita usar Type: Trait para la cuantificación tanto universal como existencial.

En cuanto al poder expresivo, el primero es más poderoso que el segundo como dices, pero no necesariamente tiene que ser así; Podrían ser igualmente poderosos si quisiéramos que fueran AFAIK (aunque no estoy diciendo en absoluto que debamos hacer eso...).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

No estoy seguro de que sea en el punto en que debamos levantar los brazos y decir "vamos a implementar todo". No hay un argumento tan claro aquí en cuanto a la ganancia.

Mi argumento es que sorprender a los usuarios con "Esta sintaxis se puede usar en otros lugares y su significado es claro aquí, pero no puede escribirlo en este lugar" cuesta más que tener dos formas de hacerlo (con las que ambos deben familiarizarse de todos modos ). Hicimos cosas similares con https://github.com/rust-lang/rfcs/pull/2300 (fusionado), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https ://github.com/rust-lang/rfcs/pull/2175 (fusionado) donde llenamos los agujeros de consistencia aunque antes era posible escribir de otra manera.

Es legible, pero no creo que sea tan claro/explícito que se está utilizando un tipo existencial.

Legible es suficiente en mi opinión; No creo que Rust se atribuya a "explícito por encima de todo" y ser demasiado detallado (lo que sí encuentro que es la sintaxis cuando se usa demasiado) también cuesta desalentar el uso.
(Si quiere que algo se use con frecuencia, déle una sintaxis más breve... cf ? como soborno contra .unwrap() ).

Esto se agrava cuando la definición debe dividirse en varias líneas debido a la limitación de esta sintaxis.

Esto no lo entiendo; Me parece que Assoc = impl Trait debería causar divisiones de línea incluso más que Assoc: Trait simplemente porque el primero es más largo.

Diría que solo admitimos where Iter: Iterator<Item = T>, T: Baz (como lo hemos hecho ahora) o vamos hasta el final con Iter = impl Iterator<Item = impl Baz> (como sugeriste).
Permitir solo la casa de transición parece una especie de evasión.

¡Exacto!, no vayamos a medias / escapemos e implementemos where Iter: Iterator<Item: Baz> ;)

@Centril Bien, me has

Editaré esto con mi respuesta completa mañana.

Editar

Como señala @Centril , ya : Trait (enlazada). p.ej

fn foo<T: Trait>(x: T) { ... }

junto a tipos universales "propios" o "reificados", por ejemplo

fn foo(x: impl Trait) { ... }

Por supuesto, el primero es más poderoso que el segundo, pero el último es más explícito (y posiblemente más legible) cuando es todo lo que se requiere. De hecho, creo firmemente que deberíamos tener una pelusa de compilación a favor de la última forma siempre que sea posible.

Ahora, ya tenemos impl Trait en la posición de retorno de la función, lo que representa un tipo existencial. Los tipos de rasgos asociados tienen forma existencial y ya usan la sintaxis : Trait .

Esto, dada la existencia de las formas propias y limitadas de los tipos universales en Rust en la actualidad, y también la existencia de formas propias y limitadas para los tipos existenciales (este último solo dentro de los rasgos en la actualidad), creo firmemente que deberíamos extender el soporte para las formas propias y enlazadas de los tipos existenciales fuera de los rasgos. Es decir, deberíamos admitir lo siguiente tanto en general como para los tipos asociados .

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

Secundo el comportamiento de pelusa del compilador sugerido en este comentario también, lo que debería reducir en gran medida la variación de la expresión de los tipos existenciales comunes en la naturaleza.

Sigo creyendo que combinar la cuantificación universal y exxisistente bajo una palabra clave fue un error, por lo que ese argumento de consistencia no funciona para mí. La única razón por la que una sola palabra clave funciona en las firmas de funciones es que el contexto necesariamente lo obliga a usar solo una forma de cuantificación en cada posición. Hay azúcares potenciales que podría ver como algo en lo que no tienes las mismas limitaciones

struct Foo {
    pub foo: impl Display,
}

¿Es esta una forma abreviada de cuantificación existencial o universal? A partir de la intuición derivada del uso de impl Trait en las firmas de funciones, no veo cómo podría decidir. Si realmente intenta usarlo como ambos, rápidamente se dará cuenta de que la cuantificación universal anónima en esta posición es inútil, por lo que debe ser una cuantificación existencial, pero eso parece inconsistente con impl Trait en los argumentos de función.

Estas son dos operaciones fundamentalmente diferentes, sí, ambas usan límites de rasgos, pero no veo ninguna razón por la que tener dos formas de declarar un tipo existencial reduzca la confusión para los recién llegados. Si intentar usar type Name: Trait es algo probable para los recién llegados, entonces esto podría resolverse a través de una pelusa:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

Y se me ocurrió una formulación alternativa de su argumento con la que estaría mucho más dispuesto, tendrá que esperar hasta que esté en una computadora real para volver a leer el RFC y publicarlo.

Siento que todavía no tengo suficiente experiencia con Rust para comentar sobre RFC. Sin embargo, estoy interesado en ver esta característica fusionada con Rust nocturno y estable, para usarla con Rust libp2p para construir un protocolo de fragmentación para Ethereum como parte de la implementación de fragmentación de Drops of Diamond . ¡Me suscribí a la edición, sin embargo, no tengo tiempo para seguir todos los comentarios! ¿Cómo puedo mantenerme actualizado en un alto nivel sobre este tema, sin tener que leer los comentarios?

Siento que todavía no tengo suficiente experiencia con Rust para comentar sobre RFC. Sin embargo, estoy interesado en ver esta característica fusionada con Rust nocturno y estable, para usarla con Rust libp2p para construir un protocolo de fragmentación para Ethereum como parte de la implementación de fragmentación de Drops of Diamond . ¡Me suscribí a la edición, sin embargo, no tengo tiempo para seguir todos los comentarios! ¿Cómo puedo mantenerme actualizado en un alto nivel sobre este tema, sin tener que leer los comentarios? Por el momento, parece que solo tendré que hacer eso registrándome de vez en cuando y no suscribiéndome al problema. Sería bueno si pudiera suscribirme por correo electrónico para recibir noticias de alto nivel sobre esto.

Sigo creyendo que fusionar la cuantificación universal y existencial bajo una palabra clave fue un error, por lo que el argumento de coherencia no funciona para mí.

Como principio general, independientemente de esta característica, encuentro problemática esta línea de razonamiento.

Creo que deberíamos abordar el diseño de lenguajes desde cómo es un lenguaje en lugar de cómo desearíamos que fuera bajo algún desarrollo alternativo de la historia. La sintaxis impl Trait como cuantificación universal en la posición del argumento se estabiliza, por lo que no puede desear que desaparezca. Incluso si cree que X, Y y Z fueron errores (y podría encontrar muchas cosas que personalmente creo que son errores en el diseño de Rust, pero los acepto y los asumo...), tenemos que vivir con ellos ahora, y creo sobre cómo podemos hacer que todo encaje junto con la nueva característica (hacer que las cosas sean consistentes).

En la discusión, creo que todo el corpus de RFC y el lenguaje tal como está deben tomarse como si no fueran axiomas, sino argumentos sólidos.


Podría argumentar (pero yo no lo haría) que:

struct Foo {
    pub foo: impl Display,
}

es semánticamente equivalente a:

struct Foo<T: Display> {
    pub foo: T,
}

bajo el razonamiento función-argumento.

Básicamente, dado impl Trait , debe pensar "¿es este tipo de retorno o argumento?" , que puede ser difícil.


Si intentar usar type Name: Trait es algo probable para los recién llegados, entonces esto podría resolverse a través de una pelusa:

Yo también sacaría pelusa, pero en la otra dirección; Creo que las siguientes formas deberían ser idiomáticas:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Ok, formulación alternativa que creo que RFC 2071 está insinuando y puede haber sido discutida en el problema, pero nunca se declaró explícitamente:

Solo hay _una manera_ de declarar tipos existencialmente cuantificados: existential type Name: Bound; (usando existential porque eso está especificado en el RFC, no estoy totalmente en contra de dejar caer la palabra clave bajo esta formulación).

Además, hay azúcar para declarar implícitamente un tipo cuantificado existencialmente sin nombre en el alcance actual: impl Bound (ignorando el azúcar de cuantificación universal en los argumentos de función por el momento).

Por lo tanto, el uso actual del tipo de devolución es una simple eliminación de azúcar:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

extender a const , static y let es igualmente trivial.

La única extensión que no se menciona en el RFC es: admite este azúcar en la sintaxis type Alias = Concrete; , por lo que cuando escribe

type Foo = impl Iterator<Item = impl Display>;

esto es en realidad azúcar para

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

que luego se basa en la naturaleza transparente de los alias de tipo para permitir que el módulo actual busque Foo y vea que se refiere a un tipo existencial.

De hecho, creo firmemente que deberíamos tener una pelusa de compilación a favor de la última forma siempre que sea posible.

Estoy en su mayoría alineado con el comentario de @alexreg , pero tengo algunas preocupaciones sobre la pelusa hacia arg: impl Trait , principalmente debido al riesgo de alentar cambios importantes en las bibliotecas ya que impl Trait no funciona con turbofish (en este momento, y necesitarías turbofish parcial para que funcione bien). Por lo tanto, la pelusa en clippy se siente menos sencilla que en el caso de los alias de tipo (donde no hay turbofish que cause ningún problema).

Estoy en su mayoría alineado con el comentario de @alexreg , pero tengo algunas preocupaciones sobre el linting hacia arg: impl Trait, principalmente debido al riesgo de alentar cambios importantes en las bibliotecas, ya que impl Trait no funciona con turbofish (en este momento, y necesitarías turbofish parcial para que funcione bien). Por lo tanto, la pelusa en clippy se siente menos sencilla que en el caso de los alias de tipo (donde no hay turbofish que cause ningún problema).

@Centril acaba de mencionar esto en IRC conmigo, y estoy de acuerdo en que es un punto justo con respecto a la compatibilidad con versiones anteriores (demasiado fácil de romper). Cuando/si aterriza un turbofish parcial, creo que se debe agregar una pelusa del compilador, pero no hasta entonces.

Entonces... hemos discutido mucho sobre la sintaxis de los tipos existenciales con nombre ahora. ¿Deberíamos tratar de llegar a una conclusión y escribirla en la publicación RFC / PR, para que alguien pueda comenzar a trabajar en la implementación real? :-)

Personalmente, una vez que hayamos nombrado existenciales, preferiría una pelusa (si corresponde) lejos de cualquier uso de impl Trait cualquier lugar .

@rpjohnst Bueno, ciertamente está de acuerdo conmigo y con @Centril con respecto a los existenciales con nombre... en cuanto a eliminarlos en argumentos de función, esa es una posibilidad, pero probablemente una discusión para otro lugar. Depende de si se desea privilegiar la simplicidad o la generalidad en este contexto.

¿Está actualizado el RFC en impl Trait en posición de argumento? Si es así, ¿es seguro afirmar que su semántica es _universal_? Si es así: quiero llorar. Profundamente.

@phaazon : notas de la versión de Rust 1.26 por impl Trait :

Nota al margen para los teóricos del tipo: esto no es existencial, sigue siendo universal. En otras palabras, Impl Trait es universal en una posición de entrada, pero existencial en una posición de salida.

Solo para expresar mis pensamientos al respecto:

  • Ya teníamos una sintaxis para las variables de tipo y, en realidad, hay algunos usos para las variables de tipo anónimo (es decir, muy a menudo desea usar la variable de tipo en varios lugares en lugar de dejarla caer en un solo lugar).
  • Los existenciales covariantes nos abrirían las puertas a las funciones de rango n, algo que es difícil de hacer en este momento sin un rasgo (ver esto ) y es una característica que realmente le falta a Rust.
  • Es fácil referirse a impl Trait como “tipo elegido por el receptor de la llamada”, porque… ¡porque es la única construcción del lenguaje por ahora que nos permite hacerlo! Elegir el tipo por parte de la persona que llama ya está disponible a través de varias construcciones.

Realmente creo que la decisión actual de impl Trait en la posición del argumento es una lástima. :llorar:

Realmente creo que el Rasgo impl en la decisión actual de la posición del argumento es una lástima. 😢

Si bien estoy un poco desgarrado por esto, ciertamente creo que sería mejor invertir tiempo en implementar let x: impl Trait ahora mismo.

Los existenciales covariantes nos abrirían las puertas a las funciones de rango n

Ya tenemos una sintaxis para ello ( fn foo(f: impl for<T: Trait> Fn(T)) ), (también conocido como "tipo HRTB"), pero aún no está implementada. fn foo(f: impl Fn(impl Trait)) produce un error de que "no se permite impl Trait anidados", y espero que queramos que signifique la versión de mayor rango, cuando obtengamos el tipo HRTB.

Esto es similar a cómo Fn(&'_ T) significa for<'a> Fn(&'a T) , por lo que no espero que sea controvertido.

Mirando el borrador actual, impl Trait en posición de argumento es un _universal_, pero ¿estás diciendo que impl for<_> Trait convierte en un _existencial_? ¿Qué tan loco es eso?

¿Por qué pensamos que teníamos la necesidad de introducir _otra forma más_ de construir un _universal_? Quiero decir:

fn foo(x: impl MyTrait)

Solo es interesante porque la variable de tipo anónimo aparece solo una vez en el tipo . Si necesita devolver el mismo tipo:

fn foo(x: impl Trait) -> impl Trait

Obviamente no funcionará. Le estamos diciendo a la gente que cambie de un idioma más general a uno más restrictivo en lugar de simplemente aprender uno y usarlo en todas partes. Esa es toda mi diatriba. Agregar una característica que no tiene where y parámetros de plantilla in situ.

Argh, supongo que todo esto ya fue aceptado y estoy despotricando por nada. Solo creo que es una verdadera lástima. Estoy bastante seguro de que no soy el único decepcionado con la decisión de RFC.

(Probablemente no tenga mucho sentido continuar debatiendo esto después de que la función se haya estabilizado, pero vea aquí un argumento convincente (con el que estoy de acuerdo) de por qué impl Trait en posición de argumento con la semántica que tiene es sensato y coherente. Tl; dr es por la misma razón por la que fn foo(arg: Box<Trait>) funciona aproximadamente de la misma manera que fn foo<T: Trait>(arg: Box<T>) , aunque dyn Trait es un existencial; ahora sustituya dyn con impl .)

Mirando el borrador actual, impl Trait en posición de argumento es universal, pero ¿está diciendo que impl for<_> Trait convierte en un existencial?

No, ambos son universales. Estoy diciendo que los usos de mayor rango se verían así:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

que podría, al mismo tiempo que se agrega (es decir, sin cambios en impl Trait ) escribirse como:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

Eso es impl Trait universal, solo que el Trait es un HRTB (similar a impl for<'a> Fn(&'a T) ).
Si decidimos (lo cual espero que sea probable) que impl Trait dentro de los argumentos Fn(...) también es universal, podría escribir esto para lograr el mismo efecto:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

Esto es lo que pensé que querías decir con "clasificación superior", si no es así, házmelo saber.

Una decisión aún más interesante podría ser aplicar el mismo tratamiento en posición existencial, es decir, permitir esto (lo que significaría "devolver algún cierre que toma cualquier otro cierre"):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

para ser escrito así:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Eso sería un impl Trait existencial que contiene un impl Trait universal (vinculado al existencial, en lugar de a la función envolvente).

@eddyb ¿No tendría más sentido tener dos palabras clave separadas para la cuantificación existencial y universal en general, por coherencia y para no confundir a los novatos?
¿La palabra clave para cuantificación existencial no sería también reutilizable para tipos existenciales?
¿Por qué estamos usando impl para cuantificación existencial ( y universal) pero existential para tipos existenciales?

Me gustaría hacer tres puntos:

  • No tiene mucho mérito discutir si impl Trait es existencial o universal. La mayoría de los programadores probablemente no leyeron suficientes manuales de teoría de tipos. La pregunta debería ser si a la gente le gusta o si lo encuentran confuso. Para responder a esa pregunta, se puede ver algún tipo de comentario aquí en este hilo, en reddit o en el foro . Si algo necesita una explicación más detallada, falla una prueba de fuego para la característica intuitiva o no sorprendente. Por lo tanto, deberíamos analizar cuántas personas y qué tan confundidas están y si se trata de más preguntas que con otras funciones. De hecho, es triste que esta retroalimentación llegue después de la estabilización y se debe hacer algo al respecto, pero es para una discusión separada.
  • Técnicamente, incluso después de la estabilización, habría una manera de deshacerse de la función en este caso (dejando de lado la decisión si debería). Sería posible eliminar las funciones de escritura que usan eso y eliminar la capacidad en la próxima edición (manteniendo la capacidad de llamarlos si provienen de cajas de ediciones diferentes). Eso satisfaría las garantías de estabilidad contra la oxidación.
  • No, agregar dos palabras clave más para especificar tipos existenciales y universales no mejoraría la confusión, solo empeoraría las cosas.

De hecho, es triste que esta retroalimentación llegue después de la estabilización y se debe hacer algo al respecto.

Ha habido objeciones a impl Trait en la posición de argumento siempre que haya sido una idea. Comentarios como este _no son nuevos_, fueron muy debatidos incluso en el hilo RFC relevante. Hubo mucha discusión no solo sobre los tipos universales/existenciales desde la perspectiva de la teoría de tipos, sino también sobre cómo esto sería confuso para los nuevos usuarios.

Cierto, no obtuvimos las perspectivas reales de los nuevos usuarios, pero esto no surgió de la nada.

@Boscop any y some se propusieron como un par de palabras clave para hacer este trabajo, pero se decidió no hacerlo (aunque no sé si alguna vez se escribió la justificación en alguna parte).

Cierto, no pudimos obtener comentarios de personas nuevas en rust y que no eran teóricos de tipos.

Y el argumento a favor de la inclusión siempre fue que facilitaría las cosas a los recién llegados. Entonces, si ahora tenemos comentarios reales de los recién llegados, ¿no debería ser un tipo de retroalimentación muy relevante en lugar de discutir cómo deberían entenderlo los recién llegados?

Supongo que si alguien tuviera tiempo, se podría hacer algún tipo de investigación a través de los foros y otros lugares sobre cuán confundidas estaban las personas antes y después de la inclusión (no era muy bueno en estadísticas, pero estoy bastante seguro de que alguien que lo era podría pensar en algo mejor que las predicciones ciegas).

Y el argumento a favor de la inclusión siempre fue que facilitaría las cosas a los recién llegados. Entonces, si ahora tenemos comentarios reales de los recién llegados, ¿no debería ser un tipo de retroalimentación muy relevante en lugar de discutir cómo deberían entenderlo los recién llegados?

¿Sí? Quiero decir que no estoy discutiendo si lo que pasó fue una buena o mala idea. Solo quiero señalar que el subproceso de RFC recibió comentarios sobre esto, y se decidió de todos modos.

Como dijiste, probablemente sea mejor tener la meta discusión sobre los comentarios en otro lugar, aunque no estoy seguro de dónde sería.

No, agregar dos palabras clave más para especificar tipos existenciales y universales no mejoraría la confusión, solo empeoraría las cosas.

¿Peor? ¿Cómo es eso? Prefiero tener más que recordar que ambigüedad/confusión.

¿Sí? Quiero decir que no estoy discutiendo si lo que pasó fue una buena o mala idea. Solo quiero señalar que el subproceso de RFC recibió comentarios sobre esto, y se decidió de todos modos.

Seguro. Pero ambos lados de la discusión eran programadores viejos, con cicatrices y experimentados con un profundo conocimiento de lo que sucede debajo del capó, adivinando sobre un grupo del que no forman parte (recién llegados) y adivinando sobre el futuro. Desde un punto de vista fáctico, eso no es mucho mejor que tirar los dados con respecto a lo que realmente sucede en la realidad. No se trata de una experiencia inadecuada de los expertos, sino de no tener datos adecuados en los que basar las decisiones.

Ahora se introdujo y tenemos una manera de obtener los datos concretos reales, o los datos más concretos que se puedan obtener sobre la cantidad de personas que se confunden en una escala del 0 al 10.

Como dijiste, probablemente sea mejor tener la meta discusión sobre los comentarios en otro lugar.

Por ejemplo aquí, ya comencé dicha discusión y hay algunos pasos reales que se pueden tomar, incluso si son pequeños: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -antes-de-estabilizar-una-función/7522/14. No tuve tiempo de escribir el RFC, así que si alguien se me adelanta o quiere ayudar, no me importará.

¿Peor? ¿Cómo es eso?

Porque, a menos que impl Trait esté en desuso, tiene los 3, por lo que tiene más para recordar además de la confusión. Si impl Trait desapareciera, la situación sería diferente y se ponderarían los pros y los contras de los dos enfoques.

impl Trait como en la selección de llamadas sería suficiente. Si intenta usarlo en posición de argumento, entonces introduce la confusión. Los HRTB eliminarían esa confusión.

@vorner Anteriormente, argumenté que deberíamos hacer pruebas A/B reales con los novatos de Rust para ver qué encuentran realmente más fácil y más difícil de aprender, porque es difícil de adivinar como alguien que domina Rust.
FWIW, recuerdo, cuando estaba aprendiendo Rust (procedente de C ++, D, Java, etc.), los genéricos de tipo de cuantificación universal (incluida su sintaxis) eran fáciles de entender (la vida útil en los genéricos era un poco más difícil).
Creo que impl Trait para los tipos de argumento generará mucha confusión entre los novatos y muchas preguntas como esta .
En ausencia de evidencia de qué cambios harían que Rust fuera más fácil de aprender, deberíamos abstenernos de hacer tales cambios y, en su lugar, hacer cambios que hagan/mantengan a Rust más consistente porque la coherencia hace que sea al menos fácil de recordar. Los novatos de Rust tendrán que leer el libro un par de veces de todos modos, por lo que introducir impl Trait para los argumentos que permitan posponer los genéricos en el libro hasta más tarde realmente no quita ninguna complejidad.

@eddyb Por cierto , ¿por qué necesitamos otra palabra clave existential para los tipos además de impl ? (Ojalá usáramos some para ambos...)

FWIW, recuerdo, cuando estaba aprendiendo Rust (procedente de C ++, D, Java, etc.), los genéricos de tipo de cuantificación universal (incluida su sintaxis) eran fáciles de entender (la vida útil en los genéricos era un poco más difícil).

Yo mismo tampoco creo que sea un problema. En mi empresa actual, dirijo clases de Rust; por ahora nos reunimos una vez a la semana y trato de enseñar en la implementación práctica. Las personas son programadores experimentados, provenientes en su mayoría de Java y Scala. Si bien hubo algunos obstáculos, los genéricos (al menos leerlos, son un poco cuidadosos al escribirlos) en la posición del argumento no fue un problema. Hubo un poco de sorpresa acerca de los genéricos en la posición de retorno (por ejemplo, la persona que llama elige lo que devuelve la función), especialmente porque a menudo se puede elidir, pero la explicación tomó como 2 minutos antes de hacer clic. Pero tengo miedo de mencionar siquiera la existencia de Rasgo impl en posición de argumento, porque ahora tendría que responder a la pregunta de por qué existe, y no tengo una respuesta real para eso. Eso no es bueno para la motivación y tener motivación es crucial para el proceso de aprendizaje.

Entonces, la pregunta es, ¿tiene la comunidad suficiente voz para reabrir el debate con algunos datos para respaldar los argumentos?

@eddyb Por cierto , ¿por qué necesitamos otra palabra clave existencial para tipos además de impl? (Ojalá usáramos algunos para ambos...)

¿Por qué no forall ... /yo me escabullo lentamente?

@phaazon Tenemos forall (es decir, "universal") y es for , por ejemplo, en HRTB ( for<'a> Trait<'a> ).

@eddyb Sí, luego utilícelo también para existencial , como lo hace Haskell con forall , por ejemplo.

Toda la discusión es muy obstinada, estoy un poco sorprendido de que la idea del argumento se haya estabilizado. Espero que haya una manera de impulsar otro RFC más tarde para deshacer eso (estoy completamente dispuesto a escribirlo porque realmente no me gusta toda la confusión que esto traerá).

Realmente no lo entiendo. ¿Cuál es el punto de tenerlos en posición de argumento? No escribo mucho sobre Rust, pero realmente me gustó poder hacer -> impl Trait . ¿Cuándo lo usaría en la posición de argumento?

Entendí que era principalmente por consistencia. Si puedo escribir el tipo impl Trait en una posición de argumento en una firma fn, ¿por qué no puedo escribirlo en otro lugar?

Dicho esto, personalmente preferiría haberle dicho a la gente "simplemente use parámetros de tipo"...

Sí, es por consistencia. Pero no estoy seguro de que sea un argumento lo suficientemente bueno, cuando los parámetros de tipo son tan fáciles de usar. Además, surge el problema de cuál pelusa a favor/en contra.

Además, surge el problema de cuál pelusa a favor/en contra.

Teniendo en cuenta que no puede expresar varias cosas con impl Trait en absoluto, funcione con impl Trait como uno de los argumentos no puede hacer turbofish y, por lo tanto, no puede tomar su dirección (¿olvidé algunos otra desventaja?), Creo que tiene poco sentido hacer pelusa contra los parámetros de tipo, porque necesita usarlos de todos modos.

por lo tanto, no puede tomar su dirección

Puede, haciéndolo inferir de la firma.

¿Cuál es el punto de tenerlos en posición de argumento?

No hay ninguno porque es exactamente lo mismo que usar un límite de rasgos.

fn foo(x: impl Debug)

es exactamente lo mismo que

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Además, considera esto:

fn foo<A>(x: A) -> A where A: Debug

impl Trait en posición de argumento no le permite hacer esto porque está anonimizado . Esta es entonces una característica bastante inútil porque ya tenemos todo lo que necesitamos para hacer frente a tales situaciones. La gente no aprenderá esa nueva característica fácilmente porque casi todo el mundo sabe escribir variables/parámetros de plantilla y Rust es el único lenguaje que usa esa sintaxis impl Trait . Es por eso que mucha gente dice que debería haberse mantenido en los enlaces return value/let, porque introdujo una semántica nueva y necesaria (es decir, el tipo elegido por el receptor de la llamada).

Para abreviar, @iopq : no necesitará esto, y no tiene otro sentido que " Agreguemos otra construcción de azúcar sintáctica que nadie realmente necesitará porque se adapta a un uso muy específico, es decir, variables de tipo anónimo" .

Además, algo que olvidé decir: hace que sea mucho más difícil ver cómo se parametriza/monomorfiza tu función.

@Verner Con

¿Cómo es consistente cuando -> impl Trait x: impl Trait la persona que llama elige el tipo, mientras que en

Entiendo que no hay otra manera de que funcione, pero eso no parece "coherente", parece lo contrario de consistente

Realmente estoy de acuerdo en que es todo menos consistente y que la gente se confundirá, tanto los recién llegados como los rustáceos avanzados y competentes.

Tuvimos dos RFC, que recibieron un total de casi 600 comentarios entre ellos desde hace más de 2 años, para resolver las preguntas que se están volviendo a litigar en este hilo:

  • rust-lang/rfcs#1522 ("Mínimo impl Trait ")
  • rust-lang/rfcs#1951 ("Finalice la sintaxis y el alcance de los parámetros para impl Trait , mientras lo expande a argumentos")

(Si lee estas discusiones, verá que inicialmente yo era un gran defensor del enfoque de dos palabras clave. Ahora creo que usar una sola palabra clave es el enfoque correcto).

Después de 2 años y cientos de comentarios, se llegó a una decisión y la característica ahora se ha estabilizado. Este es el problema de seguimiento de la función, que está abierta para realizar un seguimiento de los casos de uso aún inestables de impl Trait . Volver a litigar los aspectos resueltos de impl Trait está fuera de tema para este problema de seguimiento. Le invitamos a continuar hablando de esto, pero no en el rastreador de problemas.

¿Cómo se ha estabilizado cuando impl Trait ni siquiera ha recibido apoyo en la posición de argumento para fns en rasgos?

@daboross ¡ Entonces la casilla de verificación en la publicación original debe estar marcada!

(Solo descubrí que https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable actualmente funciona)

Creo que es extraño que https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug no funcione (todavía), además de este mensaje de error. ¿Soy el único que piensa así? Tal vez sería necesario agregar una casilla de verificación para impl Trait en la posición de retorno en las características, o fue una decisión consciente permitir solo impl Trait en la posición de argumento para funciones de características, lo que obligó a usar existential type para tipos de devolución? (que... me parecería inconsistente, pero ¿tal vez me estoy perdiendo un punto?)

@Ekleog

¿Fue una decisión consciente permitir solo impl Trait en posición de argumento para funciones de rasgo, forzando el uso de tipo existencial para tipos de retorno?

Sí, la posición de retorno impl Trait en rasgos se pospuso hasta que tengamos más experiencia práctica en el uso de tipos existenciales en rasgos.

@cramertj ¿

Me gustaría ver el rasgo impl en algunas versiones estables antes de agregar más funciones.

@mark-im No veo lo que es remotamente controvertido sobre la posición de retorno impl Trait para los métodos de rasgos, personalmente... tal vez me estoy perdiendo algo.

No creo que sea controvertido. Siento que estamos agregando funciones demasiado rápido. Sería bueno detenerse y concentrarse en la deuda técnica por un tiempo y obtener experiencia con el conjunto de funciones actual primero.

Veo. Supongo que lo considero una parte faltante de una característica existente más que una característica nueva.

Creo que @alexreg tiene razón, es muy tentador usar impl Trait existenciales en los métodos de rasgos. No es realmente una característica nueva, pero supongo que hay algunas cosas que abordar antes de intentar implementarla.

@phaazon Quizás, sí... Realmente no sé cuánto diferirían los detalles de implementación en comparación con lo que ya tenemos hoy, pero tal vez alguien podría comentar al respecto. Me encantaría ver tipos existenciales para enlaces let/const también, pero definitivamente puedo aceptar eso como una característica más allá de esta, por lo que esperaré otro ciclo más o menos antes de comenzar con él.

Me pregunto si podemos frenar el impl universal Rasgo en rasgos...

Pero sí, supongo que entiendo tu punto.

@mark-im No, no podemos, ya son estables .

Están en funciones, pero ¿qué pasa con las declaraciones de rasgos?

@mark-im como muestra el fragmento, son estables tanto en impls como en declaraciones de rasgos.

Simplemente saltando para ponerme al día sobre dónde nos estamos asentando con abstract type . Personalmente, estoy bastante de acuerdo con la sintaxis y las mejores prácticas propuestas recientemente por @Centril :

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Lo que se aplica a algún código mío, supongo que se vería así:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

No tiene sentido para mí que type Bar = (impl Display,); sería bueno, pero type Bar = impl Display; sería malo.

Si estamos decidiendo sobre diferentes sintaxis de tipo existencial alternativas (¿todas diferentes de rfc 2071 ?), ¿Sería un hilo de foro en https://users.rust-lang.org/ un buen lugar para hacerlo?

No tengo suficiente conocimiento de las alternativas para iniciar un hilo de este tipo ahora, pero como los tipos existenciales aún no están implementados, creo que la discusión en los foros y luego un nuevo RFC probablemente sería mejor que hablar de ello en el problema de seguimiento. .

¿Qué pasa con type Foo = impl Trait ?

@daboross Probablemente el foro interno en su lugar. Estoy considerando escribir un RFC al respecto para finalizar la sintaxis.

@daboross Ya ha habido más que suficiente discusión sobre la sintaxis en este hilo. Creo que si @Centril puede redactar un RFC en este momento, entonces genial.

¿Hay algún problema al que pueda suscribirme para discutir sobre existenciales en rasgos?

¿Hay algún argumento relacionado con macros para una sintaxis u otra?

@tomaka en el primer caso, type Foo = (impl Display,) es realmente la única sintaxis que tienes. Mi preferencia por type Foo: Trait sobre type Foo = impl Trait simplemente proviene del hecho de que estamos vinculando un tipo que podemos nombrar, como <TFoo: Trait> o where TFoo: Trait , mientras que con impl Trait no podemos nombrar el tipo.

Para aclarar, no digo que type Foo = impl Bar sea ​​malo, digo que type Foo: Bar es mejor en casos simples, en parte debido a la motivación de @KodrAus .

El último lo leo como: "el tipo Foo satisface Bar" y el primero como: "el tipo Foo es igual a algún tipo que satisface Bar". El primero es, por lo tanto, en mi opinión, más directo y natural desde una perspectiva extensional ("lo que puedo hacer con Foo"). Para comprender esto último, debe involucrar una comprensión más profunda de la cuantificación existencial de tipos.

type Foo: Bar también es bastante bueno porque si esa es la sintaxis utilizada como límite en un tipo asociado en un rasgo, entonces simplemente puede copiar la declaración en el rasgo al impl y simplemente funcionará (si eso es toda la información que quieras exponer..).

La sintaxis también es más concisa, especialmente cuando se trata de límites de tipos asociados y cuando hay muchos tipos asociados. Esto puede reducir el ruido y, por lo tanto, mejorar la legibilidad.

@KodrAus

Así es como leo esas definiciones de tipo:

  • type Foo: Trait significa " Foo es un tipo que implementa Trait "
  • type Foo = impl Trait significa " Foo es un alias de algún tipo que implementa Trait "

Para mí, Foo: Trait simplemente declara una restricción en Foo implementando Trait . En cierto modo, type Foo: Trait siente incompleto. Parece que tenemos una restricción, pero falta la definición real de Foo .

Por otro lado, impl Trait evoca "este es un solo tipo, pero el compilador descubre su nombre". Por lo tanto, type Foo = impl Trait implica que ya tenemos un tipo concreto (que implementa Trait ), del cual Foo es solo un alias.

Creo que type Foo = impl Trait transmite el significado correcto más claramente: Foo es un alias de algún tipo que implementa Trait .

@stjepang

type Foo: Trait significa "Foo es un tipo que implementa el rasgo"
[..]
En cierto modo, type Foo: Trait siente incompleto.

Así es como lo leo también (fraseo de módulo...), y es una interpretación extensionalmente correcta. Esto dice todo sobre lo que puede hacer con Foo (los morfismos que ofrece el tipo). Por lo tanto, es extensionalmente completo. Desde la perspectiva de los lectores y particularmente de los principiantes, creo que la extensionalidad es más importante.

Por otro lado, impl Trait evoca "este es un solo tipo, pero el compilador llena el vacío". Por lo tanto, type Foo = impl Trait implica que ya tenemos un tipo concreto (que implementa Trait ), del cual Foo es un alias, pero el compilador descubrirá qué tipo es realmente.

Esta es una interpretación más detallada e intensional relacionada con la representación que es redundante desde un punto de vista extensional. Pero esto es más completo en un sentido intensional.

@centril

Desde la perspectiva de los lectores y particularmente de los principiantes, creo que la extensionalidad es más importante.

Esta es una interpretación más detallada e intensional relacionada con la representación que es redundante desde un punto de vista extensional.

La dicotomía extensional vs intensional es interesante: nunca antes había pensado en impl Trait esta manera.

Aún así, lamento discrepar en la conclusión. FWIW, nunca he logrado asimilar los tipos existenciales en Haskell y Scala, así que cuéntame como un principiante. :) impl Trait en Rust se ha sentido muy intuitivo desde el primer día, lo que probablemente se deba al hecho de que lo considero un alias restringido en lugar de lo que se puede hacer con el tipo. Así que entre saber qué Foo es y qué se puede hacer con él, tomo la primera.

Sin embargo, solo mi 2c. Otros pueden tener diferentes modelos mentales de impl Trait .

Estoy completamente de acuerdo con este comentario : type Foo: Trait siente incompleto. Y type Foo = impl Trait siente más análogo a los usos de impl Trait otros lugares, lo que ayuda a que el lenguaje se sienta más consistente y memorable.

@joshtriplett Consulte https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 para comenzar con la discusión sobre la consistencia; Creo que permitir formularios de formulario es, de hecho, lo que se debe hacer de manera consistente. Y solo permitir una de las formas (cualquiera que sea...) es inconsistente. Permitir type Foo: Trait también encaja particularmente bien con https://github.com/rust-lang/rfcs/pull/2289 con el que podría indicar: type Foo: Iterator<Item: Display>; que hace que las cosas sean perfectamente uniformes.

@stjepang La perspectiva extensional de type Foo: Bar; no requiere que comprenda la cuantificación existencial en la teoría de tipos. Todo lo que realmente necesitas entender es que Foo te permite hacer todas las operaciones que ofrece Bar , eso es todo. Desde la perspectiva de un usuario de Foo , eso también es todo lo que es interesante.

@centril

Creo que ahora entiendo de dónde viene y el atractivo de llevar la sintaxis Type: Trait a tantos lugares como sea posible.

Hay una fuerte connotación en torno a : se usa para límites de tipo-implementación-rasgo y = se usa para definiciones de tipo y límites de tipo-igual-a-otro-tipo.

Creo que esto también es evidente en su RFC. Por ejemplo, tome estos dos límites de tipo:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

Estos dos límites al final tienen el mismo efecto, pero son (creo) sutilmente diferentes. El primero dice " Item debe implementar el rasgo Bar ", mientras que el segundo dice " Item debe ser igual a algún tipo que implemente Bar ".

Déjame tratar de ilustrar esta idea usando otro ejemplo:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

¿Cómo debemos definir un tipo existencial que implemente Person entonces?

  • type Someone: Person , que parece un límite de tipo.
  • type Someone = impl Person , que parece una definición de tipo.

@stjepang Verse como un límite de tipo no es algo malo :) Podemos implementar Person for Alice así:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

¡Mira mamá! El contenido dentro de { .. } tanto para el rasgo como para el impl es idéntico, lo que significa que puede copiar el texto del rasgo intacto en lo que respecta a Name .

Como un tipo asociado es una función de nivel de tipo (donde el primer argumento es Self ), podemos ver un alias de tipo como un tipo asociado de aridad 0, por lo que no sucede nada extraño.

Estos dos límites al final tienen el mismo efecto, pero son (creo) sutilmente diferentes. El primero dice "El elemento debe implementar la barra de rasgos", mientras que el segundo dice "El elemento debe ser igual a algún tipo que implemente la barra".

Sí; La primera frase me parece más precisa y natural. :)

@Centril Je. ¿Significa eso que solo type Thing; es suficiente para introducir un tipo abstracto?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm Creo que es técnicamente posible; pero podría preguntar si es deseable o no dependiendo de los pensamientos de uno sobre implícito/explícito. En ese caso particular, creo que sería técnicamente suficiente escribir:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

y el compilador podría simplemente inferir type Output: Sized; para usted (que es un límite profundamente poco interesante que no le brinda información). Es algo a considerar para límites más interesantes, pero no estará en mi propuesta inicial porque creo que podría alentar API de bajo costo, incluso cuando el tipo concreto es muy simple, debido a la pereza del programador :) Tampoco type Output; ser inicialmente por la misma razón.

Creo que después de leer todo esto, tiendo a estar más de acuerdo con @Centril. Cuando veo type Foo = impl Bar tiendo a pensar que Foo es un tipo particular, como con otros alias. Pero no lo es. Considere este ejemplo:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

En mi humilde opinión, es un poco extraño ver = en la declaración de Displayable pero luego no tener los tipos de retorno de foo y bar iguales (es decir, esto = no es transitivo, a diferencia de cualquier otro lugar). El problema es que Foo _no_ es un alias para un tipo particular que implica algún Rasgo. Dicho de otra manera, es un solo tipo en cualquier contexto en el que se use, pero ese tipo puede ser diferente para diferentes usos, como en el ejemplo.

Algunas personas mencionaron que type Foo: Bar siente "incompleto". Para mí esto es algo bueno. En cierto sentido, Foo está incompleto; no sabemos qué es, pero sabemos que satisface Bar .

@marca-im

El problema es que Foo no es un alias para un tipo particular que implique algún rasgo. Dicho de otra manera, es un solo tipo en cualquier contexto en el que se use, pero ese tipo puede ser diferente para diferentes usos, como en el ejemplo.

Vaya, ¿es eso realmente cierto? Eso ciertamente sería muy confuso para mí.

¿Hay alguna razón por la que Displayable sería una forma abreviada de impl Display lugar de un solo tipo concreto? ¿Es tal comportamiento incluso útil considerando que los alias de rasgos (problema de seguimiento: https://github.com/rust-lang/rust/issues/41517) se pueden usar de manera similar? Ejemplo:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@marca-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Ese no es un ejemplo válido. De la sección de referencia sobre tipos existenciales en RFC 2071 :

existential type Foo = impl Debug;

Foo se puede usar como i32 en varios lugares a lo largo del módulo. Sin embargo, cada función que usa Foo como i32 debe imponer restricciones de forma independiente sobre Foo modo que debe ser i32

Cada declaración de tipo existencial debe estar restringida por al menos un cuerpo de función o un inicializador const/static. Un cuerpo o inicializador debe restringir completamente o no imponer restricciones sobre un tipo existencial dado.

No se menciona directamente, pero se requiere para que funcione el resto del RFC, es que dos funciones en el ámbito del tipo existencial no pueden determinar un tipo concreto diferente para ese existencial. Esa será alguna forma de error de tipo conflictivo.

Supongo que su ejemplo daría algo como expected type `&'static str` but found type `i32` al devolver bar , ya que foo ya habría establecido el tipo concreto de Displayable en &'static str .

EDITAR: a menos que llegue a esto por la intuición de que

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

es equivalente a

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

en lugar de mi expectativa de

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Supongo que cuál de esas dos interpretaciones es la correcta podría depender del RFC que @Centril pueda estar escribiendo.

El problema es que Foo no es un alias para un tipo particular que implique algún rasgo.

Supongo que cuál de esas dos interpretaciones es la correcta podría depender del RFC que @Centril pueda estar escribiendo.

La razón por la que existe type Displayable = impl Display; es que es un alias para un tipo particular.
Consulte https://github.com/rust-lang/rfcs/issues/1738 , que es el problema que resuelve esta función.

@ Nemo157 Su expectativa es correcta. :)

La siguiente:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

sería desazucarado a:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

donde _0 , _1 y _2 son tipos nominalmente diferentes por lo que Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (y el instancias simétricas) están todas deshabitadas, donde Id se define en refl .

Descargo de responsabilidad: no he leído (de buena gana) el RFC (pero sé de qué se trata), para poder comentar sobre lo que se siente "intuitivo" con las sintaxis.

Para la sintaxis type Foo: Trait , esperaría que algo como esto fuera posible:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

De la misma manera que where Foo: Display, Foo: Debug actualmente es posible.

Si no está permitida la sintaxis, creo que es un problema con la sintaxis.

Ah, y creo que cuanta más sintaxis tiene Rust, más difícil se vuelve aprenderlo. Incluso si una sintaxis es "más fácil de aprender", siempre que las dos sintaxis sean necesarias, el principiante eventualmente tendrá que aprender ambas, y probablemente más temprano que tarde si ingresa a un proyecto ya existente.

@Ekleog

Para la sintaxis type Foo: Trait , esperaría que algo como esto fuera posible:

Es posible. Esos "alias de tipo" declaran tipos asociados (los alias de tipo pueden interpretarse como funciones de nivel de tipo 0-ario mientras que los tipos asociados son funciones de nivel de tipo 1+-ario). Por supuesto, no puede tener varios tipos asociados con el mismo nombre en un rasgo, sería como tratar de definir dos alias de tipo con el mismo nombre en un módulo. En un impl , type Foo: Bar también corresponde a la cuantificación existencial.

Ah, y creo que cuanta más sintaxis tiene Rust, más difícil se vuelve aprenderlo.

Ambas sintaxis ya se utilizan. type Foo: Bar; ya es legal en rasgos, y también para cuantificación universal como Foo: Bar donde Foo es una variable de tipo. impl Trait se utiliza para la cuantificación existencial en posición de retorno y para la cuantificación universal en posición de argumento. Permitiendo que ambos taponen las lagunas de consistencia en el lenguaje. También son óptimos para diferentes escenarios, por lo que tener ambos le brinda el óptimo global.

Además, es poco probable que el principiante necesite type Foo = (impl Bar, impl Baz); . La mayoría de los usos probablemente serán type Foo: Bar; .

La solicitud de extracción original para RFC 2071 menciona una palabra clave typeof que parece haber sido descartada por completo en esta discusión. Encuentro que la sintaxis propuesta actualmente es bastante implícita, ya que tanto el compilador como cualquier humano que lea el código buscan el tipo concreto.

Preferiría que esto se hiciera explícito. Entonces, en lugar de

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

escribiríamos

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(con el nombre de return_type_of para ser bikeshedded), o incluso

fn foo_func() -> impl SomeTrait as Foo { ... }

que ni siquiera necesitaría nuevas palabras clave y es fácilmente comprensible para cualquier persona que conozca la sintaxis de Trait impl. La última sintaxis es concisa y tiene toda la información en un solo lugar. Para los rasgos podría verse así:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

o incluso

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Lo siento si esto ya se ha discutido y descartado, pero no pude encontrarlo.

@centril

Es posible. Esos "alias de tipo" declaran tipos asociados (los alias de tipo pueden interpretarse como funciones de nivel de tipo 0-ario mientras que los tipos asociados son funciones de nivel de tipo 1+-ario). Por supuesto, no puede tener varios tipos asociados con el mismo nombre en un rasgo, sería como tratar de definir dos alias de tipo con el mismo nombre en un módulo. En un impl, escriba Foo: Bar también corresponde a la cuantificación existencial.

(lo siento, quise ponerlo en impl Trait for Struct , no en trait Trait )

Lo siento, no estoy seguro de entender. Lo que trato de decir es, para mí código como

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(enlace de juegos para la versión completa)
siente que debería funcionar.

Debido a que es sólo poner dos límites en Type , de la misma manera como where Type: Debug, Type: Display trabajo .

Si esto no está permitido (lo que parece entender por "Por supuesto que no puede tener múltiples tipos asociados con el mismo nombre en un rasgo"? Pero dado mi error al escribir trait Trait lugar de impl Trait for Struct No estoy seguro), entonces creo que es un problema con la sintaxis de type Type: Trait .

Luego, dentro de una declaración trait , la sintaxis ya es type Type: Trait y no permite múltiples definiciones. Así que supongo que tal vez este barco ya navegó hace mucho tiempo...

Sin embargo, como señalaron anteriormente @stjepang y @joshtriplett , type Type: Trait siente incompleto. Y si bien puede tener sentido en declaraciones trait (en realidad está diseñado para estar incompleto, aunque es raro que no permita múltiples definiciones), no tiene sentido en un bloque impl Trait , donde se supone que el tipo se conoce con certeza (y actualmente solo se puede escribir como type Type = RealType )

impl Trait se utiliza para la cuantificación existencial en posición de retorno y para la cuantificación universal en posición de argumento.

Sí, también pensé en impl Trait en la posición de argumento al escribir esto, y me preguntaba si debería decir que habría apoyado el mismo argumento para impl Trait en la posición de argumento si hubiera sabido que se estaba estabilizando. . Dicho esto, creo que sería mejor no volver a encender este debate :)

Permitiendo que ambos taponen las lagunas de consistencia en el lenguaje. También son óptimos para diferentes escenarios, por lo que tener ambos le brinda el óptimo global.

Óptimo y sencillez

Bueno, creo que a veces perder lo óptimo en favor de la simplicidad es algo bueno. Como, C y ML nacieron casi al mismo tiempo. C hizo grandes concesiones al óptimo a favor de la simplicidad, ML estaba mucho más cerca del óptimo pero mucho más complejo. Incluso contando los derivados de estos lenguajes, no creo que la cantidad de desarrolladores de C y de desarrolladores de ML sea comparable.

impl Trait y :

Actualmente, en torno a las sintaxis impl Trait y : , creo que hay una tendencia a crear dos sintaxis alternativas para el mismo conjunto de características. Sin embargo, no creo que sea algo bueno, ya que tener dos sintaxis para las mismas funciones solo puede confundir a los usuarios, especialmente cuando siempre difieren sutilmente en su semántica exacta.

Imagine un principiante que siempre vio type Type: Trait llegar a su primer type Type = impl Trait . Es probable que puedan adivinar lo que está pasando, pero estoy bastante seguro de que habrá un momento de "¿WTF es eso? ¿He estado usando Rust durante años y todavía hay una sintaxis que nunca vi?”. Que es más o menos la trampa en la que cayó C++.

Característica hinchada

Lo que pienso es que, básicamente, cuantas más funciones tenga, más difícil será aprender el idioma. Y no veo una gran ventaja de usar type Type: Trait sobre type Type = impl Trait : ¿son como 6 caracteres guardados?

Hacer que rustc genere un error al ver type Type: Trait que dice que la persona que lo escribe usa type Type = impl Trait tendría mucho más sentido para mí: al menos hay una sola forma de escribir las cosas , tiene sentido para todos ( impl Trait ya se reconoce claramente como existencial en la posición de retorno) y cubre todos los casos de uso. Y si las personas intentan usar lo que creen que es intuitivo (aunque no estoy de acuerdo con eso, para mí = impl Trait es más intuitivo, en comparación con el actual = i32 ), son redirigidos correctamente al manera convencionalmente correcta de escribirlo.

La solicitud de extracción original para RFC 2071 menciona una palabra clave typeof que parece haber sido descartada por completo en esta discusión. Encuentro que la sintaxis propuesta actualmente es bastante implícita, ya que tanto el compilador como cualquier humano que lea el código buscan el tipo concreto.

typeof se discutió brevemente en el número que abrí hace 1,5 años: https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

Hablando como principiante, encuentro la sintaxis type Foo: Bar confusa. Es la sintaxis de tipo asociado, pero se supone que esos están en rasgos, no en estructuras. Si ve impl Trait una vez, puede averiguar qué es o, de lo contrario, puede buscarlo. Es más difícil hacer eso con la otra sintaxis y no estoy seguro de cuál es el beneficio.

Parece que algunas personas en el equipo de idiomas se oponen realmente a usar impl Trait para nombrar tipos existenciales, por lo que prefieren usar cualquier otra cosa en su lugar. Incluso el comentario aquí tiene poco sentido para mí.

Pero de todos modos, creo que este caballo fue golpeado hasta la muerte. Probablemente haya cientos de comentarios sobre la sintaxis y solo un puñado de sugerencias (me doy cuenta de que solo estoy empeorando las cosas). Está claro que ninguna sintaxis no hará felices a todos, y hay argumentos a favor y en contra de todos ellos. Tal vez deberíamos elegir uno y apegarnos a él.

Woah, eso no es en absoluto lo que entendí. ¡Gracias @Nemo157 por

En ese caso, preferiría la sintaxis =.

@Ekleog

entonces creo que es un problema con la sintaxis type Type: Trait .

Podría estar permitido y estaría perfectamente bien definido, pero normalmente escribes where Type: Foo + Bar lugar de where Type: Foo, Type: Bar , por lo que no parece una muy buena idea. También podría generar fácilmente un buen mensaje de error para este caso sugiriendo que escriba Foo + Bar en su lugar en el caso del tipo asociado.

type Foo = impl Bar; también tiene problemas de comprensibilidad en el sentido de que ve = impl Bar y concluye que puede sustituirlo cada vez que se usa como -> impl Bar ; pero eso no funcionaría. @mark-im hizo esta interpretación, que parece un error mucho más probable. Por lo tanto, concluyo que type Foo: Bar; es la mejor opción para aprender.

Sin embargo, como lo señalaron anteriormente @stjepang y @joshtriplett , el tipo Type: Trait se siente incompleto.

No está incompleto desde un punto de vista extensional. Obtiene exactamente tanta información de type Foo: Bar; como de type Foo = impl Bar; . Entonces, desde la perspectiva de lo que puede hacer con type Foo: Bar; , está completo. De hecho, este último está desazucarado como type _0: Bar; type Foo = _0; .

EDITAR: lo que quise decir es que, si bien puede parecer incompleto para algunos, no lo es desde un punto de vista técnico.

Dicho esto, creo que sería mejor no volver a encender este debate :)

Es una buena idea. Debemos considerar el lenguaje tal como es al diseñar, no como nos gustaría que fuera.

Bueno, creo que a veces perder lo óptimo en favor de la simplicidad es algo bueno.

Si debemos optar por la simplicidad, dejaría type Foo = impl Bar; lugar.
Debe tenerse en cuenta que la supuesta simplicidad de C (supuesta, porque Haskell Core y cosas similares son probablemente más simples mientras aún suenan...) tiene un alto precio cuando se trata de expresividad y solidez. C no es mi estrella polar en el diseño de lenguajes; lejos de ahi.

Actualmente, en torno a las sintaxis impl Trait y : , creo que hay una tendencia a crear dos sintaxis alternativas para el mismo conjunto de funciones. Sin embargo, no creo que sea algo bueno, ya que tener dos sintaxis para las mismas funciones solo puede confundir a los usuarios, especialmente cuando siempre difieren sutilmente en su semántica exacta.

Pero no diferirán en absoluto en su semántica. Uno desazúcar al otro.
Creo que la confusión de tratar de escribir type Foo: Bar; o type Foo = impl Bar solo para que uno de ellos no funcione, aunque ambos tienen una semántica perfectamente definida, solo se interpone en el camino del usuario. Si un usuario intenta escribir type Foo = impl Bar; , entonces se dispara un lint y propone type Foo: Bar; . La pelusa le está enseñando al usuario sobre la otra sintaxis.
Para mí, es importante que el lenguaje sea uniforme y consistente; Si hemos decidido usar ambas sintaxis en alguna parte, debemos aplicar esa decisión de manera consistente.

Imagine un principiante que siempre vio type Type: Trait llegar a su primer type Type = impl Trait .

En este caso específico, se dispararía una pelusa y recomendaría la sintaxis anterior. Cuando se trata de type Foo = (impl Bar, impl Baz); , el principiante tendrá que aprender -> impl Trait en cualquier caso, por lo que debería poder inferir el significado de eso.

Que es más o menos la trampa en la que cayó C++.

El problema de C++ es principalmente que es bastante antiguo, tiene el bagaje de C y muchas características que admiten demasiados paradigmas. Estas no son características distintas, solo una sintaxis diferente.

Lo que pienso es que, básicamente, cuantas más funciones tenga, más difícil será aprender el idioma.

Creo que aprender un nuevo idioma se trata principalmente de aprender sus bibliotecas importantes. Ahí es donde se pasará la mayor parte del tiempo. Las características correctas pueden hacer que las bibliotecas sean mucho más componibles y funcionen en más casos. Prefiero mucho más un lenguaje que dé un buen poder de abstracción que uno que te obligue a pensar en un nivel bajo y que provoque la duplicación. En este caso, no estamos agregando más poder de abstracción o incluso características realmente uniformes, solo una mejor ergonomía.

Y no veo una gran ventaja de usar type Type: Trait sobre type Type = impl Trait: , ¿se guardan como 6 caracteres?

Sí, solo se guardan 6 caracteres. Pero si consideramos type Foo: Iterator<Item: Iterator<Item: Display>>; , entonces obtendríamos: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; que tiene mucho más ruido. type Foo: Bar; también es más directo en comparación con este último, menos propenso a malas interpretaciones (re. sustitución...) y funciona mejor para tipos asociados (copiar el tipo del rasgo...).
Además, type Foo: Bar podría extenderse naturalmente a type Foo: Bar = ConcreteType; lo que expondría el tipo concreto pero también garantizaría que satisfaga Bar . No se puede hacer tal cosa por type Foo = impl Trait; .

Hacer que rustc emita un error al ver type Type: Trait que dice que la persona que lo escribe usa type Type = impl Trait tendría mucho más sentido para mí: al menos hay una sola forma de escribir las cosas,

son redirigidos legítimamente a la forma convencionalmente correcta de escribirlo.

Estoy proponiendo que haya una forma convencional de escribir las cosas; type Foo: Bar; .

@nicola

Hablando como principiante, encuentro la sintaxis type Foo: Bar confusa. Es la sintaxis de tipo asociado, pero se supone que esos están en rasgos, no en estructuras.

Reitero que los alias de tipo realmente se pueden ver como tipos asociados. Podrás decir:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Vemos que funciona exactamente igual en tipos asociados y alias de tipo.

Sí, solo se guardan 6 caracteres. Pero si consideramos type Foo: Iterator<Item: Iterator<Item: Display>>; , entonces obtendríamos: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; que tiene mucho más ruido.

Esto parece ortogonal a la sintaxis para declarar un existencial nombrado. Las cuatro sintaxis que recuerdo que se propusieron potencialmente permitirían esto como

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Ser capaz de usar su abreviatura propuesta Trait<AssociatedType: Bound> lugar de la sintaxis Trait<AssociatedType = impl Bound> para declarar tipos existenciales anónimos para los tipos asociados de un tipo existencial (ya sea con nombre o anónimo) es una característica independiente (pero probablemente relevante en términos de mantener consistente todo el conjunto de características de tipo existencial).

@Nemo157 Son características diferentes, sí; pero creo que es natural considerarlos juntos en aras de la coherencia.

@centril

Lo siento, pero están equivocados. No está incompleto desde un punto de vista extensional.

Nunca sugerí que a la sintaxis propuesta le faltaba información; Estaba sugiriendo que se siente incompleto; se ve mal para mí, y para los demás. Entiendo que no estés de acuerdo con eso, desde la perspectiva de la que vienes, pero eso no hace que te sientas mal.

También observe que en este hilo, las personas demostraron un problema de interpretación con esta diferencia de sintaxis exacta. type Foo = impl Trait parece que deja más claro que Foo es un tipo concreto específico pero sin nombre, sin importar cuántas veces lo use, en lugar de un alias para un rasgo que puede adoptar un tipo concreto diferente cada vez que lo use.

Creo que ayuda decirle a la gente que pueden tomar todo lo que saben sobre -> impl Trait y aplicarlo a type Foo = impl Trait ; hay un concepto generalizado impl Trait que pueden ver usado como bloque de construcción en ambos lugares. Sintaxis como type Foo: Trait oculta ese bloque de construcción generalizado.

@joshtriplett

Estaba sugiriendo que se siente incompleto; se ve mal para mí, y para los demás.

Bien; Propongo que usemos un término diferente a incomplete aquí porque, para mí, sugiere una falta de información.

También observe que en este hilo, las personas demostraron un problema de interpretación con esta diferencia de sintaxis exacta.

Lo que observé fue un error de interpretación, hecho en el hilo, sobre lo que significa type Foo = impl Bar; . Una persona interpretó los diferentes usos de Foo como no siendo nominalmente del mismo tipo, sino tipos diferentes. Es decir, exactamente: "un alias para un rasgo que puede tomar un tipo concreto diferente cada vez que lo usas" .

Algunos han declarado que type Foo: Bar; es confuso, pero no estoy seguro de cuál es la interpretación alternativa de type Foo: Bar; que es diferente al significado previsto. Me interesaría escuchar acerca de interpretaciones alternativas.

@centril

Reitero que los alias de tipo realmente se pueden ver como tipos asociados.

Pueden, pero en este momento los tipos asociados están relacionados con los rasgos. impl Trait funciona en todas partes, o casi. Si desea presentar impl Trait como una especie de tipo asociado, deberá introducir dos conceptos a la vez. Es decir, ve impl Trait como un tipo de devolución de función, adivine o lea de qué se trata, luego, cuando vea impl Trait en un alias de tipo, puede reutilizar ese conocimiento.

Compare eso con ver tipos asociados en una definición de rasgo. En ese caso, cree que es algo que otras estructuras deben definir o implementar. Pero si te encuentras con un type Foo: Debug fuera de un rasgo, no sabrás qué es. No hay nadie para implementarlo, entonces, ¿es algún tipo de declaración anticipada? ¿Tiene algo que ver con la herencia, como en C++? ¿Es como un módulo ML en el que otra persona elige el tipo? Y si has visto impl Trait antes, no hay nada que los vincule. Escribimos fn foo() -> impl ToString , no fn foo(): ToString .

tipo Foo = barra impl; también tiene problemas de comprensibilidad en el sentido de que ve = barra impl y concluye que puede sustituirlo cada vez que se usa como -> barra impl

Lo he dicho aquí antes, pero es como pensar que let x = foo(); significa que puedes usar x lugar de foo() . En cualquier caso, es un detalle que alguien puede buscar rápidamente cuando sea necesario, pero no cambia fundamentalmente el concepto.

Es decir, es fácil averiguar de qué se trata (un tipo deducido como en -> impl Trait ), incluso si no sabe exactamente cómo funciona (qué sucede cuando tiene definiciones contradictorias). Con la otra sintaxis es difícil incluso darse cuenta de lo que es.

@centril

Bien; Propongo que usemos un término diferente a incompleto aquí porque, para mí, sugiere una falta de información.

"incompleto" no tiene por qué significar falta de información , puede significar que algo parece que debería tener otra cosa y no es así.

type Foo: Trait; no parece una declaración completa. Parece que le falta algo. Y parece gratuitamente diferente de type Foo = SomeType<X, Y, Z>; .

Tal vez estemos llegando al punto en que nuestras frases ingeniosas por sí solas no pueden cerrar esta brecha de consenso entre type Inferred: Trait y type Inferred = impl Trait .

¿Cree que valdría la pena armar una implementación experimental de esta función con cualquier sintaxis (incluso la especificada en el RFC) para que podamos comenzar a jugar con ella en programas más grandes para ver cómo encaja en el contexto?

@nicola

[..] impl Trait funciona en todas partes, o casi

Bueno, Foo: Bound también funciona en casi todas partes ;)

Pero si te encuentras con un type Foo: Debug fuera de un rasgo, no sabrás qué es.

Creo que la progresión de usarlo en: rasgo -> impl -> tipo alias ayuda en el aprendizaje.
Además, creo que la inferencia de que "el tipo Foo implementa Debug" es probable de
ver type Foo: Debug en rasgos y desde límites genéricos y también es correcto.

¿Tiene algo que ver con la herencia, como en C++?

Creo que la falta de herencia en Rust debe aprenderse en una etapa mucho más temprana que cuando se aprende la función que estamos discutiendo, ya que esto es fundamental para Rust.

¿Es como un módulo ML en el que otra persona elige el tipo?

Esa inferencia también se puede hacer para type Foo = impl Bar; debido a arg: impl Bar donde la persona que llama (usuario) elige el tipo. Para mí, la inferencia de que el usuario elige el tipo parece menos probable para type Foo: Bar; .

Lo he dicho aquí antes, pero es como pensar que let x = foo(); significa que puedes usar x lugar de foo() .

Si el lenguaje es referencialmente transparente, puede sustituir x por foo() . Hasta que agreguemos type Foo = impl Foo; al sistema, los alias de tipo son afaik referencialmente transparentes. Por el contrario, si ya hay un enlace x = foo() disponible, entonces otros foo() en son reemplazables con x .

@joshtriplett

"incompleto" no tiene por qué significar falta de información, puede significar que algo parece que debería tener otra cosa y no la tiene.

Lo suficientemente justo; pero ¿qué se supone que tiene que no tiene?

type Foo: Trait; no parece una declaración completa.

me parece completo Parece un juicio que Foo satisface Trait que es precisamente el significado pretendido.

@Centril para mí, "algo que falta" es el tipo real para el que este es un alias. Eso está un poco relacionado con mi confusión anterior. No es que no exista tal tipo, solo que ese tipo es anónimo... Usar = sutilmente implica que hay un tipo y es siempre el mismo tipo pero no podemos nombrarlo.

Sin embargo, creo que estamos agotando estos argumentos. Sería genial implementar ambas sintaxis experimentalmente y ver qué funciona mejor.

@marca-im

@Centril para mí, "algo que falta" es el tipo real para el que este es un alias. Eso está un poco relacionado con mi confusión anterior. No es que no exista tal tipo, solo que ese tipo es anónimo... Usar = sutilmente implica que hay un tipo y es siempre el mismo tipo pero no podemos nombrarlo.

Eso es exactamente lo que siento para mí, también.

¿Alguna posibilidad de abordar pronto los dos elementos diferidos, además del problema de la elisión de por vida? Lo haría yo mismo, pero no tengo idea de cómo!

Todavía hay mucha confusión sobre qué significa exactamente impl Trait , y no es del todo obvio. Creo que los elementos diferidos definitivamente deberían esperar hasta que tengamos una idea clara de la semántica exacta de impl Trait (que debería estar disponible pronto).

@varkor ¿Qué semántica no está clara? AFAIK, nada sobre la semántica de impl Trait ha cambiado desde RFC 1951 y se expandió en 2071.

@alexreg No tenía ningún plan, pero aquí hay un resumen: después de agregar el análisis, debe reducir los tipos de static s y const s dentro de un impl existencial contexto de rasgo, como se hace aquí para los tipos de retorno de funciones. . Sin embargo, querrá hacer que el DefId en ImplTraitContext::Existential opcional, ya que no quiere que su impl Trait recoja genéricos de una definición de función principal. Eso debería conseguirte un poco decente del camino. Es posible que le resulte más fácil si se basa en el PR de tipo existencial de @oli-obk.

@cramertj : la semántica de impl Trait en el idioma está completamente restringida a su uso en firmas de funciones y no es cierto que extenderlo a otras posiciones tenga un significado obvio. Diré algo más detallado sobre esto pronto, donde parece estar ocurriendo la mayor parte de la conversación.

@varkor

la semántica de impl Trait en el lenguaje está completamente restringida a su uso en firmas de funciones y no es cierto que extenderlo a otras posiciones tenga un significado obvio.

El significado se especificó en RFC 2071 .

@cramertj : el significado en RFC 2071 es ambiguo y permite múltiples interpretaciones de lo que significa allí la frase "tipo existencial".

TL; DR: he tratado de establecer un significado preciso para impl Trait , que creo que aclara detalles que, al menos intuitivamente, no estaban claros; junto con una propuesta para una nueva sintaxis de alias de tipo.

Tipos existenciales en Rust (post)


Ha habido mucha discusión en el chat rust-lang de Discord sobre la semántica precisa (es decir, formal, teórica) de impl Trait en los últimos días. Creo que ha sido útil aclarar muchos detalles sobre la característica y exactamente qué es y qué no es. También arroja algo de luz sobre qué sintaxis son plausibles para los alias de tipo.

Escribí un pequeño resumen de algunas de nuestras conclusiones. Esto proporciona una interpretación de impl Trait que creo que es bastante clara y describe con precisión las diferencias entre la posición del argumento impl Trait y la posición de retorno impl Trait (que no es "universalmente- cuantificado" frente a "cuantificado existencialmente"). También hay algunas conclusiones prácticas.

En él, propongo una nueva sintaxis que cumple con los requisitos comúnmente establecidos de un "alias de tipo existencial":
type Foo: Bar = _;

Sin embargo, debido a que es un tema tan complejo, hay muchas cosas que deben aclararse primero, así que lo he escrito como una publicación separada. ¡Los comentarios son muy apreciados!

Tipos existenciales en Rust (post)

@varkor

RFC 2071 es ambiguo y permite múltiples interpretaciones de lo que significa allí la frase "tipo existencial".

¿Cómo es ambiguo? He leído tu publicación: todavía solo conozco un significado de existencial no dinámico en estática y constantes. Se comporta de la misma manera que la posición de retorno impl Trait , al introducir una nueva definición de tipo existencial por elemento.

type Foo: Bar = _;

Discutimos esta sintaxis durante el RFC 2071. Como dije allí, me gusta que demuestre claramente que Foo es un único tipo inferido y que deja espacio para tipos no inferidos que quedan fuera del módulo actual ( por ejemplo, type Foo: Bar = u32; ). No me gustaron dos aspectos: (1) no tiene palabra clave y, por lo tanto, es más difícil de buscar y (b) tiene el mismo problema de verbosidad en comparación con type Foo = impl Trait que la sintaxis de abstract type Foo: Bar; tiene: type Foo = impl Iterator<Item = impl Display>; convierte en type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . No creo que ninguno de estos sea un factor decisivo, pero no es una victoria clara de una forma u otra en mi opinión.

@cramertj La ambigüedad surge aquí:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Si Foo fuera realmente un alias de tipo para un tipo existencial, entonces f y g admitirían diferentes tipos de retorno concretos. Varias personas han leído instintivamente esa sintaxis de esta manera y, de hecho, algunos participantes en la discusión de sintaxis de RFC 2071 se dieron cuenta de que no es como parte de la reciente discusión de Discord.

El problema es que, especialmente frente a la posición de argumento impl Trait , no está del todo claro dónde debe ir el cuantificador existencial. Para los argumentos, tiene un alcance estricto; para la posición de retorno, parece tener un alcance limitado, pero resulta ser más amplio que eso; por type Foo = impl Bar ambas posiciones son plausibles. La sintaxis basada en _ empuja hacia una interpretación que ni siquiera implica "existencial", eludiendo claramente este problema.

Si Foo fuera realmente un alias de tipo para un tipo existencial

(énfasis mío). Leí que 'an' como 'específico', lo que significa que f y g _no_ admitirían diferentes tipos de retorno concretos, ya que se refieren al mismo tipo existencial. Siempre he visto que type Foo = impl Bar; el mismo significado que let foo: impl Bar; , es decir, introduce un nuevo tipo existencial anónimo; haciendo que su ejemplo sea equivalente a

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

que espero que sea relativamente inequívoco.


Un problema es que el significado de " impl Trait en alias de tipo" nunca se ha especificado en un RFC. Se menciona brevemente en la sección "Alternativas" de RFC 2071 , pero se descarta explícitamente debido a estas ambigüedades de enseñanza inherentes.

También siento que vi alguna mención de que los alias de tipo ya no son referencialmente transparentes. Creo que estaba en u.rl.o, pero no he podido encontrar la discusión después de buscar un poco.

@cramertj
Para continuar con el punto de @rpjohnst , existen múltiples interpretaciones de la semántica de impl Trait , que son consistentes con el uso actual en las firmas, pero tienen diferentes consecuencias al extender impl Trait a otros ubicaciones (conozco 2 además de la descrita en la publicación, pero que aún no están listas para la discusión). Y no creo que sea cierto que la interpretación en la publicación sea necesariamente la más obvia (yo personalmente no vi ninguna explicación similar sobre APIT y RTIP desde esa perspectiva).

Con respecto a type Foo: Bar = _; , creo que tal vez debería discutirse nuevamente: no hay nada de malo en revisar viejas ideas con nuevos ojos. En cuanto a sus problemas con él:
(1) No tiene palabra clave, pero es la misma sintaxis que la inferencia de tipos en cualquier lugar. La búsqueda de documentación para "guión bajo" / "tipo de guión bajo" / etc. podría proporcionar fácilmente una página sobre la inferencia de tipo.
(2) Sí, eso es cierto. Hemos estado pensando en una solución para esto, que creo que encaja muy bien con la notación de subrayado, que con suerte estará lista para sugerir pronto.

Al igual que @cramertj , realmente no veo el argumento aquí.

Simplemente no veo la ambigüedad fundamental que describe la publicación de @varkor ) el último es equivalente a "tipos universales" y por lo tanto la frase "tipo existencial" sería totalmente inútil si pretendiéramos permitir esa interpretación. afaik, cada RFC sobre el tema siempre ha asumido que los tipos universales y existenciales eran dos cosas distintas. Entiendo que en la teoría de tipos real eso es lo que significa y que el isomorfismo es matemáticamente muy real, pero para mí eso es solo un argumento de que hemos estado usando mal la terminología de la teoría de tipos y necesitamos elegir otra jerga para esto, no un argumento que la semántica prevista de impl Trait nunca estuvo clara y debe repensarse.

La ambigüedad de alcance que describe @rpjohnst es un problema grave, pero cada sintaxis propuesta es potencialmente confundible con alises de tipos o tipos asociados. Cuál de esas confusiones es "peor" o "más probable" es precisamente el interminable cobertizo para bicicletas que ya no logramos resolver después de varios cientos de comentarios. Me gusta que type Foo: Bar = _; parezca solucionar el problema de type Foo: Bar; de necesitar una explosión de varias declaraciones para declarar cualquier existencial ligeramente no trivial, pero no creo que eso sea suficiente para cambiar realmente el Situación de "bicicleta interminable".

De lo que estoy convencido es que cualquier sintaxis con la que terminemos debe tener una palabra clave que no sea type , porque todas las sintaxis "solo type " son demasiado engañosas. De hecho, tal vez no use type en la sintaxis _en absoluto_, por lo que no hay forma de que alguien pueda asumir que está buscando "un tipo de alias, pero de alguna manera más existencial".

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

No es obvio para mí que ninguno de estos _preven_ completamente la interpretación errónea de que f y g podrían devolver dos tipos diferentes implementando Trait , pero sospecho que esto es lo más cercano a la prevención como posiblemente podríamos conseguir.

@Ixrec
La frase "tipo existencial" es problemática específicamente _por_ la ambigüedad del alcance. No he visto a nadie más señalar que el alcance es completamente diferente para APIT y RPIT. Esto significa que una sintaxis como type Foo = impl Bar , donde impl Bar es un "tipo existencial" es inherentemente ambigua.

Sí, la terminología de la teoría de tipos ha sido mal utilizada, mucho. Pero se ha usado incorrectamente (o al menos no se ha explicado) en el RFC, por lo que existe una ambigüedad derivada del propio RFC.

La ambigüedad de alcance que describe @rpjohnst es un problema grave, pero cada sintaxis propuesta es potencialmente confundible con alises de tipos o tipos asociados. Cuál de esas confusiones es "peor" o "más probable" es precisamente el interminable cobertizo para bicicletas que ya no logramos resolver después de varios cientos de comentarios.

No, no creo que esto sea cierto. Es posible llegar a una sintaxis consistente que no tenga esta confusión. Me atrevería a decir que el despojo de bicicletas se debe a que las dos propuestas actuales son malas, por lo que realmente no satisfacen a nadie.

De lo que estoy convencido es que cualquier sintaxis con la que terminemos debe tener una palabra clave que no sea type

Tampoco creo que esto sea necesario. En sus ejemplos, ha inventado una notación completamente nueva, que es algo que desea evitar en el diseño del lenguaje siempre que sea posible; de ​​lo contrario, crea un lenguaje enorme lleno de sintaxis inconsistente. Debe explorar una sintaxis completamente nueva solo cuando no haya mejores opciones. Y sostengo que hay una mejor opción.

Aparte: en una nota al margen, creo que es posible alejarse por completo de los "tipos existenciales", aclarando toda la situación, con lo que yo o alguien más seguiremos pronto.

Me encuentro pensando que una sintaxis que no sea type también ayudaría, precisamente porque muchas personas interpretan type como un simple alias sustituible, lo que implicaría la interpretación de "tipo potencialmente diferente cada vez".

No he visto a nadie más señalar que el alcance es completamente diferente para APIT y RPIT.

Pensé que el alcance siempre fue una parte explícita de las propuestas de Rasgo impl, por lo que no necesitaba "señalarlo". Todo lo que ha dicho sobre el alcance parece reiterar lo que ya hemos aceptado en RFC anteriores. Entiendo que no es obvio para todos por la sintaxis y eso es un problema, pero no es que nadie entendiera esto antes. De hecho, pensé que una gran parte de la discusión sobre RFC 2701 se trataba de cuál debería ser el alcance de type Foo = impl Trait; , en el sentido de qué tipo de inferencia es y qué no se le permite mirar.

Es posible llegar a una sintaxis consistente que no tenga esta confusión.

¿Está tratando de decir que type Foo: Bar = _; es esa sintaxis, o cree que aún no la hemos encontrado?

No creo que sea posible llegar a una sintaxis que carezca de una confusión similar, no porque no seamos lo suficientemente creativos, sino porque la mayoría de los programadores no son teóricos de tipos. Probablemente podamos encontrar una sintaxis que reduzca la confusión a un nivel tolerable, y ciertamente hay muchas sintaxis que serían inequívocas para los veteranos de la teoría de tipos, pero nunca eliminaremos la confusión por completo.

has inventado una notación completamente nueva

Pensé que simplemente reemplacé una palabra clave con otra palabra clave. ¿Estás viendo algún cambio adicional que no pretendía?

Ahora que lo pienso, dado que hemos estado usando mal "existencial" todo este tiempo, eso significa que existential Foo: Trait / = impl Trait probablemente ya no sean sintaxis legítimas.

Así que necesitamos una nueva palabra clave para poner delante de los nombres que se refieren a algún tipo de código externo desconocido... y estoy dejando un espacio en blanco en esto. alias , secret , internal , etc., todos parecen bastante terribles y es poco probable que tengan menos "confusión de singularidad" que type .

Ahora que lo pienso, dado que hemos estado usando mal "existencial" todo este tiempo, eso significa que existential Foo: Trait / = impl Trait probablemente ya no sean sintaxis legítimas.

Sí, estoy completamente de acuerdo. Creo que debemos alejarnos del término "existencial" por completo* (ha habido algunas ideas tentativas sobre cómo hacer esto sin dejar de explicar impl Trait bien).

*(posiblemente reservando el término solo para dyn Trait )

@joshtriplett , @Ixrec : Estoy de acuerdo en que la notación _ significa que ya no se puede sustituir en la misma medida en que se podía antes, y si esa es una prioridad a mantener, necesitaríamos una sintaxis diferente.

Tenga en cuenta que _ ya es un caso especial con respecto a la sustitución de todos modos; no solo afecta a los alias de tipo: en cualquier lugar donde pueda usar actualmente _ , está impidiendo la transparencia referencial total.

Tenga en cuenta que _ ya es un caso especial con respecto a la sustitución de todos modos: no solo afecta a los alias de tipo: en cualquier lugar donde pueda usar _ actualmente, está impidiendo la transparencia referencial total.

¿Podría explicarnos qué significa esto exactamente? No conocía la noción de "transparencia referencial" que se ve afectada por _ .

Estoy de acuerdo en que la notación _ significa que ya no se puede sustituir en la misma medida en que se podía antes, y si esa es una prioridad para mantener, necesitaríamos una sintaxis diferente.

No estoy seguro de que sea una _prioridad_. Para mí, fue solo el único argumento objetivo que encontramos que parecía preferir una sintaxis sobre la otra. Pero es probable que todo eso cambie según las palabras clave que podamos encontrar para reemplazar type .

¿Podría explicarnos qué significa esto exactamente? No conocía la noción de "transparencia referencial" que se ve afectada por _ .

Sí, lo siento, estoy lanzando palabras sin explicarlas. Déjame ordenar mis pensamientos y formularé una explicación más cohesiva. Encaja bien con una forma alternativa (y potencialmente más útil) de ver impl Trait .

Por transparencia referencial se entiende que es posible sustituir una referencia por su definición y viceversa sin un cambio en la semántica. En Rust, esto claramente no se cumple a nivel de término para fn . Por ejemplo:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

si sustituimos cada ocurrencia de bar por foo() (la definición de bar ), entonces claramente obtenemos una salida diferente.

Sin embargo, para los alias de tipo, la transparencia referencial se mantiene (AFAIK) en este momento. Si tienes un alias:

type Foo = Definition;

Luego puede hacer (evitar la captura) la sustitución de ocurrencias de Foo por Definition y la sustitución de ocurrencias de Definition por Foo sin cambiar la semántica de su programa , o su tipo de corrección.

Introduciendo:

type Foo = impl Bar;

significar que cada aparición de Foo es del mismo tipo significa que si escribe:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

no puede sustituir las ocurrencias de Foo por impl Bar y viceversa. Es decir, si escribes:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

los tipos de devolución no se unificarán con Foo . Por lo tanto, la transparencia referencial se rompe para los alias de tipo al introducir impl Trait con la semántica de RFC 2071 dentro de ellos.

Sobre la transparencia referencial y type Foo = _; , continuará... (por @varkor)

Me encuentro pensando que una sintaxis que no sea type ayudaría también, precisamente porque muchas personas interpretan type como un simple alias sustituible, lo que implicaría la interpretación de "tipo potencialmente diferente cada vez".

Buen punto. ¿Pero el bit de asignación = _ implica que es de un solo tipo?

He escrito esto antes, pero...

Transparencia referencial: creo que es más útil ver type como un enlace (como let ) en lugar de una sustitución similar al preprocesador C. Una vez que lo miras de esa manera, type Foo = impl Trait significa exactamente lo que parece.

Imagino que es menos probable que los principiantes piensen en impl Trait como tipos existenciales frente a tipos universales, sino como "algo que impl es un rasgo . If they want to know more, they can read the implementa la documentación del rasgo". cambia la sintaxis, pierde la conexión entre esta y la función existente sin mucho beneficio. _Solo está reemplazando una sintaxis potencialmente engañosa con otra._

Re type Foo = _ , sobrecarga _ con un significado completamente diferente. También puede parecer complicado encontrarlo en la documentación y/o en Google.

@lnicola También podría const lugar de enlaces let , donde el primero es referencialmente transparente. Elegir let (que no es referencialmente transparente dentro de fn ) es una elección arbitraria que no creo que sea particularmente intuitiva. Creo que la visión intuitiva de los alias de tipo es que son referencialmente transparentes (incluso si esa palabra no se usa) porque son alias .

Tampoco estoy mirando type como sustitución del preprocesador C porque tiene que ser capturado evitando y respetando los genéricos (sin SFINAE). En cambio, estoy pensando en type precisamente como lo haría con un enlace en un idioma como Idris o Agda donde todos los enlaces son puros.

Me imagino que es menos probable que los principiantes piensen en impl Trait como tipos existenciales frente a tipos universales, sino como "algo que implica un rasgo".

Eso me parece una distinción sin diferencia para mí. La jerga "existencial" no se usa, pero creo que el usuario lo vincula intuitivamente con el mismo concepto que el de un tipo existencial (que no es más que "algún tipo Foo que impls Bar" en el contexto de Rust).

Re type Foo = _ , sobrecarga _ con un significado completamente diferente.

¿Cómo es eso? type Foo = _; aquí se alinea con el uso de _ en otros contextos donde se espera un tipo.
Significa "inferir el tipo real", tal como cuando escribes .collect::<Vec<_>>() .

También puede parecer complicado encontrarlo en la documentación y/o en Google.

¿No debería ser tan difícil? Es de esperar que "escriba el guión bajo del alias" muestre el resultado deseado ...?
No parece diferente a buscar "type alias impl trait".

Google no indexa caracteres especiales. Si mi pregunta de StackOverflow tiene un guión bajo, Google no lo indexará automáticamente para consultas que contengan la palabra guión bajo.

@centril

¿Cómo es eso? type Foo = _; aquí se alinea con el uso de _ en otros contextos donde se espera un tipo.
Significa "inferir el tipo real", tal como cuando escribes .collect::>().

Pero esta característica no infiere el tipo y le da un alias de tipo para él, crea un tipo existencial que (fuera de un alcance limitado como módulo o caja) no se unifica con "el tipo real".

Google no indexa caracteres especiales.

Esto ya no es cierto (¿aunque posiblemente depende de los espacios en blanco...?).

Pero esta característica no infiere el tipo y le da un alias de tipo para él, crea un tipo existencial que (fuera de un alcance limitado como módulo o caja) no se unifica con "el tipo real".

La semántica sugerida de type Foo = _; es una alternativa a tener un alias de tipo existencial, basada completamente en la inferencia. Si eso no quedó del todo claro, voy a seguir pronto con algo que debería explicar un poco mejor las intenciones.

@iopq Además de la nota de @varkor sobre los cambios recientes, también me gustaría agregar que para otros motores de búsqueda, siempre es posible que la documentación oficial y tal usen explícitamente la palabra literal "guion bajo" junto con type modo que se pueda buscar.

Todavía no obtendrá buenos resultados con _ en su consulta, por el motivo que sea. Si busca guiones bajos, obtiene cosas con la palabra guión bajo en ellos. Si buscas _ obtienes todo lo que tiene un guión bajo, así que ni siquiera sé si es relevante

@centril

Elegir let (que no es referencialmente transparente dentro de fn) es una elección arbitraria que no creo que sea particularmente intuitiva. Creo que la visión intuitiva de los alias de tipo es que son referencialmente transparentes (incluso si no se usa esa palabra) porque son alias.

Lo siento, todavía no puedo entender esto porque mi intuición está completamente al revés.

Por ejemplo, si tenemos type Foo = Bar , mi intuición dice:
"Estamos declarando Foo , que se convierte en el mismo tipo que Bar ".

Entonces, si escribimos type Foo = impl Bar , mi intuición dice:
"Estamos declarando Foo , que se convierte en un tipo que implementa Bar ".

Si Foo es solo un alias textual para impl Bar , entonces sería muy poco intuitivo para mí. Me gusta pensar en esto como alias textuales versus semánticos .

Entonces, si Foo se puede reemplazar con impl Bar cualquier lugar donde aparezca, ese es un alias textual , que para mí recuerda más a las macros y la metaprogramación. Pero si a Foo se le asignó un significado en el momento de la declaración y se puede usar en varios lugares con ese significado original (¡no el significado contextual!), eso es un alias semántico .

Además, de todos modos, no entiendo la motivación detrás de los tipos existenciales contextuales. ¿Alguna vez serían útiles, considerando que los alias de rasgos pueden lograr exactamente lo mismo?

Quizás encuentro la transparencia referencial poco intuitiva debido a mi experiencia ajena a Haskell, quién sabe... :) Pero en cualquier caso, definitivamente no es el tipo de comportamiento que esperaría en Rust.

@Nemo157 @stjepang

Si Foo fuera realmente un alias de tipo para un tipo existencial

(énfasis mío). Leí que 'an' como 'específico', lo que significa que f y g no admitirían diferentes tipos de retorno concretos, ya que se refieren al mismo tipo existencial.

Este es un mal uso del término "tipo existencial", o al menos una forma que está en desacuerdo con la publicación de Puede parecer que type Foo = impl Bar hace que Foo un alias para el tipo ∃ T. T: Trait , y si sustituye ∃ T. T: Trait todas partes donde use Foo , incluso si no -textualmente , puede obtener un tipo concreto diferente en cada posición.

El alcance de este cuantificador ∃ T (expresado en su ejemplo como existential type _0 ) es lo que está en cuestión. Es así de estricto en APIT: la persona que llama puede pasar cualquier valor que satisfaga ∃ T. T: Trait . Pero no es en RPIT, y no en el RFC 2071 de existential type declaraciones, y no en su desugaring ejemplo- allí, el cuantificador está más lejos, en el conjunto de funciones o el nivel de todo el módulo, y hacer frente a la mismo T todas partes.

Por lo tanto, la ambigüedad: ya tenemos impl Trait colocando su cuantificador en diferentes lugares según su posición, entonces, ¿cuál deberíamos esperar para type T = impl Trait ? Algunas encuestas informales, así como algunas realizaciones posteriores de los participantes en el subproceso RFC 2071, demuestran que no está claro de una forma u otra.

Esta es la razón por la que queremos alejarnos de la interpretación de impl Trait como cualquier cosa que type T = _ no tiene el mismo tipo de ambigüedad: todavía existe el nivel de superficie "no se puede copiar y pegar _ en lugar de T ", pero ya no hay "el tipo único del que T es un alias puede significar varios tipos concretos". (El comportamiento opaco/no unificado es lo que @varkor está hablando de seguimiento).

transparencia referencial

El hecho de que un alias de tipo sea actualmente compatible con la transparencia referencial no significa que las personas esperen que la característica lo siga.

Como ejemplo, el elemento const es referencialmente transparente (mencionado en https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), y eso en realidad causó confusión entre los nuevos y los antiguos. usuarios (rust-lang-nursery/rust-clippy#1560).

Así que creo que para un programador de Rust, la transparencia referencial no es lo primero que pensaría.

@stjepang @kennytm No estoy diciendo que todos esperen que los alias de tipo con type Foo = impl Trait; actúen de manera referencialmente transparente. Pero creo que una cantidad no trivial de usuarios lo hará, como lo demuestran las confusiones en este hilo y en otros lugares (a qué se refiere @rpjohnst ...). Este es un problema, pero quizás no uno insuperable. Es algo a tener en cuenta a medida que avanzamos.

Mi pensamiento actual sobre lo que se debe hacer en este asunto se ha movido en línea con @varkor y @rpjohnst.

re: transparencia referencial

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

es decir, incluso la generación de nuevos tipos en cada instancia no es referencialmente transparente en el contexto de los alias de tipos genéricos.

@centril Levanto la mano cuando se trata de esperar transparencia referencial para Foo en type Foo = impl Bar; . Sin embargo, con type Foo: Bar = _; , no esperaría transparencia referencial.

También es posible que podamos extender la posición de retorno impl Trait para admitir múltiples tipos, sin ningún tipo de mecanismo similar a enum impl Trait , al monomorfizar (partes de) la persona que llama . Esto fortalece la interpretación " impl Trait es siempre existencial", la acerca más a dyn Trait y sugiere una sintaxis de abstract type que no usa impl Trait en absoluto.

Escribí esto en las partes internas aquí: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Solo una nota para cuando estabilicemos los nuevos tipos existenciales: "existencial" siempre tuvo la intención de ser una palabra clave temporal (según el RFC) y (OMI) es terrible. Debemos pensar en algo mejor antes de estabilizarnos.

La charla sobre tipos “existenciales” no parece estar aclarando las cosas. Diría que impl Trait representa un rasgo de implementación de tipo específico e inferido. Descrito de esa manera, type Foo = impl Bar es claramente un tipo específico, siempre el mismo, y esa es también la única interpretación que es realmente útil: por lo que puede usarse en otros contextos además de aquel del que se infirió, como en estructuras.

En este sentido, tendría sentido escribir también impl Trait como _ : Trait .

@rpjohnst ,

También es posible que podamos extender la posición de retorno impl Trait para admitir múltiples tipos

Eso lo haría estrictamente menos útil en mi opinión. El punto de los alias para los tipos impl es que una función se puede definir para devolver impl Foo , pero el tipo específico aún se propaga a través del programa en otras estructuras y cosas. Eso funcionaría si el compilador generara implícitamente enum adecuados, pero no con monomorfización.

@jan-hudec Esas ideas surgieron en la discusión en Discord, y hay algunos problemas, principalmente basados ​​en el hecho de que la interpretación actual de la posición de retorno y la posición de argumento impl Trait son inconsistentes.

Hacer que impl Trait represente un tipo inferido específico es una buena opción, pero para corregir esa inconsistencia, debe ser un tipo de inferencia de tipo impl Trait . Esta es probablemente la forma más sencilla de hacerlo, pero no es tan simple como dices.

Por ejemplo, una vez que impl Trait significa "usar este nuevo tipo de inferencia para encontrar un tipo polimórfico posible que implemente Trait ", type Foo = impl Bar comienza a insinuar cosas sobre los módulos. Las reglas de RFC 2071 sobre cómo inferir un abstract type dicen que todos los usos deben inferir de forma independiente el mismo tipo, pero esta inferencia polimórfica al menos implicaría que es posible hacer más. Y si alguna vez obtuviéramos módulos parametrizados (incluso solo durante la vida útil, una idea mucho más plausible), habría preguntas sobre esa interacción.

También está el hecho de que algunas personas siempre interpretarán la sintaxis type Foo = impl Bar como un alias para un existencial, sin importar si entienden la palabra "existencial" y sin importar cómo la enseñemos. Por lo tanto, elegir una sintaxis alternativa, incluso si funciona con la interpretación basada en inferencias, probablemente siga siendo una buena idea.

Además, aunque la sintaxis _: Trait es en realidad lo que inspiró la discusión sobre la interpretación basada en inferencias en primer lugar, no hace lo que queremos. Primero, la inferencia implícita en _ no es polimórfica, por lo que es una mala analogía con el resto del lenguaje. En segundo lugar, _ implica que el tipo real está visible en otro lugar, mientras que impl Trait está específicamente diseñado para ocultar el tipo real.

Finalmente, la razón por la que escribí esa propuesta de monomorfización fue desde el punto de vista de encontrar otra forma de unificar el significado del argumento y la posición de retorno impl Trait . Y si bien sí, significa que -> impl Trait ya no garantiza un solo tipo concreto, de todos modos no tenemos una manera de aprovechar eso. Y las soluciones propuestas son todas soluciones molestas: repetitivo adicional abstract type trucos, typeof , etc. Obligar a todos los que quieran confiar en el comportamiento de un solo tipo a nombrar también ese tipo único a través de abstract type sintaxis

Esas ideas han surgido en la discusión en Discord, y hay algunos problemas, principalmente basados ​​en el hecho de que la interpretación actual de la posición de retorno y la posición de argumento impl Trait son inconsistentes.

Personalmente, no encuentro que esta inconsistencia sea un problema en la práctica. El alcance en el que se determinan los tipos concretos para la posición del argumento frente a la posición de retorno frente a la posición del tipo parece funcionar de manera bastante intuitiva.

Tengo una función donde la persona que llama decide su tipo de retorno. Por supuesto, no puedo usar Rasgo impl allí. No es tan intuitivo como insinúas hasta que entiendes la diferencia.

Personalmente, no encuentro que esta inconsistencia sea un problema en la práctica.

Por supuesto. Lo que esto me sugiere no es que debemos ignorar la inconsistencia, sino que debemos volver a explicar el diseño para que sea consistente (por ejemplo, explicándolo como inferencia de tipo polimórfico). De esta manera, las extensiones futuras (RFC 2071, etc.) se pueden comparar con la interpretación nueva y coherente para evitar que las cosas se vuelvan confusas.

@rpjohnst

Obligar a todos los que quieran confiar en el comportamiento de un solo tipo a nombrar también ese tipo único a través de la sintaxis de tipo abstracto (sea lo que sea) podría decirse que es un beneficio general.

En algunos casos, estoy de acuerdo con ese sentimiento, pero no funciona con cierres o generadores, y no es ergonómico para muchos casos en los que no te importa cuál es el tipo y lo único que te importa es que implemente un cierto rasgo. , por ejemplo, con combinadores de iterador.

@mikeyhew Me malinterpretas: funciona bien para cierres u otros tipos sin nombre, porque estoy hablando de inventar un nombre a través de la sintaxis RFC 2071 abstract type . Hay que inventar un nombre sin tener en cuenta si se va a utilizar el tipo único en cualquier otro lugar.

@rpjohnst oh ya veo, gracias por aclarar

Esperando let x: impl Trait ansiosamente.

Como otro voto por let x: impl Trait , simplificará algunos de los ejemplos de futures , aquí hay un ejemplo de ejemplo , actualmente está usando una función solo para tener la capacidad de usar impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

en cambio, esto podría escribirse como un enlace let normal:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Puedo asesorar a alguien mediante la implementación de let x: impl Trait si lo desea. No es increíblemente difícil de hacer, pero definitivamente tampoco es fácil. Un punto de entrada:

De manera similar a cómo visitamos el rasgo impl de tipo de devolución en https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159, debemos visitar el tipo de locales en https ://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 y asegúrese de que sus elementos existenciales recién generados se devuelvan junto con el local.

Luego, cuando visite el tipo de locales, asegúrese de establecer ExistentialContext en Return para habilitarlo.

Esto ya debería llevarnos muy lejos. No estoy seguro si hasta el final, no es 100% como el rasgo impl de posición de retorno, pero en su mayoría debería comportarse como tal.

@rpjohnst ,

Esas ideas han surgido en la discusión en Discord, y hay algunos problemas, principalmente basados ​​en el hecho de que la interpretación actual de la posición de retorno y el Rasgo impl de posición de argumento son inconsistentes.

Nos lleva de vuelta a los ámbitos de los que habló en su artículo. Y creo que en realidad corresponden al "paréntesis" adjunto: para la posición del argumento es la lista de argumentos, para la posición de retorno es la función, y para el alias sería el ámbito en el que se define el alias.

Abrí un RFC que propone una resolución a la sintaxis concreta de existential type , según la discusión en este hilo, el RFC original y las discusiones sincrónicas: https://github.com/rust-lang/rfcs/pull /2515.

La implementación de tipo existencial actual no se puede usar para representar todas las definiciones de posición de retorno actual impl Trait , ya que impl Trait captura cada argumento de tipo genérico, incluso si no se usa, debería ser posible hacer lo mismo con existential type , pero recibe advertencias de parámetros de tipo no utilizados: (patio de recreo)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Esto puede ser importante porque los parámetros de tipo pueden tener tiempos de vida internos que restringen el tiempo de vida de los impl Trait devueltos aunque el valor en sí no se use, elimine los <T> de Bar en el área de juegos arriba para ver que la llamada a foo falla pero bar funciona.

La implementación de tipo existencial actual no se puede usar para representar todas las definiciones de rasgo impl de posición de retorno actual

puedes, es muy inconveniente. Puede devolver un nuevo tipo con un campo PhantomData + campo de datos reales e implementar el rasgo como reenvío al campo de datos reales

@oli-obk Gracias por el consejo adicional. Con su consejo anterior y algunos de @cramertj , probablemente podría

@fasihrana @Nemo157 Ver arriba. ¡Tal vez en unas pocas semanas! :-)

¿Alguien puede aclarar que el comportamiento de existential type no captura los parámetros de tipo implícitamente (que @ Nemo157 mencionó) es intencional y permanecerá como está? Me gusta porque resuelve #42940

Lo implementé de esta manera muy a propósito.

@Arnavion Sí, esto es intencional y coincide con la forma en que otras declaraciones de elementos (por ejemplo, funciones anidadas) funcionan en Rust.

¿Ya se discutió la interacción entre existential_type y never_type ?

Tal vez ! debería poder completar cualquier tipo existencial independientemente de los rasgos involucrados.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

¿O habrá algún tipo intocable especial que sirva como nivel de tipo unimplemented!() que pueda satisfacer automáticamente cualquier tipo existencial?

@vi Creo que eso caería bajo el general "nunca el tipo debe implementar todos los rasgos sin ningún método no propio no predeterminado o tipos asociados". Sin embargo, no sé dónde se rastrearía eso.

¿Hay algún plan para extender pronto el soporte a los tipos de devolución de métodos de rasgos?

existential type ya funciona para los métodos de rasgos. Wrt impl Trait , ¿eso está cubierto por un RFC?

@alexreg Creo que eso requiere que los GAT puedan desazucarar a un tipo asociado anónimo cuando tienes algo como fn foo<T>(..) -> impl Bar<T> (se convierte en aproximadamente -> Self::AnonBar0<T> ).

@Centril, ¿quisiste hacer <T> en impl Bar allí? El comportamiento de captura de tipo implícito de impl Trait significa que obtiene la misma necesidad de GAT incluso con algo como fn foo<T>(self, t: T) -> impl Bar; .

@ Nemo157 no, lo siento, no lo hice. Pero su ejemplo ilustra el problema aún mejor. Gracias :)

@alexreg Creo que eso requiere que los GAT puedan desazucarar a un tipo asociado anónimo cuando tienes algo como fn foo(..) -> Barra impl.(se convierte aproximadamente en -> Self::AnonBar0).

Ah, ya veo. Para ser honesto, no parece estrictamente necesario, pero ciertamente es una forma de implementarlo. Sin embargo, la falta de movimiento en los GAT me preocupa un poco... no he escuchado nada en mucho tiempo.

Clasificación: https://github.com/rust-lang/rust/pull/53542 se ha fusionado, por lo que creo que se pueden marcar las casillas marcadas para {let,const,static} foo: impl Trait .

¿Seré capaz de escribir alguna vez:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Probablemente no. Pero hay planes en curso para preparar todo para que podamos obtener

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Puede experimentar con esta función todas las noches en forma de

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Un buen comienzo para obtener más información sobre esto es https://github.com/rust-lang/rfcs/pull/2071 (y todo lo relacionado desde allí)

@oli-obk en rustc 1.32.0-nightly (00e03ee57 2018-11-22) , también necesito dar los límites de características para que existential type funcione en un bloque de impl como ese. ¿Es eso lo esperado?

@jonhoo poder especificar los rasgos es útil porque puede proporcionar más que solo los rasgos requeridos

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

Los rasgos requeridos podrían agregarse implícitamente a un tipo asociado existencial, por lo que solo necesita límites allí al extenderlos, pero personalmente prefiero la documentación local de tener que ponerlos en la implementación.

@ Nemo157 Ah, lo siento, lo que quise decir es que actualmente _debes_ tener límites allí. Es decir, esto no compilará:

impl A for B {
    existential type Assoc;
    // ...
}

mientras que esto:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Ah, incluso en el caso de que un rasgo no requiera límites del tipo asociado, aún debe otorgar un límite al tipo existencial (que puede estar vacío) ( patio de recreo ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Esto me parece un caso límite, tener un tipo existencial sin límites significa que proporciona _no_ operaciones a los usuarios (aparte de los rasgos automáticos), entonces, ¿para qué podría usarse?

También cabe destacar que no hay forma de hacer lo mismo con -> impl Trait , -> impl () es un error de sintaxis y -> impl por sí solo da error: at least one trait must be specified ; si la sintaxis del tipo existencial se convierte en type Assoc = impl Debug; o similar, parece que no habría forma de especificar el tipo asociado sin al menos un límite de rasgo.

@ Nemo157 sí, solo me di cuenta porque probé literalmente el código que sugeriste anteriormente, y no funcionó: p Asumí que inferiría los límites del rasgo. Por ejemplo:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Parecía razonable no tener que especificar Future<Output = u32> una segunda vez, pero eso no funciona. Supongo que existential type Assoc: ; (que también parece una sintaxis súper rara) tampoco hará esa inferencia.

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Esto me parece un caso límite, tener un tipo existencial sin límites significa que proporciona _no_ operaciones a los usuarios (aparte de los rasgos automáticos), entonces, ¿para qué podría usarse?

¿No podrían usarse para el consumo en la misma implementación de rasgos? Algo como esto:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

Es un poco artificial, pero podría ser útil: podría imaginar una situación en la que se deba construir una parte preliminar antes de la estructura real por razones de por vida. O podría ser algo como:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

En ambos casos, existential type Assoc; sería útil.

¿Cuál es la forma correcta de definir los tipos asociados para el rasgo impl?

Por ejemplo, si tengo un rasgo Action y quiero asegurarme de que la implementación del tipo asociado al rasgo se pueda enviar, ¿puedo hacer algo como esto?

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

¿Es algo que actualmente no es posible?

@acicliczebra Creo que la sintaxis para eso es -> impl Action<Result = impl Send> - esta es la misma sintaxis que, por ejemplo, -> impl Iterator<Item = u32> simplemente usando otro tipo anónimo impl Trait .

¿Ha habido alguna discusión sobre la extensión de la sintaxis de impl Trait a elementos como los campos de estructura? Por ejemplo, si estoy implementando un contenedor alrededor de un tipo de iterador específico para mi interfaz pública:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

Sería útil en aquellas situaciones en las que realmente no me importa el tipo real, siempre que satisfaga ciertos límites de rasgos. Este ejemplo es simple, pero me he encontrado con situaciones en el pasado en las que escribo tipos muy largos con un montón de parámetros de tipos anidados, y es realmente innecesario porque realmente no me importa nada excepto que este es un ExactSizeIterator .

Sin embargo, IIRC, no creo que haya una manera de especificar límites múltiples con impl Trait en este momento, por lo que perdería algunas cosas útiles como Clone .

@AGausmann La última discusión sobre el tema está en https://github.com/rust-lang/rfcs/pull/2515. Esto le permitiría decir type Foo = impl Bar; struct Baz { field: Foo } ... . Creo que podemos querer considerar field: impl Trait como azúcar para eso después de estabilizar type Foo = impl Bar; . Se siente como una extensión de conveniencia razonable y amigable con las macros.

@Centril ,

Creo que podríamos querer considerar field: impl Trait como azúcar

No creo que esto sea razonable. Un campo de estructura aún debe tener un tipo concreto, por lo que debe decirle al compilador el retorno de la función a la que está vinculado. Podría inferirlo, pero si tiene varias funciones, no sería tan fácil encontrar cuál es, y la política habitual de Rust es ser explícito en tales casos.

Podría inferirlo, pero si tiene varias funciones, entonces no sería tan fácil encontrar cuál es.

Aumentaría el requisito de definir usos para el tipo principal. Serían entonces todas esas funciones en el mismo módulo las que devuelven el tipo padre. No me parece tan difícil de encontrar. Sin embargo, creo que queremos resolver la historia en type Foo = impl Bar; antes de seguir adelante con las extensiones.

Creo que encontré un error en la implementación actual de existential type .


Código

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Error

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

patio de juegos

También puede encontrar esto en stackoverflow .

No estoy 100 % seguro de que podamos respaldar este caso de inmediato, pero lo que puede hacer es reescribir la función para que tenga dos parámetros genéricos:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

¡Gracias!
Sí, esto es lo que hice como se publicó en la publicación de stackoverflow:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

¿Hay planes para que impl Trait esté disponible dentro de un contexto de rasgo?
No solo como tipo asociado, sino también como valor de retorno en los métodos.

el rasgo impl en rasgos es una característica separada de las que se rastrean aquí, y actualmente no tiene un RFC. Hay una historia bastante larga de diseños en este espacio, y se está retrasando la iteración adicional hasta que se estabilice la implementación de 2071 (tipo existencial), que está bloqueada por problemas de implementación y sintaxis no resuelta (que tiene un RFC separado).

@cramertj La sintaxis está casi resuelta. Creo que el principal bloqueador es GAT ahora.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 todavía está esperando en @withoutboats.

@varkor Sí, solo estoy siendo optimista de que pronto verán la luz con ese RFC. ;-)

¿Será posible algo como lo siguiente?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Puede hacer esto ahora, aunque solo con una función hint para especificar el tipo concreto de Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

¿Cómo lo escribiría si la devolución de llamada pudiera elegir su tipo de argumento? En realidad nvm, supongo que podrías resolverlo a través de un genérico normal.

@CryZe Lo que estás buscando no está relacionado con impl Trait . Ver https://github.com/rust-lang/rfcs/issues/2413 para todo lo que sé al respecto.

Potencialmente se vería algo así:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, interesante. ¡Gracias!

¿Se supone que esto funciona?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

¿Tenemos que mantener la palabra clave existential en el idioma para la función existential_type ?

@jethrogb Sí. El hecho de que actualmente no lo haga es un error.

@cramertj Vale. ¿Debo presentar un problema por separado para eso o mi publicación aquí es suficiente?

Presentar un problema sería genial, ¡gracias! :)

¿Tenemos que mantener la palabra clave existential en el idioma para la función existential_type ?

Creo que la intención es descartar esto de inmediato cuando se implemente la función type-alias-impl-trait (es decir, poner una pelusa) y, finalmente, eliminarlo de la sintaxis.

Sin embargo, alguien puede aclararlo.

Cerrando esto a favor de un meta-problema que rastrea impl Trait más general: https://github.com/rust-lang/rust/issues/63066

ni un solo buen ejemplo en ninguna parte sobre cómo usar el rasgo impl, muy triste

¿Fue útil esta página
0 / 5 - 0 calificaciones