Rust: Tracking-Problem für "impl Trait" (RFC 1522, RFC 1951, RFC 2071)

Erstellt am 27. Juni 2016  ·  417Kommentare  ·  Quelle: rust-lang/rust

NEUES TRACKING-PROBLEM = https://github.com/rust-lang/rust/issues/63066

Implementierungsstatus

Das grundlegende Feature gemäß RFC 1522 ist implementiert, es gab jedoch noch Überarbeitungen, an denen noch gearbeitet werden muss:

RFCs

Es gab eine Reihe von RFCs zu Impl-Eigenschaften, die alle von diesem zentralen Tracking-Thema verfolgt werden.

Ungelöste Fragen

Die Umsetzung hat auch eine Reihe interessanter Fragen aufgeworfen:

  • [x] Welche Priorität hat das Schlüsselwort impl beim Parsen von Typen? Diskussion: 1
  • [ ] Sollten wir impl Trait nach -> in fn Typen oder Zucker in Klammern zulassen? #45994
  • [ ] Müssen wir für alle Funktionen einen DAG festlegen, um eine automatische Leckage zu ermöglichen, oder können wir eine Art Zurückstellung verwenden. Diskussion: 1

    • Präsenssemantik: DAG.

  • [x] Wie sollten wir das Impl-Trait in regionck integrieren? Diskussion: 1 , 2
  • [ ] Sollten wir die Angabe von Typen zulassen, wenn einige Parameter implizit und andere explizit sind? zB fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Einige Bedenken bezüglich der Verwendung von verschachtelten Impl-Traits](https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] Sollte die Syntax in einem Impl existential type Foo: Bar oder type Foo = impl Bar lauten? ( siehe hier für Diskussion )
  • [ ] Sollte die Menge der "Definitionsverwendungen" für ein existential type in einem Impl nur Elemente des Impl sein oder verschachtelte Elemente innerhalb der Impl-Funktionen usw. enthalten? ( siehe zum Beispiel hier )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Hilfreichster Kommentar

Da dies die letzte Chance vor der Schließung des FCP ist, möchte ich ein letztes Argument gegen automatische automatische Merkmale anführen. Mir ist klar, dass dies ein bisschen in letzter Minute ist, daher möchte ich dieses Problem höchstens formell ansprechen, bevor wir uns auf die aktuelle Implementierung festlegen.

Um es allen klarzustellen, die impl Trait nicht impl X Typen repräsentiert wird, implementiert derzeit automatisch Auto-Traits, wenn und nur wenn der konkrete Typ dahinter diese Auto-Traits implementiert. Konkret, wenn die folgende Codeänderung vorgenommen wird, wird die Funktion weiterhin kompiliert, aber alle Verwendungen der Funktion, die darauf beruhen, dass der zurückgegebene Typ Send implementiert,

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(einfaches Beispiel: Funktionieren , interne Änderungen führen zum Scheitern )

Dieses Problem ist nicht eindeutig. Es gab eine sehr absichtliche Entscheidung, Auto-Traits "lecken" zu lassen: Wenn nicht, müssten wir + !Send + !Sync auf jede Funktion setzen, die etwas zurückgibt, das nicht Send oder Non-Sync zurückgibt, und wir würden haben eine unklare Geschichte mit potenziellen anderen benutzerdefinierten automatischen Merkmalen, die für den konkreten Typ, den die Funktion zurückgibt, einfach nicht implementierbar sein könnten. Dies sind zwei Probleme, auf die ich später eingehen werde.

Zuerst möchte ich einfach meinen Einwand gegen das Problem vorbringen: Dies ermöglicht es, einen Funktionskörper zu ändern, um die öffentlich zugängliche API zu ändern. Dies reduziert direkt die Wartbarkeit des Codes.

Während der Entwicklung von Rost wurden Entscheidungen getroffen, die auf der Seite der Ausführlichkeit über der Benutzerfreundlichkeit liegen. Wenn Neulinge diese sehen, denken sie, es sei Ausführlichkeit um der Ausführlichkeit willen, aber das ist nicht der Fall. Jede Entscheidung, ob Strukturen nicht automatisch Copy implementieren oder alle Typen explizit bei Funktionssignaturen sind, dient der Wartbarkeit.

Wenn ich den Leuten Rust vorstelle, kann ich ihnen Geschwindigkeit, Produktivität und Speichersicherheit zeigen. Aber gehen hat Geschwindigkeit. Ada hat Speichersicherheit. Python hat Produktivität. Was Rust hat, übertrumpft all dies, es ist wartbar. Wenn ein Bibliotheksautor einen effizienteren Algorithmus ändern oder die Struktur einer Crate neu erstellen möchte, hat der Compiler eine starke Garantie dafür, dass er ihn benachrichtigt, wenn er Fehler macht. Bei Rost kann ich sicher sein, dass mein Code nicht nur in Bezug auf Speichersicherheit, sondern auch Logik und Schnittstelle weiterhin funktioniert. _Jede Funktionsschnittstelle in Rust ist durch die Typdeklaration der Funktion vollständig darstellbar_.

Die Stabilisierung von impl Trait hat eine große Chance, diesem Glauben zu widersprechen. Sicher, es ist sehr schön, schnell Code zu schreiben, aber wenn ich Prototypen erstellen möchte, verwende ich Python. Rust ist die Sprache der Wahl, wenn eine langfristige Wartbarkeit und kein kurzfristiger schreibgeschützter Code erforderlich ist.


Ich sage, es besteht nur eine "große Chance", dass dies hier schlecht ist, denn auch hier ist das Thema nicht eindeutig. Die ganze Idee von 'Auto-Eigenschaften' ist in erster Linie nicht explizit. Send und Sync werden basierend auf dem Inhalt einer Struktur implementiert, nicht auf der öffentlichen Deklaration. Da diese Entscheidung für Rost aufgegangen ist, könnte impl Trait ähnlich vorgehen, auch gut funktionieren.

Funktionen und Strukturen werden jedoch in einer Codebasis unterschiedlich verwendet, und dies sind nicht dieselben Probleme.

Beim Ändern der Felder einer Struktur, sogar privater Felder, ist sofort klar, dass der eigentliche Inhalt geändert wird. Strukturen mit Nicht-Senden- oder Nicht-Sync-Feldern haben diese Wahl getroffen, und Bibliotheksverwalter wissen, dass sie doppelt prüfen müssen, wenn ein PR die Felder einer Struktur ändert.

Wenn Sie die Interna einer Funktion ändern, ist es definitiv klar, dass dies sowohl die Leistung als auch die Korrektheit beeinträchtigen kann. In Rust müssen wir jedoch nicht überprüfen, ob wir den richtigen Typ zurückgeben. Funktionsdeklarationen sind ein harter Vertrag, den wir einhalten müssen, und rustc wacht über uns. Es ist ein schmaler Grat zwischen automatischen Merkmalen in Strukturen und in Funktionsrückgaben, aber das Ändern der Interna einer Funktion ist viel Routine. Sobald wir volle generatorbetriebene Future s haben, wird es noch routinemäßiger sein, Funktionen zu ändern, die -> impl Future . Dies sind alles Änderungen, die Autoren auf geänderte Send/Sync-Implementierungen überprüfen müssen, wenn der Compiler sie nicht erkennt.

Um dies zu beheben, könnten wir entscheiden, dass dies ein akzeptabler Wartungsaufwand ist, wie dies in der ursprünglichen RFC-Diskussion der Fall war . In diesem Abschnitt im konservativen Impl-Trait-RFC werden die

Ich habe meine Hauptantwort darauf bereits dargelegt, aber hier noch eine letzte Anmerkung. Das Ändern des Layouts einer Struktur ist nicht so üblich; davor kann man sich schützen. Der Wartungsaufwand, um sicherzustellen, dass Funktionen weiterhin dieselben automatischen Merkmale implementieren, ist größer als der von Strukturen, einfach weil sich Funktionen viel mehr ändern.


Abschließend möchte ich sagen, dass automatische automatische Merkmale nicht die einzige Option sind. Wir haben uns für diese Option entschieden, aber die Alternative zur Deaktivierung von automatischen Merkmalen ist immer noch eine Alternative.

Wir könnten verlangen, dass Funktionen, die Nicht-Sende-/Nicht-Sync-Elemente zurückgeben, entweder + !Send + !Sync oder ein Merkmal (möglicherweise ein Alias?) zurückgeben, das diese Grenzen hat. Das wäre keine gute Entscheidung, aber vielleicht besser als die, die wir derzeit wählen.

Was die Bedenken bezüglich benutzerdefinierter automatischer Eigenschaften angeht, würde ich argumentieren, dass keine neuen automatischen Eigenschaften nur für neue Typen implementiert werden sollten, die nach der automatischen Eigenschaft eingeführt wurden. Dies könnte ein größeres Problem darstellen, als ich jetzt ansprechen kann, aber es ist kein Problem, das wir nicht mit mehr Design angehen können.


Dies ist sehr spät und sehr langwierig, und ich bin sicher, dass ich diese Einwände schon einmal vorgebracht habe. Ich freue mich, nur ein letztes Mal kommentieren zu können und sicherzustellen, dass wir mit der Entscheidung, die wir treffen, voll und ganz einverstanden sind.

Vielen Dank fürs Lesen, und ich hoffe, dass die endgültige Entscheidung Rust in die beste Richtung weist, in die es gehen kann.

Alle 417 Kommentare

@aturon Können wir den RFC tatsächlich in das Repository stellen? ( @mbrubeck hat dort kommentiert, dass dies ein Problem sei.)

Fertig.

Erster Implementierungsversuch ist #35091 (zweiter, wenn man meinen Zweig vom letzten Jahr mitzählt).

Ein Problem, auf das ich gestoßen bin, ist mit Lebenszeiten. Typinferenz legt gerne Regionsvariablen _überall_ ab, und ohne Änderungen bei der Regionsprüfung ziehen diese Variablen nur auf lokale Gültigkeitsbereiche.
Der konkrete Typ _muss_ jedoch exportierbar sein, also habe ich ihn auf 'static und explizit früh gebundene Lebenszeitparameter benannt, aber es ist _nie_ einer davon, wenn irgendeine Funktion beteiligt ist - selbst ein String-Literal folgert nicht auf 'static , es ist so ziemlich völlig nutzlos.

Eine Sache, an die ich dachte, die keinen Einfluss auf die Regionsprüfung selbst hätte, ist das Löschen von Lebensdauern:

  • nichts, was den konkreten Typ eines impl Trait enthüllt, sollte sich um Lebensdauern kümmern - eine schnelle Suche nach Reveal::All legt nahe, dass dies bereits im Compiler der Fall ist
  • eine Grenze muss auf alle konkreten Typen von impl Trait im Rückgabetyp einer Funktion gesetzt werden, damit sie den Aufruf dieser Funktion überlebt - das bedeutet, dass jede Lebensdauer notwendigerweise entweder 'static oder einer der Lebensdauerparameter der Funktion - _even_ wenn wir nicht wissen können welcher (zB "kürzester von 'a und 'b ")
  • wir müssen eine Varianz für den impliziten Lebenszeitparametrismus von impl Trait wählen (dh für alle Lebenszeitparameter im Gültigkeitsbereich, genau wie bei Typparametern): Invarianz ist am einfachsten und gibt dem Angerufenen mehr Kontrolle, während Kontravarianz es dem Anrufer ermöglicht mehr und würde eine Überprüfung erfordern, dass sich jede Lebensdauer im Rückgabetyp an einer kontravarianten Position befindet (das gleiche gilt für den kovarianten Typparametrismus anstelle von invariant)
  • Der automatische Merkmals-Leaking-Mechanismus erfordert, dass dem konkreten Typ in einer anderen Funktion eine Merkmalsgrenze zugewiesen werden kann - da wir die Lebensdauern gelöscht haben und keine Ahnung haben, welche Lebensdauer wohin gehört, muss jede gelöschte Lebensdauer im konkreten Typ ersetzt werden mit einer neuen Inferenzvariable, die garantiert nicht kürzer ist als die kürzeste Lebensdauer aller tatsächlichen Lebensdauerparameter; Das Problem liegt in der Tatsache, dass Trait Impls am Ende stärkere Lifetime-Beziehungen erfordern können (zB X<'a, 'a> oder X<'static> ), die erkannt und mit Fehlern versehen werden müssen, da sie für diese nicht bewiesen werden können Lebenszeiten

Der letzte Punkt über das Durchsickern von Auto-Trait ist meine einzige Sorge, alles andere scheint einfach zu sein.
Es ist an dieser Stelle nicht ganz klar, wie viel von der Regionsüberprüfung wir unverändert wiederverwenden können. Hoffentlich alle.

cc @rust-lang/lang

@eddyb

Aber Leben _sind_ wichtig mit impl Trait - zB

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

Ich habe das mehrmals in den RFC-Threads erwähnt

merkmalsobjektlose Version:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

Dies ist entweder UB oder nicht, abhängig von der Definition von as_debug .

@arielb1 Ah, richtig, ich habe vergessen, dass einer der Gründe für das, was ich getan habe, darin bestand, nur Lebensdauerparameter zu erfassen, nicht anonyme, spät gebundene Parameter, außer dass es nicht wirklich funktioniert.

@arielb1 Haben wir eine strikte Lebensdauer-Beziehung, die wir zwischen den Lebensdauern, die im konkreten Typ vor dem Löschen gefunden wurden, und den spät gebundenen Lebensdauern in der Signatur setzen können? Andernfalls ist es möglicherweise keine schlechte Idee, sich nur lebenslange Beziehungen anzusehen und alle direkten oder _indirekten_ 'a outlives 'b sofort zu scheitern, wobei 'a _alles andere_ als 'static oder ein lebenslanger Parameter ist und 'b erscheint in der konkreten Art von impl Trait .

Entschuldigung, dass es eine Weile gedauert hat, hier zurück zu schreiben. Also habe ich nachgedacht
über dieses Problem. Mein Gefühl ist, dass wir letztendlich (und
möchte) regionck um eine neue Art von Einschränkung erweitern – ich nenne es
eine \in Einschränkung, da Sie so etwas wie '0 \in {'a, 'b, 'c} sagen können, was bedeutet, dass die für '0 verwendete Region
entweder 'a , 'b oder 'c . Ich bin mir nicht sicher, wie ich mich am besten integrieren soll
das löst sich selbst auf – sicherlich, wenn die Menge von \in ein Singleton ist
set, es ist nur eine gleichberechtigte Beziehung (die wir derzeit nicht als a
erstklassige Sache, die sich aber aus zwei Grenzen zusammensetzen lässt), aber
sonst macht es die Sache kompliziert.

Dies alles hängt mit meinem Wunsch zusammen, die Regionsbeschränkungen festzulegen
ausdrucksvoller als das, was wir heute haben. Sicherlich könnte man a . komponieren
\in Einschränkung aus OR und == Einschränkungen. Aber natürlich mehr
Ausdrucksbeschränkungen sind schwieriger zu lösen und \in ist nicht anders.

Wie auch immer, lassen Sie mich hier nur ein wenig meine Gedanken darlegen. Lass uns damit arbeiten
Beispiel:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Ich denke, die genaueste Entzuckerung für impl Trait ist wahrscheinlich a
neuer Typ:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Jetzt sollten sich die impl Iterator<Item=u32> in foo genauso verhalten wie
FooReturn<'a,'b> würde sich benehmen. Es ist jedoch kein perfektes Match. Eins
Unterschied zum Beispiel ist die Varianz, da Eddyb aufgewachsen ist – ich bin
Angenommen, wir machen impl Foo -ähnliche Typen gegenüber dem Typ invariant
Parameter von foo . Das Auto-Trait-Verhalten funktioniert jedoch.
(Ein weiterer Bereich, in dem die Übereinstimmung möglicherweise nicht ideal ist, ist, wenn wir jemals die
Fähigkeit, die impl Iterator Abstraktion zu "durchdringen", so dass Code
"innerhalb" kennt die Abstraktion den genauen Typ – dann würde sie sortiert
eine implizite "Unwrap"-Operation stattfindet.)

In gewisser Weise ist es besser, eine Art synthetisches Merkmal zu berücksichtigen:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Nun könnten wir den Typ impl Iterator als <() as FooReturn<'a,'b>>::Type . Das passt auch nicht perfekt zusammen, denn wir
würde es normalerweise weg normalisieren. Sie könnten sich vorstellen, eine Spezialisierung zu verwenden
um das zu verhindern:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

In diesem Fall würde sich <() as FooReturn<'a,'b>>::Type nicht normalisieren,
und wir haben ein viel engeres Spiel. Die Varianz verhält sich insbesondere
Rechts; wenn wir jemals einen Typ haben wollten, der "drinnen" ist
Abstraktion, sie wären gleich, aber sie dürfen
normalisieren. Es gibt jedoch einen Haken: Das Auto-Trait-Zeug tut es nicht
ziemlich Arbeit. (Wir möchten vielleicht erwägen, die Dinge hier zu harmonisieren,
eigentlich.)

Wie auch immer, mein Punkt bei der Erforschung dieser potenziellen Entzuckerungen ist nicht,
schlagen vor, dass wir "impl Trait" als _tatsächliche_ Entzuckerung implementieren
(obwohl es schön wäre...) sondern um eine Intuition für unseren Job zu geben. ich
denke, dass die zweite Entzuckerung – in Bezug auf die Projektionen – a . ist
ziemlich hilfreich, um uns vorwärts zu führen.

Ein Ort, an dem diese Projektionsentzuckerung eine wirklich nützliche Anleitung ist, ist
die Beziehung "überlebt". Wenn wir überprüfen wollten, ob <() as FooReturn<'a,'b>>::Type: 'x , sagt uns RFC 1214, dass wir dies beweisen können
solange 'a: 'x _and_ 'b: 'x hält. Das ist glaube ich, wie wir wollen
Dinge auch für Impl-Eigenschaften zu handhaben.

Zur Trans-Zeit und für Auto-Eigenschaften müssen wir wissen, was XXX
ist natürlich. Die Grundidee hier, nehme ich an, ist, einen Typ zu erstellen
Variable für XXX und überprüfen Sie, ob die tatsächlichen Werte zurückgegeben werden
können alle mit XXX vereint werden. Diese Typvariable sollte theoretisch
sagen Sie uns unsere Antwort. Aber das Problem ist natürlich, dass dieser Typ
Variable kann sich auf viele Regionen beziehen, die nicht im Geltungsbereich der
fn-Signatur -- zB die Regionen des fn-Hauptteils. (Das gleiche Problem
kommt bei Typen nicht vor; obwohl man technisch gesehen sagen könnte
zB eine struct-Deklaration im Fn-Body und sie wäre nicht benennbar,
das ist eine art künstliche einschränkung -- man könnte sich genauso gut bewegen
die Struktur außerhalb der Fn.)

Wenn Sie sich sowohl die struct desugaring als auch die impl ansehen, gibt es ein
(implizit in der lexikalischen Struktur von Rust) Einschränkung, die XXX kann
Nennen Sie nur entweder 'static oder Lebenszeiten wie 'a und 'b , was
erscheinen in der Funktionssignatur. Das sind wir nicht
hier modeln. Ich bin mir nicht sicher, wie das am besten geht - irgendeine Art
Inferenzschemata haben eine direktere Darstellung des Bereichs und
Das wollte ich Rust schon immer hinzufügen, um uns bei Schließungen zu helfen. Aber
Lassen Sie uns zuerst an kleinere Deltas denken, denke ich.

Hier kommt die Einschränkung \in . Man kann sich vorstellen, hinzuzufügen
eine Typprüfungsregel, die (im Grunde) FR(XXX) \subset {'a, 'b} --
Das bedeutet, dass die "freien Regionen", die in XXX erscheinen, nur 'a und
'b . Dies würde zu \in Anforderungen für die
verschiedene Regionen, die in XXX .

Schauen wir uns ein konkretes Beispiel an:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Hier wäre der Typ, wenn condition wahr ist, ungefähr so ​​​​wie
Cloned<SliceIter<'a, i32>> . Aber wenn condition falsch ist, würden wir
wollen Cloned<SliceIter<'b, i32>> . Natürlich würden wir in beiden Fällen
enden mit etwas wie (mit Zahlen für Typ-/Regionsvariablen):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Wenn wir dann die Variable 0 in Cloned<SliceIter<'2, i32>> instanziieren,
wir haben '0: '2 und '1: '2 oder eine Gesamtmenge von Regionsbeziehungen
mögen:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Welchen Wert sollten wir also für '2 ? Wir haben auch das zusätzliche
Einschränkung, dass '2 in {'a, 'b} . Mit der Fn wie geschrieben denke ich wir
einen Fehler melden müsste, da weder 'a noch 'b a
richtige Wahl. Interessanterweise würde es jedoch einen korrekten Wert geben, wenn wir die Einschränkung 'a: 'b hinzufügen würden ( 'b ).

Beachten Sie, dass wir, wenn wir nur den _normal_-Algorithmus ausführen, am Ende mit
'2 ist 'body . Ich bin mir nicht sicher, wie ich mit den \in Beziehungen umgehen soll
bis auf die erschöpfende Suche (wobei ich mir etwas Besonderes vorstellen kann
Fälle).

Okay, so weit bin ich gekommen. =)

Auf der PR #35091 schrieb @arielb1 :

Ich mag den Ansatz "alle Lebenszeiten im Impl-Merkmal erfassen" nicht und würde eher etwas wie Lebenszeiteliminierung bevorzugen.

Ich dachte, es wäre sinnvoller, hier zu diskutieren. @arielb1 , können Sie näher darauf vorstellen ? In Bezug auf die Analogien, die ich oben gemacht habe, meinen Sie, dass Sie im Wesentlichen davon sprechen, den Satz von Lebensdauern zu "beschneiden", der entweder als Parameter auf dem neuen Typ oder in der Projektion erscheinen würde (dh <() as FooReturn<'a>>::Type anstelle von <() as FooReturn<'a,'b>>::Type oder so?

Ich glaube nicht, dass die Lifetime-Elision-Regeln, wie sie existieren, in dieser Hinsicht ein guter Leitfaden wären: Wenn wir nur die Lebensdauer von &self auswählen würden, um nur einzuschließen, dann könnten wir dies nicht unbedingt einschließen Typparameter aus der Self Struktur noch Typparameter aus der Methode, da sie möglicherweise WF-Bedingungen haben, die es erfordern, einige der anderen Lebensdauern zu benennen.

Wie auch immer, es wäre toll, einige Beispiele zu sehen, die die Regeln veranschaulichen, die Sie im Sinn haben, und vielleicht deren Vorteile. :) (Außerdem würden wir wohl eine Syntax brauchen, um die Auswahl zu überschreiben.) Wenn alle anderen Dinge gleich sind, würde ich dies vorziehen, wenn wir vermeiden können, aus N Lebensdauern auswählen zu müssen.

Ich habe nirgendwo gesehen, wie Interaktionen von impl Trait mit dem Datenschutz diskutiert wurden.
Jetzt kann fn f() -> impl Trait einen privaten Typ S: Trait ähnlich wie Trait-Objekte fn f() -> Box<Trait> . Dh Objekte privater Typen können in anonymisierter Form frei aus ihrem Modul herausgehen.
Dies scheint vernünftig und wünschenswert - der Typ selbst ist ein Implementierungsdetail, nur seine Schnittstelle, die über ein öffentliches Merkmal Trait verfügbar ist, ist öffentlich.
Es gibt jedoch einen Unterschied zwischen Merkmalsobjekten und impl Trait . Mit Trait-Objekten allein können alle Trait-Methoden privater Typen intern verknüpft werden, sie sind jedoch weiterhin über Funktionszeiger aufrufbar. Mit impl Trait s-Trait-Methoden sind private Typen direkt von anderen Übersetzungseinheiten aus aufrufbar. Der Algorithmus, der die "Internalisierung" von Symbolen durchführt, muss sich stärker bemühen, Methoden nur für Typen zu internalisieren, die nicht mit impl Trait anonymisiert sind, oder sehr pessimistisch sein.

@nikomatsakis

Die "explizite" Art, foo zu schreiben, wäre

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Hier steht die Lebenszeitbindung außer Frage. Offensichtlich wäre es ziemlich repetitiv, jedes Mal die Lebenszeitgrenze schreiben zu müssen. Die Art und Weise, wie wir mit dieser Art von Wiederholung umgehen, geschieht jedoch im Allgemeinen durch lebenslange Elisierung. Im Fall von foo würde Elision fehlschlagen und den Programmierer zwingen, die Lebensdauer explizit anzugeben.

Ich bin dagegen, eine explizitkeitssensitive lebenslange Elision hinzuzufügen, wie es impl Trait getan hat und nicht anders.

@arielb1 hmm, ich bin mir nicht 100% sicher, wie ich über diese vorgeschlagene Syntax in Bezug auf die von mir besprochenen "Entzuckerungen" denken soll. Es ermöglicht Ihnen, anzugeben, was an eine Lebenszeit gebunden zu sein scheint, aber wir versuchen, daraus abzuleiten, welche Lebenszeiten im versteckten Typ erscheinen. Bedeutet dies, dass höchstens ein Leben "versteckt" werden könnte (und genau angegeben werden müsste?)

Es scheint, als ob ein "einzelner Lebensdauerparameter" nicht immer ausreicht:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

In diesem Fall bezieht sich der versteckte Iteratortyp sowohl auf 'a als auch auf 'b (obwohl er in beiden _variant ist; aber ich denke, wir könnten ein Beispiel finden, das invariant ist).

Also haben @aturon und ich dieses Thema etwas diskutiert und ich wollte es teilen. Es gibt hier wirklich ein paar orthogonale Fragen, und ich möchte sie trennen. Die erste Frage lautet: "Welche Typ-/Lebensdauerparameter können potenziell im versteckten Typ verwendet werden?" In Bezug auf die (Quasi-)Entzuckerung in ein default type kommt es darauf an, "welche Typparameter auf dem von uns eingeführten Merkmal erscheinen". Also zum Beispiel, wenn diese Funktion:

fn foo<'a, 'b, T>() -> impl Trait { ... }

würde zu etwas entzuckert werden:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

dann läuft diese Frage auf "Welche Typparameter erscheinen auf dem Merkmal Foo und seinen Impl"? Im Grunde die ... hier. Dies schließt eindeutig die Menge der angezeigten Typparameter ein, die von Trait selbst verwendet werden, aber welche zusätzlichen Typparameter? (Wie ich bereits angemerkt habe, ist diese Entzuckerung zu 100 % treu, mit Ausnahme des Durchsickerns von Auto-Eigenschaften, und ich würde argumentieren, dass wir auch für spezialisierte Impls Auto-Eigenschaften durchsickern sollten.)

Die Standardantwort, die wir verwendet haben, ist "alle", also wäre hier ... 'a, 'b, T (zusammen mit allen anonymen Parametern, die möglicherweise erscheinen). Dies _kann_ eine vernünftige Standardeinstellung sein, aber es ist nicht _notwendigerweise_ die beste Standardeinstellung. (Wie @arielb1 darauf hingewiesen hat.)

Dies hat Auswirkungen auf die Beziehung überlebt, denn um zu bestimmen, dass <() as Foo<...>>::Type (bezieht sich auf eine bestimmte, undurchsichtige Instanzierung von impl Trait ) 'x überlebt, müssen wir effektiv zeigen dass ...: 'x (d. h. alle Lebensdauer- und Typparameter).

Deshalb sage ich, dass es nicht ausreicht, Lifetime-Parameter zu berücksichtigen: Stellen Sie sich vor, wir haben einen Aufruf von foo wie foo::<'a0, 'b0, &'c0 i32> . Dies impliziert, dass alle drei Lebensdauern, '[abc]0 , 'x überleben müssen -- mit anderen Worten, solange der Rückgabewert verwendet wird, wird dies die Ausleihen aller in die Funktion gegebenen Daten prologieren . Aber wie @arielb1 betonte, deutet elision darauf hin, dass dies normalerweise länger als nötig sein wird.

Ich stelle mir also vor, dass wir Folgendes brauchen:

  • sich auf einen angemessenen Standard zu einigen, möglicherweise mithilfe der Intuition von Elision;
  • um eine explizite Syntax zu haben, wenn die Vorgabe nicht geeignet ist.

@aturon hat etwas wie impl<...> Trait als explizite Syntax ausgespuckt , was vernünftig erscheint. Daher könnte man schreiben:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

um anzuzeigen, dass sich der versteckte Typ nicht auf 'a oder 'b bezieht, sondern nur auf T . Oder man könnte impl<'a> Trait schreiben, um anzuzeigen, dass weder 'b noch T erfasst werden.

Was die Standardeinstellungen angeht, so scheint es, als wären mehr Daten ziemlich nützlich - aber die allgemeine Logik der Elision legt nahe, dass wir alle Parameter, die gegebenenfalls im Typ self , erfassen würden. Wenn Sie beispielsweise fn foo<'a,'b>(&'a self, v: &'b [u8]) und der Typ Bar<'c, X> , dann wäre der Typ von self &'a Bar<'c, X> und wir würden daher 'a erfassen. 'c und X standardmäßig, aber nicht 'b .


Eine andere verwandte Anmerkung ist, was die Bedeutung einer Lebenszeitbindung ist. Ich denke, dass die Lebensdauergrenzen von Sounds eine existierende Bedeutung haben, die nicht geändert werden sollte: Wenn wir impl (Trait+'a) schreiben, bedeutet dies, dass der versteckte Typ T 'a überlebt. In ähnlicher Weise kann man impl (Trait+'static) schreiben, um anzuzeigen, dass keine geliehenen Zeiger vorhanden sind (auch wenn einige Lebensdauern erfasst werden). Beim Ableiten des versteckten Typs T würde dies eine Lebenszeitgrenze wie $T: 'static implizieren, wobei $T die Rückschlussvariable ist, die wir für den versteckten Typ erstellen. Dies würde auf die übliche Weise gehandhabt. Aus der Sicht eines Aufrufers, wo der versteckte Typ versteckt ist, würde uns die 'static Grenze den Schluss erlauben, dass impl (Trait+'static) 'static überlebt, selbst wenn Lebensdauerparameter erfasst werden.

Hier verhält es sich genau so, wie sich die Entzuckerung verhalten würde:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

All dies ist orthogonal aus der Inferenz. Wir möchten (glaube ich) immer noch den Begriff einer "Auswahl"-Einschränkung hinzufügen und die Inferenz mit einigen Heuristiken und möglicherweise einer erschöpfenden Suche modifizieren (die Erfahrung aus RFC 1214 legt nahe, dass Heuristiken mit einem konservativen Fallback uns tatsächlich sehr weit bringen können; Mir sind keine Leute bekannt, die in dieser Hinsicht auf Einschränkungen stoßen, obwohl es wahrscheinlich irgendwo ein Problem gibt). Sicherlich kann das Hinzufügen von Lebensdauergrenzen wie 'static oder 'a' die Inferenz beeinflussen und somit hilfreich sein, aber das ist keine perfekte Lösung: Zum einen sind sie für den Aufrufer sichtbar und werden Teil der API, was vielleicht nicht erwünscht ist.

Möglichkeiten:

Explizite Lebensdauer gebunden mit Ausgabeparameter elision

Wie heute Trait-Objekte haben impl Trait Objekte einen einzigen Parameter für die Lebensdauer, der mithilfe der Elisionsregeln abgeleitet wird.

Nachteil: unergonomisch
Vorteil: klar

Explizite Lebensdauergrenzen mit "alles generische" Elision

Wie heute Trait-Objekte haben impl Trait Objekte einen einzigen Parameter, der an die Lebensdauer gebunden ist.

Elision erstellt jedoch neue Parameter mit früher Bindung, die alle expliziten Parameter überdauern:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Nachteil: Fügt einen Parameter mit früher Bindung hinzu

mehr.

Ich bin auf dieses Problem mit impl Trait +'a und Ausleihen gestoßen: https://github.com/rust-lang/rust/issues/37790

Wenn ich diese Änderung richtig verstehe (und die Wahrscheinlichkeit dafür ist wahrscheinlich gering!), dann sollte dieser Spielplatz-Code meiner Meinung nach funktionieren:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Sowohl ThingOne als auch ThingTwo implementieren die Eigenschaft Thing . build sagt, dass es etwas zurückgibt, das Thing implementiert, was es tut. Es wird jedoch nicht kompiliert. Ich verstehe also eindeutig etwas falsch.

Dieses "Etwas" muss einen Typ haben, aber in Ihrem Fall haben Sie zwei widersprüchliche Typen. @nikomatsakis hat zuvor vorgeschlagen, dass dies im Allgemeinen funktioniert, indem beispielsweise ThingOne | ThingTwo wenn Typkonflikte auftreten.

@eddyb könnten Sie ThingOne | ThingTwo näher erläutern? Müssen Sie nicht Box wenn wir den Typ nur zur Laufzeit kennen? Oder ist es eine Art enum ?

Ja, es könnte ein Ad-hoc-Typ sein, der enum ähnlich ist, der Aufrufe von Trait-Methoden, wo möglich, an seine Varianten delegiert.

So etwas habe ich mir auch schon gewünscht. Die anonymen Enumerationen RFC: https://github.com/rust-lang/rfcs/pull/1154

Es ist ein seltener Fall, dass etwas besser funktioniert, wenn es inferenzgesteuert ist, denn wenn Sie diese Typen nur bei einer Nichtübereinstimmung erstellen, sind die Varianten unterschiedlich (was ein Problem mit der verallgemeinerten Form ist).
Sie können auch etwas daraus ziehen, keinen Mustervergleich zu haben (außer in offensichtlich unzusammenhängenden Fällen?).
Aber IMO-Delegationszucker würde in allen relevanten Fällen "einfach funktionieren", selbst wenn Sie es schaffen, ein T | T .

Könnten Sie die anderen, impliziten Hälften dieser Sätze buchstabieren? Ich verstehe das meiste nicht und vermute, dass mir etwas Kontext fehlt. Haben Sie implizit auf die Probleme mit Gewerkschaftstypen reagiert? Dass RFC einfach anonyme Aufzählungen sind, keine Unionstypen - (T|T) wäre genauso problematisch wie Result<T, T> .

Oh, egal, ich habe die Vorschläge verwirrt (auch auf dem Handy stecken geblieben, bis ich meine fehlerhafte Festplatte aussortiert habe, also entschuldigt bitte, dass es sich wie auf Twitter anhört).

Ich finde (positionsbezogene, dh T|U != U|T ) anonyme Aufzählungen faszinierend, und ich glaube, mit ihnen könnte in einer Bibliothek experimentiert werden, wenn wir variadische Generika hätten (Sie können dies umgehen, indem Sie hlist ) und const Generika (dito, mit Peano-Nummern).

Aber wenn wir Sprachunterstützung für etwas hätten, wären es Unionstypen und keine anonymen Aufzählungen. ZB nicht Result sondern Fehlertypen (um die Langeweile der benannten Wrapper für sie zu umgehen).

Ich bin mir nicht sicher, ob dies der richtige Ort ist, um zu fragen, aber warum wird ein Schlüsselwort wie impl benötigt? Ich konnte keine Diskussion finden (könnte meine Schuld sein).

Wenn eine Funktion impl Trait zurückgibt, kann ihr Rumpf Werte jedes Typs zurückgeben, der Trait implementiert

Seit

fn bar(a: &Foo) {
  ...
}

bedeutet "akzeptiere einen Verweis auf einen Typ, der das Merkmal Foo implementiert" würde ich erwarten

fn bar() -> Foo {
  ...
}

bedeutet "einen Typ zurückgeben, der das Merkmal Foo implementiert". Ist das unmöglich?

@kud1ing der Grund ist, die Möglichkeit einer Funktion, die den Typ Trait dynamischer Größe zurückgibt, nicht zu beseitigen, wenn in Zukunft die Unterstützung für Rückgabewerte mit dynamischer Größe hinzugefügt wird. Derzeit ist Trait bereits eine gültige DST, es ist einfach nicht möglich, eine DST zurückzugeben, also müssen Sie sie einpacken, um einen Größentyp zu erstellen.

EDIT: Im verlinkten RFC-Thread wird darüber diskutiert.

Nun, zum einen bevorzuge ich die aktuelle Syntax, unabhängig davon, ob Rückgabewerte mit dynamischer Größe hinzugefügt werden. Anders als bei Merkmalsobjekten ist dies keine Typlöschung, und alle Zufälle wie "Parameter f: &Foo nimmt etwas an, das Foo impliziert, während dies etwas zurückgibt, das Foo impliziert " könnte irreführend sein.

Ich habe der RFC-Diskussion entnommen, dass impl derzeit eine Platzhalterimplementierung ist und kein impl sehr erwünscht ist. Gibt es einen Grund dafür, _nicht_ ein impl Merkmal zu wollen, wenn der Rückgabewert nicht DST ist?

Ich denke, die aktuelle Impl-Technik zum Umgang mit "Auto Trait Leakage" ist problematisch. Wir sollten stattdessen eine DAG-Reihenfolge erzwingen, so dass, wenn Sie ein fn fn foo() -> impl Iterator und Sie einen Anrufer haben, fn bar() { ... foo() ... } , wir foo() vor bar() überprüfen müssen

(Eine andere Möglichkeit, die freizügiger sein könnte, als eine strikte DAG zu verlangen, besteht darin, beide FNS bis zu einem gewissen Grad "zusammen" zu überprüfen. Ich denke, dies sollte erst in Betracht gezogen werden, nachdem wir das Merkmalsystem impl ein wenig umgestaltet haben.)

@Nercury Ich verstehe nicht. Sind Sie fragen , ob es Gründe gibt, nicht wollen fn foo() -> Trait bedeuten -> impl Trait ?

@nikomatsakis Ja, genau das habe ich gefragt, sorry für die Sprachverzerrungen :). Ich dachte, dass dies ohne das Schlüsselwort impl einfacher wäre, da dieses Verhalten genau das ist, was man erwarten würde (wenn ein konkreter Typ anstelle des Merkmalsrückgabetyps zurückgegeben wird). Allerdings könnte mir etwas fehlen, deshalb habe ich nachgefragt.

