Warum ist die Typinferenz nützlich?

37

Ich lese Code viel häufiger als ich Code schreibe, und ich gehe davon aus, dass die meisten Programmierer, die an industrieller Software arbeiten, dies tun. Der Vorteil von Typinferenz, den ich annehme, ist weniger Ausführlichkeit und weniger geschriebener Code. Wenn Sie jedoch häufiger Code lesen, möchten Sie wahrscheinlich lesbaren Code.

Der Compiler fügt den Typ hinzu. Dafür gibt es alte Algorithmen. Die eigentliche Frage ist jedoch, warum ich als Programmierer den Typ meiner Variablen ableiten möchte, wenn ich den Code lese. Ist es nicht schneller für jemanden, nur den Typ zu lesen, als zu überlegen, welcher Typ vorhanden ist?

Edit: Abschließend verstehe ich, warum es sinnvoll ist. Aber in der Kategorie der Sprachfunktionen sehe ich es in einem Eimer mit Überladung des Operators - nützlich in einigen Fällen, aber die Lesbarkeit beeinträchtigend, wenn es missbraucht wird.

m3th0dman
quelle
5
Meiner Erfahrung nach sind Typen beim Schreiben von Code weitaus wichtiger als beim Lesen. Beim Lesen von Code suche ich nach Algorithmen und bestimmten Blöcken, zu denen mich normalerweise gut benannte Variablen leiten. Ich muss wirklich keinen Prüfcode eingeben, nur um zu lesen und zu verstehen, was es tut, es sei denn, es ist schrecklich schlecht geschrieben. Wenn Sie jedoch Code lesen, der mit unnötigen Details überfüllt ist, nach denen ich nicht suche (wie zu viele Typanmerkungen), wird es oft schwieriger, die gesuchten Teile zu finden. Tippfehler Ich würde sagen, es ist ein großer Vorteil, weit mehr zu lesen als Code zu schreiben.
Jimmy Hoffa
Sobald ich den Code gefunden habe, nach dem ich suche, kann es sein, dass ich anfange, ihn zu überprüfen, aber zu einem bestimmten Zeitpunkt sollten Sie sich nicht auf mehr als 10 Codezeilen konzentrieren Schliessen Sie selbst, weil Sie den gesamten Block zunächst im Kopf auseinander nehmen und wahrscheinlich trotzdem Werkzeug verwenden, um dies zu tun. Das Herausfinden der Typen der 10 Codezeilen, auf die Sie sich konzentrieren möchten, nimmt selten viel Zeit in Anspruch, aber in diesem Teil haben Sie ohnehin vom Lesen zum Schreiben gewechselt, was ohnehin viel seltener vorkommt.
Jimmy Hoffa
Denken Sie daran, dass ein Programmierer Code zwar häufiger liest als schreibt, jedoch nicht bedeutet, dass ein Teil des Codes häufiger gelesen als geschrieben wird. Viele Codes sind möglicherweise kurzlebig oder werden auf andere Weise nie wieder gelesen, und es ist möglicherweise nicht leicht zu sagen, welcher Code überlebt und mit maximaler Lesbarkeit geschrieben werden sollte.
jpa
2
Erwägen Sie, auf den ersten Punkt von @JimmyHoffa einzugehen, das Lesen im Allgemeinen. Ist es einfacher, einen Satz zu analysieren und zu lesen, geschweige denn zu verstehen, wenn man sich auf die Wortart seiner einzelnen Wörter konzentriert? "Die (Artikel) Kuh (Substantiv-Singular) sprang (Verb-Vergangenheit) über (Präposition) den (Artikel) Mond (Substantiv). (Interpunktionsperiode)".
Zev Spitz

Antworten:

46

Werfen wir einen Blick auf Java. Java kann keine Variablen mit abgeleiteten Typen haben. Dies bedeutet, dass ich den Typ häufig buchstabieren muss, auch wenn es für einen menschlichen Leser völlig offensichtlich ist, um welchen Typ es sich handelt:

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<>();

Und manchmal ist es einfach ärgerlich, den ganzen Typ zu buchstabieren.

// 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);
    }
}

Diese wortreiche statische Eingabe behindert mich, den Programmierer. Bei den meisten Typanmerkungen handelt es sich um sich wiederholende Zeilenumbrüche, die keine Inhalte enthalten, wie wir sie bereits kennen. Ich mag jedoch das statische Tippen, da es beim Erkennen von Fehlern sehr hilfreich sein kann. Daher ist die Verwendung des dynamischen Tippens nicht immer eine gute Antwort. Typinferenz ist das Beste aus beiden Welten: Ich kann die irrelevanten Typen weglassen, aber trotzdem sicher sein, dass mein Programm (Typ-) auscheckt.

