Dlaczego dynamicznie pisane języki nie pozwalają deweloperowi określić typ?

14

Znane dynamicznie typy języków nigdy nie pozwalają programistom określać typów zmiennych, a przynajmniej mają bardzo ograniczone wsparcie dla tego.

Na przykład JavaScript nie zapewnia żadnego mechanizmu wymuszania typów zmiennych, gdy jest to wygodne. PHP pozwalają określić niektóre rodzaje argumentów metoda, ale nie ma sposobu, aby korzystać z rodzimych typów ( int, stringetc.) dla argumentów i nie ma sposobu, aby wymusić typy na cokolwiek innego niż argumentów.

Jednocześnie wygodniej byłoby mieć możliwość określenia w niektórych przypadkach typu zmiennej w języku dynamicznie wpisywanym, zamiast ręcznego sprawdzania typu.

Dlaczego są takie ograniczenia? Czy to z przyczyn technicznych / wydajności (przypuszczam, że jest tak w przypadku JavaScript), czy tylko z powodów politycznych (co, jak sądzę, w przypadku PHP)? Czy dotyczy to innych dynamicznie pisanych języków, których nie znam?


Edycja: postępując zgodnie z odpowiedziami i komentarzami, oto przykład wyjaśnienia: powiedzmy, że mamy następującą metodę w zwykłym PHP:

public function CreateProduct($name, $description, $price, $quantity)
{
    // Check the arguments.
    if (!is_string($name)) throw new Exception('The name argument is expected to be a string.');
    if (!is_string($description)) throw new Exception('The description argument is expected to be a string.');
    if (!is_float($price) || is_double($price)) throw new Exception('The price argument is expected to be a float or a double.');
    if (!is_int($quantity)) throw new Exception('The quantity argument is expected to be an integer.');

    if (!$name) throw new Exception('The name argument cannot be an empty string.');
    if ($price <= 0) throw new Exception('The price argument cannot be less or equal to zero.');
    if ($price < 0) throw new Exception('The price argument cannot be less than zero.');

    // We can finally begin to write the actual code.
    // TODO: Implement the method here.
}

Przy pewnym wysiłku można to przepisać jako (patrz także Programowanie kontraktów w PHP ):

public function CreateProduct($name, $description, $price, $quantity)
{
    Component::CheckArguments(__FILE__, __LINE__, array(
        'name' => array('value' => $name, 'type' => VTYPE_STRING),
        'description' => array('value' => $description, 'type' => VTYPE_STRING),
        'price' => array('value' => $price, 'type' => VTYPE_FLOAT_OR_DOUBLE),
        'quantity' => array('value' => $quantity, 'type' => VTYPE_INT)
    ));

    if (!$name) throw new Exception('The name argument cannot be an empty string.');
    if ($price <= 0) throw new Exception('The price argument cannot be less or equal to zero.');
    if ($price < 0) throw new Exception('The price argument cannot be less than zero.');

    // We can finally begin to write the actual code.
    // TODO: Implement the method here.
}

Ale ta sama metoda zostałaby zapisana w następujący sposób, gdyby PHP opcjonalnie akceptował typy rodzime dla argumentów:

public function CreateProduct(string $name, string $description, double $price, int $quantity)
{
    // Check the arguments.
    if (!$name) throw new Exception('The name argument cannot be an empty string.');
    if ($price <= 0) throw new Exception('The price argument cannot be less or equal to zero.');
    if ($price < 0) throw new Exception('The price argument cannot be less than zero.');

    // We can finally begin to write the actual code.
    // TODO: Implement the method here.
}

Który z nich jest krótszy do napisania? Który z nich jest łatwiejszy do odczytania?

