Scala: tipi astratti vs generici


Risposte:


257

Hai un buon punto di vista su questo problema qui:

Lo scopo del sistema di tipo Scala
Una conversazione con Martin Odersky, parte III
di Bill Venners e Frank Sommers (18 maggio 2009)

Aggiornamento (ottobre2009): quanto segue è stato effettivamente illustrato in questo nuovo articolo da Bill Venners:
membri di tipo astratto contro parametri di tipo generico in Scala (vedere il sommario alla fine)


(Ecco l'estratto pertinente della prima intervista, maggio 2009, enfasi sulla mia)

Principio generale

Ci sono sempre state due nozioni di astrazione:

  • parametrizzazione e
  • membri astratti.

In Java hai anche entrambi, ma dipende da cosa stai astrattando.
In Java hai metodi astratti, ma non puoi passare un metodo come parametro.
Non hai campi astratti, ma puoi passare un valore come parametro.
Allo stesso modo non hai membri di tipo astratto, ma puoi specificare un tipo come parametro.
Quindi in Java hai anche tutti e tre questi, ma c'è una distinzione su quale principio di astrazione puoi usare per quale tipo di cose. E potresti sostenere che questa distinzione è abbastanza arbitraria.

Il modo alla Scala

Abbiamo deciso di avere gli stessi principi costruttivi per tutti e tre i tipi di membri .
Quindi puoi avere campi astratti e parametri di valore.
Puoi passare metodi (o "funzioni") come parametri, oppure puoi astrarre su di essi.
È possibile specificare i tipi come parametri oppure è possibile astrarre su di essi.
E quello che otteniamo concettualmente è che possiamo modellare l'uno in termini di altro. Almeno in linea di principio, possiamo esprimere ogni tipo di parametrizzazione come una forma di astrazione orientata agli oggetti. Quindi, in un certo senso, potresti dire che Scala è un linguaggio più ortogonale e completo.

Perché?

Ciò che, in particolare, i tipi astratti ti acquistano è un buon trattamento per questi problemi di covarianza di cui abbiamo parlato prima.
Un problema standard, che esiste da molto tempo, è il problema degli animali e degli alimenti.
Il puzzle era di avere una lezione Animalcon un metodo eat, che mangia del cibo.
Il problema è che se eseguiamo la sottoclasse degli animali e abbiamo una classe come Cow, allora mangerebbero solo erba e non cibo arbitrario. Una mucca non poteva mangiare un pesce, per esempio.
Quello che vuoi è poter dire che una Mucca ha un metodo alimentare che mangia solo erba e non altre cose.
In realtà, non è possibile farlo in Java perché si scopre che è possibile costruire situazioni non corrette, come il problema di assegnare un Fruit a una variabile Apple di cui ho parlato in precedenza.

La risposta è che aggiungi un tipo astratto nella classe Animale .
Dici, la mia nuova classe Animal ha un tipo di SuitableFood, che non conosco.
Quindi è un tipo astratto. Non dai un'implementazione del tipo. Quindi hai un eatmetodo che mangia solo SuitableFood.
E poi in Cowclasse direi, OK, ho una Mucca, che estende la classe Animal, e per Cow type SuitableFood equals Grass.
Quindi i tipi astratti forniscono questa nozione di un tipo in una superclasse che non conosco, che poi inserirò in sottoclassi con qualcosa che conosco .

Lo stesso con la parametrizzazione?

In effetti puoi. Puoi parametrizzare la classe Animal con il tipo di cibo che mangia.
Ma in pratica, quando lo fai con molte cose diverse, porta a un'esplosione di parametri , e di solito, per di più, in limiti di parametri .
All'ECOOP del 1998, Kim Bruce, Phil Wadler e io avevamo un articolo in cui abbiamo mostrato che aumentando il numero di cose che non conosci, il tipico programma crescerà quadraticamente .
Quindi ci sono ottime ragioni per non fare parametri, ma per avere questi membri astratti, perché non ti fanno esplodere questo quadratico.


thatismatt chiede nei commenti:

Pensi che quanto segue sia un giusto riassunto:

  • I tipi astratti sono utilizzati nelle relazioni 'has-a' o 'usi-a' (ad es. A Cow eats Grass)
  • dove come generici sono generalmente relazioni "di" (ad es. List of Ints)

Non sono sicuro che la relazione sia così diversa tra l'utilizzo di tipi astratti o generici. Ciò che è diverso è:

  • come vengono utilizzati e
  • come vengono gestiti i limiti dei parametri.

Per capire di cosa parla Martin quando si tratta di "esplosione di parametri, e di solito, inoltre, in limiti di parametri ", e della sua successiva crescita quadratica quando il tipo astratto viene modellato usando i generici, puoi considerare il documento " Astrazione di componenti scalabili "scritto da ... Martin Odersky e Matthias Zenger per OOPSLA 2005, cui fa riferimento nelle pubblicazioni del progetto Palcom (terminato nel 2007).

Estratti rilevanti

Definizione

I membri di tipo astratto forniscono un modo flessibile per astrarre su tipi di componenti concreti.
I tipi astratti possono nascondere informazioni sugli interni di un componente, in modo simile al loro uso nelle firme SML . In un framework orientato agli oggetti in cui le classi possono essere estese per ereditarietà, possono anche essere utilizzate come mezzo flessibile di parametrizzazione (spesso chiamato polimorfismo familiare, vedere ad esempio questa voce del blog e l'articolo scritto da Eric Ernst ).

(Nota: il polimorfismo familiare è stato proposto per linguaggi orientati agli oggetti come soluzione per supportare classi ricorsive reciprocamente riutilizzabili ma sicure di tipo.
Un'idea chiave del polimorfismo familiare è la nozione di famiglie, che sono utilizzate per raggruppare classi reciprocamente ricorsive)

astrazione di tipo limitato

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Qui, la dichiarazione di tipo di T è vincolata da un limite di tipo superiore che consiste in un nome di classe Ordinato e in un perfezionamento { type O = T }.
Le limita limite superiore specializzazioni del T in sottoclassi a tali sottotipi di ordinato per cui l'organo tipo Odi equals T.
A causa di questo vincolo, il <metodo della classe Ordinato è garantito per essere applicabile a un destinatario e un argomento di tipo T.
L'esempio mostra che il membro del tipo limitato può apparire esso stesso come parte del limite.
(ovvero Scala supporta il polimorfismo limitato da F )

(Nota, da Peter Canning, William Cook, Walter Hill, Walter Olthoff, articolo:
Cardelli e Wegner hanno introdotto una quantificazione limitata come mezzo per scrivere funzioni che operano in modo uniforme su tutti i sottotipi di un determinato tipo.
Hanno definito un semplice modello di "oggetto" e ha usato la quantificazione limitata per verificare le funzioni che hanno senso su tutti gli oggetti che hanno una serie specifica di "attributi".
Una presentazione più realistica di linguaggi orientati agli oggetti consentirebbe oggetti che sono elementi di tipi definiti in modo ricorsivo .
In questo contesto, limitato la quantificazione non serve più allo scopo previsto: è facile trovare funzioni che abbiano senso su tutti gli oggetti che hanno una serie specifica di metodi, ma che non possono essere digitati nel sistema Cardelli-Wegner.
Per fornire una base per le funzioni polimorfiche tipizzate nei linguaggi orientati agli oggetti, introduciamo la quantificazione limitata da F)

Due facce delle stesse monete

Esistono due forme principali di astrazione nei linguaggi di programmazione:

  • parametrizzazione e
  • membri astratti.

La prima forma è tipica per i linguaggi funzionali, mentre la seconda è tipicamente utilizzata in linguaggi orientati agli oggetti.

Tradizionalmente, Java supporta la parametrizzazione per i valori e l'astrazione dei membri per le operazioni. Il più recente Java 5.0 con generics supporta la parametrizzazione anche per i tipi.

Gli argomenti per includere i generici in Scala sono duplici:

  • Innanzitutto, la codifica in tipi astratti non è così semplice da fare a mano. Oltre alla perdita di concisione, esiste anche il problema dei conflitti di nomi accidentali tra nomi di tipi astratti che emulano i parametri di tipo.

  • In secondo luogo, i generici e i tipi astratti di solito svolgono ruoli distinti nei programmi Scala.

    • I generici sono in genere utilizzati quando è necessario solo digitare l'istanza , mentre
    • i tipi astratti vengono in genere utilizzati quando è necessario fare riferimento al tipo astratto dal codice client .
      Quest'ultimo si presenta in particolare in due situazioni:
    • Si potrebbe voler nascondere la definizione esatta di un membro del tipo dal codice client, per ottenere un tipo di incapsulamento noto dai sistemi di moduli in stile SML.
    • Oppure si potrebbe voler sovrascrivere il tipo in modo covariante in sottoclassi per ottenere il polimorfismo familiare.

In un sistema con polimorfismo limitato, riscrivere il tipo astratto in generici potrebbe comportare un'espansione quadratica dei limiti di tipo .


Aggiornamento ottobre 2009

Membri di tipo astratto rispetto a parametri di tipo generico in Scala (Bill Venners)

(enfatizzare il mio)

La mia osservazione finora sui membri di tipo astratto è che sono principalmente una scelta migliore rispetto ai parametri di tipo generico quando:

  • vuoi permettere alle persone di mescolare le definizioni di quei tipi tramite tratti .
  • pensi che la menzione esplicita del nome del membro del tipo quando viene definito aiuterà la leggibilità del codice .

Esempio:

se vuoi passare tre diversi oggetti di fixture nei test, sarai in grado di farlo, ma dovrai specificare tre tipi, uno per ogni parametro. Quindi, se avessi adottato l'approccio con parametri di tipo, le tue classi di suite avrebbero potuto finire così:

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

Considerando che con l'approccio membro del tipo sarà simile a questo:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Un'altra differenza minore tra membri di tipo astratto e parametri di tipo generico è che quando viene specificato un parametro di tipo generico, i lettori del codice non vedono il nome del parametro di tipo. Quindi qualcuno ha visto questa riga di codice:

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Non saprebbero quale fosse il nome del parametro type specificato come StringBuilder senza cercarlo. Considerando che il nome del parametro type è proprio lì nel codice nell'approccio del membro di tipo astratto:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

In quest'ultimo caso, i lettori del codice potrebbero vedere che StringBuilderè il tipo di "parametro fixture".
Avrebbero comunque bisogno di capire che cosa significasse "parametro del dispositivo", ma potevano almeno ottenere il nome del tipo senza consultare la documentazione.


61
Come posso ottenere punti karma rispondendo alle domande di Scala quando vieni e fai questo ??? :-)
Daniel C. Sobral,

7
Ciao Daniel: Penso che debbano esserci esempi concreti per illustrare i vantaggi dei tipi astratti rispetto alla parametrizzazione. Pubblicare alcuni in questa discussione sarebbe un buon inizio;) So che lo voterei.
VonC,

1
Ritieni che il seguente sia un giusto sommario: i tipi astratti sono usati nelle relazioni 'ha-a' o 'usi-a' (ad es. Una mucca mangia erba) dove i generici sono di solito 'di' relazioni (ad es. Elenco di Ints)
thatismatt il

Non sono sicuro che la relazione sia così diversa tra l'utilizzo di tipi astratti o generici. Ciò che è diverso è il modo in cui vengono utilizzati e come vengono gestiti i limiti dei parametri. Più nella mia risposta in un momento.
VonC,

1
Nota per sé: vedi anche questo post del blog di maggio 2010: daily-scala.blogspot.com/2010/05/…
VonC,

37

Avevo la stessa domanda quando leggevo di Scala.

Il vantaggio di usare generics è che stai creando una famiglia di tipi. Nessuno avrà bisogno di una sottoclasse Buffer-Si può semplicemente usare Buffer[Any], Buffer[String]ecc

Se si utilizza un tipo astratto, le persone saranno costrette a creare una sottoclasse. Le persone avranno bisogno di classi come AnyBuffer, StringBufferecc

Devi decidere qual è la migliore per le tue esigenze particolari.


18
mmm thins è migliorato molto su questo fronte, puoi solo richiederlo Buffer { type T <: String }o in Buffer { type T = String }base alle tue esigenze
Eduardo Pareja Tobes

21

È possibile utilizzare i tipi astratti insieme ai parametri del tipo per stabilire modelli personalizzati.

Supponiamo che sia necessario stabilire un modello con tre tratti collegati:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

nel modo in cui gli argomenti menzionati nei parametri di tipo sono AA, BB, CC stesso rispettosamente

Puoi venire con un qualche tipo di codice:

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

che non funzionerebbe in questo modo semplice a causa dei legami dei parametri di tipo. Devi renderlo covariante per ereditare correttamente

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

Questo campione verrebbe compilato ma stabilisce requisiti rigorosi per le regole di varianza e non può essere utilizzato in alcune occasioni

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

Il compilatore si opporrà con un sacco di errori di controllo della varianza

In tal caso, puoi raccogliere tutti i requisiti di tipo in un tratto aggiuntivo e parametrizzare altri tratti su di esso

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

Ora possiamo scrivere una rappresentazione concreta per il modello descritto, definire i metodi left e join in tutte le classi e ottenere right e double gratuitamente

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

Quindi, sia i tipi astratti che i parametri del tipo vengono utilizzati per la creazione di astrazioni. Entrambi hanno un punto debole e un punto di forza. I tipi astratti sono più specifici e in grado di descrivere qualsiasi struttura di tipi, ma sono dettagliati e richiedono esplicitamente specificato. I parametri di tipo possono creare immediatamente gruppi di tipi, ma ti preoccupano ulteriormente dell'ereditarietà e dei limiti di tipo.

Si danno sinergia tra loro e possono essere usati congiuntamente per creare astrazioni complesse che non possono essere espresse con una sola di esse.


0

Penso che non ci sia molta differenza qui. I membri di tipo astratto possono essere visti come solo tipi esistenziali simili ai tipi di record in alcuni altri linguaggi funzionali.

Ad esempio, abbiamo:

class ListT {
  type T
  ...
}

e

class List[T] {...}

Quindi ListTè lo stesso di List[_]. La convenienza dei membri del tipo è che possiamo usare la classe senza un tipo concreto esplicito ed evitare troppi parametri del tipo.

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.