Czy są jakieś wzorce projektowe, które są możliwe tylko w dynamicznie pisanych językach, takich jak Python?

30

Czytałem powiązane pytanie Czy istnieją jakieś wzorce projektowe, które są niepotrzebne w dynamicznych językach, takich jak Python? i pamiętam ten cytat na Wikiquote.org

Wspaniałą rzeczą w dynamicznym pisaniu jest to, że pozwala wyrazić wszystko, co jest obliczalne. A systemy typu nie-systemy są zazwyczaj rozstrzygalne i ograniczają cię do podzbioru. Ludzie, którzy preferują układy statyczne, mówią: „jest w porządku, jest wystarczająco dobry; wszystkie ciekawe programy, które chcesz napisać, będą działały jak typy ”. Ale to niedorzeczne - kiedy już masz system typów, nie wiesz nawet, jakie są interesujące programy.

--- Software Engineering Radio Episode 140: Newspeak and Pluggable Types with Gilad Bracha

Zastanawiam się, czy istnieją użyteczne wzorce projektowe lub strategie, które wykorzystując sformułowanie cytatu „nie działają jak typy”?

użytkownik7610
źródło
3
Odkryłem, że podwójne wysyłanie i wzorzec gościa są bardzo trudne do osiągnięcia w językach o typie statycznym, ale łatwe do osiągnięcia w językach dynamicznych. Zobacz tę odpowiedź (i pytanie) na przykład: programmers.stackexchange.com/a/288153/122079
user3002473
7
Oczywiście. Na przykład dowolny wzorzec obejmujący tworzenie nowych klas w środowisku wykonawczym. (jest to również możliwe w Javie, ale nie w C ++; istnieje przesuwna skala dynamiki).
user253751,
1
Wiele zależy od tego, jak wyrafinowany jest Twój system typów :-) Języki funkcjonalne zwykle dobrze sobie z tym radzą.
Bergi,
1
Wydaje się, że wszyscy mówią takie systemy jak Java i C # zamiast Haskell lub OCaml. Język z potężnym systemem typów może być tak zwięzły jak język dynamiczny, ale zachowywać bezpieczeństwo tekstu.
Andrew mówi Przywróć Monikę
@immibis To nieprawda. Systemy typu statycznego mogą absolutnie tworzyć nowe, „dynamiczne” klasy w czasie wykonywania. Zobacz rozdział 33 praktycznych podstaw programowania języków.
ogrodnik

Odpowiedzi:

4

Typy pierwszej klasy

Pisanie dynamiczne oznacza, że ​​masz typy pierwszej klasy: możesz sprawdzać, tworzyć i przechowywać typy w czasie wykonywania, w tym typy własne języka. Oznacza to również, że wartości są wpisywane, a nie zmienne .

Język o typie statycznym może generować kod, który również opiera się na typach dynamicznych, takich jak wysyłanie metod, klasy typów itp., Ale w sposób ogólnie niewidoczny dla środowiska wykonawczego. W najlepszym razie dają ci możliwość przeprowadzenia introspekcji. Alternatywnie możesz symulować typy jako wartości, ale wtedy masz dynamiczny system typów ad-hoc.

Jednak systemy typów dynamicznych rzadko mają tylko typy pierwszej klasy. Możesz mieć pierwszorzędne symbole, pierwszorzędne pakiety, pierwszorzędne ... wszystko. Jest to w przeciwieństwie do ścisłego oddzielenia języka kompilatora od języka wykonawczego w językach o typie statycznym. To, co potrafi kompilator lub interpreter, może również wykonać środowisko wykonawcze.

Teraz zgódźmy się, że wnioskowanie o typie jest dobrą rzeczą i że chcę sprawdzić mój kod przed jego uruchomieniem. Lubię też tworzyć i kompilować kod w czasie wykonywania. Uwielbiam też wstępnie obliczać rzeczy w czasie kompilacji. W języku pisanym dynamicznie odbywa się to w tym samym języku. W OCaml masz system typu moduł / funktor, który różni się od głównego systemu typów, który różni się od języka preprocesora. W C ++ masz język szablonów, który nie ma nic wspólnego z głównym językiem, który na ogół nie zna typów podczas wykonywania. I to jest w porządku w tym języku, ponieważ nie chcą więcej.

Ostatecznie nie zmienia to tak naprawdę rodzaju oprogramowania, które można opracować, ale ekspresja zmienia sposób , w jaki je tworzysz i czy jest to trudne, czy nie.

Wzory

Wzorce oparte na typach dynamicznych to wzorce obejmujące środowiska dynamiczne: klasy otwarte, dyspozytorskie, bazy danych obiektów w pamięci, serializacja itp. Proste rzeczy, takie jak ogólne pojemniki działają, ponieważ wektor nie zapomina w czasie wykonywania o typie obiektów, które posiada (nie ma potrzeby stosowania typów parametrycznych).

