Programowanie obiektowe w czysto funkcjonalnym kontekście programowania?

Czy są jakieś zalety stosowania programowania obiektowego (OOP) w kontekście programowania funkcyjnego (FP)?

Od jakiegoś czasu używam F# i zauważyłem, że im bardziej moje funkcje są bezpaństwowe, tym mniej potrzebuję ich jako metod obiektów. W szczególności, istnieją zalety polegania na wnioskowaniu typu, aby były one użyteczne w tak szerokiej liczbie sytuacji, jak to możliwe.

Nie wyklucza to potrzeby tworzenia przestrzeni nazw jakiejś formy, która jest ortogonalny do bycia OOP. Nie zaleca się również stosowania struktur danych. W rzeczywistości rzeczywiste użycie języków FP zależy w dużej mierze od struktur danych. Jeśli spojrzysz na stos f# zaimplementowany w F Sharp Programowanie / zaawansowane struktury danych, przekonasz się, że nie jest ona zorientowana obiektowo.

W moim umyśle OOP jest silnie związane z posiadaniem metod, które działają na stan obiektu głównie po to, aby mutować obiekt. W czystym kontekście PR, który nie jest potrzebny ani pożądane.

Praktycznym powodem może być możliwość interakcji z kodem OOP, tak samo jak F# działa z . NET . Poza tym jednak, czy są jakieś powody? A jakie jest doświadczenie w świecie Haskell, gdzie programowanie jest czystszym FP?

Będę wdzięczna za wszelkie odniesienia do artykułów lub kontrafaktycznych przykładów rzeczywistych na ten temat.

Author: Peter Mortensen, 2010-11-06

3 answers

The disconnect you see is not of FP vs. OOP. Chodzi głównie o niezmienność i formalizmy matematyczne kontra zmienność i nieformalne podejścia.

Po pierwsze, zrezygnujmy z kwestii zmienności: możesz mieć FP z zmiennością i OOP z niezmiennością w sam raz. Jeszcze bardziej-funkcjonalny-niż-ty Haskell pozwala grać z mutable danych, co chcesz, po prostu trzeba być jednoznacznym o tym, co jest mutable i kolejności, w której rzeczy się dzieją; i wydajność dotyczy bok, prawie każdy zmienny obiekt może konstruować i zwracać nową," zaktualizowaną " instancję zamiast zmieniać swój stan wewnętrzny.

Większym problemem są formalizmy matematyczne, w szczególności intensywne stosowanie algebraicznych typów danych w języku niewiele usuniętym z rachunku lambda. Oznaczyłeś to Haskellem i F# , ale zdaj sobie sprawę, że to tylko połowa uniwersum programowania funkcyjnego; Rodzina Lisp ma zupełnie inny, znacznie bardziej swobodny charakter w porównaniu z językami w stylu ML. Większość OO systemy w powszechnym użyciu mają dziś bardzo nieformalny charakter-formalizmy istnieją dla oo, ale nie są wywoływane wprost tak, jak formalizmy FP są w językach w stylu ML.

Wiele pozornych konfliktów po prostu zniknie, jeśli usuniesz niedopasowanie formalizmu. Chcesz zbudować elastyczny, dynamiczny, ad-hoc system OO na Lispie? Śmiało, będzie dobrze. Chcesz dodać sformalizowany, niezmienny system OO do języka w stylu ML? Nie ma problemu, po prostu nie oczekuj, że będzie dobrze grać z. NET lub Java.


Teraz, być może zastanawiasz się, co jest odpowiednim formalizmem dla OOP? Oto puenta: pod wieloma względami jest bardziej funkcjonalny niż FP w stylu ML! Odnoszę się do jednej z moich ulubionych prac dla tego, co wydaje się być kluczowym rozróżnieniem: dane strukturalne, takie jak algebraiczne typy danych w językach w stylu ML, zapewniają konkretną reprezentację danych i możliwość definiowania operacji na nich; obiekty zapewniają abstrakcję czarnej skrzynki nad danymi. zachowanie i możliwość łatwej wymiany komponentów.

Istnieje dwoistość, która sięga głębiej niż tylko FP vs. OOP: jest ściśle związana z tym, co niektórzy teoretycy języka programowania nazywają problemem wyrażeń : z konkretnymi danymi można łatwo dodać nowe operacje, które z nimi współpracują, ale zmiana struktury danych jest trudniejsza. Z obiektami można łatwo dodawać nowe dane (np. nowe podklasy), ale dodawanie nowych operacji jest trudne (pomyśl, że dodanie nowego metoda abstrakcyjna do klasy bazowej z wieloma potomkami).

Powodem, dla którego mówię, że OOP jest bardziej skoncentrowany na funkcjach, jest to, że same funkcje reprezentują formę abstrakcji behawioralnej. W rzeczywistości, można symulować strukturę OO-style w czymś takim jak Haskell, używając rekordów posiadających kilka funkcji jako obiektów, pozwalając typ rekordu być" interfejsem "lub" abstrakcyjną klasą bazową", a funkcje tworzące rekordy zastępują konstruktory klas. Więc w tym sensie, OO języki używają funkcji wyższego rzędu znacznie częściej niż Haskell.

Dla przykładu czegoś podobnego do tego typu konstrukcji, które są bardzo przydatne w Haskell, przeczytaj Źródło pakietu graphics-drawingcombinators , w szczególności sposób, w jaki używa nieprzezroczystego typu rekordu zawierającego funkcje i łączy rzeczy tylko pod względem ich zachowania.


