Gibt es Entwurfsmuster, die nur in dynamisch typisierten Sprachen wie Python möglich sind?


30

Ich habe eine verwandte Frage gelesen. Gibt es Entwurfsmuster, die in dynamischen Sprachen wie Python nicht erforderlich sind? und erinnerte mich an dieses Zitat auf Wikiquote.org

Das Wunderbare an dynamischer Eingabe ist, dass Sie damit alles ausdrücken können, was berechenbar ist. Typsysteme sind normalerweise nicht bestimmbar und beschränken Sie auf eine Teilmenge. Leute, die statische Systeme bevorzugen, sagen: „Es ist in Ordnung, es ist gut genug. Alle interessanten Programme, die Sie schreiben möchten, funktionieren als Typen. “ Aber das ist lächerlich - wenn Sie ein Typensystem haben, wissen Sie nicht einmal, welche interessanten Programme es gibt.

--- Software Engineering Radio Folge 140: Newspeak und Pluggable Types mit Gilad Bracha

Ich frage mich, ob es nützliche Entwurfsmuster oder -strategien gibt, die mit der Formulierung des Zitats "nicht als Typen arbeiten"?


3
Ich habe festgestellt, dass der doppelte Versand und das Besuchermuster in statisch getippten Sprachen sehr schwierig, in dynamischen Sprachen jedoch leicht zu erreichen sind. Siehe diese Antwort (und die Frage) zum Beispiel: programmers.stackexchange.com/a/288153/122079
user3002473

7
Na sicher. Ein beliebiges Muster, bei dem beispielsweise zur Laufzeit neue Klassen erstellt werden. (Das ist auch in Java möglich, aber nicht in C ++; es gibt eine stufenweise Dynamik).
user253751

1
Es hängt sehr davon ab, wie ausgefeilt Ihr Typensystem ist :-) Funktionale Sprachen können dies normalerweise recht gut.
Bergi

1
Alle scheinen Systeme wie Java und C # anstelle von Haskell oder OCaml zu sprechen. Eine Sprache mit einem leistungsstarken Schriftsystem kann so präzise wie eine dynamische Sprache sein, aber die Schriftsicherheit gewährleisten.
Andrew sagt Reinstate Monica

@immibis Das ist falsch. Statische Typsysteme können zur Laufzeit absolut neue, "dynamische" Klassen erstellen. Siehe Kapitel 33 der praktischen Grundlagen für Programmiersprachen.
Gardenhead

Antworten:


4

Erstklassige Typen

Dynamische Typisierung bedeutet, dass Sie erstklassige Typen haben: Sie können Typen zur Laufzeit überprüfen, erstellen und speichern, einschließlich der sprachspezifischen Typen. Dies bedeutet auch, dass Werte eingegeben werden und keine Variablen .

Statisch typisierte Sprache kann Code erzeugen, der ebenfalls auf dynamischen Typen beruht, wie z. B. Methodenversand, Typklassen usw., jedoch auf eine Weise, die für die Laufzeit im Allgemeinen unsichtbar ist. Bestenfalls geben sie Ihnen die Möglichkeit, eine Selbstbeobachtung durchzuführen. Alternativ können Sie Typen als Werte simulieren, haben aber dann ein dynamisches Ad-hoc-Typensystem.

Dynamische Typsysteme haben jedoch selten nur erstklassige Typen. Sie können erstklassige Symbole, erstklassige Pakete, erstklassige ... alles haben. Dies steht im Gegensatz zu der strikten Trennung zwischen der Compilersprache und der Laufzeitsprache in statisch typisierten Sprachen. Was der Compiler oder Interpreter kann, kann auch die Laufzeit tun.

Lassen Sie uns nun zustimmen, dass die Typinferenz eine gute Sache ist und dass ich meinen Code gerne überprüfen lasse, bevor ich ihn ausführe. Ich mag es aber auch, zur Laufzeit Code erzeugen und kompilieren zu können. Und ich liebe es, Dinge zur Kompilierungszeit vorauszurechnen. In einer dynamisch getippten Sprache erfolgt dies mit derselben Sprache. In OCaml haben Sie das Modul- / Funktortypsystem, das sich vom Haupttypsystem unterscheidet, das sich von der Präprozessorsprache unterscheidet. In C ++ haben Sie die Vorlagensprache, die nichts mit der Hauptsprache zu tun hat, die während der Ausführung im Allgemeinen keine Typenkenntnisse aufweist. Und das ist in dieser Sprache in Ordnung , weil sie nicht mehr bieten wollen.

Letztendlich ändert das nicht wirklich, welche Art von Software Sie entwickeln können, aber die Ausdruckskraft ändert, wie Sie sie entwickeln und ob es schwierig ist oder nicht.

Muster

Muster, die auf dynamischen Typen basieren, sind Muster, die dynamische Umgebungen umfassen: offene Klassen, Dispatching, In-Memory-Datenbanken von Objekten, Serialisierung usw. Einfache Dinge wie generische Container funktionieren, weil ein Vektor zur Laufzeit nicht den Typ der darin enthaltenen Objekte vergisst (keine Notwendigkeit für parametrische Typen).

Ich habe versucht, die vielen Arten der Codeauswertung in Common Lisp sowie Beispiele für mögliche statische Analysen einzuführen (dies ist SBCL). Das Sandbox-Beispiel kompiliert eine kleine Teilmenge des Lisp-Codes, der aus einer separaten Datei abgerufen wird. Um einigermaßen sicher zu sein, ändere ich die Lesetabelle, lasse nur eine Teilmenge der Standardsymbole zu und bringe Dinge mit einer Zeitüberschreitung in einen Zeilenumbruch.

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