Próbowałem przedstawić wiele sposobów oceny kodu we Common Lisp, a także przykłady możliwych analiz statycznych (jest to SBCL). Przykład piaskownicy kompiluje niewielki podzbiór kodu Lisp pobrany z osobnego pliku. Aby być względnie bezpiecznym, zmieniam czytelność, zezwalam tylko na podzbiór standardowych symboli i zawijam czas.

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

Nic powyżej nie jest „niemożliwe” w przypadku innych języków. Podejście plug-in w Blenderze, w oprogramowaniu muzycznym lub IDE dla języków skompilowanych statycznie, które wykonują rekompilację „w locie” itp. Zamiast narzędzi zewnętrznych dynamiczne języki preferują narzędzia, które wykorzystują informacje, które już tam są. Wszyscy znani rozmówcy FOO? wszystkie podklasy BAR? wszystkie metody, które są wyspecjalizowane przez klasę ZOT? to są zinternalizowane dane. Typy to tylko kolejny aspekt tego.


(patrz także: CFFI )

rdzeń rdzeniowy
źródło
39

Krótka odpowiedź: nie, ponieważ równoważność Turinga.

Długa odpowiedź: ten facet jest trollem. Chociaż prawdą jest, że systemy typów „ograniczają cię do podzbioru”, rzeczy poza tym podzbiorem są z definicji rzeczami, które nie działają.

Wszystko, co możesz zrobić w dowolnym języku programowania kompletnym Turinga (który jest językiem zaprojektowanym do programowania ogólnego, plus wiele innych, których nie ma; to dość niski pasek do wyczyszczenia i istnieje kilka przykładów systemu, który staje się Turing- niezamierzone wypełnienie) możesz zrobić w dowolnym innym języku programowania Turing-complete. Nazywa się to „równoważnością Turinga” i oznacza tylko dokładnie to, co mówi. Co ważne, nie oznacza to, że możesz zrobić drugą rzecz równie łatwo w innym języku - niektórzy twierdzą, że o to właśnie chodzi w tworzeniu nowego języka programowania: aby dać ci lepszy sposób na wykonanie pewnych czynności rzeczy, które ssą istniejące języki.

Na przykład system typu dynamicznego można emulować na podstawie statycznego systemu typu OO, po prostu deklarując wszystkie zmienne, parametry i zwracane wartości jako Objecttyp podstawowy, a następnie używając refleksji, aby uzyskać dostęp do określonych danych wewnątrz, więc kiedy sobie to uświadomisz widać, że dosłownie nic nie można zrobić w języku dynamicznym, czego nie można zrobić w języku statycznym. Ale zrobienie tego w ten sposób byłoby oczywiście ogromnym bałaganem.

Facet z cytatu ma rację, że typy statyczne ograniczają to, co możesz zrobić, ale to ważna funkcja, a nie problem. Linie na drodze ograniczają to, co możesz zrobić w samochodzie, ale czy uważasz je za restrykcyjne lub pomocne? (Wiem, że nie chciałbym jeździć po ruchliwej, złożonej drodze, na której nic nie mówi samochodom jadącym w przeciwnym kierunku, aby trzymały się z boku i nie podjeżdżały tam, gdzie jadę!) Ustalając zasady, które jasno określają, co jest uważane za nieprawidłowe zachowanie i zapewniające, że tak się nie stanie, znacznie zmniejszasz ryzyko wystąpienia nieprzyjemnego wypadku.

Poza tym źle opisuje drugą stronę. Nie chodzi o to, że „wszystkie ciekawe programy, które chcesz pisać, będą działały jak typy”, ale „wszystkie interesujące programy, które chcesz pisać, będą wymagać typów”. Po przekroczeniu pewnego poziomu złożoności bardzo trudno jest utrzymać bazę kodu bez systemu typów, który utrzymywałby Cię w linii z dwóch powodów.

Po pierwsze, ponieważ kod bez adnotacji typu jest trudny do odczytania. Rozważ następujący Python:

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

Jak wyglądają dane, które odbiera system na drugim końcu połączenia? A jeśli odbiera coś, co wygląda zupełnie nie tak, jak możesz dowiedzieć się, co się dzieje?

Wszystko zależy od struktury value.someProperty. Ale jak to wygląda? Dobre pytanie! Co znajduje się powołanie sendData()? Co to przechodzi? Jak wygląda ta zmienna? Skąd to się wzieło? Jeśli nie jest lokalny, musisz prześledzić całą historię, valueaby śledzić, co się dzieje. Może przekazujesz coś innego, co również ma somePropertywłaściwość, ale nie robi tego, co myślisz, że ma?

Teraz spójrzmy na to z adnotacjami typu, jak możesz zobaczyć w języku Boo, który używa bardzo podobnej składni, ale jest statycznie wpisany:

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

Jeśli coś pójdzie nie tak, nagle zadanie debugowania stało się o rząd wielkości łatwiejsze: spójrz na definicję MyDataType! Dodatkowo szansa na złe zachowanie, ponieważ minąłeś jakiś niezgodny typ, który również ma właściwość o tej samej nazwie, nagle spada do zera, ponieważ system typów nie pozwoli ci popełnić tego błędu.

