En quoi la saisie statique est-elle vraiment utile dans les grands projets?

9

En curieux sur la page principale du site d'un langage de programmation de script, j'ai rencontré ce passage:

Lorsqu'un système devient trop gros pour rester dans votre tête, vous pouvez ajouter des types statiques.

Cela m'a fait me rappeler que dans de nombreuses guerres de religion entre les langages compilés statiques (comme Java) et les langages dynamiques et interprétés (principalement Python parce qu'il est plus utilisé, mais c'est un "problème" partagé entre la plupart des langages de script), l'une des plaintes de statique les fans de langages typés par-dessus les langages typés dynamiquement, c'est qu'ils ne s'adaptent pas bien aux grands projets parce que "un jour, vous oublierez le type de retour d'une fonction et vous devrez la rechercher, tandis qu'avec les langages typés statiquement, tout est explicitement déclaré ".

Je n'ai jamais compris des déclarations comme celle-ci. Pour être honnête, même si vous déclarez le type de retour d'une fonction, vous pouvez et vous l'oublierez après avoir écrit de nombreuses lignes de code, et vous devrez toujours revenir à la ligne dans laquelle il est déclaré à l'aide de la fonction de recherche de votre éditeur de texte pour le vérifier.

De plus, au fur et à mesure que les fonctions sont déclarées avec type funcname()..., sans savoir que typevous devrez rechercher sur chaque ligne dans laquelle la fonction est appelée, car vous ne savez funcnameque, en Python et autres, vous pouvez simplement rechercher def funcnameou function funcnamece qui ne se produit qu'une seule fois, à la déclaration.

De plus, avec les REPL, il est trivial de tester une fonction pour son type de retour avec différentes entrées, tandis qu'avec les langages typés statiquement, vous devrez ajouter quelques lignes de code et tout recompiler juste pour connaître le type déclaré.

Donc, à part connaître le type de retour d'une fonction qui n'est clairement pas un point fort des langages typés statiquement, comment le typage statique est-il vraiment utile dans des projets plus importants?

user6245072
la source
2
si vous lisez les réponses à l'autre question, vous obtiendrez probablement les réponses dont vous avez besoin pour celle-ci, ils demandent essentiellement la même chose sous des angles différents :)
sara
1
Swift et les terrains de jeux sont une REPL d'une langue typée statiquement.
daven11
2
Les langages ne sont pas compilés, les implémentations le sont. La façon d'écrire un REPL pour un langage "compilé" consiste à écrire quelque chose qui peut interpréter le langage, ou au moins le compiler et l'exécuter ligne par ligne, en conservant l'état nécessaire. De plus, Java 9 sera livré avec un REPL.
Sebastian Redl
2
@ user6245072: Voici comment faire un REPL pour un interprète: lire le code, l'envoyer à l'interpréteur, imprimer le résultat. Voici comment créer un REPL pour un compilateur: lire le code, l'envoyer au compilateur, exécuter le code compilé , imprimer le résultat. C'est de la tarte. C'est exactement ce que font FSi (F♯ REPL), GHCi (GHC Haskell's REPL), Scala REPL et Cling.
Jörg W Mittag

Réponses:

21

De plus, avec les REPL, il est trivial de tester une fonction pour son type de retour avec différentes entrées

Ce n'est pas anodin. Ce n'est pas trivial du tout . C'est très simple de le faire pour les fonctions triviales.

Par exemple, vous pourriez définir trivialement une fonction où le type de retour dépend entièrement du type d'entrée.

getAnswer(v) {
 return v.answer
}

Dans ce cas, getAnswerne pas vraiment avoir un seul type de retour. Il n'y a aucun test que vous puissiez écrire qui appelle cela avec un exemple d'entrée pour savoir quel est le type de retour. Cela dépendra toujours de l'argument réel. Lors de l'exécution.

Et cela n'inclut même pas les fonctions qui, par exemple, effectuent des recherches de base de données. Ou faites des choses en fonction de l'entrée de l'utilisateur. Ou recherchez des variables globales, qui sont bien sûr de type dynamique. Ou changez leur type de retour dans des cas aléatoires. Sans oublier la nécessité de tester manuellement chaque fonction individuelle à chaque fois.

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

