Czy programowanie funkcjonalne jest nadzbiorem obiektowym?

26

Im bardziej funkcjonalne programowanie, tym bardziej czuję, że dodaje dodatkową warstwę abstrakcji, która wydaje się, że warstwa cebuli obejmuje wszystkie poprzednie warstwy.

Nie wiem, czy to prawda, więc odejście od zasad OOP, nad którymi pracowałem od lat, może ktoś wytłumaczyć, jak funkcjonalne dokładnie przedstawia lub nie przedstawia żadnej z nich: enkapsulacja, abstrakcja, dziedziczenie, polimorfizm

Myślę, że wszyscy możemy powiedzieć, tak, ma on hermetyzację za pomocą krotek, czy krotki liczą się pod względem technicznym jako „programowanie funkcjonalne”, czy są one tylko użytecznością języka?

Wiem, że Haskell może spełnić wymóg „interfejsów”, ale znowu nie jesteś pewien, czy jego metoda jest funkcjonalna? Zgaduję, że fakt, że funktory mają podstawy matematyczne, można powiedzieć, że są to określone wbudowane oczekiwania dotyczące funkcjonalności?

Proszę szczegółowo opisać, w jaki sposób funkcjonalność spełnia lub nie spełnia 4 zasad OOP.

Edycja: Rozumiem różnice między paradygmatem funkcjonalnym a paradygmatem zorientowanym obiektowo i doskonale zdaję sobie sprawę z tego, że w dzisiejszych czasach istnieje wiele języków wieloparadygmatu, które mogą to zrobić. Naprawdę szukam tylko definicji tego, jak bezpośredni fp (myśl purysta, jak haskell) może zrobić jedną z 4 wymienionych tu rzeczy lub dlaczego nie może zrobić żadnej z nich. tzn. „Kapsułkowanie można wykonać przy zamknięciach” (lub jeśli się mylę w tym przekonaniu, proszę podać powód).

Jimmy Hoffa
źródło
7
Te 4 zasady nie powodują „OOP”. OOP po prostu „rozwiązuje” je za pomocą klas, hierarchii klas i ich instancji. Ale ja też chciałbym uzyskać odpowiedź, jeśli istnieją sposoby na osiągnięcie tego w programowaniu funkcjonalnym.
Euphoric
2
@Euforia W zależności od definicji robi OOP.
Konrad Rudolph
2
@KonradRudolph Wiem, że wiele osób twierdzi, że te rzeczy i korzyści, jakie przynoszą jako unikalne właściwości OOP. Zakładając, że „polimorfizm” oznacza „polimorfizm podtypu”, mogę iść z tymi dwoma ostatnimi będącymi integralną częścią OOP. Ale jeszcze nie spotkałem się z użyteczną definicją enkapsulacji i abstrakcji, która wyklucza zdecydowanie podejścia inne niż OOP. Możesz ukryć szczegóły i ograniczyć do nich dostęp nawet w Haskell. Haskell ma także polimorfizm ad hoc, po prostu nie polimorfizm podtypowy - pytanie brzmi, czy bit „podtyp” ma znaczenie?
1
@KonradRudolph To nie czyni go bardziej akceptowalnym. Jeśli już, to zachęta, by przyspieszyć i dać osobom, które go rozpowszechniają, powód do ponownego rozważenia.
1
Abstrakcja jest nieodłącznym elementem każdego programowania, przynajmniej każdego programowania poza surowym kodem maszynowym. Hermetyzacja istniała na długo przed OOP i jest nieodłącznym elementem programowania funkcjonalnego. Język funkcjonalny nie musi zawierać jawnej składni ani dziedziczenia, ani polimorfizmu. Myślę, że to dodaje się do „nie”.
sdenham

Odpowiedzi:

44

