Rust: Problema de seguimiento para abrazadera RFC

Creado en 26 ago. 2017  ·  101Comentarios  ·  Fuente: rust-lang/rust

Problema de seguimiento para https://github.com/rust-lang/rfcs/pull/1961

PR aquí: # 44097 # 58710
PR de estabilización: https://github.com/rust-lang/rust/pull/77872

HACER:

  • [x] Hacer que RFC pase el período de comentarios final
  • [x] Implementar RFC
  • [] Estabilizar
B-unstable C-tracking-issue Libs-Tracked T-libs

Comentario más útil

Parece que estamos en un lugar bastante malo si algún método que la gente quisiera lo suficiente para definir un rasgo de extensión nunca se pueda agregar a la biblioteca estándar.

Todos 101 comentarios

Tenga en cuenta: Esto rompió Servo y Pathfinder.

cc @ rust-lang / libs, este es un caso similar a min / max , donde el ecosistema ya estaba usando el nombre clamp y, por lo tanto, agregarlo ha causado ambigüedad . Esto es una rotura permitida según la política de semver, pero, sin embargo, está causando problemas posteriores.

Nominando para la reunión de triaje el martes.

¿Alguna idea mientras tanto?

Estoy un poco con @bluss en esto porque sería bueno no repetirlo. "Clamp" es probablemente un gran nombre, pero ¿podríamos evitarlo eligiendo un nombre diferente?

restrict
clamp_to_range
min_max (Porque es como combinar mínimo y máximo).
Estos podrían funcionar. ¿Podemos usar el cráter para determinar qué tan grave es realmente el impacto de clamp ? clamp es bien reconocido en varios idiomas y bibliotecas.

Si pensamos que podríamos necesitar cambiar el nombre, probablemente sea mejor revertir el PR de inmediato y luego probar con más cuidado con el cráter, etc. @Xaeroxe , ¿

Seguro. Nunca antes había usado el cráter, pero puedo aprender.

@Xaeroxe ah lo siento, me refería a conseguir un cambio de relaciones públicas rápidamente. (Estoy de vacaciones hoy, por lo que es posible que necesite a alguien más en bibliotecas, como @BurntSushi o @alexcrichton , para ayudarlo a conseguirlo).

Estoy preparando las relaciones públicas ahora. Diviertete en tus vacaciones!

¿Podría clamp_to_range(min, max) estar compuesto por clamp_to_min(min) y clamp_to_max(max) (con la afirmación adicional de que min <= max ), pero esas funciones también podrían llamarse de forma independiente?

Supongo que esa idea exige un RFC.

Debo decir que he estado trabajando para obtener una función de 4 líneas en la biblioteca estándar durante 6 meses. Estoy un poco agotado. La misma función se fusionó en num en 2 días y eso es lo suficientemente bueno para mí. Si alguien más quiere esto en la biblioteca estándar, adelante, pero no estoy listo para otros 6 meses de esto.

Voy a reabrir esto para que aún se vea la nominación anterior de @aturon .

Creo que esto debería ir como está escrito o la guía sobre los cambios que se pueden hacer deberían actualizarse para evitar perder el tiempo de las personas en el futuro.

Desde un principio quedó muy claro que esto podría provocar la rotura que provocó. Personalmente, lo comparé con ord_max_min que rompió un montón de cosas:

Y la respuesta a eso fue "Se agregó la función Ord::min [...] El equipo de libs decidió hoy que se acepta la rotura". Y esa era una característica de TMTOWTDI con un nombre más común, mientras que clamp no existía ya en std en una forma diferente.

Me parece, subjetivamente, que si se revierte este RFC, la regla real es "Básicamente, no se pueden poner nuevos métodos en rasgos en std, excepto tal vez Iterator ".

Tampoco puede poner nuevos métodos en tipos reales. Considere la situación en la que alguien tenía un "rasgo de extensión" para un tipo en std. Ahora std implementa un método con el rasgo de extensión proporcionado como un método real en este tipo. Entonces esto se vuelve estable, pero este nuevo método todavía está detrás de una bandera de función. Luego, el compilador se quejará de que el método está detrás de una marca de característica y no se puede usar con la cadena de herramientas estable, en lugar de que el compilador elija el método del rasgo de extensión como antes y, por lo tanto, cause una falla en el compilador estable.

También vale la pena señalar: este no es solo un problema de biblioteca estándar. La sintaxis de llamadas a métodos hace que sea realmente difícil evitar la introducción de cambios importantes en cualquier parte del ecosistema.

(meta) Simplemente copiando mi comentario en irlo aquí.