Fondamentalement, prouver le type de retour de la fonction dans le cas général est littéralement mathématiquement impossible (Halting Problem). La seule façon de garantir le type de retour est de restreindre l'entrée afin que la réponse à cette question ne relève pas du domaine du problème d'arrêt en interdisant les programmes qui ne sont pas prouvables, et c'est ce que fait la saisie statique.

En outre, comme les fonctions sont déclarées avec le type funcname () ..., sans connaître le type, vous devrez rechercher sur chaque ligne dans laquelle la fonction est appelée, car vous ne connaissez que funcname, tandis qu'en Python et autres, vous pouvez simplement recherchez def funcname ou la fonction funcname qui ne se produit qu'une seule fois, lors de la déclaration.

Les langues typées statiquement ont des choses appelées «outils». Ce sont des programmes qui vous aident à faire des choses avec votre code source. Dans ce cas, je ferais simplement un clic droit et aller à la définition, grâce à Resharper. Ou utilisez le raccourci clavier. Ou passez la souris dessus et cela me dira quels sont les types impliqués. Je ne me soucie pas du tout de saluer les fichiers. Un éditeur de texte à lui seul est un outil pathétique pour éditer le code source d'un programme.

De mémoire, def funcnamene serait pas suffisant en Python, car la fonction pourrait être ré-assignée arbitrairement. Ou pourrait être déclaré à plusieurs reprises dans plusieurs modules. Ou en cours. Etc.

et vous devrez toujours revenir à la ligne dans laquelle il est déclaré en utilisant la fonction de recherche de votre éditeur de texte pour le vérifier.

La recherche de fichiers pour le nom de la fonction est une terrible opération primitive qui ne devrait jamais être nécessaire. Cela représente une défaillance fondamentale de votre environnement et de vos outils. Le fait que vous envisagiez même d'avoir besoin d'une recherche de texte en Python est un point énorme contre Python.

DeadMG
la source
2
Pour être honnête, ces «outils» ont été inventés dans des langages dynamiques, et les langages dynamiques en avaient bien avant les langages statiques. Aller à la définition, la complétion de code, la refactorisation automatisée, etc. existaient dans les IDE graphiques Lisp et Smalltalk avant même que les langages statiques n'aient des graphiques ou des IDE, sans parler des IDE graphiques.
Jörg W Mittag
Connaître le type de fonctions de retour ne vous dit pas toujours ce que les fonctions DO . Au lieu d'écrire des types, vous pourriez avoir écrit des tests de doc avec des exemples de valeurs. par exemple, comparer (mots «certains mots oue») => [«certains», «mots», «oeu»] avec (mots chaîne) -> [chaîne], (zip {abc} [1..3]) => [(a, 1), (b, 2), (c, 3)] avec son type.
aoeu256
18

Pensez à un projet avec de nombreux programmeurs, qui a changé au fil des ans. Vous devez maintenir cela. Il y a une fonction

getAnswer(v) {
 return v.answer
}

Que diable fait-il? Quoi v? D'où vient l'élément answer?

getAnswer(v : AnswerBot) {
  return v.answer
}

Maintenant, nous avons plus d'informations -; il a besoin d'un type de AnswerBot.

Si nous allons dans une langue de classe, nous pouvons dire

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Maintenant, nous pouvons avoir une variable de type AnswerBotet appeler la méthode getAnsweret tout le monde sait ce qu'elle fait. Toutes les modifications sont détectées par le compilateur avant la fin des tests d'exécution. Il existe de nombreux autres exemples mais peut-être que cela vous donne l'idée?

daven11
la source
1
Cela semble déjà plus clair - à moins que vous n'indiquiez qu'une telle fonction n'a aucune raison d'exister, mais ce n'est bien sûr qu'un exemple.
user6245072
C'est le problème lorsque vous avez plusieurs programmeurs sur un grand projet, des fonctions comme ça existent (et pire), c'est le truc des cauchemars. considérez également que les fonctions dans les langages dynamiques sont dans l'espace de noms global, donc au fil du temps, vous pourriez avoir quelques fonctions getAnswer - et elles fonctionnent toutes les deux et elles sont toutes les deux différentes car elles sont chargées à des moments différents.
daven11
1
Je suppose que c'est un malentendu de la programmation fonctionnelle qui cause cela. Cependant, que voulez-vous dire en disant qu'ils sont dans l'espace de noms global?
user6245072
3
"les fonctions dans les langages dynamiques sont par défaut dans l'espace de noms global" c'est un détail spécifique au langage, et non une contrainte causée par la typage dynamique.
sara
2
@ daven11 "Je pense au javascript ici", en effet, mais d'autres langages dynamiques ont des espaces de noms / modules / packages réels et peuvent vous avertir des redéfinitions. Vous généralisez peut-être un peu trop.
coredump
10

