Używam Pythona od kilku dni i myślę, że rozumiem różnicę między pisaniem dynamicznym a statycznym. Nie rozumiem, w jakich okolicznościach byłoby to preferowane. Jest elastyczny i czytelny, ale kosztem większej liczby kontroli środowiska wykonawczego i dodatkowych wymaganych testów jednostkowych.
Oprócz niefunkcjonalnych kryteriów, takich jak elastyczność i czytelność, jakie są powody, aby wybierać dynamiczne pisanie? Co mogę zrobić z dynamicznym pisaniem, które inaczej nie jest możliwe? Jak myślisz, jaki konkretny przykład kodu ilustruje konkretną zaletę dynamicznego pisania?
dynamic-typing
static-typing
Justin984
źródło
źródło
Odpowiedzi:
Ponieważ poprosiłeś o konkretny przykład, dam ci jeden.
Massive ORM Roba Conery'ego to 400 linii kodu. Jest tak mały, ponieważ Rob jest w stanie mapować tabele SQL i zapewniać wyniki obiektów bez konieczności posiadania wielu typów statycznych do dublowania tabel SQL. Dokonuje się tego za pomocą
dynamic
typu danych w języku C #. Strona Roba szczegółowo opisuje ten proces, ale wydaje się jasne, że w tym konkretnym przypadku użycia dynamiczne pisanie jest w dużej mierze odpowiedzialne za zwięzłość kodu.Porównaj z Dapper Sama Saffrona , który wykorzystuje typy statyczne;
SQLMapper
klasa sama wynosi 3000 linii kodu.Pamiętaj, że obowiązują zwykłe wyłączenia odpowiedzialności, a przebieg może się różnić; Dapper ma inne cele niż Massive. Podkreślam to jako przykład czegoś, co można zrobić w 400 liniach kodu, co prawdopodobnie nie byłoby możliwe bez dynamicznego pisania.
Pisanie dynamiczne umożliwia odroczenie decyzji dotyczących typu w czasie wykonywania. To wszystko.
Niezależnie od tego, czy używasz języka z dynamicznym pisaniem, czy ze statycznym, twój wybór tekstu musi być rozsądny. Nie dodasz dwóch ciągów razem i nie oczekujesz odpowiedzi numerycznej, chyba że ciągi zawierają dane liczbowe, a jeśli nie, otrzymasz nieoczekiwane wyniki. Język o typie statycznym nie pozwoli ci tego zrobić w pierwszej kolejności.
Zwolennicy języków o typie statycznym zwracają uwagę, że kompilator może w znacznym stopniu wykonać „sprawdzanie poprawności” kodu w czasie kompilacji, zanim zostanie wykonany pojedynczy wiersz. To jest Dobra Rzecz ™.
C # zawiera
dynamic
słowo kluczowe, które pozwala ci odroczyć decyzję typu uruchomieniowego bez utraty korzyści bezpieczeństwa statycznego w pozostałej części kodu. Typ inferencji (var
) eliminuje wiele problemów związanych z pisaniem w języku o typie statycznym, eliminując potrzebę zawsze jawnego deklarowania typów.Dynamiczne języki wydają się faworyzować bardziej interaktywne, bezpośrednie podejście do programowania. Nikt nie oczekuje, że będziesz musiał napisać klasę i przejść przez cykl kompilacji, aby wpisać trochę kodu Lisp i zobaczyć, jak się wykonuje. Ale właśnie tego mam się spodziewać w języku C #.
źródło
Zwroty takie jak „pisanie statyczne” i „pisanie dynamiczne” są często rzucane, a ludzie używają subtelnie różnych definicji, więc zacznijmy od wyjaśnienia, co mamy na myśli.
Rozważ język, który ma typy statyczne sprawdzane podczas kompilacji. Ale powiedzmy, że błąd typu generuje tylko ostrzeżenie nie kończące się śmiercią, aw czasie wykonywania wszystko jest zapisywane na maszynie. Te typy statyczne służą wyłącznie wygodzie programisty i nie wpływają na codegen. To pokazuje, że pisanie statyczne samo w sobie nie narzuca żadnych ograniczeń i nie wyklucza się wzajemnie z pisaniem dynamicznym. (Cel C jest podobny do tego.)
Ale większość systemów typu statycznego nie zachowuje się w ten sposób. Istnieją dwie wspólne właściwości systemów typu statycznego, które mogą nakładać ograniczenia:
Kompilator może odrzucić program zawierający błąd typu statycznego.
Jest to ograniczenie, ponieważ wiele programów typu bezpiecznego musi zawierać błąd typu statycznego.
Na przykład mam skrypt w języku Python, który musi działać zarówno jako Python 2, jak i Python 3. Niektóre funkcje zmieniły typy parametrów między Python 2 i 3, więc mam taki kod:
Sprawdzanie statycznego typu Python 2 odrzuciłoby kod Python 3 (i odwrotnie), nawet jeśli nigdy nie zostałoby wykonane. Bezpieczny program mojego typu zawiera błąd typu statycznego.
Jako kolejny przykład rozważmy komputer Mac, który chce działać w systemie OS X 10.6, ale skorzystaj z nowych funkcji w wersji 10.7. Metody 10.7 mogą, ale nie muszą istnieć w czasie wykonywania, i wykrycie ich zależy ode mnie, programisty. Sprawdzanie typu statycznego jest zmuszone albo odrzucić mój program, aby zapewnić bezpieczeństwo typu, albo zaakceptować program, wraz z możliwością spowodowania błędu typu (brak funkcji) w czasie wykonywania.
Statyczne sprawdzanie typu zakłada, że środowisko wykonawcze jest odpowiednio opisane w informacjach o czasie kompilacji. Ale przewidywanie przyszłości jest niebezpieczne!
Oto jeszcze jedno ograniczenie:
Kompilator może generować kod, który zakłada, że typ środowiska wykonawczego jest typem statycznym.
Zakładanie, że typy statyczne są „prawidłowe”, daje wiele możliwości optymalizacji, ale te optymalizacje mogą być ograniczające. Dobrym przykładem są obiekty proxy, np. Zdalne. Załóżmy, że chcesz mieć lokalny obiekt proxy, który przekazuje wywołania metod do rzeczywistego obiektu w innym procesie. Byłoby miło, gdyby serwer proxy był ogólny (aby mógł maskować się jako dowolny obiekt) i przezroczysty (aby istniejący kod nie musiał wiedzieć, że rozmawia z serwerem proxy). Ale aby to zrobić, kompilator nie może wygenerować kodu, który zakłada, że typy statyczne są poprawne, np. Przez statyczne wstawianie wywołań metod, ponieważ to się nie powiedzie, jeśli obiekt jest proxy.
Przykłady takiego zdalnego działania w akcji to NSXPCConnection ObjC lub TransparentProxy w języku C # (którego implementacja wymagała kilku pesymizacji w środowisku wykonawczym - patrz tutaj dyskusja).
Gdy codegen nie jest zależny od typów statycznych i masz takie funkcje, jak przekazywanie wiadomości, możesz robić wiele fajnych rzeczy z obiektami proxy, debugowaniem itp.
To jest próbka niektórych rzeczy, które możesz zrobić, jeśli nie musisz spełniać wymagań sprawdzania typu. Ograniczenia nie są narzucane przez typy statyczne, ale przez wymuszone sprawdzanie typów statycznych.
źródło
A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error.
W dowolnym rozsądnym języku statycznym można to zrobić za pomocąIFDEF
instrukcji preprocesora typu, zachowując bezpieczeństwo typu w obu przypadkach.Zmienne typu kaczego są pierwszą rzeczą, o której wszyscy myślą, ale w większości przypadków te same korzyści można uzyskać przez wnioskowanie o typie statycznym.
Ale pisanie kaczek w dynamicznie tworzonych kolekcjach jest trudne do osiągnięcia w inny sposób:
Więc jaki typ
JSON.parse
powraca? Słownik tablic liczb całkowitych lub słowników ciągów? Nie, nawet to nie jest wystarczająco ogólne.JSON.parse
musi zwrócić jakąś „wartość wariantu”, która może być zerowa, bool, zmiennoprzecinkowa, łańcuch, tablica dowolnego z tych typów rekurencyjnie lub słownik od łańcucha do dowolnego z tych typów rekurencyjnie. Główne zalety pisania dynamicznego wynikają z posiadania tego rodzaju wariantów.Jak dotąd jest to zaleta typów dynamicznych , a nie języków dynamicznie typowanych. Przyzwoity język statyczny może idealnie symulować każdy taki typ. (A nawet „złe” języki często mogą je symulować, łamiąc bezpieczeństwo pod maską i / lub wymagając niezgrabnej składni.)
Zaletą języków dynamicznie typowanych jest to, że takie typy nie mogą być wywnioskowane przez statyczne systemy wnioskowania typu. Musisz napisać ten typ jawnie. Ale w wielu takich przypadkach - w tym raz - kod opisujący typ jest dokładnie tak skomplikowany jak kod do parsowania / konstruowania obiektów bez opisywania typu, więc niekoniecznie jest to zaletą.
źródło
Ponieważ każdy zdalnie praktyczny system typu statycznego jest poważnie ograniczony w porównaniu do języka programowania, którego dotyczy, nie może wyrazić wszystkich niezmienników, które kod mógłby sprawdzić w czasie wykonywania. Aby nie obchodzić gwarancji, które system stara się udzielić, decyduje się być konserwatywny i nie zezwala na przypadki użycia, które przejdą takie testy, ale nie można tego udowodnić (w systemie typów).
Dam przykład. Załóżmy, że wdrażasz prosty model danych do opisywania obiektów danych, ich zbiorów itp., Który jest statycznie wpisany w tym sensie, że jeśli model mówi, że atrybut
x
obiektu typu Foo zawiera liczbę całkowitą, musi zawsze zawierać liczbę całkowitą. Ponieważ jest to konstrukcja wykonawcza, nie można jej wpisać statycznie. Załóżmy, że przechowujesz dane opisane w plikach YAML. Tworzysz mapę skrótu (do późniejszego przekazania do biblioteki YAML), dostajeszx
atrybut, przechowujesz go na mapie, dostajesz ten inny atrybut, który tak się składa, że jest łańcuchem, ... poczekaj chwilę? Jaki jestthe_map[some_key]
teraz typ ? Cóż, strzelaj, wiemy, że taksome_key
jest'x'
i dlatego wynik musi być liczbą całkowitą, ale system typów nie może nawet zacząć o tym myśleć.Niektóre aktywnie badane systemy typów mogą działać w tym konkretnym przykładzie, ale są one niezwykle skomplikowane (zarówno dla autorów kompilatorów do zaimplementowania, jak i dla programistów do uzasadnienia), szczególnie dla czegoś tak „prostego” (to znaczy, właśnie wyjaśniłem to w jednym ustęp).
Oczywiście, dzisiejszym rozwiązaniem jest boksowanie wszystkiego, a następnie rzutowanie (lub posiadanie wielu przesłoniętych metod, z których większość podnosi wyjątki „nie zaimplementowane”). Ale to nie jest statycznie wpisane, jest to włamanie do systemu typów w celu sprawdzenia typów w czasie wykonywania.
źródło
Nic nie można zrobić z dynamicznym pisaniem, czego nie można zrobić z pisaniem statycznym, ponieważ można zaimplementować pisanie dynamiczne na podstawie języka statycznego.
Krótki przykład w Haskell:
Przy wystarczającej liczbie przypadków można wdrożyć dowolny system typu dynamicznego.
I odwrotnie, możesz także przetłumaczyć dowolny program o typie statycznym na równoważny program dynamiczny. Oczywiście stracilibyście wszystkie zapewnienia poprawności w czasie kompilacji zapewniane przez język typowany statycznie.
Edycja: Chciałem zachować prostotę, ale oto więcej szczegółów na temat modelu obiektowego
Funkcja przyjmuje listę danych jako argumenty i wykonuje obliczenia z efektami ubocznymi w ImplMonad i zwraca dane.
DMember
jest wartością elementu lub funkcją.Rozszerz,
Data
aby objąć obiekty i funkcje. Obiekty to listy nazwanych członków.Te typy statyczne są wystarczające do wdrożenia każdego systemu obiektów o typie dynamicznym, który znam.
źródło
Data
.+
operator, który łączy dwieData
wartości w innąData
wartość.Data
reprezentuje standardowe wartości w systemie typu dynamicznego.Membrany :
Każdy typ jest zawijany przez typ, który ma ten sam interfejs, ale który przechwytuje wiadomości i zawija i rozpakowuje wartości podczas przechodzenia przez membranę. Jaki jest typ funkcji zawijania w twoim ulubionym języku wpisywanym statycznie? Być może Haskell ma typ dla tych funkcji, ale większość języków o typie statycznym tego nie robi lub używają Object → Object, skutecznie rezygnując z odpowiedzialności za sprawdzanie typów.
źródło
class Foo a where ...
data Wrapper = forall a. Foo a => Wrapper a
String
ponieważ jest to konkretny typ w Javie. Smalltalk nie ma tego problemu, ponieważ nie próbuje pisać#doesNotUnderstand
.Jak ktoś wspomniał, teoretycznie niewiele można zrobić z dynamicznym pisaniem, czego nie można zrobić z pisaniem statycznym, gdybyś sam wdrożył niektóre mechanizmy. Większość języków zapewnia mechanizmy relaksacji typów, które wspierają elastyczność typów, takie jak wskaźniki pustki i typ obiektu głównego lub pusty interfejs.
Lepszym pytaniem jest, dlaczego dynamiczne pisanie jest bardziej odpowiednie i bardziej odpowiednie w określonych sytuacjach i problemach.
Najpierw zdefiniujmy
Podmiot - potrzebowałbym ogólnego pojęcia o pewnym bycie w kodzie. Może to być wszystko - od liczby pierwotnej po złożone dane.
Zachowanie - powiedzmy, że nasza istota ma pewien stan i zestaw metod, które pozwalają światu na instruowanie istoty do pewnych reakcji. Nazwijmy stan + interfejs tego bytu jego zachowaniem. Jedna jednostka może mieć więcej niż jedno zachowanie połączone w określony sposób przez narzędzia zapewniane przez język.
Definicje bytów i ich zachowań - każdy język zapewnia pewne abstrakcje, które pomagają zdefiniować zachowania (zestaw metod + stan wewnętrzny) niektórych bytów w programie. Możesz przypisać nazwę do tych zachowań i powiedzieć, że wszystkie wystąpienia tego zachowania są określonego typu .
Prawdopodobnie jest to coś, co nie jest tak obce. I jak powiedziałeś, zrozumiałeś różnicę, ale nadal. Prawdopodobnie niepełne i najdokładniejsze wyjaśnienie, ale mam nadzieję, że będzie wystarczająco fajna, aby przynieść jakąś wartość :)
Wpisywanie statyczne - zachowanie wszystkich bytów w twoim programie jest sprawdzane w czasie kompilacji, przed uruchomieniem kodu. Oznacza to, że jeśli na przykład chcesz, aby twoja encja typu Osoba miała zachowanie Magika, musisz zdefiniować encję MagicianPerson i nadać jej zachowania magowi typu throwMagic (). Jeśli podasz kod, omyłkowo powiesz to zwykłemu kompilatorowi Person.throwMagic ()
"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
Pisanie dynamiczne - w środowiskach pisania dynamicznego dostępne zachowania jednostek nie są sprawdzane, dopóki naprawdę nie spróbujesz zrobić czegoś z określoną jednostką. Uruchomienie kodu Ruby, który prosi Person.throwMagic (), nie zostanie przechwycone, dopóki twój kod nie pojawi się naprawdę. Brzmi frustrująco, prawda? Ale to także brzmi rewelacyjnie. Na podstawie tej właściwości możesz robić ciekawe rzeczy. Załóżmy, że projektujesz grę, w której wszystko może zmienić się w Maga i nie wiesz, kto to będzie, dopóki nie dojdziesz do określonego punktu w kodzie. A potem przychodzi Żaba i mówisz
HeyYouConcreteInstanceOfFrog.include Magic
i od tego momentu ta Żaba staje się jedną konkretną Żabą, która ma magiczne moce. Inne żaby, wciąż nie. Widzisz, w statycznych językach do pisania, musiałbyś zdefiniować tę relację za pomocą standardowej kombinacji kombinacji zachowań (takich jak implementacja interfejsu). W dynamicznym języku pisania możesz to zrobić w czasie wykonywania i nikogo to nie obchodzi.Większość dynamicznych języków pisania ma mechanizmy zapewniające ogólne zachowanie, które przechwytuje każdy komunikat przekazywany do ich interfejsu. Na przykład Ruby
method_missing
i PHP,__call
jeśli dobrze pamiętam. Oznacza to, że możesz wykonywać dowolne ciekawe rzeczy w czasie wykonywania programu i podejmować decyzje dotyczące typu na podstawie bieżącego stanu programu. Daje to narzędzia do modelowania problemu, które są znacznie bardziej elastyczne niż, powiedzmy, konserwatywny statyczny język programowania, taki jak Java.źródło