Arseni Mourzenko
źródło
1
Możesz opcjonalnie określić typy w niektórych dynamicznie wpisywanych językach - np. W Common Lisp.
SK-logic
Sporo dynamicznie wpisywanych języków używa rzutów, aby wymusić typ ...
Trezoid
Niektórzy. Na przykład cel C jest dynamicznie wpisywany, ale możesz zadeklarować typ dla zmiennych, a kompilator wyświetli ostrzeżenia, jeśli nie uzyskasz oczekiwanego typu.
mipadi
1
Clojure jest przykładem języka, który jest zwykle dynamicznie wpisywany, ale możesz opcjonalnie podać typy zmiennych poprzez „podpowiedzi typu” (zazwyczaj jest to wykonywane tylko tam, gdzie jest to potrzebne, aby uzyskać korzyści wydajnościowe z informacji o typie kompilacji)
mikera
1
Groovy to kolejny przykład dynamicznie piszącego języka, który pozwala określić typ.
Eric Wilson,

Odpowiedzi:

17

Istotą pisania statycznego jest zdolność do statycznego udowodnienia, że twój program jest poprawny pod względem typów (uwaga: nie do końca poprawna pod każdym względem). Jeśli masz system statyczny przez cały czas, możesz wykryć błędy typu przez większość czasu.

Jeśli masz tylko częściowe informacje o typie, możesz sprawdzić tylko małe fragmenty wykresu połączeń, na których informacje o typie są kompletne. Ale poświęciłeś czas i wysiłek na określenie informacji o typie niekompletnych części, które nie mogą pomóc, ale mogą dać fałszywe poczucie bezpieczeństwa.

Aby wyrazić informacje o typie, potrzebujesz części języka, która nie może być zbyt prosta. Wkrótce przekonasz się, że takie informacje intto za mało; będziesz potrzebować czegoś takiego List<Pair<Int, String>>, a następnie typów parametrycznych itp. Może to być dość mylące, nawet w dość prostym przypadku Javy.

Następnie musisz obsługiwać te informacje podczas fazy tłumaczenia i fazy wykonania, ponieważ głupotą jest sprawdzanie tylko błędów statycznych; użytkownik spodziewa się, że ograniczenia typu zawsze będą obowiązywać, jeśli w ogóle zostaną określone. Dynamiczne języki nie są zbyt szybkie, a takie kontrole jeszcze bardziej spowolnią wydajność. Język statyczny może z trudem sprawdzać typy, ponieważ robi to tylko raz; dynamiczny język nie może.

Teraz wyobraź sobie, że dodajesz i utrzymujesz to wszystko, aby ludzie czasami opcjonalnie korzystali z tych funkcji, wykrywając tylko niewielką część błędów typu. Nie sądzę, żeby było warto.

Istotą dynamicznych języków jest posiadanie bardzo małego i bardzo plastycznego frameworka, w którym można łatwo robić rzeczy, które są o wiele bardziej zaangażowane, gdy wykonywane są w języku statycznym: różne formy łatania małp, które są używane do metaprogramowania, kpiny i testowanie, dynamiczne zastępowanie kodu itp. Smalltalk i Lisp, oba bardzo dynamiczne, doprowadziły go do takiej skrajności, że wysyłały obrazy środowiska zamiast budować ze źródła. Ale jeśli chcesz się upewnić, że poszczególne ścieżki danych są bezpieczne dla typu, dodaj asercje i napisz więcej testów jednostkowych.

9000
źródło
1
+1, chociaż testy mogą wykazać, że błędy nie występują w określonych sytuacjach. Są słabym zamiennikiem dowodu, że błędy (typu) są niemożliwe.
Ingo
1
@Ingo: na pewno. Ale dynamiczne języki świetnie nadają się do majsterkowania i szybkiego prototypowania, gdzie bardzo szybko wyrażasz stosunkowo proste pomysły. Jeśli potrzebujesz kuloodpornego kodu produkcyjnego, możesz później przejść do języka statycznego, po wyodrębnieniu niektórych stabilnych podstawowych komponentów.
9000
1
@ 9000, nie wątpię, że są świetne. Chciałem tylko zaznaczyć, że pisanie 3 lub 4 lamalnych testów nie jest i nie może zapewnić bezpieczeństwa typu .
Ingo
2
@ 9000, Prawda, a zła wiadomość jest taka, że ​​nawet wtedy jest to praktycznie niemożliwe. Nawet kod Haskell lub Agda opiera się na założeniach, takich jak na przykład, że biblioteka używana w środowisku wykonawczym jest poprawna. To powiedziawszy, w projekcie z około 1000 LOC rozproszonymi w kilkudziesięciu plikach kodu źródłowego, jest tak fajnie, gdy możesz coś zmienić i wiesz, że kompilator wskaże każdą linię, w której zmiana ma wpływ.
Ingo
4
Źle napisane testy nie zastępują statycznego sprawdzania typu: są gorsze. Dobrze napisane testy również nie zastępują statycznego sprawdzania typu: są lepsze.
Rein Henrichs
8

