Wie unterscheidet sich die Syntax für <> von einer regulären Lebensdauergrenze?

75

Betrachten Sie den folgenden Code:

trait Trait<T> {}

fn foo<'a>(_b: Box<dyn Trait<&'a usize>>) {}
fn bar(_b: Box<dyn for<'a> Trait<&'a usize>>) {}

Beide Funktionen foound barscheinen ein zu akzeptieren Box<Trait<&'a usize>>, obwohl fooes prägnanter ist als bar. Was ist der Unterschied zwischen ihnen?

In welchen Situationen würde ich außerdem die oben beschriebene for<>Syntax benötigen ? Ich weiß, dass die Rust-Standardbibliothek sie intern verwendet (häufig im Zusammenhang mit Schließungen), aber warum benötigt mein Code sie möglicherweise?

George Hilliard
quelle

Antworten:

107

for<>Die Syntax wird als höherrangiges Trait Bound (HRTB) bezeichnet und wurde in der Tat hauptsächlich aufgrund von Schließungen eingeführt.

Kurz gesagt, der Unterschied zwischen foound barbesteht darin, dass in foo()der Lebensdauer für die interne usizeReferenz vom Aufrufer der Funktion bereitgestellt wird , während in bar()derselben Lebensdauer die Funktion selbst bereitgestellt wird . Und diese Unterscheidung ist sehr wichtig für die Umsetzung von foo/ bar.

In diesem speziellen Fall Traitist diese Unterscheidung jedoch sinnlos , wenn keine Methoden vorhanden sind, die den Typparameter verwenden. Stellen Sie sich also TraitFolgendes vor:

trait Trait<T> {
    fn do_something(&self, value: T);
}

Denken Sie daran, dass die Lebensdauerparameter den generischen Typparametern sehr ähnlich sind. Wenn Sie eine generische Funktion verwenden, geben Sie immer alle Typparameter an und geben konkrete Typen an. Der Compiler monomorphisiert die Funktion. Gleiches gilt für Lebensdauerparameter: Wenn Sie eine Funktion mit einem Lebensdauerparameter aufrufen, geben Sie die Lebensdauer an, wenn auch implizit:

// imaginary explicit syntax
// also assume that there is TraitImpl::new::<T>() -> TraitImpl<T>,
// and TraitImpl<T>: Trait<T>

'a: {
    foo::<'a>(Box::new(TraitImpl::new::<&'a usize>()));
}

Und jetzt gibt es eine Einschränkung, was foo()mit diesem Wert geschehen kann, dh mit welchen Argumenten er aufgerufen werden kann do_something(). Dies wird beispielsweise nicht kompiliert:

fn foo<'a>(b: Box<Trait<&'a usize>>) {
    let x: usize = 10;
    b.do_something(&x);
}

Dies wird nicht kompiliert, da lokale Variablen eine Lebensdauer haben, die streng kleiner ist als die durch die Lebensdauerparameter angegebene Lebensdauer (ich denke, es ist klar, warum dies so ist). Sie können daher nicht aufrufen, b.do_something(&x)da für das Argument eine Lebensdauer erforderlich 'aist streng größer als das von x.

Sie können dies jedoch tun mit bar:

fn bar(b: Box<for<'a> Trait<&'a usize>>) {
    let x: usize = 10;
    b.do_something(&x);
}

Dies funktioniert, da jetzt baranstelle des Anrufers von die erforderliche Lebensdauer ausgewählt werden kann bar.

Dies ist wichtig, wenn Sie Verschlüsse verwenden, die Referenzen akzeptieren. Angenommen, Sie möchten eine filter()Methode schreiben für Option<T>:

impl<T> Option<T> {
    fn filter<F>(self, f: F) -> Option<T> where F: FnOnce(&T) -> bool {
        match self {
            Some(value) => if f(&value) { Some(value) } else { None }
            None => None
        }
    }
}