Nichts oben ist "unmöglich", mit anderen Sprachen zu tun. Der Plug-in-Ansatz in Blender, in Musiksoftware oder IDEs für statisch kompilierte Sprachen, die eine schnelle Neukompilierung durchführen usw. Anstelle von externen Tools bevorzugen dynamische Sprachen Tools, die bereits vorhandene Informationen verwenden. Alle bekannten Anrufer von FOO? alle Unterklassen von BAR? alle Methoden, die von Klasse ZOT spezialisiert sind? Dies sind internalisierte Daten. Typen sind nur ein weiterer Aspekt.


(siehe auch: CFFI )


39

Kurze Antwort: Nein, weil Turing Äquivalenz.

Lange Antwort: Dieser Typ ist ein Troll. Während es stimmt, dass Typsysteme "Sie auf eine Teilmenge beschränken", sind die Dinge außerhalb dieser Teilmenge per Definition Dinge, die nicht funktionieren.

Alles, was Sie in einer Turing-vollständigen Programmiersprache tun können (eine Sprache, die für die allgemeine Programmierung entwickelt wurde, plus viele, die nicht vorhanden sind). Es ist ein ziemlich niedriger Balken, der zu übersehen ist, und es gibt mehrere Beispiele dafür, wie ein System zu Turing wird. komplettieren Sie unbeabsichtigt), die Sie in jeder anderen Programmiersprache von Turing-complete ausführen können. Dies wird "Turing-Äquivalenz" genannt und bedeutet nur genau das, was es sagt. Wichtig ist, dass Sie das andere nicht genauso einfach in der anderen Sprache tun können - einige würden argumentieren, dass dies der springende Punkt bei der Erstellung einer neuen Programmiersprache ist: Ihnen eine bessere Möglichkeit zu geben, bestimmte Dinge zu tun Dinge, an denen existierende Sprachen scheißen.

Ein dynamisches Typsystem kann beispielsweise über ein statisches OO-Typsystem emuliert werden, indem einfach alle Variablen, Parameter und Rückgabewerte als Basistyp deklariert werden Objectund dann mithilfe von Reflection auf die spezifischen Daten in diesem System zugegriffen wird Sie sehen, dass Sie in einer dynamischen Sprache buchstäblich nichts tun können, was Sie in einer statischen Sprache nicht tun können. Aber das wäre natürlich ein großes Durcheinander.

Der Typ aus dem Zitat ist richtig, dass statische Typen Ihre Möglichkeiten einschränken, aber das ist ein wichtiges Merkmal, kein Problem. Die Linien auf der Straße beschränken, was Sie in Ihrem Auto tun können, aber finden Sie sie einschränkend oder hilfreich? (Ich weiß, dass ich nicht auf einer viel befahrenen, komplexen Straße fahren möchte, auf der die Autos nicht in die entgegengesetzte Richtung fahren müssen, um auf ihrer Seite zu bleiben und nicht dahin zu kommen, wo ich fahre!) Indem Sie Regeln aufstellen, die klar definieren, was ist Sie werden als ungültiges Verhalten eingestuft und stellen sicher, dass dies nicht passiert. Dadurch verringern Sie die Wahrscheinlichkeit eines bösen Absturzes erheblich.

Außerdem charakterisiert er die andere Seite falsch. Es ist nicht so, dass "alle interessanten Programme, die Sie schreiben möchten, als Typen funktionieren", sondern dass "alle interessanten Programme, die Sie schreiben möchten, Typen erfordern ". Sobald Sie ein bestimmtes Maß an Komplexität überschritten haben, wird es aus zwei Gründen sehr schwierig, die Codebasis ohne ein Typsystem aufrechtzuerhalten, das Sie auf dem Laufenden hält.

Erstens, weil Code ohne Typanmerkungen schwer zu lesen ist. Betrachten Sie das folgende Python:

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

Wie sehen die Daten aus, die das System am anderen Ende der Verbindung empfängt? Und wenn es etwas empfängt, das völlig falsch aussieht, wie finden Sie heraus, was los ist?

Auf die Struktur von kommt es an value.someProperty. Aber wie sieht es aus? Gute Frage! Was ruft an sendData()? Was vergeht? Wie sieht diese Variable aus? Wo ist es hergekommen? Wenn es nicht lokal ist, müssen Sie die gesamte Historie nachverfolgen, valueum herauszufinden, was los ist. Möglicherweise übergeben Sie etwas anderes, das ebenfalls eine somePropertyEigenschaft hat, aber nicht das tut, was Sie denken, dass dies der Fall ist?

Schauen wir uns das jetzt mit Typanmerkungen an, wie Sie vielleicht in der Boo-Sprache sehen, die eine sehr ähnliche Syntax verwendet, aber statisch geschrieben ist:

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

Wenn etwas schief geht, ist Ihre Debug-Aufgabe plötzlich um eine Größenordnung einfacher geworden: Sehen Sie sich die Definition von MyDataType! Außerdem wird die Wahrscheinlichkeit, dass Sie sich schlecht verhalten, weil Sie einen inkompatiblen Typ übergeben haben, der auch eine Eigenschaft mit demselben Namen hat, plötzlich auf Null gesetzt, weil das Typensystem Sie nicht dazu veranlasst, diesen Fehler zu machen.

