Existe-t-il des modèles de conception qui ne sont possibles que dans des langages typés dynamiquement comme Python?

30

J'ai lu une question connexe. Existe-t-il des modèles de conception inutiles dans les langages dynamiques comme Python? et je me suis souvenu de cette citation sur Wikiquote.org

La chose merveilleuse à propos de la frappe dynamique est qu'elle vous permet d'exprimer tout ce qui est calculable. Et les systèmes de type ne le font pas - les systèmes de type sont généralement décidables, et ils vous limitent à un sous-ensemble. Les gens qui préfèrent les systèmes de type statique disent: «ça va, c'est assez bien; tous les programmes intéressants que vous voulez écrire fonctionneront comme des types ». Mais c'est ridicule - une fois que vous avez un système de type, vous ne savez même pas quels programmes intéressants existent.

--- Radio Génie Logiciel Episode 140: Newspeak et types enfichables avec Gilad Bracha

Je me demande, y a-t-il des modèles ou stratégies de conception utiles qui, en utilisant la formulation de la citation, "ne fonctionnent pas comme des types"?

user7610
la source
3
J'ai trouvé que la double répartition et le modèle de visiteur étaient très difficiles à réaliser dans les langues typées statiquement, mais facilement réalisables dans les langues dynamiques. Voir cette réponse (et la question) par exemple: programmers.stackexchange.com/a/288153/122079
user3002473
7
Bien sûr. Par exemple, tout modèle impliquant la création de nouvelles classes lors de l'exécution. (c'est également possible en Java, mais pas en C ++; il y a une échelle mobile de dynamisme).
user253751
1
Cela dépendrait beaucoup de la sophistication de votre système de typage :-) Les langages fonctionnels font généralement très bien dans ce domaine.
Bergi
1
Tout le monde semble parler de systèmes de type comme Java et C # au lieu de Haskell ou OCaml. Un langage avec un système de type puissant peut être aussi concis qu'un langage dynamique mais garder la sécurité du type.
Andrew dit Réintégrer Monica le
@immibis C'est incorrect. Les systèmes de type statique peuvent absolument créer de nouvelles classes "dynamiques" au moment de l'exécution. Voir le chapitre 33 de Fondements pratiques pour les langages de programmation.
gardenhead

Réponses:

4

Types de première classe

La frappe dynamique signifie que vous avez des types de première classe: vous pouvez inspecter, créer et stocker des types au moment de l'exécution, y compris les propres types du langage. Cela signifie également que les valeurs sont typées, pas des variables .

Le langage à typage statique peut produire du code qui s'appuie également sur des types dynamiques, comme la répartition des méthodes, les classes de type, etc., mais d'une manière généralement invisible pour le runtime. Au mieux, ils vous donnent un moyen d'effectuer une introspection. Vous pouvez également simuler des types sous forme de valeurs, mais vous disposez alors d'un système de type dynamique ad hoc.

Cependant, les systèmes de types dynamiques n'ont que rarement des types de première classe. Vous pouvez avoir des symboles de première classe, des packages de première classe, de première classe ... tout. Ceci contraste avec la séparation stricte entre le langage du compilateur et le langage d'exécution dans les langages typés statiquement. Ce que le compilateur ou l'interpréteur peut faire, le runtime peut le faire aussi.

Maintenant, convenons que l'inférence de type est une bonne chose et que j'aime faire vérifier mon code avant de l'exécuter. Cependant, j'aime aussi pouvoir produire et compiler du code lors de l'exécution. Et j'aime aussi précalculer les choses au moment de la compilation. Dans une langue typée dynamiquement, cela se fait avec la même langue. Dans OCaml, vous avez le système de type module / fonctor, qui est différent du système de type principal, qui est différent du langage du préprocesseur. En C ++, vous avez le langage de modèle qui n'a rien à voir avec le langage principal, qui ignore généralement les types lors de l'exécution. Et c'est très bien dans ces langues, car ils ne veulent pas en fournir plus.

En fin de compte, cela ne change pas vraiment ce type de logiciel que vous pouvez développer, mais l'expressivité changements que la façon dont vous les développer et s'il est difficile ou non.

Patterns

Les modèles qui s'appuient sur des types dynamiques sont des modèles qui impliquent des environnements dynamiques: classes ouvertes, répartition, bases de données d'objets en mémoire, sérialisation, etc. Des choses simples comme les conteneurs génériques fonctionnent parce qu'un vecteur n'oublie pas au moment de l'exécution le type d'objets qu'il contient (pas besoin de types paramétriques).

J'ai essayé d'introduire les nombreuses façons dont le code est évalué en Common Lisp ainsi que des exemples d'analyses statiques possibles (c'est SBCL). L'exemple sandbox compile un minuscule sous-ensemble de code Lisp extrait d'un fichier séparé. Afin d'être raisonnablement sûr, je change la table de lecture, n'autorise qu'un sous-ensemble de symboles standard et encapsule les choses avec un délai d'attente.

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

