Perché l'inferenza del tipo è utile?


37

Leggo il codice molto più spesso di quanto scrivo codice e presumo che la maggior parte dei programmatori che lavorano su software industriale lo faccia. Il vantaggio dell'inferenza di tipo presumo sia meno verbosità e meno codice scritto. D'altra parte, se leggi il codice più spesso, probabilmente vorrai un codice leggibile.

Il compilatore deduce il tipo; ci sono vecchi algoritmi per questo. Ma la vera domanda è: perché il programmatore dovrei dedurre il tipo delle mie variabili quando leggo il codice? Non è più veloce per chiunque solo leggere il tipo che pensare che tipo c'è?

Modifica: come conclusione capisco perché è utile. Ma nella categoria delle caratteristiche del linguaggio lo vedo in un secchio con sovraccarico dell'operatore - utile in alcuni casi ma che influisce sulla leggibilità se abusato.


5
Nella mia esperienza i tipi sono di gran lunga più importanti quando si scrive codice che non leggerlo. Quando leggo il codice, cerco algoritmi e blocchi specifici a cui solitamente mi dirigeranno variabili ben denominate. Non ho davvero bisogno di digitare il codice di controllo solo per leggere e capire cosa sta facendo a meno che non sia terribilmente mal scritto. Tuttavia, quando si legge un codice gonfio con dettagli extra inutili che non sto cercando (come troppe annotazioni di tipo) rende spesso più difficile trovare i bit che sto cercando. Inferenza di tipo direi che è un grande vantaggio per leggere molto più che scrivere codice.
Jimmy Hoffa,

Una volta trovato il pezzo di codice che sto cercando, allora potrei iniziare a controllarlo ma in qualsiasi momento non dovresti concentrarti su più di forse 10 righe di codice, a quel punto non è una seccatura fare il deduci te stesso perché stai mentalmente selezionando l'intero blocco a parte per iniziare, e probabilmente usando gli strumenti per aiutarti comunque. Capire i tipi delle 10 righe di codice su cui stai tentando di fare la zona raramente richiede molto tempo, ma questa è la parte in cui sei passato dalla lettura alla scrittura, che è comunque molto meno comune.
Jimmy Hoffa,

Ricorda che anche se un programmatore legge il codice più spesso di quanto non lo scriva, ciò non significa che un pezzo di codice venga letto più spesso di quanto scritto. Un sacco di codice può essere di breve durata o non essere mai più letto e potrebbe non essere facile dire quale codice sopravviverà e dovrebbe essere scritto con la massima leggibilità.
jpa,

2
Elaborando il primo punto di @JimmyHoffa, considera la lettura in generale. Una frase è più facile da analizzare e leggere, per non parlare della comprensione, quando si concentra sulla parte del discorso delle sue singole parole? "La mucca (articolo) (sostantivo-singolare) ha saltato (verbo-passato) sopra (preposizione) la luna (articolo) (sostantivo). (Periodo di punteggiatura)".
Zev Spitz,

Risposte:


46

Diamo un'occhiata a Java. Java non può avere variabili con tipi dedotti. Ciò significa che spesso devo precisare il tipo, anche se è perfettamente ovvio per un lettore umano quale sia il tipo:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

E a volte è semplicemente fastidioso precisare l'intero tipo.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Questa tipizzazione statica dettagliata mi ostacola, programmatore. La maggior parte delle annotazioni di tipo sono ripetitivi riempimenti di riga, rigurgitazioni senza contenuto di ciò che già conosciamo. Tuttavia, mi piace la tipizzazione statica, poiché può davvero aiutare a scoprire i bug, quindi usare la digitazione dinamica non è sempre una buona risposta. L'inferenza del tipo è il migliore dei due mondi: posso omettere i tipi irrilevanti, ma essere sicuro che il mio programma (tipo-) verifichi.

Sebbene l'inferenza del tipo sia davvero utile per le variabili locali, non dovrebbe essere utilizzata per le API pubbliche che devono essere documentate in modo inequivocabile. E a volte i tipi sono davvero fondamentali per capire cosa sta succedendo nel codice. In tali casi, sarebbe sciocco fare affidamento solo sull'inferenza del tipo.

Esistono molte lingue che supportano l'inferenza del tipo. Per esempio:

  • C ++. La autoparola chiave attiva l'inferenza del tipo. Senza di essa, sillabare i tipi di lambda o per le voci nei contenitori sarebbe un inferno.

  • C #. È possibile dichiarare variabili con var, che innesca una forma limitata di inferenza di tipo. Gestisce ancora la maggior parte dei casi in cui si desidera l'inferenza del tipo. In alcuni punti è possibile tralasciare completamente il tipo (ad es. In lambdas).

  • Haskell e qualsiasi lingua della famiglia ML. Mentre il sapore specifico dell'inferenza di tipo usato qui è abbastanza potente, spesso vedi ancora annotazioni di tipo per funzioni, e per due motivi: il primo è la documentazione, e il secondo è un controllo che l'inferenza di tipo ha effettivamente trovato i tipi che ti aspettavi. Se c'è una discrepanza, probabilmente c'è qualche tipo di bug.