Der zweite Grund baut auf dem ersten auf: In einem großen und komplexen Projekt haben Sie höchstwahrscheinlich mehrere Mitwirkende. (Und wenn nicht, bauen Sie es über einen langen Zeitraum selbst, was im Wesentlichen dasselbe ist. Versuchen Sie, Code zu lesen, den Sie vor 3 Jahren geschrieben haben, wenn Sie mir nicht glauben!) Dies bedeutet, dass Sie nicht wissen, was war Ich gehe den Kopf der Person durch, die fast jeden Teil des Codes zu dem Zeitpunkt geschrieben hat, als sie ihn geschrieben hat, weil Sie nicht da waren, oder ich erinnere mich nicht, ob es vor langer Zeit Ihr eigener Code war. Wenn Sie Typdeklarationen haben, können Sie die Absicht des Codes besser verstehen!

Leute wie der Typ im Zitat bezeichnen die Vorteile des statischen Tippens häufig als "Hilfe für den Compiler" oder "Alles über Effizienz" in einer Welt, in der nahezu unbegrenzte Hardwareressourcen dies mit jedem Jahr weniger relevant machen. Aber wie ich gezeigt habe, gibt es diese Vorteile sicherlich, aber der Hauptvorteil liegt in den menschlichen Faktoren, insbesondere der Lesbarkeit und Wartbarkeit des Codes. (Die gesteigerte Effizienz ist aber sicherlich ein schöner Bonus!)


24
"Dieser Typ ist ein Troll." - Ich bin mir nicht sicher, ob ein Ad-Hominem-Angriff Ihrem ansonsten gut präsentierten Fall helfen wird. Und obwohl mir klar ist, dass das Argument der Autorität ein ebenso schlechter Irrtum ist wie das der Ad-Hominem, möchte ich dennoch darauf hinweisen, dass Gilad Bracha wahrscheinlich mehr Sprachen und (für diese Diskussion am relevantesten) statischere Typensysteme entworfen hat als die meisten anderen. Nur ein kleiner Auszug: Er ist der einzige Designer von Newspeak, Co-Designer von Dart, Co-Autor der Java Language Specification und der Java Virtual Machine Specification. Er hat am Design von Java und der JVM gearbeitet, entworfen…
Jörg W Mittag

10
Strongtalk (ein statisches Typsystem für Smalltalk), das Dart-Typsystem, das Newspeak-Typsystem, seine Doktorarbeit über Modularität ist die Grundlage für so ziemlich jedes moderne Modulsystem (z. B. Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke) , Seph), seine Abhandlung (en) über Mixins revolutionierten die Art und Weise, wie wir über sie denken. Nun, das ist nicht bedeuten , dass er richtig ist, aber ich kann denken , dass mehrere statische Typsysteme macht ihn ein bisschen mehr als ein „Troll“ entworfen zu haben.
Jörg W Mittag

17
"Während es wahr ist, dass Typsysteme Sie auf eine Teilmenge beschränken," sind die Dinge außerhalb dieser Teilmenge per Definition Dinge, die nicht funktionieren. " - Das ist falsch. Wir wissen aus der Unentscheidbarkeit des Halteproblems, dem Satz von Rice und der Vielzahl anderer Unentscheidbarkeits- und Unberechenbarkeitsergebnisse, dass eine statische Typprüfung nicht für alle Programme entscheiden kann, ob sie typsicher oder typunsicher sind. Es kann diese Programme nicht akzeptieren (von denen einige nicht typsicher sind). Daher ist es nur sinnvoll, sie abzulehnen (einige davon sind jedoch typsicher). Alternativ muss die Sprache in ... gestaltet werden
Jörg W Mittag

9
… Auf eine Weise, die es dem Programmierer unmöglich macht, diese unentscheidbaren Programme zu schreiben, aber auch hier sind einige davon tatsächlich typsicher. Egal wie Sie es in Scheiben schneiden: Der Programmierer kann keine typsicheren Programme schreiben. Und da es tatsächlich unendlich viele von ihnen ( in der Regel) ist, können wir fast sicher sein , dass zumindest einige von ihnen sind nicht nur Dinge , die tun Arbeit, sondern auch nützlich.
Jörg W Mittag

8
@ MasonWheeler: Das Problem des Anhaltens tritt immer wieder auf, gerade im Rahmen der statischen Typprüfung. Wenn Sprachen nicht sorgfältig entwickelt wurden, um zu verhindern, dass der Programmierer bestimmte Arten von Programmen schreibt, wird die statische Typprüfung schnell zur Lösung des Halteproblems. Entweder Sie sich mit Programmen beenden Sie nicht zu schreiben erlaubt , weil sie die Art Kontrolleur verwirren könnten, oder Sie mit Programmen am Ende Sie sind zu schreiben erlaubt , aber sie unendlich viel Zeit , um Typ - Check nehmen.
Jörg W Mittag

27

Ich werde den 'Muster'-Teil auslassen, weil ich denke, dass er sich in der Definition dessen, was ein Muster ist oder nicht, widerspiegelt und ich das Interesse an dieser Debatte schon lange verloren habe. Was ich sagen werde, ist, dass es Dinge gibt, die Sie in einigen Sprachen tun können, die Sie in anderen nicht tun können. Lassen Sie mich klar sein, ich sage nicht , dass es Probleme gibt, die Sie in einer Sprache lösen können, die Sie in einer anderen nicht lösen können. Mason hat bereits auf die Vollständigkeit hingewiesen.