W większości dynamicznych języków można przynajmniej dynamicznie przetestować typ obiektu lub wartości.

Istnieją również statyczne wnioskowania, kontrolery i / lub moduły egzekwujące dla niektórych dynamicznych języków: np

A Perl 6 będzie obsługiwał opcjonalny system pisania ze statycznym pisaniem.


Ale wydaje mi się, że sedno jest takie, że wiele osób używa dynamicznie języków, ponieważ są one dynamicznie pisane, a dla nich opcjonalne pisanie statyczne jest bardzo „huczące”. I wiele innych osób korzysta z nich, ponieważ są „łatwe w użyciu dla programistów”, głównie w wyniku dynamicznego pisania wybaczającego natury. Dla nich opcjonalne pisanie jest czymś, czego albo nie zrozumieją, albo nie będą mieli kłopotów z użyciem.

Jeśli jesteś cyniczny, możesz powiedzieć, że opcjonalne pisanie statyczne oferuje najgorsze z obu światów. Zealot typu statycznego nie zapobiega wszystkim awariom typu dynamicznego. Dla fanów typu dynamicznego nadal jest to prosta kurtka ... choć paski nie są mocno napięte.

Stephen C.
źródło
2
Należy zauważyć, że w większości przypadków samodzielne sprawdzanie typów jest źle oceniane przez większość społeczności. Używaj polimorfizmu (w szczególności „typowania kaczego”), mając do czynienia z hierachiami obiektowymi, w miarę możliwości / rozsądnie używaj oczekiwanego typu. Pozostaje kilka przypadków, w których po prostu nie ma sensu zezwalanie na dowolny typ, ale w wielu językach i tak dostajesz wyjątek w większości tych przypadków, więc sprawdzanie typu jest rzadko przydatne.
4
„ludzie zazwyczaj używają języków dynamicznych, ponieważ są dynamicznie wpisywani” : JavaScript jest używany, ponieważ jest to jedyny język obsługiwany przez większość przeglądarek. PHP jest używane, ponieważ jest popularne.
Arseni Mourzenko,
2

JavaScript planował dołączyć opcjonalne pisanie statyczne i wydaje się, że wiele dojrzałych języków dynamicznych zmierza w tym kierunku -

Powodem jest to, że kiedy pierwszy raz kodujesz, chcesz być szybki i dynamicznie pisać. Gdy kod jest już solidny, działa i ma wiele zastosowań (r), chcesz zablokować projekt, aby zmniejszyć liczbę błędów. (jest to korzystne zarówno dla użytkowników, jak i programistów, ponieważ ci pierwsi otrzymają kontrolę błędów podczas swoich wywołań, a drudzy nie spowodują przypadkowego uszkodzenia.

Ma to dla mnie sens, ponieważ zwykle stwierdzam, że na początku projektu jest za dużo sprawdzania typu, za mało na koniec jego życia, bez względu na to, jakiego języka używam;).