Der Unterschied besteht darin, dass Funktionen, die impl Trait immer denselben Typ zurückgeben. IIUC, Funktionen, die nur Trait , könnten jede Implementierung dieses Merkmals dynamisch zurückgeben, aber der Aufrufer müsste darauf vorbereitet sein, Platz für den Rückgabewert über etwas wie box foo() zuzuweisen.

@Nercury Der einfache Grund ist, dass die Syntax -> Trait bereits eine Bedeutung hat, also müssen wir für diese Funktion etwas anderes verwenden.

Ich habe tatsächlich Menschen , die beiden Arten von Verhalten von Standard erwarten gesehen, und diese Art von Verwirrung kommt oft genug ginge , würde ich ehrlich gesagt lieber , dass fn foo() -> Trait nicht gemein alles (oder eine Warnung von Standard sein) und es gab explizites Schlüsselwörter sowohl für den Fall "ein Typ, der zur Kompilierzeit bekannt ist, den ich auswählen kann, aber der Aufrufer nicht sieht" als auch den Fall "Eigenschaftsobjekt, das dynamisch an jeden Typ implementiert werden könnte, der eine Eigenschaft implementiert", z. B. fn foo() -> impl Trait gegen fn foo() -> dyn Trait . Aber offensichtlich sind diese Schiffe gesegelt.

Warum generiert der Compiler keine Aufzählung, die alle verschiedenen Rückgabetypen der Funktion enthält, implementiert die Eigenschaft, die durch die Argumente an jede Variante geht, und gibt diese stattdessen zurück?

Das würde die einzige erlaubte Rückgabeart umgehen.

@NeoLegends Dies manuell zu tun ist ziemlich üblich, und etwas Zucker dafür könnte nett sein und wurde in der Vergangenheit vorgeschlagen, aber es ist eine dritte völlig andere Semantik als die Rückgabe von impl Trait oder einem Merkmalsobjekt, also ist es nicht wirklich relevant für diese Diskussion.

@Ixrec Ja, ich weiß, dass dies manuell erfolgt, aber der wahre Anwendungsfall der anonymen Enumerationen als vom Compiler generierte Rückgabetypen sind Typen, die Sie nicht buchstabieren können, wie lange Iteratorketten oder zukünftige Adapter.

Wie ist diese unterschiedliche Semantik? Anonyme Enums (soweit der Compiler sie generiert, nicht nach dem anonymen Enums RFC) als Rückgabewerte machen nur dann wirklich Sinn, wenn es eine gemeinsame API wie ein Trait gibt, das die verschiedenen Varianten abstrahiert. Ich schlage eine Funktion vor, die immer noch wie das reguläre impl-Trait aussieht und sich verhält, nur wenn das Ein-Typ-Limit durch eine vom Compiler generierte Enumeration entfernt wird, wird der Verbraucher der API nie direkt sehen. Der Verbraucher sollte immer nur 'impl Trait' sehen.

Anonyme, automatisch generierte Aufzählungen verursachen impl Trait versteckte Kosten, die leicht zu übersehen sind. Das sollten Sie also in Betracht ziehen.

Ich vermute, dass die Sache "Auto Enum Pass-Through" nur für objektsichere Eigenschaften sinnvoll ist. Gilt dasselbe für impl Trait selbst?

@rpjohnst Sofern dies nicht der Crate- Metadaten und wird an der Aufrufstelle monomorphisiert. Dies setzt natürlich voraus, dass der Wechsel von einer Variante zur anderen den Anrufer nicht stört. Und das könnte zu magisch sein.

@glaebhörl

Ich vermute, dass die Sache "Auto Enum Pass-Through" nur für objektsichere Eigenschaften sinnvoll ist. Gilt das Gleiche auch für Impl Trait selbst?

das ist ein interessanter punkt! Ich habe darüber diskutiert, was der richtige Weg ist, um Impl-Eigenschaften zu "desucar" " Deutung. Dies scheint jedoch so etwas wie eine generalisierte Newtype-Ableitung zu implizieren, die sich in Haskell natürlich F<T> aus einem Impl . generieren wollen für T .

@nikomatsakis

Das Problem ist, in Rust ausgedrückt

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

Das tl;dr ist, dass die generalisierte Newtype-Ableitung implementiert wurde (und wird), indem einfach transmute die vtable geschrieben wird – schließlich besteht eine vtable aus Funktionen auf dem Typ, und ein Typ und sein newtype haben die gleiche Repräsentation , sollte also in Ordnung sein, oder? Aber es bricht, wenn diese Funktionen auch Typen verwenden, die durch Verzweigung auf Typebene auf der Identität (statt Repräsentation) des gegebenen Typs bestimmt werden - zB durch Verwendung von Typfunktionen oder assoziierten Typen (oder in Haskell, GADTs). Denn es gibt keine Garantie dafür, dass die Darstellungen dieser Typen auch kompatibel sind.

Beachten Sie, dass dieses Problem nur aufgrund der Verwendung von unsicherer Transmutierung möglich ist. Wenn es stattdessen nur den langweiligen Boilerplate-Code generiert, um den neuen Typ überall zu verpacken/zu entpacken und jede Methode vom Basistyp an seine Implementierung weiterzuleiten (wie einige der automatischen Delegierungsvorschläge für Rust IIRC?), dann wäre das schlechteste Ergebnis ein Typ Fehler oder vielleicht ein ICE. Wenn Sie keinen unsicheren Code verwenden, können Sie konstruktionsbedingt kein unsicheres Ergebnis haben. Ebenso besteht keine Gefahr, wenn wir Code für eine Art "automatischen Enum-Passthrough" generieren würden, dafür aber keine unsafe Primitive verwenden.

(Ich bin mir nicht sicher, ob oder inwiefern dies mit meiner ursprünglichen Frage zusammenhängt, ob die mit impl Trait verwendeten Merkmale und/oder das automatische Aufzählungs-Passthrough zwangsläufig objektsicher sein müssten?)

@rpjohnst Man könnte den Enum-Fall

fn foo() -> enum impl Trait { ... }

Das ist jedoch mit ziemlicher Sicherheit Nahrung für einen anderen RFC.

@glaebhoerl ja, ich habe einige Zeit damit verbracht, mich mit dem Thema zu beschäftigen und war ziemlich überzeugt, dass es zumindest hier kein Problem sein würde.

Entschuldigung, wenn es etwas Offensichtliches ist, aber ich versuche, die Gründe zu verstehen, warum impl Trait nicht in Rückgabetypen von Merkmalsmethoden erscheinen kann, oder ob es überhaupt Sinn macht? Z.B:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Es macht absolut Sinn, und AFAIK beabsichtigt, dass dies funktioniert, aber es wurde noch nicht implementiert.

Es macht irgendwie Sinn, aber es ist nicht dasselbe zugrunde liegende Merkmal (dies wurde übrigens viel diskutiert):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Insbesondere impl Trait in den mit impl Trait for Type verknüpften RHS-Typen würde der heute implementierten Funktion ähneln, da sie nicht für stabiles Rust entzuckert werden kann, während in den trait es kann sein.

Ich weiß, dass dies wahrscheinlich sowohl zu spät ist als auch hauptsächlich Fahrradabwurf, aber wurde irgendwo dokumentiert, warum das Schlüsselwort impl eingeführt wurde? Es scheint mir, als hätten wir im aktuellen Rust-Code bereits eine Möglichkeit zu sagen, "der Compiler findet heraus, welcher Typ hier hingehört", nämlich _ . Könnten wir dies hier nicht wiederverwenden, um die Syntax anzugeben:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Das ist nicht das, was die Funktion tut, der Typ ist nicht der, der von der Funktion zurückgegeben wird, sondern ein "semantischer Wrapper", der alles außer den ausgewählten APIs (und OIBITs, weil diese lästig sind) verbirgt.

Wir könnten einigen Funktionen erlauben, Typen in ihren Signaturen abzuleiten, indem wir einen DAG erzwingen, aber ein solches Feature wurde nie genehmigt und es ist unwahrscheinlich, dass es jemals zu Rust hinzugefügt wird, da es "globale Inferenz" berühren würde.

Schlagen Sie die Verwendung der Syntax @Trait , um impl Trait zu ersetzen, wie hier erwähnt.

Es ist einfacher, auf andere Typpositionen und Kompositionen wie Box<@MyTrait> oder &@MyTrait .

@Trait für any T where T: Trait und ~Trait für some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

In fn func(t: T) -> V muss kein t oder ein v unterschieden werden, also als Merkmal.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

funktioniert noch.

@JF-Liu Ich persönlich bin dagegen, dass any und some zu einem Schlüsselwort/einem Siegel zusammengefasst werden, aber Sie haben technisch Recht, dass wir ein einzelnes Siegel haben und es wie das ursprüngliche impl Trait könnten

@JF-Liu @eddyb Es gab einen Grund, warum

@ wird auch beim Mustervergleich verwendet und nicht aus der Sprache entfernt.

Ich dachte daran, dass AFAIK-Sigillen überstrapaziert wurden.

Syntax bikesheding: Ich bin zutiefst unglücklich über die impl Trait Notation, weil es viel zu laut ist, ein Schlüsselwort (fette Schrift in einem Editor) zu verwenden, um eine Schrift zu benennen. Erinnern Sie sich an Cs struct und Stroustroup laute Syntaxbeobachtung (Folie 14)?

In https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 schlug @konstin die <Trait> Syntax vor. Es sieht wirklich schön aus, besonders in den Eingabepositionen:

fn take_iterator(iterator: <Iterator<Item=i32>>)

Ich sehe, dass es etwas mit UFCS in Konflikt geraten wird, aber vielleicht kann dies gelöst werden?

Auch ich halte die Verwendung von spitzen Klammern anstelle von impliziten Merkmalen für die bessere Wahl, zumindest in der Position des Rückgabetyps, z.

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait> Syntaxkonflikte mit Generika, bedenken Sie:

Vec<<FnOnce() -> bool>> vs Vec<@FnOnce() -> bool>

Wenn Vec<FnOnce() -> bool> erlaubt ist, dann ist <Trait> eine gute Idee, es bedeutet die Äquivalenz zum generischen Typparameter. Da sich Box<Trait> jedoch von Box<@Trait> , müssen Sie die Syntax von <Trait> aufgeben.

Ich bevorzuge die impl Schlüsselwortsyntax, da dies beim schnellen Lesen der Dokumentation weniger Möglichkeiten bietet, Prototypen falsch zu lesen.
Was denkst du ?

Ich stelle gerade fest, dass ich diesem RFC im Internals-Thread eine Supermenge vorgeschlagen habe (Danke für @matklad, dass du mich hierher hinweist):

Erlauben Sie die Verwendung von Merkmalen in Funktionsparametern und Rückgabetypen, indem Sie sie wie im folgenden Beispiel mit spitzen Klammern umgeben:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

Der Compiler würde dann den Parameter monomorphisieren, indem er die gleichen Regeln verwendet, die derzeit für Generics gelten. Der Rückgabetyp könnte zB aus der Funktionsimplementierung abgeleitet werden. Dies bedeutet, dass Sie diese Methode nicht einfach für Box<Trait_with_transform> aufrufen oder sie allgemein für dynamisch verteilte Objekte verwenden können, aber die Regeln würden dadurch immer freizügiger. Ich habe nicht alle RFC-Diskussionen gelesen, also gibt es vielleicht schon eine bessere Lösung, die ich übersehen habe.

Ich bevorzuge die Impl-Schlüsselwortsyntax, da dies beim schnellen Lesen der Dokumentation weniger Möglichkeiten bietet, Prototypen falsch zu lesen.

Eine andere Farbe in der Syntaxhervorhebung sollte den Zweck erfüllen.

Dieses Papier von Stroustrup diskutiert ähnliche syntaktische Optionen für C++-Konzepte in Abschnitt 7: http://www.stroustrup.com/good_concepts.pdf

Verwenden Sie nicht dieselbe Syntax für Generics und Existentials. Sie sind nicht dasselbe. Generics erlauben dem Aufrufer zu entscheiden, was der konkrete Typ ist, während (diese eingeschränkte Teilmenge von) Existenzialen es der aufgerufenen Funktion ermöglicht, zu entscheiden, was der konkrete Typ ist. Dieses Beispiel:

fn transform(iter: <Iterator>) -> <Iterator>

sollte entweder äquivalent dazu sein

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

oder es sollte äquivalent dazu sein

fn transform(iter: impl Iterator) -> impl Iterator

Das letzte Beispiel wird auch nachts nicht richtig kompiliert und ist mit dem Iterator-Merkmal nicht wirklich aufrufbar, aber ein Merkmal wie FromIter würde es dem Aufrufer ermöglichen, eine Instanz zu konstruieren und sie an die Funktion zu übergeben, ohne in der Lage zu sein um die konkrete Art dessen zu bestimmen, was sie passieren.

Vielleicht sollte die Syntax ähnlich sein, aber sie sollte nicht gleich sein.

Es ist nicht erforderlich, im Typnamen zwischen (Generika) oder einigen (Existenziellen) zu unterscheiden, es hängt davon ab, wo der Typ verwendet wird. Wenn sie in Variablen verwendet werden, akzeptieren Argumente und Strukturfelder immer T, wenn sie im Rückgabetyp fn verwendet werden, erhalten Sie immer einen Teil von T.

  • Verwenden Sie Type , &Type , Box<Type> für konkrete Datentypen, statischer Versand
  • Verwenden Sie @Trait , &@Trait , Box<@Trait> und den generischen Typparameter für abstrakten Datentyp, statischer Versand
  • Verwenden Sie &Trait , Box<Trait> für abstrakten Datentyp, dynamischer Versand

fn func(x: @Trait) entspricht fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) kann einfach als fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T Parameter wird noch in fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } entspricht struct Foo<T: Trait> { field: T } .

Wenn sie in Variablen verwendet werden, akzeptieren Argumente und Strukturfelder immer T, wenn sie im Rückgabetyp fn verwendet werden, erhalten Sie immer einen Teil von T.

Sie können Any-of-Trait sofort in stabilem Rust zurückgeben, indem Sie die vorhandene generische Syntax verwenden. Es ist eine sehr stark genutzte Funktion. serde_json::de::from_slice nimmt &[u8] als Parameter und gibt T where T: Deserialize .

Sie können auch einige der Eigenschaften sinnvoll zurückgeben, und das ist die Funktion, über die wir sprechen. Sie können keine Existentials für die Deserialize-Funktion verwenden, genauso wie Sie keine Generics verwenden können, um unverpackte Closures zurückzugeben. Es sind unterschiedliche Funktionen.

Für ein bekannteres Beispiel kann Iterator::collect jedes T where T: FromIterator<Self::Item> , was meine bevorzugte Schreibweise impliziert: fn collect(self) -> any FromIterator<Self::Item> .

Wie wäre es mit der Syntax
fn foo () -> _ : Trait { ... }
für Rückgabewerte und
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
für Parameter?

Für mich kommt wirklich keiner der neuen Vorschläge in seiner Eleganz an impl Trait heran. impl ist ein Schlüsselwort, das bereits jedem Rust-Programmierer bekannt ist, und da es zum Implementieren von Eigenschaften verwendet wird, schlägt es tatsächlich vor, was das Feature allein macht.

Ja, es scheint mir ideal, bei bestehenden Keywords zu bleiben; Ich würde gerne impl für Existenzielles und for für Universalien sehen.

Ich persönlich bin dagegen, dass any und some in einem Schlüsselwort/einem Siegel zusammengefasst werden

@eddyb Ich würde es nicht als Verschmelzung betrachten. Aus der Regel folgt natürlich:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Bearbeiten: Es ist eine Einbahnstraße, kein Isomorphismus.


Nicht verwandt: Gibt es einen entsprechenden Vorschlag, impl Trait in anderen kovarianten Positionen zuzulassen, wie z

~rostfn foo(Rückruf: F) -> Rwobei F: FnOnce(impl SomeTrait) -> R {Rückruf(create_something())}~

Im Moment ist dies kein notwendiges Feature, da Sie immer eine konkrete Zeit für impl SomeTrait angeben können, was die Lesbarkeit beeinträchtigt, aber ansonsten keine große Sache ist.

Aber wenn sich die RFC 1522-Funktion stabilisiert, dann wäre es unmöglich, Programmen wie den oben genannten eine create_something zu impl SomeTrait (zumindest ohne es zu boxen). Das halte ich für problematisch.

@Rufflewind In der realen Welt sind die Dinge nicht so eindeutig, und diese Funktion ist eine sehr spezifische Marke von Existenzialien (Rust hat inzwischen mehrere).

Aber selbst dann haben Sie nur die Verwendung von Kovarianz, um zu bestimmen, was impl Trait innerhalb und außerhalb von Funktionsargumenten bedeutet.

Das reicht nicht für:

  • das Gegenteil der Vorgabe verwenden
  • Begriffsklärung innerhalb eines Feldtyps (wobei sowohl any als auch some gleichermaßen wünschenswert sind)

@Rufflewind Das scheint die falsche Klammer für das zu sein, was impl Trait ist. Ich weiß, dass Haskell diese Beziehung ausnutzt, um nur das Schlüsselwort forall zu verwenden, um sowohl Universalien als auch Existenzielles darzustellen, aber es funktioniert nicht in dem Kontext, den wir diskutieren.

Nehmen Sie zum Beispiel diese Definition:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Wenn wir die Regel verwenden, dass " impl in Argumenten universell ist, impl in Rückgabetypen existentiell ist", dann ist der Typ des Funktionselementtyps foo logischerweise dieser (in erfundene Typnotation):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

impl naiv so zu behandeln, dass es technisch nur universell oder nur existenziell bedeutet und die Logik sich selbst ausarbeiten lässt, funktioniert nicht wirklich. Sie würden entweder dies erhalten:

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Oder dieses:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Und keines davon reduziert sich durch logische Regeln auf das, was wir wollen. Letztendlich erfassen any / some also einen wichtigen Unterschied, den Sie nicht mit einem einzigen Schlüsselwort erfassen können. Es gibt sogar vernünftige Beispiele in std wo Sie Universals in der Return-Position haben wollen. Zum Beispiel diese Iterator Methode:

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

Und es gibt keine Möglichkeit, es mit impl und der Argument/Return-Regel zu schreiben.

tl;dr mit impl kontextuell entweder universell oder existentiell bezeichnen, gibt ihm wirklich zwei unterschiedliche Bedeutungen.


Als Referenz sieht in meiner Notation die erwähnte forall/exists-Beziehung @Rufflewind so aus:

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

Was damit zusammenhängt, dass das Konzept von Merkmalsobjekten (Existentials) äquivalent zu Generika (Universalen) ist, aber nicht mit dieser impl Trait Frage.

Das heißt, ich bin nicht mehr stark für any / some . Ich wollte genau sagen, worüber wir sprechen, und any / some haben diese theoretische und visuelle Schönheit, aber ich würde gerne impl mit dem Kontext verwenden Regel. Ich denke, es deckt alle gängigen Fälle ab, vermeidet kontextbezogene Schlüsselwortgrammatikprobleme und wir können für den Rest auf benannte Typparameter übergehen.

Um der vollen Allgemeinheit von Universalien zu entsprechen, werden wir meiner Meinung nach irgendwann eine Syntax für benannte Existenziale brauchen, die beliebige Where-Klauseln und die Möglichkeit ermöglicht, dasselbe Existenzial an mehreren Stellen in der Signatur zu verwenden.

Zusammenfassend würde ich mich freuen über:

  • impl Trait als Abkürzung für Universalien und Existenzialien (kontextuell).
  • Benannte Typparameter als die vollständig allgemeine Langschrift für Universalien und Existenzialien. (Weniger häufig notwendig.)

Impl naiv so zu behandeln, dass es technisch nur universell oder nur existentiell bedeutet und die Logik sich selbst ausarbeiten lässt, funktioniert nicht wirklich. Sie würden entweder dies erhalten:

@solson Für mich würde eine „naive“ Übersetzung dazu führen, dass die existenziellen Quantoren direkt neben dem quantifizierten Typ stehen. Somit

~rost(impl MyTrait)~

ist nur syntaktischer Zucker für

~rost(existiertT)~

das ist eine einfache lokale Transformation. Eine naive Übersetzung, die der Regel „ impl ist immer eine existenzielle“ gehorcht, würde also folgendes ergeben:

~rostfn(existiertT) -> (existiertR)~

Wenn Sie dann den Quantor aus dem Funktionsargument herausziehen, wird er zu

~rostProfn(T) -> (existiertR)~

Obwohl also T relativ zu sich selbst immer existentiell ist , erscheint es relativ zum gesamten Funktionstyp als universell .


IMO, ich denke, impl könnte genauso gut das De-facto-Schlüsselwort für existenzielle Typen werden. In Zukunft könnte man sich vielleicht kompliziertere Existenztypen konstruieren wie:

~~rost(impli(Vec, T))~ ~

analog zu Universaltypen (über HRTB)

~rost(für<'a> FnOnce(&'a T))~

@Rufflewind Diese Ansicht funktioniert nicht, weil fn(T) -> (exists<R: ReturnTrait>(R)) logisch nicht mit exists<R: ReturnTrait>(fn(T) -> R) äquivalent ist, was der Rückgabetyp impl Trait wirklich bedeutet.

(Zumindest nicht in der konstruktiven Logik, die normalerweise auf Typsysteme angewendet wird, wo der spezifische Zeuge, der für ein Existenzial ausgewählt wird, relevant ist. Ersteres impliziert, dass die Funktion verschiedene Typen auswählen könnte, um beispielsweise basierend auf den Argumenten zurückzugeben, während letzteres impliziert, dass es vorhanden ist ein bestimmter Typ für alle Aufrufe der Funktion, wie es in impl Trait der Fall ist.)

Ich habe auch das Gefühl, dass wir etwas weit weg sind. Ich denke, kontextbezogenes impl ist ein guter Kompromiss, und ich denke nicht, dass es notwendig oder besonders hilfreich ist, nach dieser Art von Rechtfertigung zu greifen (wir würden die Regel sicherlich nicht in Bezug auf diese Art von logischen Verbindungen lehren ).

@solson Ja, du hast Recht: Existenzielles können nicht herausgeschwemmt werden. Dieser gilt nicht allgemein:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

in der Erwägung, dass diese allgemein gelten:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

Letzteres ist für die Umdeutung von Existenzialien in Argumenten als Generika verantwortlich.

Edit: Ups, (∀A. g(A) → T) → (∃A. g(A)) → T hält.

Ich habe einen RFC mit einem detaillierten Vorschlag zur Erweiterung und Stabilisierung von impl Trait gepostet. Es stützt sich auf viele Diskussionen in diesem und früheren Threads.

Erwähnenswert ist, dass https://github.com/rust-lang/rfcs/pull/1951 akzeptiert wurde.

Wie ist der aktuelle Stand dazu? Wir haben einen RFC, der gelandet ist, wir haben Leute, die die ursprüngliche Implementierung verwenden, aber mir ist nicht klar, was zu tun ist.

In #43869 wurde festgestellt, dass die Funktion -> impl Trait keinen rein divergierenden Körper unterstützt:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

Ist dies erwartet (da ! Iterator nicht impliziert) oder wird dies als Fehler angesehen?

Wie wäre es mit der Definition von abgeleiteten Typen, die nicht nur als Rückgabewerte verwendet werden könnten, sondern für alles (ich schätze), dass ein Typ derzeit verwendet werden kann?
Etwas wie:
type Foo: FnOnce() -> f32 = #[infer];
Oder mit einem Stichwort:
infer Foo: FnOnce() -> f32;

Der Typ Foo könnte dann als Rückgabetyp, Parametertyp oder irgendetwas anderes verwendet werden, wofür ein Typ verwendet werden kann, aber es wäre illegal, ihn an zwei verschiedenen Stellen zu verwenden, die einen anderen Typ erfordern, selbst wenn das type implementiert in beiden Fällen FnOnce() -> f32 . Folgendes würde beispielsweise nicht kompiliert:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Dies sollte nicht kompiliert werden, da auch die Rückgabetypen von return_closure und return_closure2 beide FnOnce() -> f32 , ihre Typen sind tatsächlich unterschiedlich, da in Rust keine zwei Closures den gleichen Typ haben . Damit das Obige kompiliert werden kann, müssten Sie also zwei verschiedene abgeleitete Typen definieren:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Ich denke, was hier passiert, ist ziemlich offensichtlich, nachdem Sie den Code gesehen haben, auch wenn Sie vorher nicht wussten, was das Schlüsselwort infer macht, und es ist sehr flexibel.

Das Schlüsselwort infer (oder Makro) würde dem Compiler im Wesentlichen sagen, dass er den Typ basierend auf dem Verwendungsort herausfinden soll. Wenn der Compiler den Typ nicht ableiten kann, würde er einen Fehler auslösen. Dies kann passieren, wenn nicht genügend Informationen vorhanden sind, um den Typ einzugrenzen (wenn der abgeleitete Typ beispielsweise nirgendwo verwendet wird, obwohl vielleicht ist es besser, diesen speziellen Fall zu einer Warnung zu machen), oder wenn es unmöglich ist, einen Typ zu finden, der überall passt, wo er verwendet wird (wie im obigen Beispiel).

@cramertj Ahh, deshalb war dieses Thema so verstummt..

Also fragte mich @cramertj , wie es meiner Meinung nach am besten wäre, das Problem der spät gebundenen Regionen zu lösen, auf das sie in ihrer PR gestoßen sind. Meine Meinung ist, dass wir wahrscheinlich unsere Implementierung ein wenig "umrüsten" wollen, um zu versuchen und uns auf das anonymous type Foo Modell zu freuen.

Für den Kontext ist die Idee ungefähr so ​​​​

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

wäre (irgendwie) zu so etwas entzuckert

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Beachten Sie, dass in dieser Form können Sie sehen, welche generische Parameter erfasst , weil sie als Argumente erscheinen Foo - vor allem, 'b wird nicht erfasst, weil es in der Eigenschaft Referenz wird nicht in wie auch immer, aber die Typparameter T und U immer.

Wie auch immer, wenn Sie derzeit im Compiler eine impl Debug Referenz haben, erstellen wir eine def-id, die - effektiv - diesen anonymen Typ repräsentiert. Dann haben wir die Abfrage generics_of , die ihre generischen Parameter berechnet. Im Moment gibt dies dasselbe zurück wie der Kontext "einschließend", dh die Funktion foo . Das wollen wir ändern.

Auf der "anderen Seite", das heißt in der Signatur von foo , stellen wir impl Foo als TyAnon . Dies ist im Grunde richtig – das TyAnon repräsentiert den Verweis auf Foo , den wir in der obigen Entzuckerung sehen. Aber die Art und Weise, wie wir die "Substs" für diesen Typ erhalten, besteht darin, die Funktion "identity" zu verwenden , was eindeutig falsch ist - oder zumindest nicht verallgemeinert.

Hier findet also insbesondere eine Art "Namespace-Verletzung" statt. Wenn wir die "Identitäts"-Substrate für ein Element generieren, erhalten wir normalerweise die Ersetzungen, die wir bei der Typprüfung dieses Elements verwenden würden - d. h. mit all seinen generischen Parametern im Gültigkeitsbereich. Aber in diesem Fall erstellen wir die Referenz auf Foo , die innerhalb der Funktion foo() , und deshalb möchten wir, dass die generischen Parameter von foo() in Substs , nicht die von Foo . Das funktioniert zufällig, weil das im Moment ein und dasselbe ist, aber es ist nicht wirklich richtig .

Ich denke, was wir tun sollten , ist in etwa so:

Wenn wir zunächst die generischen Typparameter von Foo berechnen (d. h. den anonymen Typ selbst), würden wir damit beginnen, einen neuen Satz von Generika zu konstruieren. Natürlich würde es die Typen enthalten. Aber ein Leben lang gingen wir über die Merkmalsgrenzen und identifizierten jede der Regionen, die darin auftauchten. Das ist diesem bestehenden Code, den cramertj geschrieben hat , sehr ähnlich, außer dass wir keine Def-IDs akkumulieren möchten, da nicht alle Regionen im Geltungsbereich Def-IDs haben.

Ich denke, was wir tun möchten, ist, die angezeigten Regionen zu sammeln und sie in eine bestimmte Reihenfolge zu bringen und auch die Werte für diese Regionen aus der Sicht von foo() verfolgen. Dies ist etwas nervig, da wir keine einheitliche Datenstruktur haben, die einen logischen Bereich darstellt. (Früher hatten wir die Vorstellung von FreeRegion , was fast funktioniert hätte, aber wir verwenden FreeRegion für früh gebundene Sachen, sondern nur noch für spät gebundene Sachen.)

Die vielleicht einfachste und beste Option wäre, einfach ein Region<'tcx> , aber Sie müssten die Debruijn-Indextiefen verschieben, wenn Sie alle eingeführten Binder "aufheben" möchten. Dies ist jedoch vielleicht die beste Wahl.

Also im Grunde , wie wir Rückrufe in bekommen visit_lifetime , würden wir diese in eine Transformation Region<'tcx> ausgedrückt in der Anfangstiefe (wir , als wir durch Bindemittel passieren müssen verfolgen). Wir werden diese in einem Vektor akkumulieren und Duplikate eliminieren.

Wenn wir fertig sind, haben wir zwei Dinge:

  • Zuerst müssen wir für jede Region im Vektor einen generischen Regionsparameter erstellen. Sie können alle anonyme Namen haben oder was auch immer, es spielt keine Rolle (obwohl wir sie vielleicht brauchen, um def-ids oder sowas zu haben...? Ich muss mir die RegionParameterDef Datenstrukturen ansehen...) .
  • Zweitens sind die Regionen im Vektor auch die Dinge, die wir für die "Substs" verwenden möchten.

OK, tut mir leid, wenn das kryptisch ist. Ich kann es nicht ganz klar sagen, wie ich es ausdrücken soll. Etwas, bei dem ich mir jedoch nicht sicher bin – im Moment denke ich, dass unser Umgang mit Regionen ziemlich komplex ist, also gibt es vielleicht eine Möglichkeit, die Dinge umzugestalten, um sie einheitlicher zu machen? Ich würde $10 wetten, dass @eddyb hier einige Gedanken hat. ;)

@nikomatsakis Ich glaube, vieles davon ähnelt dem, was ich @cramertj gesagt konkreter !

Ich habe über existenzielle impl Trait nachgedacht und bin auf einen merkwürdigen Fall gestoßen, bei dem wir meiner Meinung nach mit Vorsicht vorgehen sollten. Betrachten Sie diese Funktion:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Wie Sie beim Spielen überprüfen können, wird dieser Code heute kompiliert. Wenn wir uns jedoch genauer damit befassen, was passiert, hebt es etwas hervor, das eine "Zukunftskompatibilität"-Gefahr birgt, die mich beunruhigt.

Insbesondere ist klar, wie wir den Typ herleiten, der hier zurückgegeben wird ( () ). Es ist weniger klar, wie wir den Typ des Parameters impl Debug herleiten. Das heißt, Sie können sich diesen Rückgabewert wie folgt vorstellen: -> ?T wobei ?T: Foo<?U> . Wir müssen die Werte von ?T und ?U nur aus der Tatsache herleiten, dass ?T = () .

Im Moment tun wir dies, indem wir uns die Tatsache zunutze machen, dass es nur ein Impl gibt. Dies ist jedoch eine fragile Eigenschaft. Wenn ein neues Impl hinzugefügt wird, wird der Code nicht mehr kompiliert , da wir jetzt nicht mehr eindeutig bestimmen können, was ?U muss.

Dies kann in vielen Szenarien in Rust passieren – was besorgniserregend, aber orthogonal ist – aber es gibt etwas anderes im Fall impl Trait . Im Fall von impl Trait haben wir keine Möglichkeit für Benutzer, Typanmerkungen hinzuzufügen, um die Schlussfolgerung zu leiten! Einen solchen Plan haben wir auch nicht wirklich. Die einzige Lösung besteht darin, die Fn-Schnittstelle auf impl Foo<()> oder etwas anderes explizites zu ändern.

In Zukunft könnte man sich mit abstract type vorstellen, Benutzern zu erlauben, den versteckten Wert explizit anzugeben (oder vielleicht nur unvollständige Hinweise mit _ ), was dann bei der Schlussfolgerung helfen könnte, während die ungefähren Werte beibehalten werden gleiche öffentliche Schnittstelle

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Dennoch denke ich, dass es ratsam wäre, "verschachtelte" Verwendungen von existenziellen Impl-Eigenschaften nicht zu stabilisieren, außer in zugehörigen Typbindungen (zB impl Iterator<Item = impl Debug> leidet nicht unter diesen Mehrdeutigkeiten).

Im Fall von impl Trait haben wir keine Möglichkeit für Benutzer, Typanmerkungen hinzuzufügen, um die Inferenz zu leiten! Einen solchen Plan haben wir auch nicht wirklich.

Vielleicht könnte es wie UFCS aussehen? zB <() as Foo<()>> -- den Typ nicht wie ein bloßes as ändern, sondern nur eindeutig machen. Dies ist derzeit eine ungültige Syntax, da erwartet wird, dass :: und weitere folgen.

Ich habe gerade einen interessanten Fall bezüglich der Typinferenz mit impl Trait für Fn :
Der folgende Code lässt sich gut kompilieren :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

Wenn wir die Unterzeile auskommentieren, wird ein Kompilierungsfehler ausgegeben :

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Dies scheint nichts mit impl Trait tun zu haben - es gilt für Schlussfolgerungen im Allgemeinen. Versuchen Sie diese geringfügige Modifikation Ihres Beispiels:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

Sieht so aus, als ob das jetzt geschlossen ist:

ICEs bei Interaktion mit Elision

Eine Sache, die ich in dieser Ausgabe oder in der Diskussion nicht aufgeführt sehe, ist die Möglichkeit, Closures und Generatoren – die nicht vom Aufrufer bereitgestellt werden – in struct-Feldern zu speichern. Im Moment ist dies möglich, sieht aber hässlich aus: Sie müssen der Struktur für jedes Closure/Generator-Feld einen Typparameter hinzufügen und dann in der Signatur der Konstruktorfunktion diesen Typparameter durch impl FnMut/impl Generator ersetzen. Hier ist ein Beispiel , und es funktioniert, was ziemlich cool ist! Aber es lässt sehr zu wünschen übrig. Es wäre viel besser, wenn Sie den Typparameter loswerden könnten:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait möglicherweise nicht der richtige Weg, dies zu tun – wahrscheinlich abstrakte Typen, wenn ich RFC 2071 richtig gelesen und verstanden habe. Was wir brauchen, ist etwas, das wir in die Strukturdefinition schreiben können, damit der tatsächliche Typ ( [generator@src/main.rs:15:17: 21:10 _] ) abgeleitet werden kann.

@mikeyhew abstrakte Typen wären in der Tat die Art und Weise, wie wir davon ausgehen, dass dies funktioniert, glaube ich. Die Syntax würde ungefähr so ​​aussehen

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

Gibt es einen Fallback-Pfad, wenn es sich um das impl Generator anderen Person handelt, das ich in meine Struktur einfügen möchte, aber kein abstract type für mich erstellt wurde?

@scottmcm Sie können immer noch Ihre eigenen abstract type deklarieren:

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Warte, abstrakte Typen sind schon in der Nacht?! Wo ist die PR?

@alexreg Nein, sind sie nicht.

Edit: Grüße, Besucher aus der Zukunft! Das folgende Problem wurde behoben.


Ich möchte auf diesen unkonventionellen Anwendungsfall aufmerksam machen, der in #47348 auftaucht

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

Sollte es überhaupt erlaubt sein, eine Projektion auf impl Trait wie diese zurückzugeben? (weil derzeit __es ist.__)

Ich konnte weder eine Diskussion über eine solche Verwendung finden, noch konnte ich Testfälle dafür finden.

@ExpHP Hmm . Es scheint problematisch zu sein, aus dem gleichen Grund wie impl Foo<impl Bar> problematisch ist. Im Grunde haben wir keine wirkliche Beschränkung auf den fraglichen Typ – nur auf die Dinge, die daraus projiziert werden.

Ich denke, wir möchten die Logik um "eingeschränkte Typparameter" von impls wiederverwenden. Kurz gesagt, die Angabe des Rückgabetyps sollte impl Sub "einschränken". Die Funktion, auf die ich mich beziehe, ist diese:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Kleine Triage für Leute, die Checkboxen mögen:

  • #46464 ist fertig -> Checkbox
  • #48072 ist fertig -> Checkbox

@rfcbot fcp zusammenführen