Si estamos de acuerdo en que el número 44438 está justificado,

  1. Es posible que debamos reconsiderar si la rotura de inferencia de tipo garantizado como realmente se puede descartar como XIB.

    Actualmente, los RFC 1105 y 1122 consideran aceptable el cambio de inferencia de tipo, ya que siempre se puede usar UFCS u otras formas de forzar un tipo. Pero a la comunidad realmente no le gusta la rotura causada por # 42496 ( Ord::{min, max} ). Además, # 41336 (primer intento de T += &T ) se cerró "solo" debido a regresiones de inferencia de 8 tipos.

  2. Siempre que agreguemos un método, debe haber un cráter para asegurarnos de que el nombre no exista.

    Tenga en cuenta que agregar métodos inherentes también puede causar fallas en la inferencia: # 41793 fue causado por agregar los métodos inherentes {f32, f64}::from_bits , que entran en conflicto con el método ieee754::Ieee754::from_bits en el rasgo descendente.

  3. Cuando la caja descendente no especificó #![feature(clamp)] , el candidato Ord::clamp nunca debe considerarse (aún se puede emitir una advertencia compatible con el futuro) a menos que esta sea la solución única. Esto permitirá la introducción de nuevos métodos de rasgos que no "romperán instantáneamente", pero el problema volverá cuando se estabilice.

Parece que estamos en un lugar bastante malo si algún método que la gente quisiera lo suficiente para definir un rasgo de extensión nunca se pueda agregar a la biblioteca estándar.

Max / min golpeó un punto particularmente malo con respecto al uso de nombres de métodos comunes en un rasgo común. No es necesario que se aplique lo mismo a la abrazadera.

Todavía quiero decir que sí, pero @sfackler, ¿realmente tenemos que agregar métodos en un rasgo que se implementa tan comúnmente, por diversos tipos? Tenemos que tener cuidado cuando agregamos a la API de todos los tipos que se han incorporado a un rasgo existente.

Con la especialización que viene, no perdemos nada al poner métodos de extensión en un rasgo de extensión.

Una parte molesta es que si el nuevo método std rompe su código: aparecerá mucho antes de que realmente pueda usarlo, ya que es inestable. Aparte de eso, no es tan malo si el conflicto es con un método que tiene el mismo significado.

Creo que darle a esta función un nombre diferente para evitar roturas es una mala solución. Si bien funciona, está optimizando no romper algunas cajas (todas las cuales están optando por la noche) en lugar de optimizar para la legibilidad futura de cualquier código que use esta función.

Tengo algunas preocupaciones de las cuales algunas no me preocupan.

  • el nombre y el sombreado no es ideal, pero funciona
  • para vectores numéricos y matrices, creo que max / min / clamp no son ideales, pero esto se resuelve al no usar Ord en absoluto. A Ndarray le gustaría hacer abrazaderas de argumentos genéricos y de elementos (escalares o matrices), pero Ord no es utilizado por nosotros o bibliotecas similares. Así que no te preocupes.
  • Tipos de compuestos existentes que no son numéricos: BtreeMap obtendrá una abrazadera de método con este cambio. ¿Eso tiene sentido en general? ¿Puede implementar un significado razonable aparte del predeterminado?
  • El modo de llamada por valor no se ajusta a todas las implementaciones. De nuevo, BtreeMap. ¿Debería Clamp consumir 3 mapas y devolver uno de ellos?

tipos compuestos

Creo que tiene tanto sentido como BtreeSet<BtreeSet<impl Ord>>::range . Pero hay casos particulares que incluso podrían ser útiles, como Vec<char> .

modo de llamada por valor

Cuando esto surgió en el RFC, la respuesta fue simplemente usar Cow .

Por supuesto, podría ser algo como esto , reutilizar el almacenamiento:

    fn clamp<T>(mut self, low: &T, high: &T) -> Self
        where T: ?Sized + ToOwned<Owned=Self> + Ord, Self : Borrow<T>
    {
        assert!(low <= high);
        if self.borrow() < &low {
            low.clone_into(&mut self);
        } else if self.borrow() >= &high {
            high.clone_into(&mut self);
        }
        self
    }

Qué https://github.com/rust-lang/rfcs/pull/2111 podría hacer ergonómico llamar.

El equipo de bibliotecas discutió esto durante el triaje hace unos días y la conclusión fue que deberíamos hacer una carrera de cráter para ver cuál es la ruptura en todo el ecosistema para este cambio. Los resultados de eso determinarían qué acciones deberían tomarse precisamente sobre este tema.

Hay una serie de posibles funciones de lenguaje en el futuro que podríamos agregar para facilitar la adición de apis como esta, como rasgos de baja prioridad o usar rasgos de extensión de una manera más sabrosa. Sin embargo, no queremos bloquear esto necesariamente en avances como esos.

¿Alguna vez ocurrió un cráter para esta característica?

Planeo revivir el método clamp() después de fusionar # 48552. Sin embargo, RangeInclusive se estabilizará antes de eso, lo que significa que la alternativa basada en rango ahora es viable para su consideración (que en realidad es la propuesta original, pero se retracta porque ..= era tan inestable 😄):

// Current
trait Ord {
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}
assert_eq!(9.clamp(6, 7), 7);


// Alternative
trait Ord {
    fn clamp(self, range: RangeInclusive<Self>) -> Self { ... }
}
assert_eq!(9.clamp(6..=7), 7);

Un RangeInclusive estable también abre otras posibilidades, como cambiar las cosas (lo que permite algunas posibilidades interesantes con autoref y evita las colisiones de nombres por completo):

impl<T: Ord + Clone> RangeInclusive<T> {
    fn clamp(&self, mut x: T) -> T {
        if x < self.start { x.clone_from(&self.start); }
        else if x > self.end { x.clone_from(&self.end); }
        x
    } 
} 

    assert_eq!((1..=10).clamp(11), 10);

    let strings = String::from("aa")..=String::from("b");
    assert_eq!(strings.clamp(String::from("a")), "aa");
    assert_eq!(strings.clamp(String::from("aaa")), "aaa");