Rien ci-dessus n'est "impossible" à faire avec d'autres langues. L'approche du plug-in dans Blender, dans les logiciels de musique ou les IDE pour les langages compilés statiquement qui effectuent une recompilation à la volée, etc. Au lieu des outils externes, les langages dynamiques privilégient les outils qui utilisent des informations déjà présentes. Tous les appelants connus de FOO? toutes les sous-classes de BAR? toutes les méthodes spécialisées par classe ZOT? ce sont des données internalisées. Les types ne sont qu'un autre aspect de cela.


(voir aussi: CFFI )

coredump
la source
39

Réponse courte: non, car équivalence de Turing.

Réponse longue: Ce mec est un troll. S'il est vrai que les systèmes de types "vous limitent à un sous-ensemble", les éléments extérieurs à ce sous-ensemble sont, par définition, des éléments qui ne fonctionnent pas.

Tout ce que vous êtes capable de faire dans n'importe quel langage de programmation complet de Turing (qui est un langage conçu pour la programmation à usage général, et beaucoup d'autres qui ne le sont pas; c'est une barre assez basse à effacer et il existe plusieurs exemples d'un système qui devient Turing- terminé involontairement) que vous pouvez faire dans n'importe quel autre langage de programmation Turing-complete. C'est ce qu'on appelle «l'équivalence de Turing» et cela ne signifie exactement que ce qu'elle dit. Surtout, cela ne signifie pas que vous pouvez faire l'autre chose tout aussi facilement dans l'autre langage - certains diront que c'est tout l'intérêt de créer un nouveau langage de programmation en premier lieu: pour vous donner une meilleure façon de faire certains des choses que les langues existantes sucent.

Un système de type dynamique, par exemple, peut être émulé au-dessus d'un système de type OO statique en déclarant simplement toutes les variables, paramètres et valeurs de retour comme Objecttype de base , puis en utilisant la réflexion pour accéder aux données spécifiques à l'intérieur, donc quand vous vous rendez compte vous voyez qu'il n'y a littéralement rien que vous puissiez faire dans un langage dynamique que vous ne pouvez pas faire dans un langage statique. Mais le faire de cette façon serait un énorme gâchis, bien sûr.

Le gars de la citation a raison: les types statiques limitent ce que vous pouvez faire, mais c'est une fonctionnalité importante, pas un problème. Les lignes sur la route restreignent ce que vous pouvez faire dans votre voiture, mais les trouvez-vous restrictives ou utiles? (Je sais que je ne voudrais pas conduire sur une route très fréquentée et complexe où rien ne dit aux voitures qui vont dans la direction opposée de rester à leurs côtés et de ne pas venir où je conduis!) En établissant des règles qui définissent clairement ce qui est considéré comme un comportement invalide et en veillant à ce qu'il ne se produise pas, vous réduisez considérablement les risques de survenue d'un crash indésirable.

En outre, il dénature l'autre côté. Ce n'est pas que "tous les programmes intéressants que vous voulez écrire fonctionneront comme des types", mais plutôt "tous les programmes intéressants que vous voulez écrire nécessiteront des types". Une fois que vous avez dépassé un certain niveau de complexité, il devient très difficile de maintenir la base de code sans un système de type pour vous maintenir en ligne, pour deux raisons.

Premièrement, parce que le code sans annotations de type est difficile à lire. Considérez le Python suivant:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

À quoi pensez-vous que les données ressembleront que le système à l'autre extrémité de la connexion recevra? Et s'il reçoit quelque chose qui semble complètement faux, comment déterminez-vous ce qui se passe?

Tout dépend de la structure de value.someProperty. Mais à quoi ça ressemble? Bonne question! Qu'est-ce qui appelle sendData()? Qu'est-ce que ça passe? À quoi ressemble cette variable? D'où vient-il? Si ce n'est pas local, vous devez retracer l'historique complet de valuepour retrouver ce qui se passe. Peut-être que vous passez quelque chose d'autre qui a également une somePropertypropriété, mais il ne fait pas ce que vous pensez qu'il fait?

Maintenant, regardons-le avec des annotations de type, comme vous pouvez le voir dans le langage Boo, qui utilise une syntaxe très similaire mais qui est typé statiquement:

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

S'il y a quelque chose qui ne va pas, tout à coup, votre travail de débogage est devenu plus simple: recherchez la définition de MyDataType! De plus, la possibilité d'obtenir un mauvais comportement parce que vous avez passé un type incompatible qui a également une propriété avec le même nom passe soudainement à zéro, car le système de type ne vous laissera pas faire cette erreur.

