Perché è utile l'inferenza di tipo?

34

Ho letto codice 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 che presumo è meno verbosità e meno codice scritto. Ma d'altra parte se leggi il codice più spesso, probabilmente vorrai codice leggibile.

Il compilatore deduce il tipo; ci sono vecchi algoritmi per questo. Ma la vera domanda è perché dovrei, il programmatore, voler 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: in 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.

    
posta m3th0dman 28.09.2014 - 15:30
fonte

6 risposte

45

Diamo un'occhiata a Java. Java non può avere variabili con tipi dedotti. Ciò significa che spesso devo specificare il tipo, anche se è perfettamente ovvio per un lettore umano qual è 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 compitare 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 prolissa digitazione statica interferisce con me, il programmatore. La maggior parte delle annotazioni di tipo sono ripetitive line-filler, regenerationtions senza contenuto di ciò che già sappiamo. Tuttavia, mi piace la digitazione statica, poiché può davvero aiutare a scoprire bug, quindi l'utilizzo della digitazione dinamica non è sempre una buona risposta. L'inferenza di tipo è il migliore di entrambi i mondi: posso omettere i tipi irrilevanti, ma essere sicuro che il mio programma (tipo-) controlli.

Mentre l'inferenza di tipo è veramente utile per le variabili locali, non dovrebbe essere usata per le API pubbliche che devono essere documentate in modo non ambiguo. E a volte i tipi sono davvero fondamentali per capire cosa sta succedendo nel codice. In questi casi, sarebbe sciocco fare affidamento solo sull'inferenza di tipo.

Esistono molte lingue che supportano l'inferenza di tipo. Ad esempio:

  • C ++. La parola chiave auto attiva l'inferenza di tipo. Senza di esso, l'ortografia dei tipi per lambda o per le voci in contenitori sarebbe un inferno.

  • C #. Puoi dichiarare le variabili con var , che innesca una forma limitata di inferenza di tipo. Gestisce ancora la maggior parte dei casi in cui si desidera inferire il tipo. In alcuni punti puoi omettere completamente il tipo (ad esempio in lambda).

  • Haskell e qualsiasi lingua nella famiglia ML. Mentre il sapore specifico dell'inferenza di tipo qui usata è piuttosto potente, si vedono spesso le annotazioni di tipo per le 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 ci si aspettava. Se c'è una discrepanza, è probabile che ci sia un qualche tipo di errore.

risposta data 28.09.2014 - 16:18
fonte
25

È vero che il codice viene letto molto più spesso di quanto non sia scritto. Tuttavia, la lettura richiede anche del tempo e due schermate di codice sono più difficili da navigare e leggere di una schermata di codice, quindi è necessario dare la priorità per comprimere il miglior rapporto informazioni utili / lettura-sforzo. Questo è un principio UX generale: troppe informazioni in una volta travolgono e in realtà riducono l'efficacia dell'interfaccia.

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

Un motivo per non annidare espressioni come non c'è domani è che le linee si allungano e il flusso di valori diventa poco chiaro. L'introduzione di una variabile temporanea aiuta con questo, impone un ordine e dà un nome a un risultato parziale. Tuttavia, non tutto ciò che trae vantaggio da questi aspetti trae vantaggio dall'espressione del suo tipo:

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

Importa se user è un oggetto entità, un intero, una stringa o qualcos'altro? Per la maggior parte degli scopi, non è sufficiente, è 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 è importante , 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 infatti, la convenzione in Haskell e ML (dove tutto può essere dedotta senza sforzi aggiuntivi) è scrivere i tipi di funzioni di funzioni non locali, e anche di variabili e funzioni locali ogni volta che è opportuno. Solo i novizi consentono di desumere ogni tipo.

    
risposta data 28.09.2014 - 16:21
fonte
4

Penso che l'inferenza di tipo sia abbastanza importante e dovrebbe essere supportata in qualsiasi linguaggio moderno. Tutti sviluppiamo in IDE e potrebbero essere di grande aiuto nel caso in cui si desideri conoscere il tipo desunto, solo alcuni di noi si incastrano in vi . Pensa a verbosità e codice della cerimonia in Java, per 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 lì senza l'aiuto dell'inferenza di tipo, i tipi anonimi C # per esempio.

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