https://play.rust-lang.org/?gist=38def79ba2f3f8380197918377dc66f5&version=nightly

Sin embargo, no he decidido si creo que eso es mejor ...

Usaría un nombre diferente si se usara como método de rango.

Seguramente disfrutaría tener la función más temprano que tarde, sin importar la forma.

¿Cuál es el estado actual?
Me parece que hay consenso en que agregar abrazadera a RangeInclusive podría ser una mejor alternativa.
Entonces, ¿alguien tiene que escribir un RFC?

Probablemente no se necesite un RFC completo en este momento. Solo una decisión sobre qué ortografía elegir:

  1. value.clamp(min, max) (siga el RFC tal cual)
  2. value.clamp(min..=max)
  3. (min..=max).clamp(value)

La opción 2 o 3 permitiría una sujeción parcial más sencilla. Puede hacer value.clamp(min..) o value.clamp(..=max) , sin necesidad de métodos especiales clamp_to_start o clamp_to_end .

@egilburg : ya tenemos esos métodos especiales: clamp_to_start es max y clamp_to_end es min : guiño:

Sin embargo, la consistencia es buena.

@egilburg Rust no admite la sobrecarga directa. Para que la opción 2 funcione con su sugerencia, necesitaremos un nuevo rasgo implementado para RangeInclusive , RangeToInclusive y RangeFrom , que se sienten bastante pesados.

Creo que la opción 3 es la mejor opción.

1 o 2 son los menos sorprendentes. Me quedaría con 1 ya que una gran cantidad de código tendría menos que hacer para reemplazar la implementación local con la estándar.

Creo que deberíamos planear usar _todos_ los tipos de rango * o _ ninguno_ de ellos.

Por supuesto, eso es más difícil para cosas como Range que para RangeInclusive . Pero hay algo bueno en (0.0..1.0).clamp(2.0_f32) => 0.99999994_f32 .

@kennytm Entonces, si abriera una solicitud de extracción con la opción 3, ¿crees que se fusionaría?
¿O qué opinas sobre cómo proceder a continuación?

@EdorianDark Para esto, necesitaremos preguntar a @ rust-lang / libs 😃

Personalmente, me gusta la opción 2, con RangeInclusive solamente. Como se mencionó, la "sujeción parcial" ya existe con min y max .

Estoy de acuerdo con @SimonSapin , aunque también estaría bien con la opción 1. Con la opción 3, probablemente no usaría la función porque me parece al revés. En los otros lenguajes / bibliotecas con abrazadera que @kennytm encuestó anteriormente , 5 de 7 (todos menos Swift y Qt) tienen el valor primero, luego el rango.

¡La abrazadera está ahora en master otra vez!

Estoy satisfecho, aunque todavía estoy tratando de averiguar qué cambio hizo que esto fuera aceptable ahora, mientras que no estaba en el n. ° 44097.

Ahora tenemos un período de advertencia debido a # 48552, en lugar de romper instantáneamente la inferencia incluso antes de estabilizar.

¡Qué buenas noticias, gracias!

@kennytm Solo quiero agradecerle por el trabajo de campo que hizo para hacer realidad # 48552, y @EdorianDark gracias por su interés en esto y por implementarlo. Es maravilloso ver que esto finalmente se fusionó.

https://rust.godbolt.org/z/JmLWJi

pub fn clamped(a: f32) -> f32 {
   a.clamp(0.,255.)
}

Compila para:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm1, xmm0
  vmovss xmm1, dword ptr [rip + .LCPI0_0]
  vminss xmm0, xmm1, xmm0

lo cual no es tan malo (se usan vmaxss y vminss ), pero:

pub fn maxmined(a: f32) -> f32 {
   (0f32).max(a).min(255.)
}

usa una instrucción menos:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm0, xmm1
  vminss xmm0, xmm0, dword ptr [rip + .LCPI1_0]

¿Es inherente a la implementación de la abrazadera o simplemente una peculiaridad de la optimización LLVM?

@kornelski clamp ing a NAN se supone que conserva ese NAN , que ese maxmined no hace, porque max / min preservar el _no_- NAN .

Sería genial encontrar una implementación que cumpla con las expectativas de NAN y sea más corta. Y sería bueno que las pruebas de documentación mostraran el manejo de NAN. Parece que el PR original tenía algunos:

https://github.com/rust-lang/rust/blob/b762283e57ff71f6763effb9cfc7fc0c7967b6b0/src/libstd/f32.rs#L1089 -L1094

¿Por qué los flotadores de sujeción entran en pánico si el valor mínimo o máximo es NaN? Cambiaría la aserción de assert!(min <= max) a assert!(!(min > max)) , de modo que un mínimo o máximo de NaN no tenga ningún efecto, al igual que en los métodos max y min.

NAN para min o max en la abrazadera es probablemente indicativo de un error de programación, y pensamos que era mejor entrar en pánico antes que posiblemente enviar datos no bloqueados a IO. Si no desea un límite superior o inferior, esta función no es para usted.