Ich schlage vor, dass wir die Funktionen conservative_impl_trait und universal_impl_trait mit einer ausstehenden Änderung (einem Fix für https://github.com/rust-lang/rust/issues/46541) stabilisieren .

Tests, die aktuelle Semantiken dokumentieren

Die Tests für diese Funktionen finden Sie in den folgenden Verzeichnissen:

Run-Pass/Impl-Eigenschaft
ui/impl-Eigenschaft
Kompilieren-Fehler/Impl-Eigenschaft

Während der Implementierung gelöste Fragen

Die Details zum Parsen von impl Trait wurden in RFC 2250 gelöst und in https://github.com/rust-lang/rust/pull/45294 implementiert

impl Trait wurde von verschachtelten, nicht-assoziierten Positionen und bestimmten qualifizierten Pfadpositionen gesperrt, um Mehrdeutigkeiten zu vermeiden. Dies wurde in https://github.com/rust-lang/rust/pull/48084 implementiert

Verbleibende instabile Funktionen

Nach dieser Stabilisierung wird es möglich sein, impl Trait in der Argumentposition und der Rückgabeposition von Nicht-Eigenschaftsfunktionen zu verwenden. Die Verwendung von impl Trait überall in der Fn Syntax ist jedoch immer noch nicht erlaubt, um zukünftige Design-Iterationen zu ermöglichen. Darüber hinaus ist die manuelle Angabe der Typparameter von Funktionen, die impl Trait in der Argumentposition verwenden, nicht zulässig.

Teammitglied @cramertj hat vorgeschlagen, dies zusammenzuführen. Der nächste Schritt ist die Überprüfung durch den Rest der markierten Teams:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @ohneboote

Derzeit keine Bedenken aufgeführt.

Sobald eine Mehrheit der Gutachter zustimmt (und keine Einwände erheben), tritt die letzte Kommentarfrist 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.

Nach dieser Stabilisierung wird es möglich sein, impl Trait in Argumentposition und Rückgabeposition von Nicht-Trait-Funktionen zu verwenden. Die Verwendung von impl Trait überall in der Fn-Syntax ist jedoch immer noch nicht erlaubt, um zukünftige Design-Iterationen zu ermöglichen. Darüber hinaus ist die manuelle Angabe der Typparameter von Funktionen, die das impl-Trait in der Argumentposition verwenden, nicht zulässig.

Was ist der Status der Verwendung von impl Trait in Argument-/Rückgabepositionen in Merkmalsfunktionen oder in der Fn-Syntax?

@alexreg Return-position impl Trait in Traits ist in einem RFC blockiert, obwohl RFC 2071 nach der Implementierung ähnliche Funktionen zulassen wird. Die Argument-Position impl Trait in Traits ist für keine mir bekannten technischen Funktionen blockiert, aber im RFC war sie nicht ausdrücklich erlaubt und wurde daher vorerst weggelassen.

impl Trait in der Argumentposition von Fn Syntax ist auf Typ-Level-HRTB blockiert, da einige Leute denken, dass T: Fn(impl Trait) zu T: for<X: Trait> Fn(X) entzuckern sollte. impl Trait in der Rückgabeposition von Fn Syntax wird aus technischen Gründen, die mir bekannt sind, nicht blockiert, aber sie wurde im RFC nicht zugelassen, da weitere Designarbeiten ausstehen - ich würde erwarten, dass Sehen Sie sich einen anderen RFC oder zumindest einen separaten FCP an, bevor Sie diesen stabilisieren.

@cramertj Okay, danke für das Update. Hoffentlich können wir sehen, dass diese beiden Funktionen, die für nichts blockiert sind, nach einiger Diskussion bald grünes Licht bekommen. Die Entzuckerung macht in der Argumentposition Sinn, ein Argument foo: T wobei T: Trait äquivalent zu foo: impl Trait , es sei denn, ich irre mich.

Sorge: https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 ist immer noch dieselbe. Ist es möglich, Folgendes zuzulassen?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm Nein, das ist im Moment nicht möglich. Diese Funktion gibt ! , was weder das von Ihnen angegebene Merkmal implementiert, noch haben wir einen Mechanismus, um es in einen geeigneten Typ zu konvertieren. Das ist bedauerlich, aber es gibt derzeit keine einfache Möglichkeit, es zu beheben (abgesehen von der Implementierung weiterer Merkmale für ! ). Es ist auch abwärtskompatibel, um es in Zukunft zu beheben, da es funktionieren würde, um mehr Code zu kompilieren.

Die Turbofisch-Frage ist nur halbwegs gelöst. Wir sollten zumindest vor impl Trait in Argumenten effektiver öffentlicher Funktionen warnen, wobei wir impl Trait in Argumenten als privaten Typ für das neue private in public check betrachten .

Die Motivation besteht darin, libs daran zu hindern, die Turbofish der Benutzer zu zerstören, indem ein Argument von explizit generisch in impl Trait geändert wird. Wir haben noch kein gutes Referenzhandbuch für Libs, um zu wissen, was eine Breaking Change ist und was nicht, und es ist sehr unwahrscheinlich, dass Tests dies erkennen. Dieses Thema wurde nicht ausreichend diskutiert, wenn wir uns stabilisieren wollen, bevor wir uns endgültig entscheiden, sollten wir zumindest die Waffe vom Fuß der lib-Autoren weg richten.

Die Motivation besteht darin, libs daran zu hindern, die Turbofish der Benutzer zu zerstören, indem ein Argument von explizit generisch in impl Trait geändert wird.

Ich hoffe, wenn dies passiert und sich die Leute beschweren, werden die Leute im Lang-Team, die derzeit Zweifel haben, davon überzeugt sein, dass impl Trait die explizite Bereitstellung von Typargumenten mit Turbofish unterstützen sollte.

@leodasvacas

Die Turbofisch-Frage ist nur halbwegs gelöst. Wir sollten zumindest vor impl Trait in Argumenten von effektiv öffentlichen Funktionen warnen, indem wir impl Trait in Argumenten als privaten Typ für das neue Private in der öffentlichen Prüfung betrachten.

Ich bin anderer Meinung - das wurde gelöst. Wir verbieten Turbofish für diese Funktionen vorerst komplett. Die Änderung der Signatur einer öffentlichen Funktion, um impl Trait anstelle von expliziten generischen Parametern zu verwenden, ist eine grundlegende Änderung.

Wenn wir in Zukunft Turbofish für diese Funktionen zulassen, wird es wahrscheinlich nur die Angabe von Nicht- impl Trait Typ-Parametern erlauben.

:bell: Dies geht jetzt in seine letzte Kommentarperiode , wie in der obigen Überprüfung beschrieben . :Glocke:

Ich sollte hinzufügen, dass ich mich nicht stabilisieren möchte, bis https://github.com/rust-lang/rust/pull/49041 landet. (Aber hoffentlich bald.)

#49041 enthält also einen Fix für #46541, aber dieser Fix hat mehr Auswirkungen als ich erwartet hatte – zB bootet der Compiler jetzt nicht – und gibt mir eine gewisse Pause über den richtigen Kurs hier. Das Problem in #49041 ist, dass wir versehentlich Lebenszeiten durchsickern lassen, die wir nicht sollten. So manifestiert sich dies im Compiler. Wir könnten eine Methode wie diese haben:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

Das Entscheidende hier ist, dass TyCtxt invariant ist w/r/t 'tcx und 'gcx , also müssen sie im Rückgabetyp erscheinen. Und doch erscheinen nur 'cx und 'tcx in den Impl-Trait-Grenzen, also sollen nur diese beiden Lebenszeiten "eingefangen" werden. Der alte Compiler hat dies akzeptiert, weil 'gcx: 'cx , aber das ist nicht wirklich richtig, wenn man an die Entzuckerung denkt, die wir im Sinn haben. Diese Entzuckerung würde einen abstrakten Typ wie diesen erzeugen:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

und dennoch wäre der Wert für diesen abstrakten Typ Baz<'cx, 'gcx, 'tcx> -- aber 'gcx ist nicht im Geltungsbereich!

Die Problemumgehung hier ist, dass wir 'gcx in den Grenzen benennen müssen. Das ist irgendwie nervig; wir können 'cx + 'gcx . Wir können wohl eine Dummy-Eigenschaft erstellen:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

und dann etwas in der Art zurückgeben impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Etwas, das ich vergessen habe zu beachten: Wenn der deklarierte Rückgabetyp dyn Iterator<Item = &'tcx Foo> + 'cx wäre, wäre das in Ordnung, da von dyn Typen erwartet wird, dass sie Lebensdauern löschen. Deshalb glaube ich nicht jede unsoundness hier möglich ist, vorausgesetzt , Sie nicht etwas problematisch mit einem tun impl Trait , die nicht möglich wären , mit einem dyn Trait .

Man könnte sich vage die Vorstellung vorstellen, dass der Wert des abstrakten Typs ein ähnliches Existenzial ist: exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

Es scheint mir jedoch in Ordnung, eine konservative Teilmenge zu stabilisieren (die die obigen Fns ausschließt) und dies später als mögliche Erweiterung zu überdenken, sobald wir uns entschieden haben, wie wir darüber nachdenken wollen.

UPDATE: Um meine Bedeutung von dyn Eigenschaften zu verdeutlichen: Ich sage, dass sie ein Leben lang wie 'gcx "verstecken" können, solange die Grenze ( 'cx , hier) dies gewährleistet 'gcx wird immer noch live sein, wo immer dyn Trait verwendet wird.

@nikomatsakis Das ist ein interessantes Beispiel, aber ich glaube nicht, dass es die grundlegende Berechnung hier ändert, dh wir möchten, dass alle relevanten Lebensdauern allein aus dem Rückgabetyp ersichtlich sind.

Das Merkmal Captures scheint ein guter, leichter Ansatz für diese Situation zu sein. Es scheint, als könnte es im Moment als instabil in std::marker gehen?

@nikomatsakis Ihr 'gcx wegzulassen, dh 'gcx ist kein "relevante Lebensdauer" aus Sicht des Auftraggebers. Auf jeden Fall scheint es in Ordnung zu sein, konservativ zu beginnen.

Meine persönliche Meinung ist, dass https://github.com/rust-lang/rust/issues/46541 nicht wirklich ein Fehler ist - es ist das Verhalten, das ich erwarten würde, ich sehe nicht, wie es unlauter gemacht werden könnte. und es ist eine Qual, herumzuarbeiten. IMO sollte es möglich sein, einen Typ zurückzugeben, der Trait implementiert und die Lebensdauer 'a als impl Trait + 'a überlebt, egal welche anderen Lebensdauern er enthält. Ich bin jedoch damit einverstanden, einen konservativeren Ansatz zu stabilisieren, wenn @rust-lang/lang dies bevorzugt.

(Noch etwas zu klären: Das einzige Mal, dass Sie bei der Korrektur in #49041 Fehler erhalten, ist, wenn der versteckte Typ invariant in Bezug auf die fehlende Lebensdauer 'gcx , daher tritt dies wahrscheinlich relativ selten auf.)

@cramertj

Meine persönliche Meinung ist, dass #46541 nicht wirklich ein Fehler ist - es ist das Verhalten, das ich erwarten würde, ich sehe nicht, wie es unzuverlässig gemacht werden könnte, und es ist mühsam, es zu umgehen.

Ich habe Verständnis für diesen POV, aber ich zögere, etwas zu stabilisieren, für das wir nicht wissen, wie wir es entzuckern sollen (zB weil es auf einer vagen Vorstellung von existentiellen Lebenszeiten zu beruhen scheint).

@rfcbot betrifft Mehrfach-Rückkehr-Sites

Ich möchte noch ein letztes Anliegen mit dem existenziellen Impl-Merkmal anmerken. In einem erheblichen Teil der Fälle, in denen ich das Merkmal Impl verwenden möchte, möchte ich tatsächlich mehr als einen Typ zurückgeben. Beispielsweise:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Natürlich funktioniert das heute nicht und es ist definitiv außerhalb des Rahmens, um es zum Laufen zu bringen. So wie das Merkmal Impl jetzt funktioniert, schließen wir jedoch effektiv die Tür, damit es jemals funktioniert (mit dieser Syntax). Dies liegt daran, dass Sie – derzeit – Einschränkungen von mehreren Rückgabestandorten ansammeln können:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Hier ist der abgeleitete Typ (i32, bool) , wobei der erste return den i32 Teil und der zweite return bool Teil einschränkt.

Dies impliziert, dass wir niemals Fälle unterstützen könnten, in denen die beiden return Anweisungen sich nicht vereinigen (wie in meinem ersten Beispiel) – sonst wäre es sehr ärgerlich, dies zu tun.

Ich frage mich, ob wir eine Einschränkung einfügen sollten, die erfordert, dass jedes return (im Allgemeinen jede Quelle einer Einschränkung) unabhängig vollständig spezifiziert werden muss? (Und wir vereinen sie im Nachhinein?)

Dies würde mein zweites Beispiel illegal machen und uns Raum lassen, den ersten Fall möglicherweise irgendwann in der Zukunft zu unterstützen.

@rfcbot löst multiple-return-sites auf

Also habe ich ein bisschen mit @cramertj über #rust-lang gesprochen . Wir diskutierten die Idee, die "frühe Rückkehr" für impl Trait instabil zu machen, damit wir sie eventuell ändern könnten.

Sie argumentierten, dass es besser wäre, sich explizit für diese Art von Syntax zu entscheiden, insbesondere weil es andere Fälle gibt (z. B. let x: impl Trait = if { ... } else { ... } ), in denen man es haben möchte und wir nicht erwarten können sie alle implizit zu behandeln (definitiv nicht).

Das finde ich ziemlich überzeugend. Zuvor war ich davon ausgegangen, dass wir hier sowieso eine Opt-in-Syntax haben würden, aber ich wollte nur sichergehen, dass wir keine Türen vorzeitig schließen. Schließlich ist es ziemlich schwierig zu erklären, wann Sie das "dynamische Shim" einfügen müssen.

@nikomatsakis Nur meine möglicherweise weniger informierte Meinung: Es kann zwar nützlich sein, einer Funktion die Rückgabe eines von mehreren zur Laufzeit möglichen Typen zu ermöglichen, aber ich würde ungern dieselbe Syntax für sowohl die statische Rückgabe des Typs auf einen einzelnen Typ als auch die gleiche Syntax verwenden Ermöglicht Situationen, in denen intern eine Laufzeitentscheidung erforderlich ist (was Sie gerade als "dynamisches Shim" bezeichnet haben).

Dieses erste foo Beispiel könnte, soweit ich das Problem verstanden habe, entweder (1) ein umrahmtes + typgelöschtes Iterator<Item = u32> oder (2) einen Summentyp von std::option::Iter oder std::slice::Iter , was wiederum eine Iterator Implementierung ableiten würde. Ich versuche mich kurz zu fassen, da es einige Updates in der Diskussion gab (nämlich habe ich jetzt die IRC-Logs gelesen) und es immer schwieriger wird, es zu verstehen: Ich würde sicherlich einer Dyn-ähnlichen Syntax für das dynamische Shim zustimmen, obwohl ich auch verstehen, dass es nicht ideal ist, es dyn zu nennen.

Schamloser Stecker und ein kleiner Hinweis nur fürs Protokoll: "Anonyme" Summenarten und Produkte erhalten Sie ganz einfach mit:

@Centril Ja, das Zeug von Frunk ist super cool. Beachten Sie jedoch, dass, damit CoprodInjector::inject funktioniert, der resultierende Typ ableitbar sein muss, was normalerweise unmöglich ist, ohne den resultierenden Typ zu benennen (zB -> Coprod!(A, B, C) ). Es ist oft der Fall, dass Sie mit nicht benennbaren Typen arbeiten, daher benötigen Sie -> Coprod!(impl Trait, impl Trait, impl Trait) , was bei der Inferenz fehlschlägt, weil es nicht weiß, welche Variante welchen impl Trait Typ enthalten soll.

@cramertj Sehr wahr (Nebenbemerkung: jede "Variante" darf nicht ganz unbenennbar sein, sondern nur teilweise, zB: Map<Namable, Unnameable> ).

Die Idee von enum impl Trait wurde bereits in https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695 diskutiert

@Centril Ja, das stimmt. Ich denke speziell an Futures, wo ich oft Dinge schreibe wie

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj Ich würde nicht sagen, dass anonyme Aufzählung enum impl Trait ähnlich ist, weil wir nicht schließen können X: Tr && Y: Tr(X|Y): Tr (Zählerbeispiel: Default Eigenschaft). Bibliotheksautoren müssen also manuell impl Future for (X|Y|Z|...) .

@kennytm Vermutlich möchten wir einige Merkmalsimpls für anonyme Enumerationen automatisch generieren, daher scheint es im Grunde genommen die gleiche Funktion zu sein.

@cramertj Da eine anonyme Aufzählung benannt werden kann (heh), könnten wir <(i32|String)>::default() schreiben, wenn ein Default Impl für (i32|String) generiert wird. OTOH <enum impl Default>::default() einfach nicht kompilieren, egal was wir automatisch generieren, es wäre immer noch sicher, da es überhaupt nicht aufgerufen werden kann.

Dennoch gibt es einige Fälle, in denen die automatische Generierung immer noch Probleme mit enum impl Trait . Erwägen

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

Es ist völlig normal, dass wir bei einem mut rng: (XorShiftRng|IsaacRng) rng.next_u32() oder rng.gen::<u64>() berechnen könnten. rng.gen_iter::<u16>() kann jedoch nicht konstruiert werden, da die automatische Generierung nur (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) erzeugen kann, während wir eigentlich Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Vielleicht kann der Compiler einen delegierten-unsicheren Aufruf automatisch ablehnen, genau wie die Sized Prüfung.)

FWIW scheint mir, dass diese Funktion im Geiste näher an Closures als an Tupeln ist (die natürlich das anonyme struct Gegenstück zu den hypothetischen anonymen enum s sind). Die Art und Weise, in der diese Dinge "anonym" sind, ist unterschiedlich.

Für anonyme struct s und enum s (Tupel und "Disjoins") ist das "Anonyme" im Sinne von "strukturellen" (im Gegensatz zu "nominalen") Typen - sie' re-integriert, vollständig generisch über ihre Komponententypen und sind keine benannte Deklaration in einer Quelldatei. Aber der Programmierer schreibt sie immer noch aus und verwendet sie wie jeden anderen Typ, Trait-Implementierungen für sie werden wie üblich explizit aufgeschrieben und sie sind nicht besonders magisch (abgesehen von der eingebauten Syntax und der Tatsache, dass sie 'variadisch' sind, was andere Typen kann noch nicht sein). In gewissem Sinne haben sie einen Namen, sondern alphanumerische zu sein, ihre ‚name‘ ist die Syntax schreiben sie verwendet (Klammern und Kommata).

Verschlüsse hingegen sind anonym in dem Sinne, dass ihr Name geheim ist . Der Compiler generiert jedes Mal, wenn Sie einen neuen Typ schreiben, einen neuen Typ mit einem neuen Namen, und es gibt keine Möglichkeit, diesen Namen herauszufinden oder darauf zu verweisen, selbst wenn Sie es wollten. Der Compiler implementiert ein oder zwei Merkmale für diesen geheimen Typ, und Sie können nur über diese Merkmale damit interagieren.

In der Lage zu sein, verschiedene Typen aus verschiedenen Zweigen eines if , hinter einem impl Trait , scheint letzterem näher zu sein -- der Compiler generiert implizit einen Typ, der die verschiedenen Zweige enthält, implementiert das angeforderte Merkmal darauf, um es an den entsprechenden zu senden, und der Programmierer schreibt nie auf oder sieht, was dieser Typ ist, und kann sich nicht darauf beziehen und hat auch keinen wirklichen Grund, dies zu wollen.

(Tatsächlich fühlt sich diese Funktion irgendwie mit den hypothetischen "Objektliteralen" verwandt an -- was für andere Merkmale die vorhandene Closure-Syntax für Fn . Das heißt, anstelle eines einzelnen Lambda-Ausdrucks, Sie Ich würde jede Methode des angegebenen Merkmals (wobei self implizit ist) mithilfe der Variablen im Gültigkeitsbereich implementieren, und der Compiler würde einen anonymen Typ generieren, um die Upvars zu halten, und das angegebene Merkmal dafür implementieren, es würde einen optionalen move Modus auf die gleiche Weise haben usw. Wie auch immer, ich vermute, eine andere Art, if foo() { (some future) } else { (other future) } auszudrücken, wäre object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (nun, du brauchst auch das Ergebnis von foo() in ein let zu heben, damit es nur einmal ausgeführt wird). Das ist eher weniger ergonomisch und sollte wahrscheinlich nicht als echte *Alternative zu den anderen angesehen werden Funktion, aber es deutet auf eine Beziehung hin. Vielleicht könnte ersteres in letzteres übergehen oder so.)

@glaebhörl das ist eine sehr interessante Idee! Auch hier gibt es etwas Stand der Technik aus Java.

Einige Gedanken aus dem Kopf (also nicht sehr gebacken):

  1. [bikeshed] das Präfix object deutet darauf hin, dass dies eher ein Merkmalsobjekt als nur existentiell ist – aber das ist es nicht.

Eine mögliche alternative Syntax:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [Makros/Zucker] können Sie etwas trivialen syntaktischen Zucker bereitstellen, damit Sie Folgendes erhalten:
future!(if foo() { a.poll() } else { b.poll() })

Ja, die Syntaxfrage ist ein Durcheinander, weil nicht klar ist, ob Sie sich von struct Literalen, -Verschlüssen oder impl Blöcken inspirieren lassen wollen :) Sake. (Wie auch immer, mein Hauptpunkt war nicht, dass wir Objektliterale hinzufügen sollten [obwohl wir] sollten, sondern dass anonyme enum s hier ein Ablenkungsmanöver sind [obwohl wir sie auch hinzufügen sollten].)

In der Lage zu sein, verschiedene Typen von verschiedenen Zweigen eines if zurückzugeben, hinter einem impliziten Trait, scheint letzterem näher zu sein -- der Compiler generiert implizit einen Typ, der die verschiedenen Zweige enthält, implementiert das angeforderte Merkmal darauf, um es an den entsprechenden zu senden. und der Programmierer schreibt nie auf oder sieht, was dieser Typ ist, und kann sich nicht darauf beziehen und hat auch keinen wirklichen Grund, dies zu wollen.

Hmm. Ich war also davon ausgegangen, dass wir, anstatt "frische Namen" für Enum-Typen zu generieren, stattdessen | Typen verwenden würden, die folgenden Impls entsprechen:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Offensichtlich würde dies Kohärenzprobleme in dem Sinne geben, dass mehrere Funktionen identische Impls erzeugen. Aber selbst wenn ich diese beiseite lasse, merke ich jetzt, dass diese Idee aus anderen Gründen möglicherweise nicht funktioniert - zB wenn mehrere Typen zugeordnet sind, müssen sie in einigen Kontexten möglicherweise gleich sein, in anderen jedoch dürfen sie unterschiedlich sein. Vielleicht kehren wir zum Beispiel zurück:

-> impl IntoIterator<Item = Y>

aber woanders machen wir das

-> impl IntoIterator<IntoIter = X, Item = Y>

Dies wären zwei überlappende Impls, die nicht "zusammengeführt" werden können; naja, vielleicht mit spezialisierung.

Wie auch immer, der Begriff "geheime Aufzählungen" scheint überall sauberer zu sein, nehme ich an.

Ich möchte noch ein letztes Anliegen mit dem existenziellen Impl-Merkmal anmerken. In einem erheblichen Teil der Fälle, in denen ich das Merkmal Impl verwenden möchte, möchte ich tatsächlich mehr als einen Typ zurückgeben.

@nikomatsakis : Ist es fair zu sagen, dass das, was in diesem Fall zurückgegeben wird, näher an dyn Trait als an impl Trait , da der synthetische/anonyme Rückgabewert so etwas wie dynamischer Versand implementiert?

cc https://github.com/rust-lang/rust/issues/49288 , ein Problem, auf das ich in letzter Zeit häufig bei der Arbeit mit Future s und Future -Returning-Trait-Methoden gestoßen bin.

Da dies die letzte Chance vor der Schließung des FCP ist, möchte ich ein letztes Argument gegen automatische automatische Merkmale anführen. Mir ist klar, dass dies ein bisschen in letzter Minute ist, daher möchte ich dieses Problem höchstens formell ansprechen, bevor wir uns auf die aktuelle Implementierung festlegen.

Um es allen klarzustellen, die impl Trait nicht impl X Typen repräsentiert wird, implementiert derzeit automatisch Auto-Traits, wenn und nur wenn der konkrete Typ dahinter diese Auto-Traits implementiert. Konkret, wenn die folgende Codeänderung vorgenommen wird, wird die Funktion weiterhin kompiliert, aber alle Verwendungen der Funktion, die darauf beruhen, dass der zurückgegebene Typ Send implementiert,

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(einfaches Beispiel: Funktionieren , interne Änderungen führen zum Scheitern )

Dieses Problem ist nicht eindeutig. Es gab eine sehr absichtliche Entscheidung, Auto-Traits "lecken" zu lassen: Wenn nicht, müssten wir + !Send + !Sync auf jede Funktion setzen, die etwas zurückgibt, das nicht Send oder Non-Sync zurückgibt, und wir würden haben eine unklare Geschichte mit potenziellen anderen benutzerdefinierten automatischen Merkmalen, die für den konkreten Typ, den die Funktion zurückgibt, einfach nicht implementierbar sein könnten. Dies sind zwei Probleme, auf die ich später eingehen werde.

Zuerst möchte ich einfach meinen Einwand gegen das Problem vorbringen: Dies ermöglicht es, einen Funktionskörper zu ändern, um die öffentlich zugängliche API zu ändern. Dies reduziert direkt die Wartbarkeit des Codes.

Während der Entwicklung von Rost wurden Entscheidungen getroffen, die auf der Seite der Ausführlichkeit über der Benutzerfreundlichkeit liegen. Wenn Neulinge diese sehen, denken sie, es sei Ausführlichkeit um der Ausführlichkeit willen, aber das ist nicht der Fall. Jede Entscheidung, ob Strukturen nicht automatisch Copy implementieren oder alle Typen explizit bei Funktionssignaturen sind, dient der Wartbarkeit.

Wenn ich den Leuten Rust vorstelle, kann ich ihnen Geschwindigkeit, Produktivität und Speichersicherheit zeigen. Aber gehen hat Geschwindigkeit. Ada hat Speichersicherheit. Python hat Produktivität. Was Rust hat, übertrumpft all dies, es ist wartbar. Wenn ein Bibliotheksautor einen effizienteren Algorithmus ändern oder die Struktur einer Crate neu erstellen möchte, hat der Compiler eine starke Garantie dafür, dass er ihn benachrichtigt, wenn er Fehler macht. Bei Rost kann ich sicher sein, dass mein Code nicht nur in Bezug auf Speichersicherheit, sondern auch Logik und Schnittstelle weiterhin funktioniert. _Jede Funktionsschnittstelle in Rust ist durch die Typdeklaration der Funktion vollständig darstellbar_.

Die Stabilisierung von impl Trait hat eine große Chance, diesem Glauben zu widersprechen. Sicher, es ist sehr schön, schnell Code zu schreiben, aber wenn ich Prototypen erstellen möchte, verwende ich Python. Rust ist die Sprache der Wahl, wenn eine langfristige Wartbarkeit und kein kurzfristiger schreibgeschützter Code erforderlich ist.


Ich sage, es besteht nur eine "große Chance", dass dies hier schlecht ist, denn auch hier ist das Thema nicht eindeutig. Die ganze Idee von 'Auto-Eigenschaften' ist in erster Linie nicht explizit. Send und Sync werden basierend auf dem Inhalt einer Struktur implementiert, nicht auf der öffentlichen Deklaration. Da diese Entscheidung für Rost aufgegangen ist, könnte impl Trait ähnlich vorgehen, auch gut funktionieren.

Funktionen und Strukturen werden jedoch in einer Codebasis unterschiedlich verwendet, und dies sind nicht dieselben Probleme.

Beim Ändern der Felder einer Struktur, sogar privater Felder, ist sofort klar, dass der eigentliche Inhalt geändert wird. Strukturen mit Nicht-Senden- oder Nicht-Sync-Feldern haben diese Wahl getroffen, und Bibliotheksverwalter wissen, dass sie doppelt prüfen müssen, wenn ein PR die Felder einer Struktur ändert.

Wenn Sie die Interna einer Funktion ändern, ist es definitiv klar, dass dies sowohl die Leistung als auch die Korrektheit beeinträchtigen kann. In Rust müssen wir jedoch nicht überprüfen, ob wir den richtigen Typ zurückgeben. Funktionsdeklarationen sind ein harter Vertrag, den wir einhalten müssen, und rustc wacht über uns. Es ist ein schmaler Grat zwischen automatischen Merkmalen in Strukturen und in Funktionsrückgaben, aber das Ändern der Interna einer Funktion ist viel Routine. Sobald wir volle generatorbetriebene Future s haben, wird es noch routinemäßiger sein, Funktionen zu ändern, die -> impl Future . Dies sind alles Änderungen, die Autoren auf geänderte Send/Sync-Implementierungen überprüfen müssen, wenn der Compiler sie nicht erkennt.

Um dies zu beheben, könnten wir entscheiden, dass dies ein akzeptabler Wartungsaufwand ist, wie dies in der ursprünglichen RFC-Diskussion der Fall war . In diesem Abschnitt im konservativen Impl-Trait-RFC werden die

Ich habe meine Hauptantwort darauf bereits dargelegt, aber hier noch eine letzte Anmerkung. Das Ändern des Layouts einer Struktur ist nicht so üblich; davor kann man sich schützen. Der Wartungsaufwand, um sicherzustellen, dass Funktionen weiterhin dieselben automatischen Merkmale implementieren, ist größer als der von Strukturen, einfach weil sich Funktionen viel mehr ändern.


Abschließend möchte ich sagen, dass automatische automatische Merkmale nicht die einzige Option sind. Wir haben uns für diese Option entschieden, aber die Alternative zur Deaktivierung von automatischen Merkmalen ist immer noch eine Alternative.

Wir könnten verlangen, dass Funktionen, die Nicht-Sende-/Nicht-Sync-Elemente zurückgeben, entweder + !Send + !Sync oder ein Merkmal (möglicherweise ein Alias?) zurückgeben, das diese Grenzen hat. Das wäre keine gute Entscheidung, aber vielleicht besser als die, die wir derzeit wählen.

Was die Bedenken bezüglich benutzerdefinierter automatischer Eigenschaften angeht, würde ich argumentieren, dass keine neuen automatischen Eigenschaften nur für neue Typen implementiert werden sollten, die nach der automatischen Eigenschaft eingeführt wurden. Dies könnte ein größeres Problem darstellen, als ich jetzt ansprechen kann, aber es ist kein Problem, das wir nicht mit mehr Design angehen können.


Dies ist sehr spät und sehr langwierig, und ich bin sicher, dass ich diese Einwände schon einmal vorgebracht habe. Ich freue mich, nur ein letztes Mal kommentieren zu können und sicherzustellen, dass wir mit der Entscheidung, die wir treffen, voll und ganz einverstanden sind.

Vielen Dank fürs Lesen, und ich hoffe, dass die endgültige Entscheidung Rust in die beste Richtung weist, in die es gehen kann.

Aufbauend auf @daboross ‚Die Meinung von WRT. Merkmals-Aliasnamen könnte man die Ergonomie von nicht-leckenden automatischen Merkmalen wie folgt verbessern:

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Das ist nicht so schlimm – Sie müssten sich einen guten Namen einfallen lassen (was FutureNSS nicht ist). Der Hauptvorteil besteht darin, dass der Papierschnitt durch die Wiederholung der Grenzen reduziert wird.

Wäre es nicht möglich, diese Funktion mit den Anforderungen zu stabilisieren, Automerkmale explizit anzugeben und diese Anforderungen später möglicherweise zu entfernen, sobald wir eine geeignete Lösung für dieses Wartungsproblem gefunden haben oder wenn wir sicher genug sind, dass es tatsächlich keinen Wartungsaufwand gibt, indem die Entscheidung, die Anforderungen aufzuheben?

Was ist, wenn Send sei denn, es ist als !Send markiert, aber nicht Sync sei denn, es ist als Sync gekennzeichnet? Soll Send im Vergleich zu Sync nicht häufiger vorkommen?

So was:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Gibt es eine Inkonsistenz zwischen impl Trait in der Argumentposition und der Rückgabeposition bzgl. Auto-Eigenschaften?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Dies ist für die Argumentposition sinnvoll, denn wenn Sie hier Send annehmen dürften, würden Sie Post-Monomorphisierungsfehler erhalten. Natürlich müssen die Regeln für Rückgabeposition und Argumentposition hier nicht übereinstimmen, aber die Erlernbarkeit stellt ein Problem dar.

Was die Bedenken bezüglich benutzerdefinierter automatischer Eigenschaften angeht, würde ich argumentieren, dass keine neuen automatischen Eigenschaften nur für neue Typen implementiert werden sollten, die nach der automatischen Eigenschaft eingeführt wurden.

Nun, dies gilt auch für die kommende Auto trait Unpin (nur nicht-implmented für selbstbezüglicher Generatoren), aber ... , die pure Glück zu sein scheint? Ist das eine Einschränkung, mit der wir wirklich leben können? Ich kann nicht glauben, dass es in Zukunft nichts geben wird, was für zB &mut oder Rc deaktiviert werden müsste ...

Ich glaube, das wurde diskutiert, und das ist natürlich sehr spät, aber ich bin immer noch unzufrieden mit impl Trait in der Argumentposition.

Die Fähigkeiten sowohl a) Arbeit mit Verschlüssen / Futures nach Wert, und b) zu behandeln , einige Arten als „Ausgänge“ und damit Implementierungsdetails sind idiomatische und haben seit der Zeit vor 1,0 gewesen, weil sie direkt Rust Kernwerte von Leistung, Stabilität unterstützen, und Sicherheit.

-> impl Trait erfüllt also lediglich ein Versprechen von 1.0, entfernt einen Randfall oder verallgemeinert vorhandene Funktionen: Es fügt Ausgabetypen zu Funktionen hinzu, wobei der gleiche Mechanismus verwendet wird, der immer verwendet wurde, um anonyme Typen zu behandeln, und ihn anwendet in mehr Fällen. Es mag prinzipieller gewesen sein, mit abstract type , dh Ausgabetypen für Module, aber da Rust kein ML-Modulsystem hat, ist die Reihenfolge keine große Sache.

fn f(t: impl Trait) fühlt sich stattdessen so an, als ob es "nur weil wir können" hinzugefügt wurde, was die Sprache größer und fremder macht, ohne dafür genug zurückzugeben. Ich habe gekämpft und konnte kein vorhandenes Framework finden, in das es passt. Ich verstehe das Argument um die Prägnanz von fn f(f: impl Fn(...) -> ...) und die Begründung, dass Grenzen bereits in den Klauseln <T: Trait> und where , aber diese fühlen sich hohl an. Sie negieren die Nachteile nicht:

  • Sie müssen jetzt zwei Syntaxen für Grenzen lernen – mindestens <> / where teilen sich eine einzige Syntax.

    • Dies schafft auch eine Lernklippe und verschleiert die Idee, denselben generischen Typ an mehreren Stellen zu verwenden.

    • Die neue Syntax macht es schwieriger zu erkennen, was eine Funktion generisch ist – Sie müssen die gesamte Argumentliste durchsuchen.

  • Was nun das Implementierungsdetail einer Funktion sein sollte (wie sie ihre Typparameter deklariert) wird Teil ihrer Schnittstelle, da Sie ihren Typ nicht schreiben können!

    • Dies hängt auch mit den derzeit diskutierten Auto-Trait-Komplikationen zusammen – weitere Verwirrung darüber, was die öffentliche Schnittstelle einer Funktion ist und was nicht.

  • Die Analogie zu dyn Trait ist ehrlich gesagt falsch:

    • dyn Trait bedeutet immer dasselbe und "infiziert" seine umgebenden Deklarationen nur durch den vorhandenen automatischen Merkmalsmechanismus.

    • dyn Trait in Datenstrukturen verwendet werden, und dies ist wirklich einer der Hauptanwendungsfälle. impl Trait in Datenstrukturen macht keinen Sinn, ohne sich alle Verwendungen der Datenstruktur anzusehen.

    • Ein Teil dessen, was dyn Trait bedeutet, ist das Löschen von Typen, aber impl Trait impliziert nichts über seine Implementierung.

    • Der vorherige Punkt wird noch verwirrender, wenn wir nicht-monomorphisierte Generika einführen. Tatsächlich wird fn f(t: impl Trait) in einer solchen Situation wahrscheinlich a) nicht mit der neuen Funktion funktionieren und/oder b) noch mehr Randfallanwälte erfordern, wie das Problem mit den automatischen Merkmalen. Stellen Sie sich fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :Schrei:

Für mich kommt es also darauf an, dass impl Trait in der Argumentposition Grenzfälle hinzufügt, mehr vom Fremdheitsbudget verwendet, die Sprache größer erscheinen lässt usw. während impl Trait in der Rückgabeposition vereint, vereinfacht und lässt die Sprache enger zusammenhängen.

Was ist mit Senden, wenn es nicht als !Senden markiert ist, aber keine Synchronisierung bereitzustellen, es sei denn, es ist als Synchronisierung markiert? Soll Send im Vergleich zu Sync nicht häufiger vorkommen?

Das fühlt sich sehr… willkürlich und ad-hoc an. Vielleicht ist es weniger Tippen, aber mehr Erinnerung und mehr Chaos.

Fahrradschuppen-Idee hier, um nicht von meinen obigen Punkten abzulenken: Verwenden Sie anstelle von impl type ? Dies ist das Schlüsselwort, das für verknüpfte Typen verwendet wird, wahrscheinlich (eines der) Schlüsselwörter, die für abstract type , es ist immer noch ziemlich natürlich und weist mehr auf die Idee von "Ausgabetypen für Funktionen" hin:

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Vielen Dank fürs Lesen, und ich hoffe, dass die endgültige Entscheidung Rust in die beste Richtung weist, in die es gehen kann.

Ich schätze den gut formulierten Einwand. Wie Sie bereits erwähnt haben, waren automatische Merkmale immer eine bewusste Entscheidung, um einige Implementierungsdetails "aufzudecken", von denen man erwartet hätte, dass sie verborgen bleiben. Ich denke, dass diese Wahl - bis jetzt - eigentlich ganz gut funktioniert hat, aber ich gestehe, dass ich ständig nervös deswegen bin.

Es scheint mir , dass die wichtige Frage ist das Ausmaß , in dem Funktionen wirklich von structs verschieden sind:

Das Ändern des Layouts einer Struktur ist nicht so üblich; davor kann man sich schützen. Der Wartungsaufwand, um sicherzustellen, dass Funktionen weiterhin dieselben automatischen Merkmale implementieren, ist größer als der von Strukturen, einfach weil sich Funktionen viel mehr ändern.

Es ist wirklich schwer zu sagen, wie wahr das sein wird. Es scheint , wie die allgemeine Regel sein wird, dass die Einführung von Rc etwas ist mit Vorsicht durchgeführt werden - es ist nicht so sehr eine Frage, wo Sie es speichern. (Eigentlich ist der Fall, an dem ich wirklich arbeite, nicht Rc , sondern die Einführung von dyn Trait , da dies weniger offensichtlich sein kann.)