13
Si noti inoltre che C # ha tipi anonimi, ovvero tipi senza nome, ma C # ha un sistema di tipi nominale, ovvero un sistema di tipi basato sui nomi. Senza l'inferenza del tipo, quei tipi non potrebbero mai essere usati!
Jörg W Mittag,

10
Alcuni esempi sono un po 'inventati, secondo me. L'inizializzazione a 42 non significa automaticamente che la variabile è una int, potrebbe essere qualsiasi tipo numerico compreso anche char. Inoltre non vedo perché vorresti precisare l'intero tipo per Entryquando puoi semplicemente digitare il nome della classe e lasciare che il tuo IDE esegua l'importazione necessaria. L'unico caso in cui devi precisare l'intero nome è quando hai una classe con lo stesso nome nel tuo pacchetto. Ma mi sembra comunque un cattivo design.
Malcolm,

10
@Malcolm Sì, tutti i miei esempi sono inventati. Servono per illustrare un punto. Quando ho scritto l' intesempio, stavo pensando al (a mio avviso comportamento piuttosto sano) della maggior parte delle lingue che presentano un'inferenza di tipo. Di solito inferiscono una into Integerqualunque cosa venga chiamata in quella lingua. Il bello dell'inferenza del tipo è che è sempre facoltativo; puoi comunque specificare un tipo diverso se ne hai bisogno. Per quanto riguarda l' Entryesempio: buon punto, lo sostituirò con Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java non ha nemmeno alias di tipo :(
amon,

4
@ m3th0dman Se il tipo è importante per la comprensione, puoi ancora menzionarlo esplicitamente. L'inferenza del tipo è sempre facoltativa. Ma qui, il tipo di colKeyè sia ovvio che irrilevante: ci interessa solo che sia adatto come secondo argomento di doSomethingWith. Se dovessi estrarre quel loop in una funzione che produce un Iterable di (key1, key2, value)-plastiche, la firma più generale sarebbe <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). All'interno di quella funzione, il tipo reale di colKey( Integer, non K2) è assolutamente irrilevante.
amon,

4
@ m3th0dman è un'affermazione piuttosto ampia, in cui il codice " maggior parte " è questo o quello. Statistiche aneddotiche. C'è sicuramente nessun punto nella scrittura del tipo due volte in inizializzatori: View.OnClickListener listener = new View.OnClickListener(). Sapresti ancora il tipo anche se il programmatore fosse "pigro" e lo abbreviasse var listener = new View.OnClickListener(se ciò fosse possibile). Questo tipo di ridondanza è comune - non voglio rischiare una stima approssimativa qui - e la rimozione non derivano da pensare a futuri lettori. Ogni funzionalità linguistica dovrebbe essere usata con cura, non lo sto mettendo in discussione.
Konrad Morawski,

26

È vero che il codice viene letto molto più spesso di quanto non sia scritto. Tuttavia, anche la lettura richiede tempo e due schermate di codice sono più difficili da navigare e leggere rispetto a una schermata di codice, quindi è necessario stabilire le priorità per impacchettare il miglior rapporto informazioni utili / sforzo di lettura. Questo è un principio generale UX: troppe informazioni contemporaneamente travolgono e degradano effettivamente l'efficacia dell'interfaccia.

Ed è la mia esperienza che spesso il tipo esatto non è (quello) importante. Sicuramente a volte espressioni nido: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Ognuno di questi contiene sottoespressioni che valutano un valore il cui tipo non è scritto. Eppure sono perfettamente chiari. Va bene lasciare il tipo non dichiarato perché è abbastanza facile da capire dal contesto e scriverlo farebbe più male che bene (ingombrare la comprensione del flusso di dati, prendere schermo prezioso e spazio di memoria a breve termine).

Una ragione per non annidare espressioni come se non ci fosse un domani è che le linee si allungano e il flusso di valori diventa poco chiaro. L'introduzione di una variabile temporanea aiuta in questo, impone un ordine e dà un nome a un risultato parziale. Tuttavia, non tutto ciò che beneficia di questi aspetti beneficia anche della descrizione del suo tipo:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Importa se userun oggetto entità, un numero intero, una stringa o qualcos'altro? Per la maggior parte, non è sufficiente sapere che rappresenta un utente, proviene dalla richiesta HTTP e viene utilizzato per recuperare il nome da visualizzare nell'angolo in basso a destra della risposta.

E quando si fa materia, l'autore è libero di scrivere il tipo. Questa è una libertà che deve essere utilizzata in modo responsabile, ma lo stesso vale per tutto ciò che può migliorare la leggibilità (nomi di variabili e funzioni, formattazione, progettazione dell'API, spazio bianco). E in effetti, la convenzione di Haskell e ML (dove tutto può essere dedotto senza ulteriore sforzo) è quella di scrivere i tipi di funzioni di funzioni non locali e anche di variabili e funzioni locali ogni volta che è opportuno. Solo i novizi lasciano dedurre ogni tipo.


