1 września 2017

Ciemna Ścieżka

Przez ostatnich kilka miesięcy babrałem się w dwóch nowych językach. Swift i Kotlin. Te języki mają wiele podobieństw. Te podobieństwa są tak wyraźne, że zastanawiam się, czy to nie nowy trend w swoistym językowym biciu piany. Jeżeli tak, to jest to ciemna ścieżka.

Oba języki zawierają w sobie pewne właściwości funkcyjne. Na przykład, oba mają lambdy. Ogólnie, to dobrze. Im więcej wiemy o językach funkcyjnych, tym lepiej. Chociaż te języki są bardzo daleko od nazwania ich prawdziwie funkcyjnymi językami, to każdy krok w tę stronę to dobry krok.

Poniższy tekst jest luźnym tłumaczeniem wpisu z bloga Roberta Cecila Wujka Boba Martina stąd:
Proszę o komentarze, gdyby ta luźność była zbyt daleko posunięta.


Mam problem z tym, że oba te języki mocno zaakcentowały statyczne typowanie. W obu przypadkach widać intencje zamknięcia każdej jednej luki w temacie typów ich macierzystych języków. W przypadku Swifta, macierzystym językiem jest dziwaczna beztypowa hybryda C i Smalltalka zwana Objective-C; a więc nacisk na typowanie może być jakkolwiek zrozumiały. W przypadku Kotlina rodzicem jest już raczej mocno typowana Java.

A teraz nie chcę, żebyś pomyślał, że mam coś przeciwko statycznie typowanym językom. Nie mam. Są oczywiście wady i zalety obu dynamicznie i statycznie typowanych języków; ja osobiście z powodzeniem używam obu ich rodzajów. Osobiście preferuję odrobinę bardziej dynamiczne typowanie; i dlatego piszę dużo w Clojure. Z drugiej strony, prawdopodobnie pisze więcej kodu w Javie niż w Clojure. Możesz więc nazwać mnie dwu-typowym. Chodzę po dwóch stronach ulicy - że tak powiem.

To nie tak, że niepokoi mnie to, że Swift czy Kotlin są statycznie typowane jako takie. Raczej coś jest w głębi tego typowania statycznego.

Nie nazwałbym Javy językiem silnie zafiksowanym jeżeli chodzi o statycznie typowanie. W Javie możesz stworzyć struktury, które grzecznie podążają za zasadami typów; ale możesz też naruszyć te zasady kiedy tylko chcesz lub potrzebujesz. Język będzie trochę narzekał, jeśli to zrobisz; i rzuci Ci pod nogi trochę kłód, ale nie na tyle, żeby to była przeszkoda nie do przejścia.

Z drugiej strony Swift i Kotlin, są zupełnie nieelastyczne jeżeli chodzi o ich zasady typowania. Na przykład Swift, gdy zadeklarujesz funkcję, jako mogącą rzucić wyjątek, wtedy, na Boga, każde wywołanie tej funkcji, całą drogę na górę stosu, musi być ozdobione blokiem do-try lub try!, lub try?. Nie ma w tym języku innej możliwości, aby po cichu wyrzucić wyjątek aż na górę do najwyższego poziomu bez układania kostki po kostce całego utwardzonego traktu przez całe drzewo wywołań (Możesz obejrzeć mnie i Justina zmagającego się z tym w naszych filmach video z Warsztatu Tworzenia Aplikacji Mobilnej).

Teraz, może sobie pomyślisz, że to coś dobrego. Może myślisz, że będzie dużo bugów w systemach spowodowanych przez nieokiełznane wyjątki. Może pomyślisz, że wyjątki, które nie są eskortowane, krok po kroku, na górę stosu wywołań są ryzykowne i błędogenne. I, oczywiście, masz rację. Niezadeklarowane i niezarządzane wyjątki są bardzo ryzykowne.

Pytanie jest takie: Kto ma za zadanie zarządzać tym ryzykiem? Czy to zadanie języka? Czy to zadanie programisty?

W Kotlin nie podziedziczysz z klasy, nie przeciążysz funkcji, jeżeli nie ozdobisz tej klasy czy funkcji w open. Także nie przeciążysz funkcji, no chyba że ustroisz przeciążającą funkcję w override. Jeżeli zaniedbasz upiększania klasy w open, język nie pozwoli ci podziedziczyć po niej.

Teraz, może sobie pomyślisz, że to coś dobrego. Być może uważasz, że dziedziczenie i przeciążanie różnych hierarchii, którym pozwala się rosnąć bez ograniczeń to źródło błędów i ryzyka. Być może myślisz, że możemy wyeliminować całe klasy błędów zmuszając programistów aby jawnie deklarowali swoje klasy jako open. I możesz mieć rację. Przeciążanie i dziedziczenie ryzykowne. Wiele rzeczy może pójść źle w przeciążonej metodzie podziedziczonej klasy.

Pytanie jest takie: Kto ma za zadanie zarządzać tym ryzykiem? Czy to zadanie języka? Czy to zadanie programisty?

Oba Swift i Kotlin zawierają w sobie pomysł typów nulowalnych. To, że zmienna może przyjmować null stało się typem tej zmiennej. Zmienna typu String nie może przyjmować null; może zawierać jedynie urzeczowiony String. Z drugiej strony, zmienna typu String? ma typ nulowalny i może przyjmować null.

Zasady języka wymagają tego, że w momencie użycia zmiennej nulowalnej musisz najpierw sprawdzić czy ta zmienna jest nullem. A więc jeżeli s jest Stringiem? wtedy var l = s.length() się nie skompiluje. Zamiast tego musisz napisać:
var l = s.length() ?: 0
lub
var l = if (s!=null) s.length() else 0

