Diversity w polskim IT
Tomasz Trębski
Tomasz TrębskiSenior Development Engineer @ Fujitsu Technology Solutions

Dlaczego Python i JavaScript bez typów to kiepski pomysł

Sprawdź, kiedy i dlaczego typowanie będzie ma sens, a kiedy wręcz przeciwnie.
5.06.20218 min
Dlaczego Python i JavaScript bez typów to kiepski pomysł

Nowe biblioteki i szkielety programistyczne (ang. framework) wyrastają jak grzyby po deszczu. Jedne są dobre, inne jeszcze lepsze, a niektóre mają za zadanie zwojować ten świat. Koniec końców łączy je to, że zostały napisane w jakimś, a na potrzeby tego artykułu, wyższym języku programowania.

Zatrzymajmy się na chwilę i porozmawiajmy o językach programowania. Zgodnie z definicją, język programowania jest językiem, podobnie jak język polski, który charakteryzuje się pewnymi cechami. Posiada on więc alfabet - składnie oraz reguły ortografii, czyli semantykę. Ale to, co nas najbardziej tutaj interesuje, to jak ów hipotetyczny język odnosi się do typów danych. 

O typach słów kilka

Ahh... typy danych, no wiecie, string, integer czy też list<boolean>. Właściwie każdy język wyższego poziomu, Java, Elm, Python lub JavaScript; co by wymienić kilka, posiada takie typy danych. Jednak każdy z nich różnie do typów danych podchodzi. Wybór tych 4 języków nie jest przypadkowy tak samo jak i kolejność w jakiej zostały wymienione. W Java znajdziemy statyczny model danych, gdzie bliżej nieokreślony typ zwykle podąża za danymi. Taki model danych nazywać będziemy typowaniem statycznym. I wszystko byłoby pięknie, gdyby nie Object[] mixedTypes = {1, "1", true}

Jeśli spojrzeć na Elm, poniekąd pochodną Haskell, przejdziemy płynnym krokiem do języka silnie typowanego, zupełnie jak stara dobra Java, z tą różnicą, że w Elm większość typów będzie odgadnięta przez kompilator na podstawie wartości przypisanej do zmiennej, a kolekcję będę musiały zawierać ZAWSZE ten sam danych. Nie dość, że wygodniej, to jeszcze bezpieczniej, nieprawdaż?

A jak sprawy mają się w Pythonie? Python jest językiem typowanym dynamicznie, gdzie jedna etykieta x może przyjąć N różnych typów, jeśli zajdzie taka potrzeba. W dodatku typowanie jest bezpieczne, czyli nie ma domyślnej możliwości wykonania kodu typu '1' + 1. Niestety, kod dalej pozostaje nietypowany, co utrudnia jego zrozumienia. Sytuacja jednak w Pythonie się zmieniła, i jeśli mogę być tak śmiały; uważam, że zmieniła się na lepsze. Od jakiegoś czasu, możliwe jest deklarowanie typów danych, i choć te typy giną, w większości na etapie działania programu, specjalne narzędzie takie jak mypy potrafią zweryfikować aplikację i zgłosić naruszenia typów. Niestety, nawet z mypy,  kod taki jak: a: typing.List[typing.Any] = [1, "1", None], wciąż jest możliwy.

A co z JavaScriptem? Tutaj, właściwie, po staremu. Sytuacja wygląda wciąż podobnie jak w Pythonie - jest to język dynamicznie typowany, gdzie sytuację ratuje jedynie operator typeof, aczkolwiek typeof null === "object", czy też typeof (1 + 2 + " 3" + " " + 3) === "string", budzi lekki niepokój, a potencjalnych kandydatów na stanowisku przyprawia o dreszcze. 

Mimo tych wszystkich niedogodności, pomijając to, jak dany język jest strict, jedno jest pewne - typy danych ratują życie programistom. Może nie to fizyczne, dane nam przez rodziców, ale z pewnością psychiczne. 

Tym wstępem, przechodzimy do sedna tego artykułu. Czy pisanie kodu w językach programowania, takich jak Python czy Javascript, bez użycia jawnych typów danych (oczywiście w przypadku Javascript jest to możliwe do osiągnięcia jedynie poprzez przejścia na TypeScript lub używanie biblioteki flow), jest złym pomysłem.

Typy nie zawsze spod ciemnej gwiazdy...

Gdy zaczynałem przygodę z programowaniem, odpowiedziałbym zdecydowanym głosem, nie przyjmującym do wiadomości żadnego sprzeciwu, że TAK. Ciągle musieć pamiętać o deklarowaniu tych typów; zgroza i strata czasu. Nie zapominając jeszcze o tej nieskończenie długiej liście projektów, które napisano w.w. językach i które odniosły przecież sukces.