Programowanie funkcjonalne nie jest warstwą powyżej OOP; to zupełnie inny paradygmat. Możliwe jest wykonywanie OOP w funkcjonalnym stylu (F # został napisany właśnie w tym celu), a na drugim końcu spektrum masz rzeczy takie jak Haskell, który wyraźnie odrzuca zasady orientacji obiektowej.

Możesz wykonywać enkapsulację i abstrakcję w dowolnym języku wystarczająco zaawansowanym, aby obsługiwać moduły i funkcje. OO zapewnia specjalne mechanizmy enkapsulacji, ale nie jest to coś nieodłącznego dla OO. Punktem OO jest druga wspomniana para: dziedziczenie i polimorfizm. Pojęcie to jest formalnie znane jako podstawienie Liskowa i nie można go uzyskać bez wsparcia na poziomie programowania programowania obiektowego. (Tak, w niektórych przypadkach można go sfałszować, ale tracisz wiele zalet, które OO przynosi do stołu.)

Programowanie funkcjonalne nie koncentruje się na podstawieniu Liskowa. Koncentruje się na zwiększeniu poziomu abstrakcji oraz na zminimalizowaniu użycia stanu zmiennego i procedur z „efektami ubocznymi”, co jest terminem, który programiści funkcjonalni lubią używać do tworzenia procedur, które faktycznie coś robią (w przeciwieństwie do zwykłego obliczania czegoś) straszny. Ale znowu są to zupełnie odrębne paradygmaty, których można używać razem, lub nie, w zależności od języka i umiejętności programisty.

Mason Wheeler
źródło
1
Cóż, dziedziczenie (w tych wyjątkowo rzadkich przypadkach, gdy jest potrzebne) jest osiągalne w porównaniu ze składem i jest czystsze niż dziedziczenie na poziomie typu. Polimorfizm jest naturalny, szczególnie w obecności typów polimorficznych. Ale oczywiście zgadzam się, że FP nie ma nic wspólnego z OOP i jej zasadami.
SK-logic
Zawsze można go sfałszować - możesz implementować obiekty w dowolnym języku, który wybierzesz. Zgadzam się ze wszystkim innym :)
Eliot Ball
5
Nie sądzę, że termin „skutki uboczne” został wymyślony (lub jest przede wszystkim używany) przez funkcjonalnych programistów.
sepp2k
4
@ sepp2k: Nie powiedział, że wymyślili ten termin, tylko że posługują się nim, używając w przybliżeniu tego samego tonu, jakiego normalnie używa się w odniesieniu do dzieci, które nie chcą zejść z trawnika.
Aaronaught
16
@Aaronaught nie przeszkadzają mi dzieci na trawniku, to ich cholerne skutki uboczne! Gdyby przestali mutować na całym moim trawniku, nie miałbym ich wcale.
Jimmy Hoffa
10

Uważam, że poniższa intuicja jest przydatna do porównywania OOP i FP.

Zamiast rozważać FP jako nadzbiór OOP, pomyśl o OOP i FP jako dwóch alternatywnych sposobach spojrzenia na podobny bazowy model obliczeniowy, w którym masz:

  1. Niektóre operacje, które są wykonywane,
  2. niektóre argumenty wejściowe do operacji,
  3. niektóre stałe dane / parametry, które mogą wpływać na definicję operacji,
  4. pewna wartość wyniku oraz
  5. prawdopodobnie efekt uboczny.

W OOP jest to rejestrowane przez

  1. Metoda, która jest wykonywana,
  2. argumenty wejściowe metody,
  3. obiekt, na którym wywoływana jest metoda, zawierający niektóre lokalne dane w postaci zmiennych składowych,
  4. wartość zwracana przez metodę (prawdopodobnie nieważna),
  5. skutki uboczne tej metody.

W FP jest to uchwycone przez

  1. Zamknięcie, które jest wykonywane,
  2. argumenty wejściowe zamknięcia,
  3. przechwycone zmienne zamknięcia,
  4. wartość zwrotna zamknięcia,
  5. możliwe skutki uboczne zamknięcia (w czystych językach, takich jak Haskell, dzieje się to w bardzo kontrolowany sposób).

Dzięki tej interpretacji obiekt może być postrzegany jako zbiór zamknięć (jego metod), z których wszystkie przechwytują te same zmienne nielokalne (zmienne składowe obiektu wspólne dla wszystkich zamknięć w kolekcji). Ten widok jest również wspierany przez fakt, że w językach obiektowych zamknięcia są często modelowane jako obiekty przy użyciu dokładnie jednej metody.