Drugi powód opiera się na pierwszym: w dużym i złożonym projekcie najprawdopodobniej masz wielu współpracowników. (A jeśli nie, to budujesz go sam przez długi czas, co jest w zasadzie to samo. Spróbuj przeczytać kod napisany 3 lata temu, jeśli mi nie wierzysz!) Oznacza to, że nie wiesz, co było przechodząc przez głowę osoby, która napisała prawie każdą część kodu w momencie, gdy go napisali, ponieważ nie było cię tam lub nie pamiętasz, czy to był twój własny kod dawno temu. Posiadanie deklaracji typu naprawdę pomaga zrozumieć, jaki był zamiar kodu!

Ludzie tacy jak facet w cytacie często mylnie opisują zalety pisania statycznego jako „pomoc kompilatorowi” lub „wszystko o wydajności” w świecie, w którym prawie nieograniczone zasoby sprzętowe sprawiają, że z każdym rokiem staje się to coraz mniej istotne. Ale jak wykazałem, chociaż te korzyści z pewnością istnieją, podstawową korzyścią są czynniki ludzkie, w szczególności czytelność kodu i łatwość konserwacji. (Jednak dodatkowa wydajność to z pewnością niezły bonus!)

Mason Wheeler
źródło
24
„Ten facet jest trollem”. - Nie jestem pewien, czy atak ad hominem pomoże twojej dobrze skomentowanej sprawie. I chociaż doskonale zdaję sobie sprawę, że argument władzy jest równie złym błędem, co ad hominem, nadal chciałbym zauważyć, że Gilad Bracha prawdopodobnie zaprojektował więcej języków i (najbardziej odpowiednie w tej dyskusji) więcej typów statycznych niż większość. Tylko mały fragment: jest on jedynym projektantem nowomowy, współ-projektant dart, współautor języka Java Specification i Java Virtual Machine Specification, pracował nad projektem Java i JVM, zaprojektowany ...
Jörg W Mittag
10
Strongtalk (system typu static dla Smalltalk), system typu Dart, system typu Newspeak, jego praca doktorska na temat modułowości jest podstawą prawie każdego nowoczesnego systemu modułowego (np. Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke , Seph), jego praca (y) na temat mixin zrewolucjonizowały sposób, w jaki o nich myślimy. Teraz, że nie nie znaczy, że ma rację, ale nie sądzę, że po zaprojektowany wielu systemów typu statycznego czyni go nieco więcej niż „trolla”.
Jörg W Mittag,
17
„Chociaż prawdą jest, że systemy typów” ograniczają cię do podzbioru, „rzeczy poza tym podzbiorem to z definicji rzeczy, które nie działają”. - To jest źle. Wiemy z nierozstrzygalności problemu zatrzymania, twierdzenia Rice'a i niezliczonych innych wyników nierozstrzygalności i niezliczalności wynika, że ​​statyczny moduł sprawdzania typu nie może zdecydować dla wszystkich programów, czy są one bezpieczne dla typu czy dla typu. Nie może zaakceptować tych programów (niektóre z nich są niebezpieczne), więc jedynym rozsądnym wyborem jest ich odrzucenie (jednak niektóre z nich są bezpieczne). Ewentualnie język należy zaprojektować w…
Jörg W Mittag,
9
… W taki sposób, aby uniemożliwić programistom pisanie tych niezdecydowanych programów, ale znowu niektóre z nich są w rzeczywistości bezpieczne dla typu. Tak więc, bez względu na to, jak je pokroisz: programista nie może pisać programów bezpiecznych dla typu. A ponieważ nie są w rzeczywistości nieskończenie wiele z nich (zazwyczaj), możemy być niemal pewni, że przynajmniej niektóre z nich nie są tylko rzeczy, które robi pracę, ale także użyteczne.
Jörg W Mittag,
8
@MasonWheeler: problem zatrzymania pojawia się cały czas, dokładnie w kontekście sprawdzania typu statycznego. O ile języki nie są starannie zaprojektowane, aby uniemożliwić programistom pisanie określonych rodzajów programów, statyczne sprawdzanie typów szybko staje się równoważne z rozwiązaniem problemu zatrzymania. Albo skończysz z programami, których nie wolno ci pisać, ponieważ mogą mylić moduł sprawdzania typu, albo skończysz z programami, które możesz pisać, ale ich sprawdzenie zajmie nieskończoną ilość czasu.
Jörg W Mittag,
27

Zamierzam przejść boczną część „wzorca”, ponieważ myślę, że przekształca się ona w definicję tego, co jest wzorzec lub nie, i od dawna straciłem zainteresowanie tą debatą. Powiem tylko, że są rzeczy, które możesz zrobić w niektórych językach, a których nie możesz zrobić w innych. Pozwólcie, że wyrażę się jasno, nie mówię, że istnieją problemy, które można rozwiązać w jednym języku, których nie można rozwiązać w innym języku. Mason wskazał już na kompletność Turinga.