Ich vermute stark, dass in Code, der Futures zurückgibt, die Arbeit mit nicht-thread-sicheren Typen usw. selten sein wird. Sie werden diese Art von Bibliotheken eher meiden. (Außerdem lohnt es sich natürlich immer, Tests in realistischen Szenarien durchführen zu lassen.)

Das ist auf jeden Fall frustrierend, denn so etwas ist im Voraus schwer zu wissen, egal wie lange wir eine Stabilisierungsphase vorgeben.

Abschließend möchte ich sagen, dass automatische automatische Merkmale nicht die einzige Option sind. Wir haben uns für diese Option entschieden, aber die Alternative zur Deaktivierung von automatischen Merkmalen ist immer noch eine Alternative.

Stimmt, obwohl ich definitiv nervös bin bei der Idee, bestimmte Automerkmale wie Send "herauszuheben". Beachten Sie auch, dass es neben Futures noch andere Anwendungsfälle für Impl-Eigenschaften gibt. Zum Beispiel das Zurückgeben von Iteratoren oder Schließungen – und in diesen Fällen ist es nicht offensichtlich, dass Sie standardmäßig senden oder synchronisieren möchten. Auf jeden Fall, was Sie wirklich wollen und was wir versuchen aufzuschieben =), ist eine Art "bedingte" Bindung (Senden, wenn T Send ist). Genau das geben Ihnen Auto-Merkmale.

@rpjohnst

Ich glaube das wurde diskutiert

Tatsächlich hat es :) seit dem ersten impl Trait RFC vor vielen Jahren. (Woah, 2014. Ich fühle mich alt.)

Ich habe gekämpft und konnte kein vorhandenes Framework finden, in das es passt.

Ich empfinde ganz das Gegenteil. Für mich hebt sich ohne impl Trait in Argumentposition impl Trait in Returnposition umso mehr ab. Der verbindende Thread, den ich sehe, ist:

  • impl Trait -- wo es erscheint, zeigt es an, dass es "einen monomorphisierten Typ geben wird, der Trait implementiert". (Die Frage, wer diesen Typ angibt – der Anrufer oder der Angerufene – hängt davon ab, wo impl Trait erscheint.)
  • dyn Trait -- wo es erscheint, zeigt es an, dass es einen Typ geben wird, der Trait implementiert, aber dass die Auswahl des Typs dynamisch erfolgt.

Es gibt auch Pläne, die Anzahl der Orte zu erweitern, an denen impl Trait können, und baut auf dieser Intuition auf. Zum Beispiel erlaubt https://github.com/rust-lang/rfcs/pull/2071

let x: impl Trait = ...;

Es gilt das gleiche Prinzip: Die Wahl des Typs ist statisch bekannt. In ähnlicher Weise führt derselbe RFC abstract type (wofür impl Trait als eine Art syntaktischer Zucker verstanden werden kann), die in Trait-Impls und sogar als Member in Modulen auftreten können.

Fahrradschuppen-Idee hier, um nicht von meinen obigen Punkten abzulenken: Verwenden Sie anstelle von impl type ?

Ich persönlich bin nicht geneigt, hier einen Fahrradschuppen wieder aufzubauen. Wir haben einige Zeit damit verbracht, die Syntax in https://github.com/rust-lang/rfcs/pull/2071 und anderswo zu diskutieren. Es scheint kein "perfektes Schlüsselwort" zu geben, aber das Lesen von impl als "einer Typ, der implementiert" funktioniert imo ziemlich gut.

Lassen Sie mich noch ein bisschen mehr zum Thema Auto Trait Leakage hinzufügen:

Zunächst einmal denke ich, dass Auto Trait Leakage hier eigentlich das Richtige ist, gerade weil es mit dem Rest der Sprache übereinstimmt. Auto-Eigenschaften waren – wie ich bereits sagte – immer ein Glücksspiel, aber sie scheinen sich im Wesentlichen ausgezahlt zu haben. Ich sehe nur nicht, dass impl Trait so sehr unterschiedlich ist.

Aber ich bin auch ziemlich nervös, hier zu verzögern. Ich stimme zu, dass es noch andere interessante Punkte im Designbereich gibt und ich bin nicht 100% sicher, dass wir den richtigen Punkt erreicht haben, aber ich weiß nicht, ob wir uns jemals sicher sein werden. Ich bin ziemlich besorgt, wenn wir es jetzt verzögern, wird es uns schwer fallen, unsere Roadmap für das Jahr einzuhalten.

Betrachten wir abschließend die Implikationen, wenn ich falsch liege: Worüber wir hier im Grunde sprechen, ist, dass die Beurteilung von Semver noch subtiler wird. Dies ist meiner Meinung nach ein Problem, das jedoch auf verschiedene Weise gemildert werden kann. Zum Beispiel können wir Lints verwenden, die warnen, wenn die Typen !Send oder !Sync eingeführt werden. Wir haben lange über die Einführung eines Semver-Checkers gesprochen, der Ihnen hilft, versehentliche Semver-Verstöße zu verhindern - dies scheint ein weiterer Fall zu sein, in dem dies hilfreich wäre. Kurz gesagt, ein Problem, aber ich denke nicht, dass es ein kritisches ist.

Ich fühle mich also – zumindest zum jetzigen Zeitpunkt – immer noch geneigt, den bisherigen Weg fortzusetzen.

Ich persönlich bin nicht geneigt, hier einen Fahrradschuppen wieder aufzubauen.

Ich bin auch nicht sonderlich darin investiert; es war ein nachträglicher Einfall auf meinem Eindruck aus , dass impl Trait von „in Löchern Füllung“ in Argumente Position motivierte zu sein scheint syntaktisch nicht semantisch, die angesichts Ihrer Antwort richtig zu sein scheint. :)

Für mich hebt sich ohne impl Trait in Argumentposition impl Trait in Returnposition umso mehr ab.

Angesichts der Analogie zu assoziierten Typen kommt dies sehr ähnlich rüber wie "ohne type T in der Argumentposition stechen die assoziierten Typen umso mehr hervor." Ich vermute, dass kein besonderer Einwand erhoben wurde, weil die von uns gewählte Syntax sie unsinnig erscheinen lässt – die vorhandene Syntax dort ist gut genug, dass niemand das Bedürfnis nach syntaktischem Zucker wie trait Trait<type SomeAssociatedType> verspürt.

Wir haben bereits eine Syntax für "einen monomorphisierten Typ, der Trait implementiert". Bei den Merkmalen haben wir sowohl vom "Anrufer" als auch vom "Angerufenen" spezifizierte Varianten. Bei Funktionen haben wir nur die vom Aufrufer spezifizierte Variante, also brauchen wir die neue Syntax für die vom Aufrufer spezifizierte Variante.

Die Erweiterung dieser neuen Syntax auf lokale Variablen könnte gerechtfertigt sein, da dies auch eine sehr ähnliche Situation ist, die einem assoziierten Typ ähnelt - es ist eine Möglichkeit, den Ausgabetyp eines Ausdrucks zu verbergen und zu benennen, und ist nützlich, um die Ausgabetypen von aufgerufenen Funktionen weiterzuleiten.

Wie ich in meinem vorherigen Kommentar erwähnt habe, bin ich auch ein Fan von abstract type . Auch hier handelt es sich lediglich um eine Erweiterung des Konzepts "Ausgabetyp" auf Module. Und die Anwendung der Inferenz von -> impl Trait , let x: impl Trait und abstract type auf die zugeordneten Typen von Trait Impls ist ebenfalls großartig.

Es ist insbesondere das Konzept, diese neue Syntax für Funktionsargumente hinzuzufügen, das mir nicht gefällt. Es macht nicht dasselbe wie die anderen Funktionen, mit denen es eingezogen wird. Es macht dasselbe wie die Syntax, die wir bereits haben, nur mit mehr Grenzfällen und weniger Anwendbarkeit. :/

@nikomatsakis

Es ist wirklich schwer zu sagen, wie wahr das sein wird.

Es scheint mir, dass wir dann auf der Seite des Konservativen irren sollten? Können wir mit mehr Zeit mehr Vertrauen in das Design gewinnen (indem wir Auto-Trait-Leakage unter einem separaten Feature-Gate und nur nachts durchführen lassen, während wir den Rest von impl Trait stabilisieren)? Wir können später immer noch Unterstützung für das Durchsickern von Auto-Eigenschaften hinzufügen, wenn wir jetzt nicht durchsickern.

Aber ich bin auch ziemlich nervös, hier zu verzögern. [..] Ich bin ziemlich besorgt, wenn wir es jetzt verzögern, werden wir es schwer haben, unsere Roadmap für das Jahr einzuhalten.

Verständlich! Die Entscheidungen hier werden uns aber noch viele Jahre begleiten, und das haben Sie sicher schon bedacht.

Zum Beispiel können wir Lints verwenden, die warnen, wenn die Typen !Send oder !Sync eingeführt werden. Wir haben lange über die Einführung eines Semver-Checkers gesprochen, der Ihnen hilft, versehentliche Semver-Verstöße zu verhindern - dies scheint ein weiterer Fall zu sein, in dem dies hilfreich wäre. Kurz gesagt, ein Problem, aber ich denke nicht, dass es ein kritisches ist.

Das ist gut zu hören! 🎉 Und ich denke, das besänftigt meine Bedenken größtenteils.

Stimmt, obwohl ich definitiv nervös bin bei der Idee, bestimmte Automerkmale wie Send "herauszuheben".

Dieser Meinung stimme ich voll und ganz zu 👍.

Auf jeden Fall, was Sie wirklich wollen und was wir versuchen aufzuschieben =), ist eine Art "bedingte" Bindung (Senden, wenn T Send ist). Genau das geben Ihnen Auto-Merkmale.

Ich denke, dass T: Send => Foo<T>: Send besser verstanden werden würde, wenn der Code dies explizit angeben würde.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Tho, wie wir in WG-Traits besprochen haben, bekommst du hier möglicherweise überhaupt keine Schlussfolgerungen, also musst du immer Extra angeben, wenn du etwas anderes als Send willst, was ein totaler Mist wäre .

@rpjohnst

Die Analogie zu dyn Trait ist ehrlich gesagt falsch:

In Bezug auf impl Trait in der Argumentposition ist es falsch, aber nicht so bei -> impl Trait da beide existentielle Typen sind.

  • Was nun das Implementierungsdetail einer Funktion sein sollte (wie sie ihre Typparameter deklariert) wird Teil ihrer Schnittstelle, da Sie ihren Typ nicht schreiben können!

Ich möchte anmerken, dass die Reihenfolge der Typparameter aufgrund von turbofish nie ein Implementierungsdetail war, und in dieser Hinsicht denke ich, dass impl Trait hilfreich sein kann, da Sie bestimmte Typargumente in turbofish unspezifiziert lassen können .

[..] die dort vorhandene Syntax ist gut genug, dass niemand das Bedürfnis nach syntaktischem Zucker wie Trait Trait . verspürt.

Sag niemals nie? https://github.com/rust-lang/rfcs/issues/2274

Wie @nikomatsakis schätze ich die Sorgfalt, die in diesen Last-Minute-Kommentaren verwendet wird; Ich weiß, dass es sich anfühlen kann, als würde man sich vor einen Güterzug werfen, besonders bei einem so lang ersehnten Feature wie diesem!


@daboross , ich wollte die Opt-out-Idee noch etwas genauer untersuchen. Auf den ersten Blick scheint es vielversprechend zu sein, da es uns ermöglichen würde, die Signatur vollständig anzugeben, jedoch mit Standardeinstellungen, die den allgemeinen Fall prägnant machen.

Leider treten jedoch einige Probleme auf, wenn Sie das Gesamtbild betrachten:

  • Wenn automatische Merkmale für impl Trait als Opt-out behandelt wurden, sollten sie auch für dyn Trait .
  • Dies gilt natürlich auch dann, wenn diese Konstrukte in Argumentposition verwendet werden.
  • Aber dann wäre es ziemlich seltsam, wenn sich Generika anders verhalten würden. Mit anderen Worten, für fn foo<T>(t: T) können Sie standardmäßig T: Send erwarten.
  • Dafür haben wir natürlich einen Mechanismus, der derzeit nur auf Sized angewendet wird; Es ist eine Eigenschaft, die standardmäßig überall angenommen wird und für die Sie sich abmelden, indem Sie ?Sized schreiben

Der ?Sized Mechanismus bleibt einer der obskursten und am schwersten zu erlernenden Aspekte von Rust, und wir haben es im Allgemeinen sehr abgeneigt, ihn auf andere Konzepte auszuweiten. Es für ein so zentrales Konzept wie Send scheint riskant – ganz zu schweigen davon, dass es eine große bahnbrechende Veränderung wäre.

Was mehr ist , aber: wir wirklich, für Generika in einem Auto - Zug Annahme nicht backen wollen , weil ein Teil der Schönheit von Generika heute ist , dass Sie effektiv generic darüber , ob ein Typ implementiert ein Auto - Merkmal sein kann, und haben diese Informationen nur "durchströmen". Betrachten Sie beispielsweise fn f<T>(t: T) -> Option<T> . Wir können T unabhängig davon, ob es Send , und die Ausgabe ist Send wenn T war. Dies ist ein enorm wichtiger Teil der Generika-Geschichte in Rust.

Es gibt auch Probleme mit dyn Trait . Insbesondere aufgrund der separaten Kompilierung müssten wir diese "Opt-out"-Natur ausschließlich auf "bekannte" automatische Merkmale wie Send und Sync ; es würde wahrscheinlich bedeuten, auto trait niemals für den externen Gebrauch zu stabilisieren.

Abschließend sei noch einmal darauf hingewiesen, dass das "Leakage"-Design explizit nach dem modelliert wurde, was heute passiert, wenn Sie einen Newtype-Wrapper erstellen, um einen undurchsichtigen Typ zurückzugeben. Grundsätzlich glaube ich, dass "Leckage" in erster Linie ein inhärenter Aspekt von Auto-Eigenschaften ist; Es hat Kompromisse, aber es ist der Kern der Funktion, und ich denke, wir sollten nach neuen Funktionen streben, um entsprechend damit zu interagieren.


@rpjohnst

Nach den ausführlichen Diskussionen über den RFC und den zusammenfassenden Kommentar von @nikomatsakis oben habe ich nicht viel hinzuzufügen.

Was nun das Implementierungsdetail einer Funktion sein sollte (wie sie ihre Typparameter deklariert) wird Teil ihrer Schnittstelle, da Sie ihren Typ nicht schreiben können!

Ich verstehe nicht, was du damit meinst. Können Sie erweitern?

Ich möchte auch darauf hinweisen, dass Sätze wie:

fn f(t: impl Trait) fühlt sich stattdessen so an, als ob es hinzugefügt wurde "nur weil wir es können"

die Diskussion in gutem Glauben untergraben (ich rufe dies hervor, weil es ein sich wiederholendes Muster ist). Der RFC unternimmt erhebliche Anstrengungen, um das Feature zu motivieren und einige der Argumente zu widerlegen, die Sie hier vorbringen – ganz zu schweigen natürlich von der Diskussion im Thread und in früheren Iterationen des RFC usw.

Es gibt Kompromisse, es gibt tatsächlich Nachteile, aber es hilft uns nicht, eine begründete Schlussfolgerung zu ziehen, um "die andere Seite" der Debatte zu karikieren.

Danke an alle für ihre ausführlichen Kommentare! Ich freue mich wirklich sehr, impl Trait endlich auf Stable auszuliefern, daher bin ich stark von der aktuellen Implementierung und den Designentscheidungen, die dazu geführt haben, voreingenommen. Trotzdem werde ich mein Bestes geben, so unparteiisch wie möglich zu antworten und die Dinge so zu betrachten, als würden wir bei Null beginnen:

auto Trait Leckage

Die Idee von auto Trait Undichtigkeiten hat mich lange Zeit gestört - in gewisser Weise kann es vielen von Rusts Designzielen widersprüchlich erscheinen. Im Vergleich zu seinen Vorfahren, wie C++ oder der ML-Familie, ist Rust insofern ungewöhnlich, als es erfordert, dass generische Grenzen in Funktionsdeklarationen explizit angegeben werden. Dies macht meiner Meinung nach die generischen Funktionen von Rust einfacher zu lesen und zu verstehen, und es macht relativ klar, wann eine abwärts-inkompatible Änderung vorgenommen wird. Wir haben dieses Muster in unserem Ansatz für const fn fortgesetzt, der verlangt, dass Funktionen sich selbst explizit als const angeben, anstatt const aus Funktionskörpern abzuleiten. Ähnlich wie bei expliziten Merkmalsgrenzen ist es einfacher zu erkennen, welche Funktionen auf welche Weise verwendet werden können, und gibt Bibliotheksautoren die Gewissheit, dass kleine Änderungen an der Implementierung den Benutzern nicht schaden.

Trotzdem habe ich die Return-Position impl Trait in meinen eigenen Projekten ausgiebig verwendet, einschließlich meiner Arbeit am Fuchsia-Betriebssystem, und ich glaube, dass Auto-Trait-Leakage hier der richtige Standard ist. Praktisch hätte das Entfernen von Leckagen zur Folge, dass ich zurückgehen und + Send zu praktisch jeder impl Trait -Funktion hinzufügen müsste, die ich je geschrieben habe. Negative Grenzen (die + !Send erfordern) sind für mich eine interessante Idee, aber dann würde ich + !Unpin für fast alle gleichen Funktionen schreiben. Explizitheit ist hilfreich, wenn sie die Entscheidungen der Benutzer informiert oder Code verständlicher macht. In diesem Fall denke ich, dass es beides nicht tun würde.

Send und Sync sind "Kontexte", in denen Benutzer programmieren: Es ist äußerst selten, dass ich eine Anwendung oder Bibliothek schreibe, die sowohl die Typen Send als auch !Send verwendet (insbesondere beim Schreiben von asynchronem Code, der auf einem zentralen Executor ausgeführt werden soll, der entweder multithreaded ist oder nicht). Die Entscheidung, Thread-sicher zu sein oder nicht, ist eine der ersten Entscheidungen, die beim Schreiben einer Anwendung getroffen werden muss, und von da an bedeutet die Entscheidung, Thread-sicher zu sein, dass alle meine Typen Send . Bei Bibliotheken bevorzuge ich fast immer Send Typen, da ihre Nichtverwendung normalerweise bedeutet, dass meine Bibliothek unbrauchbar ist (oder die Erstellung eines dedizierten Threads erfordert), wenn sie in einem Thread-Kontext verwendet wird. Eine unbestrittene parking_lot::Mutex fast identische Leistung hat zu RefCell , wenn auf modernen CPUs verwendet, so dass ich jede Motivation zu Push - Benutzer nicht auf spezialisierte Bibliothek Funktionalität sehe für !Send Nutzungs- Fälle. Aus diesen Gründen ist es meiner Meinung nach nicht wichtig, zwischen Send und !Send Typen auf Funktionssignaturebene unterscheiden zu können, und ich glaube nicht, dass dies alltäglich sein wird für Bibliotheksautoren, versehentlich !Send Typen in impl Trait Typen einzuführen, die zuvor Send . Es stimmt, dass diese Wahl mit Kosten für Lesbarkeit und Klarheit verbunden ist, aber ich glaube, dass sich der Kompromiss für die ergonomischen und benutzerfreundlichen Vorteile lohnt.

Argumentposition impl Trait

Ich habe hier nicht allzu viel zu sagen, außer dass ich jedes Mal, wenn ich nach der Argumentposition impl Trait gegriffen habe, festgestellt habe, dass dies die Lesbarkeit und die allgemeine Annehmlichkeit meiner Funktionssignaturen erheblich verbessert hat. Es ist wahr, dass es keine neue Fähigkeit hinzufügt, die im heutigen Rust nicht möglich ist, aber es ist eine großartige Verbesserung der Lebensqualität für komplizierte Funktionssignaturen, es passt konzeptionell gut zu der Rückgabeposition impl Trait . und es erleichtert OOP-Programmierern den Übergang zu glücklichen Rustaceen. Derzeit gibt es eine Menge Redundanz bei der Einführung eines benannten generischen Typs, nur um eine Grenze bereitzustellen (zB F in fn foo<F>(x: F) where F: FnOnce() vs. fn foo(x: impl FnOnce()) ). Diese Änderung behebt dieses Problem und führt zu Funktionssignaturen, die einfacher zu lesen und zu schreiben sind, und IMO fühlt sich wie eine natürliche Ergänzung zu -> impl Trait .

TL; DR: Ich denke, unsere ursprünglichen Entscheidungen waren richtig, obwohl sie zweifellos mit Kompromissen verbunden sind.
Ich weiß es wirklich zu schätzen, dass jeder seine Meinung sagt und so viel Zeit und Mühe investiert, um sicherzustellen, dass Rust die beste Sprache ist, die es sein kann.

@Centril

In Bezug auf Impl Trait in Argumentposition ist dies falsch, aber nicht so bei -> Impl Trait, da beide existentielle Typen sind.

Ja, das meinte ich.

@aturon

Sätze wie ... untergraben die Diskussion in gutem Glauben

Du hast recht, entschuldige das. Ich glaube, ich habe meinen Standpunkt woanders besser ausgedrückt.

Was nun das Implementierungsdetail einer Funktion sein sollte (wie sie ihre Typparameter deklariert) wird Teil ihrer Schnittstelle, da Sie ihren Typ nicht schreiben können!

Ich verstehe nicht, was du damit meinst. Können Sie erweitern?

Mit Unterstützung für impl Trait in der Argumentposition können Sie diese Funktion auf zwei Arten schreiben:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

Die Wahl der Form bestimmt, ob der API-Consumer überhaupt den Namen einer bestimmten Instanzierung aufschreiben kann (zB um deren Adresse zu übernehmen). Die Variante impl Trait lässt dies nicht zu, und dies kann nicht immer umgangen werden, ohne die Signatur neu zu schreiben, um die Syntax <T> . Außerdem ist der Wechsel zur <T> Syntax eine bahnbrechende Änderung!

Auf die Gefahr hin, weiter zu karikieren, ist die Motivation dafür, dass es einfacher zu lehren, zu lernen und zu verwenden ist. Da die Wahl zwischen den beiden jedoch auch ein wichtiger Teil der Schnittstelle der Funktion ist, genau wie die Typparameterreihenfolge, denke ich, dass dies nicht angemessen berücksichtigt wurde - ich bin nicht wirklich anderer Meinung, dass es einfacher zu verwenden ist oder dass es führt zu angenehmeren Funktionssignaturen.

Ich bin mir nicht sicher, ob unsere anderen "einfachen, aber begrenzten -> komplexen, aber allgemeinen" Änderungen, die durch Erlernbarkeit / Ergonomie motiviert sind, auf diese Weise schnittstellenbrechende Änderungen beinhalten. Entweder ist die komplexe Äquivalent verhält sich die einfache Art und Weise identisch und nur umschalten müssen , wenn Sie bereits die Schnittstelle oder das Verhalten (zB Lebensdauer elision, match Ergonomie, Ändern -> impl Trait ) oder die Änderung ebenso allgemein und soll universell einsetzbar sein (zB Module/Pfade, In-Band-Lebensdauer, dyn Trait ).

Um konkreter zu sein, ich befürchte, dass wir dieses Problem in Bibliotheken bekommen werden, und es wird ungefähr so ​​aussehen wie "jeder muss daran denken, Copy / Clone abzuleiten", aber schlimmer, weil a) das wird eine bahnbrechende Veränderung sein, und b) es wird immer eine Spannung geben, sich zurückzuziehen, insbesondere weil das Feature dafür entwickelt wurde!

@cramertj Was die Redundanz der Funktionssignatur

@rpjohnst

Außerdem ist der Wechsel zur <T> Syntax eine bahnbrechende Änderung!

Nicht unbedingt, mit https://github.com/rust-lang/rfcs/pull/2176 könnten Sie am Ende einen zusätzlichen Typparameter T: Trait hinzufügen und Turbofish würde immer noch funktionieren (es sei denn, Sie beziehen sich auf Bruch durch andere Mittel als Turbofish-Bruch).

Die Variante impl Trait lässt dies nicht zu, und dies kann nicht immer umgangen werden, ohne die Signatur neu zu schreiben, um die Syntax <T> . Außerdem ist der Wechsel zur <T> Syntax eine bahnbrechende Änderung!

Ich denke auch, dass Sie meinen, dass der Wechsel von der <T> Syntax eine bahnbrechende Änderung darstellt (weil Anrufer den Wert von T nicht mehr explizit mit Turbofish angeben können).

UPDATE: Beachten Sie, dass, wenn eine Funktion impl Trait verwendet, wir die Verwendung von Turbofish derzeit überhaupt nicht zulassen – selbst wenn sie einige normale generische Parameter hat.

@nikomatsakis Der Wechsel zur expliziten Syntax kann ebenfalls eine bahnbrechende Änderung sein, wenn die alte Signatur eine Mischung aus expliziten Typparametern und impliziten hatte – jeder, der n Typparameter bereitgestellt hat, muss jetzt n + 1 statt. Dies war einer der Fälle, die der RFC von

UPDATE: Beachten Sie, dass, wenn eine Funktion impl Trait verwendet, wir die Verwendung von Turbofish derzeit überhaupt nicht zulassen – selbst wenn sie einige normale generische Parameter hat.

Dies reduziert technisch die Anzahl der Bruchfälle, erhöht aber andererseits die Anzahl der Fälle, in denen Sie eine bestimmte Instanziierung nicht benennen können. :(

@nikomatsakis

Vielen Dank, dass Sie dieses Anliegen aufrichtig ansprechen.

Ich zögere immer noch, zu sagen, dass Auto Trait Leaking die _die richtige_ Lösung ist, aber ich stimme zu, dass wir erst im Nachhinein wirklich wissen können, was das Beste ist.

Ich hatte hauptsächlich den Anwendungsfall Futures in Betracht gezogen, aber das ist nicht der einzige. Ohne Send/Sync von lokalen Typen durchsickern zu lassen, gibt es keine wirklich gute Geschichte für die Verwendung von impl Trait in vielen verschiedenen Kontexten. Angesichts dessen und angesichts zusätzlicher automatischer Merkmale ist mein Vorschlag nicht wirklich praktikabel.

Ich wollte nicht Sync und Send herausgreifen und _nur_ davon ausgehen, da das ein bisschen willkürlich ist und nur für _einen_ Anwendungsfall am besten geeignet ist. Die Alternative, alle Auto-Eigenschaften anzunehmen, wäre jedoch auch nicht gut. + !Unpin + !... für jeden Typ klingt nicht nach einer praktikablen Lösung.

Wenn wir noch fünf Jahre Sprachdesign hätten, um ein Effektsystem und andere Ideen zu entwickeln, von denen ich jetzt keine Ahnung habe, könnten wir uns etwas Besseres einfallen lassen. Aber im Moment und für Rust scheint es der beste Weg zu sein, 100% "automatische" Auto-Eigenschaften zu haben.

@lfairy

Der Wechsel zur expliziten Syntax kann auch eine entscheidende Änderung sein, wenn die alte Signatur eine Mischung aus expliziten und impliziten Typparametern hatte – jeder, der n Typparameter bereitgestellt hat, muss jetzt stattdessen n + 1 angeben.

Das ist derzeit nicht erlaubt. Wenn Sie impl Trait , erhalten Sie keinen Turbofish für alle Parameter (wie ich bemerkt habe). Dies ist jedoch keine langfristige Lösung, sondern eher ein konservativer Schritt, um Meinungsverschiedenheiten über das weitere Vorgehen auszuweichen, bis wir Zeit haben, ein abgerundetes Design zu entwickeln. (Und, wie @rpjohnst bemerkte, hat es seine eigenen Nachteile .)

Das Design, das ich gerne sehen würde, ist (a) Ja, um @centrils RFC oder etwas Ähnliches zu akzeptieren und (b) zu sagen, dass Sie Turbofish für explizite Parameter verwenden können (aber nicht für impl Trait Typen). Wir haben dies jedoch nicht getan, zum Teil, weil wir uns gefragt haben, ob es möglicherweise eine Geschichte gibt, die die Migration von einem expliziten Parameter zu einem Impl-Merkmal ermöglicht.

@lfairy

Dies war einer der Fälle, die der RFC von

_[Trivia]_ Übrigens war es eigentlich @nikomatsakis, der mich darauf aufmerksam gemacht hat, dass partielle Turbofish die Breaks zwischen <T: Trait> und impl Trait erleichtern könnten ;) Es war kein Ziel des RFC bei alles von Anfang an, aber es war eine schöne Überraschung. 😄.

Sobald wir mehr Vertrauen in Bezug auf Inferenz, Voreinstellungen, benannte Parameter usw. gewonnen haben, können wir hoffentlich auch partielle Turbofish haben, Eventually™.

Die letzte Kommentarfrist ist nun abgeschlossen.

Wenn dies in 1.26 ausgeliefert wird, scheint mir https://github.com/rust-lang/rust/issues/49373 sehr wichtig zu sein, Future und Iterator sind zwei der Hauptverwendungszwecke -Fälle und sie sind beide sehr abhängig von der Kenntnis der zugehörigen Typen.

Habe eine schnelle Suche im Issue Tracker durchgeführt, und #47715 ist ein ICE, der noch behoben werden muss. Können wir das bekommen, bevor es in den Stall geht?

Etwas, das mir heute mit impl Trait begegnet ist:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Anscheinend ist impl Trait mit unimplemented!() kompatibel - ist das ein bekanntes Problem?

ja, siehe #36375 und #44923

Ich habe gerade festgestellt, dass die Annahme 2 von RFC 1951 auf einige meiner geplanten Verwendungen von impl Trait mit asynchronen Blöcken stößt. Insbesondere wenn Sie einen generischen AsRef oder Into Parameter verwenden, um eine ergonomischere API zu erhalten, und diesen dann in einen eigenen Typ umwandeln, bevor Sie einen async Block zurückgeben, erhalten Sie immer noch die zurückgegebene impl Trait Typ, der an alle Lebenszeiten in diesem Parameter gebunden ist, zB

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

Damit erhalten Sie ein error[E0597]: `url` does not live long enough da get die Lebensdauer der temporären Referenz in das zurückgegebene impl Future . Dieses Beispiel ist insofern etwas erfunden, als Sie die URL als Wert an get könnten, aber es wird mit ziemlicher Sicherheit ähnliche Fälle in echtem Code geben.

Soweit ich das beurteilen kann, handelt es sich bei der erwarteten Lösung um abstrakte Typen, insbesondere

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

Durch Hinzufügen der Indirektionsschicht müssen Sie explizit durchgehen, welche generischen Typ- und Lebensdauerparameter für den abstrakten Typ erforderlich sind.

Ich frage mich, ob es möglicherweise eine prägnantere Möglichkeit gibt, dies zu schreiben, oder wird dies nur dazu führen, dass abstrakte Typen für fast jede Funktion verwendet werden und niemals der bloße Rückgabetyp impl Trait ?

