Pourquoi l'inférence de type est-elle utile?

37

Je lis le code beaucoup plus souvent que je l'écris, et je suppose que la plupart des programmeurs travaillant sur des logiciels industriels le font. Je suppose que l’avantage de l’inférence de type est moins de verbosité et moins de code écrit. Par contre, si vous lisez le code plus souvent, vous voudrez probablement du code lisible.

Le compilateur en déduit le type; il y a de vieux algorithmes pour cela. Mais la vraie question est: pourquoi le programmeur voudrait-il déduire le type de mes variables lorsque je lis le code? N’est-il pas plus rapide pour quiconque de lire le type plutôt que de penser au type?

Edit: En conclusion, je comprends pourquoi c'est utile. Mais dans la catégorie des fonctionnalités linguistiques, je le vois dans un seau avec une surcharge d’opérateur - utile dans certains cas, mais affectant la lisibilité en cas d’abus.

m3th0dman
la source
5
D'après mon expérience, les types sont beaucoup plus importants pour écrire du code que pour le lire. Lors de la lecture du code, je recherche des algorithmes et des blocs spécifiques vers lesquels des variables bien nommées me dirigent habituellement. Je n'ai vraiment pas besoin de taper le code de contrôle juste pour lire et comprendre ce qu'il fait sauf s'il est terriblement mal écrit. Cependant, lorsque je lis du code qui regorge de détails superflus superflus que je ne cherche pas (comme trop d'annotations de type), il est souvent plus difficile de trouver les bits que je recherche. Je dirais que l'inférence de type est un avantage énorme pour lire beaucoup plus que pour écrire du code.
Jimmy Hoffa
Une fois que je trouve le morceau de code que je cherche, je pourrais peut-être commencer à le vérifier, mais à tout moment, vous ne devriez pas vous focaliser sur plus de 10 lignes de code. vous déduire vous-même parce que vous démarrez mentalement tout le bloc et que vous utilisez probablement des outils pour vous aider à le faire de toute façon. Déterminer les types des 10 lignes de code que vous essayez de zoner prend rarement beaucoup de temps, mais c'est la partie où vous êtes passé de la lecture à l'écriture, ce qui est beaucoup moins courant de toute façon.
Jimmy Hoffa
Rappelez-vous que même si un programmeur lit le code plus souvent que l'écrit, cela ne signifie pas qu'un morceau de code est lu plus souvent qu'écrit. Une grande partie du code peut être de courte durée ou ne plus jamais être relue, et il peut être difficile de dire quel code survivra et devrait être écrit pour une lisibilité maximale.
Jpa
2
En développant le premier point de @ JimmyHoffa, envisagez de lire en général. Une phrase est-elle plus facile à analyser et à lire, encore moins à comprendre, lorsqu’on se concentre sur la partie du discours de ses mots individuels? "La vache (article) nom-singulier a sauté (mot passé) sur (préposition) la lune (article) lune (nom). (Période de ponctuation)".
Zev Spitz

Réponses:

46

Jetons un coup d'oeil à Java. Java ne peut pas avoir de variables avec des types inférés. Cela signifie que je dois souvent épeler le type, même si cela est parfaitement évident pour un lecteur humain.

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

Et parfois, il est tout simplement ennuyeux de préciser le type entier.

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

Ce typage statique prolixe me gêne, le programmeur. La plupart des annotations de type sont des remplissages de lignes répétitifs, des régurgiations sans contenu de ce que nous savons déjà. Cependant, j'aime bien la dactylographie statique, car elle peut vraiment aider à détecter les bogues, alors utiliser une dactylographie dynamique n'est pas toujours une bonne réponse. L'inférence de type est le meilleur des deux mondes: je peux omettre les types non pertinents, mais assurez-vous quand même que mon programme (type) se termine.

Bien que l'inférence de type soit vraiment utile pour les variables locales, elle ne devrait pas être utilisée pour les API publiques qui doivent être documentées sans ambiguïté. Et parfois, les types sont vraiment essentiels pour comprendre ce qui se passe dans le code. Dans de tels cas, il serait insensé de ne compter que sur l'inférence de type.

De nombreuses langues supportent l'inférence de type. Par exemple:

  • C ++. Le automot clé déclenche l'inférence de type. Sans cela, épeler les types de lambdas ou d'entrées en conteneurs serait un enfer.

  • C #. Vous pouvez déclarer des variables avec var, ce qui déclenche une forme limitée d'inférence de type. Il gère toujours la plupart des cas où vous souhaitez une inférence de type. Dans certains endroits, vous pouvez laisser le type complètement (par exemple, dans lambdas).

  • Haskell et toutes les langues de la famille ML. Bien que le type d'inférence de type utilisé ici soit assez puissant, vous voyez toujours des annotations de type pour des fonctions, et ce pour deux raisons: la première est la documentation et la seconde est une vérification indiquant que l'inférence de type a réellement trouvé les types que vous attendiez. S'il y a une différence, il y a probablement une sorte de bogue.

Amon
la source
13
Notez également que C # a des types anonymes, c'est-à-dire des types sans nom, mais C # a un système de types nominal, c'est-à-dire un système de types basé sur des noms. Sans inférence de type, ces types ne pourraient jamais être utilisés!
Jörg W Mittag Le
10
Certains exemples sont un peu artificiels, à mon avis. L'initialisation à 42 ne signifie pas automatiquement que la variable est un int, il peut s'agir de n'importe quel type numérique, même pair char. De plus, je ne vois pas pourquoi vous voudriez épeler tout le type car Entryvous pouvez simplement taper le nom de la classe et laisser votre IDE effectuer l’importation nécessaire. Le seul cas où vous devez épeler le nom complet est lorsque vous avez une classe du même nom dans votre propre paquet. Mais cela me semble de toute façon mal conçu.
Malcolm
10
@ Malcolm Oui, tous mes exemples sont artificiels. Ils servent à illustrer un point. Lorsque j'ai écrit l' intexemple, je pensais au (à mon avis, un comportement plutôt sain d'esprit) de la plupart des langages qui comportent une inférence de type. Ils en déduisent généralement un intou Integerquoi que ce soit appelé dans cette langue. La beauté de l'inférence de type est qu'elle est toujours facultative. vous pouvez toujours spécifier un type différent si vous en avez besoin. En ce qui concerne l' Entryexemple: bon point, je le remplacerai par Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java n'a même pas d'alias de type :(
amon le
4
@ m3th0dman Si le type est important pour la compréhension, vous pouvez toujours le mentionner explicitement. L'inférence de type est toujours optionnelle. Mais ici, le type de colKeyest à la fois évident et sans importance: nous ne nous soucions que de ce qu’il soit approprié comme second argument de doSomethingWith. Si je devais extraire cette boucle dans une fonction qui produit un Iterable de- (key1, key2, value)triples, la signature la plus générale serait <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Dans cette fonction, le type réel de colKey( Integer, not K2) est absolument sans importance.
amon
4
@ m3th0dman c'est une déclaration assez large, à propos de "la plupart " du code qui est ceci ou cela. Statistiques anecdotiques. Il n'y a certainement pas de point par écrit le type deux fois dans initializers: View.OnClickListener listener = new View.OnClickListener(). Vous sauriez toujours le type même si le programmeur était "paresseux" et le raccourcissait à var listener = new View.OnClickListener(si cela était possible). Ce type de redondance est commune - je ne vais pas risquer une devinette ici - et en le retirant ne découlent de la réflexion sur les lecteurs futurs. Chaque fonctionnalité linguistique doit être utilisée avec précaution, je ne remets pas cela en question.
Konrad Morawski
26

C'est vrai que le code est lu beaucoup plus souvent qu'il n'est écrit. Cependant, la lecture prend aussi du temps, et deux écrans de code sont plus difficiles à naviguer et à lire qu'un écran de code. Nous devons donc définir des priorités pour obtenir le meilleur rapport informations utiles / effort de lecture. Ceci est un principe UX général: trop d’informations à la fois submergent et dégradent réellement l’efficacité de l’interface.

Et, selon mon expérience , le type exact n'a souvent aucune importance. Vous nichez parfois des expressions:x + y * z , monkey.eat(bananas.get(i)), factory.makeCar().drive(). Chacune de celles-ci contient des sous-expressions qui donnent une valeur dont le type n'est pas écrit. Pourtant, ils sont parfaitement clairs. Nous sommes d'accord pour laisser le type non déclaré, car il est assez facile de comprendre le contexte, et l'écrire ferait plus de mal que de bien (encombrer la compréhension du flux de données, prendre un écran précieux et un espace mémoire à court terme).

Une des raisons pour ne pas imbriquer des expressions comme il n'y a pas de lendemain est que les lignes deviennent longues et que le flux de valeurs devient flou. L'introduction d'une variable temporaire aide à cela, elle impose un ordre et donne un nom à un résultat partiel. Cependant, tout ce qui profite de ces aspects ne tire pas également des avantages de son type:

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

Est-ce important user un objet entité, d'un entier, d'une chaîne ou de quelque chose d'autre? Dans la plupart des cas, il ne suffit pas de savoir qu'il représente un utilisateur, provient de la requête HTTP et permet d'extraire le nom à afficher dans le coin inférieur droit de la réponse.

Et quand il fait affaire, l'auteur est libre d'écrire le type. C'est une liberté qui doit être utilisée de manière responsable, mais il en va de même pour tout ce qui peut améliorer la lisibilité (noms de variable et de fonction, formatage, conception de l'API, espace blanc). Et en effet, la convention dans Haskell et ML (où tout peut être déduit sans effort supplémentaire) consiste à écrire les types de fonctions non locales, ainsi que de variables et de fonctions locales, le cas échéant. Seuls les novices permettent d'inférer chaque type.


la source
2
+1 Cela devrait être la réponse acceptée. Il va de soi que la déduction de types est une excellente idée.
Christian Hayter
Le type exact d’ userimportance importe si vous essayez d’élargir la fonction, car elle détermine ce que vous pouvez faire avec le fichier user. Ceci est important si vous souhaitez ajouter un contrôle de cohérence (en raison d'une vulnérabilité de sécurité, par exemple), ou oublier que vous devez réellement faire quelque chose avec l'utilisateur en plus de simplement l'afficher. Certes, ces types de lecture pour l’agrandissement sont moins souvent que la lecture du code, mais ils constituent également une partie essentielle de notre travail.
cmaster
@cmaster Et vous pouvez toujours trouver ce type assez facilement (la plupart des IDE vous le diront, et il existe une solution peu sophistiquée consistant à provoquer intentionnellement une erreur de type et à laisser le compilateur imprimer le type réel). ne vous ennuie pas dans le cas commun.
4

Je pense que l'inférence de type est très importante et devrait être supportée dans n'importe quelle langue moderne. Nous développons tous dans les IDE et ils pourraient beaucoup vous aider si vous souhaitez connaître le type inféré, et que peu d’entre nous y travaillent vi. Pensez à la verbosité et au code de cérémonie en Java, par exemple.

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

Mais vous pouvez dire que ça va, mon IDE va ​​m'aider, ça pourrait être un argument valable. Cependant, certaines fonctionnalités n'existeraient pas sans l'aide de l'inférence de type, les types anonymes C # par exemple.

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

Linq ne serait pas aussi bien qu'il l'est maintenant sans l'aide de l'inférence de type, Selectpar exemple

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

Ce type anonyme sera clairement déduit de la variable.

Je n'aime pas l'inférence de type sur les types de retour Scalacar je pense que votre argument s'applique ici. Il devrait être clair pour nous ce qu'une fonction renvoie pour pouvoir utiliser l'API plus facilement.

Sleiman Jneidi
la source
Map<String,HashMap<String,String>>? Bien sûr, si vous n'êtes pas en utilisant les types, puis les épelant a peu d' avantages. Table<User, File, String>est plus informatif cependant, et il est avantageux de l'écrire.
MikeFHay
4

Je pense que la réponse à cette question est très simple: cela évite de lire et d’écrire des informations redondantes. Particulièrement dans les langages orientés objet où vous avez un type des deux côtés du signe égal.

Ce qui vous indique également quand vous devriez ou ne devriez pas l'utiliser - quand l'information n'est pas redondante.

Jmoreno
la source
3
Eh bien, techniquement, les informations sont toujours redondantes lorsqu'il est possible d'omettre les signatures manuelles: sinon le compilateur ne pourrait pas les déduire! Mais je comprends ce que vous voulez dire: lorsque vous copiez simplement une signature sur plusieurs points d’une même vue, c’est vraiment redondant pour le cerveau , alors que quelques caractères bien placés donnent des informations à rechercher longtemps, éventuellement avec un bon nombre de transformations non évidentes.
leftaroundabout
@leftaroundabout: redondant à la lecture par le programmeur.
Jmoreno
3

Supposons que l'on voit le code:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Si someBigLongGenericTypeest assignable à partir du type de retour desomeFactoryMethod , quelle est la probabilité qu'une personne lisant le code remarque si les types ne correspondent pas exactement, et avec quelle facilité une personne qui a remarqué la différence peut-elle reconnaître si elle était intentionnelle ou non?

En permettant l'inférence, un langage peut suggérer à quelqu'un qui lit le code que, lorsque le type d'une variable est explicitement indiqué, il doit essayer de trouver une raison à cela. Cela permet aux lecteurs de code de mieux concentrer leurs efforts. Si, au contraire, dans la grande majorité des cas, lorsqu'un type est spécifié, il se trouve être exactement le même que ce qui aurait été déduit, quelqu'un qui lit le code peut être moins enclin à remarquer le temps où il est subtilement différent. .

supercat
la source
2

Je vois qu'il y a déjà un certain nombre de bonnes réponses. Je vais en répéter certaines, mais parfois, vous voulez simplement mettre les choses dans vos propres mots. Je vais commenter avec quelques exemples de C ++ parce que c'est le langage avec lequel je suis le plus familier.

Ce qui est nécessaire n’est jamais imprudent. L'inférence de type est nécessaire pour rendre pratiques d'autres fonctionnalités du langage. En C ++, il est possible d'avoir des types indicibles.

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

C ++ 11 a ajouté des lambdas qui sont également indéniables.

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

L'inférence de type sous-tend également les modèles.

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

Mais vos questions étaient les suivantes: "Pourquoi le programmeur voudrait-il déduire le type de mes variables lorsque je lis le code? N’est-il pas plus rapide pour quiconque de lire le type que de penser au type qui existe?"

L'inférence de type supprime la redondance. Lorsqu’il s’agit de lire du code, il peut parfois être plus rapide et plus facile d’avoir des informations redondantes dans le code, mais la redondance peut occulter les informations utiles . Par exemple:

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

Il ne faut pas beaucoup de connaissances de la bibliothèque standard pour qu'un programmeur C ++ identifie que i est un itérateur de i = v.begin()sorte que la déclaration de type explicite a une valeur limitée. Par sa présence, il masque les détails les plus importants (tels que ile début du vecteur). La réponse précise de @amon fournit un exemple encore meilleur de la verbosité qui éclipse les détails importants. En revanche, l'utilisation de l'inférence de type met davantage en évidence les détails importants.

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

Bien que la lecture du code soit importante, cela ne suffit pas. À un moment donné, vous devrez arrêter de lire et commencer à écrire un nouveau code. La redondance dans le code rend la modification du code plus lente et plus difficile. Par exemple, disons que j'ai le fragment de code suivant:

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

Dans le cas où j'ai besoin de changer le type de valeur du vecteur pour changer le code deux fois:

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

Dans ce cas, je dois modifier le code à deux endroits. Contraste avec l'inférence de type où le code d'origine est:

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

Et le code modifié:

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

Notez que je n'ai maintenant plus qu'à changer une ligne de code. Extrapolez ceci à un programme volumineux et l'inférence de type peut propager les modifications de types beaucoup plus rapidement qu'avec un éditeur.

La redondance dans le code crée la possibilité de bogues. Chaque fois que votre code dépend de la conservation de deux informations équivalentes, une erreur est possible. Par exemple, il existe une incohérence entre les deux types dans cette instruction qui n’est probablement pas destinée:

int pi = 3.14159;

La redondance rend l’intention plus difficile à discerner. Dans certains cas, l'inférence de type peut être plus facile à lire et à comprendre, car elle est plus simple que la spécification de type explicite. Considérons le fragment de code:

int y = sq(x);

Dans le cas qui sq(x)renvoie un int, il n'est pas évident de savoir s'il ys'agit d'un inttype de retour sq(x)ou s'il convient aux déclarations utilisées y. Si je modifie un autre code tel que sq(x)ne retourne plus int, il est incertain à partir de cette ligne seule si le type de ydoit être mis à jour. Contrastez avec le même code mais en utilisant l'inférence de type:

auto y = sq(x);

En cela, l'intention est claire, ydoit être du même type que celui renvoyé par sq(x). Lorsque le code change le type de retour de sq(x), le type dey changement à faire correspondre automatiquement.

En C ++, il y a une deuxième raison pour laquelle l'exemple ci-dessus est plus simple avec l'inférence de type. L'inférence de type ne peut pas introduire de conversion de type implicite. Si le type de retour sq(x)n'est pas int, le compilateur insère silencieusement une conversion implicite en int. Si le type de retour sq(x)est un type complexe qui définit operator int(), cet appel de fonction masqué peut être arbitrairement complexe.

Bowie Owens
la source
C'est un bon point sur les types indicibles en C ++. Cependant, je pense que c'est moins une raison pour ajouter l'inférence de type que pour corriger le langage. Dans le premier cas que vous présentez, le programmeur n’aurait qu’à donner un nom à la chose pour éviter d’utiliser l’inférence de type, ce n’est donc pas un bon exemple. Le second exemple n’a de sens que parce que C ++ interdit explicitement que les types lambda soient définissables, même le typage avec utilisation de typeofest rendu inutile par le langage. Et c’est un déficit de la langue elle-même qui devrait être corrigé à mon avis.
cmaster