Vous semblez avoir quelques idées fausses sur le travail avec de grands projets statiques qui peuvent brouiller votre jugement. Voici quelques conseils:

même si vous déclarez le type de retour d'une fonction, vous pouvez et vous l'oublierez après avoir écrit de nombreuses lignes de code, et vous devrez toujours revenir à la ligne dans laquelle il est déclaré en utilisant la fonction de recherche de votre éditeur de texte pour vérifie ça.

La plupart des personnes travaillant avec des langages typés statiquement utilisent soit un IDE pour la langue, soit un éditeur intelligent (tel que vim ou emacs) qui a une intégration avec des outils spécifiques à la langue. Il existe généralement un moyen rapide de trouver le type de fonction dans ces outils. Par exemple, avec Eclipse sur un projet Java, il existe généralement deux façons de trouver le type d'une méthode:

  • Si je veux utiliser une méthode sur un autre objet que «this», je tape une référence et un point (par exemple someVariable.) et Eclipse recherche le type de someVariableet fournit une liste déroulante de toutes les méthodes définies dans ce type; lorsque je fais défiler la liste, le type et la documentation de chacun sont affichés pendant leur sélection. Notez que cela est très difficile à réaliser avec un langage dynamique, car il est difficile (ou dans certains cas impossible) pour l'éditeur de déterminer le type de someVariable, il ne peut donc pas générer facilement la liste correcte. Si je veux utiliser une méthode, thisje peux simplement appuyer sur ctrl + espace pour obtenir la même liste (bien que dans ce cas ce ne soit pas si difficile à réaliser pour les langages dynamiques).
  • Si j'ai déjà une référence écrite pour une méthode spécifique, je peux déplacer le curseur de la souris dessus et le type et la documentation de la méthode sont affichés dans une info-bulle.

Comme vous pouvez le voir, c'est un peu mieux que l'outillage typique disponible pour les langages dynamiques (pas que cela soit impossible dans les langages dynamiques, car certains ont de très bonnes fonctionnalités IDE - smalltalk en est un qui me vient à l'esprit - mais c'est plus difficile pour une langue dynamique et donc moins susceptible d’être disponible).

En outre, comme les fonctions sont déclarées avec le type funcname () ..., sans connaître le type, vous devrez rechercher sur chaque ligne dans laquelle la fonction est appelée, car vous ne connaissez que funcname, tandis qu'en Python et autres, vous pouvez simplement recherchez def funcname ou la fonction funcname qui ne se produit qu'une seule fois, lors de la déclaration.

Les outils de langage statique fournissent généralement des capacités de recherche sémantique, c'est-à-dire qu'ils peuvent trouver la définition et les références à des symboles particuliers avec précision, sans avoir besoin d'effectuer une recherche de texte. Par exemple, en utilisant Eclipse pour un projet Java, je peux mettre en surbrillance un symbole dans l'éditeur de texte et faire un clic droit dessus et choisir soit «aller à la définition» ou «trouver des références» pour effectuer l'une de ces opérations. Vous n'avez pas besoin de rechercher le texte d'une définition de fonction, car votre éditeur sait déjà exactement où il se trouve.

Cependant, l'inverse est que la recherche d'une définition de méthode par texte ne fonctionne vraiment pas aussi bien dans un grand projet dynamique que vous le suggérez, car il pourrait facilement y avoir plusieurs méthodes du même nom dans un tel projet, et vous n'avez probablement pas des outils facilement disponibles pour lever l’ambiguïté sur l’un d’eux que vous invoquez (car de tels outils sont difficiles à écrire au mieux, ou impossibles dans le cas général), vous devrez donc le faire à la main.

