Na stronie 839 drugiego wydania Steve McConnell omawia wszystkie sposoby, w jakie programiści mogą „pokonać złożoność” w dużych programach. Jego wskazówki kończą się tym stwierdzeniem:
„Programowanie obiektowe zapewnia poziom abstrakcji, który dotyczy jednocześnie algorytmów i danych , rodzaj abstrakcji, której nie zapewnił sam rozkład funkcjonalny”.
W połączeniu z konkluzją, że „zmniejszenie złożoności jest prawdopodobnie najważniejszym kluczem do bycia efektywnym programistą” (ta sama strona), wydaje się, że jest to wyzwanie dla programowania funkcjonalnego.
Debata między FP i OO jest często omawiana przez zwolenników FP wokół kwestii złożoności, które wynikają konkretnie z wyzwań związanych z współbieżnością lub równoległością. Ale współbieżność z pewnością nie jest jedynym rodzajem złożoności, który programiści muszą pokonać. Być może skupienie się na zmniejszeniu jednego rodzaju złożoności znacznie go zwiększa w innych wymiarach, tak że w wielu przypadkach zysk nie jest wart kosztu.
Jeśli zmienilibyśmy warunki porównania FP i OO z konkretnych kwestii, takich jak współbieżność lub możliwość ponownego użycia, do zarządzania globalną złożonością, jak wyglądałaby ta debata?
EDYTOWAĆ
Kontrast, który chciałem podkreślić, polega na tym, że OO wydaje się hermetyzować i abstrahować od złożoności zarówno danych, jak i algorytmów, podczas gdy programowanie funkcjonalne wydaje się zachęcać do pozostawiania szczegółów implementacji struktur danych bardziej „ujawnionych” w całym programie.
Patrz np Stuart Halloway (zwolennikiem Clojure FP) tutaj mówiąc, że „nadmierne specyfikacja typów danych” jest „negatywna konsekwencja idiomatycznym stylu oo” i faworyzowanie konceptualizacji adresową jako prosty wektora lub map zamiast bogatszej obiektu OO z dodatkowymi (nie wektorowymi i niemapowymi) właściwościami i metodami. (Również zwolennicy projektowania i zarządzania opartego na domenie mogą powiedzieć, że wystawienie książki adresowej jako wektora lub mapy prześwietla enkapsulowane dane metodami, które są nieistotne, a nawet niebezpieczne z punktu widzenia domeny).
Odpowiedzi:
Pamiętaj, że książka została napisana ponad 20 lat temu. Dla ówczesnych profesjonalnych programistów FP nie istniała - była całkowicie w sferze naukowców i badaczy.
Musimy wykadrować „rozkład funkcjonalny” we właściwym kontekście pracy. Autor nie odnosi się do programowania funkcjonalnego. Musimy powiązać to z „programowaniem strukturalnym” i
GOTO
napełnionym bałaganem, który się przed nim pojawił. Jeśli twoim punktem odniesienia jest stary FORTRAN / COBOL / BASIC, który nie miał funkcji (być może, gdybyś miał szczęście, uzyskałby jeden poziom GOSUB), a wszystkie twoje zmienne są globalne, jest w stanie rozbić twój program na warstwy funkcji jest dużym dobrodziejstwem.OOP to dalsze udoskonalenie tego rodzaju „rozkładu funkcjonalnego”. Nie tylko możesz łączyć instrukcje w funkcje, ale także grupować powiązane funkcje z danymi, nad którymi pracują. Rezultatem jest jasno zdefiniowany fragment kodu, który można przejrzeć i zrozumieć (najlepiej) bez konieczności gonienia całej bazy kodu, aby dowiedzieć się, co jeszcze może działać na twoich danych.
źródło
Wyobrażam sobie, że zwolennicy programowania funkcjonalnego twierdzą, że większość języków FP zapewnia więcej sposobów abstrakcji niż „sam funkcjonalny rozkład” i faktycznie dopuszcza sposoby abstrakcji porównywalne pod względem mocy do języków zorientowanych obiektowo. Na przykład można przytoczyć klasy typów Haskella lub moduły wyższego rzędu ML jako takie metody abstrakcji. Zatem stwierdzenie (które jestem prawie pewien, że dotyczy orientacji obiektowej vs. programowanie proceduralne, a nie programowanie funkcjonalne) nie ma do nich zastosowania.
Należy również zauważyć, że FP i OOP są koncepcjami ortogonalnymi i nie wykluczają się wzajemnie. Dlatego nie ma sensu porównywać ich ze sobą. Można bardzo dobrze porównać „imperatywny OOP” (np. Java) vs. „funkcjonalny OOP” (np. Scala), ale cytowane przez ciebie stwierdzenie nie będzie miało zastosowania do tego porównania.
źródło
Uważam, że programowanie funkcjonalne jest niezwykle pomocne w zarządzaniu złożonością. Zwykle myślisz o złożoności w inny sposób, definiując ją jako funkcje, które działają na niezmiennych danych na różnych poziomach, zamiast enkapsulacji w sensie OOP.
Na przykład niedawno napisałem grę w Clojure, a cały stan gry został zdefiniowany w jednej niezmiennej strukturze danych:
Główną pętlę gry można zdefiniować jako stosowanie w pętli niektórych czystych funkcji do stanu gry:
Wywoływana jest kluczowa funkcja
update-game
, która uruchamia krok symulacji, biorąc pod uwagę poprzedni stan gry i dane wprowadzone przez użytkownika, i zwraca nowy stan gry.Więc gdzie jest złożoność? Moim zdaniem udało się to całkiem dobrze:
OOP może również zarządzać złożonością poprzez enkapsulację, ale jeśli porównasz to do OOP, funkcjonalność ma bardzo duże zalety:
Na koniec, dla osób, które są zainteresowane większą wiedzą na temat zarządzania złożonością w językach funkcjonalnych i OOP, zdecydowanie polecam film z przemówienia Richa Hickeya Simple Made Easy (nakręcony podczas konferencji technologicznej Strange Loop )
źródło
Sam rozkład funkcjonalny nie wystarczy do stworzenia jakiegokolwiek algorytmu lub programu: musisz również przedstawić dane. Myślę, że powyższe stwierdzenie domyślnie zakłada (a przynajmniej można to zrozumieć), że „dane” w przypadku funkcjonalnym są najbardziej szczątkowe: po prostu listy symboli i nic więcej. Programowanie w takim języku jest oczywiście niezbyt wygodne. Jednak wiele, zwłaszcza nowe i nowoczesne, funkcjonalne (lub wieloparadigmowe) języki, takie jak Clojure, oferują bogate struktury danych: nie tylko listy, ale także ciągi znaków, wektory, mapy i zbiory, rekordy, struktury - i obiekty! - z metadanymi i polimorfizmem.
Trudno kwestionować ogromny praktyczny sukces abstrakcji OO. Ale czy to ostatnie słowo? Jak pisałeś, problemy z współbieżnością już są głównym problemem, a klasyczny OO nie zawiera żadnego pojęcia o współbieżności. W rezultacie de facto rozwiązania OO do radzenia sobie z współbieżnością to po prostu nakładająca się taśma izolacyjna: działa, ale łatwo ją zepsuć, zabiera znaczną ilość zasobów mózgu od niezbędnego zadania i nie skaluje się dobrze. Może można wziąć to, co najlepsze z wielu światów. Do tego dążą współczesne języki wieloparadygmatu.
źródło
Zmienny stan jest źródłem większości zawiłości i problemów związanych z programowaniem i projektowaniem oprogramowania / systemu.
OO obejmuje stan zmienny. FP nie znosi stanu zmiennego.
Zarówno OO, jak i FP mają swoje zastosowania i słodkie punkty. Wybierz mądrze. I pamiętaj przysłowie: „Zamknięcia są obiektami biednego człowieka. Obiekty są zamknięciem biednego człowieka”.
źródło
Programowanie funkcjonalne może mieć obiekty, ale obiekty te są zwykle niezmienne. Czyste funkcje (funkcje bez skutków ubocznych) działają następnie na tych strukturach danych. Możliwe jest tworzenie niezmiennych obiektów w obiektowych językach programowania, ale nie zostały one zaprojektowane do tego i nie tak się je stosuje. Utrudnia to rozumowanie programów zorientowanych obiektowo.
Weźmy bardzo prosty przykład. Powiedzmy, że Oracle zdecydowało, że Java Strings powinna mieć metodę odwrotną i napisałeś następujący kod.
co ocenia ostatnia linia? Potrzebujesz specjalnej wiedzy na temat klasy String, aby wiedzieć, że będzie to fałsz.
Co jeśli stworzę własną klasę WuHoString
Nie można wiedzieć, co ocenia ostatnia linia.
W stylu programowania funkcjonalnego byłby zapisany w następujący sposób:
i powinno być prawdą.
Jeśli tak trudno jest zrozumieć 1 funkcję w jednej z najbardziej podstawowych klas, to zastanawia się, czy wprowadzenie tej koncepcji obiektów zmiennych zwiększyło lub zmniejszyło złożoność.
Oczywiście istnieją różnego rodzaju definicje tego, co stanowi obiektowe i co oznacza być funkcjonalnym i co to znaczy mieć jedno i drugie. Dla mnie możesz mieć „funkcjonalny styl programowania” w languagess, który nie ma rzeczy takich jak funkcje pierwszej klasy, ale stworzono dla niego inne języki.
źródło
Myślę, że w większości przypadków klasyczna abstrakcja OOP nie obejmuje złożoności współbieżności. Dlatego OOP (w swoim pierwotnym znaczeniu) nie wyklucza FP i dlatego widzimy rzeczy takie jak scala.
źródło
Odpowiedź zależy od języka. Na przykład Lisps ma naprawdę porządnie, że kod to dane - algorytmy, które piszesz, to tak naprawdę tylko listy Lisp! Przechowujesz dane w taki sam sposób jak piszesz program. Ta abstrakcja jest jednocześnie prostsza i dokładniejsza niż OOP i pozwala ci robić naprawdę fajne rzeczy (sprawdź makra).
Haskell (i podobny język, jak sądzę) ma zupełnie inną odpowiedź: algebraiczne typy danych. Algebraiczny typ danych jest podobny do
C
struktury, ale ma więcej opcji. Te typy danych zapewniają abstrakcję potrzebną do modelowania danych; funkcje zapewniają abstrakcję potrzebną do modelowania algorytmów. Klasy typów i inne zaawansowane funkcje zapewniają jeszcze wyższy poziom abstrakcji w obu przypadkach.Na przykład, dla zabawy pracuję nad językiem programowania o nazwie TPL. Algebraiczne typy danych naprawdę ułatwiają reprezentowanie wartości:
To, co mówi - w bardzo wizualny sposób - jest to, że TPLValue (dowolna wartość w moim języku) może być a
Null
lub aNumber
zInteger
wartością lub nawetFunction
z listą wartości (parametry) i wartością końcową (treść ).Następnie mogę użyć klas typów, aby zakodować niektóre typowe zachowania. Na przykład, mógłbym zrobić
TPLValue
i którego wystąpienieShow
oznacza, że można go przekonwertować na ciąg.Ponadto mogę używać własnych klas typów, gdy muszę określić zachowanie niektórych typów (w tym tych, których sam nie zaimplementowałem). Na przykład mam
Extractable
klasę typów, która pozwala mi napisać funkcję, która przyjmuje aTPLValue
i zwraca odpowiednią wartość normalną. W ten sposóbextract
można przekonwertować aNumber
na aInteger
lubString
aString
tak długo, jakInteger
iString
są instancjamiExtractable
.Wreszcie główna logika mojego programu obejmuje kilka funkcji, takich jak
eval
iapply
. To naprawdę rdzeń - biorąTPLValue
s i zamieniają je w więcejTPLValue
s, a także obsługę stanu i błędów.Ogólnie rzecz biorąc, abstrakcje używam w moim kodu Haskell są rzeczywiście bardziej wydajne niż to, co użyłem w języku OOP.
źródło
eval
. „Hej, spójrz na mnie! Nie muszę pisać własnych dziur w zabezpieczeniach; mam usterkę wykonania dowolnego kodu wbudowaną w język programowania!” Łączenie danych z kodem jest główną przyczyną jednej z dwóch najpopularniejszych klas luk w zabezpieczeniach wszechczasów. Za każdym razem, gdy zobaczysz, że ktoś został zhakowany z powodu ataku iniekcji SQL (między innymi), dzieje się tak dlatego, że jakiś programista nie wie, jak właściwie oddzielić dane od kodu.eval
nie zależy w dużej mierze od struktury Lispa - możesz miećeval
takie języki jak JavaScript i Python. Prawdziwa moc pochodzi z pisania makr, które są w zasadzie programami, które działają na programy takie jak dane i generują inne programy. To sprawia, że język jest bardzo elastyczny i łatwe tworzenie potężnych abstrakcji.and
. zwarcieor
.let
.let-rec
.cond
.defn
. Żadnego z nich nie można zaimplementować za pomocą funkcji w aplikacyjnych językach porządkowych.for
(lista ze zrozumieniem).dotimes
.doto
.and
!” Słyszę: „hej, spójrz na mnie, mój język jest tak okaleczony, że nawet nie ma zwarciaand
i muszę wszystko wymyślić na nowo !”Cytowane zdanie, jak widzę, nie ma już żadnej ważności.
Współczesne języki OO nie mogą abstrakcji nad typami, których rodzaj nie jest *, tzn. Typy o wyższym rodzaju są nieznane. Ich system typów nie pozwala wyrazić idei „jakiegoś kontenera z elementami Int, który pozwala zamapować funkcję na elementach”.
Stąd ta podstawowa funkcja jak Haskells
nie można łatwo napisać w Javie *), na przykład przynajmniej nie w sposób bezpieczny dla typu. Dlatego, aby uzyskać podstawową funkcjonalność, musisz napisać wiele bojlerów, ponieważ potrzebujesz
A jednak te pięć metod to w zasadzie ten sam kod, daj lub weź trochę. Natomiast w Haskell potrzebowałbym:
Zauważ, że nie zmieni się to w Javie 8 (wystarczy, że można łatwiej zastosować funkcje, ale wtedy dokładnie pojawi się powyższy problem. Dopóki nie masz funkcji wyższego rzędu, najprawdopodobniej nawet nie w stanie zrozumieć, do czego nadają się typy wyższego rzędu).
Nawet nowe języki OO, takie jak Cejlon, nie mają wyższych rodzajów. (Ostatnio spytałem Gavina Kinga, a on powiedział mi, że w tej chwili nie było to ważne). Jednak nie wiem o Kotlinie.
*) Aby być uczciwym, możesz mieć interfejs Functor z metodą fmap. Złe jest to, że nie można powiedzieć: Hej, wiem, jak zaimplementować fmap dla klasy biblioteki SuperConcurrentBlockedDoublyLinkedDequeHasMap, drogi kompilatorze, proszę zaakceptować, że od teraz wszystkie SuperConcurrentBlockedDoublyLinkedDequeHasMaps są Functors.
źródło
Każdy, kto kiedykolwiek programował w dBase, wiedziałby, jak przydatne są makr jednoliniowe w tworzeniu kodu wielokrotnego użytku. Chociaż nie programowałem w Lisp, czytałem od wielu innych, którzy przeklinają makra czasu kompilacji. Pomysł wstrzykiwania kodu do kodu w czasie kompilacji jest stosowany w prostej formie w każdym programie C z dyrektywą „włącz”. Ponieważ Lisp może to zrobić za pomocą programu Lisp i ponieważ Lisp jest wysoce refleksyjny, zyskujesz znacznie bardziej elastyczną obsługę.
Każdy programista, który po prostu pobierze dowolny tekst z sieci i przekaże go do swojej bazy danych, nie jest programistą. Podobnie każdy, kto pozwoliłby, aby dane „użytkownika” automatycznie stały się kodem wykonywalnym, jest oczywiście głupi. Nie oznacza to, że zezwalanie programom na manipulowanie danymi w czasie wykonywania, a następnie wykonywanie ich, ponieważ kod jest złym pomysłem. Wierzę, że ta technika będzie niezbędna w przyszłości, w której „inteligentny” kod faktycznie pisze większość programów. Cały „problem danych / kodu” lub nie jest kwestią bezpieczeństwa w języku.
Jednym z problemów związanych z większością języków jest to, że zostały stworzone dla jednej osoby do samodzielnego wykonywania niektórych funkcji. Rzeczywiste programy wymagają, aby wiele osób miało dostęp przez cały czas i jednocześnie z wielu rdzeni i wielu klastrów komputerowych. Bezpieczeństwo powinno być częścią języka, a nie systemu operacyjnego, aw niedalekiej przyszłości będzie.
źródło