Myślę, że różne punkty widzenia wynikają z faktu, że widok zorientowany obiektowo jest wyśrodkowany na obiektach (danych), podczas gdy widok funkcjonalny jest wyśrodkowany na funkcjach / zamknięciach (operacjach).

Giorgio
źródło
8

To zależy od tego, kogo poprosisz o definicję OOP. Zapytaj pięć osób, a prawdopodobnie otrzymasz sześć definicji. Wikipedia mówi :

Próby znalezienia konsensusowej definicji lub teorii za obiektami nie okazały się bardzo skuteczne

Tak więc, gdy ktoś udzieli bardzo ostatecznej odpowiedzi, weź ją z odrobiną soli.

To powiedziawszy, istnieje dobry argument, że tak, FP jest nadzbiorem OOP jako paradygmatu. W szczególności definicja programowania obiektowego Alana Kaya nie jest sprzeczna z tym pojęciem (ale Kristen Nygaard ). Kay była naprawdę zaniepokojona tym, że wszystko jest przedmiotem, a logika jest realizowana poprzez przekazywanie komunikatów między obiektami.

Być może bardziej interesujące dla twojego pytania, o klasach i obiektach można myśleć w kategoriach funkcji i zamknięć zwracanych przez funkcje (które działają jednocześnie jako klasy i konstruktory). Jest to bardzo zbliżone do programowania opartego na prototypach, a JavaScript pozwala na to dokładnie.

var cls = function (x) {
    this.y = x;
    this.fun = function () { alert(this.y); };
    return this;
};

var inst = new cls(42);
inst.fun();

(Oczywiście JavaScript pozwala na mutowanie wartości, które są nielegalne w czysto funkcjonalnym programowaniu, ale nie jest również wymagane w ścisłej definicji OOP.)

Ważniejsze pytanie brzmi jednak: czy jest to znacząca klasyfikacja OOP? Czy pomocne jest myślenie o nim jako o podziale programowania funkcjonalnego? Myślę, że w większości przypadków tak nie jest.

Konrad Rudolph
źródło
1
Wydaje mi się, że zastanawianie się nad tym, gdzie powinna zostać narysowana linia, przy zmianie paradygmatów może mieć sens. Jeśli powiedziano, że nie ma sposobu na osiągnięcie nietypowego polimorfizmu w fp, nie zawracam sobie głowy próbą użycia fp do modelowania czegoś, co by do niego pasowało. Jeśli jednak jest to możliwe, mogę poświęcić trochę czasu, aby osiągnąć dobry sposób na zrobienie tego (chociaż dobry sposób może nie być możliwy), gdy ciężko pracuję w przestrzeni fp, ale chcę nietypowy polimorfizm w kilku niszowych przestrzeniach. Oczywiście, jeśli większość systemu się z tym zgadza, lepiej byłoby użyć OOP.
Jimmy Hoffa
6

FP jak OO nie jest dobrze zdefiniowanym terminem. Istnieją szkoły o różnych, czasem sprzecznych definicjach. Jeśli weźmiesz to, co ich łączy, przejdziesz do:

  • programowanie funkcjonalne to programowanie z funkcjami pierwszej klasy

  • Programowanie OO to programowanie z polimorfizmem inkluzyjnym połączeniu z co najmniej ograniczoną formą dynamicznie rozwiązywanego przeciążenia. (Uwaga dodatkowa: w kręgach OO polimorfizm zazwyczaj oznacza polimorfizm inkluzyjny , podczas gdy szkoły FP zwykle oznaczają polimorfizm parametryczny ).

Wszystko inne jest obecne gdzie indziej lub w niektórych przypadkach nieobecne.

FP i OO to narzędzie do budowania dwóch abstrakcji. Każdy z nich ma swoje mocne i słabe strony (na przykład mają inny preferowany kierunek rozszerzenia w problemie ekspresji), ale żaden z nich nie jest z natury silniejszy od drugiego. Możesz zbudować system OO na jądrze FP (CLOS jest jednym z takich systemów). Możesz użyć frameworka OO, aby uzyskać funkcje pierwszej klasy (zobacz na przykład sposób definiowania funkcji lambda w C ++ 11).

