Rust: Problema de seguimiento para especialización (RFC 1210)

Creado en 23 feb. 2016  Â·  236Comentarios  Â·  Fuente: rust-lang/rust

Este es un problema de seguimiento para la especialización (rust-lang / rfcs # 1210).

Pasos principales de implementación:

  • [x] Tierra https://github.com/rust-lang/rust/pull/30652 =)
  • [] Restricciones sobre el envío de por vida (actualmente un agujero de solidez )
  • [] default impl (https://github.com/rust-lang/rust/issues/37653)
  • [] Integración con consts asociados
  • [] Los límites no siempre se aplican correctamente (https://github.com/rust-lang/rust/issues/33017)
  • [] ¿Deberíamos permitir las impls vacías si el padre no tiene miembros default ? https://github.com/rust-lang/rust/issues/48444
  • [] implementar "siempre aplicable" implica https://github.com/rust-lang/rust/issues/48538
  • [] describen y prueban las condiciones de ciclo precisas en torno a la creación del gráfico de especialización (consulte, por ejemplo, este comentario , que indica que hoy tenemos una lógica muy cuidadosa)

Preguntas sin resolver del RFC:

  • ¿Debería el tipo asociado ser especializable en absoluto?
  • ¿Cuándo debe revelar la proyección un default type ? ¿Nunca durante typeck? ¿O cuando es monomórfico?
  • ¿Deben considerarse los elementos de rasgos predeterminados default (es decir, especializables)?
  • ¿Deberíamos tener default impl (donde todos los elementos son default ) o partial impl (donde default es opcional); consulte https://github.com/rust-lang/rust/issues/37653#issuecomment -616116577 para ver algunos ejemplos relevantes de dónde default impl es limitante.
  • ¿Cómo debemos lidiar con la capacidad de envío de por vida?

Tenga en cuenta que la función specialization implementada actualmente no es unsafe . min_specialization evita la mayoría de las trampas .

A-specialization A-traits B-RFC-approved B-RFC-implemented B-unstable C-tracking-issue F-specialization T-lang

Comentario más útil

He estado usando #[min_specialization] en una biblioteca experimental que he estado desarrollando, así que pensé en compartir mis experiencias. El objetivo es utilizar la especialización en su forma más simple: tener algunos casos estrechos con implementaciones más rápidas que el caso general. En particular, tener algoritmos criptográficos en el caso general que se ejecuten en tiempo constante pero luego si todas las entradas están marcadas Public tener una versión especializada que se ejecute en tiempo variable más rápido (porque si son públicas no preocuparse por filtrar información sobre ellos a través del tiempo de ejecución). Además, algunos algoritmos son más rápidos dependiendo de si el punto de la curva elíptica está normalizado o no. Para que esto funcione, comenzamos con

#![feature(rustc_attrs, min_specialization)]

Luego, si necesita hacer un rasgo de _prédico de especialización_ como se explica en la especialización mínima máxima , marque la declaración del rasgo con #[rustc_specialization_trait] .

Toda mi especialización se realiza en este archivo y aquí hay un ejemplo de un rasgo de predicado de especialización.

La función funciona y hace exactamente lo que necesito. Obviamente, se utiliza un marcador interno rustc y, por lo tanto, es propenso a romperse sin previo aviso.

El único comentario negativo es que no creo que la palabra clave default tenga sentido. Básicamente, lo que default significa ahora mismo es: "este impl es especializable, así que interprete los impl que cubren un subconjunto de este como especialización del mismo en lugar de un impl en conflicto". El problema es que conduce a un código de aspecto muy extraño:

https://github.com/LLFourn/secp256kfun/blob/6766b60c02c99ca24f816801fe876fed79643c3a/secp256kfun/src/op.rs#L196 -L206

Aquí, el segundo impl está especializado en el primero, pero también es default . El significado de default parece haberse perdido. Si observa el resto de las impls, es bastante difícil averiguar qué impls se especializan en cuáles. Además, cuando hice una implicación errónea que se superponía con una existente, a menudo era difícil averiguar dónde me equivoqué.

Me parece que esto sería más sencillo si todo fuera especializable y cuando te especializas en algo declaras precisamente en qué implica te especializas. Transformando el ejemplo en el RFC en lo que tenía en mente:

impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
{
    // no need for default
    fn extend(&mut self, iterable: T) {
        ...
    }
}

// We declare explicitly which impl we are specializing repeating all type bounds etc
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
    // And then we declare explicitly how we are making this impl narrower with ‘when’.
    // i.e. This impl is like the first except replace all occurances of ‘T’ with ‘&'a [A]’
    when<'a> T = &'a [A]
{
    fn extend(&mut self, iterable: &'a [A]) {
        ...
    }
}

Todos 236 comentarios

Algunas preguntas abiertas adicionales:

  • ¿Deberíamos revisar las reglas huérfanas a la luz de la especialización? ¿Hay formas de hacer las cosas más flexibles ahora?
  • ¿Deberíamos extender la "regla de la cadena" en el RFC a algo más expresivo, como la llamada "regla de celosía"?
  • En relación con los dos anteriores, ¿cómo encaja el razonamiento negativo en la historia? ¿Podemos recuperar el razonamiento negativo que necesitamos mediante un uso suficientemente inteligente de las reglas de especialización / huérfanas, o deberíamos hacerlo más de primera clase?

No estoy seguro de que la especialización cambie las reglas huérfanas:

  • Las reglas huérfanas de "vinculación" deben permanecer iguales, porque de lo contrario no tendría vinculación segura.
  • No creo que las reglas huérfanas de "compatibilidad futura" deban cambiar. Agregar un impl no especializable debajo de usted aún sería un cambio importante.

Peor que eso, las reglas huérfanas de la "compatibilidad futura" mantienen la especialización cruzada bajo un control bastante estricto. Sin ellos, los impls predeterminados que dejan sus métodos abiertos se vuelven mucho peores.

Nunca me gustó el razonamiento negativo explícito. Creo que la especialización del razonamiento negativo total es un buen compromiso.

¿Debería permitirse esta implicación con la especialización implementada? ¿O me estoy perdiendo algo?
http://is.gd/3Ul0pe

Lo mismo con este, habría esperado que se compilara: http://is.gd/RyFIEl

Parece que existen algunas peculiaridades a la hora de determinar la superposición cuando se trata de tipos asociados. Esto compila: http://is.gd/JBPzIX , mientras que este código efectivamente idéntico no lo hace: http://is.gd/0ksLPX

Aquí hay un fragmento de código que esperaba compilar con especialización:

http://is.gd/3BNbfK

#![feature(specialization)]

use std::str::FromStr;

struct Error;

trait Simple<'a> {
    fn do_something(s: &'a str) -> Result<Self, Error>;
}

impl<'a> Simple<'a> for &'a str {
     fn do_something(s: &'a str) -> Result<Self, Error> {
        Ok(s)
    }
}

impl<'a, T: FromStr> Simple<'a> for T {
    fn do_something(s: &'a str) -> Result<Self, Error> {
        T::from_str(s).map_err(|_| Error)
    }
}

fn main() {
    // Do nothing. Just type check.
}

La compilación falla con el compilador citando conflictos de implementación. Tenga en cuenta que &str no implementa FromStr , por lo que no debería haber un conflicto.

@sgrif

Tuve tiempo para mirar los dos primeros ejemplos. Aquí están mis notas.

Ejemplo 1

Primer caso, tienes:

  • FromSqlRow<ST, DB> for T where T: FromSql<ST, DB>
  • FromSqlRow<(ST, SU), DB> for (T, U) where T: FromSqlRow<ST, DB>, U: FromSqlRow<SU, DB>,

El problema es que estas implicaciones se superponen pero ninguna es más específica que la otra:

  • Potencialmente, puede tener un T: FromSql<ST, DB> donde T no es un par (por lo que coincide con la primera impl, pero no con la segunda).
  • Potencialmente, puede tener un (T, U) donde:

    • T: FromSqlRow<ST, DB> ,

    • U: FromSqlRow<SU, DB> , pero _no_

    • (T, U): FromSql<(ST, SU), DB>

    • (por lo que el segundo impl coincide, pero no el primero)

  • Las dos implicaciones se superponen porque puede tener un (T, U) tal que:

    • T: FromSqlRow<ST, DB>

    • U: FromSqlRow<SU, DB>

    • (T, U): FromSql<(ST, SU), DB>

Este es el tipo de situación que permitirían las implicaciones de celosía: tendrías que escribir una tercera implícita para el caso superpuesto y decir qué debería hacer. Alternativamente, las implicaciones de rasgos negativos pueden brindarle una forma de descartar la superposición o modificar qué coincidencias son posibles.

Ejemplo 2

Tienes:

  • Queryable<ST, DB> for T where T: FromSqlRow<ST, DB>
  • Queryable<Nullable<ST>, DB> for Option<T> where T: Queryable<ST, DB>

Estos se superponen porque puede tener Option<T> donde:

  • T: Queryable<ST, DB>
  • Option<T>: FromSqlRow<Nullable<ST>, DB>

Pero ninguna de las implicaciones es más específica:

  • Puede tener un T tal que T: FromSqlRow<ST, DB> pero T no sea un Option<U> (coincide con la primera implicación pero no con la segunda)
  • Puede tener un Option<T> tal que T: Queryable<ST, DB> pero no Option<T>: FromSqlRow<Nullable<ST>, DB>

@SergioBenitez

La compilación falla con el compilador citando conflictos de implementación. Tenga en cuenta que &str no implementa FromStr , por lo que no debería haber un conflicto.

El problema es que el compilador asume de manera conservadora que &str podría llegar a implementar FromStr en el futuro. Eso puede parecer una tontería para este ejemplo, pero en general, agregamos nuevas implicaciones todo el tiempo y queremos proteger el código descendente para que no se rompa cuando agregamos esas implicaciones.

Esta es una opción conservadora y es algo que quizás deseemos relajar con el tiempo. Puede obtener los antecedentes aquí:

Gracias por aclarar esos dos casos. Tiene mucho sentido ahora

El martes 22 de marzo de 2016 a las 6:34 p. M. Aaron Turon [email protected] escribió:

@SergioBenitez https://github.com/SergioBenitez

La compilación falla con el compilador citando conflictos de implementación. Nota
que & str no implementa FromStr, por lo que no debería haber un conflicto.

El problema es que el compilador asume de forma conservadora que & str
podría llegar a implementar FromStr en el futuro. Eso puede parecer una tontería para
este ejemplo, pero en general, agregamos nuevas implicaciones todo el tiempo, y queremos
proteger el código descendente para que no se rompa cuando agreguemos esos impl.

Esta es una opción conservadora, y es algo que quizás deseemos relajarnos.
tiempo extraordinario. Puede obtener los antecedentes aquí:

-
http://smallcultfollowing.com/babysteps/blog/2015/01/14/little-orphan-impls/

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/31844#issuecomment -200093757

@aturon

El problema es que el compilador asume de forma conservadora que & str podría llegar a implementar FromStr en el futuro. Eso puede parecer una tontería para este ejemplo, pero en general, agregamos nuevas implicaciones todo el tiempo y queremos proteger el código descendente para que no se rompa cuando agregamos esas implicaciones.

¿No es esto exactamente lo que la especialización está tratando de abordar? Con especialización, esperaría que incluso si se agregara una implementación de FromStr para &str en el futuro, la implementación directa del rasgo Simple para &str tendría prioridad.

@SergioBenitez necesitas poner default fn en el impl más general. Tu
ejemplo no es especializable.

El martes 22 de marzo de 2016 a las 6:54 p.m. Sergio Benitez [email protected]
escribió:

@aturon https://github.com/aturon

El problema es que el compilador asume de forma conservadora que & str
podría llegar a implementar FromStr en el futuro. Eso puede parecer una tontería para esto
ejemplo, pero en general, agregamos nuevas implicaciones todo el tiempo, y queremos
proteger el código descendente para que no se rompa cuando agreguemos esos impl.

¿No es esto exactamente lo que la especialización está tratando de abordar? Con
especialización, esperaría que incluso si una implementación de FromStr
for & str se agregaron en el futuro, la implementación directa para el
el rasgo para & str tendría prioridad.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/31844#issuecomment -200097995

Creo que los elementos de rasgo "predeterminados" que se consideran automáticamente default suena confuso. Es posible que desee tanto parametricidad para un rasgo como en Haskell, etc. junto con la relajación de impl s. Además, no puede grep fácilmente para ellos como puede hacerlo por default . No es difícil escribir la palabra clave default y dar una implementación predeterminada, pero no se pueden separar tal cual. Además, si uno quiere aclarar el lenguaje, entonces estos elementos de características "predeterminados" podrían cambiarse de nombre a elementos de "características propuestas" en la documentación.

Nota de # 32999 (comentario) : si seguimos la regla de celosía (o permitimos restricciones negativas), el truco de "usar un rasgo intermedio" para evitar una mayor especialización de algo ya no funcionará.

@Stebalien

¿Por qué no funciona? El truco limita la especialización a un rasgo privado. No puede especializar el rasgo privado si no puede acceder a él.

@ arielb1 Ah. Buen punto. En mi caso, el rasgo no es privado.

No creo que el razonamiento "los externos no se puedan especializar porque el razonamiento huérfano de compatibilidad hacia adelante + regla de coherencia" sea particularmente interesante o útil. Especialmente cuando no nos comprometemos con nuestras reglas de coherencia específicas.

¿Hay alguna forma de acceder a un default impl anulado? Si es así, esto podría ayudar a construir pruebas. Consulte Diseño por contrato y libhoare .

Permitir la proyección de tipos asociados predeterminados durante la verificación de tipos permitirá imponer la desigualdad de tipos en tiempo de compilación: https://gist.github.com/7c081574958d22f89d434a97b626b1e4

#![feature(specialization)]

pub trait NotSame {}

pub struct True;
pub struct False;

pub trait Sameness {
    type Same;
}

mod internal {
    pub trait PrivSameness {
        type Same;
    }
}

use internal::PrivSameness;

impl<A, B> Sameness for (A, B) {
    type Same = <Self as PrivSameness>::Same;
}

impl<A, B> PrivSameness for (A, B) {
    default type Same = False;
}
impl<A> PrivSameness for (A, A) {
    type Same = True;
}

impl<A, B> NotSame for (A, B) where (A, B): Sameness<Same=False> {}

fn not_same<A, B>() where (A, B): NotSame {}

fn main() {
    // would compile
    not_same::<i32, f32>();

    // would not compile
    // not_same::<i32, i32>();
}

editado por el comentario de @burdges

Solo fyi @rphmeier , probablemente uno debería evitar is.gd porque no se resuelve para los usuarios de Tor debido al uso de CloudFlare. GitHub funciona bien con URL completas. Y play.rust-lang.org funciona bien en Tor.

@burdges FWIW play.rust-lang.org usa is.gd para su botón "Acortar".

Sin embargo, probablemente se pueda cambiar: https://github.com/rust-lang/rust-playpen/blob/9777ef59b/static/web.js#L333

usar así (https://is.gd/Ux6FNs):

#![feature(specialization)]
pub trait Foo {}
pub trait Bar: Foo {}
pub trait Baz: Foo {}

pub trait Trait {
    type Item;
}

struct Staff<T> { }

impl<T: Foo> Trait for Staff<T> {
    default type Item = i32;
}

impl<T: Foo + Bar> Trait for Staff<T> {
    type Item = i64;
}

impl<T: Foo + Baz> Trait for Staff<T> {
    type Item = f64;
}

fn main() {
    let _ = Staff { };
}

Error:

error: conflicting implementations of trait `Trait` for type `Staff<_>`: [--explain E0119]
  --> <anon>:20:1
20 |> impl<T: Foo + Baz> Trait for Staff<T> {
   |> ^
note: conflicting implementation is here:
  --> <anon>:16:1
16 |> impl<T: Foo + Bar> Trait for Staff<T> {
   |> ^

error: aborting due to previous error

¿Feture specialization admite esto y hay algún otro tipo de implementaciones actualmente?

@zitsen

Estas implicaciones no están permitidas por el diseño de especialización actual, porque ni T: Foo + Bar ni T: Foo + Baz son más especializados que el otro. Es decir, si tiene T: Foo + Bar + Baz , no está claro qué implícita debería "ganar".

Tenemos algunas ideas sobre un sistema más expresivo que le permitiría _también_ dar una impl por T: Foo + Bar + Baz y así eliminar la ambigüedad, pero eso aún no se ha propuesto por completo.

Si el rasgo negativo limita trait Baz: !Bar alguna vez, eso también podría usarse con especialización para demostrar que los conjuntos de tipos que implementan Bar y los que implementan Baz son distintos y se pueden especializar individualmente.

Parece que la respuesta de @rphmeier es exactamente lo que quiero, las implicaciones de T: Foo + Bar + Baz también ayudarían.

Simplemente ignore esto, todavía tengo algo que ver con mi caso, y siempre es emocionante el aterrizaje de specialization y otras características.

Gracias @aturon @rphmeier .

Últimamente he estado jugando con la especialización y me encontré con este caso extraño:

#![feature(specialization)]

trait Marker {
    type Mark;
}

trait Foo { fn foo(&self); }

struct Fizz;

impl Marker for Fizz {
    type Mark = ();
}

impl Foo for Fizz {
    fn foo(&self) { println!("Fizz!"); }
}

impl<T> Foo for T
    where T: Marker, T::Mark: Foo
{
    default fn foo(&self) { println!("Has Foo marker!"); }
}

struct Buzz;

impl Marker for Buzz {
    type Mark = Fizz;
}

fn main() {
    Fizz.foo();
    Buzz.foo();
}

Salida del compilador:

error: conflicting implementations of trait `Foo` for type `Fizz`: [--explain E0119]
  --> <anon>:19:1
19 |> impl<T> Foo for T
   |> ^
note: conflicting implementation is here:
  --> <anon>:15:1
15 |> impl Foo for Fizz {
   |> ^

corralito

Creo que lo anterior _ debería_ compilarse, y hay dos variaciones interesantes que realmente funcionan según lo previsto:

1) Eliminar el límite where T::Mark: Fizz :

impl<T> Foo for T
    where T: Marker //, T::Mark: Fizz
{
    // ...
}

corralito

2) Agregar un "alias vinculado a un rasgo":

trait FooMarker { }
impl<T> FooMarker for T where T: Marker, T::Mark: Foo { }

impl<T> Foo for T where T: FooMarker {
    // ...
}

corralito

(Lo que _no_ funciona si Marker se define en una caja separada (!), Vea este ejemplo de repositorio )

También creo que este problema podría estar relacionado con el # 20400 de alguna manera.

EDITAR : Abrí un problema sobre esto: # 36587

Tengo un problema con la especialización. No estoy seguro si es un problema de implementación o un problema en la forma en que se especifica la especialización.

use std::vec::IntoIter as VecIntoIter;

pub trait ClonableIterator: Iterator {
    type ClonableIter;

    fn clonable(self) -> Self::ClonableIter;
}

impl<T> ClonableIterator for T where T: Iterator {
    default type ClonableIter = VecIntoIter<T::Item>;

    default fn clonable(self) -> VecIntoIter<T::Item> {
        self.collect::<Vec<_>>().into_iter()
    }
}

impl<T> ClonableIterator for T where T: Iterator + Clone {
    type ClonableIter = T;

    #[inline]
    fn clonable(self) -> T {
        self
    }
}

( corralito )
(por cierto, sería bueno si este código eventualmente aterrizara en stdlib algún día)

Este código falla con:

error: method `clonable` has an incompatible type for trait:
 expected associated type,
    found struct `std::vec::IntoIter` [--explain E0053]
  --> <anon>:14:5
   |>
14 |>     default fn clonable(self) -> VecIntoIter<T::Item> {
   |>     ^

Cambiar el valor de retorno a Self::ClonableIter da el siguiente error:

error: mismatched types [--explain E0308]
  --> <anon>:15:9
   |>
15 |>         self.collect::<Vec<_>>().into_iter()
   |>         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected associated type, found struct `std::vec::IntoIter`
note: expected type `<T as ClonableIterator>::ClonableIter`
note:    found type `std::vec::IntoIter<<T as std::iter::Iterator>::Item>`

Aparentemente, no puede referirse al tipo concreto de un tipo asociado predeterminado, que me parece bastante limitante.

@tomaka debería funcionar, el texto RFC tiene esto:

impl<T> Example for T {
    default type Output = Box<T>;
    default fn generate(self) -> Box<T> { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> bool { self }
}

(https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md#the-default-keyword)

Lo que parece lo suficientemente similar a su caso para ser relevante.

@ahora, ese ejemplo no parece compilarse con la definición intuitiva del rasgo de ejemplo: https://play.rust-lang.org/?gist=97ff3c2f7f3e50bd3aef000dbfa2ca4e&version=nightly&backtrace=0

el código de especialización explícitamente no permite esto - vea # 33481, que inicialmente pensé que era un error, pero resultó ser un problema de diagnóstico. Mis PR para mejorar los diagnósticos aquí pasaron desapercibidos, y no los he mantenido al último maestro durante bastante tiempo.

@rphmeier, el texto de RFC sugiere que debería permitirse, sin embargo, ese ejemplo está copiado de él.

Jugué con un código que podría beneficiarse de la especialización. Creo firmemente que deberíamos optar por la regla de la celosía en lugar del encadenamiento: se siente natural y era la única forma de obtener la flexibilidad que necesitaba (afaict).

Si optamos por default en el impl , así como en elementos individuales, ¿podríamos hacer cumplir que si se anula algún elemento, entonces todos deben serlo? Eso nos permitiría razonar en función del tipo preciso de un tipo de asociación predeterminado (por ejemplo) en los otros elementos, lo que parece un impulso útil en la expresividad.

¿Debería permitirse lo siguiente? Quiero especializar un tipo para que ArrayVec sea Copy cuando su tipo de elemento sea Copiar y que, de lo contrario, tenga un destructor. Estoy tratando de lograrlo usando un campo interno que se reemplaza por especialización.

Esperaba que esto se compilara, es decir, que dedujera la capacidad de copia de los campos de ArrayVec<A> de los tipos de campo que son seleccionados por A: Copy + Array bound (fragmento compilable en el patio de recreo) .

impl<A: Copy + Array> Copy for ArrayVec<A>
    //where <A as Repr>::Data: Copy
{ }

La cláusula where comentada no se desea porque expone un tipo privado Repr en la interfaz pública. (También ICE de todos modos).

Editar: Me había olvidado de que ya informé el problema # 33162 sobre esto, lo siento.

Haga un seguimiento de mi comentario, mi caso de uso real:

// Ideal version

trait Scannable {}

impl<T: FromStr> Scannable for T {}
impl<T: FromStr> Scannable for Result<T, ()> {}

// But this doesn't follow from the specialisation rules because Result: !FromStr
// Lattice rule would allow filling in that gap or negative reasoning would allow specifying it.

// Second attempt

trait FromResult {
    type Ok;
    fn from(r: Result<Self::Ok, ()>) -> Self;
}

impl<T> Scannable for T {
    default type Ok = T;
    default fn from(r: Result<T, ()>) -> Self {...} // error can't assume Ok == T, could do this if we had `default impl`
}

impl<T> Scannable for Result<T, ()> {
    type Ok = T;
    default fn from(r: Result<T, ()>) -> Self { r }
}

fn scan_from_str<T: FromResult>(x: &str) -> T
    where <T as FromResult>::Ok: FromStr  // Doesn't hold for T: FromStr because of the default on T::Ok
{ ... }

// Can also add the FromStr bound to FromResult::Ok, but doesn't help

// Third attempt
trait FromResult<Ok> {
    fn from(r: Result<Ok, ()>) -> Self;
}

impl<T> FromResult<T> for T {
    default fn from(r: Result<Self, ()>) -> Self { ... }
}

impl<T> FromResult<T> for Result<T, ()> {
    fn from(r: Result<T, ())>) -> Self { r }
}


fn scan_from_str<U: FromStr, T: FromResult<U>>(x: &str) -> T { ... }

// Error because we can't infer that U == String
let mut x: Result<String, ()> = scan_from_str("dsfsf");

@tomaka @Aatch

El problema es que no puede confiar en el valor de otros elementos predeterminados. Entonces, cuando tienes este impl:

impl<T> ClonableIterator for T where T: Iterator {
    default type ClonableIter = VecIntoIter<T::Item>;

    default fn clonable(self) -> VecIntoIter<T::Item> {
    //                           ^^^^^^^^^^^^^^^^^^^^
        self.collect::<Vec<_>>().into_iter()
    }
}

En el lugar donde resalté, clonable depende de Self::ClonableIter , pero debido a que CloneableIter se declara como predeterminado, no puede hacer eso. La preocupación es que alguien pueda especializarse y anular CloneableIter pero _no_ clonable .

Habíamos hablado de algunas posibles respuestas aquí. Uno de ellos fue permitirle usar default para agrupar elementos donde, si anula uno, debe anular todos:

impl<T> ClonableIterator for T where T: Iterator {
    default {
        type ClonableIter = VecIntoIter<T::Item>;
        fn clonable(self) -> VecIntoIter<T::Item> { ... }
    }
}

Esto está bien, pero un poco "induce a la deriva hacia la derecha". El default también parece un ámbito de denominación, que no lo es. Puede haber una variante más simple que le permita alternar entre "anular cualquiera" (como hoy) y "anular todo" (lo que necesita).

También esperábamos poder arreglárnoslas aprovechando impl Trait . La idea sería que esto surja con mayor frecuencia, como es el caso aquí, cuando desea personalizar el tipo de retorno de los métodos. Entonces, tal vez si pudiera reescribir el rasgo para usar impl Trait :

pub trait ClonableIterator: Iterator {
    fn clonable(self) -> impl Iterator;
}

Esto sería efectivamente una especie de abreviatura cuando se implemente para un grupo predeterminado que contenga el tipo y la fn. (Sin embargo, no estoy seguro de si habría una forma de hacerlo puramente implícito).

PD, perdón por la gran demora en responder a sus mensajes, que veo que datan de _Julio_.

Si bien impl Trait ayuda, no hay ningún RFC que haya sido aceptado o implementado que permita su uso con cuerpos de rasgos en cualquier forma, por lo que buscarlo en este RFC se siente un poco extraño.

Estoy interesado en implementar la función default impl (donde todos los elementos son default ).
¿Aceptaría una contribución sobre eso?

@giannicic ¡Definitivamente! También estaría feliz de ayudar a guiar el trabajo.

¿Existe actualmente una conclusión sobre si los tipos asociados deberían ser especializables?

La siguiente es una simplificación de mi caso de uso, que demuestra la necesidad de tipos asociados especializables.
Tengo una estructura de datos genérica, digamos Foo , que coordina una colección de objetos de rasgo de contenedor ( &trait::Property ). El rasgo trait::Property está implementado por Property<T> (respaldado por Vec<T> ) y PropertyBits (respaldado por BitVec , un vector de bits).
En métodos genéricos en Foo , me gustaría poder determinar la estructura de datos subyacente correcta para T través de tipos asociados, pero esto requiere especialización para tener una implicación general para casos no especiales como sigue.

trait ContainerFor {
    type P: trait::Property;
}

impl<T> ContainerFor for T {
    default type P = Property<T>; // default to the `Vec`-based version
}

impl ContainerFor for bool {
    type P = PropertyBits; // specialize to optimize for space
}

impl Foo {
    fn add<T>(&mut self, name: &str) {
        self.add_trait_obj(name, Box::new(<T as ContainerFor>::P::new())));
    }
    fn get<T>(&mut self, name: &str) -> Option<&<T as ContainerFor>::P> {
        self.get_trait_obj(name).and_then(|prop| prop.downcast::<_>());
    }
}

¡Gracias @aturon !
Básicamente, estoy haciendo el trabajo agregando un nuevo atributo "defaultness" a la estructura ast::ItemKind::Impl (y luego uso el nuevo atributo junto con el atributo implícito "defaultness") pero también hay un método rápido y fácil
posibilidad que consiste en configurar por defecto todos los elementos impl de default impl durante el análisis.
Para mí, esta no es una solución "completa", ya que perdimos la información de que el "valor predeterminado" está relacionado con el impl y no con cada elemento del impl,
Además, si hay un plan para introducir un partial impl la primera solución ya proporcionaría un atributo que se puede usar para almacenar default así como partial . Pero solo para estar seguro y
sin perder el tiempo, ¿en qué piensas?

@giannicic @aturon ¿ puedo proponer que creemos un tema específico para discutir default impl ?

¿Me permitiría la regla de celosía, dado:

trait Foo {}

trait A {}
trait B {}
trait C {}
// ...

agregue implementaciones de Foo para el subconjunto de tipos que implementan alguna combinación de A , B , C , ...:

impl Foo for T where T: A { ... }
impl Foo for T where T: B { ... }
impl Foo for T where T: A + B { ... }
impl Foo for T where T: B + C { ... }
// ...

y permítame "prohibir" algunas combinaciones, por ejemplo, que A + C nunca debería suceder:

impl Foo for T where T: A + C = delete;

?

Contexto: Llegué a querer esto al implementar un rasgo ApproxEqual(Shape, Shape) para diferentes tipos de formas (puntos, cubos, polígonos, ...) donde todos estos son rasgos. Tuve que solucionar esto refactorizando esto en diferentes rasgos, por ejemplo, ApproxEqualPoint(Point, Point) , para evitar implementaciones conflictivas.

@gnzlbg

y permítame "prohibir" algunas combinaciones, por ejemplo, que A + C nunca debería suceder:

No, esto no es algo que permita la regla de la celosía. Ese sería más el dominio del "razonamiento negativo" de alguna forma o tipo.

Contexto: Llegué a querer esto al implementar un rasgo ApproxEqual (Forma, Forma) para diferentes tipos de formas (puntos, cubos, polígonos, ...) donde todos estos son rasgos. Tuve que solucionar esto refactorizando esto en diferentes rasgos, por ejemplo, ApproxEqualPoint (Point, Point), para evitar implementaciones conflictivas.

Entonces @withoutboats ha estado promoviendo la idea de "grupos de exclusión", donde puedes declarar que un cierto conjunto de rasgos son mutuamente excluyentes (es decir, puedes implementar como máximo uno de ellos). Visualizo esto como una especie de enumeración (es decir, los rasgos se declaran todos juntos). Me gusta la idea de esto, especialmente porque (¡creo!) Ayuda a evitar algunos de los aspectos más perniciosos del razonamiento negativo. Pero siento que se necesita más reflexión en este frente, y también una buena reseña que intente resumir todos los "datos" que flotan sobre cómo pensar sobre el razonamiento negativo. Quizás ahora que he terminado (en su mayoría) mi serie de especialización y HKT, pueda pensar en eso ...

@nikomatsakis :

Entonces @withoutboats ha estado promoviendo la idea de "grupos de exclusión", donde puedes declarar que un cierto conjunto de rasgos son mutuamente excluyentes (es decir, puedes implementar como máximo uno de ellos). Visualizo esto como una especie de enumeración (es decir, los rasgos se declaran todos juntos). Me gusta la idea de esto, especialmente porque (¡creo!) Ayuda a evitar algunos de los aspectos más perniciosos del razonamiento negativo. Pero siento que se necesita más reflexión en este frente, y también una buena reseña que intente resumir todos los "datos" que flotan sobre cómo pensar sobre el razonamiento negativo. Quizás ahora que he terminado (en su mayoría) mi serie de especialización y HKT, pueda pensar en eso ...

Pensé en grupos de exclusiones mientras escribía esto (lo mencionaste en los foros el otro día), pero no creo que puedan funcionar ya que en este ejemplo en particular no todas las implementaciones de rasgos son exclusivas. El ejemplo más trivial son los rasgos Point y Float : un Float _can_ puede ser un punto 1D, por lo que ApproxEqualPoint(Point, Point) y ApproxEqualFloat(Float, Float) no pueden ser exclusivo. Hay otros ejemplos como Square y Polygon , o Box | Cube y AABB (cuadro delimitador alineado con el eje) donde la "jerarquía de rasgos" en realidad necesita restricciones más complejas.

No, esto no es algo que permita la regla de la celosía. Ese sería más el dominio del "razonamiento negativo" de alguna forma o tipo.

Al menos podría implementar el caso particular y poner un unimplemented!() en él. Eso sería suficiente, pero obviamente me gustaría más si el compilador capturara estáticamente aquellos casos en los que llamo a una función con un unimplemented!() en ella (y en este punto, estamos nuevamente en terreno de razonamiento negativo) .

La especialización en celosía de llorar

La idea de "grupos de exclusión" es en realidad sólo límites de superretrato negativos. Una cosa que no hemos explorado demasiado a fondo es la noción de especialización de polaridad inversa, lo que le permite escribir un impl especializado que es de polaridad inversa a su impl menos especializado. Por ejemplo, en este caso simplemente escribiría:

impl<T> !Foo for T where T: A + C { }

No estoy completamente seguro de cuáles son las implicaciones de permitir eso. Creo que se conecta con los problemas que Niko ya destacó sobre cómo la especialización es una especie de combinación de reutilización de código con polimorfismo en este momento.

Con toda esta discusión sobre el razonamiento negativo y las implicaciones negativas, me siento obligado a volver a mencionar la vieja idea de Haskell de las "cadenas de instancias" ( papel , papel , rastreador de problemas de GHC , Rust pre-RFC ), como una fuente potencial de inspiración si nada más.

Básicamente, la idea es que en cualquier lugar donde pueda escribir un trait impl, también puede escribir cualquier número de "cláusulas else if" especificando un impl que debería aplicarse en caso de que el (los) anterior (es) no lo hizo, con una "cláusula else" final opcional que especifica una implicación negativa (es decir, si no se aplica ninguna de las cláusulas para Trait , entonces se aplica !Trait ).

@sin barcos

La idea de "grupos de exclusión" es en realidad sólo límites de superretrato negativos.

Creo que eso sería suficiente para mis casos de uso.

Creo que se conecta con los problemas que Niko ya destacó sobre cómo la especialización es una especie de combinación de reutilización de código con polimorfismo en este momento.

No sé si estos se pueden desenredar. Quiero tener:

  • polimorfismo: un rasgo único que abstrae diferentes implementaciones de una operación para muchos tipos diferentes,
  • reutilización de código: en lugar de implementar la operación para cada tipo, quiero implementarlos para grupos de tipos que implementan algunos rasgos,
  • rendimiento: ser capaz de anular una implementación ya existente para un tipo particular o un subconjunto de tipos que tiene un conjunto de restricciones más específico que las implementaciones ya existentes,
  • productividad: ser capaz de escribir y probar mi programa de forma incremental, en lugar de tener que agregar muchos impl s para que se compile.

Cubrir todos los casos es difícil, pero si el compilador me obliga a cubrir todos los casos:

trait Foo {}
trait A {}
trait B {}

impl<T> Foo for T where T: A { ... }
impl<T> Foo for T where T: B { ... }
// impl<T> Foo for T where T: A + B { ... }  //< compiler: need to add this impl!

y también me da implicaciones negativas:

impl<T> !Foo for T where T: A + B { }
impl<T> !Foo for T where T: _ { } // _ => all cases not explicitly covered yet

Podría agregar incrementalmente las impls según las necesite y también obtener buenos errores del compilador cuando intento usar un rasgo con un tipo para el que no hay impl.

No estoy completamente seguro de cuáles son las implicaciones de permitir eso.

Niko mencionó que existen problemas con el razonamiento negativo. FWIW, para lo único que se usa el razonamiento negativo en el ejemplo anterior es para indicar que el usuario sabe que se requiere un impl para un caso particular, pero ha decidido explícitamente no proporcionar una implementación para él.

Acabo de presionar # 33017 y todavía no lo veo vinculado aquí. Está marcado como un agujero de solidez, por lo que sería bueno rastrearlo aquí.

Para https://github.com/dtolnay/quote/issues/7 necesito algo similar a este ejemplo del RFC que aún no funciona. cc @tomaka @Aatch @rphmeier quien comentó sobre esto anteriormente.

trait Example {
    type Output;
    fn generate(self) -> Self::Output;
}

impl<T> Example for T {
    default type Output = Box<T>;
    default fn generate(self) -> Box<T> { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> bool { self }
}

Me encontré con la siguiente solución que da una forma de expresar lo mismo.

#![feature(specialization)]

use std::fmt::{self, Debug};

///////////////////////////////////////////////////////////////////////////////

trait Example: Output {
    fn generate(self) -> Self::Output;
}

/// In its own trait for reasons, presumably.
trait Output {
    type Output: Debug + Valid<Self>;
}

fn main() {
    // true
    println!("{:?}", Example::generate(true));

    // box("s")
    println!("{:?}", Example::generate("s"));
}

///////////////////////////////////////////////////////////////////////////////

/// Instead of `Box<T>` just so the "{:?}" in main() clearly shows the type.
struct MyBox<T: ?Sized>(Box<T>);

impl<T: ?Sized> Debug for MyBox<T>
    where T: Debug
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "box({:?})", self.0)
    }
}

///////////////////////////////////////////////////////////////////////////////

/// Return type of the impl containing `default fn`.
type DefaultOutput<T> = MyBox<T>;

impl Output for bool {
    type Output = bool;
}

impl<T> Example for T where T: Pass {
    default fn generate(self) -> Self::Output {
        T::pass({
            // This is the impl you wish you could write
            MyBox(Box::new(self))
        })
    }
}

impl Example for bool {
    fn generate(self) -> Self::Output {
        self
    }
}

///////////////////////////////////////////////////////////////////////////////
// Magic? Soundness exploit? Who knows?

impl<T: ?Sized> Output for T where T: Debug {
    default type Output = DefaultOutput<T>;
}

trait Valid<T: ?Sized> {
    fn valid(DefaultOutput<T>) -> Self;
}

impl<T: ?Sized> Valid<T> for DefaultOutput<T> {
    fn valid(ret: DefaultOutput<T>) -> Self {
        ret
    }
}

impl<T> Valid<T> for T {
    fn valid(_: DefaultOutput<T>) -> Self {
        unreachable!()
    }
}

trait Pass: Debug {
    fn pass(DefaultOutput<Self>) -> <Self as Output>::Output;
}

impl<T: ?Sized> Pass for T where T: Debug, <T as Output>::Output: Valid<T> {
    fn pass(ret: DefaultOutput<T>) -> <T as Output>::Output {
        <T as Output>::Output::valid(ret)
    }
}

Todavía estoy trabajando en https://github.com/dtolnay/quote/issues/7 y necesitaba un patrón de diamante. Aquí está mi solución. cc @zitsen que preguntó sobre esto antes y @aturon y @rphmeier que respondieron.

#![feature(specialization)]

/// Can't have these impls directly:
///
///  - impl<T> Trait for T
///  - impl<T> Trait for T where T: Clone
///  - impl<T> Trait for T where T: Default
///  - impl<T> Trait for T where T: Clone + Default
trait Trait {
    fn print(&self);
}

fn main() {
    struct A;
    A.print(); // "neither"

    #[derive(Clone)]
    struct B;
    B.print(); // "clone"

    #[derive(Default)]
    struct C;
    C.print(); // "default"

    #[derive(Clone, Default)]
    struct D;
    D.print(); // "clone + default"
}

trait IfClone: Clone { fn if_clone(&self); }
trait IfNotClone { fn if_not_clone(&self); }

impl<T> Trait for T {
    default fn print(&self) {
        self.if_not_clone();
    }
}

impl<T> Trait for T where T: Clone {
    fn print(&self) {
        self.if_clone();
    }
}

impl<T> IfClone for T where T: Clone {
    default fn if_clone(&self) {
        self.clone();
        println!("clone");
    }
}

impl<T> IfClone for T where T: Clone + Default {
    fn if_clone(&self) {
        self.clone();
        Self::default();
        println!("clone + default");
    }
}

impl<T> IfNotClone for T {
    default fn if_not_clone(&self) {
        println!("neither");
    }
}

impl<T> IfNotClone for T where T: Default {
    fn if_not_clone(&self) {
        Self::default();
        println!("default");
    }
}

Detecte un error (o al menos un comportamiento inesperado desde mi perspectiva) con especialización e inferencia de tipo: # 38167

Se debe esperar que estas dos implicaciones sean válidas con especialización, ¿verdad? Parece que no lo está recogiendo con éxito.

impl<T, ST, DB> ToSql<Nullable<ST>, DB> for T where
    T: ToSql<ST, DB>,
    DB: Backend + HasSqlType<ST>,
    ST: NotNull,
{
    ...
}

impl<T, ST, DB> ToSql<Nullable<ST>, DB> for Option<T> where
    T: ToSql<ST, DB>,
    DB: Backend + HasSqlType<ST>,
    ST: NotNull,
{
    ...
}

Presenté https://github.com/rust-lang/rust/issues/38516 por algún comportamiento inesperado con el que me encontré mientras trabajaba en la construcción de especialización en Serde. Similar a https://github.com/rust-lang/rust/issues/38167 , este es un caso en el que el programa se compila sin el impl especializado y cuando se agrega hay un error de tipo. cc @bluss que estaba preocupado por esta situación anteriormente.

¿Qué pasaría si permitiéramos la especialización sin la palabra clave default dentro de una sola caja, similar a como permitimos el razonamiento negativo dentro de una sola caja?

Mi principal justificación es esta: "el patrón de iteradores y vectores". A veces, los usuarios quieren implementar algo para todos los iteradores y vectores:

impl<I> Foo for I where I: Iterator<Item = u32> { ... }
impl Foo for Vec<u32> { ... }

(Esto es relevante para otras situaciones que no sean iteradores y vectores, por supuesto, este es solo un ejemplo).

Hoy esto no se acumula, y hay tsuris y crujir de dientes. La especialización resuelve este problema:

default impl<I> Foo for I where I: Iterator<Item = u32> { ... }
impl Foo for Vec<u32> { ... }

Pero al resolver este problema, ha agregado un contrato público a su caja: es posible anular la implícita del iterador de Foo . Quizás no queremos forzarlo a hacer eso, por lo tanto, la especialización local sin default .


La pregunta que supongo es cuál es exactamente el papel de default . Requerir default fue, creo, originalmente un gesto hacia la explicitación y el código autodocumentado. Así como el código de Rust es inmutable por defecto, privado por defecto, seguro por defecto, también debería ser final por defecto. Sin embargo, debido a que la "no finalidad" es una propiedad global, no puedo especializar un artículo a menos que te permita especializarlo.

Requerir default fue, creo, originalmente un gesto hacia la explicitación y el código de auto-documentación. Sin embargo [..] No puedo especializar un artículo a menos que te permita especializarte en un artículo.

¿Es eso realmente tan malo? Si quieres especializarte en un impl, quizás otras personas también quieran hacerlo.

Me preocupa porque solo pensar en este RFC ya me está dando flashbacks de PTSD de trabajar en bases de código C ++ que usan cantidades obscenas de sobrecarga y herencia y no tengo idea de que wtf está sucediendo en cualquier línea de código que tenga una llamada a un método. Realmente aprecio todo lo que @aturon ha hecho para hacer que la especialización sea explícita y autodocumentada.

¿Es eso realmente tan malo? Si quieres especializarte en un impl, quizás otras personas también quieran hacerlo.

Si otras personas sólo "tal vez" quieren especializarse también, y si hay buenos casos en los que no queremos que lo hagan, no deberíamos hacer que sea imposible especificar esto. (un poco similar a la encapsulación: desea acceder a algunos datos y tal vez otras personas también lo deseen, por lo que marca explícitamente _ estos datos_ como públicos, en lugar de predeterminar todos los datos para que sean públicos).

Me preocupo porque solo pensar en este RFC ya me está dando flashbacks de TEPT ...

Pero, ¿cómo impediría esta especificación evitar que sucedan estas cosas?

si hay buenos casos en los que no quisiéramos que lo hicieran, no deberíamos hacer que sea imposible especificar esto.

No es necesariamente una buena idea darles a los usuarios un poder cada vez que tengan un buen caso de uso. No si también permite a los usuarios escribir código confuso.

Pero, ¿cómo impediría esta especificación evitar que sucedan estas cosas?

Digamos que ve foo.bar() y quiere ver lo que hace bar() . En este momento, si encuentra el método implementado en un tipo coincidente y no está marcado como default , sabe que es la definición de método que está buscando. Con la propuesta de @withoutboats , esto ya no será cierto; en cambio, nunca sabrá con certeza si realmente está mirando el código que se está ejecutando.

en cambio, nunca sabrá con certeza si realmente está mirando el código que se está ejecutando.

Esto es una exageración del efecto de permitir la especialización de las impls no predeterminadas para los tipos locales. Si está mirando un impl concreto, sabe que está mirando el impl correcto. Y tienes acceso a toda la fuente de esta caja; puede determinar si esta implicación es especializada o no mucho antes de "nunca".

Mientras tanto, incluso con default , el problema persiste cuando no se ha finalizado una impl. Si el impl correcto es en realidad un impl default , usted se encuentra en la misma situación de tener dificultades para no estar seguro de si es el impl correcto. Y, por supuesto, si se emplea la especialización, este será el caso con bastante frecuencia (por ejemplo, este es el caso hoy en día para casi todas las implicaciones de ToString ).

De hecho, creo que este es un problema bastante serio, pero no estoy convencido de que default resuelva. Lo que necesitamos son mejores herramientas de navegación de código. Actualmente, rustdoc hace un enfoque de "mejor esfuerzo" cuando se trata de implicaciones de rasgos: no se vincula a su fuente y ni siquiera enumera las implicaciones proporcionadas por las implicaciones generales.

No estoy diciendo que este cambio sea un fracaso de ninguna manera, pero creo que vale la pena considerarlo con más matices.

No es necesariamente una buena idea darles a los usuarios un poder cada vez que tengan un buen caso de uso. No si también permite a los usuarios escribir código confuso.

Exactamente, estoy absolutamente de acuerdo. Creo que estoy hablando de un "usuario" diferente aquí, que es el usuario de las cajas que escribe. No quiere que se especialicen libremente en los rasgos de su caja (posiblemente afectando el comportamiento de su caja de una manera hacky). Por otro lado, le daríamos más poder al "usuario" del que estás hablando, es decir, el autor de la caja, pero incluso sin la propuesta de @withoutboats , tendrías que usar "predeterminado" y encontrarte con el mismo problema .

Creo que default ayuda en el sentido de que si desea simplificar la lectura de un código, puede pedir que nadie use default o que establezca reglas de documentación rigurosas para su uso. En ese punto, solo necesita preocuparse por los default s de std , que presumiblemente la gente entendería mejor.

Recuerdo que la idea de que las reglas de documentación se pudieran imponer a los usos de la especialización contribuyó a que se aprobara la RFC de especialización.

@withoutboats, ¿tengo razón al leer su motivación para aflojar default ya que desea una forma restringida de default que significa "anulable, pero solo en esta caja" (es decir, pub(crate) pero por default )? Sin embargo, para mantener las cosas simples, está proponiendo cambiar la semántica de omitir default , en lugar de agregar graduaciones de default -ness?

Correcto. Hacer algo como default(crate) parece exagerado.

A priori, me imagino que se podría simular eso a través de lo que exporta la caja, ¿no? ¿Hay situaciones en las que no podría simplemente introducir un rasgo de ayuda privada con los métodos default y llamarlo desde sus propios impl s finales? ¿Quiere que el usuario utilice sus default s pero no proporcione ninguno propio?

Correcto. Hacer algo como predeterminado (caja) parece una exageración.

Estoy en desacuerdo. Realmente quiero una forma restringida de incumplimiento. Tenía la intención de proponerlo. Mi motivación es que a veces las implicaciones de intersección, etc., lo obligarán a agregar valores predeterminados, pero eso no significa que desee permitir que las cajas arbitrarias cambien su comportamiento. Lo siento, tengo una reunión, puedo intentar elaborar con un ejemplo en un momento.

@nikomatsakis Tengo la misma motivación, lo que propongo es que eliminemos el requisito predeterminado de especializarnos en la misma caja, en lugar de agregar más palancas. :-)

Si por casualidad este valor predeterminado no exportado pudiera ser el uso más común, entonces una característica #[default_export] sería más fácil de recordar por analogía con #[macro_export] . Una opción intermedia podría permitir esta función de exportación para líneas pub use o pub mod .

Usar la palabra clave pub sería mejor, ya que Macros 2.0 admitirá macros como elementos normales y usará pub lugar de #[macro_use] . Usar pub para indicar visibilidad en todos los ámbitos sería una gran ventaja por su consistencia.

@withoutboats independientemente, creo que a veces querrás especializarte localmente, pero no necesariamente abrir las puertas a todos.

Usar la palabra clave pub sería mejor

Tener pub default fn significa "exportar públicamente el valor predeterminado de fn" en lugar de afectar la visibilidad de la función en sí sería muy confuso para los recién llegados.

@jimmycuadra, ¿eso es lo que quiso decir con la palabra clave pub ? Estoy de acuerdo con @sgrif en que parece más confuso, y si vamos a permitirle establecer el alcance de la default explícitamente, la misma sintaxis que decidimos para la visibilidad del alcance parece ser la ruta correcta.

Probablemente no pub default fn exactamente, porque eso es ambiguo, como ambos mencionan. Solo estaba diciendo que es valioso que pub signifique universalmente "exponer algo que de otra manera sería privado al exterior". Probablemente haya alguna formulación de sintaxis que involucre pub que sería visualmente diferente para no confundirse con hacer pública la función en sí.

Aunque tiene un poco de sintaxis, no me opondría a que default(foo) trabaje como pub(foo) ; la simetría entre los dos supera marginalmente la complejidad de la sintaxis para mí.

Advertencia de bikeshed: ¿hemos considerado llamarlo overridable lugar de default ? Es más literalmente descriptivo, y overridable(foo) me parece mejor que default(foo) ; este último sugiere "este es el valor predeterminado dentro del alcance de foo , pero algo más podría ser el predeterminado en otro lugar ", mientras que el primero dice" esto es anulable dentro del alcance de foo ", lo cual es correcto.

Creo que las dos primeras preguntas son realmente: ¿Exportar o no exportar default ness es significativamente más común? ¿ No debería exportar default ness ser el comportamiento predeterminado?

Caso de sí: podría maximizar la similitud con las exportaciones en otros lugares dicte algo como pub mod mymodule default; y pub use mymodule::MyTrait default; , o tal vez con overridable . Si es necesario, puede exportar default ness solo para algunos métodos con pub use MyModule::MyTrait::{methoda,methodb} default;

No hay caso: debes expresar privacidad, no publicidad, que de todos modos difiere considerablemente de cualquier otra cosa en Rust, por lo que ahora default(crate) convierte en la forma normal de controlar estas exportaciones.

Además, si exportar y no exportar default ness son comparativamente comunes, entonces ustedes probablemente pueden elegir arbitrariamente estar en el caso de sí o no, por lo que nuevamente solo elegir pub use MyModule::MyTrait::{methoda,methodb} default; funciona bien.

Todas estas notaciones parecen compatibles de todos modos. Otra opción podría ser un impl que cerró los default s, pero eso suena complejo y extraño.

@burdges ¿Tiene las etiquetas "sí caso" y "no caso" al revés, o estoy entendiendo mal lo que está diciendo?

¡Sí, uy! ¡Fijo!

Tenemos impl<T> Borrow<T> for T where T: ?Sized para que un límite de Borrow<T> pueda tratar los valores propios como si fueran prestados.

Supongo que podríamos usar la especialización para optimizar las llamadas ausentes a clone desde Borrow<T> , ¿no?

pub trait CloneOrTake<T> {
    fn clone_or_take(self) -> T;
}

impl<B,T> CloneOrTake<T> for B where B: Borrow<T>, T: Clone {
    #[inline]
    default fn clone_or_take(b: B) -> T { b.clone() }
}
impl<T> CloneOrTake<T> for T {
    #[inline]
    fn clone_or_take(b: T) -> T { b };
}

Creo que esto podría hacer que Borrow<T> pueda usar en más situaciones. Dejé caer el límite de T: ?Sized porque presumiblemente uno necesita Sized al devolver T .

Otro enfoque podría ser

pub trait ToOwnedFinal : ToOwned {
    fn to_owned_final(self) -> Self::Owned;
}

impl<B> ToOwnedFinal for B where B: ToOwned {
    #[inline]
    default fn to_owned_final(b: B) -> Self::Owned { b.to_owned() }
}
impl<T> ToOwnedFinal for T {
    #[inline]
    fn to_owned_final(b: T) -> T { b };
}

Hoy hemos hecho algunos descubrimientos posiblemente preocupantes, puede leer los registros de IRC aquí: https://botbot.me/mozilla/rust-lang/

No estoy 100% seguro de todas las conclusiones a las que llegamos, especialmente porque los comentarios de Niko después de los hechos parecen alentadores. Por un momento me pareció un poco apocalíptico.

Una cosa de la que estoy bastante seguro es que requerir default no se puede hacer compatible con una garantía de que agregar nuevos default impls siempre es compatible con versiones anteriores. Aquí está la demostración:

caja parent v 1.0.0

trait A { }
trait B { }
trait C {
    fn foo(&self);
}

impl<T> C for T where T: B {
    // No default, not specializable!
    fn foo(&self) { panic!() }
}

caja client (depende de parent )

extern crate parent;

struct Local;

impl parent::A for Local { }
impl parent::C for Local {
    fn foo(&self) { }
}

Implementos locales A y C pero no B . Si local implementado B , su impl de C entraría en conflicto con el impl general no especializable de C for T where T: B .

caja parent v 1.1.0

// Same code as before, but add:
default impl<T> B for T where T: A { }

Este impl se ha agregado y es un impl completamente especializable, por lo que hemos dicho que es un cambio inquebrantable. Sin embargo , crea una implicación transitiva: ya teníamos "todo B impl C (no especializable)", al agregar "todo A impl B (especializable)", hemos agregado implícitamente la declaración "todo A impl C (no especializable)". ". Ahora la caja para niños no se puede actualizar.


Podría darse el caso de que la idea de garantizar que agregar impls especializables no sea un cambio rotundo esté totalmente fuera de la ventana, porque Aaron mostró (como puede ver en los registros vinculados arriba) que puede escribir impls que ofrecen garantías equivalentes con respecto a la morosidad . Sin embargo, los comentarios posteriores de Niko sugieren que tales implicaciones pueden estar prohibidas (o al menos prohibidas) por las reglas huérfanas.

Por lo tanto, no estoy seguro de que la garantía de "los implícitos no se rompan" sea recuperable, pero es cierto que no es compatible con el control explícito sobre la finalidad implícita.

¿Hay algún plan para permitir esto?

struct Foo;

trait Bar {
    fn bar<T: Read>(stream: &T);
}

impl Bar for Foo {
    fn bar<T: Read>(stream: &T) {
        let stream = BufReader::new(stream);

        // Work with stream
    }

    fn bar<T: BufRead>(stream: &T) {
        // Work with stream
    }
}

Entonces, esencialmente una especialización para una función de plantilla que tiene un parámetro de tipo con un límite en A donde la versión especializada tiene un límite en B (que requiere A ).

@torkleyy no actualmente, pero puede hacerlo en secreto creando un rasgo que se implemente para T: Read y T: BufRead y que contenga las partes de su código en las que desea especializarse en las implicaciones de ese rasgo. Ni siquiera necesita estar visible en la API pública.

Con respecto al problema de compatibilidad con versiones anteriores, creo que gracias a las reglas huérfanas podemos salirse con la nuestra:

_Un impl es compatible con versiones anteriores para agregar a menos que : _

  • _El rasgo implícito es un rasgo automático_
  • _El receptor es un parámetro de tipo y todos los rasgos del impl existían previamente.

Es decir, creo que en todos los ejemplos problemáticos, el impl agregado es un impl general. Queríamos decir que las implicaciones generales predeterminadas también están bien, pero creo que solo tenemos que decir que agregar las implicaciones generales existentes puede ser un cambio radical.

La pregunta es qué garantía queremos hacer frente a eso, por ejemplo, creo que sería una propiedad muy buena si al menos una implicación general solo puede ser un cambio rotundo basado en el código en su caja, para que pueda revisar su caja y sepa con certeza si necesita o no incrementar la versión principal.

@sin barcos

Con respecto al problema de compatibilidad con versiones anteriores, creo que gracias a las reglas huérfanas podemos salirse con la nuestra:

_Un impl es compatible con versiones anteriores para agregar a menos que : _

  • _El rasgo implícito es un rasgo automático_
  • _El receptor es un parámetro de tipo y todos los rasgos del impl existían previamente.

Es decir, creo que en todos los ejemplos problemáticos, el impl agregado es un impl general. Queríamos decir que las implicaciones generales predeterminadas también están bien, pero creo que solo tenemos que decir que agregar las implicaciones generales existentes puede ser un cambio radical.

Una semana y muchas discusiones después, lamentablemente este no ha sido el caso .

Los resultados que hemos tenido son: crying_cat_face :, pero creo que lo que escribí allí es lo mismo que tu conclusión. Agregar implicaciones generales es un cambio radical, pase lo que pase. Pero solo implicaciones generales (e implicaciones de rasgos automáticos); Hasta donde yo sé, no hemos encontrado un caso en el que un impl no general pudiera romper el código descendente (y eso sería muy malo).

En un momento pensé que podríamos relajar las reglas huérfanas para que pudieras implementar rasgos para tipos como Vec<MyType> , pero si lo hiciéramos, esta situación se desarrollaría exactamente de la misma manera:

//crate A

trait Foo { }

// new impl
// impl<T> Foo for Vec<T> { }
// crate B
extern crate A;

use A::Foo;

trait Bar {
    type Assoc;
}

// Sadly, this impl is not an orphan
impl<T> Bar for Vec<T> where Vec<T>: Foo {
    type Assoc = ();
}
// crate C

struct Baz;

// Therefore, this impl must remain an orphan
impl Bar for Vec<Baz> {
    type Assoc = bool;
}

@withoutboats Ah, entendí tu lista de dos viñetas como o en lugar de y , ¿qué parece que es lo que querías decir?

@aturon Sí, quise decir 'o'; esos son los dos casos en los que se trata de un cambio radical. Cualquier implicación de rasgo automático, no importa cuán concreto sea, es un cambio rotundo debido a la forma en que permitimos que se propague el razonamiento negativo sobre ellos: https://is.gd/k4Xtlp

Es decir, a menos que contenga nombres nuevos. AFAIK, un impl que contiene un nuevo nombre nunca se rompe.

@withoutboats Me pregunto si podemos / debemos restringir a las personas que confían en la lógica negativa en torno a los rasgos automáticos. Es decir, si dijéramos que agregar nuevas implicaciones de rasgos automáticos es un cambio de ruptura legal, podríamos advertir sobre las implicaciones que podrían romperse si una caja aguas arriba agregue Send . Esto funcionaría mejor si tuviéramos:

  1. especialización estable, uno podría superar las advertencias agregando default en lugares estratégicos (gran parte del tiempo);
  2. alguna forma de implicaciones negativas explícitas, de modo que tipos como Rc podrían declarar su intención de nunca ser Send , pero luego tenemos esos para rasgos automáticos, por lo que podríamos tenerlos en cuenta.

No sé, creo que depende de si hay una motivación fuerte o no. Parece especialmente improbable que se dé cuenta de que un tipo podría tener un unsafe impl Send/Sync después de haberlo lanzado; Creo que la mayor parte del tiempo sería seguro, habrás escrito un tipo con el conocimiento previo de que sería seguro (porque ese es el punto del tipo).

Añado unsafe impl Send/Sync después del hecho todo el tiempo. A veces porque lo hago seguro para subprocesos, a veces porque me doy cuenta de que la API de C con la que estoy interactuando está bien para compartir entre subprocesos, y a veces es solo porque algo debería ser Send / Sync isn No es en lo que estoy pensando cuando presento un tipo.

Los agrego después del hecho también al vincular las API de C, a menudo porque alguien solicita explícitamente esos límites, así que luego reviso y verifico lo que garantiza la biblioteca subyacente.

Una cosa que no me gusta de cómo funcionan los rasgos asociados especializados en este momento, este patrón no funciona:

trait Buffer: Read {
    type Buffered: BufRead;
    fn buffer(self) -> impl BufRead;
}

impl<T: Read> Buffer for T {
    default type Buffered = BufReader<T>;
    default fn buffer(self) -> BufReader<T> {
        BufReader::new(self)
    }
}

impl<T: BufRead> Buffer for T {
    type Buffered = Self;
    fn buffer(self) -> T {
        self
    }
}

Esto se debe a que el sistema actual requiere que esta implicación sea válida:

impl Buffer for SomeRead {
    type Buffered = SomeBufRead;
    // no overriding of fn buffer, it no longer returns Self::Buffered
}

impl Trait en rasgos liberaría mucho deseo por este tipo de patrón, pero me pregunto si no hay una solución mejor donde el impl genérico es válido pero esa especialización no funciona porque introduce un error de tipo ?

@withoutboats Sí, esta es una de las principales preguntas sin resolver sobre el diseño (que me había olvidado de mencionar en discusiones recientes). Hay bastante discusión sobre esto en el hilo RFC original, pero intentaré escribir un resumen de las opciones / compensaciones pronto.

@aturon ¿Es la solución actual la más conservadora (compatible hacia adelante con lo que queramos hacer) o es una decisión que tenemos que tomar antes de estabilizarnos?

Personalmente, creo que la única solución real a este problema que planteó @withoutboats es permitir que los elementos se "agrupen" cuando se especifica la etiqueta default . Es una especie de solución mejor es mejor, pero creo que la variante peor es mejor (anular cualquier medio que anule todo) es bastante peor. (Pero en realidad @withoutboats la forma en que escribió este código es confusa. Creo que en lugar de usar impl BufRead como el tipo de retorno de Buffer , se refería a Self::BufReader , ¿verdad?)

En ese caso, se permitiría lo siguiente:

trait Buffer: Read {
    type Buffered: BufRead;
    fn buffer(self) -> impl BufRead;
}

impl<T: Read> Buffer for T {
    default {
        type Buffered = BufReader<T>;
        fn buffer(self) -> BufReader<T> {
            BufReader::new(self)
        }
    }
}

impl<T: BufRead> Buffer for T {
    type Buffered = Self;
    fn buffer(self) -> T {
        self
    }
}

Pero, ¿quizás podamos inferir estos agrupamientos? No lo he pensado mucho, pero parece que el hecho de que los elementos predeterminados estén "enredados" es visible en la definición de características.

Pero en realidad @withoutboats, la forma en que escribiste este código es confusa. Creo que en lugar de usar impl BufRead como el tipo de retorno de Buffer, te refieres a Self :: BufReader, ¿verdad?

Sí, modifiqué la solución a una basada en un rasgo implícito y luego volví a cambiar, pero perdí el tipo de retorno en el rasgo.

Quizás algo así como el sistema de tipos de este lenguaje también pueda ser interesante, ya que parece ser similar a Rusts, pero con algunas características, que pueden solucionar los problemas actuales.
( A <: B sería verdadero en Rust cuando A es una estructura e implementa el rasgo B , o cuando A es un rasgo, y las implementaciones genéricas para objetos de este rasgo existe, creo)

Parece que hay un problema con el rasgo Display de especialización.
Por ejemplo, este ejemplo no compila:

use std::fmt::Display;

pub trait Print {
    fn print(&self);
}

impl<T: Display> Print for T {
    default fn print(&self) {
        println!("Value: {}", self);
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

con el siguiente error:

error[E0119]: conflicting implementations of trait `Print` for type `()`:
  --> src/main.rs:41:1
   |
35 |   impl<T: Display> Print for T {
   |  _- starting here...
36 | |     default fn print(&self) {
37 | |         println!("Value: {}", self);
38 | |     }
39 | | }
   | |_- ...ending here: first implementation here
40 | 
41 |   impl Print for () {
   |  _^ starting here...
42 | |     fn print(&self) {
43 | |         println!("No value");
44 | |     }
45 | | }
   | |_^ ...ending here: conflicting implementation for `()`

mientras esto compila:

pub trait Print {
    fn print(&self);
}

impl<T: Default> Print for T {
    default fn print(&self) {
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

Gracias por solucionar este problema.

@antoyo, ¿estás seguro de que es porque Display es especial, o podría ser porque Display no está implementado para tuplas mientras que Default ?

@hepmaster
No sé si se trata de Display , pero lo siguiente funciona con un rasgo Custom no implementado para tuplas:

pub trait Custom { }

impl<'a> Custom for &'a str { }

pub trait Print {
    fn print(&self);
}

impl<T: Custom> Print for T {
    default fn print(&self) {
    }
}

impl Print for () {
    fn print(&self) {
        println!("No value");
    }
}

fn main() {
    "Hello, world!".print();
    ().print();
}

Por cierto, esto es lo que realmente quiero lograr con la especialización:

pub trait Emit<C, R> {
    fn emit(callback: C, value: Self) -> R;
}

impl<C: Fn(Self) -> R, R, T> Emit<C, R> for T {
    default fn emit(callback: C, value: Self) -> R {
        callback(value)
    }
}

impl<C> Emit<C, C> for () {
    fn emit(callback: C, _value: Self) -> C {
        callback
    }
}

Quiero llamar a una función de forma predeterminada o devolver un valor si el parámetro sería la unidad.
Recibo el mismo error sobre implementaciones conflictivas.
¿Es posible (o será posible) hacer eso con especialización?
si no, cuales son las alternativas?

Editar: creo que descubrí por qué no se compila:
T en for T es más general que () en for () por lo que el primer impl no puede ser la especialización.
Y C es más general que C: Fn(Self) -> R por lo que el segundo impl no puede ser la especialización.
Dime si me equivoco.
Pero todavía no entiendo por qué no funciona con el primer ejemplo con Display .

Este es actualmente el comportamiento correcto.

En el ejemplo Custom , esas implicaciones no se superponen debido a un razonamiento negativo local especial. Debido a que el rasgo es de esta caja, podemos inferir que () , que no tiene una implicación de Custom , no se superpone con T: Custom . No se necesita especialización.

Sin embargo, no realizamos este razonamiento negativo para los rasgos que no son de su caja. La biblioteca estándar podría agregar Display for () en la próxima versión, y no queremos que eso sea un cambio importante. Queremos que las bibliotecas tengan la libertad de realizar ese tipo de cambios. Entonces, aunque () no implica Display, no podemos usar esa información en la verificación de superposición.

Pero también, debido a que () no implica Display, no es más específico que T: Display . Esta es la razón por la que la especialización no funciona, mientras que en el caso predeterminado, (): Default , por lo tanto, esa impl es más específica que T: Default .

Impls como este están en una especie de 'limbo' donde no podemos asumir que se superpone o no. Estamos tratando de encontrar una forma basada en principios para hacer que esto funcione, pero no es la primera implementación de la especialización, es una extensión compatible con versiones anteriores de esa característica que vendrá más adelante.

Presenté el número 40582 para rastrear el problema de solidez relacionado con la vida.

Tuve un problema al tratar de usar la especialización, no creo que sea lo mismo que lo que tenía

@ afonso360 No, una edición separada está bien.

Como punto general: en este punto, el trabajo adicional sobre especialización está bloqueado en el trabajo sobre Chalk , lo que debería permitirnos abordar los problemas de solidez y también es probable que aclare los ICE que se están golpeando hoy.

¿Alguien puede aclarar si esto es un error o algo que está prohibido a propósito? https://is.gd/pBvefi

@sgrif Creo que el problema aquí es que no se permite la proyección de tipos asociados predeterminados. Sin embargo, los diagnósticos podrían ser mejores: https://github.com/rust-lang/rust/issues/33481

¿Podría explicar por qué se espera que no se permita? Sabemos que no se podría agregar ningún impl más específico, ya que violaría las reglas huérfanas.

Este comentario indica que es necesario en algunos casos para requerir solidez (aunque no sé por qué) y en otros para obligar a los consumidores de la interfaz a tratarla como un tipo abstracto: https://github.com/rust- lang / rust / blob / e5e664f / src / librustc / traits / project.rs # L41

¿Alguien pudo alguna vez mirar https://github.com/rust-lang/rust/issues/31844#issuecomment -266221638? Hasta donde yo sé, esas implicaciones deberían ser válidas con especialización. Creo que hay un error que los está impidiendo.

@sgrif Creo que el problema con su código puede ser similar al problema en https://github.com/rust-lang/rust/issues/31844#issuecomment -284235369 que @withoutboats explicó en https://github.com / rust-lang / rust / issues / 31844 # issuecomment -284268302. Dicho esto, basado en el comentario de @withoutboats , parece que el razonamiento local actual debería permitir que su ejemplo se compile, pero tal vez estoy equivocado en cuanto a lo que se espera que funcione.

Además, intenté implementar lo siguiente, sin éxito:

trait Optional<T> {
    fn into_option(self) -> Option<T>;
}

impl<R, T: Into<R>> Optional<R> for T {
    default fn into_option(self) -> Option<R> {
        Some(self.into())
    }
}

impl<R> Optional<R> for Option<R> {
    fn into_option(self) -> Option<R> {
        self
    }
}

Intuitivamente esperaba que Option<R> fuera más específico que <R, T: Into<R>> T , pero, por supuesto, nada impide un impl<R> Into<R> for Option<R> en el futuro.

Sin embargo, no estoy seguro de por qué no está permitido. Incluso si se agregó un impl<R> Into<R> for Option<R> en el futuro, aún esperaría que Rust elija la implementación que no sea default , por lo que puedo ver, permitir este código no tiene implicaciones en el reenvío. compatibilidad.

En general, me resulta muy frustrante trabajar con la especialización. Casi todo lo que espero que funcione no funciona. Los únicos casos en los que he tenido éxito con la especialización son aquellos que son muy simples, como tener dos impl s que incluyen T where T: A y T where T: A + B . Me cuesta trabajo hacer que otras cosas funcionen y los mensajes de error no indican por qué los intentos de especialización no funcionan. Por supuesto, todavía queda un camino por recorrer, así que no espero mensajes de error muy útiles. Pero parece haber bastantes casos en los que realmente espero que algo funcione (como el anterior) pero simplemente no lo hace, y actualmente es bastante difícil para mí determinar si es porque no he entendido lo que está permitido (y lo que es más importante, por qué), si algo está mal o si algo no se ha implementado todavía. Una buena descripción general de lo que está sucediendo con esta función tal como está sería muy útil.

No estoy seguro de que esté en el lugar correcto, pero encontramos un problema en el foro de usuarios que me gustaría mencionar aquí.

El siguiente código (que se adapta del RFC aquí ) no se compila todas las noches:

#![feature(specialization)]

trait Example {
    type Output;
    fn generate(self) -> Self::Output;
}

default impl<T> Example for T {
    type Output = Box<T>;
    fn generate(self) -> Self::Output { Box::new(self) }
}

impl Example for bool {
    type Output = bool;
    fn generate(self) -> Self::Output { self }
}

Esto realmente no parece un problema técnico, sino más bien un problema de usabilidad: si un impl hipotético solo se especializara en el tipo asociado en el ejemplo anterior, el defaulti impl de generate no mecanografíe.

Enlace al hilo aquí

@ burns47 hay una solución confusa pero útil aquí: https://github.com/rust-lang/rust/issues/31844#issuecomment -263175793.

@dtolnay No del todo satisfactorio, ¿qué

¿Alguien puede comentar si el código en el siguiente número se rechaza intencionalmente? https://github.com/rust-lang/rust/issues/45542

¿Permitiría la especialización agregar algo como lo siguiente a libcore?

impl<T: Ord> Eq for T {}

impl<T: Ord> PartialEq for T {
    default fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl<T: Ord> PartialOrd for T {
    default fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

De esta manera, podría implementar Ord para su tipo personalizado y hacer que Eq , PartialEq y PartialOrd se implementen automáticamente.

Tenga en cuenta que implementar Ord y derivar simultáneamente PartialEq o PartialOrd es peligroso y puede dar lugar a errores muy sutiles. Con estas implicaciones predeterminadas, estaría menos tentado a derivar esos rasgos, por lo que el problema se mitigaría un poco.


Alternativamente, modificamos la derivación para aprovechar la especialización. Por ejemplo, escribir #[derive(PartialOrd)] arriba de struct Foo(String) podría generar el siguiente código:

impl PartialOrd for Foo {
    default fn partial_cmp(&self, other: &Foo) -> Option<Ordering> {
        self.0.partial_cmp(&other.0)
    }
}

impl PartialOrd for Foo where Foo: Ord {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

De esta manera, se usa el impl predeterminado si Ord no está implementado. Pero si es así, entonces PartialOrd basa en Ord . Desafortunadamente, esto no se compila: error[E0119]: conflicting implementations of trait `std::cmp::PartialOrd` for type `Foo`

@stjepang Ciertamente espero que se puedan agregar mantas como esa, impl<T:Copy> Clone for T también.

Yo creo que

impl<T: Ord> PartialEq for T

debiera ser

impl<T, U> PartialEq<U> for T where T : PartialOrd<U>

porque PartialOrd requiere PartialEq y también puede proporcionarlo.

En este momento, realmente no se pueden usar tipos asociados para restringir una especialización, tanto porque no pueden dejarse sin especificar como porque desencadenan una recursividad no necesaria . Ver https://github.com/dhardy/rand/issues/18#issuecomment -358147645

Eventualmente, me encantaría ver lo que llamo grupos de especialización con la sintaxis propuesta por @nikomatsakis aquí https://github.com/rust-lang/rust/issues/31844#issuecomment -249355377 e independientemente por mí. Me gustaría escribir un RFC sobre esa propuesta más adelante, cuando estemos más cerca de estabilizar la especialización.

En caso de que nadie lo viera, esta publicación de blog cubre una propuesta para hacer que la especialización suene frente al despacho de por vida.

Como los cierres de copias ya estaban estabilizados en Beta, los desarrolladores ahora tienen más motivación para estabilizarse en la especialización. La razón es que Fn y FnOnce + Clone representan dos conjuntos de cierres superpuestos y, en muchos casos, necesitamos implementar características para ambos.

Solo tenga en cuenta que la redacción de

  • FnOnce (un cierre move con todas las variables capturadas que no son ni Copy ni Clone )
  • FnOnce + Clone (un cierre de move con todas las variables capturadas Clone )
  • FnOnce + Copy + Clone (un cierre move con todas las variables capturadas siendo Copy y así Clone )
  • FnMut + FnOnce (un cierre no move con variables capturadas mutadas)
  • Fn + FnMut + FnOnce + Copy + Clone (un cierre no move sin variables capturadas mutadas)

Entonces, si la especificación no está disponible en un futuro cercano, tal vez deberíamos actualizar nuestra definición de Fn rasgos para que Fn no se superponga con FnOnce + Clone ?

Entiendo que es posible que alguien ya haya implementado tipos específicos que son Fn sin Copy/Clone , pero ¿debería quedar obsoleto? Creo que siempre hay una mejor manera de hacer lo mismo.

¿Se supone que lo siguiente está permitido por especialización (tenga en cuenta la ausencia de default ) o es un error?

#![feature(specialization)]
mod ab {
    pub trait A {
        fn foo_a(&self) { println!("a"); }
    }

    pub trait B {
        fn foo_b(&self) { println!("b"); }
    }

    impl<T: A> B for T {
        fn foo_b(&self) { println!("ab"); }
    }

    impl<T: B> A for T {
        fn foo_a(&self) { println!("ba"); }
    }
}

use ab::B;

struct Foo;

impl B for Foo {}

fn main() {
    Foo.foo_b();
}

sin especialización, esto no se puede construir con:

error[E0119]: conflicting implementations of trait `ab::B` for type `Foo`:
  --> src/main.rs:24:1
   |
11 |     impl<T: A> B for T {
   |     ------------------ first implementation here
...
24 | impl B for Foo {}
   | ^^^^^^^^^^^^^^ conflicting implementation for `Foo`

@glandium, ¿qué diablos está pasando allí? Buen ejemplo, aquí el enlace del patio de recreo: https://play.rust-lang.org/?gist=fc7cf5145222c432e2bd8de1b0a425cd&version=nightly&mode=debug

¿Lo es? no hay ningún impl vacío en mi ejemplo.

@glandio

 impl B for Foo {}

@MoSal pero ese impl "no está vacío" ya que B agrega un método con una implementación predeterminada.

@gnzlbg Está vacío por definición. Nada entre tirantes.


#![feature(specialization)]

use std::borrow::Borrow;

#[derive(Debug)]
struct Bla {
    bla: Vec<Option<i32>>
}

// Why is this a conflict ?
impl From<i32> for Bla {
    fn from(i: i32) -> Self {
        Bla { bla: vec![Some(i)] }
    }
}

impl<B: Borrow<[i32]>> From<B> for Bla {
    default fn from(b: B) -> Self {
        Bla { bla: b.borrow().iter().map(|&i| Some(i)).collect() }
    }
}

fn main() {
    let b : Bla = [1, 2, 3].into();
    println!("{:?}", b);
}

error[E0119]: conflicting implementations of trait `std::convert::From<i32>` for type `Bla`:
  --> src/main.rs:17:1
   |
11 | impl From<i32> for Bla {
   | ---------------------- first implementation here
...
17 | impl<B: Borrow<[i32]>> From<B> for Bla {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Bla`
   |
   = note: upstream crates may add new impl of trait `std::borrow::Borrow<[i32]>` for type `i32` in future versions

¿No evitaría la especialización posibles conflictos futuros?

¡Dios mío, esta es una función de movimiento lento! Sin progreso en más de dos años, al parecer (ciertamente de acuerdo con la publicación original). ¿El equipo de lang ha abandonado esto?

@alexreg, consulte http://aturon.github.io/2018/04/05/sound-specialization/ para conocer las últimas novedades.

@alexreg Resulta que la solidez es _duro_. Creo que hay algo de trabajo sobre la idea de "Impls siempre aplicables" que se está produciendo actualmente, por lo que hay progreso. Consulte https://github.com/rust-lang/rust/pull/49624. Además, creo que el grupo de trabajo de tiza también está trabajando en la implementación de la idea de "implicaciones siempre aplicables", pero no sé qué tan lejos ha llegado.

Después de un poco de discusión, parece que es posible implementar de manera efectiva las implicaciones de intersección a través de un truco usando specialization y overlapping_marker_traits .

https://play.rust-lang.org/?gist=cb7244f41c040db41fc447d491031263&version=nightly&mode=debug

Traté de escribir una función especializada recursiva para implementar un equivalente a este código C ++:


Código C ++

#include <cassert>
#include <vector>

template<typename T>
size_t count(T elem)
{
    return 1;
}

template<typename T>
size_t count(std::vector<T> vec)
{
    size_t n = 0;
    for (auto elem : vec)
    {
        n += count(elem);
    }
    return n;
}

int main()
{
    auto v1 = std::vector{1, 2, 3};
    assert(count(v1) == 3);

    auto v2 = std::vector{ std::vector{1, 2, 3}, std::vector{4, 5, 6} };
    assert(count(v2) == 6);

    return 0;
}


Probé esto:


Código de óxido

#![feature(specialization)]

trait Count {
    fn count(self) -> usize;
}

default impl<T> Count for T {
    fn count(self) -> usize {
        1
    }
}

impl<T> Count for T
where
    T: IntoIterator,
    T::Item: Count,
{
    fn count(self) -> usize {
        let i = self.into_iter();

        i.map(|x| x.count()).sum()
    }
}

fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(v.count(), 3);

    let v = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
    ];
    assert_eq!(v.count(), 6);
}


Pero estoy obteniendo un:

overflow evaluating the requirement `{integer}: Count`

No creo que esto deba suceder porque impl<T> Count for T where T::Item: Count no debería desbordarse.

EDITAR: lo siento, acabo de ver que esto ya se mencionó

@Boiethios Su caso de uso está funcionando si está predeterminado en el fn y no en el impl:

#![feature(specialization)]

trait Count {
    fn count(self) -> usize;
}

impl<T> Count for T {
    default fn count(self) -> usize {
        1
    }
}

impl<T> Count for T
where
    T: IntoIterator,
    T::Item: Count,
{
    fn count(self) -> usize {
        let i = self.into_iter();

        i.map(|x| x.count()).sum()
    }
}

fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(v.count(), 3);

    let v = vec![vec![1, 2, 3], vec![4, 5, 6]];
    assert_eq!(v.count(), 6);
}

¿Aún no se ha reparado el orificio de solidez?

@alexreg No lo creo. Ver http://smallcultfollowing.com/babysteps/blog/2018/02/09/maximally-minimal-specialization-always-applicable-impls/

Supongo que todo el mundo está centrado en la edición ahora mismo ...

De acuerdo, gracias ... parece que este problema se prolonga para siempre, pero es justo. Es duro, lo sé. Y la atención se dirige a otra parte en este momento, lamentablemente.

¿Alguien puede explicar de manera más concreta la razón de ser de no permitir proyecciones para tipos asociados predeterminados en casos totalmente monomórficos? Tengo un caso de uso en el que me gustaría esa funcionalidad (en particular, sería semánticamente incorrecto que el rasgo se invocara con tipos que no fueran completamente monomórficos), y si no hay un problema de solidez, no entiendo completamente por qué no está permitido.

@pythonesque Hay algo de discusión en https://github.com/rust-lang/rust/pull/42411

Ah, entiendo si resulta que la proyección interactúa mal con la especialización en general. . Y es cierto que lo que quiero es un sabor de "razonamiento negativo" (aunque los rasgos cerrados no serían suficientes).

Desafortunadamente, no estoy seguro de si realmente hay alguna forma de hacer lo que quiero sin dicha característica: me gustaría tener un tipo asociado que dé como resultado "Verdadero" cuando dos tipos pasados ​​que implementan un rasgo en particular son sintácticamente iguales, y "Falso" cuando no lo son (con el caso "Falso" activando una búsqueda de rasgos más costosa que puede decidir si son "semánticamente" iguales). La única alternativa real me parece (para mí) ser siempre hacer la búsqueda costosa; lo cual está bien en teoría, pero puede ser mucho más caro.

(Podría solucionar esto si el rasgo estuviera destinado a cerrarse, simplemente enumerando cada par posible de constructores en la posición principal y haciendo que muestren True o False; pero está destinado a estar abierto a la extensión fuera del repositorio, por lo que puede posiblemente funcione, especialmente porque las implementaciones en dos repositorios de usuarios diferentes no necesariamente se conocen entre sí).

De todos modos, tal vez esto sea solo una indicación de que lo que quiero hacer no encaja bien con el sistema de rasgos y debería cambiar a algún otro mecanismo, como macros: P

Y es cierto que lo que quiero es un sabor de "razonamiento negativo" (aunque los rasgos cerrados no serían suficientes).

Una alternativa al razonamiento negativo es requerir que un tipo implemente solo un rasgo de un conjunto cerrado de rasgos, de modo que las implementaciones con otros rasgos del conjunto no puedan superponerse (por ejemplo, T implementa uno de { Float | Int | Bool | Ptr } ).

Incluso si hubiera una forma de hacer cumplir eso en Rust (que no hay, ¿AFAIK?), No creo que eso resolvería mi problema. Me gustaría que los usuarios de diferentes cajas pudieran implementar un número arbitrario de nuevas constantes, que deberían ser comparables solo entre sí y no iguales a todas las demás constantes definidas, incluidas las desconocidas en el momento de la definición de la caja. No veo cómo un conjunto cerrado de rasgos (o incluso un conjunto de familias de rasgos) puede lograr ese objetivo por sí solo: este es un problema que fundamentalmente no se puede resolver sin mirar directamente a los tipos. La razón por la que sería viable con las proyecciones predeterminadas es que podría predeterminar todo para "no comparar igual" y luego implementar la igualdad de su nueva constante a sí misma en cualquier caja en la que haya definido la constante, lo que no entraría en conflicto con huérfano reglas porque todos los tipos en la implementación del rasgo estaban en la misma caja. Si quisiera casi cualquier regla que no fuera la igualdad, ni siquiera esto funcionaría, pero la igualdad es lo suficientemente buena para mí :)

En el presente todas las noches, esto funciona:

trait Foo {}
trait Bar {}

impl<T: Bar> Foo for T {}
impl Foo for () {}

pero incluso con especialización, y usando todas las noches, esto no:

#![feature(specialization)]

trait Foo<F> {}
trait Bar<F> {}

default impl<F, T: Bar<F>> Foo<F> for T {}
impl<F> Foo<F> for () {}

¿Tiene esto una justificación o es un error?

@rmanoka ¿No son solo las reglas huérfanas normales? En el primer caso, ninguna caja descendente podría impl Bar for () por lo que el compilador lo permite, pero en el segundo ejemplo, una caja descendente podría impl Bar<CustomType> for () que entraría en conflicto con su impl predeterminado.

@Boscop En ese escenario, el impl predeterminado debería ser anulado de todos modos por el no predeterminado a continuación. Por ejemplo, si tuviera: impl Bar<bool> for () {} agregado antes de las otras implicaciones, entonces esperaría que funcione (según RFC / expectativa). ¿No es eso correcto?

Profundizando en las líneas del contraejemplo que ha mencionado, me doy cuenta (o creo) que el ejemplo satisface la prueba de "siempre aplicable" y es posible que se esté trabajando.

Este problema probablemente depende de # 45814.

¿Hay planes para admitir límites de rasgos predeterminados que no están presentes en la especialización?

Como un ejemplo para el cual esto sería muy útil, de modo que pueda componer fácilmente el manejo de diferentes tipos creando un Struct genérico con Inner arbitrario para la funcionalidad que no debe compartirse.

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Inner;
impl Handler<f64> for Inner {
    fn handle(&self, m : f64) {
        println!("inner got an f64={}", m);
    }
}

struct Struct<T>(T);
impl<T:Handler<M>, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
        self.0.handle(m)
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
impl<T> Handler<u32> for Struct<T> {
    fn handle(&self, m : u32) {
        println!("got a u32={}", m);
    }
}

fn main() {
    let s = Struct(Inner);
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
    s.handle(5 as u32);
}

Además, en el ejemplo anterior, he experimentado algo extraño: después de eliminar el rasgo vinculado en el Handler impl (y también self.0.handle (m)), el código se compila sin problemas. Sin embargo, cuando elimina la implementación para u32, parece romper la deducción del otro rasgo:

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Struct<T>(T);
impl<T, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
// impl<T> Handler<u32> for Struct<T> {
//     fn handle(&self, m : u32) {
//         println!("got a u32={}", m);
//     }
// }
fn main() {
    let s = Struct(());
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
}

Aunque no hay un código que llame al controlador para u32, la no especialización hace que el código no se compile.

Editar: esto parece ser el mismo que el segundo problema ("Sin embargo, cuando eliminas la implementación para u32, parece romper la deducción del otro rasgo") que Gladdy mencionó en una publicación.

Con rustc 1.35.0-nightly (3de010678 2019-04-11), el siguiente código da un error:

#![feature(specialization)]
trait MyTrait<T> {
    fn print(&self, parameter: T);
}

struct Message;

impl<T> MyTrait<T> for Message {
    default fn print(&self, parameter: T) {}
}

impl MyTrait<u8> for Message {
    fn print(&self, parameter: u8) {}
}

fn main() {
    let message = Message;
    message.print(1_u16);
}

error:

error[E0308]: mismatched types
  --> src/main.rs:20:19
   |
18 |     message.print(1_u16);
   |                   ^^^^^ expected u8, found u16

Sin embargo, el código se compila y funciona cuando omito el bloque impl MyTrait<u8> :

#![feature(specialization)]
trait MyTrait<T> {
    fn print(&self, parameter: T);
}

struct Message;

impl<T> MyTrait<T> for Message {
    default fn print(&self, parameter: T) {}
}

/*
impl MyTrait<u8> for Message {
    fn print(&self, parameter: u8) {}
}
*/

fn main() {
    let message = Message;
    message.print(1_u16);
}

¿Es esto por diseño, es porque la implementación está incompleta o es un error?

Además, me gustaría saber si este caso de uso para la especialización (implementación de rasgos con parámetros de tipo superpuestos para un solo tipo concreto en lugar de implementar el mismo rasgo para tipos superpuestos) será compatible. Leyendo la sección "Definición de las reglas de precedencia" en RFC 1210, creo que sería compatible, pero el RFC no da tales ejemplos y no sé si seguimos estrictamente este RFC.

Informar una rareza:

trait MyTrait {}
impl<E: std::error::Error> MyTrait for E {}

struct Foo {}
impl MyTrait for Foo {}  // OK

// But this one is conflicting with error message:
//
//   "... note: upstream crates may add new impl of trait `std::error::Error` for type
//    std::boxed::Box<(dyn std::error::Error + 'static)>` in future versions"
//
// impl MyTrait for Box<dyn std::error::Error> {}

¿Por qué Box<dyn std::error::Error> peculiar (evite usar la palabra "especial") en este caso? Incluso si implica std::error::Error en el futuro, impl MyTrait for Box<dyn std::error::Error> sigue siendo una especialización válida de impl<E: std::error::Error> MyTrait for E , ¿no?

sigue siendo una especialización válida

En su caso, impl<E: std::error::Error> MyTrait for E no puede ser especializado, ya que no tiene ningún método default .

@ bjorn3 Esto parece que debería funcionar, pero no funciona incluso si agrega métodos ficticios

en caja bar

pub trait Bar {}
impl<B: Bar> Bar for Box<B> {}

En caja foo

#![feature(specialization)]

use bar::*;

trait Trait {
    fn func(&self) {}
}

impl<E: Bar> Trait for E {
    default fn func(&self) {}
}

struct Foo;
impl Trait for Foo {}  // OK

impl Trait for Box<dyn Bar> {} // Error error[E0119]: conflicting implementations of trait

Tenga en cuenta que si cambia la caja bar a

pub trait Bar {}
impl<B: ?Sized + Bar> Bar for Box<B> {}

Luego compila la caja foo .

@ bjorn3 Parece que no necesitamos un método default para especializarlo (zona de juegos ).

@KrishnaSannasi No puedo reproducir el error de "implementaciones en conflicto" en su ejemplo (zona de juegos ).

Actualización: Oh, ya veo. El rasgo Bar debe ser de una caja corriente arriba para que el ejemplo funcione.

@updogliu tu ejemplo no muestra especialización porque Foo no implementa Error .

¿Estoy programando demasiado tarde esta noche, o esto no debería causar un desbordamiento de pila?

#![feature(specialization)]
use std::fmt::Debug;

trait Print {
    fn print(self);
}

default impl<T> Print for [T; 1] where T: Debug {
    fn print(self) {
        println!("{:?}", self);
    }
}

impl<T> Print for [T; 1] where T: Debug + Clone {
    fn print(self) {
        println!("{:?}", self.clone());
    }
}

fn main() {
    let x = [0u8];
    x.print();
}

Enlace de juegos

Los bloques de grano grueso default impl siempre han estado haciendo cosas muy raras para mí, sugeriría probar la sintaxis de especialización de grano fino default fn lugar.

EDITAR: Al verificar el RFC, se espera esto, ya que default impl realidad _no_ significa que todos los elementos del bloque impl son default ed. Encuentro esa semántica sorprendente por decir lo menos.

Enlace de juegos

@ HadrienG2 De hecho, siempre he usado default fn en este proyecto, pero esta vez olvidé la palabra clave default y el compilador sugirió agregarla a impl . No había visto el problema de recursividad de pila antes y no estaba seguro de si se esperaba en esta etapa. Gracias por la sugerencia, default fn funciona bien.

En cuanto al RFC original, hay una sección sobre la especialización de las implicaciones inherentes. ¿Alguien dio que lo intenté?

Es posible que el enfoque propuesto en el RFC ya no funcione directamente, al menos, para los métodos de const inherentes:

// This compiles correctly today:
#![feature(specialization)] 
use std::marker::PhantomData;
struct Foo<T>(PhantomData<T>);
impl<T> Foo<T> {
    default const fn foo() -> Self { Self(PhantomData) }
    // ^^should't default here error?
}
// ----
// Adding this fails:
impl<T: Copy> Foo<T> {
    const fn foo() -> Self { Self(PhantomData) }
}

El RFC original propone convertir el método en un rasgo, implementarlo para el tipo y especializar el impl. Supongo que para los métodos const fn, esas implicaciones del rasgo para el tipo tendrían que ser constantes.

Para cualquiera que se encuentre con esto y sienta curiosidad por el estado, hubo un par de avances conceptuales significativos en 2018:
http://smallcultfollowing.com/babysteps/blog/2018/02/09/maximally-minimal-specialization-always-applicable-impls/
http://aturon.github.io/tech/2018/04/05/sound-specialization/

Más recientemente, el mes pasado @nikomatsakis escribió (como ejemplo, en otro contexto; negrita mía) que:

había un problema clave [en la especialización] que nunca se resolvió satisfactoriamente, una preocupación de solidez técnica en torno a vidas y rasgos [...] Entonces, [esos dos puestos vinculados arriba]. Parece que estas ideas básicamente han resuelto el problema , pero hemos estado ocupados mientras tanto y no hemos tenido tiempo de hacer un seguimiento.

Suena esperanzador, aunque claramente todavía hay trabajo por hacer.

(Publicando esto porque encontré este hilo hace algunas semanas y no tenía idea del progreso del año pasado, luego, más recientemente, encontré esas publicaciones por accidente. Hay comentarios arriba que las mencionan, pero GitHub hace que sea cada vez más difícil ver cualquiera que no sea el primero y últimos comentarios en un hilo largo: llorar:. Podría ser útil si esta actualización se incluye en la descripción del problema).

¡Hola a todos! ¿Alguien podría decirme por qué este caso de uso no funciona? ¿Errores o comportamientos esperados?

Como este ejemplo . impl A for i32 está bien, pero impl A for () no se puede compilar en 1.39.0-nightly.

#![feature(specialization)]

trait A {
    fn a();
}

default impl <T: ToString> A for T {
    fn a() {}
}

impl A for i32 {
    fn a() {}
}

impl A for () {
    fn a() {}
}

mensaje de compilación:

error[E0119]: conflicting implementations of trait `A` for type `()`:
  --> src/lib.rs:16:1
   |
8  | default impl <T: ToString> A for T {
   | ---------------------------------- first implementation here
...
16 | impl A for () {
   | ^^^^^^^^^^^^^ conflicting implementation for `()`
   |
   = note: upstream crates may add new impl of trait `std::fmt::Display` for type `()` in future versions

@Hexilee Pon default en los métodos, no en el impl.

@KrishnaSannasi ejemplo 2

@zserik sí, lo sé. No creo que se haya implementado todavía o se eliminó. En cualquier caso, ahora no funciona.

Obviamente no funciona ahora, pero creo que debería funcionar.

Estoy preguntando esto aquí, porque no he notado que este tema haya surgido en ningún otro lugar: ¿hay algún plan para default -ify varias funciones de biblioteca estándar, de manera similar a como hemos tenido const -ified funciones cuando se considera seguro hacerlo? La razón principal por la que pregunto es que las implementaciones genéricas From y Into predeterminadas ( impl<T, U: From<T>> Into<U> for T y impl<T> From<T> for T ) dificultan la escritura genérica completa From y Into implementaciones posteriores a core , y sería bueno si pudiera anular esas conversiones en mis propias cajas.

Incluso si permitimos la especialización para From / Into , no ayudaría a las impls genéricas debido al problema del enrejado.

@KrishnaSannasi No creo que ese sea el caso. Por ejemplo, este código debería funcionar si From y Into fueran especializables, pero no es así porque no lo son:

impl<M: Into<[S; 2]>, S> From<M> for GLVec2<S> {
    fn from(to_array: M) -> GLVec2<S> {
        unimplemented!()
    }
}
impl<M, S> Into<M> for GLVec2<S>
where
    [S; 2]: Into<M>,
{
    fn into(self) -> M {
        unimplemented!()
    }
}

pub struct GLVec2<S> {
    pub x: S,
    pub y: S,
}

Eso funciona si convierte From y Into en un rasgo personalizado que no tiene esas implementaciones genéricas: https://play.rust-lang.org/?version=stable&mode=debug&edition= 2018 & gist = cc126b016ff62643946aebc6bab88c98

@Osspial Bueno, si intentas simular usando una impl predeterminada, verás el problema,

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=e5b9da0eeca05d063e2605135a0b5ead

Repetiré, cambiar From/Into impl para que sea una impl predeterminada en la biblioteca estándar no hará posibles las implicaciones genéricas para Into . (y no afecta a las implicaciones genéricas de From )

Hola, hay un error grave en la implementación de la especialización actual. Lo etiqueto como un error porque incluso si fue una decisión de diseño explícita, nos impide usar una de las características de especialización más poderosas, a saber, la posibilidad de crear "tipos opacos" (este no es un nombre formal). Este patrón es uno de los bloques de construcción más primitivos en otros lenguajes que proporcionan clases de tipos, como Haskell o Scala.

Este patrón es simple: podemos definir estructuras como WithLabel o WithID que agregan algunos campos y métodos a las estructuras subyacentes, por lo que, por ejemplo, si creamos WithLabel<WithID<MyType>> entonces podremos para obtener id , label y todos los campos / métodos de MyType también. Desafortunadamente, con la implementación actual, no es posible.

A continuación se muestra un código de ejemplo que muestra el uso de este patrón. El código comentado no se compila, aunque debería hacer que este patrón sea realmente útil:

#![feature(specialization)]

use std::ops::Deref;
use std::ops::DerefMut;

// =================
// === WithLabel ===
// =================

struct WithLabel<T>(String, T);

pub trait HasLabel {
    fn label(&self) -> &String;
}

impl<T> HasLabel for WithLabel<T> {
    fn label(&self) -> &String { 
        &self.0
    }
}

// THIS SHOULD COMPILE, BUT GETS REJECTED
// impl<T> HasLabel for T
// where T: Deref, <Self as Deref>::Target : HasLabel {
//     default fn label(&self) -> &String { 
//         self.deref().label() 
//     }
// }

impl<T> Deref for WithLabel<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// ==============
// === WithID ===
// ==============

struct WithID<T>(i32, T);

pub trait HasID {
    fn id(&self) -> &i32;
}

impl<T> HasID for WithID<T> {
    fn id(&self) -> &i32 { 
        &self.0
    }
}

// THIS SHOULD COMPILE, BUT GETS REJECTED
// impl<T> HasID for T
// where T: Deref, <Self as Deref>::Target : HasID {
//     default fn id(&self) -> &i32 { 
//         self.deref().id() 
//     }
// }

impl<T> Deref for WithID<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// =============
// === Usage ===
// =============

struct A(i32);

type X = WithLabel<WithID<A>>;

fn test<T: HasID + HasLabel> (t: T) {
    println!("{:?}", t.label());
    println!("{:?}", t.id());
}

fn main() {
    let v1 = WithLabel("label1".to_string(), WithID(0, A(1)));
    // test(v1); // THIS IS EXAMPLE USE CASE WHICH DOES NOT COMPILE
}

Para hacer que la línea test(v1) funcione, necesitamos agregar dicho rasgo impl manualmente:

impl<T: HasID> HasID for WithLabel<T> {
    fn id(&self) -> &i32 { 
        self.deref().id()
    }
}

Por supuesto, para completarlo, también necesitaríamos hacer este rasgo impl:

impl<T: HasLabel> HasLabel for WithID<T> {
    fn label(&self) -> &String { 
        self.deref().label()
    }
}

Y esto es MUY MALO . Para solo 2 tipos, es simple. Sin embargo, imagine que tenemos 10 definiciones diferentes de tipos opacos, que agregan campos diferentes, como WithID , WithLabel , WithCallback , ... lo que sea. Con el comportamiento actual de las especializaciones, necesitaríamos definir ... ¡más de 1000 implementaciones de rasgos diferentes! Si el código comentado fuera aceptado, necesitaríamos solo 10 implementaciones de rasgos y la implementación de cada nuevo tipo requerirá solo una implementación adicional .

No estoy seguro de cómo se relaciona su código con la especialización. Su argumento (su código inicial se compila pero la línea test(v1); comentada no se compila sin la implicación manual que presenta) aún se aplica si se elimina la primera línea #![feature(specialization)] .

@qnighy El código debería compilarse después de descomentar las impls HasLabel for T y HasID for T - están usando especialización. Actualmente, están rechazados (¡intente descomentarlos en el código que proporcioné!). ¿Tiene sentido ahora para ti? 🙂

Consideremos tres instancias WithLabel<WithID<A>> , WithID<WithLabel<A>> y WithLabel<WithLabel<A>> . Luego

  • el primer impl cubre WithLabel<WithID<A>> y WithLabel<WithLabel<A>> .
  • el segundo impl cubre WithID<WithLabel<A>> y WithLabel<WithLabel<A>> .

Por lo tanto, el par de implicaciones no satisface la siguiente cláusula del RFC :

Para asegurarnos de que la especialización sea coherente, nos aseguraremos de que para dos implicaciones I y J que se superpongan, tengamos I < J o J < I . Es decir, uno debe ser verdaderamente más específico que el otro.

Y también es un problema real en su caso porque la HasLabel impl de WithLabel<WithLabel<A>> podría interpretarse de dos maneras.

Cómo podemos cubrir este caso también se discute en el RFC , y la conclusión es:

Las limitaciones que aborda la regla de la celosía son bastante secundarias a los objetivos principales de la especialización (como se establece en la Motivación), por lo que, dado que la regla de la celosía se puede agregar más adelante, el RFC se apega a la regla de la cadena simple por ahora.

@qnighy , gracias por pensarlo.

Y también es un problema real en su caso porque la implicación de HasLabel de WithLabel<WithLabel<A>> podría interpretarse de dos maneras.

Esto es cierto si no consideramos el impl<T> HasLabel for WithLabel<T> como más especializado que impl<T> HasLabel for T para la entrada de WithLabel<WithLabel<A>> . La parte del RFC que pegó cubre eso, sin embargo, creo que esta es una limitación seria y pediría que se reconsidere el soporte para este caso de uso en la primera versión de esta extensión.

Mientras tanto, estaba jugando con negative trait impls porque en realidad pueden resolver los puntos cubiertos. Creé un código que no tiene los problemas que describe (a menos que me falte algo), sin embargo, todavía no se compila. Esta vez, no entiendo de dónde provienen las restricciones mencionadas en el error, ya que la resolución no debe ser ambigua.

Lo bueno es que en realidad todo se compila ahora (incluidas las especializaciones) pero no el uso de test(v1) :

#![feature(specialization)]
#![feature(optin_builtin_traits)]

use std::ops::Deref;
use std::ops::DerefMut;

// =================
// === WithLabel ===
// =================

struct WithLabel<T>(String, T);

auto trait IsNotWithLabel {}
impl<T> !IsNotWithLabel for WithLabel<T> {}

pub trait HasLabel {
    fn label(&self) -> &String;
}

impl<T> HasLabel for WithLabel<T> {
    fn label(&self) -> &String { 
        &self.0
    }
}

impl<T> HasLabel for T
where T: Deref + IsNotWithLabel, <Self as Deref>::Target : HasLabel {
    default fn label(&self) -> &String { 
        self.deref().label() 
    }
}

impl<T> Deref for WithLabel<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// ==============
// === WithID ===
// ==============

struct WithID<T>(i32, T);

pub trait HasID {
    fn id(&self) -> &i32;
}

impl<T> HasID for WithID<T> {
    fn id(&self) -> &i32 { 
        &self.0
    }
}

auto trait IsNotWithID {}
impl<T> !IsNotWithID for WithID<T> {}

impl<T> HasID for T
where T: Deref + IsNotWithID, <Self as Deref>::Target : HasID {
    default fn id(&self) -> &i32 { 
        self.deref().id() 
    }
}

impl<T> Deref for WithID<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.1
    }
}

// =============
// === Usage ===
// =============

struct A(i32);

type X = WithLabel<WithID<A>>;

fn test<T: HasID + HasLabel> (t: T) {
    println!("{:?}", t.label());
    println!("{:?}", t.id());
}

fn main() {
    let v1 = WithLabel("label1".to_string(), WithID(0, A(1)));
    test(v1);
}

Mientras tanto, puede explotar RFC1268 overlapping_marker_traits para permitir la superposición de rasgos no marcadores, pero este truco requiere tres rasgos más (uno para pasar por los rasgos del marcador, dos para volver a adquirir datos borrados mediante la especialización).

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b66ee0021db73efaaa5d46edfb4f3990

@qnighy He creado un problema separado sobre este error: https://github.com/rust-lang/rust/issues/66041

Ok, acabo de descubrir que auto traits nunca será la solución aquí, ya que (según https://doc.rust-lang.org/nightly/unstable-book/language-features/optin-builtin-traits. html) se propagan a todos los campos en una estructura:

Los rasgos automáticos, como Enviar o Sincronizar en la biblioteca estándar, son rasgos de marcador que se implementan automáticamente para cada tipo, a menos que el tipo, o un tipo que contiene, se haya excluido explícitamente a través de un impl negativo.

EDITAR
@qnighy de alguna manera pasé por alto que proporcionaste un enlace al patio de juegos. ❤️ Muchas gracias por ello. Funciona y estoy sorprendido por lo hacky que es esta solución. ¡Es increíble que podamos expresar eso actualmente y espero que esta posibilidad no desaparezca en el futuro!

En tal situación, overlapping marker traits son el único truco que podemos usar ahora, pero creo que sería bueno permitir en el futuro algún tipo de solución más fácil para expresar tipos opacos (como se describe en mi publicación anterior: https : //github.com/rust-lang/rust/issues/31844#issuecomment-549023367).

Un ejemplo muy simple (simplificación del ejemplo anterior ) que falla:

trait Trait<T> {}
impl<T> Trait<T> for T {}
impl<T> Trait<()> for T {}

No creo que esto afecte al problema identificado con las reglas de celosía , sin embargo, ¿quizás un solucionador demasiado simplista cree que sí?

Sin esto, la implementación actual es inútil para mis propósitos. Si se permitiera lo anterior, creo que también sería posible implementar From en los tipos de envoltura (aunque no estoy seguro de Into ).

Para cualquier cuerpo que aún no lo sepa: existe este truco asombroso descubierto por dtolnay que permite usar la especialización (muy limitada) en óxido estable

No estoy seguro de si esto ya se ha abordado, pero los rasgos con implementaciones predeterminadas para sus métodos deben redefinirse solo para que puedan marcarse como default . Ejemplo;

trait Trait {
    fn test(&self) { println!("default implementation"); }
}

impl<T> Trait for T {
    // violates DRY principle
    default fn test(&self) { println!("default implementation"); }
}

Propongo la siguiente sintaxis para arreglar esto (si es necesario arreglarlo):

impl<T> Trait for T {
    // delegates to the already existing default implementation
    default fn test(&self);
}

Movido a # 68309

@jazzfool Por favor, vuelva a presentar esto como un problema (se aplica generalmente a todos los que hacen preguntas similares aquí) y escríbame en ese.

¿Existe un enfoque para probar la especialización? Por ejemplo, al escribir una prueba que verifica la exactitud de una especialización, primero necesita saber si la especialización que está tratando de probar se aplica realmente en lugar de la implementación predeterminada.

@ the8472 ¿te refieres a probar el compilador , o te refieres a probar en tu propio código? Ciertamente, puede escribir pruebas unitarias que se comporten de manera diferente (es decir, llamar a fn y ver si obtiene la variante especializada). ¿Quizás esté diciendo que las dos variantes son equivalentes, excepto que se supone que una es más rápida y, por lo tanto, no está seguro de cómo probar qué versión está obteniendo? En ese caso, estoy de acuerdo, no sé cómo puedes probar eso ahora mismo.

Supongo que podrías hacer algún otro rasgo con el mismo conjunto de implicaciones, pero donde los fns se comportan de manera diferente, solo para tranquilizarte.

¿Quizás esté diciendo que las dos variantes son equivalentes, excepto que se supone que una es más rápida y, por lo tanto, no está seguro de cómo probar qué versión está obteniendo? En ese caso, estoy de acuerdo, no sé cómo puedes probar eso ahora mismo.

Puedes probar eso usando una macro. Estoy un poco oxidado con mi Rust, pero algo por el estilo ...

[#cfg(test)]
static mut SPECIALIZATION_TRIGGERED : bool = false;

[#cfg(test)]
macro_rules! specialization_trigger {
    () =>  { SPECIALIZATION_TRIGGERED = true; };
}

[#cfg(not(test))]
macro_rules! specialization_trigger {
    () => {};
}

Luego use specialization_trigger!() en el impl especializado, y en las pruebas use assert!(SPECIALIZATION_TRIGGERED);

[#cfg(test)]
static mut SPECIALIZATION_TRIGGERED : bool = false;
...

Querrá usar thread_local! { static VAR: Cell<bool> = Cell::new(false); } lugar de static mut porque de lo contrario la variable podría establecerse en un hilo de caso de prueba y leerse por error desde otro hilo. Además, recuerde restablecer la variable al comienzo de cada prueba, de lo contrario obtendrá el true de la prueba anterior.

Tengo una pregunta sobre el texto RFC, espero que este sea un buen lugar para preguntar.

En la sección de reutilización , se da este ejemplo:

trait Add<Rhs=Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
    fn add_assign(&mut self, rhs: Rhs);
}

// the `default` qualifier here means (1) not all items are implied
// and (2) those that are can be further specialized
default impl<T: Clone, Rhs> Add<Rhs> for T {
    fn add_assign(&mut self, rhs: Rhs) {
        let tmp = self.clone() + rhs;
        *self = tmp;
    }
}

Me pregunto cómo se supone que esto debe verificar el tipo, dado que tmp tiene el tipo Self::Output y no se sabe nada sobre este tipo asociado. El texto de RFC no parece explicar eso, al menos no cerca de donde se da el ejemplo.

¿Hay algún mecanismo aquí que se suponga que funcione?

¿Podría restringirse ese valor predeterminado where T: Add<Output = T> ? ¿O es un ciclo de causalidad?

@RalfJung Estoy de acuerdo en que parece mal.

Tengo una pregunta sobre el procedimiento: ¿qué tan significativo es este problema y qué tan significativo es para las personas probar esta función? Según tengo entendido, la implementación actual no es sólida e incompleta y probablemente será reemplazada por completo por tiza u otra cosa. Si eso es cierto, ¿deberíamos simplemente des-implementar esta característica y otras (por ejemplo, GAT) hasta que se puedan rehacer correctamente?

No de-implemente. Roto, defectuoso e incompleto todavía permite la experimentación.

Si eso es cierto, ¿deberíamos simplemente des-implementar esta característica y otras (por ejemplo, GAT) hasta que se puedan rehacer correctamente?

No lo hagas, PyO3 (biblioteca de enlaces de Python) actualmente depende de la especialización. Ver https://github.com/PyO3/pyo3/issues/210

¿No depende una buena cantidad de std de ello también? Pensé que recordaba haber visto muchas implementaciones internas especializadas para cosas relacionadas con vectores y cadenas. No es que eso evite la desinstalación, solo que no sería tan simple como eliminar las secciones relevantes del verificador de tipos.

@Lucretiel sí, muchas optimizaciones útiles (especialmente alrededor de iteradores) dependen de la especialización, por lo que sería una gran regresión de rendimiento implementarla.

Por ejemplo, FusedIterator y TrustedLen son inútiles sin especialización.

PyO3 (biblioteca de enlaces de Python) actualmente depende de la especialización

Eso da miedo, debido a las partes "defectuosas". La biblioteca estándar tenía errores críticos de solidez debido al uso incorrecto de la especialización. ¿Qué tan seguro estás de que no tienes los mismos errores? Intente usar min_specialization lugar, es de esperar que al menos sea menos incorrecto.

Quizás specialization debería recibir una advertencia similar a const_generics diga "esta función está incompleta, defectuosa y rota, no la use en producción ".

muchas optimizaciones útiles (especialmente alrededor de iteradores) dependen de la especialización, por lo que sería una gran regresión de rendimiento implementarla.

En estos días dependen de min_specialization (ver, por ejemplo, https://github.com/rust-lang/rust/pull/71321), que tiene los mayores agujeros de solidez tapados.

@nikomatsakis

Estoy de acuerdo en que parece mal.

¿Alguna idea de cuál es el código previsto? Primero pensé que default impl estaba destinado a establecer también type Output = Self; , pero eso es realmente imposible en el RFC propuesto . Entonces, ¿quizás la intención era tener un límite de Output = T ?

@RalfJung ¿Hay alguna posibilidad de que min_specialization se documenten? Siento que es más arriesgado usar una función completamente indocumentada en una caja que una que tiene errores de solidez conocidos (y posiblemente desconocidos). Ninguno es bueno, pero al menos el último no es solo componentes internos del compilador.

No pude encontrar ninguna mención de min_specialization de este problema de seguimiento fuera del PR # 71321, y según el libro Unstable, este es el problema de seguimiento de esa función.

Tampoco sé mucho sobre esa función, acabo de ver las correcciones de solidez de libstd. Se introdujo en https://github.com/rust-lang/rust/pull/68970, que explica algunas cosas más al respecto.

@matthewjasper, ¿ tendría sentido documentar esto un poco más y pedirles a los usuarios nocturnos de feature(specialization) que migren?

Parece que al menos debería haber una advertencia. Parece que esta función está descaradamente rota y es peligrosa de usar en su estado actual.

Creo que specialization podría convertirse en un sinónimo de min_specialization , pero agregue otra función unsound_specialization si es necesario para proyectos existentes, como PyO3 o lo que sea. Ahorraría a cualquiera que solo use min_specialization un esfuerzo considerable, pero cualquier otra persona recibe el mensaje de error y puede buscar aquí el nuevo nombre.

@RalfJung

¿Alguna idea de cuál es el código previsto?

Bueno, en algún momento, habíamos estado considerando un modo en el que los valores predeterminados pudieran depender entre sí. Así que imagino que en ese punto lo siguiente habría funcionado:

default impl<T: Clone, Rhs> Add<Rhs> for T {
    type Output = T;

    fn add_assign(&mut self, rhs: Rhs) {
        let tmp = self.clone() + rhs;
        *self = tmp;
    }
}

La advertencia era que si anulaba cualquier miembro de impl , tenía que anularlos a todos . Más tarde nos retiramos de esta idea, y luego lanzamos varias iteraciones, como "grupos predeterminados" (que también funcionarían aquí), y finalmente no adoptamos ninguna solución porque pensamos que podríamos llegar a ella más tarde una vez que lidiemos con el otro, er, problemas urgentes (cc # 71420).

No lo hagas, PyO3 (biblioteca de enlaces de Python) actualmente depende de la especialización. Ver PyO3 / pyo3 # 210

Responsable de PyO3 aquí: estamos a favor de alejarnos de la especialización para poder llegar a Rust estable. ¿Es probable que min_specialization se estabilice antes de que se complete el resto de la especialización?

Creo que hubo una discusión sobre tratar de estabilizar min_specialization en la reunión de diseño de idioma de planificación de la edición 2021 (está en youtube; lo siento, estoy en mi teléfono, o intentaría encontrar un enlace). Aunque olvidé lo que dijeron al respecto

Creo que hubo una discusión sobre tratar de estabilizar min_specialization en la reunión de diseño de idioma de planificación de la edición 2021 (está en youtube; lo siento, estoy en mi teléfono, o intentaría encontrar un enlace). Aunque olvidé lo que dijeron al respecto

Creo que este es el enlace de YouTube correcto: https://youtu.be/uDbs_1LXqus
(también en mi teléfono)

Sí, eso es todo. Aquí hay un enlace a la discusión específica: https://youtu.be/uDbs_1LXqus?t=2073

He estado usando #[min_specialization] en una biblioteca experimental que he estado desarrollando, así que pensé en compartir mis experiencias. El objetivo es utilizar la especialización en su forma más simple: tener algunos casos estrechos con implementaciones más rápidas que el caso general. En particular, tener algoritmos criptográficos en el caso general que se ejecuten en tiempo constante pero luego si todas las entradas están marcadas Public tener una versión especializada que se ejecute en tiempo variable más rápido (porque si son públicas no preocuparse por filtrar información sobre ellos a través del tiempo de ejecución). Además, algunos algoritmos son más rápidos dependiendo de si el punto de la curva elíptica está normalizado o no. Para que esto funcione, comenzamos con

#![feature(rustc_attrs, min_specialization)]

Luego, si necesita hacer un rasgo de _prédico de especialización_ como se explica en la especialización mínima máxima , marque la declaración del rasgo con #[rustc_specialization_trait] .

Toda mi especialización se realiza en este archivo y aquí hay un ejemplo de un rasgo de predicado de especialización.

La función funciona y hace exactamente lo que necesito. Obviamente, se utiliza un marcador interno rustc y, por lo tanto, es propenso a romperse sin previo aviso.

El único comentario negativo es que no creo que la palabra clave default tenga sentido. Básicamente, lo que default significa ahora mismo es: "este impl es especializable, así que interprete los impl que cubren un subconjunto de este como especialización del mismo en lugar de un impl en conflicto". El problema es que conduce a un código de aspecto muy extraño:

https://github.com/LLFourn/secp256kfun/blob/6766b60c02c99ca24f816801fe876fed79643c3a/secp256kfun/src/op.rs#L196 -L206

Aquí, el segundo impl está especializado en el primero, pero también es default . El significado de default parece haberse perdido. Si observa el resto de las impls, es bastante difícil averiguar qué impls se especializan en cuáles. Además, cuando hice una implicación errónea que se superponía con una existente, a menudo era difícil averiguar dónde me equivoqué.

Me parece que esto sería más sencillo si todo fuera especializable y cuando te especializas en algo declaras precisamente en qué implica te especializas. Transformando el ejemplo en el RFC en lo que tenía en mente:

impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
{
    // no need for default
    fn extend(&mut self, iterable: T) {
        ...
    }
}

// We declare explicitly which impl we are specializing repeating all type bounds etc
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
    // And then we declare explicitly how we are making this impl narrower with ‘when’.
    // i.e. This impl is like the first except replace all occurances of ‘T’ with ‘&'a [A]’
    when<'a> T = &'a [A]
{
    fn extend(&mut self, iterable: &'a [A]) {
        ...
    }
}

Gracias por la respuesta.

Mi comentario aquí , específicamente el artículo 6, proporciona un caso concreto en la biblioteca estándar donde puede ser deseable tener una especialización que sea solo parcialmente reemplazable: IndexSet necesitaría un tipo Output distinto porque IndexSet podría implementarse sin Index , pero probablemente no queremos permitir que los dos tipos coexistan con diferentes tipos Output . Dado que IndexSet podría tener una implementación predeterminada en términos de IndexMut , sería razonable permitir la especialización del método index_set sin permitir la especialización de Output .

Tengo dificultades con los videos, así que no puedo buscar el video vinculado; sin embargo, tengo una pregunta sobre #[min_specialization] . Tal como está, hay un atributo rustc_unsafe_specialization_marker en rasgos como FusedIterator que brindan sugerencias de optimización, para que puedan especializarse. @matthewjasper escribió:

Esto no es correcto, pero lo permitimos a corto plazo porque no puede causar uso después de liberaciones con código puramente seguro de la misma manera que lo hacen los métodos especializados en rasgos.

Asumo que el plan es implementar la propuesta de @aturon y agregar una modalidad de especialización para rasgos como estos ( where specialize(T: FusedIterator) ). Pero actualmente, parece que cualquier código puede especializarse en estos rasgos . Si se estabiliza tal como está, las personas podrían escribir especializaciones estables que dependan de él, lo que significa que esta falta de solidez se estabilizaría.

Entonces, ¿la especialización en estos rasgos también debería limitarse a la biblioteca estándar? ¿La biblioteca estándar obtiene suficientes beneficios de poder especializarse en ellos?

Si se estabiliza tal como está, las personas podrían escribir especializaciones estables que dependan de él, lo que significa que esta falta de solidez se estabilizaría.

Tengo entendido que min_specialization tal cual no está destinado a la estabilización.

En segundo lugar, me gustaría tener algún tipo de marcador sobre las implicaciones especializadas. Ha habido bastantes casos de código en rustc y la biblioteca estándar no hace lo que parece porque no hay forma de saber que la especialización está realmente sucediendo:

Una especialización innecesaria de Copy :
https://github.com/rust-lang/rust/pull/72707/files#diff -3afa644e1d09503658d661130df65f59L1955

Una "especialización" que no es:
https://github.com/rust-lang/rust/pull/71321/files#diff -da456bd3af6d94a9693e625ff7303113L1589

Una implementación generada por una macro a menos que se pase una bandera que anule una implícita predeterminada:
https://github.com/rust-lang/rust/pull/73851/files?file-filters%5B%5D=#diff -ebb36dd2ac01b28a3fff54a1382527ddR124

@matthewjasper, el último enlace no parece vincular a ningún fragmento específico.

No estoy seguro de si este es un objetivo explícito, pero AIUI, el hecho de que las implicaciones especializadas no estén marcadas le brinda una forma de evitar cambios rotos en las implicaciones generales. Un nuevo default impl<T> Trait for T no entra en conflicto con las implicaciones posteriores, sino que simplemente se especializan.

¿Podría ser una advertencia solo para que no se marque?

Ha habido bastantes casos de código en rustc y la biblioteca estándar no hace lo que parece porque no hay forma de saber que la especialización está realmente sucediendo.

Mi experiencia con Java es similar (aunque no exactamente análoga). Puede ser difícil averiguar qué subclase de una clase se está ejecutando realmente ...

Sin embargo, también querríamos un marcador en las implicaciones especializables, también para mayor claridad al leer, ¿verdad?

Podríamos poner los marcadores en ambos lugares, lo que luego mejora los mensajes de error o advertencia rustc porque ahora saben si se desea especialización y pueden apuntar al otro lugar si existe.

Si una caja ascendente agrega un impl, entonces, además de simplemente actualizar, una caja descendente podría emplear trucos que permitan compilar tanto en la versión nueva como en la anterior, no estoy seguro de que sea beneficioso.

Creo que la diferencia puede ser demasiado grande para mostrar el cambio. Apunta a esto: https://github.com/rust-lang/rust/blob/fb818d4321dee29e1938c002c1ff79b0e7eaadff/src/librustc_span/def_id.rs#L124

Re: Impls general, están rompiendo cambios de todos modos:

  • Es posible que se superpongan parcialmente con un impl descendente, lo cual no está permitido
  • La coherencia puede asumir su inexistencia de formas más sutiles (razón por la cual las implicaciones de reserva se agregaron internamente)
  • Las implicaciones especializadas tienen que ser siempre aplicables, lo que significa:

    • Rompemos las implicaciones de las personas (lo que hace min_specialization ).

    • Les exigimos que de alguna manera anoten sus límites de rasgos como siempre aplicables cuando sea necesario.

    • Realizamos el cambio siempre aplicable para ellos implícitamente y potencialmente introducimos errores sutiles en tiempo de ejecución cuando ahora se aplica el impl predeterminado.

@cuviper en realidad, siento que todavía hay casos extremos en torno a la adición de nuevas implicaciones generales, incluso con especialización. Recuerdo que estaba tratando de averiguar qué se necesitaría para permitirnos agregar un impl<T: Copy> Clone for T { } imp . Escribí esta publicación de blog al respecto , en cualquier caso ... pero ahora no puedo recordar cuál fue mi conclusión .

Independientemente, podríamos convertirlo en una advertencia de pelusa para no tener una anotación #[override] .

Dicho esto, si pudiéramos hacer que el usuario declarara qué implicaciones se están especializando (no tengo idea de cómo lo haríamos), simplificaría algunas cosas. En este momento, el compilador tiene que deducir las relaciones entre las implicaciones y eso siempre es un poco complicado.

Uno de los elementos pendientes que tenemos que hacer en el proyecto de tiza es intentar volver atrás y explicar cómo se debe expresar la especialización allí.

Ha habido bastantes casos de código en rustc y la biblioteca estándar no hace lo que parece porque no hay forma de saber que la especialización está realmente sucediendo.

Mi experiencia con Java es similar (aunque no exactamente análoga). Puede ser difícil averiguar qué subclase de una clase se está ejecutando realmente ...

En mayo, propuse una alternativa a la especialización en IRLO que en realidad no depende de las implicaciones superpuestas, sino que permite que una única implícita where match en su parámetro de tipo:

impl<R, T> AddAssign<R> for T {
    fn add_assign(&mut self, rhs: R) where match T {
        T: AddAssignSpec<R> => self.add_assign(rhs),
        T: Add<R> + Copy => *self = *self + rhs,
        T: Add<R> + Clone => { let tmp = self.clone() + rhs; *self = tmp; }
    }
}

Las cajas en sentido descendente pueden usar ese impl para implementar la "especialización", porque por convención, tal impl para el rasgo Trait primero coincidiría con los tipos que implementan otro rasgo TraitSpec , y tipos descendentes podría implementar ese rasgo para anular el comportamiento genérico:

// Crate upstream
pub trait Foo { fn foo(); }
pub trait FooSpec { fn foo(); }

impl<T> Foo for T {
    fn foo() where T {
        T : FooSpec => T::foo(),
        _ => { println!("generic implementation") }
    }
}

fn foo<T : Foo>(t: T) {
    T::foo()
}

// crate downstream
struct A {}
struct B {}

impl upstream::FooSpec for A {
    fn foo() { println!("Specialized"); }
}

fn main() {
    upstream::foo(A); // prints "specialized"
    upstream::foo(B); // prints "generic"
}

Esta formulación le da más control al upstream para elegir el orden de las implicaciones aplicables, y como esto es parte de la firma del rasgo / función, esto aparecería en la documentación. En mi opinión, esto evita la "persecución implícita" para saber qué rama es realmente aplicable, ya que el orden de resolución es explícito.

Esto podría hacer que los errores relacionados con la vida útil y la igualdad de tipos sean más evidentes, ya que solo en sentido ascendente podría cumplirlos al implementar la especialización (ya que en sentido descendente solo se implementa un "rasgo de especialización").

Los inconvenientes de esta formulación son que es una ruta muy diferente a la de la RFC, y que se está implementando desde 2016, y que al menos algunas personas en el hilo expresaron preocupaciones de que no sería tan expresiva y / o intuitiva como la actual. característica de especialización (me parece que la "coincidencia de tipos" es bastante intuitiva, pero estoy sesgado cuando propongo la formulación).

La sintaxis del partido podría tener otro beneficio (sintáctico): si en algún momento en el futuro se extendiera con guardias de partido evaluados const, entonces no sería necesario hacer gimnasia de tipos para expresar límites condicionados a las expresiones const. Por ejemplo, se podrían aplicar especializaciones basadas en size_of , align_of , needs_drop o tamaños de matriz.

@dureuill gracias por la información! De hecho, es una idea interesante. Una preocupación que tengo es que no necesariamente resuelve algunos de los otros casos de uso anticipados para la especialización, especialmente el caso de "comportamiento de refinamiento incremental" como lo describe @aturon en esta publicación de blog . Aún así, vale la pena tenerlo en cuenta.

@dureuill La idea es realmente interesante y puede tener mucho potencial, pero la alternativa no siempre es un intercambio equivalente.
La razón por la que no creo que lo sea es que no se da la oportunidad de reemplazar completamente la implementación más general. También otro problema podría ser el hecho de que en realidad no tenemos soporte para todas las características presentes en la RFC de sintaxis where de la que depende su sugerencia.
La sugerencia es intrigante, por lo que tal vez podría tener su propio RFC como una característica separada en lugar de un competidor de la especialización porque ambos serían útiles y no veo ninguna razón por la que no puedan vivir juntos.

@ the8472 @nikomatsakis , @ Dark-Legion: ¡Gracias por los comentarios positivos! Intento responder a algunos de sus comentarios en el hilo de IRLO , ya que no quiero ser demasiado ruidoso con el tema del seguimiento (lo siento por cada uno de ustedes que esperaba noticias sobre especialización y acaba de encontrar mis divagaciones: enrojecido :).

Puedo abrir un RFC separado si logro escribir algo publicable. Mientras tanto, estoy muy abierto a recibir comentarios sobre el

También estoy a favor de tener algún tipo de marcador sobre las implicaciones especializadas.

Se acerca la edición 2021, que nos permite reservar más palabras clave (como specialize ). En cuanto a la complejidad y la historia de esta función, no creo que se estabilice antes del lanzamiento de la edición 2021 (no dude en demostrar que estoy equivocado) lo que significa, en mi opinión, jugar con (una) nueva palabra clave (s ) es razonable.

De lo contrario, la única palabra clave existente que parece ... bueno ... adecuada como marcador, podría ser super ?

Resumen reutilizando el ejemplo de @LLFourn de https://github.com/rust-lang/rust/issues/31844#issuecomment -639977601:

  • super (ya reservado, pero también podría malinterpretarse como alternativa a default )
super impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • specialize
specialize impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • spec (abreviatura de specialize como impl es por implement ) ( preocupación válida planteada por @ssokolow en https://github.com/rust-lang / rust / issues / 31844 # issuecomment-690980762)
spec impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>
  • override (ya reservado, gracias @ the8472 https://github.com/rust-lang/rust/issues/31844#issuecomment-691042082)
override impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A>

las palabras clave ya reservadas se encuentran aquí

o spec (abreviatura de specialize como impl es para implement )

"spec" ya es más familiar para la gente como una abreviatura de "especificación" (por ejemplo, "La especificación HTML 5"), así que no creo que sea una buena forma abreviada de "especializar".

override es una palabra clave reservada, supongo que estaba destinada a funciones, por lo que podría usarse para un bloque implícito.

specialize También depende de la configuración regional; como australiano, es una especialización para mí, por lo que el uso de 'spec' elimina la ambigüedad de la configuración regional.

specialize También depende de la configuración regional; como australiano, es una especialización para mí, por lo que el uso de 'spec' elimina la ambigüedad de la configuración regional.

eso funciona, excepto que 'spec' es una abreviatura común para la especificación, así que creo que usar 'spec' para significar especialización sería confuso. Incluso si las palabras se escriben de manera diferente en Australia, todos pueden entender qué palabra se entiende si se escribe con una 'z' o una 's'.

Como canadiense, debo decir que especializarse / especializarse no es la única palabra utilizada en la programación que varía según la configuración regional.

Aquí, usamos "color", pero siempre me hace tropezar en las raras ocasiones en que un lenguaje de programación o biblioteca lo usa en lugar de "color". Para bien o para mal, el inglés estadounidense es un estándar de facto en el diseño de API y, con palabras como color / color, elegir favorecer una ortografía sobre la otra es inevitable sin ser realmente artificial.

Dado lo mucho que espero que "spec" signifique "especificación", creo que esta es otra situación en la que deberíamos considerar la ortografía del inglés americano como la peor opción.

Puede que sea el defacto, pero eso no significa que esté bien usarlos. Me encuentro haciendo importaciones como "usar color como color", por ejemplo. Siempre me tropiezo con el s vs z también. Creo que dada la actitud positiva de Rust hacia la inclusión y la accesibilidad, tiene sentido elegir términos de lenguaje que no dependan de la ubicación, ya que las frustraciones de los pequeños usuarios como color / color y s / z se acumulan.

Estoy de acuerdo en principio. Solo soy escéptico de que, para este caso, haya una opción de configuración regional neutral que no cause más problemas de los que resuelve.

Como hablante nativo que no es inglés, me parece algo divertido que los hablantes nativos de inglés se quejen de los u adicionales como una barrera para la inclusión. Imagínense cómo sería si todo no estuviera escrito un poco raro, sino en un idioma completamente diferente.

Dicho de otra manera: cada término utilizado en Rust depende de la configuración regional.

Para bien o para mal, las cosas en Rust se escriben en inglés estadounidense. Para muchos aquí esto significa trabajar en su segundo o tercer idioma; para otros significa tener que ajustar un poco la ortografía. Esto es lo que se necesita para que un montón de personas trabajen juntas. Creo que el beneficio de tratar de elegir palabras que se escriban de la misma manera en muchas variantes del inglés es marginal en comparación con elegir un término bueno y sin ambigüedades, y spec es ambiguo, como se señaló anteriormente.

utilizar special como palabra clave?

Alternativa, cree dos palabras clave: specialize y specialise y conviértalas en equivalentes ...

(O ustedes, los no estadounidenses divertidos, pueden aprender a escribir correctamente: nosotros: 😂)

No puedo hablar era lo que hacen la mayoría de los idiomas, pero CSS usa la ortografía del inglés americano para todo lo nuevo. Como anécdota, el inglés americano también parece usarse con más frecuencia en la programación.

@ mark-im Desafortunadamente, esa es una pendiente resbaladiza que lleva a argumentos de que Rust debería tener conjuntos alternativos de palabras clave en todos los idiomas principales de los que los estudiantes podrían provenir.

También complica innecesariamente el lenguaje tener múltiples sinónimos para palabras clave, ya que las personas y los analizadores solo están acostumbrados a la idea de que los espacios en blanco pueden variar así.

(Sin mencionar que potencialmente generaría un impulso para sinónimos equivalentes en bibliotecas, lo que luego requeriría un trabajo de diseño e implementación de rustdoc para evitar que sean netamente negativos).

En lugar de discutir sobre en qué dialecto del inglés queremos que esté este identificador, ¿quizás podamos comprometernos y ponerlo en hebreo ?

@ssokolow, aunque el argumento de pendiente resbaladiza en general no es un argumento sólido para usar, estoy de acuerdo contigo en este caso. Se podría argumentar que varios idiomas están bien, pero hay al menos dos razones por las que no lo es:

  • Algunas palabras en diferentes idiomas tienen el mismo aspecto pero significan cosas diferentes (no puedo dar un ejemplo relacionado con la programación en este momento, pero un ejemplo aleatorio: a en eslovaco es and en inglés)
  • Las personas tendrán grandes problemas para leer el código en otro idioma, incluso si conocen el idioma . Lo sé por experiencia. (En pocas palabras: tuve un gran problema para entender algunos textos con términos traducidos directamente del inglés a mi "lengua materna" en la estafa de educación

Ahora, trabajando al revés, ¿por qué deberían preferirse los diferentes dialectos del inglés si no otros idiomas? No veo sentido. La consistencia (todo es inglés de EE. UU.) Parece más simple, más fácil de entender y menos propensa a errores.

Dicho todo esto, estaría muy feliz con "¿Te refieres a XXX?" enfoque de mensaje de error. Las palabras neutrales que no tienen otros problemas también están bien.

Al menos nadie necesita hablar de fútbol en código. ;)

Alrededor del 70% de los hablantes nativos de inglés viven en países que utilizan la ortografía estadounidense.

También..

"La ortografía -ize a menudo se ve incorrectamente como un americanismo en Gran Bretaña. Ha estado en uso desde el siglo XV, anterior a -ise en más de un siglo. -Ize proviene directamente del griego -ιζειν -izein y del latín -izāre, mientras que ise viene a través del francés -iser. El Oxford English Dictionary (OED) recomienda -ize y enumera la forma -ise como alternativa ".

"Las publicaciones de Oxford University Press (OUP), como A Dictionary of Modern English Usage de Henry Watson Fowler, Hart's Rules y The Oxford Guide to English Usage, también recomiendan -ize. Sin embargo, Pocket Fowler's Modern English Usage de Robert Allan considera la ortografía para ser aceptable en cualquier lugar menos en Estados Unidos ".

árbitro. https://en.wikipedia.org/wiki/American_and_British_English_spelling_differences# -ise, _- ize _ (- isation, _- ization)

Parece que el español y el italiano tienen az o dos, así que ¿no estás seguro de dónde se pone el francés -iser, tal vez del alemán?

¿Se puede mover más bikeshedding en torno a palabras clave específicas y nombres a un hilo interno ? Estoy siguiendo este problema para obtener actualizaciones sobre el progreso de la función, y esta discusión comienza a ser un poco ruidosa.

¿Fue útil esta página
0 / 5 - 0 calificaciones