Ich habe zum Beispiel eine Klasse in Python geschrieben, die ein XML-DOM-Element umschließt und es zu einem erstklassigen Objekt macht. Das heißt, Sie können den Code schreiben:

doc.header.status.text()

und Sie haben den Inhalt dieses Pfads in einem analysierten XML-Objekt. Art ordentlich und ordentlich, IMO. Und wenn es keinen Hauptknoten gibt, gibt er nur Dummy-Objekte zurück, die nichts als Dummy-Objekte enthalten (Schildkröten bis zum Ende). In Java gibt es keine echte Möglichkeit, dies zu tun. Sie müssten im Voraus eine Klasse kompiliert haben, die auf einigen Kenntnissen der XML-Struktur basiert. Abgesehen davon, dass dies eine gute Idee ist, ändert dies die Art und Weise, wie Sie Probleme in einer dynamischen Sprache lösen. Ich sage nicht, dass es sich auf eine Weise ändert, die notwendigerweise immer besser ist. Dynamische Ansätze sind mit gewissen Kosten verbunden, und die Antwort von Mason gibt einen guten Überblick. Ob sie eine gute Wahl sind, hängt von vielen Faktoren ab.

Nebenbei bemerkt, Sie können dies in Java tun, da Sie einen Python-Interpreter in Java erstellen können . Die Tatsache, dass das Lösen eines bestimmten Problems in einer bestimmten Sprache bedeuten kann, einen Dolmetscher oder etwas Ähnliches zu bauen, wird oft übersehen, wenn über die Vollständigkeit von Turing gesprochen wird.


4
In Java ist dies nicht möglich, da Java schlecht konzipiert ist. In C # wäre das nicht so schwierig IDynamicMetaObjectProvider, und in Boo ist es denkbar einfach. ( Hier ist eine Implementierung in weniger als 100 Zeilen, die als Teil des Standardquellbaums von GitHub enthalten ist, weil es so einfach ist!)
Mason Wheeler

6
@MasonWheeler "IDynamicMetaObjectProvider"? Steht das in Zusammenhang mit dem dynamicSchlüsselwort von C # ? ... was greift eigentlich nur beim dynamischen Tippen in C # an? Ich bin mir nicht sicher, ob Ihr Argument zutrifft, wenn ich recht habe.
jpmc26

9
@MasonWheeler Du kommst in die Semantik. Ohne in eine Debatte über Kleinigkeiten zu geraten (wir entwickeln hier keinen mathematischen Formalismus für SE), ist dynamisches Typisieren die Praxis, auf Entscheidungen zur Kompilierungszeit für Typen zu verzichten, insbesondere die Überprüfung, ob jeder Typ die bestimmten Mitglieder hat, auf die das Programm zugreift. Das ist das Ziel, dynamicdas in C # erreicht wird. "Reflection and Dictionary Lookups" finden zur Laufzeit statt, nicht zur Kompilierungszeit. Ich bin mir wirklich nicht sicher, wie Sie einen Fall erstellen können, bei dem der Sprache keine dynamische Typisierung hinzugefügt wird. Mein Punkt ist, dass Jimmys letzter Absatz das behandelt.
jpmc26

44
Trotz dem nicht ein großer Fan von Java zu sein, auch ich wage zu sagen , dass Aufruf Java „schlecht konzipiert“ speziell , weil es nicht dynamische Eingabe hinzugefügt haben ist ... übereifrig.
jpmc26

5
Wie unterscheidet sich das von einem Wörterbuch, abgesehen von der etwas bequemeren Syntax?
Theodoros Chatzigiannakis

10

Das Zitat ist richtig, aber auch sehr unaufrichtig. Lassen Sie es uns zusammenfassen, um zu sehen, warum:

Das Wunderbare an dynamischer Eingabe ist, dass Sie damit alles ausdrücken können, was berechenbar ist.

Nicht ganz. Mit einer Sprache mit dynamischer Eingabe können Sie alles ausdrücken, solange Turing vollständig ist , was die meisten sind. Das Typensystem selbst lässt Sie nicht alles ausdrücken. Lassen Sie uns ihm den Vorteil des Zweifels hier geben.

Typsysteme sind normalerweise nicht bestimmbar und beschränken Sie auf eine Teilmenge.

Dies ist wahr, aber beachten Sie, dass wir jetzt genau darüber sprechen, was das Typensystem zulässt und nicht, was die Sprache , die ein Typensystem verwendet, zulässt. Während es möglich ist, ein Typensystem zu verwenden, um Sachen zur Kompilierungszeit zu berechnen, ist dies im Allgemeinen nicht vollständig (da das Typensystem im Allgemeinen entscheidbar ist), aber fast jede statisch typisierte Sprache ist auch in ihrer Laufzeit vollständig (abhängig typisierte Sprachen) nicht, aber ich glaube nicht, dass wir hier über sie sprechen).

Leute, die statische Systeme bevorzugen, sagen: „Es ist in Ordnung, es ist gut genug. Alle interessanten Programme, die Sie schreiben möchten, funktionieren als Typen. “ Aber das ist lächerlich - wenn Sie ein Typensystem haben, wissen Sie nicht einmal, welche interessanten Programme es gibt.