AProgrammer
źródło
Myślę, że masz na myśli „funkcje pierwszej klasy”, a nie „funkcje pierwszego rzędu”.
dan_waterworth
Errr ... C ++ 11 lambda nie są funkcjami pierwszej klasy: każda lambda ma swój własny typ ad-hoc (dla wszystkich praktycznych celów, anonimowa struktura), niekompatybilny z rodzimym typem wskaźnika funkcji. I std::functiondo którego można przypisać zarówno wskaźniki funkcji, jak i lambdy, jest zdecydowanie ogólny, a nie obiektowy. Nie jest to niespodzianką, ponieważ ograniczony rodzaj polimorfizmu orientacji obiektowej (polimorfizm podtypu) jest ściśle mniej silny niż polimorfizm parametryczny (nawet Hindley-Milner, nie mówiąc już o pełnym Systemie F-omega).
pyon
Nie mam dużego doświadczenia z purystycznymi językami funkcjonalnymi, ale jeśli potrafisz zdefiniować klasy z jedną metodą statyczną w zamknięciach i przekazać je do różnych kontekstów, powiedziałbym, że jesteś (niezręcznie) przynajmniej w połowie tam na opcje w stylu funkcjonalnym. Istnieje wiele sposobów obejścia ścisłych parametrów w większości języków.
Erik Reppen
2

Nie; OOP może być postrzegany jako nadzór programowania proceduralnego i różni się zasadniczo od paradygmatu funkcjonalnego, ponieważ ma stan reprezentowany w polach instancji. W paradygmacie funkcjonalnym zmienne są funkcjami, które są stosowane do stałych danych w celu uzyskania pożądanego wyniku.

W rzeczywistości można rozważyć programowanie funkcjonalne jako podzbiór OOP; jeśli sprawisz, że wszystkie twoje klasy będą niezmienne, możesz pomyśleć, że masz jakieś funkcjonalne programowanie.

m3th0dman
źródło
3
Niezmienne klasy nie wykonują funkcji wyższego rzędu, nie zawierają list ani nie zamykają. Fp nie jest podzbiorem.
Jimmy Hoffa
1
@Jimmy Hoffa: Możesz łatwo zasymulować wyższą funkcję oredera, tworząc klasę, która ma jedną metodę, która przyjmuje lub więcej obiektów podobnego typu, a także zwraca obiekt tego podobnego typu (typ, który ma metodę i nie zawiera pól) . Kompresja list nie jest czymś związanym z paradygmatem języka programowania, a nie jest obsługiwana przez Smalltalk (jest to OOP). Zamknięcia są obecne w języku C # i zostaną wstawione również w Javie.
m3th0dman
tak C # ma zamknięcia, ale to dlatego, że jest to wieloparadygmat, zamknięcia zostały dodane wraz z innymi kawałkami fp do C # (za co jestem wiecznie wdzięczny), ale ich obecność w języku oop nie powoduje, że się wyrzucają. Zaletą funkcji wyższego rzędu jest jednak to, że enkapsulacja metody w klasie umożliwia takie samo zachowanie.
Jimmy Hoffa
2
Tak, ale jeśli użyjesz zamknięć w celu zmiany stanu, czy nadal programowałbyś w funkcjonalnym paradygmacie? Chodzi o to - paradygmat funkcjonalny dotyczy braku stanu, a nie funkcji wyższego rzędu, rekurencji lub zamknięć.
m3th0dman
ciekawa myśl na temat definicji fp. Muszę się nad tym zastanowić, dziękuję za podzielenie się spostrzeżeniami.
Jimmy Hoffa
2

Odpowiedź:

Wikipedia ma świetny artykuł na temat programowania funkcjonalnego z niektórymi przykładami, o które prosisz. @Konrad Rudolph podał już link do artykułu OOP .