De plus, avec les REPL, il est trivial de tester une fonction pour son type de retour avec différentes entrées

Il n'est pas impossible d'avoir un REPL pour une langue typée statiquement. Haskell est l'exemple qui me vient à l'esprit, mais il existe également des REPL pour d'autres langages typés statiquement. Mais le fait est que vous n'avez pas besoin d'exécuter du code pour trouver le type de retour d'une fonction dans un langage statique - il peut être déterminé par examen sans avoir à exécuter quoi que ce soit.

tandis qu'avec des langages typés statiquement, vous auriez besoin d'ajouter quelques lignes de code et de tout recompiler juste pour connaître le type déclaré.

Les chances sont que même si vous en aviez besoin, vous n'auriez pas à tout recompiler . La plupart des langages statiques modernes ont des compilateurs incrémentiels qui ne compileront que la petite partie de votre code qui a changé, de sorte que vous pouvez obtenir un retour presque instantané pour les erreurs de type si vous en faites un. Eclipse / Java, par exemple, mettra en évidence les erreurs de frappe pendant que vous les saisissez .

Jules
la source
4
You seem to have a few misconceptions about working with large static projects that may be clouding your judgement.Eh bien, je n'ai que 14 ans et je ne programme que depuis moins d'un an sur Android, donc c'est possible je suppose.
user6245072
1
Même sans IDE, si vous supprimez une méthode d'une classe en Java et que certaines choses dépendent de cette méthode, tout compilateur Java vous donnera une liste de chaque ligne qui utilisait cette méthode. En Python, il échoue lorsque le code d'exécution appelle la méthode manquante. J'utilise régulièrement Java et Python et j'aime Python pour la rapidité avec laquelle vous pouvez faire fonctionner les choses et les choses intéressantes que Java ne prend pas en charge, mais la réalité est que j'ai des problèmes dans les programmes Python qui n'arrivent tout simplement pas avec (directement) Java. Le refactoring en particulier est beaucoup plus difficile en Python.
JimmyJames
6
  1. Parce que les vérificateurs statiques sont plus faciles pour les langues typées statiquement.
    • Au strict minimum, sans fonctionnalités de langage dynamique, s'il se compile, alors au moment de l'exécution, il n'y a pas de fonctions non résolues. Ceci est courant dans les projets ADA et C sur les microcontrôleurs. (Les programmes de microcontrôleur grossissent parfois ... comme des centaines de kloc gros.)
  2. Les vérifications de référence de compilation statiques sont un sous-ensemble d'invariants de fonction, qui dans un langage statique peuvent également être vérifiés au moment de la compilation.
  3. Les langages statiques ont généralement plus de transparence référentielle. Le résultat est qu'un nouveau développeur peut plonger dans un seul fichier et comprendre ce qui se passe, et corriger un bogue ou ajouter une petite fonctionnalité sans avoir à connaître toutes les choses étranges dans la base de code.

Comparez avec disons, javascript, Ruby ou Smalltalk, où les développeurs redéfinissent les fonctionnalités du langage de base au moment de l'exécution. Cela rend la compréhension du grand projet plus difficile.

Les projets de plus grande envergure ont non seulement plus de gens, ils ont plus de temps. Assez de temps pour que tout le monde oublie ou passe à autre chose.

Pour l'anecdote, une de mes connaissances a une programmation sécurisée "Job For Life" à Lisp. Personne, sauf l'équipe, ne peut comprendre la base de code.