Das Problem ist, dass dynamisch typisierte Sprachen einen statischen Typ haben. Manchmal ist alles eine Zeichenfolge, und häufiger gibt es eine getaggte Vereinigung, bei der alles entweder eine Tasche mit Eigenschaften oder ein Wert wie ein int oder ein double ist. Das Problem ist, dass dies auch mit statischen Sprachen möglich ist. In der Vergangenheit war dies etwas umständlicher. Moderne statisch typisierte Sprachen machen dies jedoch so einfach wie die Verwendung einer dynamisch typisierten Sprache. Wie kann es also Unterschiede geben? was kann der programmierer als interessantes programm sehen? Statische Sprachen haben genau die gleichen Gewerkschaften wie andere Typen.

Um die Frage im Titel zu beantworten: Nein, es gibt keine Entwurfsmuster, die nicht in einer statisch typisierten Sprache implementiert werden können, da Sie immer genügend dynamische Systeme implementieren können, um sie abzurufen. Es kann Muster geben, die Sie kostenlos in einer dynamischen Sprache erhalten. Dies könnte es wert sein, die Nachteile dieser Sprachen für YMMV in Kauf zu nehmen .


2
Ich bin mir nicht ganz sicher, ob Sie gerade mit Ja oder Nein geantwortet haben. Klingt für mich eher nach einem Nein.
User7610

1
@TheodorosChatzigiannakis Ja, wie würden andere dynamische Sprachen implementiert? Zunächst müssen Sie sich als Astronautenarchitekt ausgeben, wenn Sie jemals ein dynamisches Klassensystem oder etwas anderes implementieren möchten. Zweitens haben Sie wahrscheinlich nicht die Ressource, um es debug-fähig, vollständig nachvollziehbar und performant zu machen ("nur ein Wörterbuch verwenden" ist die Art und Weise, wie langsame Sprachen implementiert werden). Drittens werden einige dynamische Funktionen am besten verwendet, wenn sie in die gesamte Sprache integriert werden, nicht nur als Bibliothek: Denken Sie beispielsweise an Garbage Collection (es gibt GCs als Bibliotheken, aber sie werden nicht häufig verwendet).
Coredump

1
@Theodoros Laut dem Artikel, den ich hier bereits einmal verlinkt habe, können alle Strukturen mit Ausnahme von 2,5% (in Python-Modulen, auf die sich die Forschungen bezogen) leicht in einer typisierten Sprache ausgedrückt werden. Vielleicht lohnt es sich, die Kosten für dynamisches Tippen mit 2,5% zu bezahlen. Genau darum ging es in meiner Frage. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610

3
@JiriDanek Soweit ich das beurteilen kann, hindert nichts eine statisch getippte Sprache daran, polymorphe Aufrufpunkte zu haben und dabei die statische Typisierung beizubehalten. Siehe Static Type Checking von Multi-Methoden . Vielleicht verstehe ich deinen Link falsch.
Theodoros Chatzigiannakis

1
"Mit einer Sprache mit dynamischer Schreibweise können Sie alles ausdrücken, solange Turing vollständig ist, was die meisten sind." Dies ist natürlich eine echte Aussage, aber sie ist in der "realen Welt" nicht wirklich zutreffend, da die Menge an Text vorhanden ist zu schreiben könnte extrem groß sein.
Daniel Jour

4

Es gibt sicherlich Dinge, die Sie nur in dynamisch getippten Sprachen tun können. Aber sie wären nicht unbedingt gutes Design.

Sie können derselben Variablen zuerst eine Ganzzahl 5, dann eine Zeichenfolge 'five'oder ein CatObjekt zuweisen . Aber Sie machen es einem Leser Ihres Codes nur schwerer, herauszufinden, was los ist und wozu jede Variable dient.