Na przykład napisałem klasę w Pythonie, która pobiera element XML DOM i zamienia go w obiekt pierwszej klasy. Oznacza to, że możesz napisać kod:

doc.header.status.text()

i masz zawartość tej ścieżki w parsowanym obiekcie XML. trochę schludnie i schludnie, IMO. A jeśli nie ma węzła głównego, to po prostu zwraca fikcyjne obiekty, które zawierają jedynie fikcyjne obiekty (żółwie do samego końca). Nie ma prawdziwego sposobu na zrobienie tego w, powiedzmy, Javie. Trzeba było wcześniej skompilować klasę opartą na pewnej wiedzy o strukturze XML. Odkładając na bok, czy to dobry pomysł, tego rodzaju rzeczy naprawdę zmieniają sposób rozwiązywania problemów w dynamicznym języku. Nie twierdzę jednak, że zmienia się w sposób, który zawsze musi być zawsze lepszy. Podejście dynamiczne wiąże się z pewnymi kosztami, a odpowiedź Masona daje dobry przegląd. To, czy są dobrym wyborem, zależy od wielu czynników.

Na marginesie, możesz to zrobić w Javie, ponieważ możesz zbudować interpreter Pythona w Javie . Fakt, że rozwiązanie konkretnego problemu w danym języku może oznaczać budowę tłumacza lub coś podobnego, jest często pomijany, gdy ludzie mówią o kompletności Turinga.

JimmyJames
źródło
4
Nie możesz tego zrobić w Javie, ponieważ Java jest źle zaprojektowana. Przy użyciu C # nie byłoby to takie trudne IDynamicMetaObjectProvider, aw Boo jest to bardzo proste. ( Oto implementacja w mniej niż 100 wierszach, zawarta jako część standardowego drzewa źródeł w GitHub, ponieważ to takie proste!)
Mason Wheeler
6
@MasonWheeler "IDynamicMetaObjectProvider"? Czy to jest związane ze dynamicsłowem kluczowym C # ? ... który skutecznie po prostu dynamicznie pisze na C #? Nie jestem pewien, czy twój argument jest ważny, jeśli mam rację.
jpmc26,
9
@MasonWheeler Wchodzisz w semantykę. Bez wdawania się w debatę na temat drobiazgów (tutaj nie rozwijamy matematycznego formalizmu dotyczącego SE), dynamiczne pisanie jest praktyką polegającą na rezygnacji z kompilacji decyzji dotyczących typów, zwłaszcza weryfikacji, że każdy typ ma poszczególnych członków, do których program ma dostęp. To jest cel, który dynamicosiąga się w języku C #. „Refleksje i wyszukiwania słownikowe” mają miejsce w czasie wykonywania, a nie w czasie kompilacji. Naprawdę nie jestem pewien, jak można stwierdzić, że nie dodaje dynamicznego pisania do języka. Chodzi mi o to, że ostatni akapit Jimmy'ego to obejmuje.
jpmc26,
44
Pomimo tego, że nie jestem wielkim fanem Javy, ośmielam się również powiedzieć, że nazywanie Javy „źle zaprojektowanym” specjalnie dlatego, że nie dodawało dynamicznego pisania, jest ... nadgorliwe.
jpmc26,
5
Czym różni się od słownika nieco nieco wygodniejsza składnia?
Theodoros Chatzigiannakis,
10

Cytat jest poprawny, ale także bardzo nieszczery. Podzielmy to, aby zobaczyć, dlaczego:

Wspaniałą rzeczą w dynamicznym pisaniu jest to, że pozwala wyrazić wszystko, co jest obliczalne.

Cóż, niezupełnie. Język z dynamicznego typowania pozwala wyrazić coś tak długo, jak to Turinga kompletne , których większość jest. Sam system typów nie pozwala wyrazić wszystkiego. Dajmy mu jednak wątpliwości.

A systemy typu nie-systemy są zazwyczaj rozstrzygalne i ograniczają cię do podzbioru.

To prawda, ale zauważmy, że teraz zdecydowanie mówimy o tym, na co pozwala system typów , a nie o to, na jaki język, który używa systemu typów. Chociaż możliwe jest użycie systemu typów do obliczania rzeczy w czasie kompilacji, nie jest to generalnie Turing zakończony (ponieważ system typów jest generalnie rozstrzygalny), ale prawie każdy statycznie typowany język jest również Turing ukończony w czasie wykonywania (języki zależne nie, ale nie sądzę, że tutaj o nich rozmawiamy).

Ludzie, którzy preferują układy statyczne, mówią: „jest w porządku, jest wystarczająco dobry; wszystkie ciekawe programy, które chcesz napisać, będą działały jak typy ”. Ale to niedorzeczne - kiedy już masz system typów, nie wiesz nawet, jakie są interesujące programy.

