Tracking-Problem für https://github.com/rust-lang/rfcs/pull/1961
PR hier: #44097 #58710
Stabilisierungs-PR: https://github.com/rust-lang/rust/pull/77872
MACHEN:
Bitte beachten: Dies brach Servo und Pathfinder.
cc @rust-lang/libs, dies ist ein ähnlicher Fall wie min
/ max
, wo das Ökosystem bereits den Namen clamp
verwendet hat, und daher hat das Hinzufügen zu Mehrdeutigkeiten geführt . Dies ist erlaubt, pro Server zu brechen, aber es verursacht dennoch nachgelagerte Schmerzen.
Nominierung für das Triage-Meeting am Di.
Irgendwelche Gedanken in der Zwischenzeit?
Ich bin in dieser Hinsicht mit
restrict
clamp_to_range
min_max
(Weil es so ist, als würde man Min und Max kombinieren.)
Diese könnten funktionieren. Können wir den Krater verwenden, um festzustellen, wie schlimm die Auswirkungen von clamp
tatsächlich sind? clamp
wird in mehreren Sprachen und Bibliotheken gut erkannt.
Wenn wir denken, dass wir möglicherweise umbenennen müssen, ist es wahrscheinlich am besten, den PR sofort zurückzusetzen und dann sorgfältiger mit Krater usw. zu testen .
Sicher. Ich habe noch nie Krater benutzt, aber ich kann es lernen.
@Xaeroxe ah sorry, ich meinte, schnell einen Revert-PR zu bekommen. (Ich bin heute im Urlaub, also brauchst du vielleicht jemand anderen auf Libs, wie @BurntSushi oder @alexcrichton , um zu helfen, es zu landen).
Ich bereite jetzt die PR vor. Viel Spaß in Ihrem Urlaub!
Könnte clamp_to_range(min, max)
aus clamp_to_min(min)
und clamp_to_max(max)
(mit der zusätzlichen Behauptung, dass min <= max
), aber diese Funktionen könnten auch unabhängig voneinander aufgerufen werden?
Ich nehme an, dass diese Idee einen RFC vorschreibt.
Ich muss sagen, obwohl ich seit 6 Monaten daran arbeite, eine 4-Zeilen-Funktion in die std-Bibliothek zu bekommen. Ich bin irgendwie erschöpft. Die gleiche Funktion wurde in 2 Tagen in num zusammengeführt und das ist gut genug für mich. Wenn jemand anderes dies wirklich in der std-Bibliothek haben möchte, gehen Sie vor, aber ich bin einfach nicht bereit für weitere 6 Monate.
Ich öffne das wieder , damit die vorherige Nominierung von
Ich denke, dass dies entweder wie geschrieben aufgenommen werden sollte oder die Anleitung zu den möglichen Änderungen aktualisiert werden sollte, um die Zeit der Menschen in Zukunft nicht zu verschwenden.
Es war von Anfang an klar, dass dies zu dem Bruch führen könnte, den es verursacht hat. Persönlich habe ich es mit ord_max_min
verglichen, was eine Menge Dinge kaputt gemacht hat:
Und die Antwort darauf war "Die Funktion Ord::min
wurde hinzugefügt [...] Das libs-Team hat heute entschieden, dass dies als Bruch akzeptiert wird". Und das war ein TMTOWTDI-Feature mit einem gebräuchlicheren Namen, während clamp
in std noch nicht in einer anderen Form existierte.
Es fühlt sich für mich subjektiv so an, dass, wenn dieser RFC zurückgesetzt wird, die eigentliche Regel lautet: "Sie können im Grunde keine neuen Methoden auf Merkmale in std anwenden, außer vielleicht Iterator
".
Sie können auch nicht wirklich neue Methoden auf tatsächliche Typen anwenden. Betrachten Sie die Situation, in der jemand ein "Erweiterungsmerkmal" für einen Typ in std hatte. Jetzt implementiert std eine Methode, die das Erweiterungsmerkmal als tatsächliche Methode für diesen Typ bereitstellt. Dann erreicht dies stabil, aber diese neue Methode steckt noch hinter einem Feature-Flag. Der Compiler beschwert sich dann, dass sich die Methode hinter einem Feature-Flag befindet und nicht mit der Stable-Toolchain verwendet werden kann, anstatt dass der Compiler wie zuvor die Methode des Erweiterungsmerkmals wählt und somit den Stable-Compiler beschädigt.
Es ist auch erwähnenswert: Dies ist nicht nur ein Standard-Bibliotheksproblem. Die Syntax von Methodenaufrufen macht es wirklich schwierig, die Einführung von Breaking Changes fast überall im Ökosystem zu vermeiden.
(meta) Kopiere einfach
Wenn wir zustimmen, dass #44438 gerechtfertigt ist,
Wir müssen möglicherweise noch einmal darüber nachdenken, ob garantierte Typinferenzbrüche wirklich als XIB ignoriert werden können.
Derzeit wird eine Änderung der Typinferenz von den RFCs 1105 und 1122 als akzeptabel angesehen, da man immer UFCS oder andere Methoden verwenden könnte, um einen Typ zu erzwingen. Aber die Community mag den Bruch, der durch #42496 ( Ord::{min, max}
) verursacht wird, nicht wirklich . Außerdem wurde #41336 (erster Versuch von T += &T
) "nur" aufgrund von 8 Typinferenzregressionen geschlossen.
Immer wenn wir eine Methode hinzufügen, sollte es einen Kraterlauf geben, um sicherzustellen, dass der Name nicht bereits existiert.
Beachten Sie, dass das Hinzufügen von inhärenten Methoden auch zu Inferenzfehlern führen kann – #41793 wurde durch das Hinzufügen der inhärenten Methoden {f32, f64}::from_bits
, die mit der Methode ieee754::Ieee754::from_bits
im Downstream-Merkmal kollidieren.
Wenn nachgelagerte Crate #![feature(clamp)]
nicht angegeben hat, sollte der Kandidat Ord::clamp
niemals in Betracht gezogen werden (eine zukunftskompatible Warnung kann dennoch ausgegeben werden), es sei denn, dies ist die eindeutige Lösung. Dies ermöglicht die Einführung neuer Eigenschaftsmethoden, die nicht "insta-breaking" sind, aber das Problem wird bei der Stabilisierung immer noch auftreten.
Es scheint, als wären wir an einem ziemlich schlechten Ort, wenn eine Methode, die die Leute genug wollten, um ein Erweiterungsmerkmal zu definieren, niemals zur Standardbibliothek hinzugefügt werden kann.
Max/Min traf eine besonders schlechte Stelle, wenn es um die Verwendung gängiger Methodennamen für ein gemeinsames Merkmal ging. Das gleiche muss nicht für die Klemme gelten.
Ich möchte immer noch ja sagen, aber @sfackler müssen wir wirklich Methoden zu einem Merkmal hinzufügen, das so häufig von verschiedenen Typen implementiert wird? Wir müssen vorsichtig sein, wenn wir der API aller Typen hinzufügen, die sich in eine vorhandene Eigenschaft eingekauft haben.
Mit der kommenden Spezialisierung verlieren wir nichts, wenn wir Erweiterungsmethoden in ein Erweiterungsmerkmal einfügen.
Ein nerviger Teil ist, dass, wenn die neue std-Methode Ihren Code beschädigt: Sie erscheint, lange bevor Sie sie tatsächlich verwenden können, da sie instabil ist. Ansonsten ist es nicht so schlimm, wenn der Konflikt mit einer Methode besteht, die dieselbe Bedeutung hat.
Ich denke, dieser Funktion einen anderen Namen zu geben, um einen Bruch zu vermeiden, ist eine schlechte Lösung. Während es funktioniert, optimiert es nicht, ein paar Kisten zu zerbrechen (die sich alle nachts entscheiden), anstatt die zukünftige Lesbarkeit von Code zu optimieren, der diese Funktion verwendet.
Ich habe ein paar Bedenken, von denen einige imo keine Sorge sind.
zusammengesetzte Typen
Ich denke, es macht genauso viel Sinn wie BtreeSet<BtreeSet<impl Ord>>::range
. Aber es gibt spezielle Fälle, die sogar hilfreich sein könnten, wie Vec<char>
.
Aufrufmodus nach Wert
Als dies im RFC aufkam, lautete die Antwort einfach use Cow .
Natürlich könnte es so aussehen , um Speicher wiederzuverwenden:
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
}
Welche https://github.com/rust-lang/rfcs/pull/2111 könnte ergonomisch zu nennen sein.
Das libs-Team diskutierte dies während der Triage vor ein paar Tagen und kam zu dem Schluss, dass wir einen Kraterlauf machen sollten, um zu sehen, was der Durchbruch für diese Veränderung im Ökosystem ist. Die Ergebnisse daraus würden bestimmen, welche Maßnahmen in genau dieser Frage zu ergreifen sind.
Es gibt eine Reihe möglicher zukünftiger Sprachfunktionen, die wir hinzufügen könnten, um das Hinzufügen von APIs wie dieser zu erleichtern, z. Wir wollen dies jedoch nicht unbedingt bei solchen Weiterentwicklungen blockieren.
Gab es für diese Funktion jemals einen Kraterlauf?
Ich plane, die Methode clamp()
wiederzubeleben, nachdem #48552 zusammengeführt wurde. Allerdings wird RangeInclusive
vorher stabilisiert, was bedeutet, dass die bereichsbasierte Alternative nun in Betracht gezogen werden kann (was eigentlich der ursprüngliche Vorschlag ist, aber zurückgezogen, weil ..=
so instabil war 😄):
// 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);
Ein stabiles RangeInclusive
eröffnet auch andere Möglichkeiten, wie das Umdrehen (was einige interessante Möglichkeiten mit Autoref ermöglicht und die Namenskollisionen insgesamt vermeidet):
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
Ich habe mich aber noch nicht entschieden, ob ich das besser finde...
Ich würde einen anderen Namen verwenden, wenn er als Bereichsmethode verwendet wird.
Sicherlich würde ich es genießen, das Feature früher als später zu haben, egal in welcher Form.
Wie ist der aktuelle Stand?
Es scheint mir, dass es Konsens gibt, dass das Hinzufügen einer Klemme zu RangeInclusive eine bessere Alternative sein könnte.
Also muss jemand einen RFC schreiben?
Ein vollständiger RFC wird zu diesem Zeitpunkt wahrscheinlich nicht benötigt. Nur eine Entscheidung, welche Schreibweise zu wählen ist:
value.clamp(min, max)
(befolgen Sie den RFC wie er ist)value.clamp(min..=max)
(min..=max).clamp(value)
Option 2 oder 3 würde eine einfachere teilweise Klemmung ermöglichen. Sie können value.clamp(min..)
oder value.clamp(..=max)
, ohne dass spezielle clamp_to_start
oder clamp_to_end
Methoden erforderlich sind.
@egilburg : diese speziellen Methoden haben wir bereits: clamp_to_start
ist max
und clamp_to_end
ist min
:wink:
Die Konsistenz ist aber schön.
@egilburg Rust unterstützt kein direktes Überladen. Damit Option 2 mit Ihrem Vorschlag funktioniert, müssen wir eine neue Eigenschaft für RangeInclusive
, RangeToInclusive
und RangeFrom
implementieren, die sich ziemlich schwer anfühlen.
Ich denke, dass Option 3 die beste Option ist.
1 oder 2 sind am wenigsten überraschend. Ich würde bei 1 bleiben, da viel Code weniger zu tun hätte, um die lokale Implementierung durch die std-Implementierung zu ersetzen.
Ich denke, wir sollten entweder planen, _alle_ Range*-Typen oder _keinen_ davon zu verwenden.
Natürlich ist das für Dinge wie Range
schwieriger als für RangeInclusive
. Aber (0.0..1.0).clamp(2.0_f32)
=> 0.99999994_f32
etwas Schönes.
@kennytm Wenn ich also eine Pull-Anfrage mit Option 3 öffnen würde, glauben Sie, dass sie zusammengeführt wird?
Oder was denkst du über das weitere Vorgehen?
@EdorianDark Dafür müssen wir @rust-lang/libs fragen 😃
Ich persönlich mag Option 2, nur mit RangeInclusive
. Wie bereits erwähnt gibt es "Teilspannung" bereits mit min
und max
.
Ich stimme @SimonSapin zu , obwohl ich mit Option 1 auch in Ordnung wäre. Mit Option 3 würde ich die Funktion wahrscheinlich nicht verwenden, da sie mir rückwärts vorkommt. In den anderen Sprachen/Bibliotheken mit Klammer, die @kennytm zuvor untersucht hat , haben 5 von 7 (alle außer Swift und Qt) zuerst den Wert und dann den Bereich.
Klemme ist jetzt wieder im Master!
Ich freue mich, obwohl ich immer noch versuche herauszufinden, was dies jetzt akzeptabel gemacht hat, obwohl es nicht in #44097 war
Wir haben jetzt eine Warnfrist wegen #48552, anstatt die Schlussfolgerungen sofort zu brechen, noch bevor sie sich stabilisieren.
Das sind tolle Neuigkeiten, danke!
@kennytm Ich möchte Ihnen nur für die @EdorianDark danke für Ihr Interesse daran und die Implementierung. Es ist wunderbar zu sehen, dass dies endlich zusammengeführt wird.
https://rust.godbolt.org/z/JmLWJi
pub fn clamped(a: f32) -> f32 {
a.clamp(0.,255.)
}
Kompiliert zu:
vxorps xmm1, xmm1, xmm1
vmaxss xmm0, xmm1, xmm0
vmovss xmm1, dword ptr [rip + .LCPI0_0]
vminss xmm0, xmm1, xmm0
was nicht schlecht ist ( vmaxss
und vminss
werden verwendet), aber:
pub fn maxmined(a: f32) -> f32 {
(0f32).max(a).min(255.)
}
verwendet eine Anweisung weniger:
vxorps xmm1, xmm1, xmm1
vmaxss xmm0, xmm0, xmm1
vminss xmm0, xmm0, dword ptr [rip + .LCPI1_0]
Ist das inhärent mit der Clamp-Implementierung oder nur eine Eigenart der LLVM-Optimierung?
@kornelski clamp
ing a NAN
soll das NAN
bewahren, was das maxmined
nicht tut, weil max
/ min
bewahrt die _non_- NAN
.
Es wäre großartig, eine Implementierung zu finden, die sowohl die NAN-Erwartungen erfüllt als auch kürzer ist. Und es wäre gut für die doctests, die NAN-Handhabung zu demonstrieren. Sieht so aus, als hätte die ursprüngliche PR einige:
@scottmcm https://github.com/rust-lang/rust/pull/59327 fertig
Warum geraten Klemmschwimmer in Panik, wenn min oder max NaN ist? Ich würde die Behauptung von assert!(min <= max)
in assert!(!(min > max))
ändern, so dass ein NaN-Minimum oder -Maximum keine Auswirkungen hat, genau wie bei den max- und min-Methoden.
NAN für min
oder max
in der Klemme weist wahrscheinlich auf einen Programmierfehler hin, und wir dachten, es wäre besser, früher in Panik zu geraten, als möglicherweise ungeklemmte Daten an IO weiterzugeben. Wenn Sie keine Ober- oder Untergrenze wünschen, ist diese Funktion nichts für Sie.
Sie könnten immer INF und -INF verwenden, wenn Sie keine Ober- oder Untergrenze wollen, richtig? Was im Gegensatz zu NaN auch mathematisch sinnvoll ist. Aber meistens ist es besser, dafür max
und min
zu verwenden.
@Xaeroxe Vielen Dank für die Implementierung.
Vielleicht könnte dies in der nächsten Ausgabe passieren, wenn es stabilen Code bricht?
Eine Sache, die IMO näher betrachtet werden sollte, ist die einseitige Klemmung von f32
/ f64
. Die Diskussion scheint dieses Thema zwar kurz berührt, aber nicht wirklich im Detail betrachtet zu haben.
Wenn die Eingabe für eine einseitige Klemme NAN ist, ist es in den meisten Fällen nützlicher, dass das Ergebnis NAN ist, als dass das Ergebnis die Klemmgrenze ist. Die vorhandenen Funktionen f32::min
und f64::max
funktionieren also für diesen Anwendungsfall nicht gut. Für einseitiges Spannen benötigen wir separate Funktion(en). (Siehe rost-num/num-traits#122.)
Der Grund, warum ich dies erwähne, ist, dass es das Design der zweiseitigen clamp
, da es für zweiseitige und einseitige Klemmen schön wäre, eine einheitliche Schnittstelle zu haben. Einige Optionen sind:
input.clamp(min, max)
, input.clamp_min(min)
und input.clamp_max(max)
input.clamp(min..=max)
, input.clamp(min..)
, input.clamp(..=max)
input.clamp(min, max)
, input.clamp(min, std::f64::INFINITY)
, input.clamp(std::f64::NEG_INFINITY, max)
Bei der aktuellen Implementierung ( min
und max
als separate f32
/ f64
Parameter) müssten wir Option 1 wählen, was ich für absolut sinnvoll halte , oder Option 3, die IMO zu ausführlich ist. Wir sollten uns nur bewusst sein, dass das Opfer darin besteht, separate clamp_min
und clamp_max
Funktionen hinzuzufügen oder vom Benutzer zu verlangen, positive/negative Unendlichkeiten zu schreiben.
Es ist auch erwähnenswert, dass wir zur Verfügung stellen könnten
impl f32 {
pub fn clamp<T>(self, bounds: T) -> f32
where
T: RangeBounds<f32>,
{
// ...
}
}
// and for f64
denn für f32
/ f64
wissen wir eigentlich, wie man exklusive Grenzen behandelt, im Gegensatz zu allgemeinen Ord
. Natürlich möchten wir Ord::clamp
aus Konsistenzgründen in ein RangeInclusive
Argument ändern. Es scheint, als ob es keine eindeutige Meinung gab, ob man zwei Argumente oder ein einzelnes RangeInclusive
Argument für Ord::clamp
bevorzugen sollte.
Wenn dies bereits ein geklärtes Problem ist, können Sie meinen Kommentar gerne ablehnen. Ich wollte diese Dinge nur ansprechen, weil ich sie in früheren Diskussionen nicht gesehen habe.
Triage: Die unten aufgeführten APIs sind derzeit instabil und zeigen hierher. Gibt es andere Aspekte als die Handhabung von NaN zu berücksichtigen? Lohnt es sich, zuerst Ord::clamp
zu stabilisieren, ohne es beim NaN-Handling zu blockieren?
```rost
Pub-Eigenschaft Ord: Eq + PartialOrd
// …
fn Klemme(selbst, min: Selbst, max: Selbst) -> Selbst wo Selbst: Größe {…}
}
impl f32 {
pub fn Klemme(self, min: f32, max: f32) -> f32 {…}
}
impl f64 {
pub fn Klemme(self, min: f64, max: f64) -> f64 {…}
}
@SimonSapin Ich würde das Ganze gerne persönlich stabilisieren
+1, dies hat einen vollständigen RFC durchlaufen und ich glaube nicht, dass seitdem Material dazugekommen ist. Zum Beispiel wurde der Umgang mit NaN im IRLO und in der RFC-Diskussion ausführlich behandelt .
Okay, das klingt fair genug.
@rfcbot fcp zusammenführen
Teammitglied @SimonSapin hat vorgeschlagen, dies zusammenzuführen. Der nächste Schritt ist die Überprüfung durch den Rest der markierten Teammitglieder:
Derzeit keine Bedenken aufgeführt.
Sobald die Mehrheit der Gutachter zugestimmt hat (und höchstens 2 Genehmigungen ausstehen), tritt die letzte Kommentierungsphase ein. Wenn Sie ein wichtiges Problem entdecken, das zu keinem Zeitpunkt in diesem Prozess angesprochen wurde, melden Sie sich bitte!
In diesem Dokument finden Sie Informationen darüber, welche Befehle mir markierte Teammitglieder geben können.
Wurde eine Entscheidung bezüglich x.clamp(7..=13)
vs. x.clamp(7, 13)
? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 erwähnt, dass der erste für die Konsistenz mit einem potenziellen zukünftigen f64::clamp
besser sein könnte.
@m-ou-se Siehe die Diskussion ab https://github.com/rust-lang/rust/issues/44095#issuecomment -411457313 und auch https://github.com/rust-lang/rust/pull/ 58710#pullrequestreview -207529970.
Ich würde sagen, das ist eine ziemlich unglückliche Lösung, da .min
und .max
häufig Fehler verursachen, wenn Sie .min(...)
, um die obere Grenze anzugeben, und .max(...)
, um die anzugeben untere Grenze. Das ist unglaublich verwirrend und ich habe so viele Fehler damit gesehen. .clamp(..1.0)
und .clamp(0.0..)
sind einfach viel klarer.
@CryZe macht einen sehr guten Punkt: Selbst wenn Sie mit min = obere Grenze, max = untere Grenze nie einen Fehler machen, müssen Sie immer noch mentale Gymnastik machen, um sich daran zu erinnern, welche Sie verwenden müssen. Diese kognitive Belastung sollte besser für jedes Problem ausgegeben werden, das Sie lösen möchten.
Ich weiß, dass x.clamp(y, z)
mehr erwartet wird, aber vielleicht ist dies eine Gelegenheit für Innovationen;)
Ich habe in der Anfangsphase ziemlich viel mit Bereichen experimentiert und den RFC sogar um mehrere Monate verschoben, damit wir mit inklusiven Bereichen experimentieren konnten. (Dies wurde begonnen, bevor sie stabilisiert wurden)
Ich entdeckte, dass es nicht möglich war, Clamp für exklusive Bereiche auf Gleitkommazahlen zu implementieren. Nur einige Bereichstypen zu unterstützen, aber andere nicht, war ein zu überraschendes Ergebnis. Obwohl ich den RFC mehrere Monate verzögert hatte, um auf diese Weise damit zu experimentieren, entschied ich letztendlich, dass Bereiche nicht die Lösung waren.
@m-ou-se Siehe die Diskussion ab #44095 (Kommentar) und auch #58710 (Rezension).
Bearbeiten: Wie unten erwähnt, enthält die Diskussion im Pull-Request (#58710) mehr Diskussionen über die Designentscheidung als über das Tracking-Problem. Leider wurde dies hier, wo normalerweise Designdiskussionen stattfinden, nicht kommuniziert, aber es wurde diskutiert.
Nur einige Bereichstypen zu unterstützen, andere jedoch nicht, war ein zu überraschendes Ergebnis
Rust behandelt bereits einige Bereiche anders als andere (zB verwendet sie für Iterationen), daher erscheint es mir nicht überraschend, einige Bereiche nur als clamp
Argumente zuzulassen.
Hier ist die hilfreichste Analyse dazu: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351
@Xaeroxe Nur einige Bereichstypen zu unterstützen, andere jedoch nicht, war ein zu überraschendes Ergebnis
Wenn Sie darüber nachgedacht haben, bevor sie sich stabilisiert haben, haben die Zeit und der allgemeine Gebrauch Ihre Meinung geändert oder glauben Sie, dass dies immer noch der Fall ist?
Ich würde argumentieren, dass exklusive Bereiche sowieso nie für Floats implementiert werden sollten, da sie sich anders verhalten als ganze Zahlen (der Bereich 0..10
schließt die untere Grenze ein und schließt die obere Grenze aus, warum also sollte der hypothetische Bereich 0.0...10.0
beide ausschließen?). Ich glaube nicht, dass es überraschend wäre, zumindest für mich.
@varkor Aber dies wurde dann nach einem einzigen Kommentar in der Rezension geändert, ohne dass das Tracking-Problem diskutiert wurde.
Dies könnte zu konfrontativ wirken, versuchen Sie etwas wie "Als ich das Gespräch durchgesehen habe, habe ich kein überzeugendes Argument gefunden, warum wir keine Bereiche verwenden sollten, kann mich jemand darauf hinweisen?".
Ich vermute, das gesuchte Argument ist hier: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351
BEARBEITEN @Xaeroxe hat mich geschlagen :)
hat die zeit und die allgemeine nutzung deine meinung geändert
Bisher nicht, aber Bereiche sind etwas, das ich in meiner täglichen Codierung ziemlich selten verwende. Ich bin offen dafür, mich von Codebeispielen und vorhandenen APIs mit Teilbereichsunterstützung überzeugen zu lassen. Aber selbst wenn wir diese Frage lösen, gibt es noch einige andere ausgezeichnete Punkte, die scottmcm im RFC-Kommentar anspricht, die angesprochen werden müssen. Step
ist beispielsweise nicht auf so vielen Typen wie Ord
implementiert.
Wenn ich dies mit Bereichen implementieren würde ,
Es gibt also einige Gründe, warum wir diesen Ansatz meiner Meinung nach nicht verfolgen sollten.
Die Auswahl der benötigten Bereiche ist neu genug, um eine neue Eigenschaft zu erfordern, und schließt ausdrücklich den gebräuchlichsten Bereich aus, Range
.
Wir sind im RFC-Prozess schon so weit und das einzige, was std daraus gewinnt, ist eine weitere Möglichkeit, .max()
oder .min()
zu schreiben. Ich möchte den RFC nicht wirklich auf den Anfang des Prozesses zurücksetzen, um etwas zu implementieren, was wir bereits in Rust tun können.
Es verdoppelt die Anzahl der Verzweigungen in der Funktion, um einen Anwendungsfall zu berücksichtigen, von dem wir noch nicht sicher sind, dass er existiert. Ich kann das nicht in Benchmarks anzeigen lassen.
... das einzige, was std daraus gewinnt, ist eine weitere Möglichkeit,
.max()
oder.min()
zu schreiben.
Der Hauptpunkt, den ich ansprechen wollte, ist, dass ich diese scheinbare Äquivalenz zwischen .min()
/ .max()
und einseitigen Klemmen mehrmals in der Diskussion gesehen habe, aber die Operationen sind nicht gleichwertig für Gleitkommazahlen so, wie sie NAN
.
Betrachten Sie beispielsweise input.max(0.)
als einen Ausdruck, um negative Zahlen auf Null zu klemmen. Wenn input
nicht NAN
, funktioniert es einwandfrei. Wenn input
jedoch NAN
, wird es zu 0.
ausgewertet. Dies ist fast nie das gewünschte Verhalten; einseitiges Klemmen sollte NAN
Werte beibehalten. (Siehe diesen Kommentar und diesen Kommentar .) Zusammenfassend lässt sich sagen, dass .max()
gut geeignet ist, die größere von zwei Zahlen zu nehmen, aber nicht gut für einseitiges Spannen.
Wir brauchen also einseitige Klemmoperationen (getrennt von .min()
/ .max()
) für Gleitkommazahlen. Andere brachten gute Argumente für den Nutzen von einseitigen Spannvorgängen auch für Nicht-Gleitkomma-Typen. Die nächste Frage ist, wie wir diese Operationen ausdrücken wollen.
.clamp()
mit INFINITY
Mit anderen Worten, fügen Sie keinen einseitigen Spannvorgang hinzu; Sagen Sie den Benutzern einfach, dass sie .clamp()
mit INFINITY
oder NEG_INFINITY
Grenzen verwenden sollen. Sagen Sie den Benutzern beispielsweise, dass sie input.clamp(0., std::f64::INFINITY)
schreiben sollen.
Dies ist sehr ausführlich, was die Benutzer dazu bringt, die falschen .min()
/ .max()
wenn sie sich der Nuancen der NAN
Behandlung nicht bewusst sind. Außerdem hilft es nicht für T: Ord
, und IMO ist es weniger klar als die Alternativen.
.clamp_min()
und .clamp_max()
Eine sinnvolle Option besteht darin, die Methoden .clamp_min()
und .clamp_max()
hinzuzufügen, die keine Änderungen an der derzeit vorgeschlagenen Implementierung erfordern würden. Ich denke, dies ist ein vernünftiger Ansatz; Ich wollte nur sicherstellen, dass wir uns bewusst sind, dass wir diesen Ansatz verwenden müssen, wenn wir die derzeit vorgeschlagene Implementierung von clamp
stabilisieren.
Eine andere Option ist, clamp
ein Bereichsargument zu verwenden. @Xaeroxe hat eine Möglichkeit gezeigt, dies zu implementieren, aber diese Implementierung hat, wie er erwähnt, einige Nachteile. Eine alternative Möglichkeit, die Implementierung zu schreiben, ähnelt der derzeit implementierten Slicing-Methode (das Merkmal SliceIndex
). Dies löst alle Einwände, die ich in der Diskussion gesehen habe, mit Ausnahme der Bedenken hinsichtlich der Bereitstellung von Implementierungen für eine Teilmenge von Bereichstypen und der zusätzlichen Komplexität. Ich stimme zu, dass es etwas Komplexität hinzufügt, aber IMO ist es nicht viel schlimmer, als .clamp_min()
/ .clamp_max()
hinzuzufügen. Für Ord
würde ich so etwas vorschlagen:
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
}
}
Einige Gedanken dazu:
T: Ord + Step
.Clamp
nur nachts beibehalten, ähnlich dem Merkmal SliceIndex
.Um f32
/ f64
, könnten wir
Lockern Sie die Implementierungen auf T: PartialOrd
. (Ich bin mir nicht sicher, warum die aktuelle Implementierung von clamp
auf Ord
statt auf PartialOrd
. Vielleicht habe ich etwas in der Diskussion übersehen? Es scheint wie PartialOrd
würde reichen.)
oder schreiben Sie Implementierungen speziell für f32
und f64
. (Wenn gewünscht, können wir später jederzeit ohne Breaking Change auf Option i umsteigen.)
und dann hinzufügen
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)
}
}
Wir könnten Clamp
für exklusive Bereiche mit f32
/ f64
später implementieren, falls gewünscht. ( @scottmcm kommentierte, dass dies nicht einfach ist, weil std
absichtlich keine f32
/ f64
Vorgängeroperationen hat. Ich bin mir nicht sicher warum std
hat diese Operationen nicht; vielleicht Probleme mit denormalen Nummern? Unabhängig davon könnte das später behoben werden.)
Auch wenn wir keine Implementierungen von Clamp
für exklusive Bereiche mit f32
/ f64
hinzufügen, bin ich nicht der Meinung, dass dies zu überraschend wäre. Wie @varkor darauf hinweist, behandelt Rust verschiedene Bereichstypen bereits unterschiedlich im Sinne von Copy
und Iterator
/ IntoIterator
. (IMO, dies ist eine Warze von std
, aber es ist mindestens ein Fall, in dem Bereichstypen unterschiedlich behandelt werden.) Außerdem wäre die Fehlermeldung leicht verständlich, wenn jemand versucht hat, einen exklusiven Bereich zu verwenden ( "das merkmalgebundene std::ops::Range<f32>: Clamp<f32>
ist nicht erfüllt").
Ich habe Output
einem zugeordneten Typ gemacht, um maximale Flexibilität für das Hinzufügen weiterer Implementierungen in der Zukunft zu bieten, aber das ist nicht unbedingt erforderlich.
Grundsätzlich lässt uns dieser Ansatz in Bezug auf die Merkmalsgrenzen so viel Flexibilität, wie wir möchten. Es ermöglicht auch, mit einem minimal nützlichen Satz von Clamp
Implementierungen zu beginnen und später weitere Implementierungen hinzuzufügen, ohne Änderungen zu beeinträchtigen.
Der Ansatz " .clamp()
mit INFINITY
" hat, wie oben erwähnt, erhebliche Nachteile.
Der Ansatz "vorhanden .clamp
" + .clamp_min()
+ .clamp_max()
hat folgende Nachteile:
input.clamp_min(0)
statt input.clamp(0..)
..clamp()
hinzufügen (ohne weitere Methoden hinzuzufügen). Zum Beispiel können wir das Festlegen eines u32
Werts mit u8
Grenzen nicht unterstützen, was eine angeforderte Funktion aus der RFC-Diskussion ist . Dieses spezielle Beispiel kann mit einer .saturating_into()
Funktion besser gehandhabt werden, aber es kann andere Beispiele geben, bei denen mehr Klemmimplementierungen nützlich wären..min()
, .max()
, .clamp_min()
und .clamp_max()
für einseitiges Spannen verwechseln. (Das Spannen mit .clamp_min()
ähnelt der Verwendung von .max()
, und das Spannen mit .clamp_max()
ähnelt der Verwendung von .min()
.) einseitige Spannvorgänge .clamp_lower()
/ .clamp_upper()
oder .clamp_to_start()
/ .clamp_to_end()
statt .clamp_min()
/ .clamp_max()
, obwohl das noch ausführlicher ( input.clamp_lower(0)
gegenüber input.clamp(0..)
).Der Ansatz mit Range-Argumenten hat die folgenden Nachteile:
.clamp_min()
/ .clamp_max()
.Clamp
für die exklusiven Bereichstypen zu implementieren oder zu implementieren, kann dies überraschend sein.Ich habe keine starke Meinung zu dem Ansatz "existierende .clamp
" + .clamp_min()
+ .clamp_max()
Vergleich zum Ansatz mit Range-Argumenten. Es ist ein Kompromiss.
@Xaeroxe
Es verdoppelt die Anzahl der Verzweigungen in der Funktion, um einen Anwendungsfall zuberücksichtigen,von dem wir noch nicht sicher sind, dass er existiert.Ich kann das nicht in Benchmarks anzeigen lassen.
Vielleicht wird der zusätzliche Zweig von LLVM wegoptimiert?
Bei einseitigem Spannen
Da die Klemmung auf beiden Seiten inklusive ist, kann man nur die min/max links/rechts angeben um das einseitige Klemmverhalten zu erhalten. Ich denke, das ist völlig akzeptabel und wohl schöner als .clamp((Bound::Unbounded, Inclusive(3.2)))
wo es sowieso keinen Typ Range*
:
x.clamp(i32::MIN, 10);
x.clamp(-f32::INFINITY, 10.0);
Es gibt keinen Leistungsverlust, da LLVM trivialerweise in der Lage ist, die tote Seite weg zu optimieren: https://rust.godbolt.org/z/l_uBLO
Bereichssyntax wäre cool, aber clamp
ist einfach genug, dass zwei separate Argumente in Ordnung und leicht zu verstehen sind.
Vielleicht kann die Handhabung von min
/ max
NaN
von selbst behoben werden, zB durch Ändern der Implementierung der inhärenten Methoden von f32
? Oder die PartialOrd::min/max
? (mit einem Edition-Flag, vorausgesetzt, es gelingt Rust, einen Weg zu finden, Dinge in libstd umzuschalten).
@scottmcm Sie sollten RangeToInclusive ausprobieren .
Nachdem ich darüber nachgedacht habe, ist mir aufgefallen, dass stabil für immer ist, also sollten wir das "Zurücksetzen des RFC-Prozesses" nicht als Grund betrachten, keine Änderung vorzunehmen.
Zu diesem Zweck möchte ich zu der Denkweise zurückkehren, die ich bei der Umsetzung hatte. Clamp arbeitet konzeptionell über einen Bereich, daher ist es sinnvoll, das Vokabular zu verwenden, das Rust bereits zum Ausdrücken von Bereichen hat. Das war meine reflexartige Reaktion, und es scheint die Reaktion vieler anderer Leute zu sein. Lassen Sie uns also die Argumente dafür wiederholen, dass wir es nicht auf diese Weise tun, und sehen, ob wir sie widerlegen können.
Die Auswahl der benötigten Bereiche ist neu genug, um eine neue Eigenschaft zu erfordern, und schließt ausdrücklich den gebräuchlichsten Bereich aus, Range
.
Range*
Typen wie Ord + Step
hinzuzufügen, um Werte für exklusive Bereiche korrekt zurückzugeben. Auch wenn eine exklusive Range-Klemme oft nicht wirklich benötigt wird, können wir hier tatsächlich die gesamte Range an Ranges akzeptieren, ohne die Schnittstelle von Ranges zu beeinträchtigen, die diese technischen Einschränkungen nicht haben.Wir können nur Infinity/Min/Max für einseitiges Spannen verwenden.
Range*
Syntax weniger Zeichen und weniger Importe für diesen Anwendungsfall umfasst.Nachdem wir die Gründe dafür widerlegt haben, fehlt diesem Kommentar jegliche Motivation, die Änderung vorzunehmen, da die Optionen gleichwertig erscheinen. Lassen Sie uns etwas Motivation finden, um die Änderung vorzunehmen. Ich habe nur einen Grund, nämlich dass die allgemeine Meinung in diesem Thread zu sein scheint, dass der bereichsbasierte Ansatz die Semantik der Sprache verbessert. Nicht nur für die inklusive Double-Ended-Range-Clamp, sondern auch für Funktionen wie .min()
und .max()
.
Ich bin gespannt, ob dieser Gedankengang bei anderen Anklang findet, die dafür sind, den RFC so zu stabilisieren, wie er ist.
Ich denke, es wäre besser, Clamp in der aktuellen Form zu belassen, da es jetzt anderen Sprachen sehr ähnlich ist.
Als ich an meinem Pull-Request #58710 arbeitete, versuchte ich, eine Range-basierte Implementierung zu verwenden.
Aber rust-lang/rfcs#1961 (Kommentar) ) hat mich überzeugt, dass es in der Standardform besser ist.
Ich denke, es wäre logisch, ein #[must_use]
Attribut für die Funktion zu haben, um Leute, die nicht daran gewöhnt sind, wie Rostnumerik funktioniert, nicht zu verwirren. Das heißt, ich könnte leicht erkennen, dass jemand den folgenden (falschen) Code schreibt:
let mut x: f64 = some_number_source();
x.clamp(0.0, 1.0);
//Proceeds to assume that 0.0 <= x <= 1.0
Im Allgemeinen verfolgt Rost einen (number).method()
Ansatz für Numerik (während andere Sprachen Math.Method(number)
), aber selbst wenn man dies im Hinterkopf behält, wäre es eine logische Annahme, dass dies number
. Das ist mehr Lebensqualität als alles andere.
Das Attribut [must_use]
wurde kürzlich hinzugefügt.
@ Xaeroxe Hast du dir was für Range-based Clamp einfallen lassen?
Ich denke, dass die Funktion, wie sie jetzt ist, am besten zu den anderen numerischen Funktionen von Rost passt und möchte sie wieder stabilisieren.
Im Moment sehe ich keinen Grund, mich für eine bereichsbasierte Klemme zu entscheiden. Ja, fügen wir das must_use-Attribut hinzu und arbeiten an der Stabilisierung.
@SimonSapin @scottmcm Könnten wir den Stabilisierungsprozess neu starten?
Wie @jturner314 sagte, wäre es großartig, eine Klemme auf PartialOrd anstelle von Ord zu haben, damit sie auch auf Floats verwendet werden kann.
Wir haben in dieser Ausgabe bereits die spezialisierten f32::clamp
und f64::clamp
.
Das versuche ich zu tun:
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())
}
PartialOrd
ist kein reines Float-Merkmal. Eine float-spezifische Methode macht Clamp nicht für benutzerdefinierte PartialOrd
Typen verfügbar.
Die aktuelle Implementierung erfordert Eq
, obwohl sie es nicht verwendet.
Das Hauptproblem bei PartialOrd
war, dass es schwächere Garantien bietet, was wiederum die Garantien der Klemme schwächt. Benutzer, die dies auf PartialOrd
wünschen, könnten an einer anderen Funktion interessiert sein, die ich geschrieben habe https://docs.rs/num/0.2.1/num/fn.clamp.html
Was sind das für Garantien?
Eine ziemlich natürliche Erwartung ist, dass wenn x.clamp(a, b) == x
dann a <= x && x <= b
. Dies ist bei PartialCmp
nicht garantiert, wobei x
mit a
oder b
.
Kam heute hierher auf der Suche nach vage erinnerten clamp()
und habe die Diskussion mit Interesse gelesen.
Ich würde vorschlagen, den "Optionstrick" als Kompromiss zwischen dem Zulassen beliebiger Bereiche und mehreren benannten Funktionen zu verwenden. Ich weiß, dass dies bei einigen nicht beliebt ist, aber es scheint die gewünschte Semantik hier gut einzufangen:
#![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));
}
Wenn dies in std
, könnte auch eine pauschale Implementierung für T: Ord
, die die Bedenken bezüglich einer allgemeinen PartialOrd
Implementierung abdecken würde.
Angesichts der Tatsache, dass das Definieren einer clamp()
Funktion im Benutzercode derzeit standardmäßig eine Compiler-Warnung zu instabilen Namenskollisionen generiert, denke ich, dass der Name "clamp" für diese Funktion in Ordnung ist.
Ich denke, dass sich clamp(a,b,c)
genauso verhalten sollte wie min(max(a,b), c)
.
Da max
und min
für PartialOrd
nicht implementiert sind, sollte auch clamp
nicht implementiert werden.
Das Problem mit NaN
wurde bereits besprochen .
@EdorianDark, ich stimme zu. min, max sollte auch nur PartialOrd erfordern.
@noonien min
und max
sind seit Rust 1.0 definiert und erfordern Ord
und haben eine Definition für f32
und f64
.
Dies ist nicht der richtige Ort, um diese Funktion zu diskutieren.
Hier können wir nur dafür sorgen, dass sich min
, max
und clamp
vergleichbar und nicht überraschend verhalten.
Edit: Ich mag die Situation mit PartialOrd
und würde lieber float
Ord
implementieren lassen, aber dies ist nach 1.0 nicht mehr möglich.
Diese ist seit etwa anderthalb Jahren zusammengeführt und instabil. Wie halten wir es, dies zu stabilisieren?
Das würde ich gerne stabilisieren!
Wenn ein Konflikt mit dem clamp
Methodennamen wie ein Problem klingt, habe ich vorgeschlagen, die Namensauflösung an einer Stelle in https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812 zu ändern, und es würde helfen auch damit.
@Xaeroxe Ich denke, der Prozess besteht darin, Stabilisierungs-PR einzureichen und um einen Konsens des libs-Teams dazu zu bitten. Es scheint, dass t-libs überladen ist und mit nicht-fcped Dingen nicht Schritt halten kann.
Okay, habe das https://github.com/rust-lang/rust/pull/77872
@matklad tatsächlich wurde bereits letztes Jahr ein FCP-Vorschlag unter https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 gestartet, aber er bleibt hängen, da noch ein Kontrollkästchen übrig ist.
In diesem Fall denke ich, dass es ziemlich erträglich ist, einmal im Jahr zu einem Thema gepingt zu werden.
@Kimundi
@sfackler
@ohneboote
https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 wartet noch auf Ihre Aufmerksamkeit
Das libs-Team hat sich seit dem Start des FCP ziemlich verändert. Was halten Sie alle davon, einfach einen neuen FCP in der Stabilisierungs-PR zu starten? Das sollte nicht länger dauern, als auf die verbleibenden Kontrollkästchen hier zu warten.
@LukasKalbertodt gut von mir, macht es dir etwas aus, damit
Abbrechen des FCP hier, da dieser FCP jetzt auf dem Stabilisierungs-PR passiert ist: https://github.com/rust-lang/rust/pull/77872#issuecomment -722982535
@fcpbot abbrechen
äh
@rfcbot abbrechen
@m-ou-se-Vorschlag storniert.
Hilfreichster Kommentar
Es scheint, als wären wir an einem ziemlich schlechten Ort, wenn eine Methode, die die Leute genug wollten, um ein Erweiterungsmerkmal zu definieren, niemals zur Standardbibliothek hinzugefügt werden kann.