Siempre puede usar INF y -INF si no desea un límite superior o inferior, ¿verdad? Lo que también tiene sentido matemático, a diferencia de NaN. Pero la mayoría de las veces es mejor usar max y min para eso.

@Xaeroxe Gracias por la implementación.

¿Quizás esto podría ir en la próxima edición, si rompe el código estable?

Una cosa que IMO vale la pena considerar con más detalle es la sujeción unilateral de f32 / f64 . La discusión parece haber tocado este tema brevemente, pero realmente no lo consideró en detalle.

En la mayoría de los casos, si la entrada a una abrazadera unilateral es NAN, es más útil que el resultado sea NAN que que el resultado sea el límite de sujeción. Entonces, las funciones existentes f32::min y f64::max no funcionan bien para este caso de uso. Necesitamos funciones separadas para la sujeción unilateral. (Ver rust-num / num-traits # 122).

La razón por la que menciono esto es que afecta el diseño del clamp de dos lados, ya que sería bueno que las abrazaderas de dos lados y un lado tuvieran una interfaz consistente. Algunas opciones son:

  1. input.clamp(min, max) , input.clamp_min(min) y input.clamp_max(max)
  2. input.clamp(min..=max) , input.clamp(min..) , input.clamp(..=max)
  3. input.clamp(min, max) , input.clamp(min, std::f64::INFINITY) , input.clamp(std::f64::NEG_INFINITY, max)

Con la implementación actual ( min y max como parámetros f32 / f64 separados), tendríamos que elegir la opción 1, que creo que es perfectamente razonable , o la opción 3, que en mi opinión es demasiado detallada. Solo debemos tener en cuenta que el sacrificio es tener que agregar funciones separadas clamp_min y clamp_max o requerir que el usuario escriba infinito positivo / negativo.

También vale la pena señalar que podríamos proporcionar

impl f32 {
    pub fn clamp<T>(self, bounds: T) -> f32
    where
        T: RangeBounds<f32>,
    {
         // ...
    }
}

// and for f64

ya que para f32 / f64 realidad sabemos cómo manejar los límites exclusivos, a diferencia de los Ord generales. Por supuesto, entonces probablemente querríamos cambiar Ord::clamp para tomar un argumento RangeInclusive por coherencia. Parece que no hubo una opinión fuerte de ninguna manera sobre si preferir dos argumentos o un solo argumento RangeInclusive para Ord::clamp .

Si esto ya es un problema resuelto, no dude en rechazar mi comentario. Solo quería mencionar estas cosas porque no las vi en la discusión anterior.

Triage: las API siguientes actualmente son inestables y apuntan aquí. ¿Hay otros problemas a considerar además del manejo de NaN? ¿Vale la pena estabilizar Ord::clamp primero sin bloquearlo en el manejo de NaN?

`` óxido
pub trait Ord: Eq + PartialOrd{
//…
fn clamp (self, min: Self, max: Self) -> Self donde Self: Sized {…}
}
impl f32 {
pinza pub fn (auto, min: f32, max: f32) -> f32 {…}
}
impl f64 {
pinza pub fn (self, min: f64, max: f64) -> f64 {…}
}

@SimonSapin Estaría feliz de estabilizar todo personalmente

+1, esto pasó por un RFC completo y no creo que haya nada material que haya surgido desde entonces. Por ejemplo, el manejo de NaN surgió en detalle en IRLO y en la discusión de RFC .

Muy bien, eso suena bastante justo.

@rfcbot fcp fusionar

El miembro del equipo @SimonSapin ha propuesto fusionar esto. El siguiente paso es la revisión por el resto de los miembros del equipo etiquetados:

  • [x] @Amanieu
  • [] @Kimundi
  • [x] @SimonSapin
  • [x] @alexcrichton
  • [x] @dtolnay
  • [] @sfackler
  • [] @withoutboats

No hay preocupaciones actualmente enumeradas.

Una vez que la mayoría de los revisores aprueben (y como máximo 2 aprobaciones estén pendientes), entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

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

¿Se tomó una decisión sobre x.clamp(7..=13) vs x.clamp(7, 13) ? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 menciona que el primero podría ser mejor para la coherencia con un potencial futuro f64::clamp .

Yo diría que es una resolución bastante desafortunada ya que .min y .max frecuencia causan errores ya que usa .min(...) para especificar el límite superior y .max(...) para especificar el límite inferior. Esto es increíblemente confuso y he visto muchos errores con esto. .clamp(..1.0) y .clamp(0.0..) son mucho más claros.

@CryZe hace un muy buen punto: incluso si nunca comete un error con min = límite superior, max = límite inferior , todavía tiene que hacer gimnasia mental para recordar cuál usar. Esta carga cognitiva se gastaría mejor en cualquier problema que esté tratando de resolver.

Sé que x.clamp(y, z) es más esperado, pero tal vez esta sea una oportunidad para innovar;)

Experimenté bastante con rangos en las primeras etapas, e incluso retrasé el RFC varios meses para que pudiéramos experimentar con rangos inclusivos. (Esto se inició antes de que se estabilizaran)

