piątek, 1 kwietnia 2016

Style programowania

Ostatnimi czasy języki funkcyjne zyskały bardzo na popularności. Widać także jak "dojrzałe" imperatywne języki programowania przybierają coraz to nowe formy dodając elementy funkcyjne (myślę o c++, php, javie). O tych drugich właśnie będzie traktował ten post. I nie mam zamiaru pisać dlaczego akurat tak się dzieje lecz jak to wykorzystać.

To co chcę napisać najłatwiej będzie przyswoić patrząc na przykłady. Zacznijmy więc od takiego problemu:

Mamy tablicę z trzema elementami. Niech będą to 1, 2, 3. W PHP wyglądałoby to tak:

$tablica = [1,2,3];

Chcemy każdy z tych elementów zwiększyć o jeden. Sprawa jest banalna (celowo nie użyłem żadnej pętli):

$tablica[0]++;
$tablica[1]++;
$tablica[2]++;

Widzimy gołym okiem, że jest to styl imperatywny. Kod czytamy z góry na dół, każda kolejna linia to kolejna instrukcja, która zostanie wykonana. Teraz spójrzmy jak możemy to napisać w jakimś języku funkcyjnym, np. w Haskellu:

arrayIncrement :: [Int] -> IO [Int]
arrayIncrement a = do
  a1 <- return $ a!!0 + 1
  a2 <- return $ a!!1 + 1
  a3 <- return $ a!!2 + 1
  return [a1, a2, a3]

Oczywiście jest to bardzo dziwny Haskell. No właśnie tylko dziwny jak co? Ten kod naśladuje to co napisałem wyżej w PHP. Pewnie, że da się napisać bardziej zbliżony kod ale to co chciałem pokazać to to, że pisanie w języku funkcyjnym nie oznacza wcale, że piszemy funkcyjnie. Jasne, że to wszystko są funkcje ale kod czyta się dokładnie tak samo jak kod imperatywny. Każda linia jest właściwie osobną instrukcją wykonywaną w zadanej kolejności. Pytanie tylko czy to coś złego (w końcu kod działa poprawnie). Nie ma się co oszukiwać, nawet w Haskellu operacje IO pomimo bycia monadą są pisane imperatywnie bo po prostu inaczej się nie da.

Wszyscy piszemy imperatywnie

Każdy program zawiera jakąś część imperatywną. Wszędzie tam gdzie ważna jest kolejność wykonywania akcji (czyli funkcji, które mają jakiś efekt, np. wypisanie czegoś na ekran) kod pisany jest w sposób imperatywny. Np. łącząc się do Postgresa zazwyczaj chcemy najpierw ustawić parametr search_path, a dopiero później wykonać zapytanie. Samo ustawienie nie zwraca nic, co byłoby potrzebne przy wykonywaniu zapytania. Nie można więc wydedukować co kiedy należy wykonać inaczej niż z kolejności określonej przez programistę.

Niemal każdy program zawiera także część funkcyjną i logiczną. Są to dwa główne paradygmaty programowania poza imperatywnym. Szkopuł w tym, że te części mogą być zapisane imperatywnie, a jak pokazałem wyżej, nie da się każdej imperatywnej części zapisać w pozostałych dwóch stylach. Większość programów z jakimi miałem do czynienia była pisana od początku do końca imperatywnie, mimo iż wiele by zyskały gdyby odpowiednie ich części zapisać w odpowiednich stylach.

Część funkcyjna

Czym jest ta część najłatwiej będzie wydedukować na podstawie definicji czystej funkcji (ang. pure function). Jest to funkcja pozbawiona efektów, której wynik zależy tylko od wartości przekazanych parametrów. Nie może ona, np. łączyć się z bazą danych ani wypisywać nic na ekran. Czy ona w ogóle może coś robić? Może przekształcać jedną wartość (lub wartości) w inną, A -> B.

Jest to moim zdaniem największa część większości programów. Dla przykładu, niemal wszędzie wykorzystuje się jakąś pochodną wzorca MVC, tzn. zawsze mamy jakiś model i jakiś widok, który go wyświetla. W aplikacjach webowych jest to najczęściej operacja typu: Model -> HTML, czyli dokładnie to o co nam chodzi. Przekształcenie jednego typu na drugi.

Jak najprościej zacząć pisać funkcyjnie? Miejsce, od którego łatwo zacząć już podałem. Natomiast technicznie to zależy od języka. Dla przykładu w takim PHP mamy do dyspozycji funkcje anonimowe (lambdy) oraz cały szereg funkcji zaczynających się od array_, np. array_map. Warto więc w tych miejscach zamienić kod wykorzystujący pętle for, while, foreach na array_map lub/i array_reduce. Taki kod zrobi się bardziej czytelny i łatwiej będzie go testować. Czytelny robi się dlatego, że nazwa funkcji array_map mówi już co będziemy tutaj robić, a mianowicie mapować każdy element jakiejś kolekcji na inny. Dla porównania sformułowanie foreach mówi nam tylko tyle, że będziemy robić coś z każdym elementem kolekcji. Właściwie to pomimo nazwy nawet nie wiadomo czy z każdym (jest wiele sposobów na wyjście z pętli) ale najgorsze jest to, że nie wiemy czy na każdy element chcemy wysłać głowicę jądrową czy tylko wyświetlić jego nazwę na ekranie.

