J'utilise python depuis quelques jours maintenant et je pense que je comprends la différence entre le typage dynamique et statique. Ce que je ne comprends pas, c'est dans quelles circonstances il serait préférable. C'est flexible et lisible, mais au détriment de plus de contrôles d'exécution et de tests unitaires supplémentaires requis.
Outre les critères non fonctionnels tels que la flexibilité et la lisibilité, quelles sont les raisons de choisir le typage dynamique? Que puis-je faire avec une saisie dynamique qui ne serait pas possible autrement? À quel exemple de code spécifique pouvez-vous penser qui illustre un avantage concret de la frappe dynamique?
Puisque vous avez demandé un exemple précis, je vais vous en donner un.
Rob Conery's Massive ORM est 400 lignes de code. C'est si petit car Rob est capable de mapper des tables SQL et de fournir des résultats d'objet sans nécessiter beaucoup de types statiques pour refléter les tables SQL. Pour ce faire, utilisez le type de données dynamic
en C #. La page Web de Rob décrit ce processus en détail, mais il semble clair que, dans ce cas d'utilisation particulier, le typage dynamique est en grande partie responsable de la brièveté du code.
Comparez avec Sam Saffron Dapper , qui utilise des types statiques; la classe SQLMapper
à elle seule comprend 3 000 lignes de code.
Notez que les clauses de non-responsabilité habituelles s'appliquent et que votre kilométrage peut varier; Dapper a des objectifs différents de ceux de Massive. Je viens de le signaler comme un exemple de quelque chose que vous pouvez faire en 400 lignes de code qui ne serait probablement pas possible sans la frappe dynamique.
La saisie dynamique vous permet de reporter vos décisions de type à l'exécution. C'est tout.
Que vous utilisiez un langage de type dynamique ou un langage de type statique, vos choix de type doivent toujours être judicieux. Vous n'allez pas ajouter deux chaînes ensemble et vous attendre à une réponse numérique, sauf si les chaînes contiennent des données numériques, et si ce n'est pas le cas, vous obtiendrez des résultats inattendus. Une langue typée statiquement ne vous permettra pas de le faire en premier lieu.
Les partisans des langages de type statique soulignent que le compilateur peut effectuer une quantité substantielle de "vérification d'intégrité" de votre code au moment de la compilation, avant l'exécution d'une seule ligne. C'est une bonne chose ™.
C # a le mot clé dynamic
, qui vous permet de reporter la décision de type à l'exécution sans perdre les avantages de la sécurité de type statique dans le reste de votre code. L'inférence de type (var
) élimine une grande partie de la peine d'écrire dans un langage de type statique en supprimant la nécessité de toujours déclarer explicitement les types.
Les langages dynamiques semblent favoriser une approche plus interactive et immédiate de la programmation. Personne ne s'attend à ce que vous deviez écrire une classe et passer par un cycle de compilation pour taper un peu de code LISP et le regarder s'exécuter. Pourtant, c'est exactement ce que je dois faire en C #.
Des expressions comme "frappe statique" et "frappe dynamique" sont souvent utilisées, et les gens ont tendance à utiliser des définitions subtilement différentes, alors commençons par clarifier ce que nous voulons dire.
Prenons un langage dont les types statiques sont vérifiés au moment de la compilation. Mais disons qu'une erreur de type ne génère qu'un avertissement non fatal et, lors de l'exécution, tout est de type canard. Ces types statiques sont uniquement pour la commodité du programmeur et n'affectent pas le codegen. Cela illustre que le typage statique n'impose pas en soi de limites et n'est pas mutuellement exclusif avec le typage dynamique. (Objective-C ressemble beaucoup à ceci.)
Mais la plupart des systèmes de type statique ne se comportent pas de cette façon. Il existe deux propriétés communes aux systèmes de type statique qui peuvent imposer des limitations:
Il s'agit d'une limitation car de nombreux programmes sécurisés de type contiennent nécessairement une erreur de type statique.
Par exemple, j'ai un script Python qui doit s'exécuter à la fois Python 2 et Python 3. Certaines fonctions ont changé) leurs types de paramètres entre Python 2 et 3, j'ai donc du code comme ceci:
if sys.version_info[0] == 2:
wfile.write(txt)
else:
wfile.write(bytes(txt, 'utf-8'))
Un vérificateur de type statique Python 2 rejetterait le code Python 3 (et vice versa), même s'il ne serait jamais exécuté. Mon programme de type sécurisé contient un erreur de type statique.
Comme autre exemple, considérons un programme Mac qui souhaite s'exécuter sur OS X 10.6, mais profitez des nouvelles fonctionnalités de 10.7. Les méthodes 10.7 peuvent ou non exister au moment de l'exécution, et c'est à moi, le programmeur, de les détecter. Un vérificateur de type statique est obligé de rejeter mon programme pour garantir la sécurité du type ou d'accepter le programme, avec la possibilité de produire une erreur de type (fonction manquante) au moment de l'exécution.
La vérification de type statique suppose que l'environnement d'exécution est correctement décrit par les informations de temps de compilation. Mais prédire l'avenir est périlleux!
Voici une autre limitation:
En supposant que les types statiques sont "corrects", il existe de nombreuses possibilités d'optimisation, mais ces optimisations peuvent être limitatives. Un bon exemple est les objets proxy, par exemple à distance. Supposons que vous souhaitiez avoir un objet proxy local qui transfère les invocations de méthode à un objet réel dans un autre processus. Ce serait bien si le proxy était générique (pour qu'il puisse se faire passer pour n'importe quel objet) et transparent (pour que le code existant n'ait pas besoin de savoir qu'il parle à un proxy). Mais pour ce faire, le compilateur ne peut pas générer de code qui suppose que les types statiques sont corrects, par exemple en insérant statiquement les appels de méthode, car cela échouera si l'objet est en fait un proxy.
Des exemples de tels accès à distance en action incluent NSXPCConnection d'ObjC ou TransparentProxy de C # (dont la mise en œuvre a nécessité quelques pessimisations pendant l'exécution - voir ici pour une discussion).
Lorsque le codegen ne dépend pas des types statiques et que vous disposez d'installations telles que le transfert de messages, vous pouvez faire beaucoup de choses intéressantes avec des objets proxy, le débogage, etc.
Voilà donc un échantillon de certaines des choses que vous pouvez faire si vous n'êtes pas obligé de satisfaire un vérificateur de type. Les limitations ne sont pas imposées par les types statiques, mais par la vérification forcée des types statiques.
Les variables de type canard sont la première chose à laquelle tout le monde pense, mais dans la plupart des cas, vous pouvez obtenir les mêmes avantages grâce à l'inférence de type statique.
Mais le typage de canard dans les collections créées dynamiquement est difficile à réaliser d'une autre manière:
>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'
Alors, quel type JSON.parse
revenir? Un dictionnaire de tableaux-d'entiers-ou-dictionnaires-de-chaînes? Non, même ce n'est pas assez général.
JSON.parse
doit renvoyer une sorte de "valeur de variante" qui peut être null, bool, float, string, tableau de n'importe lequel de ces types récursivement, ou dictionnaire de chaîne à n'importe lequel de ces types récursivement. Les principaux atouts du typage dynamique proviennent du fait d'avoir de tels types de variantes.
Jusqu'à présent, c'est un avantage de la dynamique types, pas des langues à typage dynamique. Un langage statique décent peut simuler parfaitement un tel type. (Et même les "mauvais" langages peuvent souvent les simuler en cassant la sécurité des types sous le capot et/ou en exigeant une syntaxe d'accès maladroite.)
L'avantage des langages à typage dynamique est que de tels types ne peuvent pas être déduits par des systèmes d'inférence de type statique. Vous devez écrire le type explicitement. Mais dans de nombreux cas - y compris une fois - le code pour décrire le type est exactement aussi compliqué que le code pour analyser/construire les objets sans décrire le type, de sorte que ce n'est toujours pas nécessairement un avantage.
Comme tout système de type statique à distance pratique est sévèrement limité par rapport au langage de programmation qui le concerne, il ne peut pas exprimer tous les invariants que le code pourrait vérifier lors de l'exécution. Afin de ne pas contourner les garanties qu'un système de type tente de donner, il opte donc pour la prudence et interdit les cas d'utilisation qui passeraient ces contrôles, mais ne peut pas (dans le système de type) être prouvé.
Je vais faire un exemple. Supposons que vous implémentiez un modèle de données simple pour décrire les objets de données, leurs collections, etc., qui est typé statiquement dans le sens où, si le modèle dit que l'attribut x
de l'objet de type Foo contient un entier, il doit détiennent toujours un entier. Comme il s'agit d'une construction d'exécution, vous ne pouvez pas la taper statiquement. Supposons que vous stockiez les données décrites dans les fichiers YAML. Vous créez une carte de hachage (à remettre à une bibliothèque YAML plus tard), obtenez l'attribut x
, stockez-le dans la carte, obtenez cet autre attribut qui se trouve être une chaîne, ... maintenez un seconde? Quel est le type de the_map[some_key]
maintenant? Eh bien, tire, nous savons que some_key
est 'x'
et le résultat doit donc être un entier, mais le système de type ne peut même pas commencer à raisonner à ce sujet.
Certains systèmes de types activement recherchés peuvent fonctionner pour cet exemple spécifique, mais ceux-ci sont extrêmement compliqués (à la fois pour les rédacteurs du compilateur à implémenter et pour le programmeur à raisonner), en particulier pour quelque chose d'aussi "simple" (je veux dire, je viens de l'expliquer dans un paragraphe).
Bien sûr, la solution d'aujourd'hui consiste à tout boxer puis à lancer (ou à avoir un tas de méthodes remplacées, dont la plupart déclenchent des exceptions "non implémentées"). Mais ce n'est pas typé statiquement, c'est un hack autour du système de type pour faire les vérifications de type lors de l'exécution.
Il n'y a rien que vous puissiez faire avec la frappe dynamique que vous ne pouvez pas faire avec la frappe statique, car vous pouvez implémenter la frappe dynamique au-dessus d'une langue typée statiquement.
Un petit exemple à Haskell:
data Data = DString String | DInt Int | DDouble Double
-- defining a '+' operator here, with explicit promotion behavior
DString a + DString b = DString (a ++ b)
DString a + DInt b = DString (a ++ show b)
DString a + DDouble b = DString (a ++ show b)
DInt a + DString b = DString (show a ++ b)
DInt a + DInt b = DInt (a + b)
DInt a + DDouble b = DDouble (fromIntegral a + b)
DDouble a + DString b = DString (show a ++ b)
DDouble a + DInt b = DDouble (a + fromIntegral b)
DDouble a + DDouble b = DDouble (a + b)
Avec suffisamment de cas, vous pouvez implémenter n'importe quel système de type dynamique donné.
Inversement, vous pouvez également traduire tout programme de type statique en un programme dynamique équivalent. Bien sûr, vous perdriez toutes les garanties de correction de la correction fournies par le langage de type statique.
Edit: je voulais garder cela simple, mais voici plus de détails sur un modèle d'objet
Une fonction prend une liste de données comme arguments et effectue des calculs avec effets secondaires dans ImplMonad, et retourne une donnée.
type Function = [Data] -> ImplMonad Data
DMember
est soit une valeur membre soit une fonction.
data DMember = DMemValue Data | DMemFunction Function
Étendez Data
pour inclure les objets et les fonctions. Les objets sont des listes de membres nommés.
data Data = .... | DObject [(String, DMember)] | DFunction Function
Ces types statiques sont suffisants pour implémenter tous les systèmes d'objets à typage dynamique que je connais.
Une membrane est une enveloppe autour d'un graphique d'objet entier, par opposition à une enveloppe pour un seul objet. En règle générale, le créateur d'une membrane commence par envelopper un seul objet dans une membrane. L'idée clé est que toute référence d'objet qui traverse la membrane est elle-même transitoirement enveloppée dans la même membrane.
Chaque type est encapsulé par un type qui a la même interface, mais qui intercepte les messages et encapsule et décompresse les valeurs lorsqu'ils traversent la membrane. Quel est le type de la fonction d'habillage dans votre langue préférée typée statiquement? Peut-être que Haskell a un type pour ces fonctions, mais la plupart des langages typés statiquement ne le font pas ou finissent par utiliser Object → Object, abdiquant ainsi leur responsabilité en tant que vérificateurs de type.
Comme quelqu'un l'a mentionné, en théorie, vous ne pouvez pas faire grand-chose avec le typage dynamique que vous ne pourriez pas faire avec le typage statique si vous implémentiez vous-même certains mécanismes. La plupart des langages fournissent les mécanismes de relaxation de type pour prendre en charge la flexibilité de type comme les pointeurs vides et le type d'objet racine ou l'interface vide.
La meilleure question est de savoir pourquoi le typage dynamique est plus approprié et plus approprié dans certaines situations et problèmes.
Tout d'abord, définissons
Entité - J'aurais besoin d'une notion générale d'une entité dans le code. Cela peut aller du nombre primitif aux données complexes.
Comportement - disons que notre entité a un état et un ensemble de méthodes qui permettent au monde extérieur d'instruire l'entité à certaines réactions. Permet d'appeler l'état + interface de cette entité son comportement. Une entité peut avoir plus d'un comportement combiné d'une certaine manière par les outils fournis par le langage.
Définitions des entités et de leurs comportements - chaque langue fournit des moyens d'abstractions qui vous aident à définir les comportements (ensemble de méthodes + état interne) de certaines entités dans le programme. Vous pouvez attribuer un nom à ces comportements et dire que toutes les instances qui ont ce comportement sont d'un certain type .
C'est probablement quelque chose qui n'est pas si inconnu. Et comme vous l'avez dit, vous avez compris la différence, mais quand même. Explication probablement pas complète et la plus précise mais j'espère assez amusant pour apporter de la valeur :)
Typing statique - le comportement de toutes les entités de votre programme est examiné au moment de la compilation, avant que le code ne commence à s'exécuter. Cela signifie que si vous voulez par exemple que votre entité de type Personne ait un comportement (pour se comporter comme) Magicien, alors vous devrez définir l'entité MagicienPersonne et lui donner les comportements d'un magicien comme throwMagic (). Si vous dans votre code, dites par erreur au compilateur Person.throwMagic () ordinaire vous dira "Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
Typage dynamique - dans les environnements de typage dynamique, les comportements disponibles des entités ne sont pas vérifiés tant que vous n'essayez pas vraiment de faire quelque chose avec une certaine entité. L'exécution de Ruby code qui demande à Person.throwMagic () ne sera pas capturée tant que votre code n'y sera pas vraiment arrivé. Cela semble frustrant, n'est-ce pas. Mais cela semble révélateur également. propriété, vous pouvez faire des choses intéressantes. Par exemple, imaginons que vous concevez un jeu où tout peut se tourner vers Magicien et vous ne savez pas vraiment qui ce sera, jusqu'à ce que vous arriviez à un certain point du code. Et puis Frog vient et vous dites HeyYouConcreteInstanceOfFrog.include Magic
et à partir de ce moment, cette grenouille devient une grenouille particulière qui a des pouvoirs magiques. D'autres grenouilles, toujours pas. Vous voyez, dans les langages de frappe statiques, il faudrait définir cette relation par un moyen standard de combinaison de comportements (comme la mise en œuvre d'interface). Dans le langage de frappe dynamique, vous pouvez le faire à l'exécution et personne ne s'en souciera.
La plupart des langages de frappe dynamiques ont des mécanismes pour fournir un comportement générique qui interceptera tout message transmis à leur interface. Par exemple Ruby method_missing
et PHP __call
si je me souviens bien. Cela signifie que vous pouvez faire toutes sortes de choses intéressantes pendant l'exécution du programme et prendre une décision de type en fonction de l'état actuel du programme. Cela apporte des outils de modélisation d'un problème qui sont beaucoup plus flexibles que dans, disons, un langage de programmation statique conservateur comme Java.