Problem polega na tym, że dynamicznie typy języków mają typ statyczny. Czasami wszystko jest ciągiem znaków, a częściej istnieje pewien znakowany związek, w którym każda rzecz jest albo zbiorem właściwości, albo wartością taką jak int lub double. Problem polega na tym, że języki statyczne również mogą to robić, historycznie było to trochę bardziej skomplikowane, ale współczesne języki o typie statycznym sprawiają, że jest to tak samo łatwe, jak w przypadku używania języka typów dynamicznych, więc jak może być różnica w co programista może uznać za interesujący program? Języki statyczne mają dokładnie takie same oznaczone związki, jak i inne typy.

Aby odpowiedzieć na pytanie w tytule: Nie, nie ma wzorców projektowych, których nie można zaimplementować w języku o typie statycznym, ponieważ zawsze można zaimplementować wystarczającą liczbę dynamicznych systemów, aby je uzyskać. Mogą istnieć wzorce, które otrzymujesz za darmo w dynamicznym języku; dla YMMV może to być lub nie być warte znoszenia wad tych języków .

jk.
źródło
2
Nie jestem do końca pewien, czy odpowiedziałeś tak, czy nie. Brzmi dla mnie bardziej jak „nie”.
user7610,
1
@TheodorosChatzigiannakis Tak, jak inaczej można by wdrożyć języki dynamiczne? Po pierwsze, zdasz egzamin architekta-astronauty, jeśli kiedykolwiek będziesz chciał wdrożyć system klasy dynamicznej lub coś jeszcze trochę w to zaangażowanego. Po drugie, prawdopodobnie nie masz zasobów, aby uczynić go debugowalnym, w pełni introspektywnym, wydajnym („wystarczy użyć słownika” to sposób implementacji wolnych języków). Po trzecie, niektóre funkcje dynamiczne najlepiej stosować, gdy są zintegrowane z całym językiem, a nie tylko jako biblioteka: na przykład pomyśl wyrzucanie elementów bezużytecznych ( biblioteki GC bibliotekami, ale nie są powszechnie używane).
coredump
1
@Theodoros Zgodnie z artykułem, który już tu kiedyś zamieściłem, wszystkie z wyjątkiem 2,5% struktur (w modułach Python, na które patrzyli badacze) można łatwo wyrazić za pomocą języka pisanego na maszynie. Może 2,5% sprawia, że ​​opłacenie dynamicznego pisania jest tego warte. Właśnie o to chodziło w moim pytaniu. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610
3
@JiriDanek O ile mi wiadomo, nic nie stoi na przeszkodzie, aby język o typie statycznym posiadał polimorficzne miejsca wywoływania i utrzymywał pisanie w trybie statycznym. Zobacz Statyczne sprawdzanie typu wielu metod . Może nie rozumiem twojego linku.
Theodoros Chatzigiannakis
1
„Język z dynamicznym pisaniem pozwala wyrazić wszystko, pod warunkiem, że Turing jest kompletny, a większość z nich jest.” Chociaż jest to oczywiście prawdziwe stwierdzenie, tak naprawdę nie zachowuje się w „prawdziwym świecie”, ponieważ ilość tekstu pisać może być bardzo duże.
Daniel Jour
4

Z pewnością są rzeczy, które można wykonywać tylko w dynamicznie pisanych językach. Ale niekoniecznie byłyby dobrym projektem.

Możesz przypisać najpierw liczbę całkowitą 5, a następnie ciąg znaków 'five'lub Catobiekt do tej samej zmiennej. Ale utrudniasz tylko czytelnikowi twojego kodu zrozumienie, co się dzieje, jaki jest cel każdej zmiennej.