Descubrí que no era posible implementar abrazadera para rangos exclusivos en números de punto flotante. Solo admitir algunos tipos de rango, pero no otros, fue un resultado demasiado sorprendente, por lo que, aunque retrasé el RFC varios meses para experimentar con él de esta manera, finalmente decidí que los rangos no eran la solución.

@ m-ou-se Vea la discusión a partir del # 44095 (comentario) y también del # 58710 (revisión).

Editar: como se señala a continuación, la discusión en la solicitud de extracción (# 58710) contiene más discusión sobre la decisión de diseño que sobre el problema de seguimiento. Es lamentable que esto no se haya comunicado aquí, que es donde suelen tener lugar las discusiones de diseño, pero se discutió.

Apoyar solo algunos tipos de rango pero no otros fue un resultado demasiado sorprendente

Rust ya trata algunos rangos de manera diferente a otros (por ejemplo, usándolos para iteración), por lo que solo permitir algunos rangos como clamp argumentos no me sorprende en absoluto.

Aquí está el análisis más útil: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

@Xaeroxe Solo admitir algunos tipos de rango pero no otros fue un resultado demasiado sorprendente

Si estaba pensando en esto antes de que se estabilizaran, ¿el tiempo y el uso general han cambiado su opinión o cree que todavía es así?

Yo diría que los rangos exclusivos nunca deberían implementarse para los flotantes de todos modos, porque tendrían un comportamiento diferente a los enteros (el rango 0..10 incluye el límite inferior y excluye el límite superior, entonces, ¿por qué debería el rango hipotético 0.0...10.0 excluir ambos?). No creo que sea sorprendente, al menos para mí.

@varkor Pero esto se cambió luego de un solo comentario en la revisión, sin ninguna discusión sobre el problema del seguimiento.

Esto puede parecer demasiado conflictivo, intente algo como "cuando miré la conversación no encontré un argumento convincente de por qué no deberíamos usar rangos, ¿alguien puede señalarme?".

Sospecho que el argumento que está buscando está aquí: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

EDITAR @Xaeroxe me ganó :)

¿El tiempo y el uso general han cambiado tu opinión?

Hasta ahora no lo ha hecho, pero los rangos son algo que uso con poca frecuencia en mi codificación diaria. Estoy abierto a ser persuadido por ejemplos de código y API existentes con soporte de rango parcial. Sin embargo, incluso si resolvemos esa pregunta, todavía hay varios otros puntos excelentes que scottmcm plantea en el comentario de RFC que deben abordarse. Por ejemplo, Step no se implementa en tantos tipos como Ord , ¿vale la pena perder esos tipos por este pequeño cambio sintáctico? Además, ¿existe un caso de uso para abrazaderas de rango no inclusivo? Por lo que puedo decir, ningún otro lenguaje o marco sintió la necesidad de admitir una abrazadera de rango exclusiva, entonces, ¿qué beneficio obtenemos de ella? Los rangos eran mucho más difíciles de implementar de manera satisfactoria y tenían muchas desventajas y pocos beneficios.

Si tuviera que implementar esto usando rangos, se vería así.

Entonces, hay algunas razones por las que creo que no deberíamos seguir este enfoque.

  1. La selección de rangos necesarios es lo suficientemente novedosa como para requerir un nuevo rasgo y excluye específicamente el rango más común, Range .

  2. Ya estamos tan avanzados en el proceso de RFC y lo único que std gana con esto es otra forma de escribir .max() o .min() . Realmente no quiero retrasar el RFC al comienzo del proceso para implementar algo que ya podemos hacer en Rust.

  3. Duplica la cantidad de ramificaciones que ocurren en la función para adaptarse a un caso de uso que aún no estamos seguros de que exista. No puedo hacer que esto aparezca en los puntos de referencia.

Necesidad de operaciones de sujeción unilateral

... lo único que gana std de esto es otra forma de escribir .max() o .min() .

El punto principal que estaba tratando de hacer es que he visto esta aparente equivalencia entre .min() / .max() y las abrazaderas unilaterales aparecen varias veces en la discusión, pero las operaciones no son NAN .

Por ejemplo, considere input.max(0.) como una expresión para fijar los números negativos a cero. Si input no es NAN , funciona bien. Sin embargo, cuando input es NAN , se evalúa como 0. . Este casi nunca es el comportamiento deseado; La sujeción unilateral debe conservar los valores de NAN . (Vea este comentario y este comentario ). En conclusión, .max() funciona bien para tomar el mayor de dos números, pero no funciona bien para la sujeción unilateral.

Entonces, necesitamos operaciones de sujeción unilaterales (separadas de .min() / .max() ) para números de coma flotante. Otros presentaron buenos argumentos a favor de la utilidad de las operaciones de sujeción unilateral también para los tipos sin coma flotante. La siguiente pregunta es cómo queremos expresar esas operaciones.

Cómo expresar operaciones de sujeción unilaterales

.clamp() con INFINITY

En otras palabras, no agregue una operación de sujeción unilateral; solo diga a los usuarios que usen .clamp() con INFINITY o NEG_INFINITY límites. Por ejemplo, diga a los usuarios que escriban input.clamp(0., std::f64::INFINITY) .

Esto es muy detallado, lo que empujará a los usuarios a usar el .min() / .max() incorrecto si no son conscientes de los matices del manejo de NAN . Además, no ayuda por T: Ord y, en mi opinión, es menos claro que las alternativas.