Sie können einer Ruby-Klasse in der Bibliothek eine neue Methode hinzufügen und auf ihre privaten Felder zugreifen. Es mag Fälle geben, in denen ein solcher Hack nützlich sein kann, dies wäre jedoch eine Verletzung der Kapselung. (Es macht mir nichts aus, Methoden hinzuzufügen, die sich nur auf die öffentliche Schnittstelle stützen, aber das ist nichts, was statisch typisierte C # -Erweiterungsmethoden nicht können.)

Sie können einem Objekt der Klasse eines anderen ein neues Feld hinzufügen, um damit zusätzliche Daten weiterzuleiten. Es ist jedoch besser, einfach eine neue Struktur zu erstellen oder den ursprünglichen Typ zu erweitern.

Je organisierter der Code bleiben soll, desto weniger sollten Sie in der Lage sein, Typdefinitionen dynamisch zu ändern oder Werte verschiedener Typen derselben Variablen zuzuweisen. Aber Ihr Code unterscheidet sich nicht von dem, was Sie in einer statisch typisierten Sprache erreichen könnten.

Was dynamische Sprachen können, ist syntaktischer Zucker. Wenn Sie beispielsweise ein deserialisiertes JSON-Objekt lesen, verweisen Sie möglicherweise auf einen verschachtelten Wert, der einfach so obj.data.article[0].contentviel aussagekräftiger ist als beispielsweise obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Insbesondere Ruby-Entwickler könnten sich ausführlich über die Magie unterhalten, die durch die Implementierung erzielt werden kann. Diese method_missingMethode ermöglicht es Ihnen, versuchte Aufrufe nicht deklarierter Methoden zu verarbeiten. ActiveRecord ORM verwendet es beispielsweise, damit Sie einen Anruf tätigen können, User.find_by_email('joe@example.com')ohne jemals eine find_by_emailMethode zu deklarieren . Natürlich ist es nichts, was man nicht wie UserRepository.FindBy("email", "joe@example.com")in einer statisch getippten Sprache erreichen kann, aber man kann es nicht leugnen, dass es ordentlich ist.


4
Es gibt sicherlich Dinge, die Sie nur in statisch getippten Sprachen tun können. Aber sie wären nicht unbedingt gutes Design.
Coredump

2
Der Punkt über syntaktischen Zucker hat sehr wenig mit dynamischer Typisierung zu tun und alles was mit Syntax zu tun hat.
linksum den

@leftaroundabout Muster haben alles mit Syntax zu tun. Typsysteme haben auch viel damit zu tun.
user253751

4

Das Dynamic Proxy-Muster ist eine Verknüpfung zum Implementieren von Proxy-Objekten, ohne dass eine Klasse pro Typ zum Proxy benötigt wird.

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

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

Mit diesem Proxy(someObject)Befehl wird ein neues Objekt erstellt, das sich genauso verhält wie someObject. Natürlich möchten Sie auch zusätzliche Funktionen hinzufügen, aber dies ist eine nützliche Basis, um von hier aus zu beginnen. In einer vollständigen statischen Sprache müssen Sie entweder eine Proxy-Klasse pro Typ schreiben, für den Sie einen Proxy erstellen möchten, oder die dynamische Codegenerierung verwenden (die zugegebenermaßen in der Standardbibliothek vieler statischer Sprachen enthalten ist, vor allem, weil ihre Designer dies kennen die Probleme, die dazu nicht in der Lage sind).

Ein weiterer Anwendungsfall für dynamische Sprachen ist das sogenannte "Monkey Patching". In vielerlei Hinsicht ist dies ein anti-Muster eher als ein Muster, aber es kann sorgfältig wenn man sich in nützlicher Weise verwendet werden. Und obwohl es keinen theoretischen Grund dafür gibt, dass Affen-Patches nicht in einer statischen Sprache implementiert werden können, habe ich noch nie eine gesehen, die diese tatsächlich enthält.


Ich denke, ich könnte dies in Go emulieren. Es gibt eine Reihe von Methoden, die alle Proxy-Objekte haben müssen (sonst quakt die Ente möglicherweise nicht und alle fallen auseinander). Mit diesen Methoden kann ich ein Go-Interface erstellen. Ich muss mehr darüber nachdenken, aber ich denke, was ich vorhabe, wird funktionieren.
User7610

Mit RealProxy und Generics können Sie in jeder .NET-Sprache etwas Ähnliches tun.
LittleEwok

@LittleEwok - RealProxy verwendet die Generierung von Laufzeitcode - wie ich bereits sagte, haben viele moderne statische Sprachen eine solche Problemumgehung, aber es ist in einer dynamischen Sprache immer noch einfacher.
Jules

C # -Erweiterungsmethoden ähneln dem sicheren Patchen von Affen. Sie können vorhandene Methoden nicht ändern, aber neue hinzufügen.
Andrew sagt Reinstate Monica

3

Ja , es gibt viele Muster und Techniken, die nur in einer dynamisch getippten Sprache möglich sind.

Monkey Patching ist eine Technik, bei der Objekte oder Klassen zur Laufzeit um Eigenschaften oder Methoden erweitert werden. Diese Technik ist in einer statisch typisierten Sprache nicht möglich, da dies bedeutet, dass Typen und Operationen zur Kompilierungszeit nicht überprüft werden können. Oder anders ausgedrückt: Wenn eine Sprache Affen-Patches unterstützt, ist sie per Definition eine dynamische Sprache.

Es kann nachgewiesen werden, dass eine Sprache, die Affenpatching (oder ähnliche Techniken zum Ändern von Typen zur Laufzeit) unterstützt, nicht statisch typgeprüft werden kann. Es handelt sich also nicht nur um eine Einschränkung der derzeit vorhandenen Sprachen, sondern auch um eine grundlegende Einschränkung der statischen Typisierung.

Das Zitat ist also definitiv richtig - in einer dynamischen Sprache sind mehr Dinge möglich als in einer statisch typisierten Sprache. Andererseits sind bestimmte Arten der Analyse nur in einer statisch typisierten Sprache möglich. Beispielsweise wissen Sie immer, welche Operationen für einen bestimmten Typ zulässig sind, wodurch Sie ungültige Operationen beim Kompilierungstyp erkennen können. In einer dynamischen Sprache ist eine solche Überprüfung nicht möglich, wenn Operationen zur Laufzeit hinzugefügt oder entfernt werden können.

Aus diesem Grund gibt es im Konflikt zwischen statischen und dynamischen Sprachen kein offensichtliches "Beste". Statische Sprachen geben zur Laufzeit eine gewisse Leistung ab, während sie zur Kompilierungszeit eine andere Leistung zur Verfügung stellen. Sie sind der Ansicht, dass dies die Anzahl der Fehler verringert und die Entwicklung erleichtert. Einige glauben, dass sich der Kompromiss lohnt, andere nicht.

Andere Antworten haben argumentiert, dass Turing-Äquivalenz bedeutet, dass alles, was in einer Sprache möglich ist, in allen Sprachen möglich ist. Dies folgt aber nicht. Um so etwas wie das Patchen von Affen in einer statischen Sprache zu unterstützen, müssen Sie grundsätzlich eine dynamische Untersprache in der statischen Sprache implementieren. Dies ist natürlich möglich, aber ich würde behaupten, dass Sie dann in einer eingebetteten dynamischen Sprache programmieren, da Sie auch die statische Typprüfung verlieren, die in der Hostsprache vorhanden ist.

C # unterstützt seit Version 4 dynamisch typisierte Objekte. Die Sprachdesigner sehen eindeutig einen Vorteil darin, dass beide Arten der Eingabe verfügbar sind. Es zeigt aber auch, dass Sie Ihren Kuchen nicht haben und auch nicht essen können: Wenn Sie dynamische Objekte in C # verwenden, erhalten Sie die Möglichkeit, so etwas wie Affen-Patches auszuführen, verlieren jedoch auch die statische Typüberprüfung für die Interaktion mit diesen Objekten.


+1 Ihr vorletzter Absatz, denke ich, ist das entscheidende Argument. Ich würde immer noch behaupten, dass es einen Unterschied gibt, da Sie bei statischen Typen die volle Kontrolle darüber haben, wo und was Sie mit Affen patchen können
jk.

2

Ich frage mich, ob es nützliche Entwurfsmuster oder -strategien gibt, die mit der Formulierung des Zitats "nicht als Typen arbeiten"?

Ja und nein.

Es gibt Situationen, in denen der Programmierer den Typ einer Variablen genauer kennt als ein Compiler. Der Compiler kann wissen, dass etwas ein Objekt ist, aber der Programmierer wird (aufgrund der Invarianten des Programms) wissen, dass es sich tatsächlich um einen String handelt.

Lassen Sie mich einige Beispiele dafür zeigen:

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

Ich weiß, dass someMap.get(T.class)ein zurückkehren wird Function<T, String>, weil ich so eine Karte erstellt habe. Aber Java ist nur sicher, dass ich eine Funktion habe.

Ein anderes Beispiel:

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

Ich weiß, dass data.properties.rowCount eine gültige Referenz und eine Ganzzahl ist, da ich die Daten anhand eines Schemas überprüft habe. Wenn dieses Feld fehlen würde, wäre eine Ausnahme ausgelöst worden. Ein Compiler würde jedoch nur wissen, dass er entweder eine Ausnahme auslöst oder eine Art generischen JSONValue zurückgibt.

Ein anderes Beispiel:

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

Die "II6s" definieren die Art und Weise, wie Daten drei Variablen codieren. Da ich das Format angegeben habe, weiß ich, welche Typen zurückgegeben werden. Ein Compiler würde nur wissen, dass er ein Tupel zurückgibt.

Das verbindende Thema all dieser Beispiele ist, dass der Programmierer den Typ kennt, aber ein Java-Level-Typ-System kann dies nicht widerspiegeln. Der Compiler kennt die Typen nicht und daher kann ich sie in einer statisch typisierten Sprache nicht aufrufen, wohingegen dies in einer dynamisch typisierten Sprache der Fall ist.

So lautet das ursprüngliche Zitat:

Das Wunderbare an dynamischer Eingabe ist, dass Sie damit alles ausdrücken können, was berechenbar ist. Typsysteme sind normalerweise nicht bestimmbar und beschränken Sie auf eine Teilmenge.

Bei Verwendung der dynamischen Typisierung kann ich den am besten abgeleiteten Typ verwenden, den ich kenne, und nicht nur den am besten abgeleiteten Typ, den das Typensystem meiner Sprache kennt. In allen oben genannten Fällen habe ich Code, der semantisch korrekt ist, aber von einem statischen Typisierungssystem abgelehnt wird.

Um jedoch zu Ihrer Frage zurückzukehren:

Ich frage mich, ob es nützliche Entwurfsmuster oder -strategien gibt, die mit der Formulierung des Zitats "nicht als Typen arbeiten"?

Jedes der obigen Beispiele und in der Tat jedes Beispiel für dynamische Typisierung kann beim statischen Typisieren durch Hinzufügen geeigneter Casts gültig gemacht werden. Wenn Sie einen Typ kennen, den Ihr Compiler nicht kennt, teilen Sie dies dem Compiler einfach mit, indem Sie den Wert eingeben. Auf einer bestimmten Ebene erhalten Sie also keine zusätzlichen Muster, wenn Sie dynamisches Tippen verwenden. Möglicherweise müssen Sie nur mehr Casts erstellen, um mit statisch eingegebenem Code arbeiten zu können.

Der Vorteil der dynamischen Typisierung besteht darin, dass Sie diese Muster einfach verwenden können, ohne sich darüber zu ärgern, dass es schwierig ist, Ihr Typensystem von ihrer Gültigkeit zu überzeugen. Die verfügbaren Muster werden dadurch nicht geändert, sondern möglicherweise einfacher implementiert, da Sie nicht herausfinden müssen, wie Ihr Typensystem das Muster erkennt oder Casts hinzufügt, um das Typensystem zu unterwandern.


1
Warum ist Java der Cut-Off-Punkt, an dem Sie nicht zu einem 'fortgeschritteneren / komplizierteren Typensystem' wechseln sollten?
jk.

2
@jk, was veranlasst dich zu der Annahme, dass ich das sage? Ich habe ausdrücklich vermieden, Partei zu ergreifen, ob sich ein fortschrittlicheres / komplizierteres Typensystem lohnt oder nicht.
Winston Ewert

2
Einige davon sind schreckliche Beispiele, und die anderen scheinen eher sprachliche Entscheidungen zu sein als getippt oder nicht getippt. Ich bin besonders verwirrt darüber, warum die Leute denken, dass Deserialisierung in getippten Sprachen so komplex ist. Das typisierte Ergebnis wäre, data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); und wenn man keine Klasse zum Deserialisieren hat, kann man auf diese zurückgreifen, data = parseJSON(someJson); print(data["properties.rowCount"]);die immer noch typisiert ist und die gleiche Absicht ausdrückt.
NPSF3000,

