Jak czysto funkcjonalne języki radzą sobie z modułowością?

23

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.

Kawa elektryczna
źródło
1
Jak myślisz, dlaczego funkcjonalność nie oznacza zajęć? Niektóre z pierwszych klas pochodzą z LISP - CLOS Spójrz na przestrzenie nazw clojure i typy lub moduły i haskell .
Odniosłem się do języków funkcjonalnych, które NIE mają zajęć, jestem bardzo świadomy kilku, które DO
Electric Coffee
1
Caml jako przykład, jego siostrzany język OCaml dodaje obiekty, ale sam Caml ich nie ma.
Kawa elektryczna
12
Termin „czysto funkcjonalny” odnosi się do języków funkcjonalnych, które zachowują referencyjną przejrzystość i nie ma związku z tym, czy język ma jakieś cechy obiektowe.
sepp2k
2
Ciasto to kłamstwo, ponowne użycie kodu w OO jest znacznie trudniejsze niż w FP. Pomimo tego, że OO domagał się ponownego użycia kodu przez lata, widziałem, jak następuje to minimum razy. (śmiało mówię, że muszę to robić źle, czuję się dobrze z tym, jak dobrze piszę kod OO, ponieważ musiałem projektować i obsługiwać systemy OO od lat, znam jakość własnych wyników)
Jimmy Hoffa

Odpowiedzi:

19

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.

Jörg W Mittag
źródło
8
Czy możesz wyjaśnić, jak te rzeczy działają nieco bardziej szczegółowo w stosunku do oop? Zamiast zwykłego stwierdzenia, że ​​istnieją koncepcje ...
Electric Coffee
Sidenote - Moduły i jednostki to dwie różne konstrukcje w Racket - moduły są porównywalne do przestrzeni nazw, a jednostki są w połowie drogi między przestrzeniami nazw a interfejsami OO. Dokumenty opisują znacznie więcej różnic
Jack
@Jack: Nie wiedziałem, że Racket ma również koncepcję zwaną module. Myślę, że to niefortunne, że Racket ma koncepcję zwaną, modulektóra nie jest modułem, i koncepcję, która jest modułem, ale nie jest wywoływana module. 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ł?
Jörg W Mittag,
Moduły i jednostki są grupami nazw powiązanych z wartościami. Moduły mogą mieć zależności od innych określonych zestawów powiązań, podczas gdy jednostki mogą mieć zależności od niektórych ogólnych zestawów powiązań , które musi zapewnić każdy inny kod korzystający z jednostki. Jednostki są parametryzowane przez powiązania, a moduły nie. Moduł zależny od powiązania mapi jednostka zależna od powiązania mapróżnią się tym, że moduł musi odnosić się do określonego mapwiązania, takiego jak ten z racket/base, podczas gdy różni użytkownicy jednostki mogą podawać różne definicje mapjednostki.
Jack
4

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.

dan_waterworth
źródło
4

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 :

lukstafi
źródło
2
czy mógłbyś wyjaśnić więcej o tym, co robią te zasoby i dlaczego polecasz je jako odpowiedź na zadane pytanie? „Tylko odpowiedzi” nie są mile widziane na Stack Exchange
gnat
2
Właśnie podałem rzeczywistą odpowiedź, a nie tylko linki, jako edycję.
lukstafi
0

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 sortfunkcji na dowolnym typie z Ordinstancją. Instancje można łatwo utworzyć, jeśli jeszcze nie istnieją.

Weź Animalprzykład i rozważ wdrożenie funkcji karmienia. Prostą implementacją OOP jest utrzymanie kolekcji Animalobiektów i przechodzenie przez wszystkie z nich, wywołując feedmetodę na każdym z nich.

Jednak sprawy stają się trudne, gdy dochodzisz do szczegółów. AnimalObiekt 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ęc FoodStoreobiekt stał się właśnie zależność od każdego Animal, zarówno jako pole do Animalobiektu, lub przekazywana jako parametr feedmetody. Alternatywnie, aby zachować Animalspójność klasy, możesz przejść feed(animal)do FoodStoreobiektu lub stworzyć obrzydliwość klasy zwanej taką AnimalFeederlub inną .

W FP nie ma skłonności do tego, aby pola Animalzawsze pozostawały zgrupowane razem, co ma pewne interesujące implikacje dla ponownego użycia. Załóżmy, że masz listę Animalrekordów, z pola podoba name, species, location, food type, food amount, itd. Masz również listę FoodStorerekordów z dziedzin takich jak location, food typei food 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 ani Animaldo FoodStoremoduł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.

Karl Bielefeldt
źródło
-1

Jednym z popularnych rozwiązań jest rozbicie kodu na moduły, oto jak to się robi w JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

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.

David Sergey
źródło