Der Abschluss hier muss einen Verweis auf akzeptieren, Tda es sonst unmöglich wäre, den in der Option enthaltenen Wert zurückzugeben (dies ist die gleiche Begründung wie bei filter()Iteratoren).

Aber was Lebensdauer soll &Tin FnOnce(&T) -> boolhaben? Denken Sie daran, dass wir in Funktionssignaturen keine Lebensdauern angeben, nur weil eine Lebensdauerelision vorhanden ist. Tatsächlich fügt der Compiler für jede Referenz einen Lebensdauerparameter in eine Funktionssignatur ein. Es sollte eine gewisse Lebensdauer mit &Tin verbunden sein FnOnce(&T) -> bool. Der "offensichtlichste" Weg, um die Signatur oben zu erweitern, wäre folgender:

fn filter<'a, F>(self, f: F) -> Option<T> where F: FnOnce(&'a T) -> bool

Dies wird jedoch nicht funktionieren. Wie im Beispiel mit Traitoben, Lebensdauer 'aist streng länger als die Lebensdauer einer lokalen Variablen in dieser Funktion, einschließlich der valuein der Spiel - Anweisung. Daher ist es nicht möglich anzuwenden , fum &valuewegen der Lebensdauer stimmt nicht überein. Die obige Funktion, die mit einer solchen Signatur geschrieben wurde, wird nicht kompiliert.

Auf der anderen Seite, wenn wir die Signatur von so erweitern filter()(und so funktioniert die lebenslange Elision für Verschlüsse jetzt in Rust):

fn filter<F>(self, f: F) -> Option<T> where F: for<'a> FnOnce(&'a T) -> bool

Dann ist das Aufrufen fmit &valueals Argument vollkommen gültig: Wir können jetzt die Lebensdauer auswählen, daher ist die Verwendung der Lebensdauer einer lokalen Variablen absolut in Ordnung. Und deshalb sind HRTBs wichtig: Ohne sie können Sie nicht viele nützliche Muster ausdrücken.

Sie können auch eine andere Erklärung zu HRTBs in Nomicon lesen .

Vladimir Matveev
quelle
Genial! Das ist wirklich klar, danke. Hoffentlich ist dies die kanonische Antwort auf diese Frage. Als Folge (und wenn Sie einen Link haben, wäre das in Ordnung, da er meiner Meinung nach etwas über den Rahmen meiner ursprünglichen Frage hinausgeht): Warum sind sie höherrangige Merkmalsgrenzen? Haben sie etwas mit höherwertigen Typen zu tun (mit denen ich ein wenig vertraut bin)?
George Hilliard
1
@thirtythreeforty ja, ich glaube, sie werden genau nach HKTs so genannt, weil sie ihnen ähneln. Ich kann mir vorstellen, dass HKTs auch geschrieben werden könnten for, wenn sie verfügbar wären: for<T> Monad<T>oder zumindest ähnliche Konzepte haben - eine unendliche Anzahl von Merkmalsgrenzen (oder Typen, im Fall von HKTs), die mit etwas parametrisiert sind (Lebensdauern oder Typen) ). Es ist jedoch denkbar, dass HRTBs auch Typen von Lebensdauern unterstützen. Es ist nur so, dass noch niemand ein konkretes Design entwickelt hat.
Vladimir Matveev
2
Ich denke, höherwertige Typen sind orthogonal zu höherrangigen Typen. Typen mit höherem Rang beschreiben, wo eine Quantifizierung in einer Typensignatur auftreten kann, und Typen mit höherem Rang beziehen sich auf das Schreiben von polymorphem Code über Typkonstruktoren. Siehe auch: stackoverflow.com/questions/13317768/…
BurntSushi5
1
@ BurntSushi5 na ja, ich verwechsle sie immer für einander :(
Vladimir Matveev
Ist HRTB der Rust-Name für Haskell's QuantifiedConstraints?
Dspyz