2
@ NPSF3000, wie funktioniert die parseJSON-Funktion? Es scheint entweder Reflexion oder Makros zu verwenden. Wie können Daten ["properties.rowCount"] in einer statischen Sprache eingegeben werden? Woher weiß es, dass der resultierende Wert eine Ganzzahl ist?
Winston Ewert

2
@ NPSF3000, wie wollen Sie es verwenden, wenn Sie keine Ganzzahl kennen? Wie planen Sie, Elemente in einer Liste in der JSON zu durchlaufen, ohne zu wissen, dass es sich um ein Array handelt? Der Punkt meines Beispiels war, dass ich wusste, dass data.propertieses sich um ein Objekt handelte und dass data.properties.rowCountes sich um eine Ganzzahl handelte, und dass ich einfach Code schreiben konnte, der sie verwendete. Ihr Vorschlag data["properties.rowCount"]sieht nicht dasselbe vor.
Winston Ewert

1

Hier sind einige Beispiele aus Objective-C (dynamisch typisiert), die in C ++ (statisch typisiert) nicht möglich sind:

  • Objekte mehrerer verschiedener Klassen in denselben Container stellen.
    Dies erfordert natürlich eine Laufzeitüberprüfung, um anschließend den Inhalt des Containers zu interpretieren, und die meisten Freunde der statischen Typisierung werden einwenden, dass Sie dies gar nicht erst tun sollten. Aber ich habe festgestellt, dass dies jenseits der religiösen Debatten nützlich sein kann.

  • Erweitern einer Klasse ohne Unterklassen
    In Objective-C können Sie neue Elementfunktionen für vorhandene Klassen definieren, einschließlich sprachdefinierter Funktionen wie z NSString. Zum Beispiel können Sie eine Methode hinzufügen stripPrefixIfPresent:, damit Sie sagen können [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](beachten Sie die Verwendung der NSSringLiterale @"").

  • Verwendung von objektorientierten Rückrufen.
    In statisch typisierten Sprachen wie Java und C ++ müssen Sie erhebliche Anstrengungen unternehmen, damit eine Bibliothek ein beliebiges Mitglied eines vom Benutzer bereitgestellten Objekts aufrufen kann. In Java ist die Problemumgehung das Interface / Adapter-Paar plus eine anonyme Klasse, in C ++ ist die Problemumgehung normalerweise vorlagenbasiert, was impliziert, dass Bibliothekscode für Benutzercode verfügbar gemacht werden muss. In Objective-C übergeben Sie einfach die Objektreferenz sowie den Selektor für die Methode an die Bibliothek, und die Bibliothek kann den Rückruf einfach und direkt aufrufen.