Prostota testowania z kolei wynika z cech czystych funkcji. Jeśli funkcja zależy tylko od swoich argumentów to aby ją przetestować należy je dobrać i porównać otrzymany wynik z tym oczekiwanym. Nie potrzeba pisać żadnych mocków czy stubów.

Ważna jest też przewidywalność. Z array_map nie da się wyjść wcześniej (no chyba, że rzucimy wyjątek) co powoduje, że szukanie błędów jest o wiele prostsze. No bo jak inaczej nazwać badanie ciała pętli, w której występują instrukcje typu break lub continue jak nie drogą przez mękę?

Część logiczna

Ciekaw jestem ile osób jest w stanie wskazać część logiczną w swojej aplikacji. A gwarantuję, że występuje ona niemal we wszystkich. Zacznijmy ponownie od przykładowego problemu do rozwiązania. Tym razem weźmy tablicę, z której chcemy wyciągnąć co drugi element i to zwiększony o 1.

Imperatywnie:

$tablica = [1,2,3,4,5,6];
$nowa_tablica = [];
foreach ($tablica as $k => $e) {
  if ($k % 2 == 0) {
    $nowa_tablica[] = $e + 1;
  }
}
Funkcyjnie
array_map(
    function ($e) { return $e + 1; }, 
    array_filter(
        [1,2,3,4,5,6], 
        function ($k) { return $k % 2 == 0; }, 
        ARRAY_FILTER_USE_KEY
   )
);
Logicznie (pseudo kod):
[ x | y ∈ [1,2,3,4,5,6]
    , idx(y) mod 2 = 0
    , x = y + 1 ] 

Jeśli masz kłopot ze zrozumieniem ostatniego kodu to można go przeczytać jako: znajdź mi każde x, które jest większe o 1 od y, które to należy do ciągu [1,2,3,4,5,6] i jest jego parzystym elementem. Ciekaw jestem czy już widzisz, gdzie w swoim kodzie wykorzystujesz analogiczny zapis. Jeśli nadal tego nie czujesz to co powiesz jak zapiszę to nieco inaczej:

select y.v + 1 as x
from y -- zakładam, że y to tabela z kolumną v z wartościami 1,2,3,4,5,6
where y.id modulo 2 = 0

OK, teraz chyba już każdy załapał. No to na czym polega ten styl? Zamiast pisać co chcemy po kolei zrobić albo co chcemy zamienić na co, podajemy po prostu wszystko co wiemy o interesującej nas wartości, a jak to znaleźć niech martwi się implementacja. Wynika z tego, że ten styl jest najlepszy właśnie do wyszukiwania. Deklarujemy z góry wszystkie relacje interesującej nas wartości z innymi, które znamy (wypisujemy fakty). Ale czy sprowadza się to tylko do używania silnika bazy danych? Oczywiście, że nie. W takim Clojure mamy moduł core.logic, w Haskellu mamy biblioteki z implementacją miniKanrena lub chociaż Monad.Logic, a w PHP? W PHP nie ma nic bezpośrednio ale np. w poprzednim przykładzie zamiast pisać połączenie array_map i array_filter mógłbym stworzyć sobie jakąś funkcję array_find:

function array_find(array fakty) {
...
}

array_find(["member-of" => [1,2,3,4,5,6],
            "index" => function ($i) { return $i % 2 == 0; },
            "equals-to" => function ($y) { return $y + 1; }
           ]);

Nie chodzi tu o definicję tego jak array_find działa tylko w jaki sposób zapisałem to czego oczekuję. Mógłbym to zapisać bardziej obiektowo:

ArrayFinder::memberOf([1,2,3,4,5,6])
  ->index(function ($i) { return $i % 2 == 0; })
  ->equalsTo(function ($y) { return $y + 1; })
  ->find();

Plusy są dość oczywiste. Kod jest zapisany niemal pełnymi zdaniami jakbyśmy opisywali koledze o co nam chodzi. Testowanie jest tak samo banalne jak w części funkcyjnej i to właściwie z tych samych powodów. To co nie jest banalne to implementacja. Wygląda to fajnie i właściwie dlaczego by tych miejsc funkcyjnych nie zastąpić tym stylem? No to weźmy poprzedni przykład zamieniania modelu na HTMLa. Jakbyśmy opisali koledze to, że chcemy z imienia i nazwiska zbudować sobie diva, który będzie miał określone animacje, długość, będzie się pojawiał i znikał a do tego jeszcze imię i nazwisko będzie osadzone w osobnych divach to położyłby się i zasnął. Natomiast funkcyjnie zamiast słów będzie widać jakie obiekty budujemy i z jakich elementów. Co nie jest prawdą pisząc w stylu imperatywnym, gdzie widzielibyśmy bardziej jakie kroki po kolei podejmujemy by najczęściej dopiero na końcu zrozumieć o co właściwie nam chodzi.

To tyle na dzisiaj, mam nadzieję, że mój słowotok nikogo nie uśpił.

Brak komentarzy:

Prześlij komentarz