.clamp_min() y .clamp_max()

Una opción razonable es agregar los métodos .clamp_min() y .clamp_max() , que no requerirían ningún cambio en la implementación propuesta actualmente. Creo que este es un enfoque razonable; Solo quería asegurarme de que sabíamos que tendríamos que usar este enfoque si estabilizamos la implementación propuesta actualmente de clamp .

Argumento de rango

Otra opción es hacer que clamp tome un argumento de rango. @Xaeroxe ha mostrado una forma de implementar esto, pero esa implementación tiene algunas desventajas, como mencionó. Una forma alternativa de escribir la implementación es similar a la forma en que se implementa actualmente el corte (el rasgo SliceIndex ). Esto resuelve todas las objeciones que he visto en la discusión, excepto la preocupación por proporcionar implementaciones para un subconjunto de tipos de rango y la complejidad adicional. Estoy de acuerdo en que agrega algo de complejidad, pero en mi opinión, no es mucho peor que agregar .clamp_min() / .clamp_max() . Por Ord , sugeriría algo como esto:

pub trait Ord: Eq + PartialOrd<Self> {
    // ...

    fn clamp<B>(self, bounds: B) -> B::Output
    where
        B: Clamp<Self>,
    {
        bounds.clamp(self)
    }
}

pub trait Clamp<T> {
    type Output;
    fn clamp(self, input: T) -> Self::Output;
}

impl<T> Clamp<T> for RangeFull {
    type Output = T;
    fn clamp(self, input: T) -> T {
        input
    }
}

impl<T: Ord> Clamp<T> for RangeFrom<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input < self.start {
            self.start
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeToInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input > self.end {
            self.end
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        assert!(self.start <= self.end);
        let mut x = input;
        if x < self.start { x = self.start; }
        if x > self.end { x = self.end; }
        x
    }
}

Algunas reflexiones sobre esto:

  • Podríamos agregar implementaciones para rangos exclusivos donde T: Ord + Step .
  • Podríamos mantener el rasgo Clamp solo por la noche, similar al rasgo SliceIndex .
  • Para apoyar f32 / f64 , podríamos

    1. Relaja las implementaciones a T: PartialOrd . (No estoy seguro de por qué la implementación actual de clamp está en Ord lugar de PartialOrd . ¿Quizás me perdí algo en la discusión? Parece que PartialOrd sería suficiente.)

    2. o escribir implementaciones específicamente para f32 y f64 . (Si lo desea, siempre podríamos cambiar a la opción i más adelante sin un cambio importante).

    y luego agrega

    impl f32 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
    impl f64 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
  • Podríamos implementar Clamp para rangos exclusivos con f32 / f64 más tarde si lo desea. ( @scottmcm comentó que esto no es sencillo porque std intencionalmente no tiene f32 / f64 operaciones predecesoras. No estoy seguro de por qué std no tiene esas operaciones; ¿tal vez problemas con los números desnormales? Independientemente, eso podría abordarse más adelante).

    Incluso si no agregamos implementaciones de Clamp para rangos exclusivos con f32 / f64 , no estoy de acuerdo en que esto sea demasiado sorprendente. Como señala @varkor , Rust ya trata varios tipos de rango de manera diferente con el propósito de Copy y Iterator / IntoIterator . (En mi opinión, esta es una verruga de std , pero es al menos una instancia en la que los tipos de rango se tratan de manera diferente). Además, si alguien intentara usar un rango exclusivo, el mensaje de error sería fácil de entender ( "el rasgo vinculado std::ops::Range<f32>: Clamp<f32> no está satisfecho").

  • Hice Output un tipo asociado para una máxima flexibilidad para agregar más implementaciones en el futuro, pero eso no es estrictamente necesario.

Básicamente, este enfoque nos permite tanta flexibilidad como queramos con respecto a los límites de los rasgos. También hace posible comenzar con un conjunto mínimamente útil de implementaciones Clamp y agregar más implementaciones más adelante sin cambios importantes.

Comparando las opciones

El enfoque de "usar .clamp() con INFINITY " tiene desventajas sustanciales, como se mencionó anteriormente.

El enfoque "existente .clamp " + .clamp_min() + .clamp_max() tiene las siguientes desventajas:

  • Es más detallado, por ejemplo, input.clamp_min(0) lugar de input.clamp(0..) .
  • No admite rangos exclusivos.
  • No podemos agregar más implementaciones de .clamp() en el futuro (sin agregar aún más métodos). Por ejemplo, no podemos admitir la fijación de un u32 con límites de u8 , que es una característica solicitada de la discusión de RFC . Ese ejemplo en particular puede manejarse mejor con una función .saturating_into() , pero puede haber otros ejemplos en los que serían útiles más implementaciones de sujeción.
  • Alguien puede confundirse entre .min() , .max() , .clamp_min() y .clamp_max() para sujeción unilateral. (Fijar con .clamp_min() es similar a usar .max() , y sujetar con .clamp_max() es similar a usar .min() ). operaciones de sujeción unilateral .clamp_lower() / .clamp_upper() o .clamp_to_start() / .clamp_to_end() lugar de .clamp_min() / .clamp_max() , aunque eso es aún más detallado ( input.clamp_lower(0) versus input.clamp(0..) ).

