Pochodzę z zorientowanego obiektowo tła, w którym nauczyłem się, że klasy są lub przynajmniej mogą być użyte do stworzenia warstwy abstrakcji, która pozwala na łatwy recykling kodu, który może być następnie użyty do stworzenia obiektów lub do dziedziczenia.
Na przykład mogę mieć klasę zwierząt, a następnie odziedziczyć po niej koty i psy i takie, że wszystkie odziedziczą wiele takich samych cech, a na podstawie tych podklas mogę tworzyć przedmioty, które mogą określać rasę zwierząt, a nawet imię z tego.
Lub mogę użyć klas, aby określić wiele instancji tego samego kodu, który obsługuje lub zawiera nieco inne rzeczy; jak węzły w drzewie wyszukiwania lub wiele różnych połączeń z bazą danych, a co nie.
Niedawno przechodzę do programowania funkcjonalnego, więc zacząłem się zastanawiać:
jak czysto funkcjonalne języki radzą sobie z takimi rzeczami? To znaczy, języki bez pojęcia klas i obiektów.
źródło
Odpowiedzi:
Wiele języków funkcjonalnych ma system modułowy. (Nawiasem mówiąc, wiele języków obiektowych też tak robi.) Ale nawet jeśli nie ma jednego, możesz używać funkcji jako modułów.
JavaScript jest dobrym przykładem. W JavaScript funkcje są używane zarówno do implementacji modułów, jak i do enkapsulacji obiektowej. W Scheme, który był główną inspiracją dla JavaScript, są tylko funkcje. Funkcje służą do implementacji prawie wszystkiego: obiektów, modułów (zwanych jednostkami w Racket), a nawet struktur danych.
OTOH, Haskell i rodzina ML mają jawny system modułów.
Orientacja obiektowa dotyczy abstrakcji danych. to jest to! Modułowość, dziedziczenie, polimorfizm, a nawet stan zmienny są kwestiami ortogonalnymi.
źródło
module
. Myślę, że to niefortunne, że Racket ma koncepcję zwaną,module
która nie jest modułem, i koncepcję, która jest modułem, ale nie jest wywoływanamodule
. W każdym razie napisałeś: „jednostki są w połowie odległości między przestrzeniami nazw i interfejsami OO”. Czy to nie jest definicja tego, czym jest moduł?map
i jednostka zależna od powiązaniamap
różnią się tym, że moduł musi odnosić się do określonegomap
wiązania, takiego jak ten zracket/base
, podczas gdy różni użytkownicy jednostki mogą podawać różne definicjemap
jednostki.Wygląda na to, że zadajesz dwa pytania: „Jak osiągnąć modułowość w językach funkcjonalnych?” co zostało omówione w innych odpowiedziach i „jak tworzyć abstrakcje w językach funkcjonalnych?” na które odpowiem.
W językach OO masz tendencję do koncentrowania się na rzeczowniku, „zwierzęciu”, „serwerze pocztowym”, „jego ogrodowym rozwidleniu” itp. Natomiast języki funkcjonalne podkreślają czasownik „chodzić”, „pobierać pocztę” , „prod”, itp.
Nic więc dziwnego, że abstrakcje w językach funkcjonalnych są raczej oparte na czasownikach lub operacjach niż na rzeczach. Jednym z przykładów, do którego zawsze sięgam, gdy próbuję to wyjaśnić, jest analiza. W językach funkcjonalnych dobrym sposobem na pisanie parserów jest określenie gramatyki, a następnie jej interpretacja. Tłumacz tworzy abstrakcję procesu parsowania.
Innym konkretnym przykładem tego jest projekt, nad którym pracowałem niedawno. Pisałem bazę danych w Haskell. Miałem jeden „język osadzony” do określania operacji na najniższym poziomie; na przykład pozwolił mi pisać i czytać rzeczy z nośnika pamięci. Miałem inny, osobny „język osadzony” do określania operacji na najwyższym poziomie. Potem miałem, co jest w zasadzie tłumaczem, konwersję operacji z wyższego poziomu na niższy poziom.
Jest to niezwykle ogólna forma abstrakcji, ale nie jest to jedyna dostępna w językach funkcjonalnych.
źródło
Chociaż „programowanie funkcjonalne” nie ma daleko idących implikacji dla kwestii modułowości, poszczególne języki zajmują się programowaniem na dużą skalę na różne sposoby. Ponowne użycie kodu i abstrakcja współdziałają ze sobą, ponieważ im mniej ujawnisz, tym trudniej jest ponownie użyć kodu. Odkładając na bok abstrakcję, zajmę się dwoma kwestiami wielokrotnego użytku.
Statycznie typowane języki OOP tradycyjnie używały nominalnego podtypu, co oznacza, że kod zaprojektowany dla klasy / modułu / interfejsu A może poradzić sobie z klasą / modułem / interfejsem B tylko wtedy, gdy B wyraźnie wspomina o A. ten kod zaprojektowany dla A może obsłużyć B, ilekroć B ma wszystkie metody i / lub pola A. B mógł zostać utworzony przez inny zespół, zanim zaistniała potrzeba bardziej ogólnej klasy / interfejsu A. Na przykład w OCaml, podtypowanie strukturalne dotyczy systemu modułowego, systemu obiektowego podobnego do OOP i jego dość unikalnych typów wariantów polimorficznych.
Najbardziej widoczna różnica między OOP a FP wrt. modułowość polega na tym, że domyślna „jednostka” w pakiecie OOP łączy jako obiekt różne operacje na tym samym przypadku wartości, podczas gdy domyślna „jednostka” w pakiecie FP łączy się jako funkcja tej samej operacji dla różnych przypadków wartości. W FP nadal bardzo łatwo jest łączyć ze sobą operacje, na przykład jako moduły. (BTW, ani Haskell, ani F # nie mają pełnoprawnego systemu modułów z rodziny ML). Problem z wyrażaniemto zadanie polegające na stopniowym dodawaniu zarówno nowych operacji działających na wszystkich wartościach (np. dołączanie nowej metody do istniejących obiektów), jak i nowych przypadków wartości, które wszystkie operacje powinny obsługiwać (np. dodawanie nowej klasy z tym samym interfejsem). Jak omówiono w pierwszym wykładzie Ralfa Laemmela poniżej (który zawiera obszerne przykłady w języku C #), dodawanie nowych operacji jest problematyczne w językach OOP.
Połączenie OOP i FP w Scali może sprawić, że będzie to jeden z najpotężniejszych języków wrt. modułowość. Ale OCaml jest nadal moim ulubionym językiem i moim osobistym, subiektywnym zdaniem nie odbiega on od Scali. Dwa poniższe wykłady Ralfa Laemmla omawiają rozwiązanie problemu ekspresji w Haskell. Myślę, że to rozwiązanie, choć doskonale działa, utrudnia wykorzystanie uzyskanych danych z polimorfizmem parametrycznym. Rozwiązanie problemu ekspresji wariantów polimorficznych w OCaml, wyjaśnione w poniższym artykule Jaquesa Garrigue'a, nie ma tej wady. Odsyłam również do rozdziałów podręcznika, w których porównano zastosowania modułowości innej niż OOP i OOP w OCaml.
Poniżej znajdują się linki specyficzne dla Haskell i OCaml dotyczące problemu wyrażania :
źródło
W rzeczywistości kod OO jest znacznie mniej przydatny do ponownego wykorzystania, i to już z założenia. Ideą OOP jest ograniczenie operacji na określonych fragmentach danych do określonego uprzywilejowanego kodu, który znajduje się w klasie lub w odpowiednim miejscu w hierarchii dziedziczenia. Ogranicza to niekorzystne skutki zmienności. Jeśli zmienia się struktura danych, w kodzie jest tylko tyle miejsc, które mogą być odpowiedzialne.
Dzięki niezmienności nie obchodzi Cię, kto może operować na dowolnej strukturze danych, ponieważ nikt nie może zmienić twojej kopii danych. To znacznie ułatwia tworzenie nowych funkcji do pracy na istniejących strukturach danych. Po prostu tworzysz funkcje i grupujesz je w moduły, które wydają się odpowiednie z punktu widzenia domeny. Nie musisz się martwić, gdzie zmieścisz je w hierarchii dziedziczenia.
Innym rodzajem ponownego wykorzystania kodu jest tworzenie nowych struktur danych do pracy na istniejących funkcjach. Jest to obsługiwane w językach funkcjonalnych przy użyciu funkcji takich jak generics i klasy typów. Na przykład klasa typu Ord Haskella pozwala na użycie
sort
funkcji na dowolnym typie zOrd
instancją. Instancje można łatwo utworzyć, jeśli jeszcze nie istnieją.Weź
Animal
przykład i rozważ wdrożenie funkcji karmienia. Prostą implementacją OOP jest utrzymanie kolekcjiAnimal
obiektów i przechodzenie przez wszystkie z nich, wywołującfeed
metodę na każdym z nich.Jednak sprawy stają się trudne, gdy dochodzisz do szczegółów.
Animal
Obiekt naturalnie wie, jaki rodzaj pokarmu zjada, i ile potrzebuje, aby czuć się pełna. To nie nie naturalnie wiedzieć, gdzie jedzenie jest przechowywane i ile jest dostępny, a więcFoodStore
obiekt stał się właśnie zależność od każdegoAnimal
, zarówno jako pole doAnimal
obiektu, lub przekazywana jako parametrfeed
metody. Alternatywnie, aby zachowaćAnimal
spójność klasy, możesz przejśćfeed(animal)
doFoodStore
obiektu lub stworzyć obrzydliwość klasy zwanej takąAnimalFeeder
lub inną .W FP nie ma skłonności do tego, aby pola
Animal
zawsze pozostawały zgrupowane razem, co ma pewne interesujące implikacje dla ponownego użycia. Załóżmy, że masz listęAnimal
rekordów, z pola podobaname
,species
,location
,food type
,food amount
, itd. Masz również listęFoodStore
rekordów z dziedzin takich jaklocation
,food type
ifood amount
.Pierwszym krokiem w żywieniu może być mapowanie każdej z tych list rekordów na listy
(food amount, food type)
par z liczbami ujemnymi dla ilości zwierząt. Następnie możesz tworzyć funkcje do robienia różnego rodzaju rzeczy za pomocą tych par, takich jak sumowanie ilości każdego rodzaju żywności. Funkcje te nie należą idealnie aniAnimal
doFoodStore
modułu, ani do jednego , ale można z nich wielokrotnie korzystać.W efekcie powstaje mnóstwo funkcji, które robią użyteczne rzeczy,
[(Num A, Eq B)]
które są wielokrotnego użytku i modułowe, ale masz problem z ustaleniem, gdzie je umieścić lub jak nazwać je jako grupę. W rezultacie moduły FP są trudniejsze do sklasyfikowania, ale klasyfikacja jest znacznie mniej ważna.źródło
Jednym z popularnych rozwiązań jest rozbicie kodu na moduły, oto jak to się robi w JavaScript:
Pełny artykuł wyjaśniający ten wzór w JavaScript , oprócz tego istnieje wiele innych sposobów, aby zdefiniować moduł, takich jak RequireJS , CommonJS , Google zamknięcia. Innym przykładem jest Erlang, w którym masz zarówno moduły, jak i zachowania, które wymuszają API i wzorzec, odgrywając podobną rolę jak interfejsy w OOP.
źródło