Tim Williscroft
la source
Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.est-ce vraiment si mauvais? La personnalisation qu'ils ont ajoutée ne les aide-t-elle pas à être plus productifs?
user6245072
@ user6245072 Cela peut être un avantage pour les personnes qui y travaillent actuellement, mais cela rend le recrutement de nouvelles personnes plus difficile. Il faut plus de temps pour trouver quelqu'un qui connaît déjà une langue non courante ou pour lui enseigner une langue qu'il ne connaît pas déjà. Cela peut rendre plus difficile la mise à l'échelle du projet lorsqu'il réussit ou pour se remettre des fluctuations - les gens s'éloignent, sont promus à d'autres postes ... Après un certain temps, cela peut aussi être un inconvénient pour les spécialistes eux-mêmes - une fois que vous avez écrit une langue nichée pendant une dizaine d'années, il peut être difficile de passer à quelque chose de nouveau.
Hulk
Ne pouvez-vous pas simplement utiliser un traceur pour créer des tests unitaires à partir du programme Lisp en cours d'exécution? Comme en Python, vous pouvez créer un décorateur (adverbe) appelé print_args qui prend une fonction et retourne une fonction modifiée qui affiche son argument. Vous pouvez ensuite l'appliquer à l'ensemble du programme dans sys.modules, bien qu'un moyen plus simple de le faire soit d'utiliser sys.set_trace.
aoeu256
@ aoeu256 Je ne connais pas les capacités de l'environnement d'exécution Lisp. Mais ils ont beaucoup utilisé les macros, donc aucun programmeur normal ne pouvait lire le code; Il est probable qu'essayer de faire des choses "simples" au runtime ne puisse pas fonctionner car les macros changent tout sur Lisp.
Tim Williscroft
@TimWilliscroft Vous pouvez attendre que toutes les macros soient développées avant de faire ce genre de choses. Emacs a beaucoup de touches de raccourci pour vous permettre de développer des macros en ligne (et des fonctions en ligne peut-être).
aoeu256
4

Je n'ai jamais compris des déclarations comme celle-ci. Pour être honnête, même si vous déclarez le type de retour d'une fonction, vous pouvez et vous l'oublierez après avoir écrit de nombreuses lignes de code, et vous devrez toujours revenir à la ligne dans laquelle il est déclaré à l'aide de la fonction de recherche de votre éditeur de texte pour le vérifier.

Il ne s'agit pas d'oublier le type de retour - cela va toujours arriver. Il s'agit de l'outil capable de vous faire savoir que vous avez oublié le type de retour.

De plus, comme les fonctions sont déclarées avec type funcname()..., sans connaître le type, vous devrez rechercher sur chaque ligne dans laquelle la fonction est appelée, car vous ne le savez funcname, tandis qu'en Python et autres, vous pouvez simplement rechercher def funcnameou function funcnamece qui ne se produit qu'une seule fois , lors de la déclaration.

C'est une question de syntaxe, qui n'a aucun lien avec le typage statique.

La syntaxe de la famille C est en effet peu conviviale lorsque vous souhaitez rechercher une déclaration sans disposer d'outils spécialisés. Les autres langues n'ont pas ce problème. Voir la syntaxe de déclaration de Rust:

fn funcname(a: i32) -> i32

De plus, avec les REPL, il est trivial de tester une fonction pour son type de retour avec différentes entrées, tandis qu'avec les langages typés statiquement, vous devrez ajouter quelques lignes de code et tout recompiler juste pour connaître le type déclaré.

Toute langue peut être interprétée et toute langue peut avoir un REPL.


Donc, à part connaître le type de retour d'une fonction qui n'est clairement pas un point fort des langages typés statiquement, en quoi le typage statique est-il vraiment utile dans les grands projets?

Je répondrai de manière abstraite.

Un programme se compose de diverses opérations et ces opérations sont présentées telles qu'elles sont en raison de certaines hypothèses émises par le développeur.

Certaines hypothèses sont implicites et d'autres sont explicites. Certaines hypothèses concernent une opération à proximité, certaines concernent une opération à distance. Une hypothèse est plus facile à identifier lorsqu'elle est exprimée explicitement et le plus près possible des endroits où sa valeur de vérité est importante.

Un bogue est la manifestation d'une hypothèse qui existe dans le programme mais qui ne tient pas dans certains cas. Pour retrouver un bogue, nous devons identifier l'hypothèse erronée. Pour supprimer le bogue, nous devons soit supprimer cette hypothèse du programme, soit modifier quelque chose pour que l'hypothèse se vérifie.

Je voudrais classer les hypothèses en deux types.

Le premier type sont les hypothèses qui peuvent ou non tenir, selon les entrées du programme. Pour identifier une hypothèse erronée de ce type, nous devons rechercher dans l'espace toutes les entrées possibles du programme. En utilisant des suppositions éclairées et une pensée rationnelle, nous pouvons affiner le problème et chercher dans un espace beaucoup plus petit. Mais quand un programme se développe encore un peu, son espace d'entrée initial augmente à un rythme énorme - au point où il peut être considéré comme infini à toutes fins pratiques.