Linq non sarebbe bello come adesso senza l'aiuto dell'inferenza di tipo, Select per esempio

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

Questo tipo anonimo verrà dedotto in modo preciso alla variabile.

Non mi piace l'inferenza di tipo sui tipi di ritorno in Scala perché penso che il tuo punto si applichi qui, dovrebbe essere chiaro per noi cosa restituisce una funzione in modo da poter utilizzare l'API in modo più fluido

    
risposta data 28.09.2014 - 16:12
fonte
4

Penso che la risposta a questo sia davvero semplice: salva la lettura e la scrittura di informazioni ridondanti. In particolare nelle lingue orientate 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.

    
risposta data 28.09.2014 - 21:37
fonte
3

Supponiamo che uno veda il codice:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Se someBigLongGenericType è assegnabile dal tipo di ritorno di someFactoryMethod , quanto è probabile che qualcuno leggendo il codice si accorga se i tipi non coincidono con precisione e con quale prontezza potrebbe qualcuno che ha notato la discrepanza riconoscere se è stato intenzionale o no?

Consentendo l'inferenza, una lingua può suggerire a qualcuno che sta leggendo il codice che quando il tipo di una variabile è esplicitamente dichiarato, la persona dovrebbe cercare di trovare una ragione per essa. Ciò a sua volta consente alle persone che stanno leggendo il codice di concentrare meglio i loro sforzi. Se, al contrario, la maggior parte delle volte in cui viene specificato un tipo, è esattamente la stessa cosa che si sarebbe dedotta, quindi qualcuno che sta leggendo il codice potrebbe essere meno incline a notare i tempi in cui è leggermente diverso .

    
risposta data 28.09.2014 - 22:34
fonte
2

Vedo che ci sono già una serie di buone risposte. Alcune delle quali ripeterò, ma a volte vuoi solo mettere le cose con parole tue. Commenterò con alcuni esempi di C ++ perché è la lingua con cui ho più familiarità.

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

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 di 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é dovrei, il programmatore, voler dedurre il tipo delle mie variabili quando leggo il codice? Non è più veloce per chiunque solo leggere il tipo che pensare che tipo c'è? "

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

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

Non richiede molta familiarità con la libreria standard per un programmatore C ++ per identificare che i è un iteratore da i = v.begin() , quindi la dichiarazione del tipo esplicito ha un valore limitato. Con la sua presenza oscura i dettagli che sono più importanti (ad esempio che i punti all'inizio del vettore). La bella risposta di @amon fornisce un esempio ancora migliore di verbosità che oscura dettagli importanti. Al contrario, l'inferenza di tipo dà maggiore risalto ai dettagli importanti.

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

Mentre leggere il codice è importante, non è sufficiente, a un certo punto dovrai smettere di leggere e iniziare a scrivere un nuovo codice. La ridondanza nel codice rende la modifica del codice più lenta e più difficile. Ad esempio, supponiamo di avere 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 cambiando il 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 inferenza di 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 digitare l'inferenza 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 tuo codice dipende da due informazioni mantenute equivalenti, c'è una possibilità di errore. Ad esempio, c'è un'incongruenza tra i due tipi in questa affermazione che probabilmente non è intesa:

int pi = 3.14159;

La ridondanza rende più difficile discernere le intenzioni. In alcuni casi l'inferenza di tipo può essere più facile da leggere e capire perché è più semplice delle specifiche di tipo esplicite. Considera il frammento di codice:

int y = sq(x);

Nel caso in cui sq(x) restituisca un int , non è ovvio se y è un int perché è il tipo di ritorno di sq(x) o perché si adatta alle istruzioni che usano y . Se cambio altro codice in modo che sq(x) non restituisca più int , è incerto da quella sola riga se il tipo di y debba essere aggiornato. Contrasto con lo stesso codice ma usando inferenza di tipo:

auto y = sq(x);

In questo l'intento è chiaro, y deve essere dello stesso tipo restituito da sq(x) . Quando il codice cambia il tipo di ritorno di sq(x) , il tipo di y cambia per corrispondere automaticamente.

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

    
risposta data 18.08.2016 - 07:59
fonte

Leggi altre domande sui tag