Czy istnieje dynamiczna zaleta dla języków dynamicznych? [Zamknięte]

29

Najpierw chcę powiedzieć, że Java jest jedynym językiem, jakiego kiedykolwiek używałem, więc proszę wybaczyć moją ignorancję na ten temat.

Dynamicznie pisane języki pozwalają wstawić dowolną wartość do dowolnej zmiennej. Na przykład możesz napisać następującą funkcję (psuedocode):

void makeItBark(dog){
    dog.bark();
}

I możesz przekazać w nim dowolną wartość. Tak długo, jak wartość ma bark()metodę, kod będzie działał. W przeciwnym razie generowany jest wyjątek czasu wykonywania lub coś podobnego. (Proszę mnie poprawić, jeśli się mylę).

Pozornie daje to elastyczność.

Jednak przeczytałem trochę na temat języków dynamicznych, a ludzie mówią, że projektując lub pisząc kod w języku dynamicznym, myślisz o typach i bierzesz je pod uwagę, tak samo jak w języku statycznym.

Na przykład, pisząc makeItBark()funkcję, zamierzasz akceptować tylko „rzeczy, które mogą szczekać”, i nadal musisz upewnić się, że przekazujesz do niej tylko takie rzeczy. Jedyną różnicą jest to, że teraz kompilator nie powie ci, kiedy popełniłeś błąd.

Jasne, istnieje jedna zaleta tego podejścia, polegająca na tym, że w językach statycznych, aby osiągnąć „ta funkcja akceptuje wszystko, co może szczekać”, trzeba zaimplementować jawny Barkerinterfejs. Mimo to wydaje się to niewielką zaletą.

Czy coś brakuje? Co właściwie zyskuję, używając dynamicznie pisanego języka?

Aviv Cohn
źródło
6
makeItBark(collections.namedtuple("Dog", "bark")(lambda x: "woof woof")). Ten argument nie jest nawet klasą , to anonimowa krotka. Wpisywanie kaczki („jeśli kwaknie jak ...”) pozwala ci na tworzenie interfejsów ad hoc z zasadniczo zerowymi ograniczeniami i bez narzutu składniowego. Możesz to zrobić w języku takim jak Java, ale kończy się to nieporęcznymi refleksjami. Jeśli funkcja w Javie wymaga ArrayList i chcesz nadać jej inny typ kolekcji, jesteś SOL. W pythonie nawet to nie może się pojawić.
Phoshi,
2
Tego rodzaju pytanie zostało zadane wcześniej: tutaj , tutaj i tutaj . W szczególności pierwszy przykład wydaje się odpowiadać na twoje pytanie. Może możesz przeformułować swoje, aby było wyraźne?
logc
3
Zauważ, że na przykład w C ++ możesz mieć funkcję szablonu, która działa z dowolnym typem T, który ma bark()metodę, przy czym kompilator narzeka, gdy przekażesz coś złego, ale bez konieczności faktycznego deklarowania interfejsu zawierającego bark ().
Wilbert
2
@ Phoshi Argument w Pythonie wciąż musi być określonego typu - na przykład nie może być liczbą. Jeśli masz własną implementację ad-hoc obiektów, która pobiera członków za pomocą getMemberfunkcji niestandardowej , makeItBarkwysadza się w powietrze, ponieważ wywołałeś dog.barkzamiast dog.getMember("bark"). To, co sprawia, że ​​kod działa, polega na tym, że wszyscy domyślnie zgadzają się używać rodzimego typu obiektu Pythona.
Doval,
2
@ Phoshi Just because I wrote makeItBark with my own types in mind doesn't mean you can't use yours, wheras in a static language it probably /does/ mean that.Jak wskazano w mojej odpowiedzi, tak nie jest w ogóle . Tak jest w przypadku Java i C #, ale te języki mają sparaliżowane systemy typów i modułów, więc nie są reprezentatywne dla tego, co może zrobić pisanie statyczne. Potrafię napisać doskonale ogólny makeItBarkw kilku statycznie typowanych językach, nawet niefunkcjonalnych, takich jak C ++ lub D.
Doval

Odpowiedzi:

35

Języki o typie dynamicznym są jednoznaczne