Le deuxième type sont les hypothèses qui valent définitivement pour toutes les entrées, ou qui sont définitivement erronées pour toutes les entrées. Lorsque nous identifions une hypothèse de ce type comme erronée, nous n'avons même pas besoin d'exécuter le programme ou de tester une entrée. Lorsque nous identifions une hypothèse de ce type comme correcte, nous avons un suspect de moins à prendre en compte lorsque nous recherchons un bogue ( n'importe quel bogue). Par conséquent, il est utile que le plus grand nombre possible d'hypothèses appartiennent à ce type.

Pour placer une hypothèse dans la deuxième catégorie (toujours vrai ou toujours faux, indépendamment des entrées), nous avons besoin d'un minimum d'informations pour être disponible à l'endroit où l'hypothèse est faite. Dans le code source d'un programme, les informations deviennent périmées assez rapidement (par exemple, de nombreux compilateurs ne font pas d'analyse interprocédurale, ce qui fait de tout appel une limite stricte pour la plupart des informations). Nous avons besoin d'un moyen de garder à jour les informations requises (valides et à proximité).

Une façon consiste à avoir la source de ces informations aussi près que possible de l'endroit où elles vont être consommées, mais cela peut ne pas être pratique pour la plupart des cas d'utilisation. Une autre façon consiste à répéter fréquemment les informations, en renouvelant leur pertinence dans le code source.

Comme vous pouvez déjà le deviner, les types statiques sont exactement cela - des balises d'informations de type dispersées à travers le code source. Ces informations peuvent être utilisées pour placer la plupart des hypothèses sur l'exactitude des types dans la deuxième catégorie, ce qui signifie que presque toutes les opérations peuvent être classées comme toujours correctes ou toujours incorrectes en ce qui concerne la compatibilité des types.

Lorsque nos types sont incorrects, l'analyse nous fait gagner du temps en signalant le bogue plus tôt que tard. Lorsque nos types sont corrects, l'analyse nous fait gagner du temps en s'assurant que lorsqu'un bogue se produit, nous pouvons immédiatement exclure les erreurs de type.

Theodoros Chatzigiannakis
la source
3

Vous vous souvenez du vieil adage "ordures dans, ordures hors", eh bien, c'est ce que la saisie statique aide à empêcher. Ce n'est pas une panacée universelle mais la rigueur sur le type de données qu'une routine accepte et renvoie signifie que vous avez une certaine assurance que vous travaillez correctement avec elle.

Une routine getAnswer qui retourne un entier ne sera donc pas utile lorsque vous essayez de l'utiliser dans un appel basé sur une chaîne. Le typage statique vous dit déjà de faire attention, que vous faites probablement une erreur. (et bien sûr, vous pouvez ensuite le remplacer, mais vous devez savoir exactement ce que vous faites et le spécifier dans le code à l'aide d'un transtypage. En général, cependant, vous ne voulez pas faire cela - pirater un une cheville ronde dans un trou carré ne fonctionne jamais bien à la fin)

Vous pouvez maintenant aller plus loin en utilisant des types complexes, en créant une classe qui a des fonctionnalités de minerai, vous pouvez commencer à les faire circuler et vous obtenez soudainement beaucoup plus de structure dans votre programme. Les programmes structurés sont ceux qui sont beaucoup plus faciles à faire fonctionner correctement et à maintenir.

gbjbaanb
la source
Vous n'avez pas à faire d'inférence de type statique (pylint), vous pouvez faire l'inférence de type dynamique chrislaffra.blogspot.com/2016/12/… qui est également effectuée par le compilateur JIT de PyPy. Il existe également une autre version d'inférence de type dynamique dans laquelle un ordinateur place au hasard des objets fictifs dans les arguments et voit ce qui cause une erreur. Le problème d'arrêt n'a pas d'importance dans 99% des cas, si vous prenez trop de temps, arrêtez simplement l'algorithme (c'est ainsi que Python gère la récursion infinie, il a une limite de récursivité qui peut être définie).
aoeu256