Macke
źródło
Nie wiem o tych planach włączenia opcjonalnego pisania statycznego w JavaScript; ale mam nadzieję, że nie były tak okropne jak w ActiveScript. najgorszy zarówno JavaScript, jak i Java.
Javier
Zostało zaplanowane dla JS 4 (lub ECMAscript 4), ale ta wersja została odrzucona z powodu kontrowersji. Jestem pewien, że coś podobnego pojawi się w przyszłości, w jakimś języku. (W Pythonie możesz to zrobić z dekoratorami, przy okazji.)
Macke
1
Dekoratorzy dodają dynamiczne sprawdzanie typu, rodzaj twierdzeń. Nie można uzyskać kompleksowego sprawdzania statycznego typu w Pythonie, niezależnie od tego, jak bardzo się starasz, ze względu na ekstremalną dynamikę języka.
9000
@ 9000: To prawda. Jednak nie sądzę, aby dynamiczne sprawdzanie typów było złe (ale wolałbym porównywanie typu kaczego ala JS4), szczególnie w połączeniu z testami jednostkowymi, a dekoratory typowania mogłyby być bardziej przydatne w obsłudze IDE / sprawdzania kłaczków, jeśli znormalizowany.
Macke,
oczywiście nie jest źle! To nie jest kwestia moralności. W pewnym momencie typ musi zostać „sprawdzony” w taki czy inny sposób. Jeśli napiszesz * ((podwójne *) 0x98765E) w C, procesor to zrobi i sprawdzi, czy 0x98765E rzeczywiście jest wskaźnikiem do podwójnego.
Ingo
2

Obiekty Pythona zrobić mieć typ.

Typ określa się podczas tworzenia obiektu.

Jednocześnie wygodniej byłoby mieć możliwość określenia w niektórych przypadkach typu zmiennej w języku dynamicznie wpisywanym, zamiast ręcznego sprawdzania typu.

W rzeczywistości ręczne sprawdzanie typu w Pythonie jest prawie zawsze stratą czasu i kodu.

Pisanie kodu sprawdzającego typ w Pythonie jest po prostu złą praktyką.

Jeśli jakiś złośliwy socjopata zastosuje niewłaściwy typ, zwykłe metody Pythona zgłoszą zwykły wyjątek, gdy typ nie będzie odpowiedni.

Nie piszesz kodu, twój program nadal zawiesza się z TypeError.

Są bardzo rzadkie przypadki, w których należy określić typ w czasie wykonywania.

Dlaczego są takie ograniczenia?

Ponieważ nie jest to „ograniczenie”, pytanie nie jest prawdziwym pytaniem.

S.Lott
źródło
2
„Obiekty Python mają typ.” - o naprawdę? Podobnie jak obiekty perla, obiekty PHP i każdy inny element danych na świecie. Różnica między pisaniem statycznym a dynamicznym występuje tylko wtedy, gdy typ ma zostać sprawdzony, tj. Gdy pojawią się błędy typu. Jeśli pojawiają się jako błędy kompilatora, oznacza to pisanie statyczne, jeśli pojawiają się jako błędy środowiska wykonawczego, są dynamiczne.
Ingo
@Ingo: Dzięki za wyjaśnienie. Problem polega na tym, że obiekty C ++ i Java mogą być rzutowane z jednego typu na drugi, co powoduje, że typ obiektu jest nieco mętny, a zatem „sprawdzanie typu” w tych kompilatorach jest również nieco mętne. Gdzie sprawdzanie typu Pythona - nawet jeśli odbywa się w czasie wykonywania - jest znacznie mniej mętne. Pytanie to przypomina też, że dynamicznie pisane języki nie mają typów. Dobra wiadomość jest taka, że ​​nie popełnia tego powszechnego błędu.
S.Lott,
1
Masz rację, rzutowania tekstu (w przeciwieństwie do konwersji typów, tj. ((Podwójne) 42)) odwracają pisanie statyczne. Są potrzebne, gdy system typów nie jest wystarczająco wydajny. Przed Javą 5 Java nie miała żadnych parmeteryzowanych typów, wtedy nie można było żyć bez rzutów typu. Dzisiaj jest znacznie lepiej, ale w systemie typów nadal brakuje typów o wyższej klasie, nie mówiąc już o wyższym rankingu polimorfizmu. Myślę, że jest całkiem możliwe, że dynamicznie pisane języki cieszą się tyloma zwolennikami właśnie dlatego, że uwalniają jeden ze zbyt wąskich systemów typów.
Ingo
2

