Quali sono alcuni casi d'uso convincenti per i tipi di metodo dipendenti?


127

I tipi di metodi dipendenti, che prima erano una funzionalità sperimentale, ora sono stati abilitati per impostazione predefinita nel trunk e apparentemente questo sembra aver creato certo entusiasmo nella comunità Scala.

A prima vista, non è immediatamente ovvio a cosa possa essere utile. Heiko Seeberger ha pubblicato un semplice esempio di tipi di metodi dipendenti qui , che come si può vedere nel commento non può essere facilmente riprodotto con parametri di tipo sui metodi. Quindi non è stato un esempio molto convincente. (Potrei mancare qualcosa di ovvio. Per favore correggimi se è così.)

Quali sono alcuni esempi pratici e utili di casi d'uso per tipi di metodi dipendenti in cui sono chiaramente vantaggiosi rispetto alle alternative?

Quali cose interessanti possiamo fare con loro che prima non erano possibili / facili?

Cosa ci acquistano rispetto alle funzionalità del sistema di tipi esistenti?

Inoltre, i tipi di metodi dipendenti sono analoghi o traggono ispirazione da qualsiasi caratteristica presente nei sistemi di tipi di altri linguaggi tipizzati avanzati come Haskell, OCaml?



Grazie per il link, Dan! Sono a conoscenza dei tipi dipendenti in generale, ma il concetto di tipi di metodi dipendenti è relativamente nuovo per me.
missingfaktor il

Mi sembra che i "tipi di metodi dipendenti" siano semplicemente tipi che dipendono da uno o più tipi di input di un metodo (incluso il tipo di oggetto su cui viene invocato il metodo); niente di folle lì oltre l'idea generale dei tipi dipendenti. Forse mi manca qualcosa?
Dan Burton,

No, non l'hai fatto, ma a quanto pare l'ho fatto. :-) Non ho visto il collegamento tra i due prima. Adesso è cristallino.
missingfaktor il

Risposte:


112

Più o meno qualsiasi uso di tipi di membri (cioè nidificati) può far sorgere la necessità di tipi di metodi dipendenti. In particolare, sostengo che senza tipi di metodo dipendenti il ​​classico schema a torta è più vicino all'essere un anti-schema.

Allora, qual'è il problema? I tipi nidificati in Scala dipendono dall'istanza che li racchiude. Di conseguenza, in assenza di tipi di metodi dipendenti, i tentativi di utilizzarli al di fuori di tale istanza possono essere frustranti. Questo può trasformare disegni che inizialmente sembrano eleganti e accattivanti in mostruosità che sono incredibilmente rigide e difficili da refactoring.

Illustrerò che con un esercizio che do durante il mio corso di formazione alla Scala Avanzata ,

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
}

È un esempio del modello classico di torta: abbiamo una famiglia di astrazioni che vengono gradualmente raffinate attraverso un'erarchia ( ResourceManager/ Resourcesono raffinate da FileManager/ Fileche a loro volta sono raffinate da NetworkFileManager/RemoteFile ). È un esempio di giocattolo, ma il modello è reale: è usato in tutto il compilatore Scala ed è stato ampiamente utilizzato nel plugin Scala Eclipse.

Ecco un esempio dell'astrazione in uso,

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

Si noti che la dipendenza del percorso significa che il compilatore garantirà che i metodi testHashe testDuplicateson NetworkFileManagerpossano essere chiamati solo con argomenti che corrispondono ad esso, ad es. è proprio RemoteFilese nient'altro.

È innegabilmente una proprietà desiderabile, ma supponiamo di voler spostare questo codice di prova in un altro file sorgente? Con i tipi di metodi dipendenti è banalmente facile ridefinire quei metodi al di fuori della ResourceManagergerarchia,

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

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

Nota qui gli usi dei tipi di metodi dipendenti: il tipo del secondo argomento ( rm.Resource) dipende dal valore del primo argomento ( rm).

È possibile farlo senza tipi di metodi dipendenti, ma è estremamente imbarazzante e il meccanismo non è abbastanza intuitivo: insegno questo corso da quasi due anni ormai, e in quel momento nessuno ha trovato una soluzione funzionante senza impegno.

Provalo tu stesso ...

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

Dopo un po 'di fatica, probabilmente scoprirai perché io (o forse era David MacIver, non riusciamo a ricordare chi di noi ha coniato il termine) lo chiamo il Panificio del Destino.

Modifica: consenso è che Bakery of Doom è stato il conio di David MacIver ...

Per il vantaggio: la forma di Scala di tipi dipendenti in generale (e tipi di metodi dipendenti come parte di esso) è stata ispirata dal linguaggio di programmazione Beta ... derivano naturalmente dalla semantica di annidamento costante di Beta. Non conosco nessun altro linguaggio di programmazione nemmeno debolmente tradizionale che abbia tipi dipendenti in questa forma. Lingue come Coq, Cayenne, Epigram e Agda hanno una diversa forma di tipizzazione dipendente che è in qualche modo più generale, ma che differisce in modo significativo essendo parte di sistemi di tipi che, a differenza di Scala, non hanno sottotitoli.


2
Fu David MacIver a coniare il termine, ma in ogni caso è abbastanza descrittivo. Questa è una spiegazione fantastica del perché i tipi di metodi dipendenti sono così interessanti. Bel lavoro!
Daniel Spiewak,

Inizialmente è nato durante una conversazione tra noi due su #scala un po 'di tempo fa ... come ho detto, non ricordo chi di noi sia stato chi l'ha detto per primo.
Miles Sabin,

