Abhängige Methodentypen, die früher ein experimentelles Feature waren, wurden jetzt standardmäßig im Trunk aktiviert , und anscheinend scheint dies in der Scala-Community für Aufregung gesorgt zu haben .
Auf den ersten Blick ist nicht sofort klar, wofür dies nützlich sein könnte. Heiko Seeberger hat ein neues einfaches Beispiel abhängigen Verfahrenstypen hier , die wie im Kommentar sehen kann dort leicht mit Typ - Parameter auf Methoden reproduziert werden kann. Das war also kein sehr überzeugendes Beispiel. (Möglicherweise fehlt mir etwas Offensichtliches. Bitte korrigieren Sie mich, wenn ja.)
Was sind einige praktische und nützliche Beispiele für Anwendungsfälle für abhängige Methodentypen, bei denen sie gegenüber den Alternativen eindeutig vorteilhaft sind?
Welche interessanten Dinge können wir mit ihnen machen, die vorher nicht möglich / einfach waren?
Was kaufen sie uns gegenüber den vorhandenen Systemfunktionen?
Sind abhängige Methodentypen auch analog zu oder lassen sie sich von Merkmalen inspirieren, die in den Typsystemen anderer fortgeschrittener typisierter Sprachen wie Haskell, OCaml zu finden sind?
quelle
Antworten:
Mehr oder weniger kann jede Verwendung von Elementtypen (dh verschachtelten Typen) zu einem Bedarf an abhängigen Methodentypen führen. Insbesondere behaupte ich, dass das klassische Kuchenmuster ohne abhängige Methodentypen eher ein Anti-Muster ist.
Also, was ist das Problem? Verschachtelte Typen in Scala hängen von ihrer umschließenden Instanz ab. Wenn keine abhängigen Methodentypen vorhanden sind, können Versuche, sie außerhalb dieser Instanz zu verwenden, frustrierend schwierig sein. Dies kann Designs, die anfangs elegant und ansprechend erscheinen, in Monstrositäten verwandeln, die albtraumhaft starr und schwer zu überarbeiten sind.
Ich werde das veranschaulichen mit einer Übung , die ich während meiner geben Erweiterte Scala Trainingskurs ,
Es ist ein Beispiel für das klassische Kuchenmuster: Wir haben eine Familie von Abstraktionen, die schrittweise durch eine Hierarchie verfeinert werden (
ResourceManager
/Resource
werden durchFileManager
/ verfeinert,File
die wiederum durchNetworkFileManager
/ verfeinert werdenRemoteFile
). Es ist ein Spielzeugbeispiel, aber das Muster ist real: Es wird im gesamten Scala-Compiler verwendet und wurde im Scala Eclipse-Plugin ausgiebig verwendet.Hier ist ein Beispiel für die verwendete Abstraktion:
Beachten Sie, dass die Pfadabhängigkeit bedeutet, dass der Compiler garantiert, dass die Methoden
testHash
und nur mit Argumenten aufgerufen werden können, die ihr entsprechen, d. H. es ist eigen und sonst nichts.testDuplicates
NetworkFileManager
RemoteFiles
Das ist zweifellos eine wünschenswerte Eigenschaft, aber nehmen wir an, wir wollten diesen Testcode in eine andere Quelldatei verschieben? Mit abhängigen Methodentypen ist es trivial einfach, diese Methoden außerhalb der
ResourceManager
Hierarchie neu zu definieren.Beachten Sie hier die Verwendung abhängiger Methodentypen: Der Typ des zweiten Arguments (
rm.Resource
) hängt vom Wert des ersten Arguments (rm
) ab.Es ist möglich, dies ohne abhängige Methodentypen zu tun, aber es ist äußerst umständlich und der Mechanismus ist ziemlich unintuitiv: Ich unterrichte diesen Kurs seit fast zwei Jahren, und in dieser Zeit hat niemand eine funktionierende Lösung ohne Aufforderung gefunden.
Probieren Sie es aus ...
Nach kurzer Zeit werden Sie wahrscheinlich feststellen, warum ich (oder vielleicht war es David MacIver, wir können uns nicht erinnern, wer von uns den Begriff geprägt hat) dies die Bäckerei des Schicksals nenne.
Bearbeiten: Konsens ist, dass Bakery of Doom David MacIvers Münzprägung war ...
Für den Bonus: Scalas Form von abhängigen Typen im Allgemeinen (und abhängigen Methodentypen als Teil davon) wurde von der Programmiersprache Beta inspiriert ... sie ergeben sich natürlich aus der konsistenten Verschachtelungssemantik von Beta. Ich kenne keine andere, auch nur schwach verbreitete Programmiersprache, die abhängige Typen in dieser Form hat. Sprachen wie Coq, Cayenne, Epigram und Agda haben eine andere Form der abhängigen Typisierung, die in gewisser Weise allgemeiner ist, sich jedoch erheblich dadurch unterscheidet, dass sie Teil von Typsystemen ist, die im Gegensatz zu Scala keine Subtypisierung haben.
quelle
def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")
Ich nehme jedoch an, dass dies als eine andere Form von abhängigen Typen angesehen werden kann.An anderer Stelle können wir statisch garantieren, dass wir keine Knoten aus zwei verschiedenen Graphen verwechseln, z.
Natürlich hat dies bereits funktioniert, wenn es im Inneren definiert wurde
Graph
, aber sagen wir, wir können es nicht ändernGraph
und schreiben eine "pimp my library" -Erweiterung dafür.Zur zweiten Frage: Typen, die durch diese Funktion aktiviert werden, sind weitaus schwächer als vollständig abhängige Typen (eine Beschreibung finden Sie unter Abhängig typisierte Programmierung in Agda .) Ich glaube, ich habe noch nie eine Analogie gesehen.
quelle
Diese neue Funktion wird benötigt, wenn konkrete abstrakte Typelemente anstelle von Typparametern verwendet werden . Wenn Typparameter verwendet werden, kann die Abhängigkeit vom Typ des Familienpolymorphismus in der neuesten und einigen älteren Versionen von Scala ausgedrückt werden, wie im folgenden vereinfachten Beispiel.
quelle
trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}
Ich entwickle ein Modell für die Interoption einer Form der deklarativen Programmierung mit dem Umweltzustand. Die Details sind hier nicht relevant (z. B. Details zu Rückrufen und konzeptioneller Ähnlichkeit mit dem Actor-Modell in Kombination mit einem Serializer).
Das relevante Problem ist, dass Statuswerte in einer Hash-Map gespeichert und durch einen Hash-Schlüsselwert referenziert werden. Funktionen geben unveränderliche Argumente ein, die Werte aus der Umgebung sind, können andere solche Funktionen aufrufen und den Status in die Umgebung schreiben. Funktionen dürfen jedoch keine Werte aus der Umgebung lesen (der interne Code der Funktion hängt also nicht von der Reihenfolge der Zustandsänderungen ab und bleibt daher in diesem Sinne deklarativ). Wie schreibe ich das in Scala?
Die Umgebungsklasse muss über eine überladene Methode verfügen, die eine solche aufzurufende Funktion eingibt und die Hash-Schlüssel der Argumente der Funktion eingibt. Somit kann diese Methode die Funktion mit den erforderlichen Werten aus der Hash-Map aufrufen, ohne öffentlichen Lesezugriff auf die Werte zu gewähren (wodurch Funktionen nach Bedarf die Möglichkeit verweigert werden, Werte aus der Umgebung zu lesen).
Aber wenn diese Hash - Schlüssel - Strings oder Integer - Hash - Werte sind, die statische Typisierung der Hash - Map Elementtyp subsumiert auf einem oder AnyRef (Hash - Map - Code nicht weiter unten) und damit eine Laufzeitkonflikt auftreten könnte, also wäre es möglich , um einen beliebigen Wert in eine Hash-Map für einen bestimmten Hash-Schlüssel einzufügen.
Obwohl ich Folgendes nicht getestet habe, kann ich theoretisch die Hash-Schlüssel zur Laufzeit aus Klassennamen abrufen
classOf
, sodass ein Hash-Schlüssel ein Klassenname anstelle einer Zeichenfolge ist (mithilfe von Scalas Backticks kann eine Zeichenfolge in einen Klassennamen eingebettet werden).So wird statische Sicherheit erreicht.
quelle
def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A
. Wir würden keine Sammlung von Argumentschlüsseln verwenden, da die Elementtypen im Kompilierungstyp subsumiert würden (zur Kompilierungszeit unbekannt).