Przejdź do głównej zawartości

OO kontra FP


Przez kilka ostatnich lat programowałem w parach z ludźmi uczącymi się Programowania Funkcyjnego, którzy manifestowali swoją niechęć do OO. To zwykle sprowadzało się do stwierdzenia pokroju: "Ooo, to za bardzo przypomina Obiekt".

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Myślę, że to wynika z przekonania, że FP i OO są jakoś wzajemnie rozłączne. Wydaje mi się, że wiele osób myśli, że jeżeli program jest funkcyjny w całości, to nie jest już zorientowany obiektowo. Przypuszczam, że ta opinia wynika z naturalnej konsekwencji uczenia się czegoś nowego.
Wtedy, kiedy zajmujemy się nową techniką, często mamy tendencję do odrzucania starych technik używanych przez nas wcześniej. To jest naturalne, ponieważ wierzymy, że ta nowa technika będzie "lepsza" i stąd ta stara technika musi byś "gorsza".
W tym wpisie stawiam tezę, że ponieważ OO i FP są ortogonalne, nie mogą być wzajemnie rozłączne. Dobry program funkcyjny może (i powinien) być zorientowany obiektowo. I dalej, dobry program zorientowany obiektowo może (i powinien) być funkcyjny. Ale żeby osiągnąć ten cel musimy bardzo ostrożnie zdefiniować nasze pojęcia.

Co to jest OO?

Zamierzam tutaj przyjąć bardzo redukcjonistyczną postawę. Jest wiele poprawnych definicji OO, które obejmują bogaty zestaw pomysłów, zasad, technik, wzorców i filozofii. Zamierzam ominąć to wszystko i skupić się na podstawach. Powód dla takiego redukcjonizmu jest taki, że te wszystkie bogactwa otaczające OO nie są dla niego specyficzne; ale są częścią bogactwa tworzenia oprogramowania w ogólności. Tutaj, skupię się tylko na tej części OO, która jest ostateczna i nierozerwalna.
Rozważ te dwa wyrażenia:
1: f(o);
2: o.f();
Jaka jest różnica?
Jasne jest to, że właściwie nie ma różnicy w znaczeniu. Cała różnica siedzi w składni. Ale jedno wyrażenie wygląda proceduralnie, a drugie wygląda na zorientowane obiektowo. A to z takiego powodu, że wnioskujemy dodatkowe zachowanie z wyrażenia 2., którego nie znajdujemy w wyrażeniu 1. Tym dodatkowym zachowaniem w wyrażeniu jest: polimorfizm.
Kiedy widzimy wyrażenie 1. widzimy funkcję nazwaną f wołaną na obiekcie o nazwie o. Wnioskujemy, że jest tylko jedna taka funkcja o nazwie f i że nie jest jedną z grupy standardowych funkcji otaczających o.
Z drugiej strony, kiedy patrzymy na wyrażenie 2. widzimy obiekt o nazwie o, któremu wysyłana jest wiadomość o nazwie f. Wnioskujemy, że tam mogą być inne rodzaje obiektów, które akceptują wiadomość nazwaną f i z tego powodu nie wiemy, która szczególna funkcja f właściwie zostanie wywołana. Zachowanie jest zależne od typu obiektu o tzn. f jest polimorficzna.
To spodziewanie sie polimorfizmu jest istotą programowania OO. To jest definicja redukcjonistyczna; i jest nierozerwalnie związana z OO. OO bez polimorfizmu nie jest OO. Wszystkie inne cechy OO takie, jak: enkapsulacja danych, i metody łączenia się z tymi danymi, i nawet dziedziczenie jest bardziej powiązane z wyrażeniem 1. niż z wyrażeniem 2.
Programiści C i Pascala (i do pewnego stopnia nawet programiści Fortrana i Cobola) od zawsze tworzyli systemy złożone z enkapsulowanych funkcji i struktur danych. Nie wymaga to więc języka OOPL żeby tworzyć i używać takich enkapsulowanych struktur. Enkapsulacja i nawet proste dziedziczenie jest oczywiste i naturalne w tego typu językach. (Nawet bardziej naturalne w C i Pascal niż w innych.)
A więc tą rzeczą prawdziwie odróżniającą programy OO od programów nie-OO jest polimorfizm.
Mógłbyś poskarżyć się mówiąc, że przecież polimorfizm może być osiągnięty poprzez użycie instrukcji switch lub długich łańcuchów if/else w środku f. To prawda, więc muszę dodać jeszcze jedno ograniczenie do OO.
Mechanizm polimorfizmu nie może stworzyć zależności w kodzie od wołającego do wołanego.
Dla wyjaśnienia, spójrz jeszcze raz na te dwa wyrażenia. Wyrażenie 1: f(o), wydaje się posiadać zależność w kodzie do funkcji f. Wnioskujemy to z tego, co już wcześniej wywnioskowaliśmy, że tam jest tylko jedna funkcja f i dlatego wołający musi coś wiedzieć o wołanym.
Natomiast, gdy patrzymy na Wyrażenie 2. o.f() wnioskujemy coś całkiem innego. Wiemy, że może tam być wiele implementacji f, i nie wiemy, która z tych funkcji f zostanie tak naprawdę zawołana. Zatem kod źródłowy, który zawiera Wyrażenie 2. nie ma zależności kodu źródłowego funkcji, która jest wołana.
W bardziej konkretnych pojęciach oznacza to, że moduły (pliki źródłowe), które zawierają polimorficzne wywołania do funkcji nie mogą zależeć, w jakikolwiek sposób, od modułów (plików źródłowych), które zawierają implementacje tych funkcji. Nie może tam być żadnego include czy use czy require ani żadnej innej deklaracji tego typu, która powodowałaby, że jeden plik źródłowy zależy od drugiego.
A więc naszą redukcjonistyczną definicją OO będzie:
Technika wywoływania funkcji przy użyciu dynamicznego polimorfizmu, w której kod źródłowy wołający nie zależy od kodu źródłowego wołanego.