Porównując systemy typów , dynamiczne pisanie nie ma przewagi. Pisanie dynamiczne to szczególny przypadek pisania statycznego - jest to język o typie statycznym, w którym każda zmienna ma ten sam typ. To samo można osiągnąć w Javie (bez zwięzłości), ustawiając każdą zmienną na typ Objecti ustawiając wartości „obiektowe” na typ Map<String, Object>:

void makeItBark(Object dog) {
    Map<String, Object> dogMap = (Map<String, Object>) dog;
    Runnable bark = (Runnable) dogMap.get("bark");
    bark.run();
}

Tak więc, nawet bez refleksji, możesz osiągnąć ten sam efekt w prawie każdym statycznie wpisanym języku, pomijając wygodę składniową. Nie dostajesz żadnej dodatkowej mocy ekspresji; przeciwnie, masz mniej siłę wyrazu, ponieważ w dynamicznie wpisywanych języka, jesteś zaprzeczyć zdolność do ograniczania pewnych typów zmiennych.

Robienie kaczki z kory statycznie pisanym językiem

Co więcej, dobry język o typie statycznym pozwoli ci pisać kod, który działa z każdym typem, który ma barkoperację. W Haskell jest to klasa typu:

class Barkable a where
    bark :: a -> unit

Wyraża to ograniczenie, że aby dany typ amógł zostać uznany za barkalny, musi istnieć barkfunkcja, która przyjmuje wartość tego typu i nic nie zwraca.

Następnie możesz napisać funkcje ogólne pod względem Barkableograniczenia:

makeItBark :: Barkable a => a -> unit
makeItBark barker = bark (barker)

Mówi to, że makeItBarkbędzie działać na każdy typ spełniający Barkablewymagania. Może się to wydawać podobne do interfacejęzyka Java lub C #, ale ma jedną wielką zaletę - typy nie muszą z góry określać klas typów, które spełniają. Mogę powiedzieć, że ten typ Duckjest Barkablew dowolnym momencie, nawet jeśli Ducknie napisałem tego typu strony trzeciej. W rzeczywistości nie ma znaczenia, że ​​autor Ducknie napisał barkfunkcji - mogę podać ją później, gdy powiem językowi, który Duckspełnia Barkable:

instance Barkable Duck where
    bark d = quack (punch (d))

makeItBark (aDuck)

To mówi, że Ducks mogą szczekać, a ich funkcja szczekania jest realizowana przez uderzenie kaczki przed jej kwakaniem. Dzięki temu możemy wezwać makeItBarkkaczki.

Standard MLi OCamlsą jeszcze bardziej elastyczne, ponieważ możesz zaspokoić tę samą klasę typów na więcej niż jeden sposób. W tych językach mogę powiedzieć, że liczby całkowite można zamówić za pomocą konwencjonalnego porządku, a następnie odwrócić się i powiedzieć, że można je również uporządkować według podzielności (np. 10 > 5Ponieważ 10 jest podzielne przez 5). W Haskell możesz utworzyć instancję klasy tylko raz. (Dzięki temu Haskell automatycznie wie, że można zadzwonić barkdo kaczki; w SML lub OCaml musisz wyraźnie określić, której bark funkcji chcesz, ponieważ może być więcej niż jedna).

Zwięzłość

