Rust: Tracking-Problem für Klemmen-RFC

Erstellt am 26. Aug. 2017  ·  101Kommentare  ·  Quelle: rust-lang/rust

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:

  • [x] Lassen Sie RFC die letzte Kommentarfrist passieren
  • [x] RFC implementieren
  • [ ] Stabilisieren
B-unstable C-tracking-issue Libs-Tracked T-libs

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.

Alle 101 Kommentare

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,

  1. 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.

  2. 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.

  3. 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.

  • Name und Shadowing ist nicht ideal, aber es funktioniert
  • für numerische Vektoren und Matrizen denke ich, dass max / min / clamp nicht ideal sind, aber dies wird dadurch gelöst, dass Ord überhaupt nicht verwendet wird. Ndarray möchte elementweise und generische Argumentklammern (Skalar oder Array) durchführen, aber Ord wird von uns oder ähnlichen Bibliotheken nicht verwendet. Also keine Sorge.
  • Vorhandene zusammengesetzte Typen, die nicht numerisch sind: BtreeMap erhält mit dieser Änderung eine Methodenklemme. Macht das generell Sinn? Kann es eine vernünftige Bedeutung dafür implementieren, abgesehen von der Vorgabe?
  • Der Aufrufmodus nach Wert passt nicht zu jeder Implementierung. Auch hier BtreeMap. Soll Klemme 3 Karten verbrauchen und eine davon zurückgeben?

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:

  1. value.clamp(min, max) (befolgen Sie den RFC wie er ist)
  2. value.clamp(min..=max)
  3. (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:

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

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:

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

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:

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

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.

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.

  1. 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 .

  2. 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.

  3. 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.

Notwendigkeit für einseitige Spannvorgänge

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

So drücken Sie einseitige Spannvorgänge aus

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

Bereichsargument

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:

  • Wir könnten Implementierungen für exklusive Bereiche hinzufügen, in denen T: Ord + Step .
  • Wir könnten das Merkmal Clamp nur nachts beibehalten, ähnlich dem Merkmal SliceIndex .
  • Um f32 / f64 , könnten wir

    1. 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.)

    2. 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.

Vergleich der Optionen

Der Ansatz " .clamp() mit INFINITY " hat, wie oben erwähnt, erhebliche Nachteile.

Der Ansatz "vorhanden .clamp " + .clamp_min() + .clamp_max() hat folgende Nachteile:

  • Es ist ausführlicher, zB input.clamp_min(0) statt input.clamp(0..) .
  • Es unterstützt keine exklusiven Bereiche.
  • Wir können in Zukunft keine weiteren Implementierungen von .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.
  • Jemand kann zwischen .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:

  • Die Implementierung ist komplexer als das Hinzufügen von .clamp_min() / .clamp_max() .
  • Wenn wir uns entscheiden, 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 zu berü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 .

    • Mit der neuen Implementierung von @jturner314 haben wir jetzt die Möglichkeit, weitere Einschränkungen für bestimmte 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.

    • Das stimmt, und ein großer Teil der Gründe, warum diese Änderung meiner Meinung nach kein wirklich starkes Mandat ist. Ich habe nur eine Antwort darauf, und das ist, dass die 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())
}

Link zum Spielplatz.

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.

@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.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen