Un buon sistema di tipo generico


29

È comunemente accettato che i generici Java non sono riusciti in alcuni modi importanti. La combinazione di caratteri jolly e limiti ha portato ad alcuni codici seriamente illeggibili.

Tuttavia, quando guardo altre lingue, non riesco davvero a trovare un sistema di tipo generico di cui i programmatori siano contenti.

Se prendiamo quanto segue come obiettivi di progettazione di un tale sistema di tipo:

  • Produce sempre dichiarazioni di tipo di facile lettura
  • Facile da imparare (non è necessario rispolverare covarianza, contraddizione, ecc.)
  • massimizza il numero di errori in fase di compilazione

C'è qualche lingua che l'ha capito bene? Se cerco su Google, l'unica cosa che vedo sono le lamentele su come il sistema dei tipi fa schifo nel linguaggio X. Questo tipo di complessità è inerente alla tipizzazione generica? Dovremmo semplicemente rinunciare a provare a verificare la sicurezza del tipo al 100% in fase di compilazione?

La mia domanda principale è qual è la lingua che "ha fatto bene" il meglio rispetto a questi tre obiettivi. Mi rendo conto che è soggettivo, ma finora non riesco nemmeno a trovare una lingua in cui non tutti i programmatori concordano sul fatto che il sistema di tipo generico sia un disastro.

Addendum: come notato, la combinazione di sottotipo / eredità e generici è ciò che crea la complessità, quindi sto davvero cercando un linguaggio che combini entrambi ed eviti l'esplosione della complessità.


2
Cosa intendi con easy-to-read type declarations? Anche il terzo criterio è ambiguo: ad esempio, posso trasformare l'indice di array dalle eccezioni dei limiti in errori di tempo di compilazione non consentendoti di matrici di indici a meno che non sia in grado di calcolare l'indice in fase di compilazione. Inoltre, il secondo criterio esclude il sottotipo. Non è necessariamente una cosa negativa, ma dovresti essere consapevole di ciò che stai chiedendo.
Doval,


9
@gnat, questo sicuramente non è un rant contro Java. Programma quasi esclusivamente in Java. Il mio punto è che è generalmente accettato all'interno della comunità Java che i generici siano difettosi (non un errore totale, ma probabilmente uno parziale), quindi è una domanda logica chiedere come avrebbero dovuto essere implementati. Perché hanno torto e gli altri hanno capito bene? O è davvero impossibile ottenere dei generici assolutamente giusti?
Peter,

1
Se tutti rubassero dal C # ci sarebbero stati meno reclami. Soprattutto Java è in grado di recuperare, copiando. Decidono invece su soluzioni inferiori. Molte delle domande ancora discusse dai comitati di progettazione Java sono state già decise e implementate in C #. Non sembrano nemmeno guardare.
usr

2
@emodendroket: Penso che le mie due maggiori lamentele sui generici di C # siano che non c'è modo di applicare un vincolo "supertipo" (ad es. Foo<T> where SiameseCat:T) e che non c'è possibilità di avere un tipo generico a cui non è convertibile Object. IMHO, .NET trarrebbe beneficio da tipi aggregati che erano simili alle strutture, ma ancora più disossati. Se KeyValuePair<TKey,TValue>fosse un tale tipo, allora IEnumerable<KeyValuePair<SiameseCat,FordFocus>>potrebbe essere lanciato un cast IEnumerable<KeyValuePair<Animal,Vehicle>>, ma solo se il tipo non potesse essere inscatolato.
supercat,

Risposte:


24

Mentre i generici sono stati mainstream nella comunità di programmazione funzionale per decenni, l'aggiunta di generici a linguaggi di programmazione orientati agli oggetti offre alcune sfide uniche, in particolare l'interazione di sottotipizzazione e generici.

Tuttavia, anche se ci concentriamo su linguaggi di programmazione orientati agli oggetti, e Java in particolare, sarebbe stato possibile progettare un sistema generico di gran lunga migliore:

  1. I tipi generici dovrebbero essere ammessi ovunque siano gli altri tipi. In particolare, se Tè un parametro di tipo, le seguenti espressioni devono essere compilate senza avvisi:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Sì, questo richiede che i generici siano reificati, proprio come ogni altro tipo nella lingua.

  2. La covarianza e la contraddizione di un tipo generico dovrebbero essere specificate (o desunte da) la sua dichiarazione, piuttosto che ogni volta che viene usato il tipo generico, così possiamo scrivere

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    piuttosto che

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Poiché i tipi generici possono essere piuttosto lunghi, non è necessario specificarli in modo ridondante. Cioè, dovremmo essere in grado di scrivere

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    piuttosto che

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Qualsiasi tipo dovrebbe essere ammissibile come parametro di tipo, non solo tipi di riferimento. (Se possiamo avere un int[], perché non possiamo avere un List<int>)?

Tutto questo è possibile in C #.


