it-swarm.com.de

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

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

Das Wunderbare an der dynamischen Eingabe ist, dass Sie damit alles ausdrücken können, was berechenbar ist. Und Typsysteme tun dies nicht - Typsysteme sind normalerweise entscheidbar 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 - sobald Sie ein Typensystem haben, wissen Sie nicht einmal, welche interessanten Programme es gibt.

--- Software Engineering Radio Episode 140: Newspeak und steckbare Typen mit Gilad Bracha

Ich frage mich, gibt es nützliche Entwurfsmuster oder Strategien, die unter Verwendung der Formulierung des Zitats "nicht als Typen funktionieren"?

30
user7610

Erstklassige Typen

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

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

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

Lassen Sie uns nun zustimmen, dass Typinferenz eine gute Sache ist und dass ich meinen Code vor dem Ausführen überprüfen lassen möchte. Ich mag es aber auch, zur Laufzeit Code produzieren und kompilieren zu können. Und ich liebe es, Dinge auch zur Kompilierungszeit vorab zu berechnen. In einer dynamisch typisierten 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 Typen kennt. Und das ist in Ordnung in dieser Sprache, weil sie nicht mehr bieten wollen.

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

Muster

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

Ich habe versucht, die vielen Möglichkeiten vorzustellen, wie Code in Common LISP ausgewertet wird, sowie Beispiele für mögliche statische Analysen (dies ist SBCL). Das Sandbox-Beispiel kompiliert eine winzige Teilmenge des LISP-Codes, der aus einer separaten Datei abgerufen wird. Um einigermaßen sicher zu sein, ändere ich die Lesetabelle, erlaube nur eine Teilmenge von Standardsymbolen und verpacke Dinge mit einer Zeitüberschreitung.

;;
;; 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 mit anderen Sprachen "unmöglich" zu tun. Der Plug-In-Ansatz in Blender, in Musiksoftware oder IDEs für statisch kompilierte Sprachen, die eine Neukompilierung im laufenden Betrieb durchführen usw. Anstelle externer Tools bevorzugen dynamische Sprachen Tools, die bereits vorhandene Informationen verwenden. Alle bekannten Anrufer von FOO? alle Unterklassen von BAR? alle Methoden, die von der Klasse ZOT spezialisiert sind? Dies sind verinnerlichte Daten. Typen sind nur ein weiterer Aspekt davon.


(siehe auch: CFFI )

4
coredump

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", ist das Zeug außerhalb dieser Teilmenge per Definition Zeug, das nicht funktioniert.

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 es nicht sind). Es ist ein ziemlich niedriger Balken zum Löschen, und es gibt mehrere Beispiele dafür, wie ein System zu Turing wird. unbeabsichtigt vervollständigen) können Sie in jeder anderen Turing-vollständigen Programmiersprache ausführen. Dies nennt man "Turing-Äquivalenz" und bedeutet nur genau das, was es sagt. Wichtig ist nicht, dass Sie das andere 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: um Ihnen eine bessere Möglichkeit zu geben, bestimmte Dinge zu tun Dinge, an denen bestehende Sprachen scheißen.

Ein dynamisches Typsystem kann beispielsweise über einem statischen Typsystem OO] emuliert werden, indem einfach alle Variablen, Parameter und Rückgabewerte als Basistyp Object und deklariert werden Verwenden Sie dann Reflection, um auf die spezifischen Daten darin zuzugreifen. Wenn Sie also feststellen, dass Sie in einer dynamischen Sprache buchstäblich nichts tun können, was Sie in einer statischen Sprache nicht tun können. Aber dies auf diese Weise zu tun, wäre ein großes Durcheinander. Na sicher.

Der Typ aus dem Zitat hat Recht, dass statische Typen Ihre Möglichkeiten einschränken, aber das ist eine wichtige Funktion, kein Problem. Die Linien auf der Straße schränken ein, 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, um auf ihrer Seite zu bleiben und nicht dorthin zu kommen, wo ich fahre!) Indem ich Regeln aufstelle, die klar definieren, was passiert Wenn Sie das Verhalten als ungültig betrachten und sicherstellen, dass es nicht auftritt, 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 "alle interessanten Programme, die Sie schreiben möchten, werden erfordern Typen". Sobald Sie eine bestimmte Komplexität überschritten haben, wird es aus zwei Gründen sehr schwierig, die Codebasis ohne ein Typsystem zu verwalten, das Sie auf dem Laufenden hält.