El enfoque del argumento de rango tiene las siguientes desventajas:

  • La implementación es más compleja que agregar .clamp_min() / .clamp_max() .
  • Si decidimos no implementar o no podemos implementar Clamp para los tipos de rango exclusivo, esto puede resultar sorprendente.

No tengo una opinión sólida sobre el enfoque "existente .clamp " + .clamp_min() + .clamp_max() versus el enfoque de argumento de rango. Es una compensación.

@Xaeroxe Duplica la cantidad de bifurcaciones que ocurren en la función para acomodar un caso de uso que aún no estamos seguros de que exista. No puedo hacer que esto aparezca en los puntos de referencia.

¿Quizás LLVM optimizará la rama adicional?

En sujeción unilateral

Debido a que la sujeción es inclusiva en ambos lados, se puede especificar el mínimo / máximo a la izquierda / derecha para obtener el comportamiento de sujeción de un solo lado. Creo que eso es perfectamente aceptable, y posiblemente más agradable que .clamp((Bound::Unbounded, Inclusive(3.2))) donde no hay un tipo Range* que se ajuste de todos modos:

x.clamp(i32::MIN, 10);
x.clamp(-f32::INFINITY, 10.0);

No hay pérdida de rendimiento, ya que LLVM es trivialmente capaz de optimizar el lado muerto: https://rust.godbolt.org/z/l_uBLO

La sintaxis de rango sería genial, pero clamp es lo suficientemente básica como para que dos argumentos separados estén bien y sean fáciles de entender.

¿Quizás el manejo de min / max NaN se pueda arreglar por sí mismo, por ejemplo, cambiando la implementación de los métodos inherentes de f32 ? ¿O especializándote en los PartialOrd::min/max ? (con una bandera de edición, asumiendo que Rust se las arregla para encontrar una manera de alternar cosas en libstd).

@scottmcm debería consultar RangeToInclusive .

Después de reflexionar un poco más sobre esto, se me ocurrió que la estabilidad es para siempre, por lo que no deberíamos considerar "restablecer el proceso RFC" como una razón para no hacer un cambio.

Con ese fin, quiero volver a la mentalidad que tenía al implementar esto. Clamp opera conceptualmente en un rango, por lo que tiene sentido usar el vocabulario que Rust ya tiene para expresar rangos. Esa fue mi reacción instintiva, y parece ser la reacción de muchas otras personas. Así que repitamos los argumentos para no hacerlo de esta manera y veamos si podemos refutarlos.

  • La selección de rangos necesarios es lo suficientemente novedosa como para requerir un nuevo rasgo y excluye específicamente el rango más común, Range .

    • Usando la nueva implementación proporcionada por @ jturner314 , ahora tenemos la capacidad de agregar más limitaciones en tipos específicos de Range* , como Ord + Step para devolver correctamente valores para rangos exclusivos. Por lo tanto, aunque la abrazadera de rango exclusivo a menudo no es realmente necesaria, podemos aceptar aquí toda la gama de rangos, sin comprometer la interfaz de rangos que no tienen estas limitaciones técnicas.
  • Solo podemos usar Infinity / Min / Max para la sujeción por un lado.

    • Eso es cierto, y una gran parte de por qué este cambio no es realmente un mandato fuerte en mi opinión. Realmente solo tengo una respuesta a esto, y es que la sintaxis Range* involucra menos caracteres y menos importaciones para este caso de uso.

Ahora que hemos refutado las razones para no hacer esto, este comentario carece de cualquier tipo de motivación para realizar el cambio, ya que las opciones parecen equivalentes. Encontremos algo de motivación para hacer el cambio. Solo tengo una razón, que es que la opinión general en este hilo parece ser que el enfoque basado en rangos mejora la semántica del lenguaje. No solo para la abrazadera de rango de doble extremo inclusivo, sino también para funciones como .min() y .max() .

Tengo curiosidad por saber si esta línea de pensamiento tiene alguna tracción con otros que están a favor de estabilizar el RFC como está.

Creo que sería mejor dejar Clamp en la forma actual, porque ahora es muy similar a otros idiomas.
Cuando trabajé en mi solicitud de extracción # 58710, intenté usar una implementación basada en Range.
Pero rust-lang / rfcs # 1961 (comentario) ) me convenció de que es mejor en la forma estándar.

Creo que sería lógico tener un atributo #[must_use] en la función, para no confundir a las personas que no están acostumbradas a cómo funcionan los números de óxido. Es decir, podría percibir fácilmente a alguien escribiendo el siguiente código (incorrecto):

let mut x: f64 = some_number_source();
x.clamp(0.0, 1.0);
//Proceeds to assume that 0.0 <= x <= 1.0

En general, rust adopta un enfoque de (number).method() para los números (mientras que otros lenguajes usan Math.Method(number) ), pero incluso teniendo esto en cuenta, sería una suposición lógica que esto podría modificar number . Esto es más una cuestión de calidad de vida que cualquier otra cosa.