La deuxième raison s'appuie sur la première: dans un projet vaste et complexe, vous avez très probablement plusieurs contributeurs. (Et sinon, vous le construisez vous-même sur une longue période, ce qui est essentiellement la même chose. Essayez de lire le code que vous avez écrit il y a 3 ans si vous ne me croyez pas!) Cela signifie que vous ne savez pas ce qui était passer par la tête de la personne qui a écrit presque n'importe quelle partie du code au moment où ils l'ont écrit, parce que vous n'étiez pas là, ou ne vous souvenez pas si c'était votre propre code il y a longtemps. Avoir des déclarations de type vous aide vraiment à comprendre quelle était l'intention du code!

Les gens comme le gars dans la citation décrivent fréquemment les avantages du typage statique comme étant "d'aider le compilateur" ou "tout sur l'efficacité" dans un monde où les ressources matérielles presque illimitées rendent cela de moins en moins pertinent chaque année. Mais comme je l'ai montré, bien que ces avantages existent certainement, le principal avantage réside dans les facteurs humains, en particulier la lisibilité et la maintenabilité du code. (L'efficacité supplémentaire est certainement un bon bonus!)

Mason Wheeler
la source
24
"Ce mec est un troll." - Je ne suis pas sûr qu'une attaque ad hominem va aider votre cas par ailleurs bien présenté. Et bien que je sache bien que l'argument de l'autorité est une erreur tout aussi mauvaise que ad hominem, je voudrais quand même souligner que Gilad Bracha a probablement conçu plus de langues et (plus pertinent pour cette discussion) des systèmes de type plus statiques que la plupart. Juste un petit extrait: il est l'unique concepteur de Newspeak, co-concepteur de Dart, co-auteur de la Java Language Specification et de la Java Virtual Machine Specification, a travaillé sur la conception de Java et de la JVM, conçu…
Jörg W Mittag
10
Strongtalk (un système de type statique pour Smalltalk), le système de type Dart, le système de type Newspeak, sa thèse de doctorat sur la modularité est la base de presque tous les systèmes de modules modernes (par exemple Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke , Seph), ses articles sur les mixins ont révolutionné notre façon de penser à leur sujet. Maintenant, cela ne pas dire qu'il est juste, mais je ne pense que d' avoir conçu plusieurs systèmes de type statique lui fait un peu plus d'un « troll ».
Jörg W Mittag
17
"Bien qu'il soit vrai que les systèmes de types" vous limitent à un sous-ensemble ", les éléments extérieurs à ce sous-ensemble sont, par définition, des éléments qui ne fonctionnent pas." - C'est faux. Nous savons par l'indécidabilité du problème de l'arrêt, le théorème de Rice et la myriade d'autres résultats d'indécidabilité et d'informatabilité qu'un vérificateur de type statique ne peut pas décider pour tous les programmes s'ils sont de type sûr ou de type non sûr. Il ne peut pas accepter ces programmes (dont certains ne sont pas sûrs pour le type), donc le seul choix sensé est de les rejeter (cependant, certains d'entre eux sont sûrs pour le type). Alternativement, la langue doit être conçue en…
Jörg W Mittag
9
… De manière à empêcher le programmeur d'écrire ces programmes indécidables, mais encore une fois, certains d'entre eux sont en fait de type sûr. Donc, peu importe comment vous le découpez: le programmeur ne peut pas écrire de programmes de type sûr. Et comme il y a en réalité infiniment beaucoup d'entre eux ( en général), nous pouvons être à peu près certain qu'au moins certains d'entre eux ne sont pas seulement des choses qui ne le travail, mais aussi utile.
Jörg W Mittag
8
@MasonWheeler: le problème de l'arrêt se pose tout le temps, précisément dans le contexte de la vérification de type statique. À moins que les langages ne soient soigneusement conçus pour empêcher le programmeur d'écrire certains types de programmes, la vérification de type statique devient rapidement équivalente à la résolution du problème d'arrêt. Soit vous vous retrouvez avec des programmes que vous n'êtes pas autorisé à écrire car ils pourraient confondre le vérificateur de type, soit vous vous retrouvez avec des programmes que vous êtes autorisé à écrire mais ils prennent un temps infini pour taper check.
Jörg W Mittag
27

Je vais laisser de côté la partie «modèle» parce que je pense que cela dépend de la définition de ce qui est ou non un modèle et je me suis depuis longtemps désintéressé de ce débat. Ce que je dirai, c'est qu'il y a des choses que vous pouvez faire dans certaines langues que vous ne pouvez pas faire dans d'autres. Soyons clairs, je ne dis pas qu'il y a des problèmes que vous pouvez résoudre dans une langue que vous ne pouvez pas résoudre dans une autre. Mason a déjà souligné l'exhaustivité de Turing.

Par exemple, j'ai écrit une classe en python qui prend encapsule un élément DOM XML et en fait un objet de première classe. Autrement dit, vous pouvez écrire le code:

doc.header.status.text()

et vous avez le contenu de ce chemin à partir d'un objet XML analysé. sorte de propre et bien rangé, OMI. Et s'il n'y a pas de nœud principal, il renvoie simplement des objets factices qui ne contiennent que des objets factices (des tortues tout le long). Il n'y a pas de véritable moyen de le faire, disons, en Java. Il faudrait avoir compilé à l'avance une classe basée sur une certaine connaissance de la structure du XML. Mettant de côté si c'est une bonne idée, ce genre de chose change vraiment la façon dont vous résolvez les problèmes dans un langage dynamique. Je ne dis pas que cela change d'une manière qui est nécessairement toujours meilleure, cependant. Il y a des coûts précis pour les approches dynamiques et la réponse de Mason donne un aperçu décent. Leur choix dépend de nombreux facteurs.

En passant, vous pouvez le faire en Java car vous pouvez créer un interpréteur python en Java . Le fait que résoudre un problème spécifique dans une langue donnée peut signifier la construction d'un interprète ou quelque chose de similaire est souvent ignoré lorsque les gens parlent de l'intégralité de Turing.

JimmyJames
la source
4
Vous ne pouvez pas faire cela en Java car Java est mal conçu. Ce ne serait pas si difficile en utilisant C # IDynamicMetaObjectProvider, et c'est très simple dans Boo. ( Voici une implémentation en moins de 100 lignes, incluse dans le cadre de l'arborescence source standard sur GitHub, car c'est aussi simple que cela!)
Mason Wheeler
6
@MasonWheeler "IDynamicMetaObjectProvider"? Est-ce lié au dynamicmot-clé de C # ? ... qui claque efficacement sur la frappe dynamique en C #? Je ne suis pas sûr que votre argument soit valable si j'ai raison.
jpmc26
9
@MasonWheeler Vous entrez dans la sémantique. Sans entrer dans un débat sur les minuties (nous ne développons pas un formalisme mathématique sur SE ici.), Le typage dynamique est la pratique de renoncer aux décisions de temps de compilation autour des types, en particulier la vérification que chaque type a les membres particuliers auxquels le programme accède. C'est l'objectif qui dynamicatteint en C #. Les «recherches de réflexion et de dictionnaire» se produisent au moment de l'exécution, pas lors de la compilation. Je ne sais vraiment pas comment vous pouvez faire valoir qu'il n'ajoute pas de frappe dynamique à la langue. Mon point est que le dernier paragraphe de Jimmy couvre cela.
jpmc26
44
Bien que je ne sois pas un grand fan de Java, j'ose aussi dire qu'appeler Java "mal conçu", précisément parce qu'il n'ajoute pas de typage dynamique, est ... trop zélé.
jpmc26
5
Mis à part la syntaxe légèrement plus pratique, en quoi est-ce différent d'un dictionnaire?
Theodoros Chatzigiannakis
10

La citation est correcte, mais aussi vraiment mensongère. Décomposons-le pour voir pourquoi:

La chose merveilleuse à propos de la frappe dynamique est qu'elle vous permet d'exprimer tout ce qui est calculable.

Enfin, pas tout à fait. Une langue avec une frappe dynamique vous permet d'exprimer n'importe quoi tant que Turing est complet , ce que la plupart sont. Le système de type lui-même ne vous permet pas de tout exprimer. Donnons-lui le bénéfice du doute ici cependant.

Et les systèmes de type ne le font pas - les systèmes de type sont généralement décidables, et ils vous limitent à un sous-ensemble.

C'est vrai, mais notez que nous parlons maintenant fermement de ce que permet le système de types , et non de ce que permet le langage qui utilise un système de types. Bien qu'il soit possible d'utiliser un système de type pour calculer des éléments au moment de la compilation, cela n'est généralement pas Turing complet (car le système de type est généralement décidable), mais presque tous les langages typés statiquement sont également Turing complet dans son exécution (les langages typés dépendants sont non, mais je ne pense pas que nous en parlions ici).

Les gens qui préfèrent les systèmes de type statique disent: «ça va, c'est assez bien; tous les programmes intéressants que vous voulez écrire fonctionneront comme des types ». Mais c'est ridicule - une fois que vous avez un système de type, vous ne savez même pas quels programmes intéressants existent.

Le problème est que les langages de types dynamiques ont un type statique. Parfois, tout est une chaîne, et le plus souvent, il existe une union étiquetée où chaque chose est soit un sac de propriétés, soit une valeur comme un entier ou un double. Le problème est que les langages statiques peuvent également le faire, historiquement c'était un peu plus maladroit pour le faire, mais les langages modernes typés statiquement rendent cela à peu près aussi facile à faire que l'utilisation d'un langage de types dynamiques, alors comment peut-il y avoir une différence dans ce que le programmeur peut voir comme un programme intéressant? Les langages statiques ont exactement les mêmes unions étiquetées ainsi que d'autres types.

Pour répondre à la question dans le titre: Non, il n'y a pas de modèles de conception qui ne peuvent pas être implémentés dans un langage typé statiquement, car vous pouvez toujours implémenter suffisamment de système dynamique pour les obtenir. Il peut y avoir des modèles que vous obtenez gratuitement dans une langue dynamique; cela peut ou non valoir la peine de supporter les inconvénients de ces langues pour YMMV .

jk.
la source
2
Je ne sais pas vraiment si vous venez de répondre oui ou non. Cela ressemble plus à un non pour moi.
user7610
1
@TheodorosChatzigiannakis Oui, comment les langues dynamiques seraient-elles implémentées autrement? Tout d'abord, vous passerez pour un architecte astronaute si vous souhaitez implémenter un système de classe dynamique ou tout autre élément un peu impliqué. Deuxièmement, vous n'avez probablement pas la ressource pour le rendre déboguable, totalement introspectable, performant («utilisez simplement un dictionnaire» est la façon dont les langages lents sont implémentés). Troisièmement, certaines fonctionnalités dynamiques sont mieux utilisées lorsqu'elles sont intégrées dans l'ensemble du langage, pas seulement en tant que bibliothèque: pensez au garbage collection par exemple (il existe des GC en tant que bibliothèques, mais elles ne sont pas couramment utilisées).
coredump
1
@Theodoros Selon l'article que j'ai déjà lié ici une fois, toutes les structures sauf 2,5% (dans les modules Python examinés) peuvent être facilement exprimées dans un langage typé. Peut-être que les 2,5% valent la peine de payer les frais de frappe dynamique. Voilà essentiellement ma question. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610
3
@JiriDanek Pour autant que je sache, rien n'empêche un langage typé statiquement d'avoir des spots d'appel polymorphes et de maintenir un typage statique dans le processus. Voir Vérification de type statique de multi-méthodes . Peut-être que je comprends mal votre lien.
Theodoros Chatzigiannakis
1
"Une langue avec une frappe dynamique vous permet d'exprimer n'importe quoi tant que Turing est complet, ce que la plupart sont." Bien que ce soit bien sûr une vraie déclaration, elle ne tient pas vraiment dans "le monde réel" parce que la quantité de texte que l'on a écrire pourrait être extrêmement volumineux.
Daniel Jour
4

Il y a sûrement des choses que vous ne pouvez faire que dans des langues typées dynamiquement. Mais ils ne seraient pas nécessairement de bonne conception.

Vous pouvez attribuer d'abord un entier 5 puis une chaîne 'five'ou un Catobjet à la même variable. Mais vous ne faites que rendre plus difficile pour un lecteur de votre code de comprendre ce qui se passe, quel est le but de chaque variable.

Vous pouvez ajouter une nouvelle méthode à une classe Ruby de bibliothèque et accéder à ses champs privés. Il peut y avoir des cas où un tel hack peut être utile mais ce serait une violation de l'encapsulation. (Cela ne me dérange pas d'ajouter des méthodes uniquement en se basant sur l'interface publique, mais ce n'est rien que les méthodes d'extension C # typées statiquement ne peuvent pas faire.)

Vous pouvez ajouter un nouveau champ à un objet de la classe de quelqu'un d'autre pour lui transmettre des données supplémentaires. Mais il est préférable de simplement créer une nouvelle structure ou d'étendre le type d'origine.

En règle générale, plus vous voulez que votre code reste organisé, moins vous devriez tirer parti de la possibilité de modifier dynamiquement les définitions de type ou d'affecter des valeurs de différents types à la même variable. Mais votre code n'est pas différent de ce que vous pourriez obtenir dans un langage typé statiquement.

Les langages dynamiques sont bons dans le sucre syntaxique. Par exemple, lorsque vous lisez un objet JSON désérialisé, vous pouvez faire référence à une valeur imbriquée simplement comme obj.data.article[0].content- beaucoup plus propre que disons obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Les développeurs Ruby en particulier pourraient parler longuement de la magie qui peut être obtenue en implémentant method_missing, qui est une méthode vous permettant de gérer les appels tentés vers des méthodes non déclarées. Par exemple, ActiveRecord ORM l'utilise pour que vous puissiez effectuer un appel User.find_by_email('[email protected]')sans jamais déclarer de find_by_emailméthode. Bien sûr, ce n'est rien qui ne puisse pas être réalisé comme UserRepository.FindBy("email", "[email protected]")dans un langage typé, mais vous ne pouvez pas lui nier sa netteté.

kamilk
la source
4
Il y a sûrement des choses que vous ne pouvez faire que dans des langues typées statiquement. Mais ils ne seraient pas nécessairement de bonne conception.
coredump
2
Le point sur le sucre syntaxique a très peu à voir avec la frappe dynamique et tout avec, bien, la syntaxe.
leftaroundabout
@leftaroundabout Les modèles ont tout à voir avec la syntaxe. Les systèmes de type ont également beaucoup à voir avec cela.
user253751
4

Le modèle de proxy dynamique est un raccourci pour implémenter des objets proxy sans avoir besoin d'une classe par type à proxy.

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

En utilisant cela, Proxy(someObject)crée un nouvel objet qui se comporte de la même manière que someObject. Évidemment, vous voudrez également ajouter des fonctionnalités supplémentaires, mais c'est une base utile pour commencer. Dans un langage statique complet, vous devez soit écrire une classe Proxy par type que vous souhaitez proxy, soit utiliser la génération de code dynamique (qui, il est vrai, est incluse dans la bibliothèque standard de nombreux langages statiques, principalement parce que leurs concepteurs connaissent les problèmes ne pouvant pas faire cette cause).

Un autre cas d'utilisation des langages dynamiques est le "patch de singe". À bien des égards, il s'agit d'un anti-motif plutôt que d'un motif, mais il peut être utilisé de manière utile s'il est fait avec soin. Et bien qu'il n'y ait aucune raison théorique pour que le patch de singe ne puisse pas être implémenté dans un langage statique, je n'en ai jamais vu un qui le possède.

Jules
la source
Je pense que je pourrais être en mesure d'imiter cela dans Go. Il existe un ensemble de méthodes que tous les objets mandatés doivent avoir (sinon le canard pourrait ne pas charlatan et tout s'effondrer). Je peux créer une interface Go avec ces méthodes. Je vais devoir y réfléchir davantage, mais je pense que ce que j'ai en tête fonctionnera.
user7610
Vous pouvez quelque chose de similaire dans n'importe quel langage .NET avec RealProxy et génériques.
LittleEwok
@LittleEwok - RealProxy utilise la génération de code d'exécution - comme je le dis, de nombreux langages statiques modernes ont une solution de contournement comme celle-ci, mais c'est toujours plus facile dans un langage dynamique.
Jules
Les méthodes d'extension C # sont un peu comme les correctifs de singe sécurisés. Vous ne pouvez pas modifier les méthodes existantes, mais vous pouvez en ajouter de nouvelles.
Andrew dit Réintégrer Monica le
3