Wenn ich also den Kommentar von HttpClient::get , etwa `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Da der RFC explizit angibt, dass impl Trait _alle_ generischen Typparameter erfasst, und es ein Fehler ist, dass Sie einen Typ erfassen dürfen, der eine kürzere Lebensdauer als Ihre explizit deklarierte Lebensdauer enthalten kann).

Daraus ergibt sich, dass der einzige Fix noch darin zu bestehen scheint, einen nominalen abstrakten Typ zu deklarieren, um explizit deklarieren zu können, welche Typparameter erfasst werden.

Eigentlich scheint das eine bahnbrechende Veränderung zu sein. Wenn also in diesem Fall ein Fehler hinzugefügt wird, sollte dies bald erfolgen.

BEARBEITEN: Und wenn ich den Kommentar noch einmal lese, glaube ich nicht, dass das so gemeint ist, also bin ich immer noch verwirrt, ob es einen möglichen Weg gibt, ohne abstrakte Typen zu verwenden oder nicht.

@Nemo157 Ja, das Beheben von #42940 würde Ihr lebenslanges Problem beheben, da Sie angeben können, dass der Rückgabetyp unabhängig von der Lebensdauer von Url so lange leben soll wie die Ausleihe von self. Dies ist definitiv eine Änderung, die wir vornehmen möchten, aber ich glaube, dass dies abwärtskompatibel ist – es erlaubt keine kürzere Lebensdauer des Rückgabetyps, sondern schränkt die Verwendungsmöglichkeiten des Rückgabetyps zu stark ein.

Zum Beispiel die folgenden Fehler mit "der Parameter Iter lebt möglicherweise nicht lange genug":

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Es reicht nicht aus, nur Iter in den Generics für die Funktion zu haben, damit es im Rückgabetyp vorhanden ist, aber derzeit gehen die Aufrufer der Funktion fälschlicherweise davon aus. Dies ist definitiv ein Fehler und sollte behoben werden, aber ich glaube, dass er rückwärtskompatibel behoben werden kann und die Stabilisierung nicht blockieren sollte.

Es scheint, dass #46541 fertig ist. Kann jemand das OP aktualisieren?

Gibt es einen Grund, warum die Syntax abstract type Foo = ...; gegenüber type Foo = impl ...; ? Letzteres habe ich aus Gründen der Syntaxkonsistenz bevorzugt, und ich erinnere mich an eine Diskussion darüber vor einiger Zeit, kann sie aber nicht finden.

Ich mag type Foo = impl ...; oder type Foo: ...; , abstract scheint ein unnötiger Sonderling zu sein.

Wenn ich mich recht erinnere, war eines der Hauptanliegen, dass die Leute gelernt haben, type X = Y wie eine Textsubstitution zu interpretieren ("Ersetzen Sie X gegebenenfalls durch Y "). Dies funktioniert nicht für type X = impl Y .

Ich bevorzuge type X = impl Y selbst, weil meine Intuition ist, dass type wie let funktioniert, aber...

@alexreg Es gibt viele Diskussionen zu dem Thema auf RFC 2071 . TL;DR: type Foo = impl Trait; bricht die Fähigkeit, impl Trait zu entzuckern, in eine "explizitere" Form und bricht die Intuitionen der Leute über Typaliase, die als etwas intelligentere syntaktische Substitution funktionieren.

Ich tippe gerne Foo = impl ...; oder tippe Foo: ...;, abstrakt scheint ein unnötiger Sonderling zu sein

Du solltest an meinem exists type Foo: Trait; Camp teilnehmen :wink:

@cramertj Hmm . Ich habe mich gerade über einiges davon aufgefrischt, und wenn ich ehrlich bin, kann ich nicht sagen, dass ich die Argumentation von @ withoutboats verstehe. Es scheint mir sowohl das intuitivste zu sein (haben Sie ein Gegenbeispiel?) als auch das bisschen über Entzuckern, das ich einfach nicht verstehe. Ich denke, meine Intuition funktioniert wie @lnicola. Ich bin auch der Meinung, dass diese Syntax am besten geeignet ist, um Dinge wie https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 zu tun – ist dies überhaupt in der aktuellen Syntax möglich?

exists type Foo: Trait; ist eine leichte Verbesserung, obwohl ich das Schlüsselwort exists weglassen würde. type Foo: Trait; würde mich nicht genug stören, um mich zu beschweren. 😉 abstract ist einfach überflüssig/skurril, wie @eddyb sagt.

@alexreg

Ist dies in der aktuellen Syntax überhaupt möglich?

Ja, aber es ist viel umständlicher. Dies war mein Hauptgrund dafür, die Syntax = impl Trait (modulo das Schlüsselwort abstract ) zu bevorzugen.

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Bearbeiten: exists type Foo: (Foo1, Foo2); oben hätte type Foo = (Foo1, Foo2); . Entschuldigung für die Verwirrung.

@cramertj Die Syntax scheint nett zu sein. Sollte exists ein geeignetes Schlüsselwort sein?

@cramertj Richtig, ich dachte, Sie = impl Trait zu bevorzugen, denke ich! Ehrlich gesagt, wenn die Leute denken, dass die Intuition über Substitution hier für existenzielle Typen ausreichend bricht (im Vergleich zu einfachen Typaliasen), warum dann nicht der folgende Kompromiss?

exists type Foo = (impl Bar, impl Baz);

(Ehrlich gesagt, würde ich lieber nur das einzige Schlüsselwort type für alles verwenden.)

Ich finde:

exists type Foo: (Foo1, Foo2);

zutiefst seltsam. Die Verwendung von Foo: (Foo1, Foo2) wo die RHS keine Grenze ist, stimmt nicht damit überein, wie Ty: Bound anderer Stelle in der Sprache verwendet wird.

Folgende Formen erscheinen mir gut:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

Ich ziehe es auch vor, hier abstract als Wort zu verwenden.

Ich finde exists type Foo: (Foo1, Foo2); zutiefst seltsam

Das sieht für mich sicherlich nach einem Fehler aus, und ich denke, es sollte type Foo = (Foo1, Foo2); .

Wenn wir hier abstract type vs. exists type Bikeshedding machen, würde ich ersteres definitiv unterstützen. Vor allem, weil "abstrakt" als Adjektiv funktioniert. Ich könnte etwas im Gespräch leicht als "abstrakten Typ" bezeichnen, während es sich seltsam anfühlt zu sagen, dass wir einen "existierten Typ" machen.

Ich würde auch immer noch : Foo + Bar gegenüber : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar oder = (impl Foo, impl Bar bevorzugen. Die Verwendung von + funktioniert gut mit allen anderen Orten, an denen Grenzen sein können, und das Fehlen von = bedeutet wirklich, dass wir nicht den vollständigen Typ schreiben können. Wir erstellen hier keinen Typalias, sondern einen Namen für etwas, von dem wir garantieren, dass es bestimmte Grenzen hat, das wir aber nicht explizit benennen können.


Ich mag auch immer noch den Syntaxvorschlag von https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 von:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Obwohl dies, wie in diesem Thread erwähnt, ein bisschen zu wenig Unterschied und nicht sehr explizit ist.

Ich muss (impl Foo, impl Bar) ganz anders interpretieren als einige von Ihnen... für mich bedeutet dies, dass der Typ ein 2-Tupel einiger existentieller Typen ist und sich völlig von impl Foo + Bar .

@alexreg Wenn das die Absicht von @cramertj wäre, würde ich das mit der Syntax : immer noch sehr seltsam finden:

exists type Foo: (Foo1, Foo2);

scheint immer noch sehr unklar zu sein, was es tut - Grenzen geben normalerweise sowieso kein Tupel möglicher Typen an, und es könnte leicht mit der Bedeutung der Foo: Foo1 + Foo2 Syntax verwechselt werden.

= (impl Foo, impl Bar) ist eine interessante Idee - es wäre interessant, existenzielle Tupel mit Typen zu erstellen, die selbst nicht bekannt sind. Ich glaube jedoch nicht, dass wir diese unterstützen müssen, da wir einfach zwei existenzielle Typen für Impl Foo und impl Bar einführen können und dann einen dritten Typ-Alias ​​für das Tupel.

@daboross Nun, Sie machen einen "existentiellen Typ" , keinen "existierten Typ" ; so wird es in der Typentheorie genannt. Aber ich denke, die Formulierung "es gibt einen Typ Foo, der ..." funktioniert sowohl mit dem mentalen Modell als auch aus einer typentheoretischen Perspektive gut.

Ich glaube jedoch nicht, dass wir diese unterstützen müssen, da wir einfach zwei existenzielle Typen für impl Foo und impl Bar einführen können und dann einen dritten Typalias für das Tupel.

Das scheint unergonomisch... Provisorien sind imo nicht so schön.

@alexreg Hinweis: Ich wollte nicht sagen, dass impl Bar + Baz; dasselbe wie (impl Foo, impl Bar) , letzteres ist offensichtlich das 2-Tupel.

@daboross

Wenn das die Absicht von @cramertj wäre, würde ich das mit der Syntax immer noch sehr seltsam finden:

exists type Foo: (Foo1, Foo2);

scheint immer noch sehr unklar zu sein, was es tut - Grenzen spezifizieren normalerweise sowieso kein Tupel möglicher Typen, und es könnte leicht mit der Bedeutung von Foo verwechselt werden: Foo1 + Foo2-Syntax.

Es ist vielleicht ein wenig unklar (nicht so explizit wie (impl Foo, impl Bar) , was ich intuitiv sofort verstehen würde) – aber ich glaube nicht, dass ich es persönlich mit Foo1 + Foo2 verwechseln würde.

= (impl Foo, impl Bar) ist eine interessante Idee - es wäre interessant, existenzielle Tupel mit Typen zu erstellen, die selbst nicht bekannt sind. Ich glaube jedoch nicht, dass wir diese unterstützen müssen, da wir einfach zwei existenzielle Typen für Impl Foo und Impl Bar einführen können und dann einen dritten Typalias für das Tupel.

Ja, das war ein früher Vorschlag, und ich mag ihn immer noch sehr. Es wurde angemerkt, dass dies mit der aktuellen Syntax trotzdem möglich ist, jedoch 3 Zeilen Code erforderlich sind, was nicht sehr ergonomisch ist. Ich behaupte auch, dass einige Syntax wie ... = (impl Foo, impl Bar) für den Benutzer am klarsten ist, aber ich weiß, dass es hier Streit gibt.

@Centril Ich dachte zuerst nicht, aber es war etwas mehrdeutig, und dann schien @daboross es so zu interpretieren, hah. Wie auch immer, froh, dass wir das geklärt haben.

Hoppla, siehe meine Bearbeitung auf https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); hätte type Foo = (Foo1, Foo2); .

@cramertj Ah, das macht jetzt mehr Sinn. Wie auch immer, denken Sie nicht, dass es am ergonomischsten ist, Folgendes tun zu können? Selbst beim Durchstöbern des anderen Threads habe ich kein wirklich gutes Argument dagegen gesehen.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Ja, ich glaube , dass die meisten ergonomische Syntax ist.

Mit RFC https://github.com/rust-lang/rfcs/pull/2289 würde ich das Snippet von @cramertj so umschreiben:

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

Ich denke jedoch, dass das Nichteinführen von exists bei Typaliasen dazu beitragen würde, die Ausdruckskraft zu erhalten, während die Syntax der Sprache nicht unnötig komplexer wird; Von einem Komplexitätsbudget-POV scheint also impl Iterator besser zu sein als exists . Die letzte Alternative führt jedoch nicht wirklich eine neue Syntax ein und ist auch die kürzeste, während sie klar ist.

Zusammenfassend denke ich, dass beide der folgenden Formen erlaubt sein sollten (da es sowohl unter den impl Trait als auch unter den Grenzen der zugehörigen Typensyntaxen funktioniert, die wir bereits haben):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDIT: Welche Syntax soll verwendet werden? IMO, clippy sollte eindeutig die Type: Bound Syntax bevorzugen, wenn es möglich ist, sie zu verwenden, da sie am ergonomischsten und direktsten ist.

Ich bevorzuge die type Foo: Trait Variante gegenüber der type Foo = impl Trait Variante. Es entspricht der zugehörigen Typsyntax, was gut ist, da es auch ein "Ausgabetyp" des Moduls ist, das ihn enthält.

Die Syntax impl Trait wird bereits sowohl für Eingabe- als auch für Ausgabetypen verwendet, was bedeutet, dass sie den Eindruck von polymorphen Modulen erwecken kann. :(

Wenn impl Trait ausschließlich für Ausgabetypen verwendet würden, dann würde ich vielleicht die Variante type Foo = impl Trait bevorzugen, da die zugehörige Typsyntax eher für Traits (die grob ML-Signaturen entsprechen) ist, während die type Foo = .. Syntax ist eher für konkrete Module gedacht.

@rpjohnst

Ich bevorzuge die type Foo: Trait Variante gegenüber der type Foo = impl Trait Variante.

Ich stimme zu, es sollte nach Möglichkeit verwendet werden; aber was ist mit (impl T, impl U) wo die gebundene Syntax nicht direkt verwendet werden kann? Mir scheint, dass die Einführung temporärer Typaliase die Lesbarkeit beeinträchtigt.

Die bloße Verwendung von type Name: Bound scheint verwirrend zu sein, wenn es innerhalb von impl Blöcken verwendet wird:

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

Sowohl für diese Syntax als auch für den aktuellen (?) Plan des Schlüsselwortpräfixes sind die Kosten für die Einführung temporärer Typaliase, die in impl Blöcken verwendet werden sollen, ebenfalls viel größer. Diese Typaliase müssen jetzt auf Modulebene exportiert werden ( und einem semantisch aussagekräftigen Namen gegeben...), was ein (zumindest für mich) relativ häufiges Muster der Definition von Trait-Implementierungen in privaten Modulen blockiert.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

vs

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 Warum nicht beides zulassen:

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

und:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

Ich sehe nicht, warum es für dieselbe Funktion zwei Syntaxen geben muss. Es wäre immer noch möglich, nur die type Name = impl Bound; Syntax zu verwenden, die explizit Namen für die beiden Teile angibt:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Ich stimme zu, dass (und sollte) es nicht zwei verschiedene Syntaxen geben muss (und sollte). Ich finde type (ohne Präfix-Schlüsselwort) jedoch überhaupt nicht verwirrend, muss ich sagen.

@rpjohnst Was zum Teufel ist ein polymorphes Modul? :-) Wie auch immer, ich sehe nicht, warum wir die Syntax nach zugeordneten Typdefinitionen modellieren sollten, die einem Typ Eigenschaftsgrenzen auferlegen . Das hat nichts mit Grenzen zu tun.

@alexreg Ein polymorphes Modul hat Typparameter, genauso wie fn foo(x: impl Trait) . Es ist nicht etwas, das existiert, und deshalb möchte ich nicht, dass die Leute denken, dass es existiert.

abstract type ( edit: das Feature zu benennen, nicht die Verwendung des Schlüsselworts vorzuschlagen) hat alles mit Grenzen zu tun! Grenzen sind das einzige, was Sie über den Typ wissen. Der einzige Unterschied zwischen ihnen und den zugehörigen Typen besteht darin, dass sie abgeleitet werden, da sie normalerweise nicht benennbar sind.

@Nemo157 die Foo: Bar Syntax ist in anderen Kontexten bereits bekannter (Grenzen für zugeordnete Typen und Typparameter) und ist ergonomischer und (IMO) klarer, wenn sie ohne Einführung von Provisorien verwendet werden kann.

Schreiben:

type IterDisplay: Iterator<Item: Display>;

scheint viel direkter bzgl. was ich sagen will, im vergleich zu

type IterDisplay = impl Iterator<Item = impl Display>;

Ich denke, dies ist nur eine konsequente Anwendung der Syntax, die wir bereits haben; also nicht wirklich neu.

EDIT2: Die erste Syntax ist auch so, wie ich sie in Rustdoc gerendert haben möchte.

Der Übergang von einem Merkmal, das etwas für einen zugeordneten Typ erfordert, zu einem Impl wird ebenfalls sehr einfach:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

Die impl Bar Syntax scheint besser zu sein, wenn Sie sonst Temporäre einführen müssten, aber sie wendet die Syntax auch durchgehend konsequent an.

In der Lage zu sein, beide Syntaxen zu verwenden, würde sich nicht wirklich von der Möglichkeit unterscheiden, impl Trait in der Argumentposition zu verwenden und einen expliziten Typparameter T: Trait , der dann von einem Argument verwendet wird.

EDIT1: Tatsächlich wäre es eine spezielle Groß-/Kleinschreibung, nur eine Syntax zu haben, nicht umgekehrt.

@rpjohnst Ich muss da explizit mit Grenzen zu tun hat.

Wie auch immer, ich bin nicht gegen die type Foo: Bar; Syntax, aber um Himmels willen, lassen Sie uns das abstract Schlüsselwort los. type allein ist unter allen Umständen ziemlich klar.

Persönlich finde ich, dass die Verwendung von = und impl ein schöner visueller Hinweis darauf ist, dass Schlussfolgerungen stattfinden. Es macht es auch einfacher, diese Stellen beim Überfliegen einer größeren Datei zu erkennen.

Angenommen, ich sehe type Iter: Iterator<Item = Foo> ich Foo und herausfinden, ob es ein Typ oder eine Eigenschaft ist, bevor ich weiß, was los ist.

Und schließlich denke ich, dass der visuelle Hinweis auf Inferenzpunkte auch beim Debuggen von Inferenzfehlern und der Interpretation von Inferenzfehlermeldungen hilft.

Ich denke also, dass die Variante = / impl etwas mehr Papierschnitte löst.

@phaylon

Wenn ich außerdem den Typ Iter: Iterator<Item = Foo> sehe, muss ich Foo und herausfinden, ob es ein Typ oder eine Eigenschaft ist, bevor ich weiß, was los ist.

Das verstehe ich nicht; Item = Foo sollte heutzutage immer ein Typ sein, wenn man bedenkt, dass dyn Foo stabil ist (und das bloße Merkmal ausläuft...)?

@Centril

Das verstehe ich nicht; Item = Foo sollte heutzutage immer ein Typ sein, wenn man bedenkt, dass Dyn Foo stabil ist (und das bloße Merkmal ausläuft...)?

Ja, aber in der vorgeschlagenen Variante impl weniger könnte es ein abgeleiteter Typ mit einer Grenze oder ein konkreter Typ sein. ZB Iterator<Item = String> vs Iterator<Item = Display> . Ich muss die Merkmale kennen, um zu wissen, ob eine Schlussfolgerung stattfindet.

Bearbeiten: Ah, habe nicht bemerkt, dass : . Etwas, was ich mit leicht zu übersehen meine :) Aber du hast Recht, dass sie anders sind.

Bearbeiten 2: Ich denke, dieses Problem würde außerhalb der zugeordneten Typen bestehen. Angesichts von type Foo: (Bar, Baz) müssten Sie Bar und Baz kennen, um zu wissen, wo Schlussfolgerungen auftreten.

@Centril

EDIT1: Tatsächlich wäre es eine spezielle Groß-/Kleinschreibung, nur eine Syntax zu haben, nicht umgekehrt.

Derzeit gibt es nur eine Möglichkeit, _existentielle_ Typen zu deklarieren, -> impl Trait . Es gibt zwei Möglichkeiten, _universelle_ Typen zu deklarieren ( T: Trait und : impl Trait in einer Argumentliste).

Wenn wir polymorphe Module hätten, die universelle Typen aufnehmen, könnte ich einige Argumente dafür sehen, aber ich glaube, dass die derzeitige Verwendung von type Name = Type; sowohl in Modulen als auch in Merkmalsdefinitionen ein Ausgabetypparameter ist, der existenziell sein sollte Typ.


@phaylon

Ja, aber in der vorgeschlagenen Variante impl weniger könnte es ein abgeleiteter Typ mit einer Grenze oder ein konkreter Typ sein. ZB Iterator<Item = String> vs Iterator<Item = Display> . Ich muss die Merkmale kennen, um zu wissen, ob eine Schlussfolgerung stattfindet.

Ich glaube, die impl weniger Variante verwendet : Bound in allen Fällen für existenzielle Typen, also könnten Sie Iterator<Item = String> oder Iterator<Item: Display> als Merkmalsgrenzen haben, aber Iterator<Item = Display> wäre eine ungültige Deklaration.

@Nemo157
Du hast recht bezüglich des dazugehörigen Setzkastens, mein da schlecht. Aber (wie in meiner Bearbeitung erwähnt) denke ich, dass es immer noch ein Problem mit type Foo: (A, B) . Denn entweder A oder B könnte ein Typ oder eine Eigenschaft sein.

Ich glaube, das ist auch ein guter Grund, sich für = . Das : sagt Ihnen nur, dass einige Dinge abgeleitet werden, aber nicht welche. type Foo = (A, impl B) erscheint mir klarer.

Ich gehe auch davon aus, dass das Lesen und Bereitstellen von Code-Snippets mit impl einfacher ist, da zusätzlicher Kontext darüber, was eine Eigenschaft ist und was nicht, nie bereitgestellt werden muss.

Bearbeiten: Einige Credits: Mein Argument ist im Wesentlichen das gleiche wie das von @alexreg hier , ich wollte nur erläutern , warum ich denke, dass impl vorzuziehen ist.

Es gibt derzeit nur eine Möglichkeit, existentielle Typen zu deklarieren, -> impl Trait . Es gibt zwei Möglichkeiten, universelle Typen zu deklarieren ( T: Trait und : impl Trait in einer Argumentliste).

Das ist, was ich sage: PI Warum sollte die universelle Quantifizierung zwei Möglichkeiten haben, aber nur eine existenzielle (ohne Berücksichtigung von dyn Trait ) an anderen Stellen ?

Es scheint mir ebenso wahrscheinlich, dass ein Benutzer type Foo: Bound; und type Foo = impl Bound; schreiben würde, nachdem er verschiedene Teile der Sprache gelernt hat, und ich kann nicht sagen, dass eine Syntax in allen Fällen deutlich besser ist; Mir ist klar, dass eine Syntax für manche Dinge besser ist und eine andere für verschiedene Dinge.

@phaylon

Ich glaube, das ist auch ein guter Grund für =. Das : sagt Ihnen nur, dass einige Dinge abgeleitet werden, aber nicht welche. type Foo = (A, Impl B) erscheint mir klarer.

Ja, das ist wahrscheinlich ein weiterer guter Grund. Es braucht wirklich einiges Auspacken, um herauszufinden, worüber existentiell quantifiziert wird – von Definition zu Definition springen.

Eine andere Sache ist: Würde man unter dieser Syntax überhaupt : innerhalb einer zugeordneten Typbindung zulassen? Es scheint mir ein seltsamer Sonderfall zu sein, da existentielle Typen in dieser vorgeschlagenen Syntax nicht anders zusammengesetzt/kombiniert werden können. Ich könnte mir vorstellen, dass der folgende Ansatz der konsistenteste Ansatz ist, der diese Syntax verwendet:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Mit der von mir (und einigen anderen hier) bevorzugten Syntax könnten wir das alles in einer einzigen Zeile schreiben, und außerdem ist sofort klar, wo die Quantifizierung stattfindet!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Unabhängig vom oben genannten: Wann spielen wir, um let x: impl Trait in der Nacht zu implementieren? Diese Funktion vermisse ich schon seit einiger Zeit.

@alexreg

Eine andere Sache ist: Würde man unter dieser Syntax überhaupt : innerhalb einer zugeordneten Typbindung zulassen?

Ja, warum nicht; Dies wäre ein natürlicher Effekt von rust-lang/rfcs#2289 + type Foo: Bound .

Sie könnten auch tun:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Ich halte es für eine schlechte Idee, zwei alternative Syntaxen zuzulassen. Riecht nach "Wir konnten uns nicht entscheiden, also unterstützen wir einfach beide"-Syndrom. Code zu sehen, der sie kombiniert und zusammenbringt, wird ein echter Schandfleck sein!

@Centril Ich bin ein bisschen mit impl Iterator<Item = impl Baz> schreiben. Nett und explizit.

@alexreg Das ist fair;

Aber (leider) haben wir (je nach POV) bereits die "Zwei alternative Syntaxen zulassen" mit impl Trait in der Argumentposition gestartet, sodass wir sowohl Foo: Bar als auch impl Bar funktioniert, um dasselbe zu bedeuten;

Es dient der universellen Quantifizierung, aber die Notation impl Trait kümmert sich nicht wirklich darum, auf welcher Seite der Dualität sie steht; schließlich sind wir nicht mit any Trait und some Trait gegangen.

Angesichts der Tatsache, dass wir bereits die Wahl getroffen haben "Wir konnten uns nicht entscheiden" und "die Seite der Dualität ist syntaktisch egal" scheint es mir konsequent , "wir können uns nicht entscheiden" überall anzuwenden, damit die Benutzer es nicht verstehen in "aber ich könnte es so drüben schreiben, warum nicht hier?" ;)


PS:

Betreff. impl Iterator<Item = impl Baz> es funktioniert nicht als Grenze in einer where-Klausel; Sie müssten es also wie Iter: Iterator<Item = impl Baz> mischen. Du müsstest zulassen: Iter = impl Iterator<Item = impl Baz> damit es einheitlich funktioniert (vielleicht sollten wir das?).

Die Verwendung von : Bound ist auch anstelle von = impl Bound ist ebenfalls explizit, nur kürzer ^,-
Ich denke, der Unterschied im Abstand zwischen X = Ty und X: Ty macht die Syntax lesbar.

Nachdem wir meinen eigenen Rat ignoriert haben, wollen wir dieses Gespräch beim RFC fortsetzen ;)

Aber (un)glücklicherweise (abhängig von Ihrem POV) haben wir bereits die "Zwei alternative Syntaxen zulassen" mit impl Trait in der Argumentposition gestartet, so dass wir beide haben, dass Foo: Bar und Impl Bar dasselbe bedeuten;

Das haben wir getan, aber ich glaube, die Wahl wurde eher unter dem Gesichtspunkt der Symmetrie/Konsistenz getroffen. Generisch typisierte Argumente sind grundsätzlich mächtiger als universell typisierte ( impl Trait ) Argumente. Aber wir haben impl Trait in der Rückgabeposition eingeführt, es war sinnvoll, es in der Argumentposition einzuführen.

Angesichts der Tatsache, dass wir bereits die Wahl getroffen haben "wir konnten uns nicht entscheiden" und "die Seite der Dualität spielt syntaktisch keine Rolle", scheint es mir konsequent, "wir können uns nicht entscheiden" überall anzuwenden, damit die Benutzer es nicht verstehen in "aber ich könnte es so drüben schreiben, warum nicht hier?" ;)

Ich bin mir nicht sicher, ob wir jetzt die Arme heben und sagen sollten: "Lass uns alles umsetzen". Es gibt hier kein so klares Argument bezüglich des Gewinns.

PS:

Betreff. impl Iterator<Item = impl Baz> es funktioniert nicht als Grenze in einer where-Klausel; Sie müssten es also wie Iter: Iterator<Item = impl Baz> mischen. Du müsstest zulassen: Iter = impl Iterator<Item = impl Baz> damit es einheitlich funktioniert (vielleicht sollten wir das?).

Ich würde sagen, wir unterstützen entweder nur where Iter: Iterator<Item = T>, T: Baz (wie wir es jetzt getan haben) oder gehen den ganzen Weg mit Iter = impl Iterator<Item = impl Baz> (wie Sie vorgeschlagen haben). Nur das halbe Haus zuzulassen, scheint ein bisschen ein Cop-out.

Die Verwendung von : Bound ist auch anstelle von = impl Gebunden ist auch explizit, nur kürzer ^,-
Ich denke, der Unterschied im Abstand zwischen X = Ty und X: Ty macht die Syntax lesbar.

Es ist lesbar, aber ich glaube nicht, dass es annähernd so klar / explizit ist, dass ein existenzieller Typ verwendet wird. Dies wird noch verschlimmert, wenn die Definition aufgrund der Beschränkung dieser Syntax auf mehrere Zeilen aufgeteilt werden muss.

Nachdem wir meinen eigenen Rat ignoriert haben, wollen wir dieses Gespräch beim RFC fortsetzen ;)

Warte, du meinst deinen RFC? Ich denke, es ist sowohl für dieses als auch für dieses relevant, soweit ich das beurteilen kann. :-)

Warte, du meinst deinen RFC? Ich denke, es ist sowohl für dieses als auch für dieses relevant, soweit ich das beurteilen kann. :-)

OK; Lassen Sie uns dann hier fortfahren;

Das haben wir getan, aber ich glaube, die Wahl wurde eher unter dem Gesichtspunkt der Symmetrie/Konsistenz getroffen. Generisch typisierte Argumente sind grundsätzlich mächtiger als universell typisierte ( impl Trait ) Argumente. Aber wir haben impl Trait in der Rückgabeposition eingeführt, es war sinnvoll, es in der Argumentposition einzuführen.

Mein ganzer Punkt ist über Konsistenz und Symmetrie. =P
Wenn Sie impl Trait sowohl für die existentielle als auch für die universelle Quantifizierung schreiben dürfen, macht es für mich Sinn, dass Sie Type: Trait sowohl für die universelle als auch für die existentielle Quantifizierung verwenden dürfen.

In Bezug auf die Ausdruckskraft ist ersteres mächtiger als letzteres, wie Sie sagen, aber das muss nicht unbedingt der Fall sein; Sie könnten genauso mächtig sein, wenn wir wollten, dass sie AFAIK sind (obwohl ich absolut nicht sage, dass wir das tun sollten..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

Ich bin mir nicht sicher, ob wir jetzt die Arme heben und sagen sollten: "Lass uns alles umsetzen". Es gibt hier kein so klares Argument bezüglich des Gewinns.

Mein Argument ist, dass das Überraschen von Benutzern mit "Diese Syntax ist woanders verwendbar und ihre Bedeutung ist hier klar, aber Sie können sie an dieser Stelle nicht schreiben" mehr kostet, als zwei Möglichkeiten zu haben (mit denen Sie sich sowieso vertraut machen müssen) ). Ähnliches haben wir mit https://github.com/rust-lang/rfcs/pull/2300 (merged), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https . gemacht ://github.com/rust-lang/rfcs/pull/2175 (merged), wo wir Konsistenzlöcher füllen, obwohl es vorher möglich war, anders zu schreiben.

Es ist lesbar, aber ich glaube nicht, dass es annähernd so klar / explizit ist, dass ein existenzieller Typ verwendet wird.

Lesbarkeit ist meiner Meinung nach ausreichend; Ich glaube nicht, dass Rust "vor allem explizit" zuschreibt und übermäßig ausführlich ist (was meiner Meinung nach die Syntax ist, wenn sie zu viel verwendet wird), kostet auch dadurch, dass sie von der Verwendung abhält.
(Wenn Sie möchten, dass etwas häufig verwendet wird, geben Sie ihm eine knappere Syntax ... vgl. ? als Bestechung gegen .unwrap() ).

Dies wird noch verschlimmert, wenn die Definition aufgrund der Beschränkung dieser Syntax auf mehrere Zeilen aufgeteilt werden muss.

Das verstehe ich nicht; Es scheint mir, dass Assoc = impl Trait Zeilenaufteilungen noch mehr verursachen sollte als Assoc: Trait einfach weil ersteres länger ist.

Ich würde sagen, wir unterstützen entweder nur where Iter: Iterator<Item = T>, T: Baz (wie wir es jetzt getan haben) oder gehen den ganzen Weg mit Iter = impl Iterator<Item = impl Baz> (wie Sie vorgeschlagen haben).
Nur das halbe Haus zuzulassen, scheint ein bisschen ein Cop-out.

Genau!, lass uns nicht auf halbem Weg nach Hause gehen und where Iter: Iterator<Item: Baz> implementieren ;)

@Centril Okay, Sie haben mich überzeugt, hauptsächlich aufgrund des Symmetrie- / Konsistenzarguments. 😉 Die Ergonomie beider Formen hilft aber auch. Starke Flusen sind für diese Funktion jedoch in Kürze ein Muss.

Werde dies morgen mit meiner vollständigen Antwort bearbeiten.

Bearbeiten

Wie @Centril darauf hinweist, unterstützen wir bereits universelle Typen mit der : Trait (gebundenen) Syntax. z.B

fn foo<T: Trait>(x: T) { ... }

neben "richtigen" oder "verdinglichten" Universaltypen, zB

fn foo(x: impl Trait) { ... }

Natürlich ist ersteres mächtiger als letzteres, aber letzteres ist expliziter (und wohl lesbarer), wenn es nur darum geht. Tatsächlich bin ich der festen Überzeugung, dass wir nach Möglichkeit einen Compiler-Lint zugunsten der letzteren Form haben sollten.

Jetzt haben wir auch bereits impl Trait in der Rückgabeposition der Funktion, was einen existenziellen Typ darstellt. Die zugehörigen Merkmalstypen sind existenziell und verwenden bereits die : Trait Syntax.

Angesichts der Existenz der gegenwärtigen echten und gebundenen Formen universeller Typen in Rust und ebenso der Existenz echter und gebundener Formen für existenzielle Typen (letztere derzeit nur innerhalb von Merkmalen), glaube ich fest daran, dass wir erweitert Unterstützung für die richtigen und gebundene Formen der Existenzart außerhalb von Merkmalen. Das heißt, wir sollten Folgendes sowohl allgemein als auch für zugehörige Typen unterstützen .

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

Ich folge dem auch in diesem Kommentar vorgeschlagenen Linting-Verhalten des Compilers, das die Variation des Ausdrucks gängiger existentieller Typen in freier Wildbahn stark reduzieren sollte.

Ich glaube immer noch, dass es ein Fehler war, universelle und exxisistierende Quantifizierung unter einem Schlüsselwort zusammenzufassen, und daher funktioniert das Konsistenzargument für mich nicht. Der einzige Grund, warum ein einzelnes Schlüsselwort in Funktionssignaturen funktioniert, besteht darin, dass der Kontext Sie zwangsläufig dazu zwingt, an jeder Position nur eine Form der Quantifizierung zu verwenden. Es gibt potenzielle Zucker, die ich als etwas sehen könnte, bei dem Sie nicht die gleichen Einschränkungen haben

struct Foo {
    pub foo: impl Display,
}

Ist das eine Abkürzung für existenzielle oder universelle Quantifizierung? Aufgrund der Intuition, die sich aus der Verwendung von impl Trait in Funktionssignaturen ergibt, sehe ich nicht, wie Sie sich entscheiden könnten. Wenn Sie tatsächlich versuchen, es als beides zu verwenden, werden Sie schnell feststellen, dass eine anonyme universelle Quantifizierung an dieser Stelle nutzlos ist, es also eine existenzielle Quantifizierung sein muss, aber das scheint nicht mit impl Trait in Funktionsargumenten vereinbar zu sein.

Dies sind zwei grundlegend unterschiedliche Operationen, ja, beide verwenden Merkmalsgrenzen, aber ich sehe keinen Grund dafür, dass zwei Möglichkeiten zur Deklaration eines existenziellen Typs die Verwirrung für Neuankömmlinge verringern würden. Wenn der Versuch, type Name: Trait , für Neulinge wahrscheinlich ist, könnte dies durch einen Fussel gelöst werden:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

Und ich habe gerade eine alternative Formulierung Ihres Arguments entwickelt, für die ich viel zugänglicher wäre. Es muss jedoch warten, bis ich an einem echten Computer bin, um den RFC erneut zu lesen und darüber zu posten.

Ich habe das Gefühl, dass ich noch nicht genug Erfahrung mit Rust habe, um RFCs zu kommentieren. Ich bin jedoch daran interessiert zu sehen, wie dieses Feature in das nächtliche und stabile Rust integriert wird, um es mit Rust libp2p zum Sharding-Implementierung von

Ich habe das Gefühl, dass ich noch nicht genug Erfahrung mit Rust habe, um RFCs zu kommentieren. Ich bin jedoch daran interessiert zu sehen, wie dieses Feature in das nächtliche und stabile Rust integriert wird, um es mit Rust libp2p zum Sharding-Implementierung von

Ich glaube immer noch, dass es ein Fehler war, universelle und existenzielle Quantifizierung unter einem Stichwort zusammenzufassen, und so funktioniert das Konsistenzargument für mich nicht.

Grundsätzlich, unabhängig von diesem Merkmal, halte ich diese Argumentation für problematisch.

Ich glaube, dass wir das Sprachdesign so angehen sollten, wie eine Sprache ist, und nicht so, wie wir es uns im Rahmen einer alternativen Entwicklung der Geschichte impl Trait als universelle Quantifizierung in Argumentposition ist stabilisiert, weshalb man sie nicht wegwünschen kann. Auch wenn Sie glauben, dass X, Y und Z Fehler waren (und ich konnte viele Dinge finden, die ich persönlich für Fehler in Rusts Design halte, aber ich akzeptiere und gehe davon aus...), wir müssen jetzt damit leben, und ich denke darüber, wie wir angesichts der neuen Funktion alles zusammenpassen können (die Dinge konsistent machen).

In der Diskussion denke ich, dass das gesamte Korpus von RFCs und die Sprache so wie sie ist nicht als Axiome und dann als starke Argumente angesehen werden sollten.


Sie könnten den Fall machen (aber ich würde nicht), dass:

struct Foo {
    pub foo: impl Display,
}

ist semantisch äquivalent zu:

struct Foo<T: Display> {
    pub foo: T,
}

unter der Funktion-Argument-Begründung.

Im Grunde müssen Sie sich bei impl Trait überlegen: "Ist das ein Rückgabetyp oder ein Argument?" , was schwierig sein kann.


Wenn der Versuch, type Name: Trait , für Neulinge wahrscheinlich ist, könnte dies durch einen Fussel gelöst werden:

Ich würde auch fusseln, aber in die andere Richtung; Ich denke, dass die folgenden Wege idiomatisch sein sollten:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Ok, alternative Formulierung, von der ich glaube, dass RFC 2071 darauf hinweist und in der Ausgabe möglicherweise diskutiert wurde, aber nie explizit angegeben wurde:

Es gibt nur _einen Weg_, existentiell quantifizierte Typen zu deklarieren: existential type Name: Bound; (mit existential da dies im RFC angegeben ist, bin ich nicht ganz dagegen, das Schlüsselwort unter diese Formulierung fallen zu lassen).

Es gibt zusätzlich Zucker, um im aktuellen Gültigkeitsbereich einen unbenannten existentiell quantifizierten Typ implizit zu deklarieren: impl Bound (vorerst ignoriert der universelle Quantifizierungszucker in Funktionsargumenten).

Die aktuelle Verwendung des Rückgabetyps ist also eine einfache Entzuckerung:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

Die Erweiterung auf const , static und let ist ähnlich trivial.

Die einzige Erweiterung, die im RFC nicht erwähnt wird, ist: Unterstützung dieses Zuckers in der type Alias = Concrete; Syntax, also wenn Sie schreiben

type Foo = impl Iterator<Item = impl Display>;

das ist eigentlich zucker für

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

die dann auf der transparenten Natur von Typaliasen beruht, damit das aktuelle Modul Foo und feststellen kann, dass es sich auf einen existenziellen Typ bezieht.

Tatsächlich bin ich der festen Überzeugung, dass wir nach Möglichkeit einen Compiler-Lint zugunsten der letzteren Form haben sollten.

Ich stimme hauptsächlich dem Kommentar von arg: impl Trait , hauptsächlich aufgrund des Risikos, schwerwiegende Änderungen in Bibliotheken zu fördern, da impl Trait nicht funktioniert mit Turbofish (im Moment, und Sie benötigen teilweise Turbofish, damit es gut funktioniert). Daher fühlt sich das Linting in Clippy weniger einfach an als im Fall von Typaliasen (wo es keinen Turbofish gibt, der Probleme verursacht).

Ich stimme hauptsächlich dem Kommentar von

@Centril hat dies gerade im IRC

Also... wir haben jetzt ziemlich viel über die Syntax für benannte existentielle Typen diskutiert. Sollen wir versuchen, zu einem Ergebnis zu kommen und es in den RFC/PR-Beitrag zu schreiben, damit jemand mit der eigentlichen Umsetzung beginnen kann? :-)

Ich persönlich würde es vorziehen, wenn wir einmal Existenziale genannt haben, einen Fussel (falls vorhanden) weg von jeder Verwendung von impl Trait irgendwo .

@rpjohnst Nun, Sie stimmen mir und @Centril in Bezug auf benannte Existenzialien sicherlich zu ... Es hängt davon ab, ob man in diesem Zusammenhang Einfachheit oder Allgemeinheit bevorzugen möchte.

Ist der RFC auf impl Trait in Argumentposition aktuell? Wenn ja, ist es sicher zu sagen, dass seine Semantik _universal_ ist? Wenn ja: Ich möchte weinen. Tief.

@phaazon : Rust 1.26 Versionshinweise für impl Trait :

Randnotiz für Sie Typ-Theoretiker da draußen: Dies ist kein existenzielles, immer noch ein universelles. Mit anderen Worten, Impl Trait ist in einer Eingabeposition universell, aber in einer Ausgabeposition existenziell.

Um nur meine Meinung dazu auszudrücken:

  • Wir hatten bereits eine Syntax für Typvariablen und tatsächlich gibt es einige Verwendungsmöglichkeiten für beliebige Typvariablen (dh es kommt sehr oft vor, dass Sie die Typvariable an mehreren Stellen verwenden möchten, anstatt sie nur an einer einzigen Stelle abzulegen).
  • Kovariante Existenziale würden uns die Türen zu Rang-n-Funktionen öffnen, etwas, das derzeit ohne ein Merkmal schwer zu tun ist (siehe dieses ) und ein Feature, das Rust wirklich fehlt.
  • impl Trait leicht als „vom Angerufener ausgewählter Typ“ bezeichnet werden, weil… weil es derzeit das einzige Sprachkonstrukt ist, das uns dies ermöglicht! Die Auswahl des Typs durch den Aufrufer ist bereits über mehrere Konstrukte möglich.

Ich finde die aktuelle Entscheidung impl Trait in der Argumentposition wirklich schade. :schreien:

Ich finde das implizite Merkmal in der aktuellen Entscheidung der Argumentationsposition wirklich schade. 😢.

Obwohl ich darüber ein bisschen hin- und hergerissen bin, denke ich sicherlich, dass die Zeit jetzt besser für die Implementierung von let x: impl Trait !

Kovariante Existenzialien würden uns die Türen zu Rang-n-Funktionen öffnen

Wir haben bereits eine Syntax dafür ( fn foo(f: impl for<T: Trait> Fn(T)) ), (auch bekannt als "Typ HRTB"), aber sie ist noch nicht implementiert. fn foo(f: impl Fn(impl Trait)) erzeugt den Fehler "verschachteltes impl Trait ist nicht zulässig", und ich gehe davon aus, dass es die höherrangige Version bedeutet, wenn wir den Typ HRTB erhalten.

Dies ist ähnlich wie Fn(&'_ T) for<'a> Fn(&'a T) , daher erwarte ich nicht, dass es umstritten ist.

Wenn man sich den aktuellen Entwurf ansieht, ist impl Trait in Argumentposition ein _universelles_, aber Sie sagen, dass impl for<_> Trait daraus ein _existentielles_ macht?! Wie verrückt ist das?

Warum dachten wir, wir müssten _noch einen anderen Weg_ einführen, um ein _Universal_ zu konstruieren? Ich meine:

fn foo(x: impl MyTrait)

Ist nur interessant, weil die anonyme Typvariable nur einmal im Typ vorkommt . Wenn Sie denselben Typ zurückgeben müssen:

fn foo(x: impl Trait) -> impl Trait

Wird offensichtlich nicht funktionieren. Wir fordern die Leute auf, von einer allgemeineren Sprache zu einer restriktiveren zu wechseln, anstatt nur eine zu lernen und sie überall zu verwenden. Das ist mein ganzes Gerede. Das Hinzufügen einer Funktion ohne Mehrwert – die Lernannahme, die ich im RFC gelesen habe, ist ein seltsames Argument, bei dem wir anstelle der Neuankömmlinge denken – sie müssen immer noch Dinge lernen, also warum machen wir die Syntax mehrdeutig bis so ziemlich? alle, um hier die Lernkurve zu senken? Wenn sich die Leute an universell vs. existentiell gewöhnen (und mit den richtigen Worten ist das Prinzip sehr einfach), werden die Leute anfangen zu denken, warum wir die gleichen Schlüsselwörter / Muster haben, um beides auszudrücken und auch where und In-Place-Template-Parameter.

Argh, ich schätze, all das wurde bereits akzeptiert und ich schimpfe umsonst. Ich finde es einfach nur schade. Ich bin mir ziemlich sicher, dass ich nicht der einzige bin, der von der RFC-Entscheidung enttäuscht ist.

(Es macht wahrscheinlich nicht viel Sinn, darüber weiter zu diskutieren, nachdem das Feature stabilisiert wurde, aber siehe hier für ein überzeugendes Argument (dem ich zustimme), warum impl Trait in der Argumentposition mit der Semantik, die es tut, vernünftig ist und kohärent. Tl; dr es ist aus dem gleichen Grund, warum fn foo(arg: Box<Trait>) ungefähr genauso funktioniert wie fn foo<T: Trait>(arg: Box<T>) , obwohl dyn Trait existentiell ist; jetzt ersetzen Sie dyn mit impl .)

Wenn man sich den aktuellen Entwurf ansieht, ist impl Trait in der Argumentposition ein universelles Argument, aber Sie sagen, dass impl for<_> Trait daraus ein Existenzielles macht?!

Nein, sie sind beide universell. Ich sage, dass höherrangige Verwendungen so aussehen würden:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

was gleichzeitig mit dem Hinzufügen (dh ohne Änderungen an impl Trait ) geschrieben werden könnte:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

Das ist universelles impl Trait , nur dass das Trait ein HRTB ist (ähnlich wie impl for<'a> Fn(&'a T) ).
Wenn wir entscheiden (was wahrscheinlich wahrscheinlich ist), dass impl Trait innerhalb von Fn(...) Argumenten ebenfalls universell ist, könnten Sie Folgendes schreiben, um den gleichen Effekt zu erzielen:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

Ich dachte, Sie meinen das mit "höherrangig", wenn nicht, lassen Sie es mich bitte wissen.

Eine noch interessantere Entscheidung könnte sein, die gleiche Behandlung in existenzieller Position anzuwenden, dh dies zuzulassen (was bedeuten würde, " eine Schließung zurückzugeben, die jede andere Schließung erfordert"):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

so geschrieben werden:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Das wäre ein existentielles impl Trait das ein universelles impl Trait (an das Existenzielle gebunden, anstatt an die einschließende Funktion).

@eddyb Wäre es nicht sinnvoller, zwei separate Schlüsselwörter für die existenzielle und universelle Quantifizierung im Allgemeinen zu haben, für Konsistenz und um Neulinge nicht zu verwirren?
Wäre das Stichwort existenzielle Quantifizierung nicht auch für existenzielle Typen wiederverwendbar?
Warum verwenden wir impl für existentielle ( und universelle) Quantifizierung, aber existential für existentielle Typen?

Ich möchte drei Punkte ansprechen:

  • Es lohnt sich nicht, darüber zu diskutieren, ob impl Trait existenziell oder universell ist. Die meisten Programmierer da draußen haben wahrscheinlich nicht genug Handbücher zur Typtheorie gelesen. Die Frage sollte sein, ob es den Leuten gefällt oder ob sie es verwirrend finden. Um diese Frage zu beantworten, gibt es hier in diesem Thread, auf reddit oder im Forum irgendeine Form von Feedback. Wenn etwas weiter erklärt werden muss, besteht ein Lackmustest auf intuitive oder nicht überraschende Funktionen. Wir sollten uns also ansehen, wie viele Leute sie haben und wie verwirrt sie sind und ob es mehr Fragen gibt als bei anderen Funktionen. Es ist in der Tat traurig, dass dieses Feedback nach der Stabilisierung eingeht, und es sollte etwas gegen dieses Phänomen unternommen werden, aber es ist für eine separate Diskussion.
  • Technisch gesehen gäbe es in diesem Fall auch nach der Stabilisierung eine Möglichkeit, das Feature loszuwerden (wobei die Entscheidung beiseite gelegt werden sollte). Es wäre möglich, gegen das Schreiben von Funktionen zu protestieren, die dies verwenden, und die Fähigkeit in der nächsten Ausgabe zu entfernen (während die Fähigkeit erhalten bleibt, sie aufzurufen, wenn sie aus Kisten anderer Editionen stammen). Das würde den Roststabilitätsgarantien genügen.
  • Nein, das Hinzufügen von zwei weiteren Schlüsselwörtern, um existenzielle und universelle Typen zu spezifizieren, würde die Verwirrung nicht verbessern, es würde die Dinge nur noch schlimmer machen.

Es ist in der Tat traurig, dass dieses Feedback nach der Stabilisierung eingeht und es sollte etwas gegen dieses Phänomen unternommen werden

Es gab Einwände gegen impl Trait in der Argumentposition, solange es eine Idee war. Feedback wie dieses _ist nicht neu_, es wurde sogar in den entsprechenden RFC-Threads heftig diskutiert. Es gab viele Diskussionen nicht nur über universelle/existentielle Typen aus typtheoretischer Sicht, sondern auch darüber, wie dies für neue Benutzer verwirrend wäre.

Wir haben zwar keine wirklichen Perspektiven neuer Benutzer erhalten, aber das kam nicht von ungefähr.

@Boscop any und some wurden als Schlüsselwörter für diese Aufgabe vorgeschlagen, wurden aber dagegen entschieden (obwohl ich nicht weiß, ob die Begründung jemals irgendwo niedergeschrieben wurde).

Es stimmt, wir konnten kein Feedback von Leuten bekommen, die neu in Rost sind und keine Typentheoretiker waren

Und das Argument für Inklusion war immer, dass es Neueinsteigern leichter fallen würde. Wenn wir jetzt also tatsächliches Feedback von Neuankömmlingen haben, sollte das nicht eine sehr relevante Art von Feedback sein, anstatt zu argumentieren, wie Neuankömmlinge es verstehen sollten?

Ich denke, wenn jemand die Zeit hätte, könnte man in den Foren und an anderen Stellen recherchieren, wie verwirrt die Leute vor und nach der Aufnahme waren (ich war nicht sehr gut in Statistik, aber ich bin mir ziemlich sicher, dass jemand, der es konnte sich etwas einfallen lassen, das besser ist als blinde Vorhersagen).

Und das Argument für Inklusion war immer, dass es Neueinsteigern leichter fallen würde. Wenn wir also jetzt tatsächliches Feedback von Neuankömmlingen haben, sollte das nicht eine sehr relevante Art von Feedback sein, anstatt zu argumentieren, wie Neuankömmlinge es verstehen sollten?

Ja? Ich meine, ich bestreite nicht, ob das, was passiert ist, eine gute oder schlechte Idee war. Ich möchte nur darauf hinweisen, dass der RFC-Thread dazu Feedback erhalten hat und es trotzdem entschieden wurde.

Wie Sie sagten, ist es wahrscheinlich besser, die Metadiskussion zum Feedback woanders zu führen, obwohl ich nicht sicher bin, wo das sein würde.

Nein, zwei weitere Schlüsselwörter hinzuzufügen, um existenzielle und universelle Typen zu spezifizieren, würde die Verwirrung nicht verbessern, es würde die Dinge nur noch schlimmer machen.

Schlimmer? Wie ist das so? Ich bevorzuge es, mich an mehr zu erinnern als an Mehrdeutigkeit / Verwirrung.

Ja? Ich meine, ich bestreite nicht, ob das, was passiert ist, eine gute oder schlechte Idee war. Ich möchte nur darauf hinweisen, dass der RFC-Thread dazu Feedback erhalten hat und es trotzdem entschieden wurde.

Sicher. Aber beide streitenden Seiten waren alte, vernarbte und erfahrene Programmierer mit einem tiefen Verständnis dafür, was unter der Haube passiert, die über eine Gruppe rätseln, zu der sie nicht gehören (Neulinge) und über die Zukunft rätseln. Aus sachlicher Sicht ist das nicht viel besser als würfeln, was die Realität angeht. Dabei geht es nicht um unzureichende Erfahrung der Experten, sondern darum, dass keine ausreichenden Daten vorliegen, um die Entscheidungen zu stützen.

Jetzt wurde es eingeführt und wir haben eine Möglichkeit, die tatsächlichen harten Daten zu erhalten, oder so harte Daten, wie es im Land möglich ist, wie sehr die Leute auf einer Skala von 0 bis 10 verwirrt sind.

Wie Sie sagten, ist es wahrscheinlich besser, die Metadiskussion zum Feedback woanders zu führen

Hier zum Beispiel habe ich bereits eine solche Diskussion begonnen und es gibt einige konkrete Schritte, die unternommen werden können, auch wenn sie klein sind: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -bevor-der-stabilisieren-eines-Features/7522/14. Ich hatte nicht die Zeit, den RFC zu schreiben, also wenn mich jemand übertrifft oder helfen möchte, habe ich nichts dagegen.

Schlimmer? Wie ist das so?

Denn wenn impl Trait veraltet ist, haben Sie alle 3 und müssen sich zusätzlich zu der Verwirrung an mehr erinnern. Wenn impl Trait wegfallen würde, wäre die Situation eine andere und es würden Vor- und Nachteile der beiden Ansätze abgewogen.

impl Trait wie beim Callee-Picking wäre ausreichend. Wenn Sie versuchen, es in der Argumentposition zu verwenden, führen Sie die Verwirrung ein. Die HRTBs würden diese Verwirrung beseitigen.

@vorner Zuvor habe ich argumentiert, dass wir echte A/B-Tests mit Rust-Neulingen durchführen sollten, um zu sehen, was sie tatsächlich einfacher und schwerer zu erlernen finden, da es schwer zu erraten ist, dass jemand mit Rust vertraut ist.
FWIW, ich erinnere mich, als ich Rust lernte (von C++, D, Java usw. kommend), waren die universell quantifizierenden Typ-Generika (einschließlich ihrer Syntax) leicht zu verstehen (Lebenszeiten in Generika waren etwas schwieriger).
Ich denke, impl Trait für arg-Typen wird bei Neulingen auf der ganzen Linie zu viel Verwirrung und vielen Fragen wie dieser führen .
In Ermangelung jeglicher Beweise dafür, dass Änderungen Rust leichter erlernbar machen würden, sollten wir solche Änderungen unterlassen und stattdessen Änderungen vornehmen, die Rust konsistenter machen/halten, weil Konsistenz es zumindest leicht zu merken macht. Rust-Neulinge werden das Buch sowieso ein paar Mal lesen müssen, daher nimmt die Einführung von impl Trait für Argumente, um das Verschieben von Generika im Buch auf später zu ermöglichen, nicht wirklich die Komplexität.

@eddyb Übrigens , warum brauchen wir zusätzlich zu impl weiteres Schlüsselwort existential für Typen? (Ich wünschte, wir würden some für beides verwenden..)

FWIW, ich erinnere mich, als ich Rust lernte (von C++, D, Java usw. kommend), waren die universell quantifizierenden Typ-Generika (einschließlich ihrer Syntax) leicht zu verstehen (Lebenszeiten in Generika waren etwas schwieriger).

Ich selbst denke auch, dass es kein Problem ist. In meiner jetzigen Firma leite ich Rust-Kurse ‒ zur Zeit treffen wir uns einmal die Woche und ich versuche in der praktischen Umsetzung zu unterrichten. Die Leute sind erfahrene Programmierer, die hauptsächlich aus Java und Scala kommen. Es gab zwar einige Hindernisse, aber Generika (zumindest beim Lesen - sie sind ein bisschen vorsichtig beim Schreiben) in der Argumentationsposition war kein Thema. Es gab eine kleine Überraschung bei den Generika in der Rückgabeposition (zB der Aufrufer wählt aus, was die Funktion zurückgibt), insbesondere, dass sie oft eliminiert werden können, aber die Erklärung dauerte 2 Minuten, bevor sie klickte. Aber ich habe Angst, die Existenz von impl Trait in Argumentposition zu erwähnen, weil ich jetzt die Frage beantworten müsste, warum es existiert ‒ und darauf habe ich keine wirkliche Antwort. Das ist nicht gut für die Motivation und Motivation ist entscheidend für den Lernprozess.

Die Frage ist also, ob die Community genug Stimme hat, um die Debatte mit einigen Daten zur Untermauerung der Argumente erneut zu eröffnen?

@eddyb Übrigens , warum brauchen wir neben Impl ein weiteres existenzielles Schlüsselwort für Typen? (Ich wünschte, wir würden einige für beide verwenden..)

Warum nicht forall … /me schleicht sich langsam davon

@phaazon Wir haben forall (dh "universal") und es ist for , zB in HRTB ( for<'a> Trait<'a> ).

@eddyb Ja , dann verwende es auch für existenzielle Zwecke , wie es Haskell beispielsweise mit forall tut.

Die ganze Diskussion ist sehr eigensinnig, ich bin ein bisschen überrascht, dass die Argumentationsidee stabilisiert aussah. Ich hoffe, es gibt eine Möglichkeit, später einen weiteren RFC zu pushen, um das rückgängig zu machen (ich bin absolut bereit, es zu schreiben, weil ich die ganze Verwirrung, die dies mit sich bringen wird, wirklich wirklich, wirklich nicht mag).

Ich verstehe es nicht wirklich. Was bringt es, sie in einer Argumentationsposition zu haben? Ich schreibe nicht so viel Rust, aber ich mochte es wirklich, -> impl Trait tun zu können. Wann würde ich es jemals in der Argumentposition verwenden?

Mein Verständnis war, dass es hauptsächlich für die Konsistenz war. Wenn ich den Typ impl Trait an einer Argumentposition in einer Fn-Signatur schreiben kann, warum kann ich ihn dann nicht woanders schreiben?

Das heißt, ich persönlich hätte den Leuten lieber nur gesagt, dass sie "nur einen Typparameter verwenden" ...

Ja, aus Konsistenzgründen. Aber ich bin mir nicht sicher, ob es ein gutes Argument ist, wenn Typparameter so einfach zu verwenden sind. Außerdem stellt sich dann das Problem, wofür/dagegen zu fusseln ist!

Außerdem stellt sich dann das Problem, wofür/dagegen zu fusseln ist!

Wenn man bedenkt, dass man mit impl Trait mehrere Dinge überhaupt nicht ausdrücken kann, funktioniert mit impl Trait als eines der Argumente kein Turbofish und daher kann man seine Adresse nicht nehmen (habe ich einige vergessen? anderer Nachteil?), Ich denke, es macht wenig Sinn, gegen Typparameter zu lint, da Sie sie sowieso verwenden müssen.

daher kannst du seine Adresse nicht nehmen

Sie können es aus der Signatur ableiten.

Was bringt es, sie in einer Argumentationsposition zu haben?

Es gibt keine, weil es genau dasselbe ist wie die Verwendung einer Merkmalsgrenze.

fn foo(x: impl Debug)

Ist genau dasselbe wie

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Bedenken Sie auch Folgendes:

fn foo<A>(x: A) -> A where A: Debug

impl Trait in Argumentposition erlaubt Ihnen dies nicht, da es anonymisiert ist . Dies ist dann eine ziemlich nutzlose Funktion, da wir bereits alles haben, was wir brauchen, um mit solchen Situationen umzugehen. Die Leute werden diese neue Funktion nicht leicht erlernen, weil so ziemlich jeder Typvariablen / Vorlagenparameter kennt und Rust die einzige Sprache ist, die diese impl Trait Syntax verwendet. Aus diesem Grund schimpfen viele Leute, dass es beim Rückgabewert / den let-Bindungen hätte bleiben sollen, weil es eine neue, benötigte Semantik eingeführt hat (dh vom Angerufenen ausgewählter Typ).

Kurz gesagt, @iopq : Sie werden dies nicht brauchen, und es gibt keinen anderen „Lass uns ein weiteres syntaktisches Zuckerkonstrukt hinzufügen, das niemand wirklich brauchen wird, weil es mit einer sehr spezifischen Verwendung zurechtkommt – dh anonymisierten Typvariablen“ .

Außerdem habe ich vergessen zu sagen: Es macht es viel schwieriger zu sehen, wie Ihre Funktion parametrisiert / monomorphisiert ist.

@Verner Bei partiellem Turbofish ist es aus Gründen der Einfachheit, Lesbarkeit und Deutlichkeit sehr sinnvoll, dagegen zu fusseln. Ich bin jedoch nicht wirklich für das Feature in Arg-Position.

Wie ist es konsistent, wenn -> impl Trait der Angerufene den Typ wählt, während in x: impl Trait der Anrufer den Typ wählt?

Ich verstehe, dass es keine andere Möglichkeit gibt, aber das scheint nicht "konsequent" zu sein, es scheint das Gegenteil von konsistent zu sein

Ich stimme wirklich zu, dass es alles andere als konsistent ist und dass die Leute verwirrt sein werden, Neuankömmlinge ebenso wie erfahrene Rosttiere.

Wir hatten zwei RFCs, die vor mehr als 2 Jahren insgesamt fast 600 Kommentare erhielten, um die in diesem Thread neu aufgeworfenen Fragen zu lösen:

  • rust-lang/rfcs#1522 ("Minimal impl Trait ")
  • rust-lang/rfcs#1951 ("Syntax und Parameterbereich für impl Trait abschließen und auf Argumente erweitern")

(Wenn Sie diese Diskussionen lesen, werden Sie feststellen, dass ich anfangs ein starker Befürworter des Ansatzes mit zwei Schlüsselwörtern war. Heute denke ich, dass die Verwendung eines einzigen Schlüsselworts der richtige Ansatz ist.)

Nach 2 Jahren und Hunderten von Kommentaren wurde eine Entscheidung getroffen und das Feature wurde nun stabilisiert. Dies ist das Tracking-Problem für die Funktion, die offen ist, um die immer noch instabilen Anwendungsfälle für impl Trait zu verfolgen. Eine erneute Anfechtung der geregelten Aspekte von impl Trait ist für dieses Nachverfolgungsproblem nicht Thema. Sie können gerne weiter darüber sprechen, aber bitte nicht auf dem Issue Tracker.

Wie wurde es stabilisiert, wenn impl Trait nicht einmal Unterstützung in der Argumentposition für Fns in Merkmalen erhalten hat??

@daboross Dann muss die Checkbox im Originalbeitrag angekreuzt werden!

(Ich habe gerade festgestellt, dass https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable derzeit funktioniert)

Ich finde es komisch, dass https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug (noch) nicht funktioniert, außerdem diese Fehlermeldung. Bin ich der einzige der so denkt? Vielleicht müsste ein Kontrollkästchen für impl Trait in der Rückgabeposition in Merkmalen hinzugefügt werden, oder war es eine bewusste Entscheidung, nur impl Trait in der Argumentposition für Merkmalsfunktionen zuzulassen, was die Verwendung von existential type erzwingt

@Ekleog

War es eine bewusste Entscheidung, nur impl Trait in Argumentposition für Trait-Funktionen zuzulassen, was die Verwendung des existenziellen Typs für Rückgabetypen erzwingt?

Ja – die Rückgabeposition impl Trait in Merkmalen wurde verschoben, bis wir mehr praktische Erfahrung mit existentiellen Typen in Merkmalen haben.

@cramertj Sind wir schon so weit, dass wir genügend praktische Erfahrung haben, um das umzusetzen?

Ich würde Impl Trait gerne in einigen stabilen Releases sehen, bevor wir weitere Funktionen hinzufügen.

@mark-im Ich sehe nicht, was an der Rückgabeposition impl Trait für Merkmalsmethoden im Entferntesten umstritten ist, persönlich ... vielleicht übersehe ich etwas.

Ich glaube nicht, dass es umstritten ist. Ich habe einfach das Gefühl, dass wir Funktionen zu schnell hinzufügen. Es wäre schön, eine Weile innezuhalten und sich auf die technischen Schulden zu konzentrieren und zuerst Erfahrungen mit dem aktuellen Feature-Set zu sammeln.

Ich verstehe. Ich denke, ich halte es eher für einen fehlenden Teil einer vorhandenen Funktion als für eine neue Funktion.

Ich denke, @alexreg hat Recht, es ist sehr verlockend, existentielle impl Trait für die Methoden der Merkmale zu verwenden. Es ist nicht wirklich ein neues Feature, aber ich denke, es gibt ein paar Dinge zu beachten, bevor man versucht, es zu implementieren?

@phaazon Vielleicht, ja ... Ich weiß nicht wirklich, wie sehr sich die Implementierungsdetails von dem unterscheiden würden, was wir heute bereits haben, aber vielleicht kann sich jemand dazu äußern. Ich würde auch gerne existenzielle Typen für let/const-Bindungen sehen, aber ich kann dies definitiv als eine Funktion jenseits dieser hier akzeptieren und warte daher einen weiteren Zyklus oder so, bevor ich damit beginne.

Ich frage mich, ob wir uns mit universellen impliziten Eigenschaften in Eigenschaften zurückhalten können ...

Aber ja, ich glaube, ich verstehe deinen Standpunkt.

@mark-im Nein, das können wir nicht, sie sind bereits stabil .

Sie sind in Funktionen enthalten, aber was ist mit Merkmalsdeklarationen?

@mark-im wie das Snippet zeigt, sind sie sowohl in Impls als auch in Trait-Deklarationen stabil.

Einfach reinspringen, um herauszufinden, wo wir uns mit abstract type . Persönlich bin ich mit @Centril 's kürzlich vorgeschlagener Syntax und Best Practices ziemlich

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Was auf einen Code von mir zutraf, würde meiner Meinung nach ungefähr so ​​​​aussehen:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

Es macht für mich keinen Sinn, dass type Bar = (impl Display,); wäre, aber type Bar = impl Display; wäre schlecht.

Wenn wir uns für verschiedene alternative existentielle Typsyntaxen entscheiden (alle anders als Forenthread auf https://users.rust-lang.org/ ein guter Ort dafür?

Ich habe nicht genug Verständnis für die Alternativen, um jetzt einen solchen Thread zu starten, aber da existenzielle Typen immer noch nicht implementiert sind, denke ich, dass Diskussionen in den Foren und dann ein neuer RFC wahrscheinlich besser sind, als im Tracking-Thema darüber zu sprechen .

Was ist los mit type Foo = impl Trait ?

@daboross Wahrscheinlich stattdessen das interne Forum. Ich erwäge, einen RFC darüber zu schreiben, um die Syntax abzuschließen.

@daboross In diesem Thread wurde bereits mehr als genug über die Syntax diskutiert. Ich denke, wenn @Centril an dieser Stelle einen RFC dafür schreiben kann, dann großartig.

Gibt es ein Thema, das ich abonnieren kann, um über Existenzialien in Merkmalen zu diskutieren?

Gibt es ein makrobezogenes Argument für die eine oder andere Syntax?

@tomaka im ersten Fall ist type Foo = (impl Display,) wirklich die einzige Syntax, die du hast. Meine Vorliebe für type Foo: Trait gegenüber type Foo = impl Trait kommt nur von der Tatsache, dass wir einen Typ binden, den wir benennen können, wie <TFoo: Trait> oder where TFoo: Trait , wohingegen mit impl Trait Wir können den Typ nicht benennen.

Um das klarzustellen, ich sage nicht, dass type Foo = impl Bar schlecht ist, ich sage, dass type Foo: Bar in einfachen Fällen besser ist, teilweise aufgrund der Motivation von @KodrAus .

Letzteres lese ich als: "der Typ Foo erfüllt Bar" und erstere als: "der Typ Foo ist gleich einem Typ, der Bar erfüllt". Ersteres ist daher meiner Ansicht nach direkter und natürlicher aus einer extensionalen Sicht ("was ich mit Foo machen kann"). Um letzteres zu verstehen, müssen Sie ein tieferes Verständnis der existenziellen Quantifizierung von Typen einbeziehen.

type Foo: Bar ist auch ganz nett, denn wenn dies die Syntax ist, die als Grenze für einen zugeordneten Typ in einem Merkmal verwendet wird, können Sie einfach die Deklaration im Merkmal in das Impl kopieren und es funktioniert einfach (wenn das so ist) alle Informationen, die Sie veröffentlichen möchten..).

Die Syntax ist auch prägnanter, insbesondere wenn es um verknüpfte Typgrenzen geht und wenn viele verknüpfte Typen vorhanden sind. Dies kann das Rauschen reduzieren und somit die Lesbarkeit verbessern.

@KodrAus

So lese ich diese Typdefinitionen:

  • type Foo: Trait bedeutet " Foo ist ein Typ, der Trait implementiert "
  • type Foo = impl Trait bedeutet " Foo ist ein Alias ​​eines Typs, der Trait implementiert "

Für mich deklariert Foo: Trait einfach eine Einschränkung für Foo die Trait implementiert. In gewisser Weise fühlt sich type Foo: Trait unvollständig an. Es sieht so aus, als hätten wir eine Einschränkung, aber die eigentliche Definition von Foo fehlt.

Auf der anderen Seite erinnert impl Trait an "Dies ist ein einzelner Typ, aber der Compiler findet seinen Namen heraus". Daher impliziert type Foo = impl Trait , dass wir bereits einen konkreten Typ haben (der Trait implementiert), von dem Foo nur ein Alias ​​ist.

Ich glaube, dass type Foo = impl Trait die richtige Bedeutung deutlicher vermittelt: Foo ist ein Alias ​​von irgendeiner Art, der Trait implementiert.

@stjepang

type Foo: Trait bedeutet "Foo ist ein Typ, der eine Eigenschaft implementiert"
[..]
In gewisser Weise fühlt sich type Foo: Trait unvollständig an.

So habe ich es auch gelesen (Modulo-Phrasierung...), und es ist eine extensional korrekte Interpretation. Dies sagt alles darüber aus, was Sie mit Foo (den Morphismen, die der Typ bietet) machen können. Daher ist es dehnungskomplett. Aus Leser- und insbesondere Anfängerperspektive halte ich Extensionalität für wichtiger.

Auf der anderen Seite erinnert impl Trait an "Dies ist ein einzelner Typ, aber der Compiler füllt die Lücke". Daher impliziert type Foo = impl Trait , dass wir bereits einen konkreten Typ haben (der Trait implementiert), von dem Foo ein Alias ​​ist, aber der Compiler wird herausfinden, welcher Typ es wirklich ist.

Dies ist eine detailliertere und intensionale Interpretation, die sich mit der Repräsentation befasst, die aus einer extensionalen Sicht überflüssig ist. Dies ist jedoch in einem intensionalen Sinne vollständiger.

@Centril

Aus Leser- und insbesondere Anfängerperspektive halte ich Extensionalität für wichtiger.

Dies ist eine detailliertere und intensionale Interpretation, die sich mit der Repräsentation befasst, die aus einer extensionalen Sicht überflüssig ist

Die Dichotomie zwischen Extension und Intension ist interessant – ich habe mir impl Trait noch nie so vorgestellt.

Dennoch muss ich bei der Schlussfolgerung anderer Meinung sein. FWIW, ich habe es noch nie geschafft, existenzielle Typen in Haskell und Scala zu groken, also zähle mich als Anfänger. :) impl Trait in Rust hat sich vom ersten Tag an sehr intuitiv angefühlt, was wahrscheinlich daran liegt, dass ich es als eingeschränkten Alias ​​betrachte und nicht als das, was mit dem Typ gemacht werden kann. Zwischen dem Wissen, was Foo ist und was man damit machen kann, entscheide ich mich für ersteres.

Aber nur meine 2c. Andere haben möglicherweise andere mentale Modelle von impl Trait .

Ich stimme diesem Kommentar voll und ganz type Foo: Trait fühlt sich unvollständig an. Und type Foo = impl Trait fühlt sich ähnlicher an wie impl Trait anderswo, was dazu beiträgt, dass sich die Sprache konsistenter und einprägsamer anfühlt.

@joshtriplett Siehe https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 für einen Einstieg in die Konsistenzdiskussion; Ich glaube, dass das Zulassen von Formularformen tatsächlich die konsequente Vorgehensweise ist. Und nur eine der Formen (welche auch immer...) zuzulassen, ist inkonsistent. Das Erlauben von type Foo: Trait passt auch besonders gut zu https://github.com/rust-lang/rfcs/pull/2289, wo man type Foo: Iterator<Item: Display>; was die Dinge ordentlich einheitlich macht.

@stjepang Die extensionale Perspektive von type Foo: Bar; erfordert nicht, dass Sie die existenzielle Quantifizierung in der Typentheorie verstehen. Alles, was Sie wirklich verstehen müssen, ist, dass Sie mit Foo alle Operationen ausführen können, die Bar bietet, das war's. Aus der Sicht eines Benutzers von Foo ist das auch das Interessanteste.

@Centril

Ich glaube, ich verstehe jetzt, woher Sie kommen und wie attraktiv es ist, die Type: Trait Syntax an so vielen Stellen wie möglich zu verbreiten.

Es gibt eine starke Konnotation, wenn : für Typ-Implemente-Eigenschaftsgrenzen verwendet wird und = für Typdefinitionen und Typ-gleich-einem anderen Typ-Grenzen verwendet wird.

Ich denke, dies ist auch in Ihrem RFC offensichtlich. Nehmen wir zum Beispiel diese beiden Typgrenzen:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

Diese beiden Grenzen haben am Ende den gleichen Effekt, sind aber (glaube ich) subtil unterschiedlich. Ersteres sagt " Item muss Trait Bar implementieren", während letzteres sagt " Item muss gleich einem Typ sein, der Bar implementiert".

Lassen Sie mich versuchen, diese Idee an einem anderen Beispiel zu veranschaulichen:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

Wie sollten wir dann einen existenziellen Typ definieren, der Person implementiert?

  • type Someone: Person , die wie ein Typ gebunden aussieht.
  • type Someone = impl Person , die wie eine Typdefinition aussieht.

@stjepang Es ist keine schlechte Sache, wie eine Person for Alice wie folgt implementieren:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Schau, M'a! Der Inhalt von { .. } sowohl für das Merkmal als auch für Impl ist identisch, das heißt, Sie können den Text aus dem Merkmal unverändert kopieren, was Name betrifft.

Da ein zugeordneter Typ eine Funktion auf Typebene ist (wobei das erste Argument Self ), können wir einen Typalias als einen zugeordneten Typ mit 0-Arität sehen, sodass nichts Seltsames passiert.

Diese beiden Grenzen haben am Ende den gleichen Effekt, sind aber (glaube ich) subtil unterschiedlich. Ersteres sagt "Gegenstand muss Merkmalsleiste implementieren", während letzteres sagt "Gegenstand muss einem Typ entsprechen, der Balken implementiert".

Ja; Die erste Formulierung finde ich treffender und natürlicher. :)

@ Centril He . Bedeutet das, dass type Thing; allein ausreicht, um einen abstrakten Typ einzuführen?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm Ich denke, es ist technisch möglich; aber Sie könnten fragen, ob es wünschenswert ist oder nicht, abhängig von Ihren Gedanken über implizit/explizit. In diesem speziellen Fall wäre es meiner Meinung nach technisch ausreichend zu schreiben:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

und der Compiler könnte nur type Output: Sized; für Sie ableiten (was eine zutiefst uninteressante Grenze ist, die Ihnen keine Informationen gibt). Es ist etwas, das für interessantere Grenzen in Betracht gezogen werden sollte, aber es wird nicht in meinem ursprünglichen Vorschlag enthalten sein, weil ich denke, dass es APIs mit niedrigem Preis-Leistungs-Verhältnis fördern könnte, selbst wenn der konkrete Typ aufgrund der Programmierfaulheit sehr einfach ist :) Auch type Output; anfangs aus dem gleichen Grund.

Ich denke, nachdem ich das alles gelesen habe, stimme ich @Centril eher zu. Wenn ich type Foo = impl Bar sehe, neige ich dazu zu denken, dass Foo ein bestimmter Typ ist, wie bei anderen Aliasnamen. Aber es ist nicht. Betrachten Sie dieses Beispiel:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

IMHO ist es ein bisschen seltsam, in der Deklaration von Displayable = zu sehen, aber dann die Rückgabetypen von foo und bar nicht gleich zu haben (dh dies = ist nicht transitiv, anders als überall sonst). Das Problem ist, dass Foo _kein_ ein Alias ​​für einen bestimmten Typ ist, der zufällig eine Eigenschaft impliziert. Anders ausgedrückt, es ist ein einzelner Typ in jedem Kontext, in dem er verwendet wird, aber dieser Typ kann für verschiedene Zwecke unterschiedlich sein, wie im Beispiel.

Einige Leute haben erwähnt, dass sich type Foo: Bar "unvollständig" anfühlt. Für mich ist das eine gute Sache. In gewisser Weise ist Foo unvollständig; wir wissen nicht, was es ist, aber wir wissen, dass es Bar erfüllt.

@mark-im

Das Problem ist, dass Foo kein Alias ​​für einen bestimmten Typ ist, der zufällig eine Eigenschaft impliziert. Anders ausgedrückt, es ist ein einzelner Typ in jedem Kontext, in dem er verwendet wird, aber dieser Typ kann für verschiedene Zwecke unterschiedlich sein, wie im Beispiel.

Wow, stimmt das wirklich? Das würde mich sicherlich sehr verwirren.

Gibt es einen Grund, warum Displayable eine Abkürzung für impl Display und nicht ein einzelner konkreter Typ? Ist ein solches Verhalten überhaupt sinnvoll, wenn man bedenkt, dass Merkmalsaliase (Tracking-Problem: https://github.com/rust-lang/rust/issues/41517) auf ähnliche Weise verwendet werden können? Beispiel:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Das ist kein gültiges Beispiel. Aus dem Referenzabschnitt zu existenziellen Typen in RFC 2071 :

existential type Foo = impl Debug;

Foo kann an mehreren Stellen im Modul als i32 werden. Jede Funktion, die Foo als i32 muss jedoch unabhängig von Foo Beschränkungen auferlegen, sodass sie i32

Jede existentielle Typdeklaration muss durch mindestens einen Funktionsrumpf oder einen konstanten/statischen Initialisierer eingeschränkt werden. Ein Körper oder Initialisierer muss einen bestimmten existenziellen Typ entweder vollständig einschränken oder keine Einschränkungen auferlegen.

Nicht direkt erwähnt, aber für das Funktionieren des Rests des RFC erforderlich ist, dass zwei Funktionen im Rahmen des Existenzialtyps keinen anderen konkreten Typ für diesen Existenzialtyp bestimmen können. Das ist eine Art widersprüchlicher Typfehler.

Ich würde vermuten, dass Ihr Beispiel expected type `&'static str` but found type `i32` der Rückgabe von bar etwas wie expected type `&'static str` but found type `i32` bar , da foo bereits den konkreten Typ von Displayable auf &'static str .

EDIT: Es sei denn, Sie kommen aus der Intuition, die

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

ist äquivalent zu

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

eher als meine erwartung von

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Ich denke, welche dieser beiden Interpretationen richtig ist, hängt möglicherweise von dem RFC ab, den @Centril möglicherweise schreibt.

Das Problem ist, dass Foo kein Alias ​​für einen bestimmten Typ ist, der zufällig eine Eigenschaft impliziert.

Ich denke, welche dieser beiden Interpretationen richtig ist, hängt möglicherweise von dem RFC ab, den @Centril möglicherweise schreibt.

Der Grund, warum type Displayable = impl Display; existiert, ist, dass es ein Alias ​​für einen bestimmten Typ ist.
Siehe https://github.com/rust-lang/rfcs/issues/1738 , welches das Problem ist, das diese Funktion löst.

@ Nemo157 Ihre Erwartung ist richtig. :)

Die folgende:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

wäre entzuckert zu:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

wobei _0 , _1 und _2 alle nominell verschiedene Typen sind, weshalb Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (und die symmetrische Instanzen) sind alle unbewohnt, wobei Id in refl definiert ist.

Haftungsausschluss: Ich habe den RFC (freiwillig) nicht gelesen (aber weiß, worum es geht), damit ich mich dazu äußern kann, was sich mit Syntaxen „intuitive“ anfühlt.

Für die type Foo: Trait Syntax würde ich erwarten, dass so etwas möglich ist:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

Genauso wie where Foo: Display, Foo: Debug derzeit möglich ist.

Wenn die Syntax nicht zulässig ist, denke ich, dass dies ein Problem mit der Syntax ist.

Oh, und ich denke, je mehr Syntax Rust hat, desto schwieriger wird es, sie zu lernen. Auch wenn eine Syntax „leichter zu erlernen“ ist, wird der Anfänger, solange die beiden Syntaxen notwendig sind, irgendwann beide lernen müssen, und wahrscheinlich eher früher als später, wenn er sich für ein bereits bestehendes Projekt einsetzt.

@Ekleog

Für die type Foo: Trait Syntax würde ich erwarten, dass so etwas möglich ist:

Es ist möglich. Diese "Typaliase" deklarieren zugeordnete Typen (Typaliase können als Funktionen auf der Ebene des 0-ären Typs interpretiert werden, während die zugeordneten Typen Funktionen auf der Ebene des 1+-Typs sind). Natürlich können Sie nicht mehrere Typen mit demselben Namen in einem Merkmal haben, das wäre so, als würden Sie versuchen, zwei Typaliase mit demselben Namen in einem Modul zu definieren. In einem impl type Foo: Bar auch einer existenziellen Quantifizierung.

Oh, und ich denke, je mehr Syntax Rust hat, desto schwieriger wird es, sie zu lernen.

Beide Syntaxen werden bereits verwendet. type Foo: Bar; ist in Merkmalen bereits legal und auch für die universelle Quantifizierung als Foo: Bar wobei Foo eine Typvariable ist. impl Trait wird für die existentielle Quantifizierung in der Rückgabeposition und für die universelle Quantifizierung in der Argumentposition verwendet. Das Zulassen beider schließt Konsistenzlücken in der Sprache. Sie sind auch für verschiedene Szenarien optimal, sodass Sie mit beiden das globale Optimum erreichen.

Außerdem ist es unwahrscheinlich, dass der Anfänger type Foo = (impl Bar, impl Baz); . Die meisten Verwendungen werden wahrscheinlich type Foo: Bar; .

Der ursprüngliche Pull-Request für RFC 2071 erwähnt ein typeof Schlüsselwort, das in dieser Diskussion anscheinend vollständig abgetan wurde. Ich finde die derzeit vorgeschlagene Syntax eher implizit, da sowohl der Compiler als auch jeder Mensch, der den Code liest, nach dem konkreten Typ suchen.

Ich würde es vorziehen, wenn dies explizit gemacht würde. Also statt

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

wir würden schreiben

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(mit dem Namen des return_type_of, der mit dem Fahrrad verladen werden soll), oder sogar

fn foo_func() -> impl SomeTrait as Foo { ... }

die nicht einmal neue Schlüsselwörter benötigen würde und für jeden leicht verständlich ist, der die Impl-Trait-Syntax kennt. Die letztere Syntax ist prägnant und enthält alle Informationen an einem Ort. Für Merkmale könnte es so aussehen:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

oder auch

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Es tut mir leid, wenn dies bereits besprochen und abgewiesen wurde, aber ich konnte es nicht finden.

@Centril

Es ist möglich. Diese "Typaliase" deklarieren zugeordnete Typen (Typaliase können als Funktionen auf der Ebene des 0-ären Typs interpretiert werden, während die zugeordneten Typen Funktionen auf der Ebene des 1+-Typs sind). Natürlich können Sie nicht mehrere Typen mit demselben Namen in einem Merkmal haben, das wäre so, als würden Sie versuchen, zwei Typaliase mit demselben Namen in einem Modul zu definieren. Geben Sie in einem Impl Foo ein: Bar entspricht auch der existentiellen Quantifizierung.

(Entschuldigung, ich wollte es in ein impl Trait for Struct , nicht in ein trait Trait )

Es tut mir leid, ich bin nicht sicher, ob ich das verstehe. Was ich zu sagen versuche, ist für mich Code wie

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(Spielplatz-Link für Vollversion)
fühlt sich an, als ob es funktionieren sollte.

Weil es nur zwei Grenzen für Type , genauso wie where Type: Debug, Type: Display work .

Ist dies nicht erlaubt sein (was ich scheinen zu verstehen , „Natürlich nicht mehrere assoziierte Typen mit dem gleichen Namen in die man haben kann Eigenschaft“? Aber mein Fehler gegeben , schriftlich trait Trait anstelle von impl Trait for Struct Ich bin mir nicht sicher), dann denke ich, dass das ein Problem mit der type Type: Trait Syntax ist.

Dann ist die Syntax innerhalb einer trait Deklaration bereits type Type: Trait und lässt keine Mehrfachdefinitionen zu. Ich vermute also, dass dieses Boot schon vor langer Zeit gesegelt ist…

Wie jedoch von @stjepang und @joshtriplett oben erwähnt, fühlt sich type Type: Trait unvollständig an. Und während es Sinn machen kann trait Erklärungen (es ist eigentlich entwickelt , unvollständig zu sein, auch wenn es seltsam ist es nicht möglich, mehr Definitionen erlaubt), ist es nicht sinnvoll , in einem impl Trait Block , wobei der Typ sicher bekannt sein soll (und derzeit nur als type Type = RealType )

impl Trait wird für die existentielle Quantifizierung in der Rückgabeposition und für die universelle Quantifizierung in der Argumentposition verwendet.

Ja, ich dachte auch an impl Trait in Argumentposition, als ich dies schrieb, und fragte mich, ob ich sagen sollte, dass ich dasselbe Argument für impl Trait in Argumentposition unterstützt hätte, wenn ich gewusst hätte, dass es sich stabilisiert . Trotzdem denke ich, es wäre besser, diese Debatte nicht neu zu entfachen :)

Das Zulassen beider schließt Konsistenzlücken in der Sprache. Sie sind auch für verschiedene Szenarien optimal, sodass Sie mit beiden das globale Optimum erreichen.

Optimal und einfach

Nun, ich denke, manchmal ist es gut, das Optimum zugunsten der Einfachheit zu verlieren. C und ML wurden ungefähr zur gleichen Zeit geboren. C machte große Zugeständnisse an das Optimum zugunsten der Einfachheit, ML war viel näher am Optimum, aber viel komplexer. Selbst wenn man Derivate dieser Sprachen mitzählt, glaube ich nicht, dass die Zahl der C-Entwickler und der ML-Entwickler vergleichbar ist.

impl Trait und :

Derzeit habe ich das Gefühl, dass es : Syntaxen impl Trait und : einen Trend gibt, beide alternative Syntaxen für denselben Funktionsumfang zu erstellen. Ich denke jedoch, dass das nicht gut ist, da zwei Syntaxen für die gleichen Funktionen die Benutzer nur verwirren können, insbesondere wenn sie sich in ihrer genauen Semantik immer subtil unterscheiden.

Stellen Sie sich einen Anfänger vor, der bei seinem ersten type Type = impl Trait immer type Type: Trait kommen sah. Sie können wahrscheinlich erraten, was passiert, aber ich bin mir ziemlich sicher, dass es einen Moment geben wird, in dem „WTF ist das? Ich benutze Rust seit Jahren und es gibt immer noch eine Syntax, die ich noch nie gesehen habe?“. Das ist mehr oder weniger die Falle, in die C++ geraten ist.

Funktion aufblähen

Was ich denke ist im Grunde, je mehr Funktionen es hat, desto schwieriger ist es, die Sprache zu lernen. Und ich sehe keinen großen Vorteil darin, type Type: Trait gegenüber type Type = impl Trait : Es werden etwa 6 Zeichen gespeichert?

Wenn rustc einen Fehler ausgibt, wenn type Type: Trait angezeigt wird, der besagt, dass die Person, die es schreibt, type Type = impl Trait würde es für mich viel sinnvoller sein: zumindest gibt es eine einzige Möglichkeit, Dinge zu schreiben , es ist für alle sinnvoll ( impl Trait wird bereits klar als existentiell in der Rückstellung erkannt) und deckt alle Anwendungsfälle ab. Und wenn Leute versuchen, das zu verwenden, was sie für intuitiv halten (obwohl ich dem nicht zustimmen würde, ist = impl Trait für mich intuitiver als die aktuellen = i32 ), werden sie zu Recht auf die konventionell korrekte Schreibweise.

Der ursprüngliche Pull-Request für RFC 2071 erwähnt eine Art von Schlüsselwort, die in dieser Diskussion anscheinend vollständig abgetan wurde. Ich finde die derzeit vorgeschlagene Syntax eher implizit, da sowohl der Compiler als auch jeder Mensch, der den Code liest, nach dem konkreten Typ suchen.

typeof wurde kurz in der Ausgabe diskutiert, die ich vor 1,5 Jahren geöffnet habe: https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

Als Anfänger finde ich die Syntax von type Foo: Bar verwirrend. Es ist die zugehörige Typsyntax, aber diese sollen in Traits und nicht in Structs enthalten sein. Wenn Sie impl Trait einmal sehen, können Sie herausfinden, was das ist, oder Sie können es auf andere Weise nachschlagen. Dies ist mit der anderen Syntax schwieriger, und ich bin mir nicht sicher, was der Vorteil ist.

Es fühlt sich an, als ob einige Leute im Sprachteam wirklich dagegen sind, impl Trait zu verwenden, um existenzielle Typen zu benennen, also würden sie lieber etwas anderes verwenden. Selbst der Kommentar hier macht für mich wenig Sinn.

Aber wie auch immer, ich glaube, dieses Pferd wurde zu Tode geprügelt. Es gibt wahrscheinlich Hunderte von Kommentaren zur Syntax und nur eine Handvoll Vorschläge (mir ist klar, dass ich die Dinge nur noch schlimmer mache). Es ist klar, dass keine Syntax nicht alle glücklich macht, und es gibt Argumente dafür und dagegen. Vielleicht sollten wir einfach einen auswählen und dabei bleiben.

Woah, das habe ich überhaupt nicht verstanden. Danke @Nemo157, dass du mich

In diesem Fall würde ich in der Tat die =-Syntax bevorzugen.

@Ekleog

dann denke ich, dass das ein Problem mit der type Type: Trait Syntax ist.

Es könnte erlaubt sein und es wäre perfekt definiert, aber Sie schreiben normalerweise where Type: Foo + Bar anstelle von where Type: Foo, Type: Bar , das scheint also keine sehr gute Idee zu sein. Sie könnten für diesen Fall auch leicht eine gute Fehlermeldung auslösen, die vorschlägt, stattdessen Foo + Bar für den zugehörigen Typ zu schreiben.

type Foo = impl Bar; hat auch Verständlichkeitsprobleme, da Sie = impl Bar und daraus schließen, dass Sie es einfach bei jedem Vorkommen ersetzen können, wo es als -> impl Bar ; aber das würde nicht funktionieren. @mark-im hat diese Interpretation gemacht, was ein viel wahrscheinlicherer Fehler zu sein scheint. Daher schließe ich, dass type Foo: Bar; die bessere Wahl für die Erlernbarkeit ist.

Geben Sie jedoch, wie oben von @joshtriplett gezeigt , Typ ein: Trait fühlt sich unvollständig an.

Es ist nicht unvollständig von einem Extensions-POV. Von type Foo: Bar; erhalten Sie genau so viele Informationen wie von type Foo = impl Bar; . Aus der Perspektive dessen, was Sie mit type Foo: Bar; tun können , ist es also vollständig. Tatsächlich wird letzteres als type _0: Bar; type Foo = _0; entzuckert.

BEARBEITEN: Was ich meinte, war, dass es sich zwar für einige unvollständig anfühlt, aber nicht aus technischer Sicht.

Trotzdem denke ich, es wäre besser, diese Debatte nicht neu zu entfachen :)

Das ist eine gute Idee. Wir sollten die Sprache beim Entwerfen so berücksichtigen, wie sie ist, nicht wie wir es uns gewünscht haben.

Nun, ich denke, manchmal ist es gut, das Optimum zugunsten der Einfachheit zu verlieren.

Wenn wir der Einfachheit halber vorgehen sollten, würde ich stattdessen type Foo = impl Bar; .
Anzumerken ist, dass die vermeintliche Einfachheit von C (angeblich, weil Haskell Core und ähnliche Dinge wahrscheinlich einfacher sind, während sie immer noch solide sind..) einen hohen Preis in Bezug auf Ausdruckskraft und Solidität hat. C ist nicht mein Nordstern im Sprachdesign; weit davon entfernt.

Derzeit habe ich das Gefühl, dass es : Syntaxen impl Trait und : einen Trend gibt, beide alternative Syntaxen für denselben Funktionsumfang zu erstellen. Ich denke jedoch, dass das nicht gut ist, da zwei Syntaxen für die gleichen Funktionen die Benutzer nur verwirren können, insbesondere wenn sie sich in ihrer genauen Semantik immer subtil unterscheiden.

Aber sie werden sich in ihrer Semantik nicht unterscheiden. Einer entzuckert dem anderen.
Ich denke, die Verwirrung, type Foo: Bar; oder type Foo = impl Bar nur für einen von ihnen zu schreiben, um nicht zu funktionieren, obwohl beide eine perfekt definierte Semantik haben, liegt nur im Weg des Benutzers. Wenn ein Benutzer versucht, type Foo = impl Bar; zu schreiben, wird ein Lint ausgelöst und schlägt type Foo: Bar; . Der Lint bringt dem Benutzer die andere Syntax bei.
Mir ist es wichtig, dass die Sprache einheitlich und konsistent ist; Wenn wir uns entschieden haben, beide Syntaxen irgendwo zu verwenden, sollten wir diese Entscheidung konsequent anwenden.

Stellen Sie sich einen Anfänger vor, der bei seinem ersten type Type = impl Trait immer type Type: Trait kommen sah.

In diesem speziellen Fall würde ein Lint ausgelöst und die frühere Syntax empfohlen. Wenn es um type Foo = (impl Bar, impl Baz); , muss der Anfänger auf jeden Fall -> impl Trait lernen, also sollte er die Bedeutung daraus ableiten können.

Das ist mehr oder weniger die Falle, in die C++ geraten ist.

Das Problem von C++ besteht hauptsächlich darin, dass es ziemlich alt ist, das Gepäck von C hat und viele Funktionen, die zu viele Paradigmen unterstützen. Dies sind keine unterschiedlichen Funktionen, sondern nur eine andere Syntax.

Was ich denke ist im Grunde, je mehr Funktionen es hat, desto schwieriger ist es, die Sprache zu lernen.

Ich denke, beim Erlernen einer neuen Sprache geht es hauptsächlich darum, ihre wichtigen Bibliotheken zu lernen. Dort wird die meiste Zeit verbracht. Die richtigen Funktionen können Bibliotheken viel übersichtlicher machen und in mehr Fällen funktionieren. Ich bevorzuge eine Sprache, die eine gute abstrakte Kraft verleiht, als eine, die Sie dazu zwingt, auf niedriger Ebene zu denken und die Duplizierung verursacht. In diesem Fall fügen wir keine abstraktere Kraft oder nicht einmal wirklich gleichmäßige Funktionen hinzu, sondern nur eine bessere Ergonomie.

Und ich sehe keinen großen Vorteil darin, type Type: Trait gegenüber type Type = impl Trait: Es werden etwa 6 Zeichen gespeichert?

Ja, nur 6 Zeichen gespeichert. Aber wenn wir type Foo: Iterator<Item: Iterator<Item: Display>>; Betracht ziehen, dann erhalten wir stattdessen: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; was viel mehr Rauschen hat. type Foo: Bar; ist im Vergleich zu letzterem auch direkter, weniger anfällig für Fehlinterpretationen (z.
Außerdem könnte type Foo: Bar natürlich auf type Foo: Bar = ConcreteType; was den konkreten Typ freilegt, aber auch sicherstellt, dass er Bar erfüllt. Für type Foo = impl Trait; .

Wenn rustc beim Anzeigen von type Type: Trait einen Fehler ausgibt, der besagt, dass die Person, die es schreibt, type Type = impl Trait würde es für mich viel mehr Sinn machen: Zumindest gibt es eine einzige Möglichkeit, Dinge zu schreiben,

sie werden zu Recht auf die konventionell korrekte Schreibweise umgeleitet.

Ich schlage vor, dass es eine konventionelle Art gibt, Dinge zu schreiben; type Foo: Bar; .

@lnicola

Als Anfänger finde ich die Syntax von type Foo: Bar verwirrend. Es ist die zugehörige Typsyntax, aber diese sollen in Traits und nicht in Structs enthalten sein.

Ich wiederhole, dass Typaliase wirklich als verknüpfte Typen angesehen werden können. Sie werden sagen können:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Wir sehen, dass es bei zugeordneten Typen und Typaliasen genau gleich funktioniert.

Ja, nur 6 Zeichen gespeichert. Aber wenn wir type Foo: Iterator<Item: Iterator<Item: Display>>; , dann erhalten wir stattdessen: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; die viel mehr Lärm hat.

Dies scheint orthogonal zur Syntax zum Deklarieren eines benannten Existenzials zu sein. Die vier Syntaxen, von denen ich mich erinnere, dass sie vorgeschlagen wurden, würden dies alle potenziell zulassen, da

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

In der Lage zu sein, Ihre vorgeschlagene Abkürzung Trait<AssociatedType: Bound> anstelle der Trait<AssociatedType = impl Bound> Syntax zum Deklarieren anonymer existentieller Typen für die zugehörigen Typen eines existentiellen Typs (entweder benannt oder anonym) zu verwenden, ist eine unabhängige Funktion (aber wahrscheinlich relevant in Bedingungen, um den gesamten Satz von existenziellen Typmerkmalen konsistent zu halten).

@ Nemo157 Das sind unterschiedliche Funktionen, ja; aber ich denke, es ist natürlich, sie aus Gründen der Konsistenz zusammen zu betrachten.

@Centril

Es tut mir leid, aber sie liegen falsch. Es ist nicht unvollständig von einem Extensions-POV.

Ich habe nie behauptet, dass Ihrer vorgeschlagenen Syntax Informationen fehlen; Ich habe angedeutet, dass es sich unvollständig

Beachten Sie auch, dass in diesem Thread ein Interpretationsproblem mit genau diesem Syntaxunterschied demonstriert wurde. type Foo = impl Trait fühlt sich an, als würde es klarer machen, dass Foo ein spezifischer, aber unbenannter konkreter Typ ist, egal wie oft Sie ihn verwenden, und kein Alias ​​für ein Merkmal, das einen anderen konkreten Typ annehmen kann jedes Mal, wenn Sie es verwenden.

Ich denke , es hilft den Menschen zu sagen , dass sie alle die Dinge nehmen können , sie wissen über -> impl Trait und sie auf type Foo = impl Trait ; es gibt ein verallgemeinertes Konzept impl Trait , das sie an beiden Stellen als Baustein sehen können. Syntax wie type Foo: Trait verbirgt diesen verallgemeinerten Baustein.

@joshtriplett

Ich habe angedeutet, dass es sich unvollständig anfühlt; es sieht für mich und andere falsch aus.

In Ordung; Ich schlage vor, dass wir hier einen anderen Begriff als incomplete , weil er mir einen Mangel an Informationen suggeriert.

Beachten Sie auch, dass in diesem Thread ein Interpretationsproblem mit genau diesem Syntaxunterschied demonstriert wurde.

Was ich beobachtet habe, war ein Interpretationsfehler, der im Thread gemacht wurde, was type Foo = impl Bar; bedeutet. Eine Person interpretierte verschiedene Verwendungen von Foo als nicht nominell der gleiche Typ, sondern eher unterschiedliche Typen. Das heißt genau: "ein Alias ​​für ein Merkmal, das bei jeder Verwendung einen anderen konkreten Typ annehmen kann" .

Einige haben gesagt, dass type Foo: Bar; verwirrend ist, aber ich bin mir nicht sicher, was die alternative Interpretation von type Foo: Bar; ist, die sich von der beabsichtigten Bedeutung unterscheidet. Ich wäre an alternativen Interpretationen interessiert.

@Centril

Ich wiederhole, dass Typaliase wirklich als verknüpfte Typen angesehen werden können.

Sie können, aber im Moment beziehen sich zugeordnete Typen auf Merkmale. impl Trait funktioniert überall oder fast. Wenn Sie impl Trait als eine Art assoziierten Typ darstellen möchten, müssen Sie zwei Konzepte gleichzeitig einführen. Das heißt, Sie sehen impl Trait als Rückgabetyp einer Funktion, erraten oder lesen Sie, was das bedeutet. Wenn Sie dann impl Trait in einem Typalias sehen, können Sie dieses Wissen wiederverwenden.

Vergleichen Sie dies mit der Anzeige verknüpfter Typen in einer Merkmalsdefinition. In diesem Fall denken Sie, dass es etwas ist, das andere Strukturen definieren oder implementieren müssen. Aber wenn Sie auf type Foo: Debug außerhalb eines Merkmals stoßen, wissen Sie nicht, was das ist. Es gibt niemanden, der es implementiert, also ist es eine Art Vorwärtsdeklaration? Hat es etwas mit Vererbung zu tun, wie es in C++ der Fall ist? Ist es wie ein ML-Modul, bei dem jemand anders den Typ auswählt? Und wenn Sie impl Trait schon einmal gesehen haben, gibt es nichts, was eine Verbindung zwischen ihnen herstellt. Wir schreiben fn foo() -> impl ToString , nicht fn foo(): ToString .

Typ Foo = Impl Bar; hat auch Verständlichkeitsprobleme, da Sie = impl Bar sehen und schlussfolgern, dass Sie es bei jedem Vorkommen einfach ersetzen können, wo es verwendet wird als -> Impl Bar

Ich habe es hier schon einmal gesagt, aber das ist, als würde man denken, dass let x = foo(); bedeutet, dass Sie x anstelle von foo() . Auf jeden Fall ein Detail, das man bei Bedarf schnell nachschlagen kann, das aber das Konzept nicht grundlegend ändert.

Das heißt, es ist leicht herauszufinden, worum es hier geht (ein abgeleiteter Typ wie in -> impl Trait ), auch wenn Sie nicht genau wissen, wie es funktioniert (was passiert, wenn Sie widersprüchliche Definitionen dafür haben). Mit der anderen Syntax ist es schwer zu erkennen, was es ist.

@Centril

In Ordung; Ich schlage vor, dass wir hier einen anderen Begriff als unvollständig verwenden, weil er mir einen Mangel an Informationen suggeriert.

„unvollständig“ muss nicht einen Mangel an Information bedeutet, kann es sein, dass etwas sieht meine , wie es angenommen hat , um etwas anderes haben und dies nicht tut.

type Foo: Trait; sieht nicht wie eine vollständige Deklaration aus. Es sieht so aus, als ob etwas fehlt. Und es scheint völlig anders zu sein als type Foo = SomeType<X, Y, Z>; .

Vielleicht erreichen wir den Punkt, an dem unsere Einzeiler allein diese Konsenslücke zwischen type Inferred: Trait und type Inferred = impl Trait nicht wirklich überbrücken können.

Glauben Sie, dass es sich lohnen würde, eine experimentelle Implementierung dieser Funktion mit einer beliebigen Syntax (auch der im RFC angegebenen) zusammenzustellen, damit wir in größeren Programmen damit spielen können, um zu sehen, wie sie in den Kontext passt?

@lnicola

[..] impl Trait funktioniert überall oder fast

Naja, Foo: Bound funktioniert auch fast überall ;)

Aber wenn Sie auf ein type Foo: Debug außerhalb eines Merkmals stoßen, wissen Sie nicht, was das ist.

Ich denke, der Fortschritt der Verwendung in: Trait -> Impl -> Type Alias ​​hilft beim Lernen.
Außerdem denke ich, dass die Schlussfolgerung, dass "der Typ Foo Debug implementiert" wahrscheinlich von
type Foo: Debug in Merkmalen und aus generischen Grenzen zu sehen, und es ist auch richtig.

Hat es etwas mit Vererbung zu tun, wie es in C++ der Fall ist?

Ich denke, die fehlende Vererbung in Rust muss viel früher gelernt werden als beim Erlernen der Funktion, die wir besprechen, da dies für Rust so grundlegend ist.

Ist es wie ein ML-Modul, bei dem jemand anders den Typ auswählt?

Diese Schlussfolgerung kann auch für type Foo = impl Bar; aufgrund von arg: impl Bar wo der Anrufer (Benutzer) den Typ auswählt. Für mich scheint die Schlussfolgerung, dass der Benutzer den Typ auswählt, für type Foo: Bar; weniger wahrscheinlich.

Ich habe es hier schon einmal gesagt, aber das ist, als würde man denken, dass let x = foo(); bedeutet, dass Sie x anstelle von foo() .

Wenn die Sprache referentiell transparent ist, können Sie ersetzen x für foo() . Bis wir type Foo = impl Foo; zum System hinzufügen, sind Typaliase eher referenziell transparent. Umgekehrt, wenn es bereits eine Bindung x = foo() verfügbar, dann andere foo() in austauschbar mit x .

@joshtriplett

"unvollständig" muss nicht bedeuten, dass es an Informationen mangelt, es kann bedeuten, dass etwas so aussieht, als ob es etwas anderes haben sollte und es nicht hat.

Meinetwegen; aber was soll es haben, was es nicht hat?

type Foo: Trait; sieht nicht wie eine vollständige Deklaration aus.

Sieht für mich komplett aus. Es sieht aus wie ein Urteil, dass Foo Trait erfüllt, was genau die beabsichtigte Bedeutung ist.

@Centril Für mich ist "etwas, das fehlt" der tatsächliche Typ, für den dies ein Alias ​​ist. Das hängt ein bisschen mit meiner Verwirrung von vorhin zusammen. Es ist nicht so, dass es keinen solchen Typ gibt, nur dass dieser Typ anonym ist... Die Verwendung von = impliziert subtil, dass es einen Typ gibt und es immer der gleiche Typ ist, aber wir können ihn nicht benennen.

Ich denke, wir sind mit diesen Argumenten irgendwie erschöpft. Es wäre toll, einfach beide Syntaxen experimentell zu implementieren und zu sehen, was am besten funktioniert.

@mark-im

@Centril Für mich ist "etwas, das fehlt" der tatsächliche Typ, für den dies ein Alias ​​ist. Das hängt ein bisschen mit meiner Verwirrung von vorhin zusammen. Es ist nicht so, dass es keinen solchen Typ gibt, nur dass dieser Typ anonym ist... Die Verwendung von = impliziert subtil, dass es einen Typ gibt und es immer der gleiche Typ ist, aber wir können ihn nicht benennen.

Genau so fühlt es sich auch für mich an.

Gibt es eine Chance, die beiden aufgeschobenen Gegenstände bald anzugehen, plus das Problem der lebenslangen Elisierung? Ich würde es selbst machen, habe aber keine Ahnung wie!

Es herrscht immer noch viel Verwirrung darüber, was genau impl Trait bedeutet, und es ist überhaupt nicht offensichtlich. Ich denke, die zurückgestellten Elemente sollten definitiv warten, bis wir eine klare Vorstellung von der genauen Semantik von impl Trait (die bald kommen sollte).

@varkor Welche Semantik ist unklar? AFAIK hat sich seit RFC 1951 nichts an der Semantik von impl Trait geändert und 2071 erweitert.

@alexreg Das hatte ich nicht vor, aber hier ist eine grobe Übersicht: Nachdem das Parsen hinzugefügt wurde, müssen Sie die Typen von static s und const s innerhalb einer existenziellen Impl Trait-Kontext, wie hier für die Rückgabetypen von Funktionen. . Sie sollten jedoch DefId in ImplTraitContext::Existential optional machen, da Sie nicht möchten, dass Ihr impl Trait Generika von einer übergeordneten Funktionsdefinition übernimmt. Damit solltest du ein gutes Stück weiterkommen. Sie könnten es leichter haben, wenn Sie auf dem existenziellen Typ PR von @oli-obk aufbauen.

@cramertj : Die Semantik von impl Trait in der Sprache ist vollständig auf ihre Verwendung in Funktionssignaturen beschränkt und es stimmt nicht, dass eine Erweiterung auf andere Positionen eine offensichtliche Bedeutung hat. Ich werde demnächst etwas Genaueres dazu sagen, wo die meisten Gespräche zu laufen scheinen.

@varkor

Die Semantik von Impl Trait in der Sprache ist vollständig auf seine Verwendung in Funktionssignaturen beschränkt, und es stimmt nicht, dass eine Erweiterung auf andere Positionen eine offensichtliche Bedeutung hat.

Die Bedeutung wurde in RFC 2071 festgelegt .

@cramertj : Die Bedeutung in RFC 2071 ist mehrdeutig und erlaubt mehrere Interpretationen dessen, was der Ausdruck "existenzieller Typ" dort bedeutet.

TL;DR — Ich habe versucht, eine genaue Bedeutung für impl Trait , die meiner Meinung nach Details klärt, die zumindest intuitiv unklar waren; zusammen mit einem Vorschlag für eine neue Typalias-Syntax.

Existenzielle Typen in Rust (Beitrag)


Im Discord rost-lang Chat wurde in den letzten Tagen viel über die genaue (dh formale, theoretische) Semantik von impl Trait diskutiert. Ich denke, es war hilfreich, viele Details über das Feature zu klären und genau zu klären, was es ist und was nicht. Sie gibt auch Aufschluss darüber, welche Syntaxen für Typaliase plausibel sind.

Ich habe eine kleine Zusammenfassung einiger unserer Schlussfolgerungen geschrieben. Dies bietet eine Interpretation von impl Trait die meiner Meinung nach ziemlich sauber ist, und beschreibt genau die Unterschiede zwischen der Argumentposition impl Trait und der Rückgabeposition impl Trait (was nicht "universal- quantifiziert" vs "existentiell quantifiziert"). Es gibt auch einige praktische Schlussfolgerungen.

Darin schlage ich eine neue Syntax vor, die die häufig gestellten Anforderungen eines "existenziellen Typalias" erfüllt:
type Foo: Bar = _;

Da es sich um ein so komplexes Thema handelt, gibt es jedoch noch einiges zu klären, daher habe ich es als separaten Beitrag geschrieben. Feedback wird sehr geschätzt!

Existenzielle Typen in Rust (Beitrag)

@varkor

RFC 2071 ist mehrdeutig und lässt mehrere Interpretationen dessen zu, was der Begriff „existenzieller Typ“ dort bedeutet.

Wie ist es mehrdeutig? Ich habe Ihren Beitrag gelesen - mir ist immer noch nur eine Bedeutung von nicht-dynamischem Existential in Statik und Konstanten bekannt. Es verhält sich genauso wie die Rückgabeposition impl Trait , indem pro Element eine neue existenzielle Typdefinition eingeführt wird.

type Foo: Bar = _;

Wir haben diese Syntax während RFC 2071 diskutiert. Wie ich dort sagte, gefällt mir, dass sie klar zeigt, dass Foo ein einzelner abgeleiteter Typ ist und Raum für nicht abgeleitete Typen lässt, die außerhalb des aktuellen Moduls existentiell belassen werden ( zB type Foo: Bar = u32; ). Ich mochte zwei Aspekte davon nicht: (1) Es hat kein Schlüsselwort und ist daher schwieriger zu suchen und (b) es hat das gleiche Ausführlichkeitsproblem im Vergleich zu type Foo = impl Trait wie die abstract type Foo: Bar; Syntax hat: type Foo = impl Iterator<Item = impl Display>; wird zu type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . Ich glaube nicht, dass beides ein Deal-Breaker ist, aber es ist kein klarer Sieg auf die eine oder andere Weise IMO.

@cramertj Die Mehrdeutigkeit kommt hier auf:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Wenn Foo wirklich ein Typalias für einen existenziellen Typ wäre, dann würden f und g verschiedene konkrete Rückgabetypen unterstützen. Mehrere Leute haben diese Syntax instinktiv auf diese Weise gelesen, und tatsächlich haben einige Teilnehmer der RFC 2071-Syntaxdiskussion gerade erst erkannt, dass der Vorschlag im Rahmen der jüngsten Discord-Diskussion nicht so funktioniert .

Das Problem ist, dass, insbesondere angesichts der Argumentposition impl Trait , überhaupt nicht klar ist, wohin der existenzielle Quantor führen soll. Für Argumente ist es eng gefasst; für die Rückkehrposition scheint es eng gefasst zu sein, aber es stellt sich heraus, dass es breiter ist; für type Foo = impl Bar beide Positionen plausibel. Die auf _ basierende Syntax drängt auf eine Interpretation, die nicht einmal »existentiell« beinhaltet, und umgeht dieses Problem geschickt.

Wenn Foo wirklich ein Typalias für einen existenziellen Typ wäre

(Hervorhebung von mir). Ich habe gelesen, dass 'an' als 'speziell' bedeutet, was bedeutet, dass f und g _nicht_ verschiedene konkrete Rückgabetypen unterstützen würden, da sie sich auf denselben existenziellen Typ beziehen. Ich habe immer gesehen, dass type Foo = impl Bar; dieselbe Bedeutung wie let foo: impl Bar; , dh einen neuen anonymen existenziellen Typ einführt; Machen Sie Ihr Beispiel äquivalent zu

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

was ich hoffe, ist relativ eindeutig.


Ein Problem ist, dass die Bedeutung von " impl Trait in Typaliasen" nie in einem RFC angegeben wurde. Es wird kurz im Abschnitt "Alternativen" von RFC 2071 erwähnt, aber aufgrund dieser inhärenten Unklarheiten in der Lehre ausdrücklich abgelehnt.

Ich habe auch das Gefühl, dass einige Erwähnungen gesehen haben, dass Typaliase bereits nicht referenziell transparent sind. Ich glaube, es war auf u.rl.o, aber ich konnte die Diskussion nach einigem Suchen nicht finden.

@cramertj
Um an @rpjohnst anknüpfen zu impl Trait , die alle mit der aktuellen Verwendung in Signaturen übereinstimmen, aber unterschiedliche Konsequenzen haben, wenn impl Trait auf andere erweitert wird (Ich kenne 2 andere als die im Beitrag beschriebene, die aber noch nicht ganz zur Diskussion stehen). Und ich glaube nicht, dass die Interpretation im Beitrag unbedingt die offensichtlichste ist (ich persönlich habe keine ähnliche Erklärung zu APIT und RTIP aus dieser Perspektive gesehen).

Was die type Foo: Bar = _; , so denke ich, sollte sie vielleicht noch einmal diskutiert werden – es schadet nicht, alte Ideen mit neuen Augen zu überdenken. Zu deinen Problemen damit:
(1) Es hat kein Schlüsselwort, aber es hat überall dieselbe Syntax wie die Typinferenz. Die Suche in der Dokumentation nach "underscore" / "underscore type" / etc. könnte leicht eine Seite zum Typrückschluss liefern.
(2) Ja, das stimmt. Wir haben über eine Lösung nachgedacht, die meiner Meinung nach gut zu der Unterstrich-Notation passt, die hoffentlich bald vorgeschlagen werden kann.

Wie @cramertj sehe ich das Argument hier nicht wirklich.

Ich sehe einfach nicht die grundlegende Mehrdeutigkeit, die der Beitrag von @varkor beschreibt. Ich denke, wir haben "existenzieller Typ" in Rust immer als "es existiert ein _einzigartiger_ Typ, der ..." interpretiert und nicht als "es gibt mindestens einen Typ, der ..." weil (wie in @varkors Beitrag heißt) der Letzteres ist äquivalent zu "universellen Typen", und daher wäre der Ausdruck "existenzieller Typ" völlig nutzlos, wenn wir diese Interpretation zulassen wollten. afaik hat jeder RFC zu diesem Thema immer angenommen, dass universelle und existenzielle Typen zwei verschiedene Dinge sind. Ich verstehe, dass dies in der tatsächlichen Typtheorie gemeint ist und dass Isomorphismus mathematisch sehr real ist, aber für mich ist das nur ein Argument, dass wir die Terminologie der Typtheorie missbraucht haben und dafür einen anderen Jargon wählen müssen, kein Argument, das die beabsichtigte Semantik von impl Trait war immer unklar und muss überdacht werden.

Die von @rpjohnst beschriebene Mehrdeutigkeit des type Foo: Bar = _; Problem von type Foo: Bar; zu lösen scheint, eine Explosion von mehreren Aussagen zu benötigen, um etwas nicht-triviales existentiell zu deklarieren, aber ich denke nicht, dass das ausreicht, um das wirklich zu ändern "unendliche Fahrradschuppen"-Situation.

Wovon ich überzeugt bin, ist, dass jede Syntax, die wir am Ende haben, ein anderes Schlüsselwort als type , weil all die "nur type " -Syntaxen zu irreführend sind. Verwenden Sie type in der Syntax vielleicht gar nicht _überhaupt_, damit niemand davon ausgehen kann, dass es sich um einen "Typalias, aber irgendwie existentieller" handelt.

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Es ist für mich nicht offensichtlich, dass diese die Fehlinterpretation, dass f und g zwei verschiedene Typen zurückgeben könnten, die Trait implementieren, vollständig _verhindern_, aber ich vermute, dass dies der Prävention so nahe kommt wie könnten wir evtl.

@Ixrec
Der Ausdruck "existenzieller Typ" ist insbesondere _wegen_ der Mehrdeutigkeit des Umfangs problematisch. Ich habe niemanden gesehen, der darauf hingewiesen hat, dass der Geltungsbereich für APIT und RPIT völlig unterschiedlich ist. Dies bedeutet, dass eine Syntax wie type Foo = impl Bar , wobei impl Bar ein "existentieller Typ" ist, von Natur aus mehrdeutig ist.

Ja, die Terminologie der Typentheorie wurde oft missbraucht. Aber es wurde im RFC missbraucht (oder zumindest nicht erklärt) – daher gibt es Mehrdeutigkeiten, die sich aus dem RFC selbst ergeben.

Die von @rpjohnst beschriebene Mehrdeutigkeit des

Nein, ich glaube nicht, dass das stimmt. Es ist möglich, eine konsistente Syntax zu entwickeln, die diese Verwirrung nicht aufweist. Ich würde den Fahrradabwurf wagen, weil die beiden aktuellen Vorschläge schlecht sind, also niemanden wirklich zufriedenstellen.

Ich bin davon überzeugt, dass jede Syntax, die wir am Ende haben, ein anderes Schlüsselwort als type

Das halte ich auch nicht für nötig. In Ihren Beispielen haben Sie eine völlig neue Notation erfunden, die Sie im Sprachdesign nach Möglichkeit vermeiden möchten – andernfalls erstellen Sie eine riesige Sprache voller inkonsistenter Syntax. Sie sollten eine völlig neue Syntax nur dann erkunden, wenn es keine besseren Optionen gibt. Und ich behaupte, dass es eine bessere Option _gibt_.

Nebenbei: Ich halte es für möglich, sich ganz von "Existenztypen" zu lösen, um die ganze Situation klarer zu machen, was ich oder jemand anderes demnächst nachgehen werde.

Ich denke, dass auch eine andere Syntax als type helfen würde, gerade weil viele Leute type als einfachen austauschbaren Alias ​​interpretieren, was die Interpretation "potenziell anderer Typ jedes Mal" implizieren würde.

Ich habe niemanden gesehen, der darauf hingewiesen hat, dass der Geltungsbereich für APIT und RPIT völlig unterschiedlich ist.

Ich dachte, das Scoping sei immer ein expliziter Teil der Vorschläge für implizite Trait-Eigenschaften, daher brauchte es nicht "hervorgehoben" zu werden. Alles, was Sie über das Scoping gesagt haben, scheint nur zu wiederholen, was wir bereits in früheren RFCs akzeptiert haben. Ich verstehe, dass es nicht jedem aus der Syntax ersichtlich ist und das ist ein Problem, aber es ist nicht so, als hätte das vorher niemand verstanden. Tatsächlich dachte ich, ein großer Teil der Diskussion über RFC 2701 drehte sich um den Umfang von type Foo = impl Trait; im Sinne dessen, was Typinferenz ist und welche nicht betrachtet werden darf.

Es ist möglich, eine konsistente Syntax zu entwickeln, die diese Verwirrung nicht aufweist.

Versuchen Sie zu sagen, dass type Foo: Bar = _; diese Syntax ist, oder glauben Sie, wir haben sie noch nicht gefunden?

Ich glaube nicht, dass es möglich ist, eine Syntax ohne ähnliche Verwirrung zu entwickeln, nicht weil wir nicht zu kreativ sind, sondern weil die meisten Programmierer keine Typtheoretiker sind. Wir können wahrscheinlich eine Syntax finden, die Verwirrung auf ein erträgliches Maß reduziert, und sicherlich gibt es viele Syntaxen, die für Veteranen der Typtheorie eindeutig wären, aber wir werden Verwirrung nie vollständig beseitigen.

du hast eine ganz neue notation erfunden

Ich dachte, ich hätte nur ein Keyword durch ein anderes ersetzt. Sehen Sie eine zusätzliche Änderung, die ich nicht beabsichtigt habe?

Denken Sie darüber nach, da wir die ganze Zeit "existenziell" missbraucht haben, bedeutet das, dass existential Foo: Trait / = impl Trait wahrscheinlich keine legitimen Syntaxen mehr sind.

Wir brauchen also ein neues Schlüsselwort, um Namen voranzustellen, die sich auf einen unbekannten-zu-externen-Code-Typ beziehen ... und ich ziehe hier eine Lücke. alias , secret , internal usw. scheinen alle ziemlich schrecklich zu sein und es ist unwahrscheinlich, dass sie weniger "Einzigartigkeitsverwirrung" haben als type .

Denken Sie darüber nach, da wir die ganze Zeit "existenziell" missbraucht haben, bedeutet das, dass existential Foo: Trait / = impl Trait wahrscheinlich keine legitimen Syntaxen mehr sind.

Ja, ich stimme voll und ganz zu – ich denke, wir müssen uns vollständig vom Begriff „existentiell“ entfernen* (es gab einige vorläufige Ideen, wie dies zu bewerkstelligen ist, während impl Trait gut erklärt wird).

*(eventuell nur für dyn Trait reserviert)

@joshtriplett , @Ixrec : Ich stimme zu, dass die Notation _ bedeutet, dass Sie nicht mehr im gleichen Umfang ersetzen können, wie Sie es zuvor konnten, und wenn dies eine Priorität ist, die wir beibehalten möchten , bräuchten wir eine andere Syntax.

Denken Sie daran, dass _ ohnehin schon ein Sonderfall in Bezug auf die Ersetzung ist — es betrifft nicht nur Typaliase: Überall, wo Sie derzeit _ , verhindern Sie die vollständige referenzielle Transparenz.

Denken Sie daran, dass _ ohnehin schon ein Sonderfall in Bezug auf die Ersetzung ist — es betrifft nicht nur Typaliase: überall, wo Sie derzeit _ verwenden können, verhindern Sie die vollständige referenzielle Transparenz.

Könnten Sie uns erklären, was das genau bedeutet? Mir war kein Begriff von "referenzieller Transparenz" bekannt, der von _ .

Ich stimme zu, dass die _-Notation bedeutet, dass Sie nicht mehr im gleichen Umfang wie zuvor ersetzen können, und wenn dies eine Priorität ist, würden wir eine andere Syntax benötigen.

Ich bin mir nicht sicher, ob es eine _Priorität_ ist. Für mich war es das einzige objektive Argument, das wir je gefunden haben und das eine Syntax der anderen vorzuziehen schien. Aber das wird sich wahrscheinlich ändern, je nachdem, welche Keywords wir finden können, um type zu ersetzen.

Könnten Sie uns erklären, was das genau bedeutet? Mir war kein Begriff von "referenzieller Transparenz" bekannt, der von _ .

Ja, tut mir leid, ich werfe Worte um mich, ohne sie zu erklären. Lassen Sie mich meine Gedanken sammeln, und ich formuliere eine schlüssigere Erklärung. Es passt gut zu einer alternativen (und möglicherweise hilfreicheren) Betrachtungsweise von impl Trait .

Unter referenzieller Transparenz wird verstanden, dass es möglich ist, eine Referenz für ihre Definition zu ersetzen und umgekehrt, ohne dass sich die Semantik ändert. In Rust gilt dies eindeutig nicht auf Termebene für fn . Beispielsweise:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

wenn wir jedes Vorkommen von bar durch foo() (die Definition von bar ) ersetzen, dann erhalten wir eindeutig eine andere Ausgabe.

Für Typaliase gilt jedoch derzeit die referenzielle Transparenz (AFAIK). Wenn Sie einen Alias ​​haben:

type Foo = Definition;

Dann können Sie das Ersetzen von Vorkommen von Foo für Definition und Ersetzen von Vorkommen von Definition für Foo ohne Änderung der Semantik Ihres Programms durchführen (einfangen vermeiden). , oder seine Typkorrektheit.

Wir stellen vor:

type Foo = impl Bar;

zu bedeuten, dass jedes Vorkommen von Foo vom gleichen Typ ist, bedeutet Folgendes, wenn Sie schreiben:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

Sie können Foo durch impl Bar ersetzen und umgekehrt. Das heißt, wenn Sie schreiben:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

die Rückgabetypen werden nicht mit Foo . Daher wird die referenzielle Transparenz für Typaliase durch die Einführung von impl Trait mit der darin enthaltenen Semantik von RFC 2071 gebrochen.

Über referentielle Transparenz und type Foo = _; , wird fortgesetzt... (von @varkor)

Ich denke, dass auch eine andere Syntax als Typ hilfreich wäre, gerade weil viele Leute Typ als einfachen austauschbaren Alias ​​interpretieren, was die Interpretation "potenziell anderer Typ jedes Mal" implizieren würde.

Guter Punkt. Aber bedeutet das Zuweisungsbit = _ , dass es sich nur um einen einzigen Typ handelt?

Ich habe das schon einmal geschrieben, aber...

Zur referenziellen Transparenz: Ich denke, es ist sinnvoller, type als Bindung (wie let ) zu betrachten, anstatt als C-Präprozessor-ähnliche Substitution. Wenn Sie es so betrachten, bedeutet type Foo = impl Trait genau das, was es zu sein scheint.

Ich kann mir vorstellen, dass Anfänger impl Trait weniger wahrscheinlich als existenzielle vs. universelle Typen betrachten werden, sondern als "eine Sache, die impl Trait . If they want to know more, they can read the impl Trait'-Dokumentation ist Wenn Sie die Syntax ändern, verlieren Sie die Verbindung zwischen dieser und der vorhandenen Funktion ohne großen Nutzen. _Sie ersetzen nur eine potenziell irreführende Syntax durch eine andere._

Zu type Foo = _ , es überlädt _ mit einer völlig unabhängigen Bedeutung. Es kann auch schwierig erscheinen, es in der Dokumentation und/oder bei Google zu finden.

@lnicola Sie könnten genauso gut const Bindungen anstelle von let Bindungen verwenden, wobei erstere referenziell transparent ist. Die Auswahl von let (was in fn zufällig nicht referenziell transparent ist) ist eine willkürliche Wahl, die ich nicht für besonders intuitiv halte. Ich denke, die intuitive Ansicht von Typaliasen besteht darin, dass sie referenziell transparent sind (auch wenn dieses Wort nicht verwendet wird), da es sich um Aliasse handelt .

Ich betrachte type auch nicht als C-Präprozessor-Ersatz, weil es Capture vermeiden und Generika respektieren muss (kein SFINAE). Stattdessen denke ich an type genau wie an eine Bindung in einer Sprache wie Idris oder Agda, in der alle Bindungen rein sind.

Ich kann mir vorstellen, dass Anfänger impl Trait weniger wahrscheinlich als existenzielle vs. universelle Typen betrachten, sondern als "eine Sache, die eine Eigenschaft impliziert".

Das scheint mir eine Unterscheidung ohne Unterschied zu sein. Der Jargon "Existenzial" wird nicht verwendet, aber ich glaube, der Benutzer verbindet ihn intuitiv mit dem gleichen Konzept wie dem eines existenziellen Typs (was nichts anderes ist als "einer Typ Foo, der Bar impliziert" im Kontext von Rust).

Zu type Foo = _ , es überlädt _ mit einer völlig unabhängigen Bedeutung.

Wieso das? type Foo = _; hier der Verwendung von _ in anderen Kontexten, in denen ein Typ erwartet wird.
Es bedeutet "den realen Typ ableiten", genau wie wenn Sie .collect::<Vec<_>>() schreiben.

Es kann auch schwierig erscheinen, es in der Dokumentation und/oder bei Google zu finden.

Sollte nicht so schwer sein? "type alias underscore" sollte hoffentlich das gewünschte Ergebnis bringen...?
Scheint nicht anders zu sein, als nach "type alias Impl Trait" zu suchen.

Google indiziert keine Sonderzeichen. Wenn meine StackOverflow-Frage einen Unterstrich enthält, indiziert Google diesen nicht automatisch für Abfragen, die das Wort Unterstrich enthalten

@Centril

Wieso das? type Foo = _; hier der Verwendung von _ in anderen Kontexten, in denen ein Typ erwartet wird.
Es bedeutet "den realen Typ ableiten", genau wie wenn Sie .collect: schreiben:>().

Aber diese Funktion leitet den Typ nicht ab und gibt Ihnen einen Typalias dafür, es erstellt einen existenziellen Typ, der (außerhalb eines begrenzten Bereichs wie Modul oder Crate) nicht mit "dem echten Typ" übereinstimmt.

Google indiziert keine Sonderzeichen.

Dies ist nicht mehr wahr (obwohl möglicherweise Whitespace-abhängig..?).

Aber diese Funktion leitet den Typ nicht ab und gibt Ihnen einen Typalias dafür, es erstellt einen existenziellen Typ, der (außerhalb eines begrenzten Bereichs wie Modul oder Crate) nicht mit "dem echten Typ" übereinstimmt.

Die vorgeschlagene Semantik von type Foo = _; ist eine Alternative zu einem existenziellen Typ-Alias, die vollständig auf Inferenz basiert. Wenn das nicht ganz klar war, werde ich demnächst etwas nachfassen, das die Absichten etwas besser erklären sollte.

@iopq Zusätzlich zu @varkors Hinweis zu den letzten Änderungen möchte ich auch hinzufügen, dass es für andere Suchmaschinen immer möglich ist, dass offizielle Dokumentationen und dergleichen ausdrücklich das wörtliche Wort "Unterstrich" in Verbindung mit type , sodass es durchsuchbar wird.

Sie werden immer noch keine guten Ergebnisse mit _ in Ihrer Abfrage erhalten, aus welchen Gründen auch immer. Wenn Sie nach Unterstrich suchen, erhalten Sie Dinge, die das Wort Unterstrich enthalten. Wenn du _ suchst, bekommst du alles, was einen Unterstrich hat, also weiß ich nicht einmal, ob es relevant ist

@Centril

Die Auswahl von let (die in fn zufällig nicht referenziell transparent ist) ist eine willkürliche Wahl, die meiner Meinung nach nicht besonders intuitiv ist. Ich denke, die intuitive Ansicht von Typaliasen besteht darin, dass sie referenziell transparent sind (auch wenn dieses Wort nicht verwendet wird), da es sich um Aliasse handelt.

Tut mir leid, ich kann mich immer noch nicht darum kümmern, weil meine Intuition völlig zurückgeblieben ist.

Wenn wir beispielsweise type Foo = Bar , sagt meine Intuition:
"Wir deklarieren Foo , was zum gleichen Typ wie Bar ."

Wenn wir dann type Foo = impl Bar schreiben, sagt meine Intuition:
"Wir deklarieren Foo , was zu einem Typ wird, der Bar implementiert."

Wenn Foo nur ein Textalias für impl Bar , dann wäre das für mich sehr unintuitiv. Ich stelle mir das gerne als textuelle vs. semantische Aliase vor.

Wenn also Foo überall durch impl Bar werden kann, ist das ein Textalias , der für mich am meisten an Makros und Metaprogrammierung erinnert. Aber wenn Foo zum Zeitpunkt der Deklaration eine Bedeutung zugewiesen wurde und an mehreren Stellen mit dieser ursprünglichen Bedeutung (nicht kontextuellen Bedeutung!) verwendet werden kann, ist dies ein semantischer Alias.

Außerdem verstehe ich die Motivation hinter kontextuellen Existenztypen sowieso nicht. Würden sie jemals nützlich sein, wenn man bedenkt, dass Merkmalsaliase genau das gleiche erreichen können?

Vielleicht finde ich referenzielle Transparenz aufgrund meines Nicht-Haskell-Hintergrunds nicht intuitiv, wer weiß... :) Aber auf jeden Fall ist es definitiv nicht die Art von Verhalten, die ich in Rust erwarten würde.

@Nemo157 @stjepang

Wenn Foo wirklich ein Typalias für einen existenziellen Typ wäre

(Hervorhebung von mir). Ich habe gelesen, dass 'an' als 'ein bestimmtes' gelesen wird, was bedeutet, dass f und g keine verschiedenen konkreten Rückgabetypen unterstützen würden, da sie sich auf denselben existenziellen Typ beziehen.

Dies ist ein Missbrauch des Begriffs "existenzieller Typ" oder zumindest ein Weg, der im Widerspruch zu @varkors Beitrag steht. type Foo = impl Bar kann erscheinen, um Foo einem Alias ​​für den Typ ∃ T. T: Trait - und wenn Sie ∃ T. T: Trait überall ersetzen, verwenden Sie Foo , auch nicht -textuell können Sie an jeder Position einen anderen konkreten Typ erhalten.

Der Umfang dieses ∃ T Quantifizierers (in Ihrem Beispiel als existential type _0 ausgedrückt) ist die fragliche Sache. In APIT ist es so eng – der Aufrufer kann jeden Wert übergeben, der ∃ T. T: Trait erfüllt. Aber es ist nicht in RPIT und nicht in den existential type Deklarationen von RFC 2071 und nicht in Ihrem Entzuckerungsbeispiel – dort ist der Quantor weiter draußen, auf der Ebene der gesamten Funktion oder des gesamten Moduls, und Sie befassen sich mit dem überall die gleichen T .

Also die Mehrdeutigkeit - wir haben bereits impl Trait , die ihren Quantor je nach Position an verschiedenen Stellen platzieren. Welchen sollten wir also für type T = impl Trait erwarten? Einige informelle Umfragen sowie einige nachträgliche Erkenntnisse von Teilnehmern des RFC 2071-Threads beweisen, dass dies so oder so nicht klar ist.

Aus diesem Grund wollen wir uns von der Interpretation von impl Trait als alles , was mit Existenzialien zu tun hat, entfernen und stattdessen seine Semantik als Typinferenz beschreiben. type T = _ hat nicht die gleiche Art von Mehrdeutigkeit - es gibt immer noch die Oberflächenebene "kann das _ nicht kopieren und einfügen anstelle von T ", aber es gibt es nicht mehr "Der einzelne Typ , für den T ein Alias ​​ist, kann mehrere konkrete Typen bedeuten." (Das undurchsichtige/nicht vereinheitlichende Verhalten ist die Sache, über die @varkor spricht.)

referenzielle Transparenz

Nur weil ein Typalias derzeit mit referenzieller Transparenz kompatibel ist, bedeutet dies nicht, dass die Leute

Zum Beispiel ist das Element const referenziell transparent (erwähnt in https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), und das führte tatsächlich zu Verwirrung bei Neu und Alt Benutzer (rust-lang-nursery/rust-clippy#1560).

Daher denke ich, dass referenzielle Transparenz für einen Rust-Programmierer nicht das Erste ist, woran er denkt.

@stjepang @kennytm Ich sage nicht, dass jeder erwarten wird, dass Typaliase mit type Foo = impl Trait; referenziell transparent agieren. Aber ich denke, eine nicht triviale Anzahl von Benutzern wird dies tun , wie die Verwirrung in diesem Thread und anderswo zeigt ( @rpjohnst bezieht ...). Das ist ein Problem, aber vielleicht kein unüberwindbares. Es ist etwas, das wir im Auge behalten sollten, wenn wir voranschreiten.

Meine derzeitigen Überlegungen, was in dieser Angelegenheit zu tun ist, haben sich mit @varkor und @rpjohnst in Einklang gebracht.

Re: Referenzielle Transparenz

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

das heißt, selbst die Generierung neuer Typen bei jeder Instanz ist im Kontext von generischen Typaliasen nicht referenziell transparent.

@centril Ich hebe meine Hand, wenn es darum geht, referenzielle Transparenz für Foo in type Foo = impl Bar; zu erwarten. Bei type Foo: Bar = _; würde ich jedoch keine referentielle Transparenz erwarten.

Es ist auch möglich, die Rückgabeposition impl Trait zu erweitern, um mehrere Typen zu unterstützen, ohne irgendeinen enum impl Trait ähnlichen Mechanismus, indem wir (Teile von) des Aufrufers monomorphisieren. Dies verstärkt die Interpretation " impl Trait ist immer existentiell", bringt sie näher an dyn Trait und schlägt eine abstract type Syntax vor, die nicht impl Trait überhaupt.

Ich habe dies hier auf Internas geschrieben: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Nur eine Anmerkung, wenn wir die neuen Existenzialtypen stabilisieren - "Existenz" war immer als temporäres Schlüsselwort gedacht (laut RFC) und (IMO) ist schrecklich. Wir müssen uns etwas Besseres einfallen lassen, bevor wir uns stabilisieren.

Das Gerede über „existentielle“ Typen scheint die Dinge nicht zu klären. Ich würde sagen, dass impl Trait für einen bestimmten abgeleiteten Typ steht, der eine Eigenschaft implementiert. So beschrieben ist type Foo = impl Bar eindeutig ein spezifischer, immer derselbe Typ – und das ist auch die einzige wirklich nützliche Interpretation: Es kann also auch in anderen Kontexten verwendet werden, als dem, aus dem es abgeleitet wurde, wie in structs.

In diesem Sinne wäre es sinnvoll, impl Trait als _ : Trait zu schreiben.

@rpjohnst ,

Es ist auch möglich, dass wir die Rückgabeposition impl Trait , um mehrere Typen zu unterstützen

Das würde es streng weniger nützlich machen IMO. Der Sinn von Aliasen für impl -Typen besteht darin, dass eine Funktion so definiert werden kann, dass sie impl Foo zurückgibt, aber der spezifische Typ wird immer noch durch das Programm in anderen Strukturen und so weiter verbreitet. Das würde funktionieren, wenn der Compiler implizit geeignete enum generiert, jedoch nicht mit Monomorphisierung.

@jan-hudec Diese Ideen sind in der Diskussion auf Discord aufgekommen, und es gibt einige Probleme, die hauptsächlich auf der Tatsache beruhen, dass die aktuelle Interpretation von Return-Position und Argument-Position impl Trait inkonsistent ist.

impl Trait für einen bestimmten abgeleiteten Typ stehen zu lassen, ist eine gute Option, aber um diese Inkonsistenz zu beheben, muss es eine andere Art von Typinferenz sein, als Rust heute hat – es muss polymorphe Typen ableiten, damit es erhalten bleiben kann das aktuelle Verhalten von argument-position impl Trait . Dies ist wahrscheinlich der einfachste Weg, aber es ist nicht so einfach, wie Sie sagen.

Wenn beispielsweise impl Trait bedeutet, "verwenden Sie diesen neuen Inferenztyp, um einen möglichst polymorphen Typ zu finden, der Trait implementiert", beginnt type Foo = impl Bar Dinge über Module zu implizieren. Die RFC 2071-Regeln zum Ableiten eines abstract type sagen, dass alle Verwendungen unabhängig denselben Typ ableiten müssen, aber diese polymorphe Inferenz würde zumindest implizieren, dass mehr möglich ist. Und wenn wir jemals parametrisierte Module bekommen würden (sogar nur über Lebenszeiten, eine weitaus plausiblere Idee), würden Fragen zu dieser Interaktion auftauchen.

Es gibt auch die Tatsache, dass manche Leute die type Foo = impl Bar Syntax immer als Alias ​​für ein Existenzial interpretieren, unabhängig davon, ob sie das Wort "Existenzial" verstehen und unabhängig davon, wie wir es lehren. Daher ist es wahrscheinlich immer noch eine gute Idee, eine alternative Syntax auszuwählen, selbst wenn sie mit der inferenzbasierten Interpretation funktioniert.

Obwohl die _: Trait Syntax eigentlich die Diskussion um die inferenzbasierte Interpretation inspiriert hat, tut sie nicht das, was wir wollen. Erstens ist die durch _ implizierte Schlussfolgerung nicht polymorph, daher ist dies eine schlechte Analogie zum Rest der Sprache. Zweitens bedeutet _ , dass der tatsächliche Typ an anderer Stelle sichtbar ist, während impl Trait speziell dafür entwickelt wurde, den tatsächlichen Typ zu verbergen.

Schließlich habe ich diesen Monomorphisierungsvorschlag geschrieben, um einen anderen Weg zu finden, um die Bedeutung von Argument und Rückgabeposition impl Trait zu vereinen. Und obwohl es ja bedeutet, dass -> impl Trait keinen einzigen konkreten Typ mehr garantiert, haben wir derzeit sowieso keine Möglichkeit, dies zu nutzen. Und die vorgeschlagenen Lösungen sind alles nervige Workarounds – zusätzliche Boilerplate abstract type Tricks, typeof usw. Jeder, der sich auf das Verhalten eines einzelnen Typs verlassen möchte, zwingt ihn, diesen einzelnen Typ auch über die abstract type zu benennen abstract type Syntax (was immer es auch sein mag) ist insgesamt wohl ein Vorteil.

Diese Ideen sind in der Diskussion auf Discord aufgetaucht, und es gibt einige Probleme, die hauptsächlich auf der Tatsache beruhen, dass die aktuelle Interpretation von Return-Position und Argument-Position impl Trait inkonsistent ist.

Ich persönlich finde diese Inkonsistenz in der Praxis kein Problem. Der Umfang, in dem konkrete Typen für Argumentposition vs. Rückgabeposition vs. Typposition bestimmt werden, scheint ziemlich intuitiv zu funktionieren.

Ich habe eine Funktion, bei der der Aufrufer seinen Rückgabetyp bestimmt. Natürlich kann ich dort keine Impl-Eigenschaft verwenden. Es ist nicht so intuitiv, wie Sie meinen, bis Sie den Unterschied verstanden haben.

Ich persönlich finde diese Inkonsistenz in der Praxis kein Problem.

In der Tat. Dies deutet für mich nicht darauf hin, dass wir die Inkonsistenz ignorieren sollten, sondern dass wir das Design erneut erklären sollten, damit es konsistent ist (z. B. indem wir es als polymorphe Typinferenz erklären). Auf diese Weise zukünftige Erweiterungen (RFC 2071, etc.) können gegen die neue, einheitliche Auslegung überprüft werden , um Dinge zu verhindern , dass verwirrend.

@rpjohnst

Es ist wohl insgesamt ein Vorteil, jeden, der sich auf das Verhalten eines einzelnen Typs verlassen möchte, zu zwingen, diesen einzelnen Typ auch über die abstrakte Typsyntax (was immer es sein mag) zu benennen.

In einigen Fällen stimme ich diesem Gefühl zu, aber es funktioniert nicht mit Verschlüssen oder Generatoren und ist für viele Fälle unergonomisch, in denen es Ihnen egal ist, was der Typ ist und Sie sich nur darum kümmern, dass es eine bestimmte Eigenschaft implementiert , zB mit Iterator-Kombinatoren.

@mikeyhew Sie verstehen abstract type Syntax zu erfinden . Sie müssen einen Namen erfinden, unabhängig davon, ob Sie den einzelnen Typ an anderer Stelle verwenden möchten.

@rpjohnst oh ich

Warte ängstlich auf let x: impl Trait .

Als weitere Stimme für let x: impl Trait werden einige der futures Beispiele vereinfacht. Hier ist ein Beispielbeispiel , derzeit wird eine Funktion verwendet, nur um die Möglichkeit zu erhalten, impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

stattdessen könnte dies als normale let-Bindung geschrieben werden:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Wenn gewünscht, kann ich jemanden durch die Implementierung von let x: impl Trait betreuen. Es ist nicht unmöglich zu tun, aber definitiv auch nicht einfach. Ein Einstiegspunkt:

Ähnlich wie wir den Rückgabetyp Impl Trait in https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 besuchen, müssen wir den Typ der Locals in https . besuchen

Wenn Sie dann die Art von Einheimischen besuchen, stellen Sie sicher, dass ExistentialContext auf Return , um es tatsächlich zu aktivieren.

Damit sollten wir schon sehr weit kommen. Ich bin mir nicht sicher, ob es nicht zu 100% der impliziten Eigenschaft der Return-Position entspricht, sollte sich aber meistens so verhalten.

@rpjohnst ,

Diese Ideen sind in der Diskussion auf Discord aufgekommen, und es gibt einige Probleme, die hauptsächlich auf der Tatsache beruhen, dass die derzeitige Interpretation von Return-Position und Argument-Position-Impl-Eigenschaft inkonsistent ist.

Bringt uns zurück zu den Anwendungsbereichen, über die Sie in Ihrem Artikel gesprochen haben. Und ich denke, sie entsprechen tatsächlich der einschließenden „Klammer“: für Argumentposition ist es die Argumentliste, für Rückgabeposition ist es die Funktion – und für den Alias ​​wäre es der Gültigkeitsbereich, in dem der Alias ​​definiert ist.

Ich habe einen RFC eröffnet, der eine Lösung für die konkrete Syntax von existential type vorschlägt, basierend auf der Diskussion in diesem Thread, dem ursprünglichen RFC und synchronen Diskussionen: https://github.com/rust-lang/rfcs/pull /2515.

Die aktuelle Existenz Typ Implementierung kann nicht alles aktuelle Rückkehrposition verwendet werden , um impl Trait Definitionen, da impl Trait jedes Captures generisches Typargument selbst wenn sie nicht benutzt es möglich sein soll , das gleiche zu tun mit existential type , aber Sie erhalten Warnungen über nicht verwendete Typparameter: (Playground)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Dies kann von Bedeutung sein, da die Typparameter interne Lebensdauern haben können, die die Lebensdauer der zurückgegebenen impl Trait einschränken, obwohl der Wert selbst nicht verwendet wird. Entfernen Sie <T> aus Bar im Playground oben, um zu sehen, dass der Aufruf von foo fehlschlägt, aber bar funktioniert.

Die aktuelle Implementierung des existenziellen Typs kann nicht verwendet werden, um alle aktuellen Impl-Eigenschaftsdefinitionen für die Rückgabeposition darzustellen

Sie können, es ist nur sehr unpraktisch. Sie können einen neuen Typ mit einem PhantomData Feld + aktuellem Datenfeld zurückgeben und das Merkmal als Weiterleitung an das aktuelle Datenfeld implementieren

@oli-obk Danke für die zusätzlichen Ratschläge. Mit Ihren vorherigen Ratschlägen und einigen von @cramertj könnte ich es wahrscheinlich in Kürze

@fasihrana @Nemo157 Siehe oben. Vielleicht in ein paar Wochen! :-)

Kann jemand erklären , dass das Verhalten von existential type nicht Typ Parameter implizit Erfassung (das @ Nemo157 erwähnt) ist beabsichtigt und wird bleiben , wie es ist? Ich mag es, weil es #42940 löst

Ich habe es ganz bewusst so umgesetzt

@Arnavion Ja, das ist beabsichtigt und entspricht der Art und Weise, wie andere

Wurde die Interaktion zwischen existential_type und never_type bereits besprochen?

Vielleicht sollte ! in der Lage sein, jeden existenziellen Typ auszufüllen, unabhängig von den beteiligten Eigenschaften.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Oder soll es einen besonderen, unantastbaren Typ geben, der als Typ-Level unimplemented!() dient und automatisch jeden existenziellen Typ befriedigen kann?

@vi Ich denke, das würde unter den allgemeinen

Gibt es einen Plan, die Unterstützung bald auf die Rückgabetypen von Trait-Methoden auszuweiten?

existential type funktioniert bereits für Merkmalsmethoden. Wrt impl Trait , ist das überhaupt durch einen RFC abgedeckt?

@alexreg Ich glaube, dies erfordert, dass GATs in der Lage sein müssen, einen anonymen zugeordneten Typ zu entzuckern, wenn Sie etwas wie fn foo<T>(..) -> impl Bar<T> (wird ungefähr -> Self::AnonBar0<T> ).

@Centril wolltest du dort <T> auf impl Bar machen? Das implizite Typerfassungsverhalten von impl Trait bedeutet, dass Sie selbst bei etwas wie fn foo<T>(self, t: T) -> impl Bar; denselben Bedarf an GATs haben.

@ Nemo157 nein, tut mir leid, ich habe es nicht getan. Aber Ihr Beispiel veranschaulicht das Problem noch besser. Danke schön :)

@alexreg Ich glaube, das erfordert, dass GATs in der Lage sein müssen, einen anonymen zugeordneten Typ zu deaktivieren, wenn Sie so etwas wie fn foo haben(..) -> Impl Bar(wird grob -> Self::AnonBar0).

Ah ich sehe. Um ehrlich zu sein, klingt es nicht unbedingt notwendig, aber es ist sicherlich eine Möglichkeit, es umzusetzen. Etwas beunruhigend ist mir allerdings die Bewegungsarmut auf GATs... schon lange nichts mehr gehört.

Triage: https://github.com/rust-lang/rust/pull/53542 wurde zusammengeführt, sodass die Häkchen für {let,const,static} foo: impl Trait , denke ich, überprüft werden können.

Werde ich jemals schreiben können:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Wahrscheinlich nicht. Aber es gibt laufende Pläne, alles vorzubereiten, damit wir es schaffen können

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Sie können mit dieser Funktion nachts experimentieren in Form von

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Ein guter Anfang, um mehr darüber zu erfahren, ist https://github.com/rust-lang/rfcs/pull/2071 (und alles, was daraus verlinkt ist)

@oli-obk auf rustc 1.32.0-nightly (00e03ee57 2018-11-22) , ich muss auch die Merkmalsgrenzen für existential type angeben, um in einem solchen impl Block zu arbeiten. Ist das zu erwarten?

@jonhoo in der Lage zu sein, die Eigenschaften anzugeben, ist nützlich, da Sie mehr als nur die erforderlichen Eigenschaften angeben können

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

Die erforderlichen Merkmale könnten implizit zu einem existenziell assoziierten Typ hinzugefügt werden, sodass Sie dort nur Grenzen benötigen, wenn Sie sie erweitern, aber ich persönlich würde die lokale Dokumentation vorziehen, sie in die Implementierung einbinden zu müssen.

@Nemo157 Ah, Entschuldigung, was ich meinte, ist, dass Sie derzeit dort Grenzen _muss_. Das heißt, dies wird nicht kompiliert:

impl A for B {
    existential type Assoc;
    // ...
}

in der Erwägung, dass dies:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Oh, also selbst in dem Fall, in dem ein Merkmal keine Grenzen des zugeordneten Typs erfordert, müssen Sie dem existenziellen Typ (der leer sein kann) immer noch eine Grenze zuweisen ( Spielplatz ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Dies scheint mir ein Randfall zu sein, da ein grenzenloser existentieller Typ bedeutet, dass er den Benutzern _keine_ Operationen bietet (außer Auto-Eigenschaften), also wofür könnte er verwendet werden?

Bemerkenswert ist auch, dass es keine Möglichkeit gibt, dasselbe mit -> impl Trait zu tun, -> impl () ist ein Syntaxfehler und -> impl allein ergibt error: at least one trait must be specified ; Wenn die Syntax des existenziellen Typs type Assoc = impl Debug; oder ähnlich wird, scheint es keine Möglichkeit zu geben, den zugeordneten Typ ohne mindestens eine Merkmalsgrenze anzugeben.

@ Nemo157 ja, ich habe es nur gemerkt, weil ich buchstäblich den Code ausprobiert habe, den Sie oben vorgeschlagen haben, und es hat nicht funktioniert: p Ich bin irgendwie davon ausgegangen, dass es die Grenzen aus dem Merkmal ableiten würde. Beispielsweise:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Es schien vernünftig, Future<Output = u32> ein zweites Mal angeben zu müssen, aber das funktioniert nicht. Ich gehe davon aus, dass existential type Assoc: ; (was auch wie eine super seltsame Syntax aussieht) diese Schlussfolgerung auch nicht macht?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Dies scheint mir ein Randfall zu sein, da ein grenzenloser existentieller Typ bedeutet, dass er den Benutzern _keine_ Operationen bietet (außer Auto-Eigenschaften), also wofür könnte er verwendet werden?

Könnten diese nicht für den Konsum in derselben Eigenschaftsimplementierung verwendet werden? Etwas wie das:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

Es ist ein bisschen konstruiert, aber es könnte nützlich sein - ich könnte mir eine Situation vorstellen, in der aus Gründen der Lebensdauer ein vorläufiger Teil vor der eigentlichen Struktur erstellt werden muss. Oder es könnte etwas sein wie:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

In beiden Fällen wäre existential type Assoc; nützlich.

Was ist der richtige Weg, um verknüpfte Typen für Impl Trait zu definieren?

Wenn ich beispielsweise ein Action Merkmal habe und sicherstellen möchte, dass die Implementierung des zugehörigen Typs des Merkmals sendbar ist, kann ich Folgendes tun:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

Ist sowas aktuell nicht möglich?

@acycliczebra Ich denke, die Syntax dafür ist -> impl Action<Result = impl Send> - dies ist die gleiche Syntax wie beispielsweise -> impl Iterator<Item = u32> nur mit einem anderen anonymen impl Trait Typ.

Gab es eine Diskussion über die Erweiterung der impl Trait Syntax auf Dinge wie Strukturfelder? Wenn ich beispielsweise einen Wrapper um einen bestimmten Iteratortyp für meine öffentliche Schnittstelle implementiere:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

Es wäre in Situationen nützlich, in denen mir der tatsächliche Typ egal ist, solange er bestimmte Merkmalsgrenzen erfüllt. Dieses Beispiel ist einfach, aber ich bin in der Vergangenheit auf Situationen gestoßen, in denen ich sehr lange Typen mit einer Reihe von verschachtelten Typparametern schreibe, und es ist wirklich unnötig, weil mich wirklich nichts interessiert, außer dass dies ein ExactSizeIterator .

Allerdings IIRC, ich glaube nicht, dass es im Moment eine Möglichkeit gibt, mehrere Grenzen mit impl Trait anzugeben, daher würde ich einige nützliche Dinge wie Clone verlieren.

@AGausmann Die neueste Diskussion zum Thema findet sich in https://github.com/rust-lang/rfcs/pull/2515. Dies würde Ihnen erlauben, type Foo = impl Bar; struct Baz { field: Foo } ... zu sagen. Ich denke, wir sollten field: impl Trait als Zucker betrachten, nachdem wir type Foo = impl Bar; stabilisiert haben. Es fühlt sich an wie eine vernünftige makrofreundliche Komforterweiterung.

@Centril ,

Ich denke, wir sollten field: impl Trait als Zucker betrachten

Ich glaube nicht, dass dies vernünftig wäre. Ein struct-Feld muss immer noch einen konkreten Typ haben, also müssen Sie dem Compiler mitteilen, an welche Funktion es gebunden ist. Es könnte daraus schließen, aber wenn Sie mehrere Funktionen haben, wäre es nicht so einfach, herauszufinden, welche es ist – und die übliche Richtlinie von Rust besteht darin, in solchen Fällen explizit zu sein.

Es könnte daraus schließen, aber wenn Sie mehrere Funktionen haben, wäre es nicht so einfach zu finden, welche es ist

Sie würden die Anforderung zum Definieren von Verwendungen für den übergeordneten Typ aufblähen. Es wären dann alle diese Funktionen im selben Modul, die den Elterntyp zurückgeben. Scheint mir gar nicht so schwer zu finden. Ich denke jedoch, dass wir die Geschichte auf type Foo = impl Bar; regeln wollen, bevor wir mit Erweiterungen fortfahren.

Ich glaube, ich habe einen Fehler in der aktuellen existential type Implementierung gefunden.


Code

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Fehler

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Spielplatz

Sie finden dies auch auf stackoverflow .

Ich bin mir nicht 100% sicher, ob wir diesen Fall sofort unterstützen können, aber Sie können die Funktion so umschreiben, dass sie zwei generische Parameter hat:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Danke!
Yup, das habe ich wie im Stackoverflow-Beitrag gepostet:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Gibt es Pläne, dass impl Trait in einem Merkmalskontext verfügbar sein wird?
Nicht nur als assoziierter Typ, sondern auch als Rückgabewert in Methoden.

impl trait in traits ist ein separates Feature von den hier verfolgten und hat derzeit keinen RFC. Es gibt eine ziemlich lange Geschichte von Designs in diesem Bereich, und weitere Iterationen werden aufgeschoben, bis die Implementierung von 2071 (existenzieller Typ) stabilisiert ist, was aufgrund von Implementierungsproblemen sowie ungelöster Syntax (die einen separaten RFC hat) blockiert ist.

@cramertj Die Syntax ist fast aufgelöst. Ich glaube, der Hauptblocker ist jetzt GAT.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 wartet noch auf @ withoutboats.

@varkor Ja, ich bin nur optimistisch, dass sie mit diesem RFC bald das Licht sehen werden. ;-)

Wird so etwas möglich sein?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Sie können dies jetzt tun, allerdings nur mit einer hint Funktion, um den konkreten Typ von Interface anzugeben

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

Wie würden Sie es schreiben, wenn der Callback seinen Argumenttyp auswählen könnte? Eigentlich nvm, ich denke, Sie könnten das über ein normales Generikum lösen.

@CryZe Was Sie suchen, hat nichts mit impl Trait tun. Siehe https://github.com/rust-lang/rfcs/issues/2413 für alles, was ich darüber weiß.

Es würde möglicherweise ungefähr so ​​aussehen:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, interessant. Danke!

Das soll funktionieren?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Müssen wir das Schlüsselwort existential in der Sprache für die Funktion existential_type beibehalten?

@jethrogb Ja. Die Tatsache, dass dies derzeit nicht der Fall ist, ist ein Fehler.

@cramertj Okay . Sollte ich dafür ein separates Thema einreichen oder reicht mein Beitrag hier?

Eine Frage zu stellen wäre toll, danke! :)

Müssen wir das Schlüsselwort existential in der Sprache für die Funktion existential_type beibehalten?

Ich denke, die Absicht besteht darin, dies sofort zu verwerfen, wenn das Feature type-alias-impl-trait implementiert (dh in einen Lint) eingefügt wird, und es schließlich aus der Syntax zu entfernen.

Kann aber vielleicht jemand aufklären.

Schließe dies zugunsten eines Meta-Problems, das impl Trait allgemeiner verfolgt: https://github.com/rust-lang/rust/issues/63066

kein einziges gutes Beispiel dafür, wie man Impl Trait verwendet, sehr traurig

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen