Che cosa è stato appreso sul fare parte della varianza del tipo?

4

In Java, la varianza dei tipi parametrizzati è indicata a seconda di come viene utilizzata:

<A extends B,B> void store(ArrayList<B> list, A elem) {
    list.add(elem);
}

Mentre in Scala è indicato nella dichiarazione di classe con + o - .

Quello che voglio sapere è che ora che Scala è in giro da un po ', qual è stata l'esperienza delle persone nel rendere la varianza una proprietà del tipo? È generalmente abbastanza flessibile per quello che vuoi fare? Ci sono aspetti che avresti fatto diversamente, sapendo cosa sai ora?

    
posta Owen 25.08.2011 - 22:50
fonte

2 risposte

4

C'è molta incomprensione nella domanda:

Innanzitutto, non esiste una "varianza di tipi parametrici". I tipi (come List[Int] ) sono, da soli, corretti, e quindi non varianti (co / contro-variante). Cosa può essere una variante (ntra), sono costruttori di tipi , ad es. List , che non sono essi stessi tipi.

Pertanto, la linea

<A extends B,B> void store(ArrayList<B> list, A elem)

non può "indicare la varianza dei tipi", poiché non definisce alcun costruttore di tipi. È un semplice esempio di polimorfismo parametrico (limitato).

Abbiamo diversi tipi di polimorfismo nei linguaggi di programmazione. Il più naturale dei linguaggi OOP come Java è il polimorfismo del sottotipo , che è caratterizzato dal fatto che da X <: Y e t : Y , ne segue che t : Y (IOW se un termine / variabile / parametro / qualunque abbia tipo X che è un sottotipo di Y , ha anche digitato Y ed è utilizzabile in qualsiasi posto in cui sarebbe vero un Y ).

Un altro tipo di polimorfismo è polimorfismo parametrico , il che significa che alcuni tipi sono in realtà variabili di tipo, che rappresentano un segnaposto per qualsiasi tipo e sono istanziati a tipi reali durante il controllo ortografico. Il polimorfismo parametrico limitato indica che le variabili tpe possono essere vincolate per essere un sottotipo o supertipo di altro tipo. I generici offrono il polimorfismo parametrico in Java.

Ora, come giocano questi due insieme? Sembra naturale che un file di dipendenti possa essere letto come un file di cittadino, poiché ogni dipendente è naturalmente un cittadino (che viene catturato nel programma dalla relazione di sottotipo). Questo significa ReadableFile[Employee] <: ReadableFile[Citizen] . Tuttavia, le regole per determinare i sottotipi non supportano questa conclusione - entrambi i tipi sono totalmente indipendenti.

È qui che entra in gioco la varianza. Se definiamo alcuni costruttori di tipi (in questo caso ReadableFile ) come co (contra-) variante, possiamo usare regole di inferenza come "se X <: Y e T è covariante, quindi T[X] <: T[Y] ". In effetti, questa regola supporta la conclusione di cui sopra.

I costruttori di tipo parametrico in Java sono sempre invarianti.

Come già notato, a volte possiamo andare avanti senza costruttori di varianti e ottenere lo stesso effetto. Nei parametri di funzione,

def foo(x : TCovar[Reader]) : Unit

con covariant TCovar è funzionalmente equivalente a

def foo[A <: Reader](x : T[A]) : Unit

Questo non significa comunque che T nella seconda definizione sia covariante.

Tuttavia, in funzione tipo di ritorno e tipi di variabile, questo trucco in genere non aiuta. Anche in questo caso, possiamo ottenere lo stesso effetto dei costruttori del tipo variant con l'aiuto di tipi esistenziali . Quindi

val foo : TCovar[Reader]

diventa

val foo : T[X] forSome { type X <: Reader }

Puoi vedere questo in azione in questo esempio . Come puoi vedere nella definizione di v4 e v5 , i costruttori di tipo covariante possono rendere il programma molto più semplice. Questa semplificazione si applica anche alle dichiarazioni dei parametri di funzione, motivo per cui i linguaggi di programmazione funzionale con polimorfismo di sottotipo utilizzano costruttori di tipo variante (almeno per i loro tipi di funzione).

Tuttavia, affinché un costruttore di tipi sia covariante, deve soddisfare alcune regole. Queste regole fanno sì che la List<T> di Java e altre interfacce siano effettivamente invarianti, poiché forniscono sia la lettura che la scrittura dei dati del contenitore (che è tipica per la programmazione imperativa). Le strutture dati funzionali di Scala non funzionano in questo modo, quindi possono essere covarianti. È dimostrato anche nel esempio - NCov non può essere covariante, perché fornisce read e scrivere l'accesso ai suoi dati interni. Credo che questo sia il motivo principale per cui i costruttori di tipo covariante non esistono in Java, che è la risposta alla tua domanda .

Ci scusiamo per aver mixato Java e Scala in questo post. Spero che gli esempi siano così facili da capire che non ha importanza.

    
risposta data 06.09.2011 - 01:40
fonte
1

So che non si tratta veramente di risposte alla tua domanda, ma potrebbero scatenare altre risposte ...: -)

Vorrei suggerire un codice diverso. Ho delle difficoltà con alcuni punti:

    I parametri di tipo
  • sono in ordine inverso (A è il primo, ma dipende da B)
  • ArrayList è un tipo strong, List renderebbe il codice più generale (riutilizzabile nuovo, robusto alle modifiche future)
  • Mi piace anche che gli oggetti multipli (come List) finiscano con un 's' ...: -)

    <A> void store(List<A> items, A item) {
      items.add(item);
    }
    

In Java, a volte abbiamo anche il tipo di variabile, sia come parametro di un metodo o in una classe. Questa è la parte che posso commentare.

Come parametro metodo, mi è stato utile in diverse situazioni:

  • quando il metodo deve esaminare una mappa, per trovare un'istanza che dipende dalla classe effettiva necessaria.

    Registry.java
      public <E> E find(Class<E> clazz){
        return (E) map.get(clazz);
        // The cast is fine, because it is how we store in the map.
      }
    
  • Per creare un'istanza, se necessario.

    public <E extends Item> List<Item> singletonList(Class<E> itemClass){
        List<Item> items = ...;
        if (items.isEmpty()) {
          items.add(createDefaultInstanceFor(itemClass);
        }
        return items;
      }
    

Come parametro di classe, lo uso spesso:

  • in Builder con ereditarietà (per far funzionare l'ultima riga).

    ABuilder.java
    class ABuilder<E extends ABuilder> {
      private int size;
    
      public E withSize(int someSize) {
          size = someSize;
          return (E) this;
      }
    
    BBuilder.java
    class BBuilder extends ABuilder<BBuilder> {
       // here, no redefinition of method withSize
    
       public void onlyOnB(){
       }
    }
    
    Caller.java
      {
         BBuilder b = ...;
         b.withSize(2).onlyOnB();
         // The call still returns the subclass BBuilder, 
         // even though it is defined in the superclass ABuilder only.
      }
    
risposta data 29.08.2011 - 20:02
fonte

Leggi altre domande sui tag