EDIT: kilka ostatnich rzeczy, o których zapomniałem wspomnieć powyżej.

Jeśli oo rzeczywiście sprawia szerokie wykorzystanie funkcji wyższego rzędu, może na początku wydawać się, że powinien pasować bardzo naturalnie do języka funkcjonalnego, takiego jak Haskell. Niestety tak nie jest. Prawdą jest, że obiekty {[11] } jak je opisałem (por. artykuł wymieniony w linku LtU) pasuje idealnie. w rzeczywistości rezultatem jest bardziej czysty styl OO niż większość języków oo, ponieważ "prywatne elementy" są reprezentowane przez wartości ukryte przez zamknięcie używane do konstruowania "obiektu" i są niedostępne dla cokolwiek innego niż jeden konkretny przypadek. Nie masz więcej prywatności niż to!

To, co nie działa zbyt dobrze w Haskell, to Podtyp. I chociaż myślę, że dziedziczenie i podtypowanie są zbyt często nadużywane w językach OO, pewna forma podtypowania jest całkiem przydatna, aby móc łączyć obiekty w elastyczny sposób. Haskell nie ma wrodzonego pojęcia podtypowania, a ręcznie zwijane zamienniki wydają się być niezdarne w pracy.

Jako bok, większość języków OO z systemami typów statycznych tworzy również kompletny hash podtypowania, ponieważ jest zbyt luźny z substytutowalnością i nie zapewnia odpowiedniego wsparcia dla wariancji w podpisach metod. W rzeczywistości, myślę, że jedynym pełnowymiarowym językiem OO, który nie spieprzył tego całkowicie, przynajmniej o czym wiem, Jest Scala (f # zdawało się robić zbyt wiele ustępstw dla.NET, chociaż przynajmniej nie sądzę, że popełnia jakieś nowe błędy). Mam ograniczone doświadczenie z wieloma takimi językami, więc zdecydowanie może się mylić.

W specyficznej dla Haskella notce, jej "klasy typu" często wyglądają kusząco dla programistów OO, na co mówię: nie idź tam. Próba wdrożenia OOP w ten sposób skończy się tylko łzami. Pomyśl o klasach typu jako o zastępstwie przeciążonych funkcji / operatorów, a nie OOP.

 52
Author: C. A. McCann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-06 19:21:04

Jeśli chodzi o Haskella, klasy są tam mniej przydatne, ponieważ niektóre funkcje OO są łatwiejsze do osiągnięcia w inny sposób.

Enkapsulacja lub" ukrywanie danych " często odbywa się poprzez zamykanie funkcji lub typów egzystencjalnych, a nie prywatnych członków. Na przykład, oto typ danych generatora liczb losowych ze stanem zamkniętym. RNG zawiera metodę generowania wartości i wartość zalążkową. Ponieważ Typ "seed" jest zamknięty, jedyne, co można z nim zrobić, to przekazać go do metoda.
data RNG a where RNG :: (seed -> (a, seed)) -> seed -> RNG a

Metoda dynamiczna w kontekście polimorfizmu parametrycznego lub "programowania ogólnego" jest dostarczana przez klasy typu (które nie są klasami OO). Klasa type jest jak wirtualna tabela metod klasy OO. Jednak dane nie są ukrywane. Klasy typu nie "należą" do typu danych tak, jak metody klasy.

data Coordinate = C Int Int

instance Eq Coordinate where C a b == C d e = a == b && d == e

Metoda dynamiczna w kontekście podtypowania polimorfizmu lub "podklasowania" jest niemal tłumaczeniem wzorca klasy w Haskell za pomocą rekordów i funkcji.

-- An "abstract base class" with two "virtual methods"
data Object =
  Object
  { draw :: Image -> IO ()
  , translate :: Coord -> Object
  }

-- A "subclass constructor"
circle center radius = Object draw_circle translate_circle
  where
    -- the "subclass methods"
    translate_circle center radius offset = circle (center + offset) radius
    draw_circle center radius image = ...
 8
Author: Heatsink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-06 13:34:07

Myślę, że istnieje kilka sposobów zrozumienia, co oznacza OOP. Dla mnie nie chodzi o enkapsulację mutable state, ale bardziej o organizowanie i strukturyzowanie programów. Ten aspekt OOP może być doskonale stosowany w połączeniu z koncepcjami FP.

Uważam, że mieszanie tych dwóch pojęć w F# jest bardzo użytecznym podejściem - można skojarzyć Stan niezmienny z operacjami działającymi na tym stanie. Otrzymasz ładne funkcje "kropki" dla identyfikatory, możliwość łatwego użycia kodu F # z C#, itp. ale nadal możesz sprawić, że Twój kod będzie funkcjonalny. Na przykład możesz napisać coś w stylu:

type GameWorld(characters) = 
  let calculateSomething character = 
    // ...
  member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething
    GameWorld(newCharacters)

Na początku ludzie zwykle nie deklarują typów W F# - możesz zacząć od pisania funkcji, a później ewoluować swój kod, aby z nich korzystać (kiedy lepiej zrozumiesz domenę i wiesz, jaki jest najlepszy sposób na strukturę kodu). Powyższy przykład:

  • jest nadal czysto funkcjonalny (stan jest listą postaci i nie jest zmutowany)
  • Jest ona zorientowana obiektowo - jedyną niezwykłą rzeczą jest to, że wszystkie metody zwracają nową instancję "świata".]}
 6
Author: Tomas Petricek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-14 19:34:44