Rust: Problème de suivi pour la pince RFC

Créé le 26 août 2017  ·  101Commentaires  ·  Source: rust-lang/rust

Problème de suivi pour https://github.com/rust-lang/rfcs/pull/1961

RP ici : #44097 #58710
PR de stabilisation : https://github.com/rust-lang/rust/pull/77872

À FAIRE:

  • [x] Faire passer le RFC à la période de commentaires finale
  • [x] Mettre en œuvre RFC
  • [ ] Stabiliser
B-unstable C-tracking-issue Libs-Tracked T-libs

Commentaire le plus utile

Il semble que nous soyons dans une très mauvaise situation si une méthode que les gens voulaient suffisamment pour définir un trait d'extension ne pouvait jamais être ajoutée à la bibliothèque standard.

Tous les 101 commentaires

Veuillez noter : Cela a cassé Servo et Pathfinder.

cc @rust-lang/libs, c'est un cas similaire à min / max , où l'écosystème utilisait déjà le nom clamp , et donc l'ajouter a causé une ambiguïté . Il s'agit d'une politique de casse par semver autorisée, mais cela cause néanmoins des problèmes en aval.

Nomination pour la réunion de triage du mardi.

Des idées en attendant ?

Je suis un peu avec @bluss sur celui-ci en ce sens qu'il serait bien de ne pas le répéter. « Clamp » est probablement un nom génial, mais pourrions-nous l'éviter en choisissant un nom différent ?