Während die Typinferenz für lokale Variablen sehr nützlich ist, sollte sie nicht für öffentliche APIs verwendet werden, die eindeutig dokumentiert werden müssen. Und manchmal sind die Typen wirklich wichtig, um zu verstehen, was im Code vor sich geht. In solchen Fällen wäre es töricht, sich nur auf die Typinferenz zu verlassen.

Es gibt viele Sprachen, die Typinferenz unterstützen. Beispielsweise:

  • C ++. Das autoSchlüsselwort löst eine Typinferenz aus. Ohne das wäre es die Hölle, die Typen für Lambdas oder für Einträge in Containern zu buchstabieren.

  • C #. Sie können Variablen mit deklarieren var, was eine begrenzte Form der Typinferenz auslöst. Es verwaltet immer noch die meisten Fälle, in denen Sie Typinferenz möchten. An bestimmten Stellen kann man den Typ komplett weglassen (zB bei Lambdas).

  • Haskell und jede Sprache in der ML-Familie. Obwohl die hier verwendete spezifische Variante der Typinferenz ziemlich mächtig ist, werden immer noch häufig Typanmerkungen für Funktionen angezeigt, und zwar aus zwei Gründen: Die erste ist die Dokumentation, und die zweite ist eine Überprüfung, ob die Typinferenz tatsächlich die erwarteten Typen gefunden hat. Wenn es eine Diskrepanz gibt, gibt es wahrscheinlich eine Art Fehler.

