Was sind einige überzeugende Anwendungsfälle für abhängige Methodentypen?

127

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?

fehlender Faktor
quelle
Sie könnten daran interessiert sein, haskell.org/haskellwiki/Dependent_type
Dan Burton
Danke für den Link, Dan! Ich kenne abhängige Typen im Allgemeinen, aber das Konzept der abhängigen Methodentypen ist für mich relativ neu.
fehlender Faktor
Es scheint mir, dass "abhängige Methodentypen" einfach Typen sind, die von einem oder mehreren Eingabetypen einer Methode abhängig sind (einschließlich des Typs des Objekts, für das die Methode aufgerufen wird); nichts Verrücktes dort außer der allgemeinen Vorstellung von abhängigen Typen. Vielleicht fehlt mir etwas?
Dan Burton
Nein, hast du nicht, aber anscheinend habe ich. :-) Ich habe die Verbindung zwischen den beiden vorher nicht gesehen. Es ist jetzt allerdings kristallklar.
fehlender Faktor

Antworten:

112

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 ,

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

Es ist ein Beispiel für das klassische Kuchenmuster: Wir haben eine Familie von Abstraktionen, die schrittweise durch eine Hierarchie verfeinert werden ( ResourceManager/ Resourcewerden durch FileManager/ verfeinert, Filedie wiederum durch NetworkFileManager/ verfeinert werden RemoteFile). 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:

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

Beachten Sie, dass die Pfadabhängigkeit bedeutet, dass der Compiler garantiert, dass die Methoden testHashund nur mit Argumenten aufgerufen werden können, die ihr entsprechen, d. H. es ist eigen und sonst nichts.testDuplicatesNetworkFileManagerRemoteFiles

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 ResourceManagerHierarchie neu zu definieren.

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

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

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

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.

Miles Sabin
quelle
2
Es war David MacIver, der den Begriff geprägt hat, aber auf jeden Fall ist er ziemlich beschreibend. Dies ist eine fantastische Erklärung dafür, warum abhängige Methodentypen so aufregend sind. Gute Arbeit!
Daniel Spiewak
Es kam vor einiger Zeit im Gespräch zwischen uns beiden auf #scala vor einiger Zeit auf ... wie gesagt, ich kann mich nicht erinnern, wer von uns es zuerst gesagt hat.
Miles Sabin
Scheint, als hätte mein Gedächtnis mir einen Streich gespielt ... Konsens ist, dass es David MacIvers Münzprägung war.
Miles Sabin
Ja, ich war zu der Zeit nicht da (auf #scala), aber Jorge war da und dort bekam ich meine Informationen.
Daniel Spiewak
Durch die Verfeinerung von Elementen vom abstrakten Typ konnte ich die Funktion testHash4 ziemlich schmerzlos implementieren. 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.
Marco van Hilst
53
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

An anderer Stelle können wir statisch garantieren, dass wir keine Knoten aus zwei verschiedenen Graphen verwechseln, z.

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

Natürlich hat dies bereits funktioniert, wenn es im Inneren definiert wurde Graph, aber sagen wir, wir können es nicht ändern Graphund 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.

Alexey Romanov
quelle
6

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.

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^
Shelby Moore III
quelle
Dies hat nichts damit zu tun. Mit 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}
Typmitgliedern
In jedem Fall hat dies nichts mit abhängigen Methodentypen zu tun. Nehmen Sie zum Beispiel Alexeys Beispiel ( stackoverflow.com/a/7860821/333643 ). Die Verwendung Ihres Ansatzes (einschließlich der von mir kommentierten Verfeinerungsversion) erreicht das Ziel nicht. Dadurch wird sichergestellt, dass n1.Node =: = n2.Node ist, es wird jedoch nicht sichergestellt, dass sich beide im selben Diagramm befinden. IIUC DMT stellt dies sicher.
Nafg
@nafg Danke, dass du darauf hingewiesen hast. Ich habe das Wort konkret hinzugefügt , um zu verdeutlichen, dass ich mich nicht auf den Verfeinerungsfall für Typmitglieder bezog. Soweit ich sehen kann, ist dies immer noch ein gültiger Anwendungsfall für abhängige Methodentypen, obwohl Sie mir bewusst waren, dass sie in anderen Anwendungsfällen mehr Leistung haben können. Oder habe ich die grundlegende Essenz Ihres zweiten Kommentars vermisst?
Shelby Moore III
3

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.

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

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

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

So wird statische Sicherheit erreicht.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A
Shelby Moore III
quelle
Wenn wir die Argumentschlüssel in einem einzigen Wert weitergeben müssen, habe ich nicht getestet, sondern angenommen, dass wir ein Tupel verwenden können, z. B. für die Überladung mit 2 Argumenten 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).
Shelby Moore III
"Die statische Typisierung des Hash-Map-Elementtyps unterliegt Any oder AnyRef" - ich folge nicht. Wenn Sie Elementtyp sagen, meinen Sie Schlüsseltyp oder Werttyp (dh erstes oder zweites Typargument für HashMap)? Und warum sollte es subsumiert werden?
Robin Green
@RobinGreen Der Typ der Werte in der Hash-Tabelle. Afair, subsumiert, weil Sie in Scala nicht mehr als einen Typ in eine Sammlung aufnehmen können, es sei denn, Sie subsumieren ihren gemeinsamen Supertyp, weil Scala keinen Unionstyp (Disjunktionstyp) hat. Siehe meine Fragen und Antworten zur Subsumtion in Scala.
Shelby Moore III