restrict
clamp_to_range
min_max (Parce que c'est un peu comme combiner min et max.)
Ceux-ci pourraient fonctionner. Pouvons-nous utiliser le cratère pour déterminer à quel point l'impact de clamp est réellement grave ? clamp est bien reconnu dans plusieurs langages et bibliothèques.

Si nous pensons que nous pourrions avoir besoin de renommer, il est probablement préférable de rétablir le PR immédiatement, puis de tester plus attentivement avec le cratère, etc. @Xaeroxe , prêt pour ça?

Sûr. Je n'ai jamais utilisé de cratère auparavant, mais je peux apprendre.

@Xaeroxe ah désolé, je voulais dire obtenir un PR de retour rapidement. (Je suis en vacances aujourd'hui, vous aurez peut-être besoin de quelqu'un d'autre sur les bibliothèques, comme @BurntSushi ou @alexcrichton , pour vous aider à le faire atterrir).

Je prépare le PR maintenant. Amusez-vous pour vos vacances!

Prêt pour les relations publiques https://github.com/rust-lang/rust/pull/44438

clamp_to_range(min, max) pourrait-il être composé de clamp_to_min(min) et clamp_to_max(max) (avec l'assertion supplémentaire que min <= max ), mais ces fonctions pourraient également être appelées indépendamment ?

Je suppose que cette idée exige un RFC.

Je dois dire que je travaille depuis 6 mois sur l'intégration d'une fonction de 4 lignes dans la bibliothèque std. Je suis un peu épuisé. La même fonction a été fusionnée dans num en 2 jours et c'est assez bon pour moi. Si quelqu'un d'autre veut vraiment cela dans la bibliothèque std, allez-y, mais je ne suis tout simplement pas prêt pour 6 mois de plus.

Je rouvre ceci pour que la nomination précédente de @aturon soit toujours visible.

Je pense que cela devrait être tel qu'il est écrit ou que les conseils sur les changements qui peuvent être apportés devraient être mis à jour pour éviter de faire perdre du temps aux gens à l'avenir.

Il était très clair dès le début que cela pourrait causer la casse qu'il a fait. Personnellement, je l'ai comparé à ord_max_min qui a cassé un tas de choses :

Et la réponse à cela était "La fonction Ord::min été ajoutée [...] L'équipe libs a décidé aujourd'hui que c'était une rupture acceptée". Et c'était une fonctionnalité TMTOWTDI avec un nom plus commun, alors que clamp n'existait pas déjà dans std sous une forme différente.

Il me semble, subjectivement, que si cette RFC est annulée, la règle actuelle est "Vous ne pouvez fondamentalement pas mettre de nouvelles méthodes sur les traits dans std, sauf peut-être Iterator ".

Vous ne pouvez pas non plus vraiment mettre de nouvelles méthodes sur des types réels. Considérez la situation où quelqu'un avait un "trait d'extension" pour un type dans std. Maintenant, std implémente une méthode que le trait d'extension a fourni en tant que méthode réelle sur ce type. Ensuite, cela devient stable, mais cette nouvelle méthode est toujours derrière un indicateur de fonctionnalité. Le compilateur se plaindra alors que la méthode est derrière un indicateur de fonctionnalité et ne peut pas être utilisée avec la chaîne d'outils stable, au lieu que le compilateur choisisse la méthode du trait d'extension comme avant et provoque ainsi une rupture sur le compilateur stable.

Il convient également de noter : ce n'est pas seulement un problème de bibliothèque standard. La syntaxe d'appel de méthode rend très difficile d'éviter d'introduire des changements de rupture à peu près n'importe où dans l'écosystème.

(meta) Je copie juste

Si nous convenons que le #44438 est justifié,

  1. Nous devrons peut-être reconsidérer si la rupture d'inférence de type garantie comme peut vraiment être ignorée en tant que XIB.

    Actuellement, le changement d'inférence de type est considéré comme acceptable par les RFC 1105 et 1122 car on peut toujours utiliser UFCS ou d'autres moyens pour forcer un type. Mais la communauté n'aime pas vraiment la casse causée par #42496 ( Ord::{min, max} ). De plus, #41336 (premier essai de T += &T ) a été fermé "juste" en raison de 8 régressions d'inférence de type.

  2. Chaque fois que nous ajoutons une méthode, il devrait y avoir une exécution de cratère pour s'assurer que le nom n'existe pas déjà.

    Notez que l'ajout de méthodes inhérentes peut également entraîner un échec d'inférence — #41793 a été causé par l'ajout des méthodes inhérentes {f32, f64}::from_bits , qui entrent en conflit avec la méthode ieee754::Ieee754::from_bits dans le trait en aval.

  3. Lorsque la caisse en aval n'a pas spécifié #![feature(clamp)] , le candidat Ord::clamp ne doit jamais être pris en compte (un avertissement compatible avec le futur peut toujours être émis) à moins que ce ne soit la solution unique. Cela permettra l'introduction de nouvelles méthodes de traits non "insta-breaking", mais le problème reviendra quand même lors de la stabilisation.

Il semble que nous soyons dans une très mauvaise situation si une méthode que les gens voulaient suffisamment pour définir un trait d'extension ne pouvait jamais être ajoutée à la bibliothèque standard.

Max/min a touché un point particulièrement mauvais en ce qui concerne l'utilisation de noms de méthodes communs sur un trait commun. La même chose n'a pas besoin de s'appliquer à la pince.

Je veux toujours dire oui, mais @sfackler devons-nous vraiment ajouter des méthodes sur un trait qui est si couramment implémenté, par divers types ? Nous devons être prudents lorsque nous ajoutons à l'API de tous les types qui ont acheté un trait existant.

Avec la spécialisation à venir, nous ne perdons rien en mettant des méthodes d'extension dans un trait d'extension.

Une partie ennuyeuse est que si la nouvelle méthode std casse votre code : il apparaîtra bien avant que vous puissiez l'utiliser réellement, car il est instable. A part ça, ce n'est pas si grave si le conflit est avec une méthode qui a le même sens.

Je pense que donner à cette fonction un nom différent pour éviter la casse est une mauvaise solution. Bien que cela fonctionne, il optimise sans casser quelques caisses (qui s'activent toutes la nuit) au lieu d'optimiser la lisibilité future de tout code utilisant cette fonctionnalité.

J'ai quelques préoccupations dont quelques-unes ne sont pas préoccupantes, imo.

  • le nom et l'ombrage ne sont pas idéaux mais ça marche
  • pour les vecteurs et matrices numériques, je pense que max/min/clamp ne sont pas idéaux, mais cela est résolu en n'utilisant pas du tout Ord. Ndarray aimerait faire des pinces d'argument par élément et génériques (scalaire ou tableau), mais Ord n'est pas utilisé par nous ou par des bibliothèques similaires. Donc pas de soucis.
  • Types composés existants qui ne sont pas numériques : BtreeMap obtiendra une méthode clamp avec ce changement. Cela a-t-il un sens en général ? Peut-il implémenter une signification raisonnable en dehors de la valeur par défaut ?
  • le mode d'appel par valeur ne convient pas à toutes les implémentations. Encore une fois, BtreeMap. La pince doit-elle consommer 3 cartes et en renvoyer une ?

types de composés

Je pense que cela a tout autant de sens que BtreeSet<BtreeSet<impl Ord>>::range . Mais il existe des cas particuliers qui pourraient même être utiles, comme Vec<char> .

mode d'appel par valeur

Lorsque cela est apparu dans la RFC, la réponse était simplement d'utiliser Cow .

Bien sûr, cela pourrait être quelque chose comme ceci , pour réutiliser le stockage :

    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
    }

Ce qui https://github.com/rust-lang/rfcs/pull/2111 pourrait rendre l'appel ergonomique.

L'équipe libs en a discuté lors du triage il y a quelques jours et la conclusion était que nous devrions faire une analyse de cratère pour voir quelle est la rupture dans l'écosystème pour ce changement. Les résultats détermineraient les mesures à prendre précisément sur cette question.

Nous pourrions ajouter un certain nombre de fonctionnalités futures du langage pour faciliter l'ajout d'API comme celui-ci, comme des traits de faible priorité ou l'utilisation de traits d'extension d'une manière plus savoureuse. Cependant, nous ne voulons pas nécessairement bloquer cela sur des avancées comme celles-ci.

Une course de cratère s'est-elle déjà produite pour cette fonctionnalité ?

Je prévois de relancer la méthode clamp() après la fusion de #48552. Cependant, RangeInclusive va être stabilisé avant cela, ce qui signifie que l' alternative basée sur la ..= était si instable 😄) :

// 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 stable ouvre également d'autres possibilités, comme inverser les choses (ce qui permet des possibilités intéressantes avec autoref et évite complètement les collisions de noms):

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

Je n'ai pas encore décidé si je pense que c'est mieux, cependant...

J'utiliserais un nom différent s'il était utilisé comme méthode de plage.

J'aimerais sûrement avoir la fonctionnalité le plus tôt possible, quelle que soit la forme.

Quel est l'état actuel ?
Il me semble qu'il existe un consensus, que l'ajout d'une pince à RangeInclusive pourrait être une meilleure alternative.
Donc quelqu'un doit écrire un RFC ?

Une RFC complète n'est probablement pas nécessaire à ce stade. Juste une décision quelle orthographe choisir:

  1. value.clamp(min, max) (suivre le RFC tel quel)
  2. value.clamp(min..=max)
  3. (min..=max).clamp(value)

L'option 2 ou 3 permettrait un serrage partiel plus facile. Vous pouvez faire value.clamp(min..) ou value.clamp(..=max) , sans avoir besoin clamp_to_end méthodes spéciales clamp_to_start ou clamp_to_end .

@egilburg : nous avons déjà ces méthodes spéciales : clamp_to_start est max et clamp_to_end est min :wink:

La consistance est sympa par contre.

@egilburg Rust ne prend pas en charge la surcharge directe. Pour que l'option 2 fonctionne avec votre suggestion, nous aurons besoin d'un nouveau trait implémenté pour RangeInclusive , RangeToInclusive et RangeFrom , qui semblent assez lourds.

Je pense que l'option 3 est la meilleure option.

1 ou 2 sont les moins surprenants. Je resterais avec 1 car beaucoup de code aurait moins à faire pour remplacer l'implémentation locale par l'implémentation std.

Je pense que nous devrions soit prévoir d'utiliser _tous_ les types de plage*, soit _aucun_ d'entre eux.

Bien sûr, c'est plus difficile pour des choses comme Range que pour RangeInclusive . Mais il y a quelque chose de bien dans (0.0..1.0).clamp(2.0_f32) => 0.99999994_f32 .

@kennytm Donc, si
Ou que pensez-vous de la façon de procéder ensuite ?

@EdorianDark Pour cela, nous devrons demander à @rust-lang/libs 😃

Personnellement, j'aime l'option 2, avec seulement RangeInclusive . Comme mentionné, le "serrage partiel" existe déjà avec min et max .

Je suis d'accord avec @SimonSapin , bien que je serais également d'accord avec l'option 1. Avec l'option 3, je n'utiliserais probablement pas la fonction car elle me semble en arrière. Dans les autres langues/bibliothèques avec pince que @kennytm a étudiées plus tôt , 5 sur 7 (tous sauf Swift et Qt) ont la valeur en premier, puis la plage.

La pince est à nouveau en maître !

Je suis content, même si j'essaye toujours de comprendre ce qui a changé qui a rendu cela acceptable maintenant, alors que ce n'était pas dans # 44097

Nous avons maintenant une période d'avertissement en raison de #48552, au lieu de briser instantanément l'inférence avant même de se stabiliser.

C'est une excellente nouvelle, merci !

@kennytm Je veux juste vous remercier pour le travail sur le terrain que vous avez fait pour réaliser # 48552, et @EdorianDark merci pour votre intérêt pour cela et pour sa mise en œuvre. C'est merveilleux de voir cela enfin fusionné.

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

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

Se compile pour :

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

ce qui n'est pas trop mal ( vmaxss et vminss sont utilisés), mais :

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

utilise une instruction de moins :

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

Est-ce inhérent à la mise en œuvre de la pince ou simplement une bizarrerie de l'optimisation LLVM ?

@kornelski clamp ing a NAN est censé préserver ce NAN , ce que ce maxmined ne fait pas, parce que max / min préserve le _non_- NAN .

Ce serait formidable de trouver une implémentation qui réponde à la fois aux attentes du NAN et qui soit plus courte. Et il serait bon que les doctests présentent la gestion du NAN. On dirait que le PR d'origine en avait :

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

Pourquoi le serrage des flotteurs panique-t-il si min ou max est NaN ? Je changerais l'assertion de assert!(min <= max) à assert!(!(min > max)) , de sorte qu'un minimum ou un maximum de NaN n'ait aucun effet, tout comme dans les méthodes max et min.

NAN pour min ou max dans la pince est probablement le signe d'une erreur de programmation, et nous avons pensé qu'il valait mieux paniquer plus tôt plutôt que de fournir des données non verrouillées à IO. Si vous ne voulez pas de limite supérieure ou inférieure, cette fonction n'est pas pour vous.

Vous pouvez toujours utiliser INF et -INF si vous ne voulez pas de limite supérieure ou inférieure, n'est-ce pas ? Ce qui a également un sens mathématique, contrairement à NaN. Mais la plupart du temps, il vaut mieux utiliser max et min pour cela.

@Xaeroxe Merci pour la mise en œuvre.

Peut-être que cela pourrait aller dans la prochaine édition, si cela cassait le code stable ?

Une chose que l'OMI mérite d'être considérée plus en détail est le serrage unilatéral de f32 / f64 . La discussion semble avoir abordé brièvement ce sujet mais ne l'a pas vraiment examiné en détail.

Dans la plupart des cas, si l'entrée d'un clamp unilatéral est NAN, il est plus utile que le résultat soit NAN que que le résultat soit la limite de clampage. Ainsi, les fonctions f32::min et f64::max existantes ne fonctionnent pas correctement pour ce cas d'utilisation. Nous avons besoin de fonctions séparées pour le serrage unilatéral. (Voir rust-num/num-traits#122.)

La raison pour laquelle j'en parle est que cela affecte la conception du clamp recto-verso, car il serait bien que les pinces recto-verso et unilatérale aient une interface cohérente. Quelques options sont :

  1. input.clamp(min, max) , input.clamp_min(min) , et 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)

Avec l'implémentation actuelle ( min et max tant que paramètres distincts f32 / f64 ), nous devrions choisir l'option 1, ce qui me semble parfaitement raisonnable , ou l'option 3, dont l'OMI est trop verbeux. Nous devons juste être conscients que le sacrifice consiste à ajouter des fonctions distinctes clamp_min et clamp_max ou à obliger l'utilisateur à écrire l'infini positif/négatif.

Il convient également de noter que nous pourrions fournir

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

// and for f64

puisque pour f32 / f64 nous savons en fait comment gérer les limites exclusives, contrairement à Ord . Bien sûr, nous voudrions probablement changer Ord::clamp pour prendre un argument RangeInclusive pour la cohérence. Il semble qu'il n'y ait pas eu d'opinion bien arrêtée sur l'opportunité de préférer deux arguments ou un seul argument RangeInclusive pour Ord::clamp .

Si le problème est déjà réglé, n'hésitez pas à rejeter mon commentaire. Je voulais juste évoquer ces choses parce que je ne les avais pas vues dans la discussion précédente.

Triage : les API ci-dessous sont actuellement instables et pointent ici. Y a-t-il d'autres problèmes à prendre en compte que la gestion de NaN ? Cela vaut-il la peine de stabiliser Ord::clamp abord sans le bloquer lors de la gestion de NaN ?

```rouille
pub trait Ord : Eq + PartialOrd{
// …
fn clamp(self, min: Self, max: Self) -> Self où Self: Sized {…}
}
impl f32 {
pub fn clamp(self, min: f32, max: f32) -> f32 {…}
}
impl f64 {
pub fn clamp(self, min: f64, max: f64) -> f64 {…}
}

@SimonSapin je serais heureux de stabiliser le tout personnellement

+1, cela a fait l'objet d'une RFC complète et je ne pense pas qu'il y ait eu quoi que ce soit de matériel depuis lors. Par exemple, la gestion de NaN est apparue en détail sur IRLO et dans la discussion RFC .

D'accord, cela semble assez juste.

@rfcbot fusion fcp

Le membre de l'équipe @SimonSapin a proposé de fusionner cela. L'étape suivante est l'examen par le reste des membres de l'équipe tagués :

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

Aucune préoccupation actuellement répertoriée.

Une fois qu'une majorité d'examinateurs approuve (et au plus 2 approbations sont en attente), cela entrera dans sa dernière période de commentaires. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler !

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe tagués peuvent me donner.

Y a-t-il eu une décision concernant x.clamp(7..=13) vs x.clamp(7, 13) ? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 mentionne que le premier pourrait être meilleur pour la cohérence avec un futur potentiel f64::clamp .

Je dirais que c'est une résolution assez malheureuse car .min et .max provoquent fréquemment des bugs car vous utilisez .min(...) pour spécifier la limite supérieure et .max(...) pour spécifier le borne inférieure. C'est incroyablement déroutant et j'ai vu tellement de bugs avec ça. .clamp(..1.0) et .clamp(0.0..) sont tellement plus clairs.

@CryZe fait un très bon point : même si vous ne faites jamais d'erreur avec min = limite supérieure, max = limite inférieure , vous devez toujours faire de la gymnastique mentale pour vous rappeler lequel utiliser. Cette charge cognitive serait mieux dépensée pour tout problème que vous essayez de résoudre.

Je sais que x.clamp(y, z) est plus attendu, mais c'est peut-être l'occasion d'innover ;)

J'ai beaucoup expérimenté les gammes au début, et j'ai même retardé le RFC de plusieurs mois afin que nous puissions expérimenter avec des gammes inclusives. (Cela a commencé avant qu'ils ne soient stabilisés)

J'ai découvert qu'il n'était pas possible d'implémenter un clamp pour des plages exclusives sur des nombres à virgule flottante. Ne prendre en charge que certains types de plages, mais pas d'autres, était un résultat trop surprenant, donc même si j'avais retardé la RFC de plusieurs mois pour l'expérimenter de cette façon, j'ai finalement décidé que les plages n'étaient pas la solution.

@m-ou-se Voir la discussion à partir de #44095 (commentaire) et aussi #58710 (revue).

Edit : comme indiqué ci-dessous, la discussion dans la demande d'extraction (#58710) contient plus de discussion sur la décision de conception que sur le problème de suivi. Il est regrettable que cela n'ait pas été communiqué ici, où se déroulent généralement les discussions sur la conception, mais cela a été discuté.

Ne prendre en charge que certains types de plage mais pas d'autres était un résultat trop surprenant

Rust traite déjà certaines plages différemment des autres (par exemple, en les utilisant pour l'itération), donc n'autoriser que certaines plages en tant qu'arguments clamp ne me semble pas du tout surprenant.

@Xaeroxe Ne prendre en charge que certains types de plage mais pas d'autres était un résultat trop surprenant

Si vous y pensiez avant qu'ils ne se stabilisent, est-ce que le temps et l'usage général ont changé votre opinion, ou pensez-vous que c'est toujours le cas ?

Je dirais que les plages exclusives ne devraient jamais être implémentées pour les flottants de toute façon, car elles auraient un comportement différent des entiers (la plage 0..10 inclut la limite inférieure et exclut la limite supérieure, alors pourquoi la plage hypothétique 0.0...10.0 exclure les deux ?). Je ne pense pas que ce serait surprenant, du moins pour moi.

@varkor Mais cela a ensuite été modifié après un seul commentaire dans la revue, sans aucune discussion sur le problème de suivi.

Cela peut sembler trop conflictuel, essayez quelque chose comme « quand j'ai parcouru la conversation, je n'ai pas trouvé d'argument convaincant expliquant pourquoi nous ne devrions pas utiliser de plages, quelqu'un peut-il m'indiquer ? ».

Je soupçonne que l'argument que vous recherchez est ici : https://github.com/rust-lang/rfcs/pull/1961#issuecomment-302600351

EDIT @Xaeroxe m'a devancé :)

le temps et l'usage général ont-ils changé votre opinion

Jusqu'à présent, ce n'est pas le cas, mais les plages sont quelque chose que j'utilise assez rarement dans mon codage quotidien. Je suis prêt à être convaincu par des exemples de code et des API existantes avec prise en charge de plage partielle. Cependant, même si nous résolvons cette question, il reste encore plusieurs autres excellents points soulevés par scottmcm dans le commentaire de la RFC qui doivent être traités. Par exemple, Step n'est pas implémenté sur autant de types que Ord , ce changement syntaxique mineur vaut-il la peine de perdre ces types ? De plus, existe-t-il un cas d'utilisation pour les pinces de plage non inclusives ? Pour autant que je sache, aucun autre langage ou framework n'a ressenti le besoin de prendre en charge la pince de plage exclusive, alors quel avantage en tirons-nous ? Les gammes étaient beaucoup plus difficiles à mettre en œuvre de manière satisfaisante, et présentaient de nombreux inconvénients et peu d'avantages.

Si je devais implémenter cela en utilisant des plages, cela ressemblerait à ceci.

Il y a donc plusieurs raisons pour lesquelles je pense que nous ne devrions pas adopter cette approche.

  1. La sélection des plages nécessaires est suffisamment nouvelle pour nécessiter un nouveau trait et exclut spécifiquement la plage la plus courante, Range .

  2. Nous sommes déjà très avancés dans le processus RFC et la seule chose que std en retire est une autre façon d'écrire .max() ou .min() . Je ne veux pas vraiment ramener la RFC au début du processus afin d'implémenter quelque chose que nous pouvons déjà faire dans Rust.

  3. Cela double le nombre de branchements qui se produisent dans la fonction afin de s'adapter à un cas d'utilisation dont nous ne sommes toujours pas sûrs qu'il existe. Je n'arrive pas à ce que cela s'affiche dans les benchmarks.

Nécessité d'opérations de serrage unilatérales

... la seule chose que std en retire est une autre façon d'écrire .max() ou .min() .

Le point principal que j'essayais de faire est que j'ai vu cette apparente équivalence entre .min() / .max() et les pinces unilatérales revenir plusieurs fois dans la discussion, mais les opérations ne sont NAN .

Par exemple, considérons input.max(0.) comme une expression pour ramener à zéro les nombres négatifs. Si input n'est pas - NAN , cela fonctionne bien. Cependant, lorsque input vaut NAN , il vaut 0. . Ce n'est presque jamais le comportement souhaité ; le serrage unilatéral doit préserver les valeurs NAN . (Voir ce commentaire et ce commentaire .) En conclusion, .max() fonctionne bien pour prendre le plus grand de deux nombres, mais cela ne fonctionne pas bien pour le serrage unilatéral.

Nous avons donc besoin d'opérations de serrage unilatérales (séparées de .min() / .max() ) pour les nombres à virgule flottante. D'autres ont présenté de bons arguments en faveur de l'utilité des opérations de serrage unilatérales pour les types à virgule non flottante également. La question suivante est de savoir comment nous voulons exprimer ces opérations.

Comment exprimer les opérations de serrage unilatérales

.clamp() avec INFINITY

En d'autres termes, n'ajoutez pas d'opération de serrage unilatérale ; dites simplement aux utilisateurs d'utiliser .clamp() avec des limites INFINITY ou NEG_INFINITY . Par exemple, dites aux utilisateurs d'écrire input.clamp(0., std::f64::INFINITY) .

C'est très verbeux, ce qui poussera les utilisateurs à utiliser le .min() / .max() incorrect s'ils ne sont pas conscients des nuances de la gestion de NAN . De plus, cela n'aide pas pour T: Ord , et IMO c'est moins clair que les alternatives.

.clamp_min() et .clamp_max()

Une option raisonnable consiste à ajouter les méthodes .clamp_min() et .clamp_max() , ce qui ne nécessiterait aucune modification de l'implémentation actuellement proposée. Je pense que c'est une approche raisonnable; Je voulais juste m'assurer que nous savions que nous devions utiliser cette approche si nous stabilisions l'implémentation actuellement proposée de clamp .

Argument de plage

Une autre option consiste à faire en sorte que clamp prenne un argument de plage. @Xaeroxe a montré un moyen de mettre en œuvre cela, mais cette mise en œuvre présente quelques inconvénients, comme il l'a mentionné. Une autre façon d'écrire l'implémentation est similaire à la façon dont le découpage est actuellement implémenté (le trait SliceIndex ). Cela résout toutes les objections que j'ai vues dans la discussion, à l'exception du souci de fournir des implémentations pour un sous-ensemble de types de plage et de la complexité supplémentaire. Je suis d'accord que cela ajoute de la complexité, mais IMO ce n'est pas bien pire que d'ajouter .clamp_min() / .clamp_max() . Pour Ord , je suggérerais quelque chose comme ceci :

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
    }
}

Quelques réflexions à ce sujet :

  • Nous pourrions ajouter des implémentations pour des plages exclusives où T: Ord + Step .
  • Nous pourrions garder le trait Clamp uniquement la nuit, similaire au trait SliceIndex .
  • Pour soutenir f32 / f64 , nous pourrions

    1. Détendez les implémentations à T: PartialOrd . (Je ne sais pas pourquoi l'implémentation actuelle de clamp est sur Ord au lieu de PartialOrd . J'ai peut-être raté quelque chose dans la discussion ? Il semble que PartialOrd serait suffisant.)

    2. ou écrivez des implémentations spécifiquement pour f32 et f64 . (Si vous le souhaitez, nous pouvons toujours passer à l'option i plus tard sans changement décisif.)

    puis ajouter

    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)
      }
    }
    
  • Nous pourrions implémenter Clamp pour les plages exclusives avec f32 / f64 plus tard si vous le souhaitez. ( @scottmcm a commenté que ce n'est pas simple car std n'a pas intentionnellement f32 / f64 opérations prédécesseurs. Je ne sais pas pourquoi std n'a pas ces opérations ; peut-être des problèmes avec les nombres dénormalisés ? Quoi qu'il en soit, cela pourrait être résolu plus tard.)

    Même si nous n'ajoutons pas d'implémentations de Clamp pour les gammes exclusives avec f32 / f64 , je ne suis pas d'accord pour dire que ce serait trop surprenant. Comme le souligne @varkor , Rust traite déjà différents types de plages différemment dans le but de Copy et Iterator / IntoIterator . (OMI, c'est une verrue de std , mais c'est au moins un cas où les types de plage sont traités différemment.) De plus, si quelqu'un essayait d'utiliser une plage exclusive, le message d'erreur serait facile à comprendre ( "le trait lié std::ops::Range<f32>: Clamp<f32> n'est pas satisfait").

  • J'ai fait de Output un type associé pour une flexibilité maximale pour ajouter plus d'implémentations à l'avenir, mais ce n'est pas strictement nécessaire.

Fondamentalement, cette approche nous permet autant de flexibilité que nous le souhaitons en ce qui concerne les limites des traits. Il permet également de commencer avec un ensemble d'implémentations de Clamp d'une utilité minimale, et d'ajouter d'autres implémentations plus tard sans interrompre les modifications.

Comparer les options

L'approche "utiliser .clamp() avec INFINITY " présente des inconvénients substantiels, comme mentionné ci-dessus.

L'approche « .clamp » + .clamp_min() + .clamp_max() présente les inconvénients suivants :

  • C'est plus détaillé, par exemple input.clamp_min(0) au lieu de input.clamp(0..) .
  • Il ne prend pas en charge les gammes exclusives.
  • Nous ne pouvons pas ajouter d'autres implémentations de .clamp() à l'avenir (sans ajouter encore plus de méthodes). Par exemple, nous ne pouvons pas prendre en charge le blocage d'une u32 avec des limites u8 , ce qui est une fonctionnalité demandée dans la discussion RFC . Cet exemple particulier peut être mieux géré avec une fonction .saturating_into() , mais il peut y avoir d'autres exemples où plus d'implémentations de serrage seraient utiles.
  • Quelqu'un peut se tromper entre .min() , .max() , .clamp_min() , et .clamp_max() pour un serrage unilatéral. (Le serrage avec .clamp_min() est similaire à l'utilisation de .max() , et le serrage avec .clamp_max() est similaire à l'utilisation de .min() .) Nous pourrions principalement éviter ce problème en nommant le opérations de serrage unilatéral .clamp_lower() / .clamp_upper() ou .clamp_to_start() / .clamp_to_end() au lieu de .clamp_min() / .clamp_max() , bien que ce soit encore plus verbeux ( input.clamp_lower(0) contre input.clamp(0..) ).

L'approche de l'argument de la plage présente les inconvénients suivants :

  • L'implémentation est plus complexe que l'ajout de .clamp_min() / .clamp_max() .
  • Si nous décidons de ne pas ou ne pouvons pas implémenter Clamp pour les types de plage exclusifs, cela peut être surprenant.

Je n'ai pas d'opinion tranchée sur l'approche « .clamp existante » + .clamp_min() + .clamp_max() par rapport à l'approche de l'argument de la plage. C'est un compromis.

@Xaeroxe Cela double le nombre de branchements qui se produisent dans la fonction afin de s'adapter à un cas d'utilisation dont nous ne sommes toujours pas sûrs qu'il existe. Je n'arrive pas à ce que cela s'affiche dans les benchmarks.

Peut-être que la branche supplémentaire sera optimisée par LLVM ?

Sur serrage unilatéral

Étant donné que le serrage est inclusif des deux côtés, il suffit de spécifier le min/max à gauche/à droite pour obtenir le comportement de serrage d'un seul côté. Je pense que c'est parfaitement acceptable, et sans doute plus agréable que .clamp((Bound::Unbounded, Inclusive(3.2))) où il n'y a pas de type Range* qui convient de toute façon :

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

Il n'y a pas de perte de performances, car LLVM est trivialement capable d'optimiser le côté mort : https://rust.godbolt.org/z/l_uBLO

La syntaxe de plage serait cool, mais clamp est suffisamment basique pour que deux arguments distincts soient bien et faciles à comprendre.

Peut-être que la gestion de min / max NaN peut être corrigée par elle-même, par exemple en modifiant l'implémentation des méthodes inhérentes à f32 ? Ou spécialiser le PartialOrd::min/max ? (avec un indicateur d'édition, en supposant que Rust réussisse à trouver un moyen de basculer les choses dans libstd).

@scottmcm vous devriez vérifier RangeToInclusive .

Après avoir réfléchi un peu plus à cela, il m'est venu à l'esprit que la stabilité est éternelle, nous ne devrions donc pas considérer la "réinitialisation du processus RFC" comme une raison de ne pas faire de changement.

À cette fin, je veux revenir à l'état d'esprit que j'avais lors de la mise en œuvre de cela. Clamp fonctionne conceptuellement sur une plage, il est donc logique d'utiliser le vocabulaire que Rust a déjà mis en place pour exprimer les plages. C'était ma réaction instinctive, et cela semble être la réaction de beaucoup d'autres personnes. Réitérons donc les arguments pour ne pas procéder ainsi, et voyons si nous pouvons les réfuter.

  • La sélection des plages nécessaires est suffisamment nouvelle pour nécessiter un nouveau trait et exclut spécifiquement la plage la plus courante, Range .

    • En utilisant la nouvelle implémentation fournie par @jturner314, nous avons maintenant la possibilité d'ajouter plus de limitations sur des types Range* spécifiques, tels que Ord + Step afin de renvoyer correctement les valeurs pour les plages exclusives. Ainsi, même si la pince de gamme exclusive n'est souvent pas vraiment nécessaire, nous pouvons en fait accepter toute la gamme des gammes ici, sans compromettre l'interface des gammes qui n'ont pas ces limitations techniques.
  • Nous pouvons simplement utiliser Infinity/Min/Max pour un serrage unilatéral.

    • C'est vrai, et c'est en grande partie pourquoi ce changement n'est pas vraiment un mandat fort à mon avis. Je n'ai vraiment qu'une seule réponse à cela, et c'est que la syntaxe Range* implique moins de caractères et moins d'importations pour ce cas d'utilisation.

Maintenant que nous avons réfuté les raisons de ne pas le faire, ce commentaire manque de motivation pour effectuer le changement, car les options semblent équivalentes. Trouvons une certaine motivation pour faire le changement. Je n'ai qu'une seule raison, c'est que l'opinion générale dans ce fil semble être que l'approche basée sur la plage améliore la sémantique du langage. Pas seulement pour la pince de plage à extrémité double incluse, mais aussi pour des fonctions comme .min() et .max() .

Je suis curieux de savoir si cette ligne de pensée a une influence avec d'autres qui sont en faveur de la stabilisation du RFC tel quel.

Je pense qu'il serait préférable de laisser Clamp dans sa forme actuelle, car il est maintenant très similaire aux autres langages.
Lorsque j'ai travaillé sur ma pull request #58710, j'ai essayé d'utiliser une implémentation basée sur Range.
Mais rust-lang/rfcs#1961 (commentaire) ) m'a convaincu que c'est mieux sous la forme standard.

Je pense qu'il serait logique d'avoir un attribut #[must_use] sur la fonction, afin de ne pas confondre les personnes qui ne sont pas habituées au fonctionnement des chiffres de rouille. C'est-à-dire que je pourrais facilement percevoir quelqu'un écrivant le code (incorrect) suivant :

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

En général, rust adopte une approche (number).method() des chiffres (alors que d'autres langages utilisent Math.Method(number) ), mais même en gardant cela à l'esprit, ce serait une hypothèse logique que cela pourrait modifier number . C'est plus une question de qualité de vie qu'autre chose.

L'attribut [must_use] été ajouté récemment .
@ Xaeroxe Avez-vous trouvé quelque chose pour la pince basée sur la plage?
Je pense que la fonction telle qu'elle est actuellement conviendrait mieux aux autres fonctions numériques de la rouille et j'aimerais recommencer à la stabiliser.

Pour le moment, je ne vois aucune raison d'opter pour une pince basée sur la plage. Oui, ajoutons l'attribut must_use et travaillons à la stabilisation.

@SimonSapin @scottmcm Pourrions-nous redémarrer le processus de stabilisation ?

Comme l'a dit @ jturner314 , ce serait formidable d'avoir un clamp sur PartialOrd, au lieu de Ord, afin qu'il puisse également être utilisé sur des flotteurs.

Nous avons déjà les f32::clamp et f64::clamp spécialisés dans ce numéro.

C'est ce que j'essaye de faire :

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

Lien vers l'aire de jeux.

PartialOrd n'est pas un trait flottant uniquement. Le fait d'avoir une méthode spécifique au flottant ne rend pas la pince disponible pour les types PartialOrd .

L'implémentation actuelle nécessite Eq , même si elle ne l'utilise pas.

La principale préoccupation avec PartialOrd était qu'il offre des garanties plus faibles, ce qui à son tour affaiblit les garanties de serrage. Les utilisateurs qui souhaitent que cela soit sur PartialOrd peuvent être intéressés par une autre fonction que j'ai écrite https://docs.rs/num/0.2.1/num/fn.clamp.html

Quelles sont ces garanties ?

Une attente assez naturelle est que ssi x.clamp(a, b) == x then a <= x && x <= b . Ceci n'est pas garanti avec PartialCmpx peut être incomparable avec a ou b .

Je suis venu ici aujourd'hui à la recherche de clamp() souvient vaguement et a lu la discussion avec intérêt.

Je suggérerais d'utiliser le "truc d'option" comme compromis entre autoriser des plages arbitraires et avoir plusieurs fonctions nommées. Je sais que ce n'est pas populaire auprès de certains, mais cela semble bien capturer la sémantique souhaitée ici :

#![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 cela était inclus dans std une implémentation globale pourrait également être incluse pour T: Ord , ce qui couvrirait les préoccupations soulevées au sujet d'une implémentation générale de PartialOrd .

Étant donné que la définition d'une fonction clamp() dans le code utilisateur génère actuellement un avertissement du compilateur sur les collisions de noms instables par défaut, je pense que le nom "clamp" convient parfaitement à cette fonction.

Je pense que clamp(a,b,c) devrait se comporter de la même manière que min(max(a,b), c) .
Étant donné que max et min ne sont pas implémentés pour PartialOrd ni clamp .
Le problème avec NaN a déjà été discuté .

@EdorianDark je suis d'accord. min, max ne devraient également nécessiter que PartialOrd.

@noonien min et max sont définis depuis Rust 1.0 et ils nécessitent Ord et ont une définition pour f32 et f64 .
Ce n'est pas le bon endroit pour discuter de ces fonctions.
Ici, nous ne pouvons que veiller à ce que min , max et clamp se comportent
Edit : Je n'aime pas la situation avec PartialOrd et je préférerais que float implémente Ord , mais il n'est plus possible de changer après 1.0.

Cela a été fusionné et instable depuis environ un an et demi maintenant. Que pensons-nous de stabiliser cela?

J'adorerais stabiliser ça !

Si le conflit de nom de méthode clamp ressemble à un problème, j'ai suggéré de changer la résolution de nom à un moment donné dans https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812, et cela aiderait avec ça aussi.

@Xaeroxe Je pense que le processus consiste à soumettre des relations publiques de stabilisation et à demander le consensus de l'équipe des bibliothèques à ce sujet. Il semble que t-libs soit surchargé et ne puisse pas suivre les choses non-fcped.

@matklad en fait une proposition FCP a déjà commencé l'année dernière à https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395, mais elle est bloquée car il reste une case à cocher.

Dans ce cas, je pense qu'être cinglé environ une fois par an sur un problème est assez tolérable.

@Kimundi
@sfackler
@sansbateaux

https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 attend toujours votre attention

L'équipe libs a pas mal changé depuis le lancement du FCP. Que pensez-vous tous du démarrage d'un nouveau FCP dans le PR de stabilisation ? On dirait que cela ne devrait pas prendre plus de temps que d'attendre les cases à cocher restantes ici.

@LukasKalbertodt , ça me va, ça vous dérange de commencer?

Annulation du FCP ici, car ce FCP s'est maintenant produit sur le PR de stabilisation : https://github.com/rust-lang/rust/pull/77872#issuecomment -722982535

@fcpbot annuler

euh

@rfcbot annuler

@m-ou-se proposition annulée.

Cette page vous a été utile?
0 / 5 - 0 notes