Możesz dodać nową metodę do biblioteki Ruby i uzyskać dostęp do jej prywatnych pól. Mogą istnieć przypadki, w których taki hack może być użyteczny, ale byłoby to naruszenie enkapsulacji. (Nie mam nic przeciwko dodawaniu metod opartych tylko na interfejsie publicznym, ale to nic, czego nie mogą zrobić statycznie wpisane metody rozszerzenia C #).

Możesz dodać nowe pole do obiektu czyjejś klasy, aby przekazać z nim dodatkowe dane. Ale lepszym rozwiązaniem jest po prostu utworzenie nowej struktury lub rozszerzenie oryginalnego typu.

Ogólnie rzecz biorąc, im bardziej uporządkowany kod ma pozostać, tym mniejszą zaletą powinna być możliwość dynamicznej zmiany definicji typów lub przypisywania wartości różnych typów do tej samej zmiennej. Ale wtedy twój kod nie różni się od tego, co można osiągnąć w języku o typie statycznym.

Dynamiczne języki są dobre w cukrze syntaktycznym. Na przykład podczas odczytywania zdekrializowanego obiektu JSON możesz odwoływać się do zagnieżdżonej wartości po prostu jako obj.data.article[0].content- znacznie starszego niż powiedzieć obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Szczególnie programiści Ruby mogą długo mówić o magii, którą można osiągnąć poprzez wdrożenie method_missing , która jest metodą pozwalającą na obsługę prób wywołania niezadeklarowanych metod. Na przykład ActiveRecord ORM używa go, aby można było wykonać połączenie User.find_by_email('[email protected]')bez deklarowania find_by_emailmetody. Oczywiście nie jest to nic, czego nie można by było osiągnąć UserRepository.FindBy("email", "[email protected]")w statycznie pisanym języku, ale nie można zaprzeczyć, że jest porządny.

kamilk
źródło
4
Z pewnością są rzeczy, które można wykonywać tylko w językach o typie statycznym. Ale niekoniecznie byłyby dobrym projektem.
coredump
2
Pytanie o cukrze syntaktycznym ma bardzo mało wspólnego z dynamicznym pisaniem i wszystkim, cóż, składnią.
leftaroundabout
@leftaroundabout Wzory mają wszystko wspólnego ze składnią. Systemy typów również mają z tym wiele wspólnego.
user253751,
4

Wzorzec dynamicznego proxy jest skrótem do implementacji obiektów proxy bez potrzeby posiadania jednej klasy dla każdego typu, który wymaga proxy.

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

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

Używając tego Proxy(someObject) tworzy nowy obiekt, który zachowuje się tak samo jak someObject. Oczywiście będziesz także chciał w jakiś sposób dodać dodatkową funkcjonalność, ale jest to przydatna baza na początek. W pełnym języku statycznym musisz albo napisać jedną klasę proxy dla typu, który chcesz proxy, lub użyć dynamicznego generowania kodu (co oczywiście znajduje się w standardowej bibliotece wielu języków statycznych, głównie dlatego, że ich projektanci są świadomi problemy, które nie są w stanie tego zrobić).

Innym przykładem użycia języków dynamicznych jest tak zwane „łatanie małp”. Pod wieloma względami jest to raczej anty-wzór niż wzór, ale można go używać w użyteczny sposób, jeśli zostanie starannie wykonany. I choć nie ma teoretycznego powodu, dla którego łatanie małp nie mogło zostać zaimplementowane w języku statycznym, nigdy nie widziałem takiego, który faktycznie go ma.

Jules
źródło
Myślę, że mógłbym to naśladować w Go. Istnieje zestaw metod, które muszą mieć wszystkie obiekty proxy (w przeciwnym razie kaczka może nie kwakać i wszystkie rozpadną się). Za pomocą tych metod mogę utworzyć interfejs Go. Będę musiał przemyśleć więcej, ale myślę, że to, co mam na myśli, zadziała.
user7610
Możesz coś podobnego w dowolnym języku .NET za pomocą RealProxy i generycznych.
LittleEwok
@LittleEwok - RealProxy używa generowania kodu środowiska wykonawczego - jak mówię, wiele współczesnych języków statycznych ma takie obejście, ale w języku dynamicznym jest to łatwiejsze.
Jules
Metody rozszerzenia C # są trochę jak bezpieczne łatanie małp. Nie możesz zmienić istniejących metod, ale możesz dodać nowe.
Andrew mówi Przywróć Monikę
3

tak , istnieje wiele wzorców i technik, które są możliwe tylko w dynamicznie pisanym języku.

Patchowanie małp jest techniką polegającą na dodawaniu właściwości lub metod do obiektów lub klas w czasie wykonywania. Ta technika nie jest możliwa w języku o typie statycznym, ponieważ oznacza to, że typów i operacji nie można zweryfikować w czasie kompilacji. Innymi słowy, jeśli język obsługuje łatanie małp, jest to z definicji to język dynamiczny.

Można udowodnić, że jeśli język obsługuje łatanie małp (lub podobne techniki modyfikowania typów w czasie wykonywania), nie można go sprawdzić statycznie. Nie jest to więc ograniczenie w obecnie istniejących językach, ale podstawowe ograniczenie pisania statycznego.

Zatem cytat jest zdecydowanie poprawny - więcej rzeczy jest możliwe w języku dynamicznym niż w języku typowanym statycznie. Z drugiej strony, niektóre rodzaje analiz są możliwe tylko w języku o typie statycznym. Na przykład zawsze wiesz, które operacje są dozwolone na danym typie, co pozwala wykryć nielegalne operacje na typie kompilacji. Taka weryfikacja nie jest możliwa w języku dynamicznym, gdy operacje można dodawać lub usuwać w czasie wykonywania.

Dlatego nie ma oczywistego „najlepszego” konfliktu między językami statycznymi i dynamicznymi. Języki statyczne rezygnują z pewnej mocy w czasie wykonywania w zamian za inną moc w czasie kompilacji, która ich zdaniem zmniejsza liczbę błędów i ułatwia rozwój. Niektórzy uważają, że kompromis jest tego wart, inni nie.

Inne odpowiedzi dowodzą, że równoważność Turinga oznacza, że ​​wszystko, co możliwe w jednym języku, jest możliwe we wszystkich językach. Ale to nie następuje. Aby wesprzeć coś takiego jak łatanie małp w języku statycznym, musisz w zasadzie zaimplementować dynamiczny podjęzyk w języku statycznym. Jest to oczywiście możliwe, ale argumentowałbym, że programujesz wtedy we wbudowanym dynamicznym języku, ponieważ tracisz również statyczne sprawdzanie typów, które istnieją w języku hosta.

C # od wersji 4 obsługuje dynamicznie wpisywane obiekty. Najwyraźniej projektanci języków widzą korzyści z posiadania obu rodzajów pisania. Ale pokazuje również, że nie możesz mieć ciasta i jeść również: kiedy używasz dynamicznych obiektów w C #, zyskujesz zdolność robienia czegoś takiego, jak łatanie małp, ale tracisz również weryfikację typu statycznego dla interakcji z tymi obiektami.

JacquesB
źródło
+1 od drugiego do ostatniego akapitu Myślę, że jest to kluczowy argument. Nadal twierdzę, że istnieje różnica, ponieważ w przypadku typów statycznych masz pełną kontrolę nad tym, gdzie i co możesz
małpować
2

Zastanawiam się, czy istnieją użyteczne wzorce projektowe lub strategie, które wykorzystując sformułowanie cytatu „nie działają jak typy”?

Tak i nie.

Są sytuacje, w których programista zna typ zmiennej z większą precyzją niż kompilator. Kompilator może wiedzieć, że coś jest Obiektem, ale programista będzie wiedział (ze względu na niezmienniki programu), że w rzeczywistości jest to Łańcuch.

Pokażę kilka przykładów:

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

Wiem, że someMap.get(T.class)to zwróci Function<T, String>, ponieważ zbudowałem someMap. Ale Java jest tylko pewna, że ​​mam funkcję.

Inny przykład:

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

Wiem, że data.properties.rowCount będzie prawidłowym odwołaniem i liczbą całkowitą, ponieważ zweryfikowałem dane pod kątem schematu. Gdyby brakowało tego pola, zostałby zgłoszony wyjątek. Ale kompilator wiedziałby tylko, że albo zgłasza wyjątek, albo zwraca jakiś rodzajowy JSONValue.

Inny przykład:

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

„II6” określa sposób, w jaki dane kodują trzy zmienne. Ponieważ określiłem format, wiem, które typy zostaną zwrócone. Kompilator będzie wiedział tylko, że zwraca krotkę.

Motywem łączącym wszystkie te przykłady jest to, że programista zna typ, ale system typów na poziomie Java nie będzie w stanie tego odzwierciedlić. Kompilator nie zna typów, a zatem język o typie statycznym nie pozwala mi nazywać go, podczas gdy język o typie dynamicznym tak.

Właśnie o to chodzi w pierwotnym cytacie:

Wspaniałą rzeczą w dynamicznym pisaniu jest to, że pozwala wyrazić wszystko, co jest obliczalne. A systemy typu nie-systemy są zazwyczaj rozstrzygalne i ograniczają cię do podzbioru.

Używając pisania dynamicznego, mogę używać najbardziej pochodnego typu, o którym wiem, a nie tylko najbardziej pochodnego, jaki zna mój system typów w moim języku. We wszystkich powyższych przypadkach mam kod, który jest semantycznie poprawny, ale zostanie odrzucony przez statyczny system pisania.

Aby jednak wrócić do pytania:

Zastanawiam się, czy istnieją użyteczne wzorce projektowe lub strategie, które wykorzystując sformułowanie cytatu „nie działają jak typy”?

Dowolny z powyższych przykładów, a nawet dowolny przykład pisania dynamicznego, można wprowadzić do wpisywania statycznego, dodając odpowiednie rzutowania. Jeśli znasz typ, którego nie zna twój kompilator, po prostu powiedz kompilatorowi, przesyłając wartość. Tak więc, na pewnym poziomie, nie będziesz uzyskiwać żadnych dodatkowych wzorów za pomocą dynamicznego pisania. Być może trzeba będzie rzucić więcej, aby uzyskać działanie statycznego kodu.

Zaletą dynamicznego pisania jest to, że możesz po prostu używać tych wzorców, nie martwiąc się faktem, że trudno jest przekonać swój system typów o ich ważności. Nie zmienia dostępnych wzorców, po prostu ułatwia ich implementację, ponieważ nie musisz wymyślać, jak sprawić, by Twój system typów rozpoznał wzorzec lub dodał rzutowania, aby obalić system typów.

Winston Ewert
źródło
1
dlaczego Java jest punktem odcięcia, w którym nie powinieneś przechodzić do „bardziej zaawansowanego / skomplikowanego systemu typów”?
jk.
2
@jk, co prowadzi cię do myślenia, że ​​tak mówię? Wyraźnie unikałem poparcia dla tego, czy bardziej zaawansowany / skomplikowany system typów jest warty zachodu.
Winston Ewert,
2
Niektóre z nich są okropnymi przykładami, a inne wydają się podejmować więcej decyzji językowych niż pisać na maszynie niż bez pisania. Jestem szczególnie zdezorientowany, dlaczego ludzie uważają, że deserializacja jest tak złożona w językach pisanych na maszynie. Wpisany wynik byłby, data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); a jeśli nie ma klasy, do której można by dokonać deserializacji, możemy do niej wrócić data = parseJSON(someJson); print(data["properties.rowCount"]);- która jest nadal wpisana i wyraża tę samą intencję.
NPSF3000,
2
@ NPSF3000, jak działa funkcja parseJSON? Wydaje się, że używa albo odbicia, albo makr. Jak można wpisać dane [„properties.rowCount”] w języku statycznym? Skąd może wiedzieć, że wynikowa wartość jest liczbą całkowitą?
Winston Ewert,
2
@ NPSF3000, jak zamierzasz go używać, jeśli nie znasz liczby całkowitej? Jak zamierzasz zapętlać elementy na liście w JSON, nie wiedząc, że to tablica? Chodzi mi o to, że wiedziałem, że data.propertiesjest to obiekt i wiedziałem, że data.properties.rowCountto liczba całkowita, i mogłem po prostu napisać kod, który ich używał. Twoja propozycja data["properties.rowCount"]nie zapewnia tego samego.
Winston Ewert,
1