A dzisiaj? Dzisiaj nie wyobrażam sobie życia bez typów danych. Czemu? Żeby odpowiedzieć na to pytanie, zapraszam do bardzo krótkiej gry. Spójrz niżej. Kilka przykładów kodu w różnych językach. Twoim zadaniem jest odpowiedzieć jaki typ danych znajduje się na danej pozycji w kolekcji. Elementy oczywiście liczymy od zera. Zanotuj proszę czas, jaki był Ci potrzebny, aby odpowiedzieć na każde z pytań.

I jak poszło? Jeśli najkrótszy czas zanotowałeś dla przykładów 2, 4, 5 oraz 7, to zrozumiałeś pierwszą i najważniejszą zaletę jawnego typowania danych, a przy okazji zobaczyłeś, że nawet języki, gdzie typy istnieją, jeśli użyte niepoprawnie, tracą cały swój blask.

Typy i ich zalety

Czytelność

Jeśli czytasz ten artykuł, prawdopodobnie czasy HelloWorld, masz już za sobą i projekty, którym zajmujesz się obecnie, mają wskaźnik LOC na poziomie przynajmniej 2 tysięcy. A jeśli nie, to mam nadzieję, że zrozumiesz, jak trudno w takim kodzie się odnaleźć. I ta reguła nie dotyczy jedynie początkujących programistów. Tym samym kod silnie typowany, a być może zwłaszcza taki, gdzie typ danych podąża za faktycznymi wartościami do każdego zakątka, jest na wagę złota. 

Możliwość zrozumienia dowolnego kawałka kodu okazuje się dużo łatwiejsza, jeśli nie musimy zaprzątać sobie głowy odpowiadaniem na pytania w stylu: A ta funkcja, ta tutaj, to właściwie czego potrzebuje?

Darmowa dokumentacja

Skoro nie musimy zaprzątać sobie głowy, to tak naprawdę w naszym projekcie mamy teraz gotową dokumentację. Jeśli pisałeś kiedyś dokumentację do kodu Python, z pewnością kojarzysz taki kawałek dokumentacji:

:param foo: just a foo
:type foo: str


Kluczowa w tym przykładzie jest konieczność podania typu parametru foo. Okazuje się, że używając typowania, możemy ograniczyć nasz wysiłek w pisaniu dokumentacji do każdej jednej funkcji. Oczywiście nie jest to warunek wystarczający. Samo-dokumentujący się kod, to nie tylko zadeklarowane typy danych. Równie poważnie należy traktować nazwy zmiennych, nazwy funkcji lub stałych

Bezpieczeństwo

W poprzednich paragrafach, podałem proste przykłady przemawiające za podniesieniem czytelności oraz percepcji typowane kodu. Nie bez powodu, ponieważ łatwiejszy w zrozumieniu kod, to kod bezpieczniejszy. I co ciekawsze, ta zasada obowiązuje młodych adeptów sztuki programowania tak samo jak starszych stażem kolegów. Dla tych pierwszych, typy danych utożsamiają dobrego przyjaciela, gotowe wyciągnąć dłoń i pomóc w chwili zwątpienia. W przypadku tych drugich, okażą się nieocenione w przemierzaniu otchłani ogromne projektu. W szczególności, w takich projektach możliwe są sytuacje, gdzie dwie funkcje mają zbliżone do siebie nazwy, ale radykalnie różne argumenty. Proste przeoczenie i RuntimeException gotowy.

Runtime safety

Jeżeli jesteśmy na etapie działania aplikacji, wróćmy na chwilę do Pythona (bez mypy) lub JavaScriptu i wyobrażmy sobie następującą sytuację (zapisaną psedukodem):

function foo() { return 0 if Math.rand_int() % 2 1 };


Jak widać, jest to funkcja która ewidentnie zwraca 1 jeśli losowa liczba dzieli się przez 2 bez zera, a w przypadku odwrotnym zwraca 0. Idąc dalej, niech foo będzie funkcją użytą w 50 miejscach w projekcie i ktoś dokonuje zmiany w tejże funkcji.

function foo() { return 0 if Math.rand_int() % 2 else None };


Jeśli liczba nie dzieli się przez 2 bez zera, zwracamy None lub undefined. I teraz najlepsza część. Nawet z code-coverage na godnym polecania poziomie może zdarzyć się, że przynajmniej jedno wywołanie zostało przeoczone. I katastrofa gotowa, klient gotuje się ze złości, a my, programiści, szukamy na szybko, co poszło nie tak.