Nie sądzę, że jeden paradygmat jest super-zestawem drugiego. Są to różne perspektywy programowania i niektóre problemy można lepiej rozwiązać z jednej perspektywy, a niektóre z drugiej.

Twoje pytanie jest jeszcze bardziej skomplikowane przez wszystkie implementacje FP i OOP. Każdy język ma swoje dziwactwa, które są istotne dla każdej dobrej odpowiedzi na twoje pytanie.

Coraz bardziej styczne wędrowanie:

Podoba mi się pomysł, że język taki jak Scala stara się dać ci to, co najlepsze z obu światów. Martwię się, że daje to również komplikacje obu światów.

Java jest językiem OO, ale w wersji 7 dodano funkcję „spróbuj z zasobami”, której można użyć do naśladowania pewnego rodzaju zamknięcia. Tutaj imituje aktualizację lokalnej zmiennej „a” w środku innej funkcji, nie czyniąc jej widoczną dla tej funkcji. W tym przypadku pierwsza połowa drugiej funkcji to konstruktor ClosureTry (), a druga połowa to metoda close ().

public class ClosureTry implements AutoCloseable {

    public static void main(String[] args) {
        int a = 1;
        try(ClosureTry ct = new ClosureTry()) {
            System.out.println("Middle Stuff...");
            a = 2;
        }
        System.out.println("a: " + a);
    }

    public ClosureTry() {
        System.out.println("Start Stuff Goes Here...");
    }

    /** Interface throws exception, but we don't have to. */
    public void close() {
        System.out.println("End Stuff Goes Here...");
    }
}

Wydajność:

Start Stuff Goes Here...
Middle Stuff...
End Stuff Goes Here...
a: 2

Może to być przydatne do zamierzonego celu, jakim jest otwarcie strumienia, zapisanie do strumienia i niezawodne zamknięcie go, lub po prostu sparowanie dwóch funkcji w taki sposób, aby nie zapomnieć o wywołaniu drugiej po wykonaniu jakiejś pracy między nimi . Oczywiście jest to tak nowe i niezwykłe, że inny programista może usunąć blok try, nie zdając sobie sprawy, że coś psuje, więc jest to obecnie rodzaj anty-wzorca, ale interesujące, że można to zrobić.

Możesz wyrazić dowolną pętlę w większości imperatywnych języków jako rekurencję. Obiekty i zmienne można uczynić niezmiennymi. Procedury mogą być napisane w celu zminimalizowania skutków ubocznych (chociaż twierdzę, że prawdziwa funkcja nie jest możliwa na komputerze - czas potrzebny do wykonania i zużycie procesora / dysku / systemu to nieuniknione skutki uboczne). Niektóre języki funkcjonalne mogą być przystosowane do wykonywania wielu, jeśli nie wszystkich operacji obiektowych. Nie muszą się wzajemnie wykluczać, chociaż niektóre języki mają ograniczenia (np. Nie zezwalające na aktualizację zmiennych), które uniemożliwiają określone wzorce (jak zmienne pola).

Dla mnie najbardziej użytecznymi częściami programowania obiektowego są ukrywanie danych (enkapsulacja), traktowanie wystarczająco podobnych obiektów jako takich samych (polimorfizm) oraz gromadzenie danych i metod, które działają na tych danych razem (obiekty / klasy). Dziedziczenie może być okrętem flagowym OOP, ale dla mnie jest to najmniej ważna i najmniej używana część.

Najbardziej przydatne części programowania funkcjonalnego to niezmienność (tokeny / wartości zamiast zmiennych), funkcje (bez skutków ubocznych) i zamknięcia.

Nie sądzę, że jest zorientowany obiektowo, ale muszę powiedzieć, że jedną z najbardziej przydatnych rzeczy w informatyce jest możliwość deklarowania interfejsu, a następnie posiadania różnych funkcji i danych implementujących ten interfejs. Lubię też mieć kilka zmiennych danych do pracy, więc wydaje mi się, że nie czuję się całkowicie komfortowo w wyłącznie funkcjonalnych językach, chociaż staram się ograniczać zmienność i efekty uboczne we wszystkich projektach mojego programu.

GlenPeterson
źródło