Oto kilka przykładów z Objective-C (typowanie dynamiczne), które nie są możliwe w C ++ (typowanie statyczne):

  • Umieszczanie obiektów kilku różnych klas w tym samym pojemniku.
    Oczywiście wymaga to kontroli typu środowiska wykonawczego, aby następnie zinterpretować zawartość kontenera, a większość znajomych korzystających z pisania statycznego sprzeciwi się, że nie powinieneś tego robić w pierwszej kolejności. Odkryłem jednak, że poza debatami religijnymi może się to przydać.

  • Rozszerzanie klasy bez podklas.
    W Objective-C możesz zdefiniować nowe funkcje składowe dla istniejących klas, w tym zdefiniowane językowo, takie jak NSString. Na przykład możesz dodać metodę stripPrefixIfPresent:, abyś mógł powiedzieć [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](zwróć uwagę na użycie NSSringliterałów @"").

  • Korzystanie z obiektowych wywołań zwrotnych.
    W językach o typie statycznym, takich jak Java i C ++, musisz dokładać znacznych starań, aby umożliwić bibliotece wywoływanie dowolnego elementu obiektu dostarczonego przez użytkownika. W Javie obejściem jest para interfejs / adapter plus anonimowa klasa, w C ++ to obejście jest zwykle oparte na szablonie, co oznacza, że ​​kod biblioteki musi być narażony na kod użytkownika. W Objective-C po prostu przekazujesz odwołanie do obiektu plus selektor metody do biblioteki, a biblioteka może po prostu i bezpośrednio wywołać wywołanie zwrotne.

