Je lis le code beaucoup plus souvent que j'écris du code, 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. Mais d'un autre côté, si vous lisez du code plus souvent, vous voudrez probablement du code lisible.
Le compilateur déduit le type; il existe 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 que de penser à quel type existe-t-il?
Edit: En conclusion, je comprends pourquoi c'est utile. Mais dans la catégorie des fonctionnalités de langage, je le vois dans un seau avec surcharge de l'opérateur - utile dans certains cas mais affectant la lisibilité en cas d'utilisation abusive.
Jetons un coup d'œil à Java. Java ne peut pas avoir de variables avec des types inférés. Cela signifie que je dois souvent préciser le type, même s'il est parfaitement évident pour un lecteur humain quel est le type:
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 tout le type.
// 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);
}
}
Cette saisie statique verbeuse me gêne, moi le programmeur. La plupart des annotations de type sont des remplissages de ligne répétitifs et sans contenu de ce que nous savons déjà. Cependant, j'aime la frappe statique, car elle peut vraiment aider à découvrir les bogues, donc l'utilisation de la frappe 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 toujours être sûr que mon programme (type-) vérifie.
Bien que l'inférence de type soit vraiment utile pour les variables locales, elle ne doit 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 se fier uniquement à l'inférence de type.
Il existe de nombreuses langues qui prennent en charge l'inférence de type. Par exemple:
C++. Le mot clé auto
déclenche l'inférence de type. Sans cela, définir les types de lambdas ou d'entrées dans des 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 complètement le type (par exemple dans les lambdas).
Haskell et toutes les langues de la famille ML. Bien que la saveur spécifique de l'inférence de type utilisée ici soit assez puissante, vous voyez souvent des annotations de type pour les fonctions, et pour deux raisons: la première est la documentation et la seconde est une vérification 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.
Il est vrai que le code est lu beaucoup plus souvent qu'il n'est écrit. Cependant, la lecture prend également du temps, et deux écrans de code sont plus difficiles à parcourir et à lire qu'un seul écran de code, nous devons donc établir des priorités pour regrouper le meilleur rapport informations utiles/effort de lecture. Il s'agit d'un principe UX général: trop d'informations à la fois submergent et dégradent réellement l'efficacité de l'interface.
Et d'après mon expérience, souvent , le type exact n'est pas (cela) important. Vous imbriquez sûrement parfois des expressions: x + y * z
, monkey.eat(bananas.get(i))
, factory.makeCar().drive()
. Chacun d'eux contient des sous-expressions qui évaluent une valeur dont le type n'est pas écrit. Ils sont pourtant parfaitement clairs. Nous sommes d'accord pour laisser le type non déclaré car il est assez facile de le comprendre à partir du 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).
L'une des raisons de ne pas imbriquer des expressions comme s'il n'y avait pas de lendemain est que les lignes s'allongent et que le flux de valeurs devient flou. L'introduction d'une variable temporaire y contribue, elle impose un ordre et donne un nom à un résultat partiel. Cependant, tout ce qui bénéficie de ces aspects ne bénéficie pas non plus du fait que son type soit précisé:
user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)
Est-il important que user
soit un objet entité, un entier, une chaîne ou autre chose? Dans la plupart des cas, ce n'est pas le cas, il suffit de savoir qu'il représente un utilisateur, qu'il provient de la requête HTTP et qu'il est utilisé pour récupérer le nom à afficher dans le coin inférieur droit de la réponse.
Et quand cela fait importe, 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 variables et de fonctions, formatage, conception d'API, espace blanc). Et en effet, la convention dans Haskell et ML (où tout peut être déduit sans effort supplémentaire) est d'écrire les types de fonctions fonctions non locales, ainsi que des variables et fonctions locales, le cas échéant. Seuls les novices permettent de déduire chaque type.
Je pense que l'inférence de type est assez importante et devrait être prise en charge dans n'importe quel langage moderne. Nous développons tous dans des IDE et ils pourraient aider beaucoup au cas où vous voudriez connaître le type déduit, peu d'entre nous piratent dans vi
. Pensez à la verbosité et au code de cérémonie dans Java par exemple.
Map<String,HashMap<String,String>> map = getMap();
Mais vous pouvez dire que ça va bien mon IDE m'aidera, cela pourrait être un point valide. Cependant, certaines fonctionnalités ne seraient pas là 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 agréable qu'aujourd'hui sans l'aide de l'inférence de type, Select
par exemple
var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});
Ce type anonyme sera parfaitement déduit de la variable.
Je n'aime pas l'inférence de type sur les types de retour dans Scala
parce que je pense que votre point s'applique ici, il devrait être clair pour nous ce qu'une fonction retourne afin que nous puissions utiliser l'API plus couramment
Je pense que la réponse à cette question est très simple: elle permet d'économiser la lecture et l'écriture d'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 devez ou ne pas l'utiliser - lorsque les informations ne sont pas redondantes.
Supposons que l'on voit le code:
someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();
Si someBigLongGenericType
est attribuable à partir du type de retour de someFactoryMethod
, quelle est la probabilité qu'une personne lisant le code remarque si les types ne correspondent pas précisément et avec quelle facilité une personne qui a remarqué la différence reconnaître si c'était intentionnel ou non?
En permettant l'inférence, un langage peut suggérer à quelqu'un qui lit du code que lorsque le type d'une variable est explicitement indiqué, la personne devrait essayer de trouver une raison à cela. Cela permet aux personnes qui lisent du code de mieux concentrer leurs efforts. Si, en revanche, la grande majorité des fois où un type est spécifié, il se trouve qu'il est exactement le même que ce qui aurait été déduit, alors quelqu'un qui lit du code peut être moins enclin à remarquer les fois où il est subtilement différent .
Je vois qu'il y a déjà un certain nombre de bonnes réponses. Je vais répéter certaines d'entre elles, mais parfois vous voulez simplement mettre les choses dans vos propres mots. Je commenterai avec quelques exemples de C++ car c'est le langage que je connais le mieux.
Ce qui est nécessaire n'est jamais imprudent. L'inférence de type est nécessaire pour rendre les autres fonctionnalités du langage pratiques. En C++, il est possible d'avoir des types inexprimables.
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 inexprimables.
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 "pourquoi le programmeur voudrait-il déduire le type de mes variables quand je lis le code? N'est-il pas plus rapide pour quiconque de lire le type que de penser à quel type existe-t-il?"
L'inférence de type supprime la redondance. Quand 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 éclipser les informations utiles. Par exemple:
std::vector<int> v;
std::vector<int>::iterator i = v.begin();
Il ne faut pas beaucoup de familiarité avec la bibliothèque standard pour un programmeur C++ pour identifier que i est un itérateur de i = v.begin()
donc la déclaration de type explicite a une valeur limitée. Par sa présence, il masque des détails plus importants (tels que i
pointe vers le début du vecteur). La bonne réponse de @amon fournit un meilleur exemple de verbosité éclipsant des détails importants. En revanche, l'utilisation de l'inférence de type donne une plus grande importance aux détails importants.
std::vector<int> v;
auto i = v.begin();
Bien que la lecture de code soit importante, elle n'est pas suffisante, à 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ù je dois changer le type de valeur du vecteur pour changer le code en double:
std::vector<double> v;
std::vector<double>::iterator i = v.begin();
Dans ce cas, je dois modifier le code à deux endroits. Contrairement à 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 plus qu'à changer une ligne de code. Extrapoler cela à un grand programme et l'inférence de type peut propager les modifications aux types beaucoup plus rapidement qu'avec un éditeur.
La redondance dans le code crée la possibilité de bugs. Chaque fois que votre code dépend de deux informations restées équivalentes, il y a une possibilité d'erreur. Par exemple, il y a une incohérence entre les deux types dans cette déclaration qui n'est probablement pas prévue:
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 qu'une spécification de type explicite. Considérez le fragment de code:
int y = sq(x);
Dans le cas où sq(x)
renvoie un int
, il n'est pas évident que y
soit un int
car il s'agit du type de retour de sq(x)
ou parce qu'il convient aux instructions qui utilisent y
. Si je change un autre code de telle sorte que sq(x)
ne renvoie plus int
, il est incertain à partir de cette seule ligne si le type de y
doit être mis à jour. Comparez avec le même code mais en utilisant l'inférence de type:
auto y = sq(x);
En cela, l'intention est claire, y
doit être du même type que celui renvoyé par sq(x)
. Lorsque le code change le type de retour de sq(x)
, le type de y
change pour 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 de sq(x)
n'est pas int
, le compilateur insère silencieusement une conversion implicite en int
. Si le type de retour de sq(x)
est un type complexe qui définit operator int()
, cet appel de fonction masqué peut être arbitrairement complexe.