Sembra che la mia memoria mi stesse giocando brutti scherzi ... consenso è stato il conio di David MacIver.
Miles Sabin,

Sì, non ero lì (su #scala) al momento, ma Jorge era ed è lì che stavo ottenendo le mie informazioni.
Daniel Spiewak,

Utilizzando la raffinatezza astratta dei membri di tipo sono stato in grado di implementare la funzione testHash4 in modo abbastanza indolore. def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")Suppongo che questo possa essere considerato un'altra forma di tipi dipendenti, comunque.
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]
}

Da qualche altra parte possiamo garantire staticamente che non stiamo mescolando nodi da due diversi grafici, ad esempio:

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

Ovviamente, questo funzionava già se definito all'interno Graph, ma diciamo che non possiamo modificarlo Graphe stiamo scrivendo un'estensione "pimp my library".

A proposito della seconda domanda: i tipi abilitati da questa funzione sono molto più deboli dei tipi dipendenti completi (vedi la programmazione tipicamente dipendente in Agda per un sapore di questo.) Non credo di aver mai visto un'analogia prima.


6

Questa nuova funzionalità è necessaria quando vengono utilizzati membri di tipo astratto concreto anziché parametri di tipo . Quando vengono utilizzati i parametri di tipo, la dipendenza del tipo di polimorfismo familiare può essere espressa nelle versioni più recenti e in alcune versioni precedenti di Scala, come nel seguente esempio semplificato.

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, "")
         ^

Questo non è correlato. Con i membri del tipo è possibile utilizzare i perfezionamenti per lo stesso risultato: 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}ecc.
nafg

In ogni caso, ciò non ha nulla a che fare con i tipi di metodi dipendenti. Prendiamo ad esempio l'esempio di Alexey ( stackoverflow.com/a/7860821/333643 ). L'uso del tuo approccio (inclusa la versione di perfezionamento che ho commentato) non raggiunge l'obiettivo. Garantirà che n1.Node =: = n2.Node, ma non garantirà che siano entrambi nello stesso grafico. IIUC DMT lo garantisce.
nafg

@nafg Grazie per averlo sottolineato. Ho aggiunto la parola concrete per chiarire che non mi riferivo al caso di perfezionamento per i membri del tipo. Per quanto posso vedere, questo è ancora un caso d'uso valido per tipi di metodi dipendenti, nonostante il punto (di cui ero a conoscenza) che possano avere più potere in altri casi d'uso. O mi sono perso l'essenza fondamentale del tuo secondo commento?
Shelby Moore III,

3

Sto sviluppando un modello per l'interazione di una forma di programmazione dichiarativa con lo stato ambientale. I dettagli non sono rilevanti qui (ad es. Dettagli sui callback e somiglianza concettuale con il modello Actor combinato con un serializzatore).

Il problema rilevante è che i valori di stato sono memorizzati in una mappa hash e referenziati da un valore chiave hash. Le funzioni immettono argomenti immutabili che sono valori dall'ambiente, possono chiamare altre funzioni simili e scrivere lo stato nell'ambiente. Ma alle funzioni non è consentito leggere valori dall'ambiente (quindi il codice interno della funzione non dipende dall'ordine dei cambiamenti di stato e rimane quindi dichiarativo in tal senso). Come digitare questo in Scala?

La classe di ambiente deve avere un metodo sovraccarico che immette tale funzione da chiamare e immette le chiavi hash degli argomenti della funzione. Pertanto, questo metodo può chiamare la funzione con i valori necessari dalla mappa hash, senza fornire accesso in lettura pubblica ai valori (quindi, se necessario, negando alle funzioni la possibilità di leggere i valori dall'ambiente).

Ma se queste chiavi hash sono stringhe o valori hash interi, la tipizzazione statica del tipo hash map elemento sussume a qualunque o AnyRef (codice hash_map non mostrato di seguito), e quindi potrebbe verificarsi una mancata corrispondenza run-time, cioè sarebbe possibile per inserire qualsiasi tipo di valore in una mappa hash per una determinata chiave hash.

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
}

Anche se non ho testato quanto segue, in teoria posso ottenere le chiavi hash dai nomi delle classi in fase di runtime classOf, quindi una chiave hash è un nome di classe anziché una stringa (usando i backtick di Scala per incorporare una stringa in un nome di classe).

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

In questo modo si ottiene la sicurezza di tipo statico.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A

Quando abbiamo bisogno di passare le chiavi dell'argomento in un singolo valore, non ho testato, ma presumo che possiamo usare una Tupla, ad esempio per il sovraccarico di 2 argomenti def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A. Non useremmo una raccolta di chiavi dell'argomento, perché i tipi di elemento verrebbero inclusi (sconosciuti in fase di compilazione) nel tipo di raccolta.
Shelby Moore III,

"la tipizzazione statica del tipo di elemento della mappa hash si riduce a Any o AnyRef" - Non seguo. Quando dici tipo di elemento, intendi tipo di chiave o tipo di valore (ovvero argomento di primo o secondo tipo su HashMap)? E perché dovrebbe essere incluso?
Robin Green,

@RobinGreen Il tipo di valori nella tabella hash. Alla fine, sussunto perché non è possibile inserire più di un tipo in una raccolta in Scala a meno che non si sottoscriva il loro supertipo comune, poiché Scala non ha un tipo di unione (disgiunzione). Vedi le mie domande e risposte sull'assunzione in Scala.
Shelby Moore III,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.