Oczywiście, ktoś powie, że przecież będziemy widzieli traceback, który zaprowadzi nas w końcu do miejsca, gdzie coś poszło nie tak. Prawda! A co jeśli w kodzie istnieje miejsce, które dosłownie połyka ten wyjątek? Ewentualnie zła wartość pochodzi z jakiegoś wątku, innego niż ten, w którym pojawił się wyjątek? (PS. w Pythonie ta sytuacja nie spowoduje większego problemu tak długo, jak w kodzie znajdują się adekwatnie napisane wyrażenia warunkowe).

Brak przykrych niespodzianek

Oczywiście poziom bezpieczeństwa i naszego spokojnego weekendu zależy nie tyle co od doświadczenia, ponieważ przeoczyć coś może każdy, a od mechanizmów bezpieczeństwa dostępnych w języku. Języki silnie typowane i kompilowane, takie jak Java lub Elm, będą miała przewagę nad Python + mypy, Typescriptem i Javascriptem. Tutaj oczywistym zwycięzcą jest Elm. Spójrzmy chwilę na poniższych kod, który generuje 100 elementową tablicę i wyciąga z niej pierwszy element.

List.range 1 100 |> List.head


Co będzie wynikiem? 1, 0, nic, a może wyjątek? Nic z tych rzeczy! Elm w przypadku takiej operacji zwróci nam dane typu Maybe, która oznacza dokładnie to, co maybe oznacza po angielsku. Najlepsze w tym wszystkim jest to, że język nie pozwoli Ci tak po prostu umieścić wartości w kodzie strony WWW. Kod nie będzie się kompilował tak długo jak ktoś nie wpadnie na pomysł co zrobić, jeżeli tablica okazała się pusta i pierwszy element tejże tablicy nie istnieje.

Rozwiązania na sterydach

Tym samym dochodzimy do ostatniego argumentu za używaniem typowanych języków. Słyszałeś o coding by type? Jeśli nie, to zapoznaj się z tematem, a tymczasem powiem tylko tyle. Masz do rozwiązania problem. Rozumiesz ten problem i wiesz, co nie powinno się zdarzyć. Wyobraź sobie teraz, że w Twoich rękach znajduje się narzędzie, które pozwala Ci opisać ten problem, korzystając tylko z typów danych. Intrygujące, nieprawdaż? 

Niestety, tego typu podejście do programowania jest tym trudniejsze, im mniejszy jest nacisk na bezpieczeństwo danych, tudzież ich typy, którym cechuje się dany język. Tym samym, jest to niemożliwe do uzyskania w językach, gdzie typów danych nie ma. Trudne i wymagające dyscypliny w językach takich jak Python. Najłatwiej tematem tym się zainteresować podczas nauki Elm, Haskell lub F#, czyli języków funkcyjnych, z których każdy charakteryzuje się potężnym systemem typów danych, zdolnym do wykrycia niepoprawnego użycia funkcji, na dowolnym poziomie zagnieżdżenia.

Podsumowanie

Czy więc typy, mimo oczywistych zalet przedstawionych, są tym na co programiści powinni zwracać uwagę? I tak...i nie. Wszystko, podobnie jak wszędzie, zależy od tego, co chcemy uzyskać. Dla prostych aplikacji, często zawierających się w jednym pliku, typowanie nie przyniesie pewnie pożądanych rezultatów. Tego typu programy mają najczęściej jasno określony cel i łatwo zrozumieć cel pojedynczej funkcji.

Z drugiej strony, duże programy czy też biblioteki, im większe, tym trudniejsze do rozszerzenia. Nazw przywoływać nie warto, ale moja ostatnio próba kontrybucji do jednej z bibliotek implementujących standard OpenAPI... cóż, nie wiem czy chcę dalej próbować zrozumieć, co tak naprawdę jest potrzebne w tym konkretnym miejscu, żeby implementacja się zgadzała.

Tym samym, dodam jeszcze, że osobiście mypy, jako że pracuję często z Python, mam włączony domyślnie w edytorze Neovim i bardzo sobie takie życie chwalę. Czuję się bezpiecznie, wiedząc, że na straży integralności mojej aplikacji stoją typy danych. Niech Cię jednak to nie zmyli i nie zapominaj o testach. Jedynie silnie typowane języki funkcyjne, takie jak Elm czy Haskell, potrafią zapewnić integralność aplikacji jedynie poprzez typ danych jakie niesie ze sobą zmienna. W jednym z moich poprzednich projektów, nie napisaliśmy żadnego testu jednostkowe dla aplikacji webowej w Elm. Mimo to, aplikacja działa zgodnie z biznesowymi założenia i działa do tej pory!

<p>Loading...</p>