Oui , il existe de nombreux modèles et techniques qui ne sont possibles que dans un langage typé dynamiquement.

La correction de singe est une technique où des propriétés ou des méthodes sont ajoutées aux objets ou aux classes lors de l'exécution. Cette technique n'est pas possible dans un langage typé car cela signifie que les types et les opérations ne peuvent pas être vérifiés au moment de la compilation. Ou pour le dire autrement, si une langue prend en charge le patch de singe, c'est par définition une langue dynamique.

Il peut être prouvé que si un langage prend en charge la correction de singe (ou des techniques similaires pour modifier les types lors de l'exécution), il ne peut pas être vérifié statiquement. Il ne s'agit donc pas seulement d'une limitation dans les langues existantes, c'est une limitation fondamentale du typage statique.

La citation est donc tout à fait correcte - plus de choses sont possibles dans un langage dynamique que dans un langage typé statiquement. D'un autre côté, certains types d' analyses ne sont possibles que dans un langage typé statiquement. Par exemple, vous savez toujours quelles opérations sont autorisées sur un type donné, ce qui vous permet de détecter les opérations illégales au type de compilation. Aucune vérification de ce type n'est possible dans un langage dynamique lorsque des opérations peuvent être ajoutées ou supprimées au moment de l'exécution.

C'est pourquoi il n'y a pas de "meilleur" évident dans le conflit entre les langages statiques et dynamiques. Les langages statiques abandonnent une certaine puissance au moment de l'exécution en échange d'une puissance différente au moment de la compilation, ce qui, selon eux, réduit le nombre de bogues et facilite le développement. Certains croient que le compromis en vaut la peine, d'autres non.

D'autres réponses ont fait valoir que l'équivalence de Turing signifie que tout ce qui est possible dans une langue est possible dans toutes les langues. Mais cela ne suit pas. Pour prendre en charge quelque chose comme le patch de singe dans un langage statique, vous devez essentiellement implémenter un sous-langage dynamique à l'intérieur du langage statique. C'est bien sûr possible, mais je dirais que vous programmez alors dans un langage dynamique intégré, car vous perdez également la vérification de type statique qui existe dans le langage hôte.

C # depuis la version 4 prend en charge les objets typés dynamiquement. Il est clair que les concepteurs de langage voient l'avantage d'avoir les deux types de frappe disponibles. Mais cela montre également que vous ne pouvez pas avoir votre gâteau et manger moi aussi: lorsque vous utilisez des objets dynamiques en C #, vous gagnez la capacité de faire quelque chose comme le patch de singe, mais vous perdez également la vérification de type statique pour l'interaction avec ces objets.

JacquesB
la source
+1 votre avant-dernier paragraphe, je pense que c'est l'argument crucial. Je dirais toujours qu'il y a une différence, cependant, comme avec les types statiques, vous avez le contrôle total de l'endroit et de ce que vous pouvez patch de singe
jk.
2

Je me demande, y a-t-il des modèles ou stratégies de conception utiles qui, en utilisant la formulation de la citation, "ne fonctionnent pas comme des types"?

Oui et non.

Il existe des situations dans lesquelles le programmeur connaît le type d'une variable avec plus de précision qu'un compilateur. Le compilateur peut savoir que quelque chose est un objet, mais le programmeur saura (en raison des invariants du programme) qu'il s'agit en fait d'une chaîne.

Permettez-moi d'en montrer quelques exemples:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

Je sais que someMap.get(T.class)cela retournera un Function<T, String>, à cause de la façon dont j'ai construit someMap. Mais Java est seulement sûr que j'ai une fonction.

Un autre exemple:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

Je sais que data.properties.rowCount sera une référence valide et un entier, car j'ai validé les données par rapport à un schéma. Si ce champ était manquant, une exception aurait été levée. Mais un compilateur ne saurait que lancer une exception ou renvoyer une sorte de JSONValue générique.

Un autre exemple:

x, y, z = struct.unpack("II6s", data)

Les "II6" définissent la façon dont les données codent pour trois variables. Depuis que j'ai spécifié le format, je sais quels types seront retournés. Un compilateur sait seulement qu'il renvoie un tuple.

Le thème unificateur de tous ces exemples est que le programmeur connaît le type, mais un système de type de niveau Java ne pourra pas refléter cela. Le compilateur ne connaîtra pas les types, et donc un langage typé statiquement ne me permettra pas de les appeler, alors qu'un langage typé dynamiquement le fera.

C'est à cela que la citation originale aboutit:

La chose merveilleuse à propos de la frappe dynamique est qu'elle vous permet d'exprimer tout ce qui est calculable. Et les systèmes de type ne le font pas - les systèmes de type sont généralement décidables, et ils vous limitent à un sous-ensemble.

Lorsque j'utilise la frappe dynamique, je peux utiliser le type le plus dérivé que je connaisse, pas simplement le type le plus dérivé que le système de type de ma langue connaît. Dans tous les cas ci-dessus, j'ai un code sémantiquement correct, mais qui sera rejeté par un système de typage statique.

Cependant, pour revenir à votre question:

Je me demande, y a-t-il des modèles ou stratégies de conception utiles qui, en utilisant la formulation de la citation, "ne fonctionnent pas comme des types"?

N'importe lequel des exemples ci-dessus, et en fait tout exemple de typage dynamique peut être rendu valide en typage statique en ajoutant des transtypages appropriés. Si vous connaissez un type que votre compilateur ne connaît pas, dites simplement au compilateur en transtypant la valeur. Donc, à un certain niveau, vous n'obtiendrez aucun motif supplémentaire en utilisant la frappe dynamique. Vous devrez peut-être simplement convertir plus pour obtenir du code tapé de manière statique.

L'avantage de la frappe dynamique est que vous pouvez simplement utiliser ces modèles sans vous inquiéter du fait qu'il est difficile de convaincre votre système de saisie de leur validité. Cela ne change pas les modèles disponibles, cela les rend peut-être plus faciles à implémenter, car vous n'avez pas à comprendre comment faire en sorte que votre système de types reconnaisse le modèle ou ajouter des transtypages pour renverser le système de types.

Winston Ewert
la source
1
pourquoi java est-il le point de coupure auquel vous ne devriez pas aller vers un «système de type plus avancé / compliqué»?
jk.
2
@jk, qu'est-ce qui vous fait penser que c'est ce que je dis? J'ai explicitement évité de prendre parti sur l'opportunité ou non d'un système de type plus avancé / compliqué.
Winston Ewert
2
Certains de ces exemples sont terribles, et les autres semblent être davantage des décisions linguistiques plutôt que typées vs non typées. Je suis particulièrement confus quant à la raison pour laquelle les gens pensent que la désérialisation est si complexe dans les langages typés. Le résultat tapé serait data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); et si l'on n'a pas de classe à désérialiser, nous pouvons revenir à data = parseJSON(someJson); print(data["properties.rowCount"]);- qui est toujours tapé et exprime la même intention.
NPSF3000
2
@ NPSF3000, comment fonctionne la fonction parseJSON? Il semblerait soit utiliser la réflexion ou des macros. Comment les données ["properties.rowCount"] pourraient-elles être saisies dans un langage statique? Comment pourrait-il savoir que la valeur résultante est un entier?
Winston Ewert
2
@ NPSF3000, comment prévoyez-vous de l'utiliser si vous ne connaissez pas son entier? Comment prévoyez-vous de boucler sur les éléments d'une liste dans le JSON sans savoir qu'il s'agissait d'un tableau? Le point de mon exemple était que je savais que data.propertiesc'était un objet et je savais que data.properties.rowCountc'était un entier et je pouvais simplement écrire du code qui les utilisait. Votre proposition data["properties.rowCount"]ne fournit pas la même chose.
Winston Ewert
1