El atributo [must_use] se agregó recientemente .
@ Xaeroxe ¿Se te ocurrió algo para la abrazadera basada en rango?
Creo que la función tal como está ahora se ajustaría mejor a las otras funciones numéricas del óxido y me gustaría comenzar a estabilizarla nuevamente.

En este momento, no veo ninguna razón para optar por una abrazadera basada en rango. Sí, agreguemos el atributo must_use y trabajemos hacia la estabilización.

@SimonSapin @scottmcm ¿Podríamos reiniciar el proceso de estabilización?

Como dijo @ jturner314 , sería genial tener abrazadera en PartialOrd, en lugar de Ord, por lo que también se puede usar en flotadores.

Ya tenemos los f32::clamp y f64::clamp en este número.

Esto es lo que intento hacer:

use num_traits::float::FloatCore;

struct Foo<T> (T);

impl<T: FloatCore> Foo<T> {
    fn foo(&self) -> T {
        self.0.clamp(1, 10)
    }
}

fn main() {
    let foo = Foo(15.3);
    println!("{}", foo.foo())
}

Enlace al patio de recreo.

PartialOrd no es un rasgo solo flotante. Tener un método específico de flotador no hace que la abrazadera esté disponible para tipos PartialOrd .

La implementación actual requiere Eq , aunque no lo usa.

La principal preocupación con PartialOrd era que proporciona garantías más débiles, lo que a su vez debilita las garantías de sujeción. Los usuarios que quieran que esto esté en PartialOrd pueden estar interesados ​​en otra función que escribí https://docs.rs/num/0.2.1/num/fn.clamp.html

¿Cuáles son estas garantías?

Una expectativa bastante natural es que si x.clamp(a, b) == x entonces a <= x && x <= b . Esto no está garantizado con PartialCmp donde x puede ser incomparable con a o b .

Vine aquí hoy en busca de clamp() vagamente recordados y leí la discusión con interés.

Sugeriría usar el "truco de opciones" como un compromiso entre permitir rangos arbitrarios y tener varias funciones con nombre. Sé que esto no es popular entre algunos, pero parece capturar muy bien la semántica deseada aquí:

#![allow(unstable_name_collisions)]

pub trait Clamp: Sized {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>;
}

impl Clamp for f32 {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>,
    {
        let below = match lower.into() {
            None => self,
            Some(lower) => self.max(lower),
        };
        match upper.into() {
            None => below,
            Some(upper) => below.min(upper),
        }
    }
}

#[test]
fn test_clamp() {
    assert_eq!(1.0, f32::clamp(2.0, -1.0, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, 1.0));
    assert_eq!(1.0, f32::clamp(2.0, None, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, None));
    assert_eq!(2.0, f32::clamp(2.0, -1.0, None));
    assert_eq!(-2.0, f32::clamp(-2.0, None, 1.0));
}

Si esto se incluyera en std también se podría incluir una implementación general por T: Ord , que cubriría las inquietudes planteadas sobre una implementación general PartialOrd .

Dado que definir una función clamp() en el código de usuario genera actualmente una advertencia del compilador sobre colisiones de nombres inestables de forma predeterminada, creo que el nombre "clamp" está bien para esta función.

Creo que clamp(a,b,c) debería comportarse igual que min(max(a,b), c) .
Dado que max y min no están implementados para PartialOrd tampoco clamp .
El problema con NaN ya se discutió .

@EdorianDark estoy de acuerdo. min, max también solo debe requerir PartialOrd.

@noonien min y max se definen desde Rust 1.0 y requieren Ord y tienen una definición de f32 y f64 .
Este no es el lugar adecuado para discutir estas funciones.
Aquí solo podemos ocuparnos de que min , max y clamp comporten
Editar: No me gusta la situación con PartialOrd y preferiría que float implemente Ord , pero esto ya no es posible cambiar después de 1.0.

Esto se ha fusionado e inestable durante aproximadamente un año y medio. ¿Cómo nos sentimos acerca de estabilizar esto?

¡Me encantaría estabilizar esto!

Si el conflicto del nombre del método clamp parece un problema, sugerí cambiar la resolución del nombre en un punto en https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812, y ayudaría con esto también.

@Xaeroxe Creo que el proceso es enviar PR de estabilización y pedir el consenso del equipo de bibliotecas al respecto. Parece que t-libs está sobrecargado y no puede mantenerse al día con cosas que no funcionan.

@matklad en realidad, una propuesta de FCP ya comenzó el año pasado en https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395, pero está bloqueada porque queda una casilla de verificación.

En ese caso, creo que recibir un ping una vez al año sobre un tema es bastante tolerable.

@Kimundi
@sfackler
@sin barcos

https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 todavía está esperando su atención

El equipo de bibliotecas ha cambiado bastante desde que se inició el FCP. ¿Qué piensan todos acerca de comenzar un nuevo FCP en el RP de estabilización? Parece que no debería tomar más tiempo que esperar las casillas de verificación restantes aquí.

@LukasKalbertodt está bien por mí, ¿te importaría empezar con eso?

Cancelando el FCP aquí, porque ese FCP ahora ocurrió en la estabilización PR: https://github.com/rust-lang/rust/pull/77872#issuecomment -722982535

@fcpbot cancelar

Oh

@rfcbot cancelar

Propuesta de @ m-ou-se cancelada.

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