Erstens, weil Code ohne Typanmerkungen schwer zu lesen ist. Betrachten Sie den folgenden Python:

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

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

Es hängt alles von der Struktur von value.someProperty Ab. Aber wie sieht es aus? Gute Frage! Wie heißt sendData()? Was geht vorbei? Wie sieht diese Variable aus? Wo ist es hergekommen? Wenn es nicht lokal ist, müssen Sie den gesamten Verlauf von value nachverfolgen, um festzustellen, was gerade passiert. Vielleicht übergeben Sie etwas anderes, das auch eine someProperty -Eigenschaft hat, aber nicht das tut, was Sie denken?

Betrachten wir es nun mit Typanmerkungen, wie Sie vielleicht in der Boo-Sprache sehen, die eine sehr ähnliche Syntax verwendet, aber statisch typisiert ist:

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

Wenn etwas schief geht, ist Ihre Debugging-Aufgabe plötzlich um eine Größenordnung einfacher geworden: Schlagen Sie die Definition von MyDataType nach! Außerdem geht die Wahrscheinlichkeit eines schlechten Verhaltens, weil Sie einen inkompatiblen Typ übergeben haben, der auch eine Eigenschaft mit demselben Namen hat, plötzlich auf Null, weil das Typsystem lässt Sie diesen Fehler nicht 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 eine lange Zeit 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 Gehen Sie durch den Kopf der Person, die zum Zeitpunkt des Schreibens fast einen bestimmten Teil des Codes geschrieben hat, weil Sie nicht dort waren oder sich nicht erinnern, ob es vor langer Zeit Ihr eigener Code war. Typdeklarationen helfen Ihnen wirklich zu verstehen, was die Absicht des Codes war!

Leute wie der Typ im Zitat charakterisieren die Vorteile der statischen Typisierung 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 zusätzliche Effizienz ist sicherlich ein schöner Bonus!)

39
Mason Wheeler

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

Zum Beispiel habe ich 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 von einem analysierten XML-Objekt. irgendwie ordentlich und ordentlich, IMO. Und wenn es keinen Kopfknoten gibt, werden nur Dummy-Objekte zurückgegeben, die nur Dummy-Objekte enthalten (Schildkröten ganz nach unten). Es gibt keine echte Möglichkeit, dies beispielsweise in Java zu tun. Sie müssten im Voraus eine Klasse zusammengestellt haben, die auf einigen Kenntnissen über die Struktur des XML basiert. Abgesehen davon, ob dies eine gute Idee ist, ändert so etwas wirklich 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 bestimmten 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 tun dies in Java, weil Sie einen Python-Interpreter in Java erstellen können. Die Tatsache, dass das Lösen Ein bestimmtes Problem in einer bestimmten Sprache kann bedeuten, dass ein Dolmetscher gebaut wird, oder etwas Ähnliches wird oft übersehen, wenn über die Vollständigkeit von Turing gesprochen wird.

27
JimmyJames

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

Das Wunderbare an der dynamischen 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 es Turing complete ist, was die meisten sind. Das Typsystem selbst lässt Sie nicht alles ausdrücken. Lassen Sie uns ihm hier den Vorteil des Zweifels geben.

Und Typsysteme nicht - Typsysteme sind normalerweise entscheidbar und beschränken Sie auf eine Teilmenge.