Voici quelques exemples d'Objective-C (typés dynamiquement) qui ne sont pas possibles en C ++ (typés statiquement):

  • Mettre des objets de plusieurs classes distinctes dans le même conteneur.
    Bien sûr, cela nécessite une inspection de type d'exécution pour interpréter ultérieurement le contenu du conteneur, et la plupart des amis de la frappe statique objecteront que vous ne devriez pas faire cela en premier lieu. Mais j'ai trouvé qu'au-delà des débats religieux, cela peut être utile.

  • Extension d'une classe sans sous-classement.
    Dans Objective-C, vous pouvez définir de nouvelles fonctions membres pour les classes existantes, y compris celles définies par le langage comme NSString. Par exemple, vous pouvez ajouter une méthode stripPrefixIfPresent:, pour que vous puissiez dire [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](notez l'utilisation des NSSringlittéraux @"").

  • Utilisation de rappels orientés objet.
    Dans les langages typés statiquement comme Java et C ++, vous devez aller très loin pour permettre à une bibliothèque d'appeler un membre arbitraire d'un objet fourni par l'utilisateur. En Java, la solution de contournement est la paire interface / adaptateur plus une classe anonyme, en C ++, la solution de contournement est généralement basée sur un modèle, ce qui implique que le code de bibliothèque doit être exposé au code utilisateur. Dans Objective-C, vous passez simplement la référence d'objet plus le sélecteur de la méthode à la bibliothèque, et la bibliothèque peut simplement et directement appeler le rappel.

cmaster
la source
Je peux faire le premier en C ++ en transtypant void *, mais cela contourne le système de type, donc ça ne compte pas. Je peux faire le second en C # avec des méthodes d'extension, parfaitement dans un système de type. Pour le troisième, je pense que le "sélecteur de méthode" peut être un lambda, donc tout langage typé statiquement avec lambdas peut faire la même chose, si je comprends bien. Je ne connais pas ObjC.
user7610
1
@JiriDanek "Je peux faire le premier en C ++ en transtypant void *", pas exactement, le code qui lit les éléments n'a aucun moyen de récupérer le type réel par lui-même. Vous avez besoin de balises de type. En outre, je ne pense pas que dire "je peux le faire en <langue>" soit la manière appropriée / productive de voir les choses, car vous pouvez toujours les émuler. Ce qui compte, c'est le gain d'expressivité par rapport à la complexité de la mise en œuvre. De plus, vous semblez penser que si un langage possède à la fois des capacités statiques et dynamiques (Java, C #), il appartient exclusivement à la famille des langages "statiques".
coredump
1
@JiriDanek void*seul n'est pas une frappe dynamique, c'est un manque de frappe. Mais oui, dynamic_cast, tables virtuelles, etc. rendent le C ++ non purement statique. Est-ce mauvais?
coredump
1
Cela suggère qu'il est utile d'avoir la possibilité de subvertir le système de type en cas de besoin. Avoir une trappe d'évacuation lorsque vous en avez besoin. Ou quelqu'un l'a jugé utile. Sinon, ils ne le mettraient pas dans la langue.
user7610
2
@JiriDanek Je pense que vous l'avez à peu près cloué avec votre dernier commentaire. Ces trappes d'évacuation peuvent être extrêmement utiles si elles sont utilisées avec soin. Néanmoins, avec un grand pouvoir vient une grande responsabilité, et il y a beaucoup de gens qui en abusent ... Ainsi, il est beaucoup mieux d'utiliser un pointeur vers une classe de base générique dont toutes les autres classes dérivent par définition (comme c'est le cas à la fois dans Objective-C et Java), et de s'appuyer sur RTTI pour différencier les cas, plutôt que de transposer un void*en un type d'objet spécifique. Le premier produit une erreur d'exécution si vous vous trompez, le dernier entraîne un comportement indéfini.
cmaster