2
+1 Questa dovrebbe essere la risposta accettata. Va dritto al cuore del perché l'inferenza del tipo è una grande idea.
Christian Hayter,

Il tipo esatto di userimporta se stai cercando di estendere la funzione, perché determina cosa puoi fare con user. Questo è importante se si desidera aggiungere un controllo di integrità (a causa di una vulnerabilità della sicurezza, ad esempio) o se si è dimenticato di dover effettivamente fare qualcosa con l'utente oltre a visualizzarlo. È vero, questo tipo di lettura per l'espansione è meno frequente della semplice lettura del codice, ma è anche una parte vitale del nostro lavoro.
cmaster

@cmaster E puoi sempre scoprire quel tipo piuttosto facilmente (la maggior parte degli IDE te lo dirà, e c'è la soluzione a bassa tecnologia di causare intenzionalmente un errore di tipo e lasciare che il compilatore stampi il tipo reale), è semplicemente fuori mano quindi non ti infastidisce nel caso comune.

4

Penso che l'inferenza del tipo sia abbastanza importante e dovrebbe essere supportata in qualsiasi linguaggio moderno. Sviluppiamo tutti in IDE e potrebbero aiutare molto nel caso in cui tu voglia conoscere il tipo inferito, solo pochi di noi entrano in gioco vi. Pensa alla verbosità e al codice della cerimonia in Java, ad esempio.

  Map<String,HashMap<String,String>> map = getMap();

Ma puoi dire che va bene il mio IDE mi aiuterà, potrebbe essere un punto valido. Tuttavia, alcune funzionalità non sarebbero disponibili senza l'aiuto dell'inferenza di tipo, ad esempio tipi anonimi C #.

 var person = new {Name="John Smith", Age = 105};

Linq non sarebbe bello come lo è ora senza l'aiuto dell'inferenza del tipo, Selectper esempio

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Questo tipo anonimo verrà inferito ordinatamente alla variabile.

Non mi piace l'inferenza dei tipi sui tipi restituiti Scalaperché penso che il tuo punto si applichi qui, dovrebbe essere chiaro per noi cosa restituisce una funzione in modo da poter usare l'API in modo più fluido


Map<String,HashMap<String,String>>? Certo, se non stai usando i tipi, poi spiegarli ha pochi benefici. Table<User, File, String>è però più informativo e ci sono dei vantaggi nello scriverlo.
MikeFHay

4

Penso che la risposta a questa domanda sia davvero semplice: salva la lettura e la scrittura di informazioni ridondanti. Soprattutto nei linguaggi orientati agli oggetti in cui hai un tipo su entrambi i lati del segno di uguale.

Che ti dice anche quando dovresti o non dovresti usarlo - quando le informazioni non sono ridondanti.


3
Beh, tecnicamente, le informazioni sono sempre ridondanti quando è possibile omettere le firme manuali: altrimenti il ​​compilatore non sarebbe in grado di inferirle! Ma capisco cosa intendi: quando duplichi una firma su più punti in un'unica vista, è davvero ridondante per il cervello , mentre alcuni tipi ben posizionati forniscono informazioni che dovresti cercare a lungo, possibilmente con un buone molte trasformazioni non visibili.
leftaroundabout

@leftaroundabout: ridondante quando letto dal programmatore.
jmoreno,

3

Supponiamo che uno veda il codice:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Se someBigLongGenericTypeè assegnabile dal tipo restituito di someFactoryMethod, con quale probabilità qualcuno che legge il codice dovrebbe notare se i tipi non corrispondono esattamente e quanto facilmente qualcuno che ha notato la discrepanza può riconoscere se era intenzionale o no?

Consentendo l'inferenza, una lingua può suggerire a qualcuno che sta leggendo il codice che quando il tipo di una variabile viene esplicitamente dichiarato, la persona dovrebbe cercare di trovare una ragione per questo. Questo a sua volta consente alle persone che stanno leggendo il codice di focalizzare meglio i propri sforzi. Se, al contrario, la stragrande maggioranza delle volte in cui viene specificato un tipo, sembra esattamente lo stesso di quello che sarebbe stato inferito, allora qualcuno che sta leggendo il codice potrebbe essere meno incline a notare i tempi in cui è leggermente diverso .


2

Vedo che ci sono già diverse risposte eccellenti. Alcuni dei quali ripeterò, ma a volte vuoi solo mettere le cose con parole tue. Commenterò alcuni esempi del C ++ perché è il linguaggio con cui ho più familiarità.

Ciò che è necessario non è mai poco saggio. L'inferenza del tipo è necessaria per rendere pratiche le funzionalità di altre lingue. In C ++ è possibile avere tipi indicibili.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 ha aggiunto lambda che sono anche indicibili.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

L'inferenza del tipo è anche alla base dei modelli.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Ma le tue domande erano "perché, programmatore, dovrei dedurre il tipo delle mie variabili quando leggo il codice? Non è più veloce per chiunque leggere il tipo piuttosto che pensare che tipo c'è?"

L'inferenza del tipo rimuove la ridondanza. Quando si tratta di leggere il codice, a volte può essere più veloce e più semplice avere informazioni ridondanti nel codice, ma la ridondanza può oscurare le informazioni utili . Per esempio:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Non ci vuole molta familiarità con la libreria standard per un programmatore C ++ per identificare che io sono un iteratore, i = v.begin()quindi la dichiarazione esplicita del tipo ha un valore limitato. Per la sua presenza oscura i dettagli che sono più importanti (come quelli che iindicano l'inizio del vettore). La bella risposta di @amon fornisce un esempio ancora migliore di verbosità che oscura dettagli importanti. Al contrario, l'uso dell'inferenza di tipo dà maggiore risalto ai dettagli importanti.