Dies ist wahr, aber beachten Sie, dass wir jetzt fest darüber sprechen, was das Typsystem erlaubt, nicht was die Sprache , die ein Typsystem verwendet, erlaubt. Es ist zwar möglich, ein Typsystem zum Berechnen von Daten zur Kompilierungszeit zu verwenden, dies ist jedoch im Allgemeinen nicht vollständig (da das Typsystem im Allgemeinen entscheidbar ist), aber fast jede statisch typisierte Sprache ist in ihrer Laufzeit auch vollständig Turing (abhängig von typisierten 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 - sobald 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 markierte Vereinigung, bei der alles entweder eine Tüte mit Eigenschaften oder ein Wert wie ein int oder ein double ist. Das Problem ist, dass statische Sprachen dies auch können. Historisch gesehen war es etwas umständlicher, dies zu tun, aber moderne statisch typisierte Sprachen machen dies so einfach wie die Verwendung einer dynamisch typisierten Sprache. Wie kann es also einen Unterschied geben? Was kann der Programmierer als interessantes Programm sehen? Statische Sprachen haben genau die gleichen markierten 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 genug von einem dynamischen System implementieren können, um sie zu erhalten. Möglicherweise gibt es Muster, die Sie in einer dynamischen Sprache kostenlos erhalten. Dies kann es wert sein, sich mit den Nachteilen dieser Sprachen für YMMV auseinanderzusetzen.

10
jk.

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

Sie können derselben Variablen zuerst eine Ganzzahl 5 und dann eine Zeichenfolge 'five' Oder ein Cat -Objekt zuweisen. Aber Sie machen es einem Leser Ihres Codes nur schwerer, herauszufinden, was los ist und was der Zweck jeder Variablen ist.

Sie können einer Bibliothek eine neue Methode hinzufügen Ruby Klasse und auf ihre privaten Felder zugreifen. Es kann Fälle geben, in denen ein solcher Hack nützlich sein kann, aber dies wäre eine Verletzung der Kapselung. (Ich weiß nicht ' Es macht nichts aus, Methoden hinzuzufügen, die nur auf der öffentlichen Schnittstelle basieren, 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 zusätzliche Daten damit weiterzugeben. Es ist jedoch besser, nur eine neue Struktur zu erstellen oder den ursprünglichen Typ zu erweitern.

Je organisierter Ihr Code bleiben soll, desto weniger Vorteile sollten Sie im Allgemeinen daraus ziehen, Typdefinitionen dynamisch ändern oder derselben Variablen Werte unterschiedlichen Typs zuweisen zu können. Aber dann unterscheidet sich Ihr Code nicht von dem, was Sie in einer statisch typisierten Sprache erreichen könnten.

Was dynamische Sprachen gut können, ist syntaktischer Zucker. Wenn Sie beispielsweise ein deserialisiertes JSON-Objekt lesen, können Sie einen verschachtelten Wert einfach als obj.data.article[0].content Verweisen - viel übersichtlicher als obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Insbesondere Ruby-Entwickler könnten ausführlich über Magie sprechen, die durch die Implementierung von method_missing Erreicht werden kann. Mit dieser Methode können Sie versuchte Aufrufe nicht deklarierter Methoden verarbeiten. ActiveRecord ORM verwendet es beispielsweise, damit Sie User.find_by_email('[email protected]') aufrufen können, ohne jemals die Methode find_by_email Zu deklarieren. Natürlich ist es nichts, was in einer statisch typisierten Sprache nicht als UserRepository.FindBy("email", "[email protected]") erreicht werden könnte, aber Sie können es nicht leugnen, dass es ordentlich ist.

4
kamilk

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

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

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

Auf diese Weise erstellt Proxy(someObject) ein neues Objekt, das sich wie someObject verhält. Natürlich möchten Sie auch irgendwie zusätzliche Funktionen hinzufügen, aber dies ist eine nützliche Basis, um damit zu beginnen. In einer vollständigen statischen Sprache müssten Sie entweder eine Proxy-Klasse pro Typ schreiben, den Sie als Proxy verwenden möchten, oder die dynamische Codegenerierung verwenden (die zugegebenermaßen in der Standardbibliothek vieler statischer Sprachen enthalten ist, vor allem, weil ihre Designer dies wissen die Probleme, die dies nicht können).

Ein weiterer Anwendungsfall dynamischer Sprachen ist das sogenannte "Monkey Patching". In vielerlei Hinsicht ist dies eher ein Anti-Muster als ein Muster, aber es kann auf nützliche Weise verwendet werden, wenn es sorgfältig durchgeführt wird. Und obwohl es keinen theoretischen Grund gibt, warum das Patchen von Affen nicht in einer statischen Sprache implementiert werden konnte, habe ich noch nie eine gesehen, die es tatsächlich hat.

4
Jules

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

Monkey Patching ist eine Technik, bei der Objekten oder Klassen zur Laufzeit Eigenschaften oder Methoden hinzugefügt 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 das Patchen von Affen unterstützt, ist sie per Definition eine dynamische Sprache.

Es kann nachgewiesen werden, dass eine Sprache, die das Patchen von Affen unterstützt (oder ähnliche Techniken zum Ändern von Typen zur Laufzeit), nicht statisch typgeprüft werden kann. Es handelt sich also nicht nur um eine Einschränkung in 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 von 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 illegale Operationen beim Kompilierungstyp erkennen können. In einer dynamischen Sprache ist eine solche Überprüfung nicht möglich, wenn zur Laufzeit Vorgänge hinzugefügt oder entfernt werden können.

Aus diesem Grund gibt es im Konflikt zwischen statischen und dynamischen Sprachen kein offensichtliches "Bestes". Statische Sprachen geben zur Laufzeit eine bestimmte Leistung im Austausch gegen eine andere Art von Leistung zur Kompilierungszeit ab, was ihrer Meinung nach 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 argumentieren, dass Sie dann in einer eingebetteten dynamischen Sprache programmieren, da Sie auch die statische Typprüfung verlieren, die in der Host-Sprache vorhanden ist.

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

3
JacquesB

Ich frage mich, gibt es nützliche Entwurfsmuster oder -strategien, die unter Verwendung der Formulierung des Zitats "nicht als Typen funktionieren"?

Ja und nein.

Es gibt Situationen, in denen der Programmierer den Typ einer Variablen genauer kennt als ein Compiler. Der Compiler weiß möglicherweise, dass etwas ein Objekt ist, aber der Programmierer weiß (aufgrund der Invarianten des Programms), 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) einen Function<T, String> Zurückgibt, weil ich someMap 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 entweder eine Ausnahme ausgelöst wird oder eine Art generischer JSONValue zurückgegeben wird.

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 somit Eine statisch typisierte Sprache lässt mich sie nicht nennen, während eine dynamisch typisierte Sprache dies zulässt.

Das ist das ursprüngliche Zitat, auf das es kommt:

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

Wenn ich dynamische Typisierung verwende, kann ich den am meisten abgeleiteten Typ verwenden, den ich kenne, und nicht nur den am meisten 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 auf Ihre Frage zurückzukommen:

Ich frage mich, gibt es nützliche Entwurfsmuster oder -strategien, die unter Verwendung der Formulierung des Zitats "nicht als Typen funktionieren"?

Jedes der obigen Beispiele und in der Tat jedes Beispiel für dynamische Typisierung kann bei statischer Typisierung 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 umwandeln. Auf einer bestimmten Ebene erhalten Sie durch dynamische Eingabe keine zusätzlichen Muster. Möglicherweise müssen Sie mehr umwandeln, um statisch eingegebenen Code zu verwenden.

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. Es ändert die verfügbaren Muster nicht, es macht sie möglicherweise einfacher zu implementieren, da Sie nicht herausfinden müssen, wie Ihr Typsystem das Muster erkennen oder Casts hinzufügen kann, um das Typsystem zu untergraben.

2
Winston Ewert

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

  • Objekte mehrerer unterschiedlicher Klassen in denselben Container einfügen.
    Dies erfordert natürlich eine Überprüfung des Laufzeit-Typs, um anschließend den Inhalt des Containers zu interpretieren, und die meisten Freunde der statischen Typisierung werden einwenden, dass Sie dies überhaupt nicht tun sollten. Aber ich habe festgestellt, dass dies über die religiösen Debatten hinaus nützlich sein kann.

  • Erweitern einer Klasse ohne Unterklasse.
    In Objective-C können Sie neue Elementfunktionen für vorhandene Klassen definieren, einschließlich sprachdefinierter Funktionen wie NSString. Sie können beispielsweise eine Methode stripPrefixIfPresent: Hinzufügen, sodass Sie [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"] Sagen können (beachten Sie die Verwendung der Literale NSSring@"").

  • Verwendung objektorientierter Rückrufe.
    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 Problemumgehung ist das Schnittstellen-/Adapterpaar plus eine anonyme Klasse. In C++ basiert die Problemumgehung normalerweise auf Vorlagen. Dies bedeutet, dass Bibliothekscode dem Benutzercode ausgesetzt werden muss. In Objective-C übergeben Sie einfach die Objektreferenz plus den Selektor für die Methode zur Bibliothek, und die Bibliothek kann den Rückruf einfach und direkt aufrufen.