Przez większość czasu nie musisz, a przynajmniej nie na poziomie szczegółowości, który sugerujesz. W PHP operatory, których używasz, doskonale wyjaśniają, czego oczekujesz od argumentów; jest to trochę niedopatrzenie projektowe, ale PHP wyrzuci twoje wartości, jeśli to w ogóle możliwe, nawet jeśli przekażesz tablicę do operacji, która oczekuje łańcucha, a ponieważ rzutowanie nie zawsze jest znaczące, czasami otrzymujesz dziwne wyniki ( i właśnie tam przydatne kontrole typu ). Poza tym nie ma znaczenia, czy dodasz liczby całkowite 1i 5lub łańcuchy "1"i "5"- sam fakt, że używasz+operator sygnalizuje PHP, że chcesz traktować argumenty jak liczby, a PHP będzie posłuszny. Ciekawą sytuacją jest otrzymywanie wyników zapytań z MySQL: Wiele wartości liczbowych jest po prostu zwracanych jako ciągi, ale nie zauważysz, ponieważ PHP rzuca je za ciebie, ilekroć traktujesz je jako liczby.

Python jest nieco bardziej rygorystyczny w swoich typach, ale w przeciwieństwie do PHP, Python od początku miał wyjątki i konsekwentnie go używa. Paradygmat „łatwiej prosić o wybaczenie niż pozwolenie” sugeruje po prostu wykonanie operacji bez sprawdzania typu i poleganie na wyjątku zgłaszanym, gdy typy nie mają sensu. Jedynym minusem tego, o czym myślę, jest to, że czasami zauważysz, że gdzieś typ nie pasuje do tego, czego się spodziewasz, ale znalezienie przyczyny może być żmudne.

I jest jeszcze jeden powód do rozważenia: języki dynamiczne nie mają etapu kompilacji. Nawet jeśli masz ograniczenia typu, mogą one uruchamiać tylko w czasie wykonywania, po prostu dlatego, że nie ma czasu kompilacji . Jeśli mimo to kontrole prowadzą do błędów w środowisku wykonawczym, znacznie łatwiej jest odpowiednio je modelować: jako jawne kontrole (takie jak is_XXX()w PHP lub typeofw javascript) lub poprzez zgłaszanie wyjątków (tak jak Python). Funkcjonalnie masz ten sam efekt (błąd jest sygnalizowany w czasie wykonywania, gdy sprawdzenie typu nie powiedzie się), ale lepiej integruje się z resztą semantyki języka. Po prostu nie ma sensu traktować błędów typu zasadniczo różniących się od innych błędów czasu wykonywania w języku dynamicznym.

tdammers
źródło
0

Haskell może Cię zainteresować - jego system typów odczytuje typy z kodu i możesz także określać typy.

daven11
źródło
5
Haskell to świetny język. Jest to w pewnym sensie przeciwieństwo języków dynamicznych: spędzasz dużo czasu na opisywaniu typów, a zwykle po ustaleniu typów program działa :)
9000
@ 9000: Rzeczywiście. Po skompilowaniu zwykle działa. :)
Macke
@Macke - dla różnych wartości zwykle , oczywiście. :-) Dla mnie największą zaletą systemu typów i paradygmatu funkcjonalnego jest, jak zauważyłem gdzie indziej, to, że nie trzeba się martwić, czy zmiana gdzieś po cichu wpływa na jakiś zależny kod gdzie indziej - kompilator wskaże błędy typu a stan zmienny po prostu nie istnieje.
Ingo
0

Jak wspomniano w innych odpowiedziach, istnieją dwa podejścia do pisania przy wdrażaniu języka programowania.

  1. Poproś programistę, aby powiedział ci, czego używają wszystkie zmienne i funkcje dla typów. Idealnie sprawdza się również, czy specyfikacje typu są dokładne. Następnie, ponieważ wiesz, jaki rodzaj rzeczy będzie w każdym miejscu, możesz napisać kod, który zakłada, że ​​odpowiednia rzecz tam będzie i używa dowolnej struktury danych, której używasz do bezpośredniego wdrożenia tego typu.
  2. Dołącz wskaźnik typu do wartości, które będą przechowywane w zmiennych i przekazywane do funkcji i zwracane z nich. Oznacza to, że programista nie będzie musiał określać żadnych typów zmiennych ani funkcji, ponieważ typy faktycznie należą do obiektów, do których odnoszą się zmienne i funkcje.