Ich kann das erste in C ++ machen, indem ich es in void * umwandle, aber das umgeht das Typensystem, also zählt es nicht. Ich kann die zweite in C # mit Erweiterungsmethoden ausführen, perfekt innerhalb eines Typsystems. Zum dritten denke ich, dass der "Selektor für die Methode" ein Lambda sein kann, also kann jede statisch typisierte Sprache mit Lambdas dasselbe tun, wenn ich es richtig verstehe. Ich kenne mich mit ObjC nicht aus.
User7610

1
@JiriDanek "Ich kann das erste Mal in C ++ machen, indem ich * ungültig mache", nicht genau, der Code, der Elemente liest, hat keine Möglichkeit, den tatsächlichen Typ selbst abzurufen. Sie benötigen Typschilder. Außerdem denke ich nicht, dass das Sagen von "Ich kann das in <Sprache>" die angemessene / produktive Art ist, dies zu betrachten, weil Sie sie immer emulieren können. Entscheidend ist der Gewinn an Ausdruckskraft gegenüber der Komplexität der Implementierung. Außerdem scheinen Sie zu glauben, dass eine Sprache, die sowohl statische als auch dynamische Funktionen (Java, C #) besitzt, ausschließlich zur "statischen" Sprachfamilie gehört.
Coredump

1
@JiriDanek void*alleine ist kein dynamisches Tippen, es ist ein Mangel an Tippen. Aber ja, dynamic_cast, virtuelle Tabellen usw. machen C ++ nicht rein statisch typisiert. Ist das schlecht?
Coredump

1
Es empfiehlt sich, das Typsystem bei Bedarf zu untergraben. Eine Notluke haben, wenn Sie sie brauchen. Oder jemand hielt es für nützlich. Sonst würden sie es nicht in die Sprache bringen.
User7610

2
@JiriDanek Ich denke, Sie haben es mit Ihrem letzten Kommentar ziemlich genau getroffen. Diese Notluken können bei vorsichtiger Verwendung äußerst nützlich sein. Nichtsdestotrotz bringt große Macht große Verantwortung mit sich, und es gibt eine Menge Leute, die sie missbrauchen ... Daher fühlt es sich viel besser an, einen Zeiger auf eine generische Basisklasse zu verwenden, von der alle anderen Klassen per Definition abgeleitet sind (wie es der Fall ist) sowohl in Objective-C als auch in Java), und sich auf RTTI zu verlassen, um die Fälle zu unterscheiden, als auf einen void*bestimmten Objekttyp zu werfen . Ersteres führt zu einem Laufzeitfehler, wenn Sie Fehler gemacht haben. Letzteres führt zu undefiniertem Verhalten.
cmaster
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.