std::vector<int> v;
auto i = v.begin();

Sebbene la lettura del codice sia importante, non è sufficiente, ad un certo punto dovrai interrompere la lettura e iniziare a scrivere un nuovo codice. La ridondanza nel codice rende la modifica del codice più lenta e più difficile. Ad esempio, supponiamo che io abbia il seguente frammento di codice:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Nel caso in cui ho bisogno di cambiare il tipo di valore del vettore per raddoppiare la modifica del codice in:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

In questo caso, devo modificare il codice in due punti. Contrasto con l'inferenza del tipo in cui il codice originale è:

std::vector<int> v;
auto i = v.begin();

E il codice modificato:

std::vector<double> v;
auto i = v.begin();

Nota che ora devo solo cambiare una riga di codice. Estrapolarlo a un programma di grandi dimensioni e l'inferenza dei tipi può propagare le modifiche ai tipi molto più rapidamente di quanto sia possibile con un editor.

La ridondanza nel codice crea la possibilità di bug. Ogni volta che il codice dipende da due informazioni che vengono mantenute equivalenti, esiste la possibilità di errore. Ad esempio, c'è un'incongruenza tra i due tipi in questa affermazione che probabilmente non è prevista:

int pi = 3.14159;

La ridondanza rende più difficile discernere l'intenzione. In alcuni casi l'inferenza del tipo può essere più facile da leggere e comprendere perché è più semplice della specifica esplicita del tipo. Considera il frammento di codice:

int y = sq(x);

Nel caso in cui sq(x)restituisca un int, non è ovvio se ysia un intperché è il tipo restituito sq(x)o perché si adatta alle istruzioni che usano y. Se cambio altro codice in modo tale che sq(x)non ritorni più int, da quella riga è incerto se il tipo di ydebba essere aggiornato. Contrasto con lo stesso codice ma utilizzando l'inferenza del tipo:

auto y = sq(x);

In questo l'intento è chiaro, ydeve essere dello stesso tipo restituito da sq(x). Quando il codice cambia il tipo di ritorno di sq(x), il tipo di ymodifica si adatta automaticamente.

In C ++ c'è un secondo motivo per cui l'esempio sopra è più semplice con l'inferenza di tipo, l'inferenza di tipo non può introdurre una conversione di tipo implicita. Se il tipo restituito sq(x)non è int, il compilatore inserisce silenziosamente una conversione implicita in int. Se il tipo restituito sq(x)è un tipo complesso che definisce operator int(), questa chiamata di funzione nascosta può essere arbitrariamente complessa.


Piuttosto un buon punto sui tipi indicibili in C ++. Tuttavia, penso che sia meno un motivo per aggiungere un'inferenza di tipo, piuttosto che un motivo per correggere la lingua. Nel primo caso che presenti, il programmatore dovrebbe semplicemente dare alla cosa un nome per evitare di usare l'inferenza di tipo, quindi questo non è un esempio forte. Il secondo esempio è valido solo perché il C ++ proibisce esplicitamente che i tipi lambda siano enunciabili, persino la dattilografia con l'uso di typeofè resa inutile dal linguaggio. E questo è un deficit della lingua stessa che dovrebbe essere risolto secondo me.
cmaster
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.