Co to jest FP?

Znowu, zamierzam być bardzo redukcjonistyczny. FP ma bogatą historię i tradycję, która sięga daleko poza oprogramowanie. Są zasady, techniki, teorie, filozofie i pomysły, które władają tym paradygmatem. Zamierzam zignorować je wszystkie i od razu przejść do sedna, do nierozerwalnego atrybutu, który odgradza FP od każdego innego stylu. I to jest, najprościej, to:
f(a) == f(b) kiedy a == b.
W programie funkcyjnym, każdorazowo, gdy wywołujesz konkretną funkcję z konkretną wartością, otrzymujesz ten sam rezultat; nie ważne jak długo będzie działał program. To jest czasami nazywane "przejrzystością referencyjną".
Konsekwencje tego są takie, że funkcja f nie może zmienić żadnego globalnego stanu, który wpływałby na zachowanie funkcji f. Co wiecej, jeżeli założymy, że ta funkcja f reprezentuje wszystkie funkcje w systemie - wtedy wszystkie funkcje w systemie powinny być przejrzyste referencyjnie - wtedy żadna funkcja w systemie nie powinna w ogóle zmienić żadnego globalnego stanu. Żadna funkcja w systemie nie może zrobić niczego co powodowałoby to, że inna funkcja w systemie zwraca różne wartości dla tych samych wejść.
Głębsza konsekwencja tego jest taka, że żadna nazwana wartość nie może być kiedykolwiek zmieniona. Co znaczy, nie może być żadnego operatora przypisania.
I teraz, kiedy zastanowisz się nad tym bardzo uważnie, możesz dojść do wniosku, że program ułożony z niczego innego jak z referencyjnie przejrzystych funkcji nie może w ogóle nic zrobić - no bo przecież każdy użyteczny program zmienia stan czegoś; nawet, gdy to jest tylko stan drukarki czy ekranu. Jeśli jednak wykluczyć sprzęt i jakiekolwiek elementy ze świata zewnętrznego z naszego ograniczenia do referencyjnej przejrzystości, wtedy wychodzi, że zamiast tego możemy stworzyć całkiem użyteczne systemy.
Tym trikiem jest, oczywiście, rekurencja. Rozważ funkcję, która przyjmuje stanową strukturę danych jako argument. Argument zawiera całą informację o stanie, jaką funkcja używa do swojego działania. Kiedy robota jest zrobiona funkcja tworzy nową stanową strukturę danych z uaktualnionymi wartościami. Jako ostatni krok, funkcja woła samą siebie z nową stanową strukturą.
To jest tylko jeden z tych prostych trików, których program funkcyjny może używać do śledzenia zmian w stanie zewnętrznym, które wydają się nie zmieniać właściwie żadnego wewnętrznego stanu[1].
A więc, redukcjonistyczna definicja programowania funkcyjnego brzmi:
Przejrzystość referencyjna - żadnych ponownych przypisań wartości.

FP kontra OO

Do tej chwili już obie społeczności OO i FP zdążyły wycelować swoje armaty w moim kierunku. Redukcjonizm nie jest dobrą drogą do zdobywania przyjaciół. Ale czasami jest użyteczny. Myślę, że w tym przypadku przydaje się do rzucenia nieco światła na mit FP kontra OO, który wydaje się krążyć tu i tam.
Wydaje się, że te dwie redukcjonistyczne definicje, które wybrałem, są do siebie kompletnie ortogonalne. Polimorfizm i przejrzystość referencyjna nie mają ze sobą nic wspólnego. Nie ma między nimi zbieżności.