Teraz, może sobie pomyślisz, że to coś dobrego. Może już naoglądałeś się wystarczająco dużo NullPointerExcepszynsów w życiu. Może wiesz, bez cienia wątpliwości, że niesprawdzane nulle są sprawcami strat rzędu miliardów dolarów z powodu błędów w oprogramowaniu. (Rzeczywiście, dokumentacja do Kotlina nazywa NPEszyny “Bugami za Miliard Dolców”). I oczywiście masz rację. To bardzo ryzykowne mieć nulle szalejące po całym systemie bez żadnej kontroli.

Pytanie jest takie: Kto ma za zadanie zarządzać tymi nullami? Język? Czy programista?

Te języki przypominają mi przypadek małego Duńczyka wsadzającego palce do tamy. Każdorazowo, gdy pojawia się nowy rodzaj błędu, dodajemy funkcję języka zapobiegającą przed tego rodzaju błędami. I te języki angażują coraz więcej i więcej palców w dziurach w tamie. Problem jest taki, że w końcu skończą się palce u rąk i u nóg.



Ale zanim skończą się palce u rąk i u nóg, stworzymy sobie języki zawierające tuziny słów kluczowych, setki ograniczeń, pokręconą składnię i dokumentację, którą czyta się jak kodeks karny. W rzeczy samej, aby stać się ekspertem w tych językach musisz stać się językowym prawnikiem (termin został wymyślony podczas ery C++).

To jest niewłaściwa ścieżka!


Zapytaj sam siebie dlaczego próbujemy zatamować defekty poprzez funkcje w języku. Odpowiedź wydaje się oczywista. Próbujemy zatamować te defekty, bo te defekty zdarzają się zbyt często.

A teraz zapytaj sam siebie dlaczego te defekty zdarzają się zbyt często. Jeżeli Twoją odpowiedzią jest, że nasze języki nie zapobiegają temu, to ja mocno zachęcam do rzucenia Twojej programistycznej posady i nie zaprzątania sobie głowy programowaniem już nigdy więcej; ponieważ defekty nigdy nie są winą naszych języków. Defekty są winą programistów. To programiści  są tymi, którzy tworzą defekty - nie języki.

No i co więc powinni robić programiści aby zapobiec defektom? Zgaduj. Podpowiem ci. To czasownik. Zaczyna się na literkę “T”. Tak. Zgadłeś. TESTOWAĆ!

Testujesz czy Twój system nie produkuje niespodziewanych nulli. Testujesz czy Twój system obsługuje nulle na swoich punktach wejścia. Testujesz czy każdy wyjątek, który może wystrzelić jest gdzieś łapany.

Dlaczego te wszystkie języki zawierają te wszystkie funkcje? Ponieważ programiści nie testują swojego kodu. I ponieważ programiści nie testują swojego kodu, mamy teraz języki, które zmuszają nas do wpisywania słowa open na początku każdej klasy, z której chcemy podziedziczyć. Mamy teraz języki, które zmuszają nas żeby upiększać każdą funkcję, aż na górę drzewa wywołań w try!. Mamy teraz języki, które są tak ograniczające i tak nadmiernie sprecyzowane, że musisz zaprojektować cały system z góry zanim zaczniesz pisać jakikolwiek kod.

Rozważ: Jak mogę stwierdzić czy klasa jest open czy nie? Ja mogę stwierdzić, że gdzieś w dole drzewa wywołań ktoś może rzucać wyjątek? Jak dużo kodu muszę zmienić aż w końcu odkryję, że ktoś na górze drzewa wywołań koniecznie musi zwrócić nulla?

Wszystkie te ograniczenia, wymuszane przez te języki, przypuszczają, że programista ma idealną wiedzę o systemie; zanim ten system będzie napisany. Przypuszczają, że Ty wiesz, które klasy będą potrzebowały być open, a które nie. Przypuszczają, że Ty wiesz, które ścieżki wykonania rzucą wyjątkami, a które nie. Przypuszczają, że Ty wiesz, które funkcję zwrócą nulla, a które nie.

I przez te wszystkie przypuszczenia, języki te każą Cię jeśli się mylisz. Zmuszają Cię abyś z powrotem zmienił przeogromne połacie kodu, dodając try! lub ?: albo open całą drogę w górę stosu.

No więc jak uniknąć kary? Istnieją dwie drogi. Jedna, która działa i jedna, która nie działa. Ta, która nie działa to zaprojektować wszystko z góry przed kodowaniem. Ta, która działa i pozwala uniknąć kary to obejść wszystkie zabezpieczenia.

I deklarujesz wszystkie swoje klasy i wszystkie swoje funkcje jako open. Nigdy nie używasz wyjątków. I przyzwyczajasz się do używania wielu, wielu znaków ! żeby obejść sprawdzanie nulli i pozwolić NullPointerExcepszynsom szaleć po Twoim systemie.



Dlaczego reaktor jądrowy w Czernobylu zapalił się, roztopił, zniszczył małe miasto i pozostawił ogromny teren niezamieszkałym? Oni obeszli zabezpieczenia. Więc nie polegaj na zabezpieczeniach, aby zapobiec katastrofom. Zamiast tego, lepiej przyzwyczaj się do pisania wielu, wielu testów, nie ważne jakiego języka używasz!


Poniższy tekst jest luźnym tłumaczeniem wpisu z bloga Roberta Cecila Wujka Boba Martina stąd:
Proszę o komentarze, gdyby ta luźność była zbyt daleko posunięta.




Podstawy Programowania Funkcyjnego Epizod 3

Czy wszystkie Zasady Się Zmieniają? Kiedy tylko zaczynamy używać nowego paradygmatu , porównujemy z nim na...