Programowanie funkcjonalne w porównaniu do OOP z klasami

32

Ostatnio interesowałem się niektórymi koncepcjami programowania funkcjonalnego. Od jakiegoś czasu korzystam z OOP. Widzę, jak zbudowałbym dość złożoną aplikację w OOP. Każdy obiekt wiedziałby, jak to robić. Lub cokolwiek, co robi klasa rodziców. Mogę więc po prostu powiedzieć, Person().speak()żeby ta osoba mówiła.

Ale jak zrobić podobne rzeczy w programowaniu funkcjonalnym? Widzę, jak funkcje są pierwszorzędnymi elementami. Ale ta funkcja robi tylko jedną konkretną rzecz. Czy po prostu miałbym say()metodę, która pływałaby i wywoływałaby ją z równoważnym Person()argumentem, aby wiedzieć, co to za coś mówi?

Widzę więc proste rzeczy, jak zrobić OOP i obiekty w programowaniu funkcjonalnym, aby móc modulować i organizować moją bazę kodu?

Dla porównania, moim podstawowym doświadczeniem z OOP jest Python, PHP i trochę C #. Języki, na które patrzę, które mają funkcje, to Scala i Haskell. Chociaż pochylam się w stronę Scali.

Podstawowy przykład (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')
skift
źródło
Scala została zaprojektowana jako OOP + FP, więc nie musisz wybierać
Karthik T
1
Tak, jestem tego świadomy, ale chcę również wiedzieć z przyczyn intelektualnych. Nie mogę znaleźć niczego w ekwiwalencie obiektu w językach funkcjonalnych. Jeśli chodzi o Scalę, nadal chciałbym wiedzieć, kiedy / gdzie / jak powinienem używać funkcji over oop, ale IMHO to kolejne pytanie.
skift
2
„Szczególnie mocno podkreślone, IMO uważa, że ​​nie utrzymujemy stanu.”: To błędne pojęcie. Nie jest prawdą, że FP nie używa stanu, raczej FP obsługuje stan w inny sposób (np. Monady w Haskell lub unikalne typy w Clean).
Giorgio,
1
możliwy duplikat Jak organizować programy funkcjonalne
Doc Brown
3
możliwy duplikat programowania funkcjonalnego vs. OOP
Caleb,

Odpowiedzi:

21

Naprawdę pytasz o to, jak robić polimorfizm w językach funkcjonalnych, tj. Jak tworzyć funkcje, które zachowują się inaczej w zależności od ich argumentów.

Zauważ, że pierwszy argument funkcji jest zwykle równoważny „obiektowi” w OOP, ale w językach funkcjonalnych zwykle chcesz oddzielić funkcje od danych, więc „obiekt” prawdopodobnie będzie czystą (niezmienną) wartością danych.

Języki funkcjonalne ogólnie zapewniają różne opcje osiągania polimorfizmu:

  • Coś w rodzaju multimetod, które wywołują inną funkcję w oparciu o sprawdzenie podanych argumentów. Można to zrobić na podstawie typu pierwszego argumentu (który jest faktycznie równy zachowaniu większości języków OOP), ale można to również zrobić na innych atrybutach argumentów.
  • Prototypowe / obiektowe struktury danych, które zawierają pierwszorzędne funkcje jako elementy . Możesz więc osadzić funkcję „powiedz” w strukturach danych twojego psa i kota. Skutecznie uczyniłeś kod częścią danych.
  • Dopasowanie wzorca - tam gdzie logika dopasowywania wzorca jest wbudowana w definicję funkcji i zapewnia różne zachowania dla różnych parametrów. Często w Haskell.
  • Rozgałęzienie / warunki - równoważne klauzulom if / else w OOP. Może nie być wysoce rozszerzalny, ale nadal może być odpowiedni w wielu przypadkach, gdy masz ograniczony zestaw możliwych wartości (np. Czy funkcja przekazała liczbę, ciąg znaków lub wartość zerową?)

Jako przykład, oto implementacja Twojego problemu Clojure przy użyciu wielu metod:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

Zauważ, że to zachowanie nie wymaga zdefiniowania żadnych jawnych klas: zwykłe mapy działają dobrze. Funkcja wysyłania (w tym przypadku wpisz:) może być dowolną funkcją argumentów.

mikera
źródło
Nie w 100% jasne, ale wystarczające, aby zobaczyć, dokąd idziesz. Widziałem to jako kod „zwierzęcy” w danym pliku. Również część dotycząca rozgałęzień / warunków jest dobra. Nie uważałem tego za alternatywę dla if / else.
skift
11

To nie jest bezpośrednia odpowiedź, ani też niekoniecznie 100% dokładna, ponieważ nie jestem ekspertem od języków funkcjonalnych. Ale w obu przypadkach podzielę się z Wami moim doświadczeniem ...

Mniej więcej rok temu pływałem podobną łodzią jak ty. Zrobiłem C ++ i C # i wszystkie moje projekty zawsze były bardzo obciążone na OOP. Słyszałem o językach FP, czytałem informacje w Internecie, przeglądałem książkę F #, ale nadal nie mogłem naprawdę zrozumieć, w jaki sposób język FP może zastąpić OOP lub być użyteczny ogólnie, ponieważ większość przykładów, które widziałem, były po prostu zbyt proste.

Dla mnie „przełom” przyszedł, kiedy zdecydowałem się nauczyć Pythona. Pobrałem python, potem poszedłem na stronę główną projektu euler i zacząłem robić jeden problem po drugim. Python niekoniecznie jest językiem FP i z pewnością można w nim tworzyć klasy, ale w porównaniu do C ++ / Java / C # ma o wiele więcej konstrukcji FP, więc kiedy zacząłem się nim bawić, świadomie postanowiłem nie zdefiniować klasę, chyba że absolutnie musiałem.

Interesujące w Pythonie było to, jak łatwe i naturalne było przyjmowanie funkcji i „łączenie” ich w celu tworzenia bardziej złożonych funkcji, a ostatecznie problem został rozwiązany przez wywołanie jednej funkcji.

Wskazałeś, że przy kodowaniu należy przestrzegać zasady pojedynczej odpowiedzialności i jest to absolutnie poprawne. Ale tylko dlatego, że funkcja jest odpowiedzialna za jedno zadanie, nie oznacza, że ​​może ona wykonać absolutne minimum. W FP nadal masz poziomy abstrakcji. Więc twoje funkcje wyższego poziomu mogą nadal robić „jedną” rzecz, ale mogą delegować funkcje niższego poziomu, aby zaimplementować bardziej szczegółowe informacje o tym, jak osiągnąć tę „jedną” rzecz.

Kluczem w FP jest jednak to, że nie masz skutków ubocznych. Tak długo, jak traktujesz aplikację jako prostą transformację danych ze zdefiniowanym zestawem danych wejściowych i zestawem danych wyjściowych, możesz napisać kod FP, który osiągnie to, czego potrzebujesz. Oczywiście nie każda aplikacja będzie dobrze pasować do tej formy, ale kiedy zaczniesz to robić, będziesz zaskoczony, ile aplikacji pasuje. I tutaj myślę, że tutaj świecą Python, F # lub Scala, ponieważ dają konstrukty FP, ale kiedy musisz pamiętać swój stan i „wprowadzać efekty uboczne”, zawsze możesz polegać na prawdziwych i wypróbowanych technikach OOP.

Od tego czasu napisałem całą masę kodu Pythona jako narzędzia i inne skrypty pomocnicze do pracy wewnętrznej, a niektóre z nich zostały znacznie rozszerzone, ale pamiętając podstawowe zasady SOLID, większość tego kodu wciąż była bardzo łatwa w utrzymaniu i elastyczna. Podobnie jak w OOP, twój interfejs jest klasą i przesuwasz klasy podczas refaktoryzacji i / lub dodawania funkcjonalności, w FP robisz dokładnie to samo z funkcjami.

W zeszłym tygodniu zacząłem kodować w Javie i od tego czasu prawie codziennie przypomina mi się, że kiedy jestem w OOP, muszę implementować interfejsy, deklarując klasy metodami zastępującymi funkcje, w niektórych przypadkach mogłem osiągnąć to samo w Pythonie używając proste wyrażenie lambda, na przykład 20-30 linii kodu, które napisałem, aby przeskanować katalog, zawierałoby 1-2 wiersze w Pythonie i brak klas.

Same FP są językami wyższego poziomu. W Pythonie (przepraszam, moje jedyne doświadczenie w FP) mogłem połączyć rozumienie listy wewnątrz innego rozumienia listy z lambdami i innymi rzeczami wrzuconymi, a wszystko to byłoby tylko 3-4 liniami kodu. W C ++ mógłbym absolutnie osiągnąć to samo, ale ponieważ C ++ jest na niższym poziomie, musiałbym napisać znacznie więcej kodu niż 3-4 linie, a wraz ze wzrostem liczby linii moje szkolenie SRP zacznie działać i zacznę myślenie o tym, jak podzielić kod na mniejsze części (tj. więcej funkcji). Ale w trosce o łatwość konserwacji i ukrywanie szczegółów implementacji umieściłem wszystkie te funkcje w tej samej klasie i uczynię je prywatnymi. I oto masz ... Właśnie utworzyłem klasę, podczas gdy w pythonie napisałbym „return (.... lambda x: .. ....)”

DXM
źródło
Tak, nie odpowiada bezpośrednio na pytanie, ale wciąż jest świetną odpowiedzią. kiedy piszę mniejsze skrypty lub pakiety w Pythonie, nie zawsze używam klas. wiele razy samo posiadanie go w formacie pakietu idealnie pasuje. szczególnie jeśli nie potrzebuję stanu. Zgadzam się również, że listy są też cholernie przydatne. Odkąd przeczytałem o FP, zdałem sobie sprawę, jak potężne mogą być. co doprowadziło mnie do chęci dowiedzenia się więcej o FP, w porównaniu do OOP.
skift
Świetna odpowiedź. Mówi do wszystkich stojących przy basenie funkcjonalnym, ale nie ma pewności, czy zanurzyć palec w wodzie
Robben_Ford_Fan_boy,
I Ruby ... Jedna z filozofii projektowania koncentruje się na metodach przyjmujących blok kodu jako argument, zazwyczaj opcjonalny. A biorąc pod uwagę czystą składnię, myślenie i kodowanie w ten sposób jest łatwe. Trudno tak myśleć i komponować w C #. Funkcjonalne pisanie w C # jest pełne i dezorientujące, wydaje się, że jest przypisane do języka. Podoba mi się, że Ruby pomogła myśleć funkcjonalnie łatwiej, aby zobaczyć potencjał w moim zdecydowanym polu myśli C #. W końcowej analizie widzę funkcjonalność i OO jako komplementarne; Powiedziałbym, że Ruby z pewnością tak uważa.
radarbob
8

W Haskell najbliższa jest „klasa”. Ta klasa, choć nie taka sama jak klasa w Javie i C ++ , będzie działać na to, czego chcesz w tym przypadku.

W twoim przypadku tak będzie wyglądał twój kod.

klasa Zwierzęta gdzie 
say :: String -> sound 

Następnie możesz mieć indywidualne typy danych dostosowujące te metody.

instancja Animal Dog gdzie
powiedz s = "bark" ++ s 

EDYCJA: - Przed specjalizacją powiedz Dog, musisz powiedzieć systemowi, że Pies jest zwierzęciem.

data Dog = \ - coś tutaj - \ (pochodzenie Animal)

EDYCJA: - Dla Wilq.
Teraz, jeśli chcesz użyć say w funkcji say foo, będziesz musiał powiedzieć haskell, że foo może działać tylko ze Zwierzęciem.

foo :: (Animal a) => a -> String -> String
foo a str = powiedz str 

teraz, jeśli wołasz psa do psa, szczeka, jeśli wołasz kota, miauczy.

main = do 
niech d = pies (\ - parametry cstr - \)
    c = cat  
w programie $ foo d „Hello World”

Nie możesz teraz podać żadnej innej definicji funkcji. Jeśli powiedzmy, że jest wywołany z czymkolwiek innym niż zwierzę, spowoduje to błąd kompilacji.

Manoj R.
źródło
muszę naprawdę trochę więcej na haskell, aby w pełni to zrozumieć, ale myślę, że dostaję to. nadal jestem ciekawy, jak to by się stało z bardziej złożoną bazą kodu.
skift
nitpick Animal powinien zostać skapitalizowany
Daniel Gratzer
1
Skąd funkcja say wie, że nazywasz ją Psem, jeśli wymaga tylko ciągu? I czy nie „wyprowadza” tylko niektórych wbudowanych klas?
WilQu,
6

Języki funkcjonalne używają 2 konstrukcji do osiągnięcia polimorfizmu:

  • Funkcje pierwszego rzędu
  • Generics

Tworzenie z nich kodu polimorficznego jest zupełnie inne niż sposób, w jaki OOP wykorzystuje dziedziczenie i metody wirtualne. Chociaż oba mogą być dostępne w twoim ulubionym języku OOP (jak C #), większość języków funkcjonalnych (jak Haskell) podnosi je do jedenastu. Rzadko występuje funkcja, która nie jest ogólna, a większość funkcji ma funkcje jako parametry.

Trudno to wyjaśnić w ten sposób i nauka nowego sposobu będzie wymagała dużo czasu. Ale aby to zrobić, musisz całkowicie zapomnieć o OOP, ponieważ tak nie działa w funkcjonalnym świecie.

Euforyk
źródło
2
OOP polega na polimorfizmie. Jeśli uważasz, że OOP polega na powiązaniu funkcji z Twoimi danymi, to nic nie wiesz o OOP.
Euforia
4
polimorfizm jest tylko jednym aspektem OOP i myślę, że nie ten, o który OP tak naprawdę pyta.
Doc Brown
2
Polimorfizm jest kluczowym aspektem OOP. Wszystko inne jest w stanie to wspierać. OOP bez dziedziczenia / metod wirtualnych jest prawie dokładnie tak samo jak programowanie proceduralne.
Euforia
1
@ErikReppen Jeśli „zastosowanie interfejsu” nie jest często potrzebne, oznacza to, że nie wykonujesz OOP. Haskell ma również moduły.
Euforia
1
Nie zawsze potrzebujesz interfejsu. Ale są bardzo przydatne, gdy ich potrzebujesz. I IMO kolejna ważna część OOP. Jeśli chodzi o moduły w Haskell, myślę, że jest to prawdopodobnie najbliższe OOP dla języków funkcjonalnych, jeśli chodzi o organizację kodu. Przynajmniej z tego, co przeczytałem do tej pory. Wiem, że wciąż są bardzo różne.
skift
0

to naprawdę zależy od tego, co chcesz osiągnąć.

jeśli potrzebujesz tylko sposobu organizacji zachowania w oparciu o kryteria selektywne, możesz użyć np. słownika (tablicy skrótów) z obiektami funkcyjnymi. w pythonie może to być coś w stylu:

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

zauważ jednak, że (a) nie ma „wystąpień” psa lub kota i (b) będziesz musiał sam śledzić „typ” swoich obiektów.

jak na przykład: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. wtedy możesz zrobić takie zrozumienie listy jak[animals[pet[1]]['say'](pet[2]) for pet in pets]

kr1
źródło
0

Języki OO mogą być czasami używane zamiast języków niskiego poziomu do bezpośredniego połączenia z maszyną. C ++ Na pewno, ale nawet dla C # istnieją adaptery i tym podobne. Chociaż pisanie kodu do sterowania częściami mechanicznymi i drobnej kontroli nad pamięcią jest najlepiej utrzymywane jak najbliżej niskiego poziomu, jak to możliwe. Ale jeśli to pytanie jest związane z obecnym oprogramowaniem obiektowym, takim jak linia biznesu, aplikacje internetowe, IOT, usługi sieciowe i większość masowo używanych aplikacji, to ...

Odpowiedz, jeśli dotyczy

Czytelnicy mogą próbować pracować z architekturą zorientowaną na usługi (SOA). To znaczy, DDD, N-warstwowe, N-wielowarstwowe, sześciokątne, cokolwiek. Nie widziałem, aby aplikacja dla dużych firm efektywnie korzystała z „tradycyjnego” OO (Active-Record lub Rich-Models), jak to opisano w latach 70. i 80. w ostatniej dekadzie +. (Patrz uwaga 1)

Błąd nie dotyczy OP, ale z tym pytaniem wiąże się kilka problemów.

  1. Podany przykład to po prostu demonstracja polimorfizmu, nie jest to kod produkcyjny. Czasami dokładnie takie przykłady są brane dosłownie.

  2. W FP i SOA dane są oddzielone od logiki biznesowej. Oznacza to, że dane i logika nie idą w parze. Logika przechodzi do usług, a dane (modele domenowe) nie zachowują się polimorficznie (patrz uwaga 2).

  3. Usługi i funkcje mogą być polimorficzne. W FP często przekazujesz funkcje jako parametry do innych funkcji zamiast wartości. Możesz zrobić to samo w OO Languages ​​z typami takimi jak Callable lub Func, ale nie działa on gwałtownie (patrz Uwaga 3). W FP i SOA twoje modele nie są polimorficzne, tylko twoje usługi / funkcje. (Patrz uwaga 4)

  4. W tym przykładzie występuje zły przypadek kodowania. Nie mówię tylko o czerwonym sznurku „pies szczeka”. Mówię też o samych modelach CatModel i DogModel. Co dzieje się, gdy chcesz dodać owcę? Musisz wejść do swojego kodu i utworzyć nowy kod? Czemu? W kodzie produkcyjnym wolałbym zobaczyć tylko AnimalModel z jego właściwościami. W najgorszym przypadku AmphibianModel i FowlModel, jeśli ich właściwości i obsługa są tak różne.

Oto, czego oczekiwałbym w obecnym języku „OO”:

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Podstawowy przepływ

Jak przechodzisz od zajęć w OO do programowania funkcjonalnego? Jak powiedzieli inni; Możesz, ale tak naprawdę nie. Chodzi o to, aby zademonstrować, że nie powinieneś nawet używać klas (w tradycyjnym znaczeniu tego słowa) podczas programowania w Javie i C #. Kiedy już zaczniesz pisać kod w architekturze zorientowanej na usługi (DDD, warstwowe, warstwowe, heksagonalne, cokolwiek), będziesz o krok bliżej do funkcjonalności, ponieważ oddzielasz swoje dane (modele domen) od funkcji logicznych (usługi).

Język OO o krok bliżej do FP

Możesz nawet pójść o krok dalej i podzielić swoje usługi SOA na dwa typy.

Opcjonalna klasa typu 1 : Wspólne usługi implementujące interfejs dla punktów wejścia. Byłyby to „nieczyste” punkty wejścia, które mogą wywoływać inne „czyste” lub „nieczyste” funkcje. Mogą to być Twoje Punkty wejścia z API RESTful.

Opcjonalna klasa typu 2 : czyste usługi logiki biznesowej. Są to klasy statyczne, które mają funkcjonalność „czystą”. W FP „Pure” oznacza, że ​​nie ma żadnych skutków ubocznych. Nigdzie nie ustawia on wyraźnie stanu ani trwałości. (Patrz uwaga 5)

Kiedy więc pomyślisz o klasach w językach zorientowanych obiektowo, które są używane w architekturze zorientowanej na usługi, nie tylko przynosi to korzyści Twojemu kodowi OO, ale sprawia, że ​​programowanie funkcjonalne wydaje się bardzo łatwe do zrozumienia.

Notatki

Uwaga 1 : Oryginalny, zorientowany obiektowo obiekt „Rich” lub „Active-Record” jest nadal dostępny. W przeszłości, gdy ludzie „robili to dobrze” dekadę lub więcej temu, istnieje wiele takich starszych kodów. Ostatni raz widziałem tego rodzaju kod (poprawnie wykonany) z gry wideo Codebase w C ++, w którym dokładnie kontrolują pamięć i mają bardzo ograniczoną przestrzeń. Nie wspominając, że FP i architektury zorientowane na usługi są bestiami i nie powinny brać pod uwagę sprzętu. Ale priorytetem jest możliwość ciągłej zmiany, utrzymania, zmiany wielkości danych i innych aspektów. W grach wideo i sztucznej inteligencji maszyn bardzo precyzyjnie kontrolujesz sygnały i dane.

Uwaga 2 : Modele domen nie mają zachowania polimorficznego, ani nie mają zależności zewnętrznych. Są „odizolowane”. To nie znaczy, że muszą być w 100% anemiczne. Mogą mieć wiele logiki związanej z ich budową i zmiennymi zmianami właściwości, jeśli ma to zastosowanie. Zobacz DDD „Obiekty wartości” i jednostki autorstwa Erica Evansa i Marka Seemanna.

Uwaga 3 : Linq i Lambda są bardzo powszechne. Ale kiedy użytkownik tworzy nową funkcję, rzadko używa Func lub Callable jako parametrów, podczas gdy w FP byłoby dziwnie widzieć aplikację bez funkcji zgodnych z tym wzorcem.

Uwaga 4 : Nie mylić polimorfizmu z dziedziczeniem. Model CatModel może odziedziczyć AnimalBase, aby określić, jakie właściwości zwierzę zwykle ma. Ale jak pokazuję, takie modele to Zapach Kodowy . Jeśli zobaczysz ten wzorzec, możesz rozważyć jego rozbicie i przekształcenie w dane.

Uwaga 5 : Funkcje Pure mogą (i wykonują) akceptować funkcje jako parametry. Funkcja przychodząca może być nieczysta, ale może być czysta. Do celów testowych zawsze byłby czysty. Ale w produkcji, chociaż jest traktowany jako czysty, może zawierać skutki uboczne. To nie zmienia faktu, że czysta funkcja jest czysta. Chociaż funkcja parametru może być nieczysta. Nie mylące! :RE

Suamere
źródło
-2

Możesz zrobić coś takiego… php

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark
Michael Dennis
źródło
1
Nie oddałem żadnych negatywnych głosów. Ale przypuszczam, że dzieje się tak, ponieważ to podejście nie jest funkcjonalne ani zorientowane obiektowo.
Manoj R
1
Ale przekazana koncepcja jest zbliżona do dopasowania wzorca stosowanego w programowaniu funkcjonalnym, tj. $whostosayStaje się typem obiektu, który określa, co zostanie wykonane. Powyższe można zmodyfikować, aby dodatkowo zaakceptować inny parametr $whattosay, aby mógł go użyć typ, który go obsługuje (np. 'human').
syockit