Oczywiście istnieją różnice składniowe. Przedstawiony kod Pythona jest o wiele bardziej zwięzły niż napisany przeze mnie odpowiednik Java. W praktyce zwięzłość ta jest dużą częścią uroku dynamicznie pisanych języków. Jednak wnioskowanie o typach pozwala pisać kod, który jest równie zwięzły w językach o typie statycznym, dzięki czemu nie musisz jawnie pisać typów każdej zmiennej. Język o typie statycznym może również zapewniać natywną obsługę dynamicznego pisania, usuwając szczegółowość wszystkich operacji rzutowania i mapowania (np. C # dynamic).

Prawidłowe, ale źle napisane programy

Aby być sprawiedliwym, wpisywanie statyczne niekoniecznie wyklucza niektóre programy, które są technicznie poprawne, nawet jeśli moduł sprawdzania typów nie może tego zweryfikować. Na przykład:

if this_variable_is_always_true:
    return "some string"
else:
    return 6

Większość języków o typie statycznym odrzuciłaby to ifoświadczenie, nawet jeśli gałąź else nigdy się nie pojawi. W praktyce wydaje się, że nikt nie korzysta z tego typu kodu - coś zbyt sprytnego dla sprawdzania typów prawdopodobnie sprawi, że przyszli opiekunowie twojego kodu będą przeklinać ciebie i twoich najbliższych. W tym przypadku ktoś z powodzeniem przetłumaczył 4 projekty Pythona o otwartym kodzie źródłowym na Haskell, co oznacza, że ​​nie robili niczego, czego nie byłby w stanie skompilować dobry język o typie statycznym. Co więcej, kompilator znalazł kilka błędów związanych z typem, których nie wykryły testy jednostkowe.

Najsilniejszym argumentem, jaki widziałem dla dynamicznego pisania, są makra Lispa, ponieważ pozwalają one dowolnie rozszerzyć składnię języka. Jednak Typed Racket to statycznie wpisany dialekt Lisp, który ma makra, więc wydaje się, że pisanie statyczne i makra nie wykluczają się wzajemnie, choć być może trudniej jest je jednocześnie wdrożyć.

Jabłka i Pomarańcze

Na koniec nie zapominaj, że istnieją większe różnice w językach niż tylko ich system typów. Przed Javą 8 wykonywanie jakiegokolwiek programowania funkcjonalnego w Javie było praktycznie niemożliwe; prosta lambda wymagałaby 4 wierszy anonimowego kodu klasy. Java również nie obsługuje literałów kolekcji (np [1, 2, 3].). Mogą również występować różnice w jakości i dostępności narzędzi (IDE, debuggery), bibliotek i wsparcia społeczności. Jeśli ktoś twierdzi, że jest bardziej produktywny w Pythonie lub Ruby niż Java, należy wziąć pod uwagę tę rozbieżność funkcji. Istnieje różnica między porównywaniem języków ze wszystkimi dołączonymi akumulatorami , rdzeniami językowymi i systemami typów .

Doval
źródło
2
Zapomniałeś przypisać swoje źródło do pierwszego akapitu - existent
2
@Matt Re: 1, nie zakładałem, że to nie jest ważne; Zwróciłem się do tego w ramach zwięzłości. Re: 2, chociaż nigdy tego nie powiedziałem wprost, przez „dobry” mam na myśli „dokładne wnioskowanie o typie” i „ma system modułowy, który pozwala dopasowywać kod do podpisów po fakcie ”, a nie z góry jak Java / Interfejsy C #. Do 3, ciężar dowodu spoczywa na tobie, abyś wyjaśnił mi, dlaczego biorąc pod uwagę dwa języki o równoważnej składni i funkcjach, jeden dynamicznie typowany, a drugi z pełnym wnioskowaniem typu, nie byłbyś w stanie napisać kodu o równej długości w obu .
Doval
3
@MattFenwick Już to uzasadniłem - biorąc pod uwagę dwa języki z tymi samymi funkcjami, jeden dynamiczny, a drugi statyczny, główna różnica między nimi to obecność adnotacji typu, a wnioskowanie typu to zabiera. Wszelkie inne różnice w składni są powierzchowne, a wszelkie różnice w funkcjach zamieniają porównanie w jabłka i pomarańcze. To od Ciebie zależy, jak ta logika jest błędna.
Doval
1
Powinieneś rzucić okiem na Boo. Jest statycznie wpisywany z wnioskowaniem typu i zawiera makra, które pozwalają na rozszerzenie składni języka.
Mason Wheeler,
1
@Doval: True. BTW, notacja lambda nie jest używana wyłącznie w programowaniu funkcjonalnym: o ile wiem, Smalltalk ma anonimowe bloki, a Smalltalk jest tak zorientowany obiektowo, jak to tylko możliwe. Dlatego często rozwiązaniem jest przekazywanie anonimowego bloku kodu z niektórymi parametrami, bez względu na to, czy jest to anonimowa funkcja, czy anonimowy obiekt z dokładnie jedną anonimową metodą. Myślę, że te dwie konstrukcje wyrażają zasadniczo tę samą ideę z dwóch różnych perspektyw (funkcjonalnej i obiektowej).
Giorgio
11

To trudna i dość subiektywna kwestia. (Twoje pytanie może zostać zamknięte jako oparte na opiniach, ale to nie znaczy, że jest to złe pytanie - wręcz przeciwnie, nawet myślenie o takich metajęzykowych pytaniach jest dobrym znakiem - po prostu nie pasuje do formatu pytań i odpowiedzi tego forum).

Oto mój pogląd na to: celem języków wysokiego poziomu jest ograniczenie tego, co programista może zrobić z komputerem. Jest to zaskakujące dla wielu osób, ponieważ uważają, że ich celem jest zapewnienie użytkownikom większej mocy i więcej . Ale ponieważ wszystko, co piszesz w Prologu, C ++ lub Listie, jest ostatecznie wykonywane jako kod maszynowy, w rzeczywistości nie jest możliwe, aby dać programiście więcej mocy, niż zapewnia już język asemblera.

Istotą języka wysokiego poziomu jest pomoc programiście w zrozumieniu kodu, który sami stworzyli, oraz zwiększenie wydajności w wykonywaniu tego samego. Nazwa podprogramu jest łatwiejsza do zapamiętania niż adres szesnastkowy. Automatyczny licznik argumentów jest łatwiejszy w użyciu niż sekwencja wywołań, tutaj musisz uzyskać liczbę argumentów dokładnie na własną rękę, bez pomocy. System typów idzie dalej i ogranicza rodzaj argumentów, które możesz podać w danym miejscu.

Tutaj różni się postrzeganie ludzi. Niektórzy ludzie (jestem wśród nich) uważają, że dopóki procedura sprawdzania hasła i tak będzie oczekiwać dokładnie dwóch argumentów i zawsze ciąg znaków poprzedzony identyfikatorem numerycznym, przydatne jest zadeklarowanie tego w kodzie i automatyczne przypomnienie, jeśli później zapomnisz przestrzegać tej zasady. Outsourcing takich drobnych księgowości do kompilatora pomaga uwolnić umysł od problemów na wyższym poziomie i pozwala lepiej zaprojektować i zaprojektować system. Dlatego systemy typów to wygrana netto: pozwalają komputerowi robić to, w czym jest dobry, a ludzie robią to, w czym są dobrzy.

Inni traktują to zupełnie inaczej. Nie lubią, gdy kompilator mówi im, co mają robić. Nie podoba im się dodatkowy wysiłek związany z podjęciem decyzji w sprawie deklaracji typu i jej wpisaniem. Preferują eksploracyjny styl programowania, w którym piszesz rzeczywisty kod biznesowy bez planu, który powiedziałby dokładnie, jakie typy i argumenty użyć gdzie. I jeśli chodzi o styl programowania, którego używają, może to być całkiem prawda.

Oczywiście, nadmiernie upraszczam tutaj. Sprawdzanie typu nie jest ściśle powiązane z wyraźnymi deklaracjami typu; istnieje również wnioskowanie typu. Programowanie za pomocą procedur, które faktycznie biorą argumenty różnych typów, pozwala na całkiem różne i bardzo potężne rzeczy, które w innym przypadku byłyby niemożliwe, po prostu wiele osób nie jest uważnych i konsekwentnych, aby z powodzeniem korzystać z takiej swobody.

W końcu fakt, że tak różne języki są bardzo popularne i nie wykazują oznak wymarcia, pokazuje, że ludzie zajmują się programowaniem bardzo inaczej. Myślę, że funkcje języka programowania w dużej mierze dotyczą czynników ludzkich - co lepiej wspiera ludzki proces decyzyjny - i dopóki ludzie pracują bardzo inaczej, rynek zapewni jednocześnie bardzo różne rozwiązania.

Kilian Foth
źródło
3
Dziękuję za odpowiedź. Powiedziałeś, że niektórzy ludzie nie lubią, gdy kompilator mówi im, co mają robić. [..] Wolą eksploracyjny styl programowania, w którym piszesz rzeczywisty kod biznesowy bez planu, który powiedziałby dokładnie, jakie typy i argumenty użyć gdzie. Tego nie rozumiem: programowanie nie jest jak muzyczna improwizacja. Jeśli w muzyce uderzysz niewłaściwą nutę, może to brzmieć fajnie. W programowaniu, jeśli przekażesz coś do funkcji, która nie powinna tam być, najprawdopodobniej dostaniesz paskudne błędy. (kontynuacja w następnym komentarzu).
Aviv Cohn
3
Zgadzam się, ale wiele osób się nie zgadza. Ludzie są dość zaborczy wobec swoich uprzedzeń mentalnych, zwłaszcza że często ich nie wiedzą. Dlatego debaty na temat stylu programowania zwykle przeradzają się w kłótnie lub walki, a rzadko jest przydatne, aby zacząć od przypadkowych nieznajomych w Internecie.
Kilian Foth
1
Właśnie dlatego - sądząc po tym, co czytam - osoby używające języków dynamicznych biorą pod uwagę typy tak samo jak osoby używające języków statycznych. Ponieważ kiedy piszesz funkcję, powinna ona przyjmować argumenty określonego rodzaju. Nie ma znaczenia, czy kompilator wymusza to, czy nie. Sprowadza się to do pisania statycznego, które ci w tym pomaga, a pisanie dynamiczne nie. W obu przypadkach funkcja musi mieć określony rodzaj danych wejściowych. Więc nie rozumiem, jaka jest zaleta dynamicznego pisania. Nawet jeśli wolisz „eksploracyjny styl programowania”, nadal nie możesz przekazać do funkcji tego, co chcesz.
Aviv Cohn
1
Ludzie często mówią o bardzo różnych rodzajach projektów (szczególnie w odniesieniu do wielkości). Logika biznesowa witryny internetowej będzie bardzo prosta w porównaniu do pełnego systemu ERP. Ryzyko popełnienia błędu jest mniejsze, a korzyść z możliwości bardzo prostego ponownego wykorzystania kodu jest bardziej odpowiednia. Powiedzmy, że mam kod, który generuje PDF (lub trochę HTML) ze struktury danych. Teraz mam inne źródło danych (najpierw JSON z jakiegoś interfejsu API REST, teraz jest to importer Excela). W języku takim jak Ruby bardzo łatwo jest „zasymulować” pierwszą strukturę, „sprawić, by szczekał” i ponownie użyć kodu PDF.
thorsten müller
@Prog: Prawdziwą zaletą języków dynamicznych jest opisywanie rzeczy, które są naprawdę trudne w przypadku systemu typu statycznego. Na przykład funkcja w pythonie może być odwołaniem do funkcji, lambda, obiektem funkcji lub bóg wie co i wszystko będzie działać tak samo. Możesz zbudować obiekt, który otacza inny obiekt i automatycznie wywołuje metody z zerowym narzutem składniowym, a każda funkcja zasadniczo magicznie ma sparametryzowane typy. Dynamiczne języki są niesamowite do szybkiego wykonywania zadań.
Phoshi
5

Kod napisany przy użyciu języków dynamicznych nie jest sprzężony z systemem typu statycznego. Dlatego ten brak sprzężenia jest zaletą w porównaniu ze słabymi / nieodpowiednimi układami typu statycznego (chociaż może to być mycie lub wada w porównaniu z doskonałym układem typu statycznego).

Ponadto w przypadku języka dynamicznego system typu statycznego nie musi być projektowany, wdrażany, testowany i konserwowany. Może to uprościć implementację w porównaniu do języka ze statycznym systemem typów.


źródło
2
Czy ludzie nie mają tendencji do ponownego wdrażania podstawowego systemu typu statycznego za pomocą testów jednostkowych (jeśli mają na celu uzyskanie dobrego zasięgu testów)?
Den
Co rozumiesz przez „sprzęganie” tutaj? Jak przejawiłoby się to np. W architekturze mikrousług?
Den
@Den 1) dobre pytanie, jednak czuję, że jest to poza zakresem OP i mojej odpowiedzi. 2) Mam na myśli sprzężenie w tym sensie ; krótko, różne systemy typów nakładają różne (niekompatybilne) ograniczenia na kod napisany w tym języku. Przepraszam, nie mogę odpowiedzieć na ostatnie pytanie - nie rozumiem, co jest specjalnego w mikro-usługach pod tym względem.
2
@Den: Bardzo dobry punkt: często obserwuję, że testy jednostkowe, które piszę w Pythonie, obejmują błędy, które mogłyby zostać wychwycone przez kompilator w języku o typie statycznym.
Giorgio
@MattFenwick: Napisałeś, że zaletą jest to, że „... w przypadku dynamicznego języka system typu statycznego nie musi być projektowany, wdrażany, testowany i konserwowany”. i Den zauważył, że często trzeba projektować i testować typy bezpośrednio w kodzie. Tak więc wysiłek nie został usunięty, ale przeniesiony z projektu języka do kodu aplikacji.
Giorgio