cmaster
źródło
Mogę zrobić pierwszy w C ++, przesyłając do void *, ale to obchodzi system typów, więc się nie liczy. Mogę zrobić drugi w C # z metodami rozszerzenia, doskonale w systemie typów. Po trzecie, myślę, że „selektorem metody” może być lambda, więc każdy statycznie pisany język z lambdami może zrobić to samo, jeśli dobrze rozumiem. Nie znam ObjC.
user7610,
1
@JiriDanek „Mogę zrobić pierwszy w C ++ poprzez rzutowanie na void *”, nie do końca, kod odczytujący elementy nie ma możliwości samodzielnego pobrania rzeczywistego typu. Potrzebujesz tagów. Poza tym nie sądzę, że powiedzenie „mogę to zrobić w <języku>” jest odpowiednim / produktywnym sposobem patrzenia na to, ponieważ zawsze możesz je naśladować. Liczy się wzrost ekspresji vs. złożoność implementacji. Wydaje się również, że uważasz, że jeśli język ma zarówno właściwości statyczne, jak i dynamiczne (Java, C #), należy on wyłącznie do „statycznej” rodziny języków.
coredump
1
@JiriDanek void*sam w sobie nie jest dynamicznym pisaniem, lecz brakiem pisania. Ale tak, dynamic_cast, tabele wirtualne itp. Sprawiają, że C ++ nie jest typowo statycznie typowany. Czy to złe?
coredump
1
Sugeruje to, że przydatna jest opcja obalenia systemu typów w razie potrzeby. Posiadanie klapy ewakuacyjnej, gdy jej potrzebujesz. Lub ktoś uznał to za przydatne. W przeciwnym razie nie umieściliby tego w języku.
user7610,
2
@JiriDanek Myślę, że prawie przybiłeś go swoim ostatnim komentarzem. Te luki ratunkowe mogą być niezwykle przydatne, jeśli są używane ostrożnie. Niemniej jednak, z wielką mocą wiąże się wielka odpowiedzialność, a mnóstwo jest osób, które ją nadużywają ... Dlatego o wiele lepiej jest użyć wskaźnika do ogólnej klasy bazowej, z której wszystkie inne klasy pochodzą z definicji (tak jak w przypadku zarówno w Objective-C, jak i Java) i polegać na RTTI, aby rozróżnić przypadki, niż rzucać void*na określony typ obiektu. Ten pierwszy powoduje błąd w czasie wykonywania, jeśli się pomyliłeś, a później powoduje niezdefiniowane zachowanie.
cmaster