amon
quelle
13
Beachten Sie auch, dass C # anonyme Typen hat, dh Typen ohne Namen, aber C # ein nominales Typensystem hat, dh ein auf Namen basierendes Typensystem. Ohne Typinferenz könnten diese Typen niemals verwendet werden!
Jörg W Mittag
10
Einige Beispiele sind meiner Meinung nach ein bisschen erfunden. Die Initialisierung auf 42 bedeutet nicht automatisch, dass es sich bei der Variablen um eine intbeliebige numerische Variable handelt , einschließlich einer geraden char. Ich verstehe auch nicht, warum Sie den gesamten Typ für den buchstabieren möchten, Entrywenn Sie nur den Klassennamen eingeben und Ihre IDE den erforderlichen Import durchführen lassen können. Der einzige Fall, in dem Sie den ganzen Namen buchstabieren müssen, ist, wenn Sie eine Klasse mit demselben Namen in Ihrem eigenen Paket haben. Aber es scheint mir trotzdem ein schlechtes Design zu sein.
Malcolm
10
@ Malcolm Ja, alle meine Beispiele sind erfunden. Sie dienen zur Veranschaulichung eines Punktes. Als ich das intBeispiel schrieb , dachte ich über das (meiner Meinung nach ziemlich vernünftige) Verhalten der meisten Sprachen nach, bei denen es sich um Typinferenz handelt. Sie schließen normalerweise auf ein intoder Integeroder was auch immer es in dieser Sprache heißt. Das Schöne an der Typinferenz ist, dass sie immer optional ist. Sie können immer noch einen anderen Typ angeben, wenn Sie einen benötigen. In Bezug auf das EntryBeispiel: Guter Punkt, ich werde es durch ersetzen Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java hat nicht einmal Typ-Aliase :(
amon
4
@ m3th0dman Wenn der Typ für das Verständnis wichtig ist, können Sie ihn trotzdem explizit erwähnen. Typinferenz ist immer optional. Aber hier ist die Art von colKeysowohl offensichtlich als auch irrelevant: Wir kümmern uns nur darum, dass es als zweites Argument von geeignet ist doSomethingWith. Wenn ich diese Schleife in eine Funktion extrahieren (key1, key2, value)würde, die eine Iterable von -triples ergibt, wäre die allgemeinste Signatur <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Innerhalb dieser Funktion ist die tatsächliche Art von colKey( Integer, nicht K2) absolut irrelevant.
amon
4
@ m3th0dman es ist eine ziemlich umfassende Aussage, dass "der meiste " Code dies oder das ist. Anekdotenstatistik. Es gibt sicherlich keinen Sinn , die Art zweimal in initializers schriftlich wie folgt : View.OnClickListener listener = new View.OnClickListener(). Sie würden den Typ immer noch kennen, auch wenn der Programmierer "faul" wäre und ihn auf "verkürzt" var listener = new View.OnClickListener(falls dies möglich war). Diese Art von Redundanz ist üblich - Ich werde keine guesstimate hier riskieren - und entfernen sie sich von Stammzellen über Leser Zukunft zu denken. Jedes Sprachmerkmal sollte mit Vorsicht verwendet werden, das stelle ich nicht in Frage.
Konrad Morawski
26

Es ist wahr, dass Code viel öfter gelesen wird als geschrieben. Das Lesen nimmt jedoch auch Zeit in Anspruch, und zwei Codebildschirme sind schwerer zu navigieren und zu lesen als ein Codebildschirm. Daher müssen wir Prioritäten setzen, um das beste Verhältnis zwischen nützlichen Informationen und Leseaufwand zu erzielen. Dies ist ein allgemeines UX-Prinzip: Zu viele Informationen überwältigen und beeinträchtigen die Wirksamkeit der Benutzeroberfläche.

Und es ist meine Erfahrung, dass oft der genaue Typ nicht wichtig ist. Sicher haben Sie manchmal Ausdrücke Nest: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Jeder dieser Ausdrücke enthält Unterausdrücke, die einen Wert ergeben, dessen Typ nicht ausgeschrieben ist. Sie sind jedoch vollkommen klar. Wir sind damit einverstanden, den Typ nicht angegeben zu lassen, da es einfach genug ist, aus dem Kontext herauszufinden, und das Ausschreiben würde mehr schaden als nützen (das Verständnis des Datenflusses beeinträchtigen, wertvollen Bildschirm- und Kurzzeitspeicherplatz in Anspruch nehmen).

Ein Grund, Ausdrücke nicht zu verschachteln, als gäbe es kein Morgen, ist, dass die Zeilen lang werden und der Wertefluss unklar wird. Das Einführen einer temporären Variablen hilft dabei, erlegt eine Reihenfolge auf und benennt ein Teilergebnis. Nicht alles, was von diesen Aspekten profitiert, profitiert auch davon, dass sein Typ festgelegt wird:

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

Ist es wichtig, ob useres sich um ein Entitätsobjekt, eine Ganzzahl, eine Zeichenfolge oder etwas anderes handelt? In den meisten Fällen reicht es nicht aus, zu wissen, dass es einen Benutzer darstellt, aus der HTTP-Anforderung stammt und zum Abrufen des Namens verwendet wird, der in der unteren rechten Ecke der Antwort angezeigt werden soll.

Und wenn es tut Angelegenheit, ist der Autor frei , die Art zu schreiben. Dies ist eine Freiheit, die verantwortungsbewusst genutzt werden muss, aber das Gleiche gilt für alles andere, was die Lesbarkeit verbessern kann (Variablen- und Funktionsnamen, Formatierung, API-Design, Leerraum). Tatsächlich besteht die Konvention in Haskell und ML (wo alles ohne zusätzlichen Aufwand geschlossen werden kann) darin, die Typen von nicht-lokalen Funktionsfunktionen und gegebenenfalls auch von lokalen Variablen und Funktionen aufzuschreiben. Nur Anfänger lassen auf jeden Typ schließen.


quelle
2
+1 Dies sollte die akzeptierte Antwort sein. Es geht genau darum, warum Typinferenz eine großartige Idee ist.
Christian Hayter
Die genaue Art userspielt eine Rolle, wenn Sie versuchen, die Funktion zu erweitern, da sie bestimmt, was Sie mit der tun können user. Dies ist wichtig, wenn Sie eine Sicherheitsüberprüfung hinzufügen möchten (z. B. aufgrund einer Sicherheitsanfälligkeit) oder vergessen haben, dass Sie mit dem Benutzer nicht nur eine Anzeige vornehmen müssen. Diese Art des Lesens zur Erweiterung ist zwar seltener als nur das Lesen des Codes, aber sie sind auch ein wesentlicher Bestandteil unserer Arbeit.
CMASTER
@cmaster Und Sie können diesen Typ immer ziemlich einfach herausfinden (die meisten IDEs werden es Ihnen sagen, und es gibt die Low-Tech-Lösung, absichtlich einen Typfehler zu verursachen und den Compiler den tatsächlichen Typ drucken zu lassen) ärgert dich nicht im allgemeinen Fall.
4

Ich halte Typinferenz für sehr wichtig und sollte in jeder modernen Sprache unterstützt werden. Wir alle entwickeln uns in IDEs und sie könnten sehr hilfreich sein, falls Sie den abgeleiteten Typ kennenlernen möchten, nur wenige von uns hacken sich ein vi. Denken Sie zum Beispiel an Ausführlichkeit und Zeremoniencode in Java.

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

Aber Sie können sagen, es ist in Ordnung, meine IDE wird mir helfen, es könnte ein gültiger Punkt sein. Einige Funktionen wären jedoch ohne die Hilfe von Typinferenz nicht verfügbar, z. B. anonyme C # -Typen.

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

Linq wäre nicht so schön, wie es jetzt ohne die Hilfe von Typinferenz ist, Selectzum Beispiel

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

Dieser anonyme Typ wird sauber in die Variable übernommen.

Ich mag keine Typinferenz bei Rückgabetypen, Scalada ich denke, dass Ihr Punkt hier zutrifft. Es sollte uns klar sein, was eine Funktion zurückgibt, damit wir die API flüssiger verwenden können

Sleiman Jneidi
quelle
Map<String,HashMap<String,String>>? Sicher, wenn Sie nicht mit Typen, sie dann Buchstabieren hat wenig Nutzen. Table<User, File, String>ist jedoch informativer, und es ist von Vorteil, es zu schreiben.
MikeFHay
4

Ich denke, die Antwort darauf ist wirklich einfach: Es erspart das Lesen und Schreiben redundanter Informationen. Besonders in objektorientierten Sprachen, in denen Sie auf beiden Seiten des Gleichheitszeichens eine Schrift haben.

Hier erfahren Sie auch, wann Sie es verwenden sollten oder nicht - wenn die Informationen nicht redundant sind.

jmoreno
quelle
3
Nun, technisch gesehen sind die Informationen immer überflüssig, wenn manuelle Signaturen weggelassen werden können. Andernfalls könnte der Compiler sie nicht ableiten! Aber ich verstehe, was Sie meinen: Wenn Sie eine Signatur nur an mehreren Stellen in einer einzigen Ansicht duplizieren , ist sie für das Gehirn wirklich überflüssig , während einige gut platzierte Typen Informationen liefern, nach denen Sie lange suchen müssten, möglicherweise mit einem gut viele nicht offensichtliche Transformationen.
Leftaroundabout
@leftaroundabout: Redundant, wenn vom Programmierer gelesen.
Jmoreno
3

Angenommen, man sieht den Code:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Wenn dies someBigLongGenericTypeanhand des Rückgabetyps zuweisbar ist someFactoryMethod, wie wahrscheinlich ist es, dass jemand, der den Code liest, dies bemerkt, wenn die Typen nicht genau übereinstimmen, und wie schnell kann jemand, der die Diskrepanz bemerkt hat, erkennen, ob es beabsichtigt war oder nicht?

Indem eine Sprache Rückschlüsse zulässt, kann sie jemandem, der Code liest, vorschlagen, dass die Person versuchen sollte, einen Grund dafür zu finden, wenn der Typ einer Variablen explizit angegeben wird. Dies wiederum ermöglicht es Leuten, die Code lesen, ihre Bemühungen besser zu fokussieren. Wenn im Gegensatz dazu die überwiegende Mehrheit der Fälle, in denen ein Typ angegeben wird, genau derselbe ist, auf den geschlossen wurde, ist es möglicherweise weniger anfällig, dass jemand, der Code liest, bemerkt, dass er sich geringfügig unterscheidet .

Superkatze
quelle
2

Ich sehe, dass es bereits einige gute Antworten gibt. Einige davon wiederhole ich, aber manchmal möchten Sie die Dinge einfach in Ihre eigenen Worte fassen. Ich werde einige Beispiele aus C ++ kommentieren, da dies die Sprache ist, mit der ich am vertrautesten bin.

Was notwendig ist, ist niemals unklug. Typinferenz ist notwendig, um andere Sprachfunktionen praktisch zu machen. In C ++ ist es möglich, unaussprechbare Typen zu haben.

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

C ++ 11 fügte Lambdas hinzu, die auch nicht ausgesprochen werden können.

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

Typinferenz untermauert auch Vorlagen.

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

Aber Ihre Fragen lauteten: "Warum sollte ich als Programmierer den Typ meiner Variablen ableiten wollen, wenn ich den Code lese? Ist es nicht schneller für jemanden, nur den Typ zu lesen, als zu überlegen, welcher Typ vorhanden ist?"

Typinferenz beseitigt Redundanz. Wenn es darum geht, Code zu lesen, kann es manchmal schneller und einfacher sein, redundante Informationen im Code zu haben, aber Redundanz kann die nützlichen Informationen überschatten . Beispielsweise:

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

Ein C ++ - Programmierer ist mit der Standardbibliothek nur wenig vertraut, um zu erkennen, dass es sich bei i um einen Iterator handelt, i = v.begin()sodass die explizite Typdeklaration nur einen begrenzten Wert hat. Durch sein Vorhandensein verdeckt es wichtigere Details (wie das, das iauf den Anfang des Vektors zeigt). Die feine Antwort von @amon liefert ein noch besseres Beispiel für die Ausführlichkeit, die wichtige Details in den Schatten stellt. Im Gegensatz dazu werden bei der Verwendung von Typinferenz die wichtigen Details stärker hervorgehoben.

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

Obwohl das Lesen von Code wichtig ist, reicht es nicht aus. Irgendwann müssen Sie aufhören zu lesen und mit dem Schreiben von neuem Code beginnen. Redundanz im Code macht das Ändern von Code langsamer und schwieriger. Angenommen, ich habe das folgende Codefragment:

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

In dem Fall, dass ich den Werttyp des Vektors ändern muss, um den Code zu verdoppeln, in:

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

In diesem Fall muss ich den Code an zwei Stellen ändern. Vergleichen Sie die Typinferenz mit dem Originalcode:

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

Und der geänderte Code:

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

Beachten Sie, dass ich nur noch eine Codezeile ändern muss. Wenn Sie dies auf ein großes Programm extrapolieren, kann die Typinferenz Änderungen an Typen viel schneller verbreiten als mit einem Editor.

Redundanz im Code schafft die Möglichkeit von Fehlern. Immer wenn Ihr Code davon abhängig ist, dass zwei Informationen gleichwertig sind, besteht die Möglichkeit eines Fehlers. Beispielsweise gibt es eine Inkonsistenz zwischen den beiden Typen in dieser Anweisung, die wahrscheinlich nicht beabsichtigt ist:

int pi = 3.14159;

Redundanz erschwert das Erkennen von Absichten. In einigen Fällen kann die Typinferenz einfacher zu lesen und zu verstehen sein, da sie einfacher ist als die explizite Typspezifikation. Betrachten Sie das Codefragment:

int y = sq(x);

In dem Fall, dass sq(x)ein zurückgegeben wird int, ist es nicht offensichtlich, ob yes sich um einen intRückgabetyp handelt sq(x)oder ob er zu den verwendeten Anweisungen passt y. Wenn ich anderen Code so ändere, dass er sq(x)nicht mehr zurückgegeben wird int, ist allein aus dieser Zeile nicht ersichtlich, ob der Typ von yaktualisiert werden soll. Vergleichen Sie den gleichen Code, verwenden Sie jedoch die Typinferenz:

auto y = sq(x);

In diesem ist die Absicht klar, ymuss der gleiche Typ sein, wie von zurückgegeben sq(x). Wenn der Code den Rückgabetyp sq(x)von yändert, wird der Änderungstyp automatisch angepasst.

In C ++ gibt es einen zweiten Grund, warum das obige Beispiel mit der Typinferenz einfacher ist. Die Typinferenz kann keine implizite Typkonvertierung einführen. Wenn der Rückgabetyp sq(x)nicht ist int, fügt der Compiler stillschweigend eine implizite Konvertierung in ein int. Wenn der Rückgabetyp von sq(x)ein Typ komplexer Typ ist, der definiert operator int(), kann dieser verborgene Funktionsaufruf beliebig komplex sein.

Bowie Owens
quelle
Ein guter Punkt zu den unaussprechlichen Typen in C ++. Ich denke jedoch, dass dies weniger ein Grund ist, eine Typinferenz hinzuzufügen, als vielmehr ein Grund, die Sprache zu korrigieren. Im ersten Fall, den Sie präsentieren, müsste der Programmierer dem Ding nur einen Namen geben, um die Verwendung von Typinferenz zu vermeiden. Dies ist also kein gutes Beispiel. Das zweite Beispiel ist nur deshalb so aussagekräftig, weil C ++ die Äußerung von Lambda-Typen ausdrücklich verbietet, und sogar die Verwendung von Typendefinitionen typeofdurch die Sprache unbrauchbar macht. Und das ist ein Defizit der Sprache selbst, das meiner Meinung nach behoben werden sollte.
CMASTER