1
Ciò eliminerebbe anche i generici autoreferenziali? E se volessi dire che un oggetto comparabile può confrontarsi con qualcosa dello stesso tipo o di una sottoclasse? Può essere fatto? O se scrivo un metodo di ordinamento che accetta liste con oggetti comparabili, tutti devono essere comparabili tra loro. Enum è un altro buon esempio: Enum <E estende Enum <E>>. Non sto dicendo che un sistema di tipi dovrebbe essere in grado di fare queste cose, sono solo curioso di sapere come C # gestisca queste situazioni.
Peter,

1
Java 7 di tipo generico di inferenza e C ++ auto 's aiuto con alcune di queste preoccupazioni, ma sono lo zucchero sintattico e non cambiano i meccanismi sottostanti.

@Snowman L'inferenza del tipo Java ha alcuni casi angolari davvero odiosi, come non lavorare con classi anonime e non trovare i limiti giusti per i caratteri jolly quando si valuta un metodo generico come argomento di un altro metodo generico.
Doval,

@Doval è per questo che ho detto che aiuta con alcune delle preoccupazioni: non risolve nulla e non affronta tutto. I generici Java hanno molti problemi: sebbene migliori dei tipi grezzi, causano sicuramente molti mal di testa.

34

L'uso di sottotipi crea molte complicazioni durante la programmazione generica. Se insisti nell'usare un linguaggio con sottotipi, devi accettare che esiste una certa complessità intrinseca nella programmazione generica che ne deriva. Alcune lingue lo fanno meglio di altri, ma puoi portarlo solo così lontano.

Contrastalo con i generici di Haskell, per esempio. Sono abbastanza semplici che se si utilizza l'inferenza del tipo, è possibile scrivere una funzione generica corretta per errore . In realtà, se si specifica un unico tipo, il compilatore spesso dice a se stesso: "Beh, mi è stato andando a fare questo generica, ma tu mi hai chiesto di fare solo per interi, quindi, qualsiasi cosa."

Certo, le persone usano il sistema di tipi di Haskell in modi sorprendentemente complessi, rendendolo la rovina di ogni principiante, ma il sistema di tipi sottostante è elegante e molto ammirato.


1
Grazie per questa risposta Questo articolo inizia con alcuni degli esempi di Joshua Bloch in cui i generici diventano troppo complicati: artima.com/weblogs/viewpost.jsp?thread=222021 . È una differenza nella cultura tra Java e Haskell, dove tali costrutti sarebbero considerati perfetti in Haskell, o c'è una vera differenza nel sistema di tipi di Haskell che evita tali situazioni?
Peter,

10
@Peter Haskell non ha sottotipi e, come ha detto Karl, il compilatore può inferire automaticamente i tipi includendo vincoli come "tipo adeve essere una sorta di numero intero".
Doval,

In altre parole , covarianza , in lingue come la Scala.
Paul Draper,

14

Circa 20 anni fa sono state condotte molte ricerche sulla combinazione di farmaci generici e sottotipi. Il linguaggio di programmazione Thor sviluppato dal gruppo di ricerca di Barbara Liskov al MIT aveva un'idea delle clausole "where" che ti consentono di specificare i requisiti del tipo su cui stai parametrizzando. (Questo è simile a quello che C ++ sta cercando di fare con i concetti .)

L'articolo che descrive i generici di Thor e il modo in cui interagiscono con i sottotipi di Thor è: Day, M; Gruber, R; Liskov, B; Myers, AC: Sottotipi vs. clausole where: vincolo del polimorfismo parametrico , ACM Conf su Obj-Oriented Prog, Sys, Lang and Apps , (OOPSLA-10): 156-158, 1995.

Credo che, a loro volta, si siano basati sul lavoro svolto su Emerald alla fine degli anni '80. (Non ho letto quel lavoro, ma il riferimento è: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribution and Abstract Types in Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Sia Thor che Emerald erano "lingue accademiche", quindi probabilmente non avevano abbastanza uso da far capire alle persone se le clausole (concetti) risolvessero davvero qualsiasi problema reale. È interessante leggere l'articolo di Bjarne Stroustrup sul perché il primo tentativo di Concepts in C ++ non è riuscito: Stroustrup, B: Decisione "Rimuovi concetti" C ++ 0x , Dr Dobbs , 22 luglio 2009. (Ulteriori informazioni sulla home page di Stroustrup . )

Un'altra direzione che le persone sembrano provare è qualcosa chiamata tratti . Ad esempio il linguaggio di programmazione Rust di Mozilla usa tratti. A quanto ho capito (che può essere completamente sbagliato), dichiarare che una classe soddisfa un tratto è molto simile a dire che una classe implementa un'interfaccia, ma stai dicendo "si comporta come un" piuttosto che "è un". Sembra che i nuovi linguaggi di programmazione Swift di Apple stiano usando un concetto simile di protocolli per specificare i vincoli sui parametri dei generici .


I tratti di ruggine sono simili alle classi di tipo Haskell. Ma ci sono due angoli che si possono guardare: 1. Considerarlo come un mezzo per affermare i vincoli. 2. Definire un'interfaccia generica, che può essere associata a un tipo concreto o a una classe generica di tipi.
BitTickler
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.