Ale ortogonalność nie znaczy wzajemnego wykluczenia (tylko spójrz na Jamesa Clerka Maxwella). Jest całkowicie możliwe zbudowanie systemu, który zawiera te oba: dynamiczny polimorfizm i referencyjną przejrzystość. To nie tylko jest możliwe, to jest pożądane!
Dlaczego to jest pożądane? Dokładnie z tych samych powodów, dla których te oba aspekty są pożądane w pojedynkę. Pożądamy systemów zbudowanych z użyciem dynamicznego polimorfizmu, ponieważ mają niską zależność. Zależności mogą być odwracane poprzez granice architektoniczne. Są testowalne poprzez Mocki, Fake'i i inne rodzaje Dublerów Testowych. Jedne moduły mogą być zmieniane bez potrzeby zmieniania na siłę innych modułów. To sprawia, że takie systemy łatwiej zmieniać i ulepszać.
Pożądamy także systemów, które są zbudowane przy użyciu referencyjnej przejrzystości, ponieważ są one przewidywalne. Niezdolność systemu do zmiany wewnetrznego stanu czyni system łatwiejszym do zrozumienia, zmiany i poprawy. To znacznie zmniejsza szanse wystąpienia sytuacji wyścigu i innych problemów współbieżnej aktualizacji.
Podsumowanie jest następujące:
Nie ma czegoś takiego jak FP kontra OO.
FP i OO świetnie współpracują razem. Oba elementy są pożądanymi częściami współczesnych systemów. System, który jest zbudowany na zasadach obu OO i FP będzie maksymalizował elastyczność, utrzymywalność, testowalność, prostotę i stabilność. Wyrzucając jedną na korzyść drugiej tylko osłabi się strukturę systemu.

[1] Odkąd używamy maszyn z architekturą Von Neumanna, musimy założyć, że istnieją komórki pamięci, które faktycznie zmieniają stan. W przypadku rekurencyjnego algorytmu, który opisałem; optymalizacja rekurencji ogonowej zapobiegnie tworzeniu się nowych ramek stosu i spowoduje zamiast tego ponowne użycie oryginalnej ramki stosu. Ale to naruszenie referencyjnej przejrzystości jest (zwykle) w całości ukryte i nieistotne.

Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Komentarze

Popularne posty z tego bloga

Kursy IT na Pluralsight. Dlaczego warto?

Bardzo sobie cenię kursy na Pluralsight. Mam wrażenie, że każdy kurs, który przeszedłem na tej platformie, w dużym stopniu podniósł moje zdolności. Wiem, dostęp do tej platformy nie jest tani, ale w mojej ocenie warty swojej ceny. To nie jest reklama, ale forma entuzjazmu jaki mam do tej formy samodoskonalenia. O to kilka punktów pokazujących ofertę tego serwisu i dlaczego warto skorzystać: Pluralsight to kursy z Javascript, C#, Java, Angular, Python, MySQL i wielu innych technologii i umiejętności. Kursy na Pluralsight w większości mają wyższą jakość niż te, które możemy znaleźć na przykład na YouTube. Są wyselekcjonowane, mają wysoką jakość dźwięku i obrazu. Często wgryzają się głęboko w dany problem daleko poza standardowe „Hello World” danej technologii. Twórcy Pluralsight to często osoby znane ze świata IT i konferencji branżowych, jak: Scott Hanselman, Microsoft John Somnez, SimpleProgrammer.com John Skeet, Google Pluralsight udostępnia funkcjonalność ścieżek – paths.

Algorytm Dijkstry

Byłem jednego dnia na SCNA , i ktoś zagadnął mnie o TDD i algorytm Dijkstry . Zastanawiał się, czy można znaleźć sekwencję testów, która zaprowadzi do tego algorytmu. To wyglądało mi na fajne, krótkie ćwiczenie, więc zdecydowałem się spróbować. Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony: https://blog.cleancoder.com/uncle-bob/2016/10/26/DijkstrasAlg.html Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta. Zacząłem tak, jak zwykle; przez odpalenie ograniczonego przypadku testowego. public class MinPathTest { @Test public void nothing() throws Exception { } } Algorytm Dijkstry jest prostym sposobem znajdowania najkrótszej drogi w grafie o krawędziach mających konkretną długość. Podając węzeł startowy i końcowy, algorytm wskaże, jaka jest najkrótsza ścieżka i jaka jest jej długość. A więc już od samego początku są ciekawe decyzje do podjęcia. W jaki sposób p

Podstawy Programowania Funkcyjnego Epizod 3

Czy wszystkie Zasady Się Zmieniają? Kiedy tylko zaczynamy używać nowego paradygmatu , porównujemy z nim nasze dotychczasowe zasady i nawyki. Pytamy siebie czy te wszystkie zasady i nawyki są poprawne w kontekście tego nowego paradygmatu. Rozważ, dla przykładu: Test Driven Development . Czy nadal jest poprawne w Programowaniu Funkcyjnym? Jeżeli tak, to jak się do tego zabierzesz? Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 07 stycznia 2013 ze strony: https://blog.cleancoder.com/uncle-bob/2013/01/07/FPBE3-Do-the-rules-change.html Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta. Aby odpowiedzieć sobie na to pytanie spróbujmy napisać prosty funkcyjny program: Word Wrap (zawijanie tekstu). Wymagania są proste. Mając napis złożony ze słów, oddzielonych pojedynczymi spacjami