Oba podejścia są prawidłowe, a ich zastosowanie zależy częściowo od względów technicznych, takich jak wydajność, a częściowo od przyczyn politycznych, takich jak docelowy rynek dla danego języka.

Larry Coleman
źródło
0

Po pierwsze, dynamiczne języki są tworzone głównie dla łatwości użytkowania. Jak już wspomniałeś, naprawdę dobrze jest automatycznie przeprowadzić konwersję typu i zapewnić nam mniejszy narzut. Ale jednocześnie brakuje mu problemów z wydajnością.

Możesz trzymać się dynamicznych języków, w przypadku, gdy nie martwisz się o wydajność. Powiedzmy na przykład, że JavaScript działa wolniej, gdy musi wykonać konwersję wielu typów w twoim programie, ale pomaga zmniejszyć liczbę wierszy w kodzie.

I należy wspomnieć, że istnieją inne dynamiczne języki, które pozwalają programiście określić typ. Na przykład Groovy jest jednym ze słynnych języków dynamicznych działających w JVM. I to było bardzo znane nawet w ostatnich dniach. Pamiętaj, że wydajność Groovy jest taka sama jak Java.

Mam nadzieję, że to ci pomoże.

Mrówki
źródło
-1

Po prostu nie ma sensu tego robić.

Dlaczego?

Ponieważ system typów DTL jest dokładnie taki, że typów nie można określić w czasie kompilacji. Dlatego kompilator nie mógł nawet sprawdzić, czy określony typ ma sens.

Ingo
źródło
1
Dlaczego? Idealnym rozwiązaniem jest podpowiedzenie kompilatorowi, jakich typów się spodziewać. Nie będzie to sprzeczne z żadnymi ograniczeniami systemu typu.
SK-logic
1
Logika SK: jeśli dynamiczne wpisanie oznacza, że ​​każda funkcja / metoda / operacja przyjmuje obiekty typu „Dynamiczny”, „Dowolny” lub cokolwiek i zwraca „Dynamiczny”, „Dowolny” cokolwiek, generalnie nie ma sposobu, aby powiedzieć, że pewna wartość na przykład zawsze będzie liczbą całkowitą. Dlatego kod środowiska wykonawczego musi i tak sprawdzać, czy nie są liczbami całkowitymi, tak jakby typ był „Dynamiczny”. Tak właśnie działa system typu statycznego: umożliwia udowodnienie, że określony zwrot zmiennej, pola lub metody zawsze będzie określonego typu.
Ingo
@ Ingo, nie, jest sposób. Zobacz na przykład, jak jest implementowany we Common Lisp. Jest to szczególnie przydatne w przypadku zmiennych lokalnych - można znacznie zwiększyć wydajność, wprowadzając wszystkie wskazówki dotyczące pisania.
SK-logic
@ SK-logic: Być może możesz mi powiedzieć, kiedy i jak wykrywane są błędy w CL? W każdym razie @MainMa całkiem ładnie podsumował status quo w swoim pytaniu: To właśnie można się spodziewać po „czysto” dynamicznych językach.
Ingo
@Ingo, co sprawia, że ​​uważasz, że typy są przydatne tylko do statycznego potwierdzania poprawności? Nie jest to prawdą nawet w przypadku języków takich jak C, w których rzutowanie typu jest niezaznaczone. Adnotacje typu w językach dynamicznych są najczęściej przydatne jako wskazówki kompilatora, które poprawiają wydajność lub określają konkretną reprezentację liczbową. Zgadzam się, że w większości przypadków adnotacje nie powinny zmieniać semantyki kodu.
SK-logic
-1

Spójrz na Go, na powierzchni jest on statycznie wpisany, ale te typy mogą być interfejsami, które są zasadniczo dynamiczne.

dan_waterworth
źródło