Dlaczego dyskryminowane związki są związane z programowaniem funkcjonalnym?

30

Przez wiele lat programowania OO rozumiałem, czym są dyskryminowane związki, ale tak naprawdę nigdy ich nie przegapiłem. Niedawno zajmowałem się programowaniem funkcjonalnym w języku C # i teraz wciąż żałuję, że ich nie miałem. To mnie zaskakuje, ponieważ na pierwszy rzut oka koncepcja dyskryminowanych związków wydaje się całkiem niezależna od dychotomii funkcjonalnej / OO.

Czy jest coś nieodłącznego w programowaniu funkcjonalnym, które sprawia, że ​​dyskryminowane związki są bardziej użyteczne niż byłyby w OO, czy też jest to, że zmuszając się do analizowania problemu w „lepszy” sposób, po prostu podniosłem swoje standardy i teraz żądam lepszego Model?

Andy
źródło
Problem z wyrażeniem en.wikipedia.org/wiki/Expression_problem może być istotny
XJI
Nie jest to właściwie właściwa odpowiedź na pytanie, więc odpowiem w zamian, ale język programowania Cejlonu ma typy unii i najwyraźniej wkradają się do innych języków OO / mieszanych paradygmatów - przychodzą mi na myśl TypeScript i Scala. Również wyliczenia języka Java mogą być używane jako rodzaj implementacji dyskryminowanych związków.
Roland Tepp,

Odpowiedzi:

45

Zróżnicowane związki naprawdę świecą w połączeniu z dopasowaniem wzorców, w którym wybierasz różne zachowania w zależności od przypadków. Ale ten wzorzec jest zasadniczo przeciwny do czystych zasad OO.

W czystym OO różnice w zachowaniu powinny być definiowane przez same typy (obiekty) i hermetyzowane. Zatem równoważność z dopasowaniem wzorca polegałaby na wywołaniu jednej metody na samym obiekcie, który jest następnie przeciążony przez podtypy, o których mowa, w celu zdefiniowania różnych zachowań. Sprawdzanie typu obiektu z zewnątrz (co robi dopasowanie wzorca) jest uważane za antypattern.

Podstawowa różnica polega na tym, że dane i zachowanie są oddzielne w programowaniu funkcjonalnym, podczas gdy dane i zachowanie są enkapsulowane razem w OO.

To jest powód historyczny. Język taki jak C # rozwija się od klasycznego języka OO do języka z wieloma paradygmatami poprzez wprowadzanie coraz większej liczby funkcji.

JacquesB
źródło
6
Dyskryminowany typ sumy / sumy nie przypomina drzewa dziedziczenia lub wielu klas implementujących interfejs; jest to jeden typ z wieloma rodzajami wartości. Nie zapobiegają również enkapsulacji; klient nie musi wiedzieć, że masz wiele rodzajów wartości. Rozważ listę połączoną, która może być pustym węzłem lub wartością i odniesieniem do innego węzła. Bez typów sum, tak czy owak, włamujesz się do dyskryminowanego związku, mając zmienne wartości i odwołania, ale traktując obiekt z odwołaniem zerowym jako pusty węzeł i udając, że zmienna wartości nie istnieje.
Doval
2
Ogólnie rzecz biorąc, w końcu dołączasz zmienne dla wszystkich możliwych rodzajów wartości w jednej klasie, plus jakiś rodzaj flagi lub wyliczenia, aby powiedzieć ci, jaką wartość ma ta instancja, i tańczysz wokół zmiennych, które nie odpowiadają temu uprzejmy.
Doval
7
@Doval Istnieje „standardowe” kodowanie algebraicznego typu danych do klas poprzez interfejs / abstrakcyjną klasę bazową reprezentującą typ ogólnie i posiadającą podklasę dla każdego przypadku tego typu. Aby obsłużyć dopasowywanie wzorców, masz metodę, która przyjmuje funkcję dla każdego przypadku, więc dla listy, którą miałbyś w interfejsie najwyższego poziomu, List<A>metoda B Match<B>(B nil, Func<A,List<A>,B> cons). Na przykład jest to dokładnie taki sam wzór, jakiego Smalltalk używa dla boolanów. W ten sposób Scala sobie z tym radzi. Używanie wielu klas jest szczegółem implementacji, którego nie trzeba ujawniać.
Derek Elkins,
3
@Doval: Jawne sprawdzanie pustych referencji nie jest tak naprawdę uważane za idiomatyczne OO.
JacquesB
@JacquesB Kontrola zerowa jest szczegółem implementacji. W przypadku klienta połączonej listy istnieje zwykle isEmptymetoda sprawdzająca, czy odwołanie do następnego węzła jest puste.
Doval
35

Po zaprogramowaniu w Pascalu i Adzie przed nauczeniem się programowania funkcjonalnego, nie kojarzę dyskryminowanych związków z programowaniem funkcjonalnym.

Związki dyskryminowane są w pewnym sensie podwójnym dziedzictwem. Pierwszy pozwala łatwo dodawać operacje na stałym zestawie typów (te w unii), a dziedziczenie pozwala na łatwe dodawanie typów za pomocą stałego zestawu operacji. (Jak łatwo dodać oba, nazywa się problemem wyrażania ; jest to szczególnie trudny problem w przypadku języków z systemem typu statycznego).

Ze względu na nacisk OO na typy i podwójny nacisk na programowanie funkcjonalne na funkcje, funkcjonalne języki programowania mają naturalne powinowactwo do typów unii i oferują struktury składniowe ułatwiające ich użycie.

AProgrammer
źródło
7

Techniki programowania imperatywnego, często stosowane w OO, często polegają na dwóch wzorcach:

  1. Udać się lub rzucić wyjątek,
  2. Wróć, nullaby wskazać „brak wartości” lub błąd.

W paradygmacie funkcjonalnym zwykle unika się obydwu, preferując zwrócenie typu związku, który wskazuje przyczynę sukcesu / niepowodzenia lub wartość / brak wartości.

Zróżnicowane związki pasują do rachunku dla tych rodzajów związków. Na przykład w pierwszej kolejności możesz zwrócić truelub strukturę danych opisującą awarię. W drugim przypadku, związku, który zawiera wartość lub none, nilitp drugim przypadku jest tak powszechne, że wiele języków funkcjonalne mają „może” lub „option” typ wbudowany do reprezentowania tej wartości / brak związku.

Po przejściu na styl funkcjonalny z np. C # szybko znajdziesz zapotrzebowanie na te typy związków. void/throwi nullpo prostu nie czuję się dobrze z takim kodem. A dyskryminowane związki (DU) dobrze pasują do rachunku. Tak więc odkryłeś, że chcesz ich, tak jak wielu z nas.

Dobrą wiadomością jest to, że istnieje wiele bibliotek, które modelują DU w np. C # (spójrz na przykład na moją własną bibliotekę Succinc <T> ).

David Arno
źródło
2

Typy sum byłyby na ogół mniej przydatne w głównych językach OO, ponieważ rozwiązują one podobny typ problemu jak podtypowanie OO. Jednym ze sposobów patrzenia na nie jest to, że oba obsługują openpodtypy, ale OO polega na tym, że można dodawać dowolne podtypy do typu rodzica, a typy sum to closednp. Określa się z góry, które podtypy są prawidłowe.

Teraz wiele języków OO łączy podtypy z innymi pojęciami, takimi jak odziedziczone struktury, polimorfizm, pisanie odniesień itp., Aby były ogólnie bardziej użyteczne. Konsekwencją jest to, że wydają się być więcej pracy, aby skonfigurować (z klas i konstruktorów i etażerka), więc raczej nie być używane do takich rzeczy jak ResultS i OptionS i tak dalej, aż generic wpisując stały się powszechne.

Powiedziałbym również, że skupienie się na relacjach w świecie rzeczywistym, których większość ludzi nauczyła się, kiedy rozpoczęli programowanie OO, np. Dog isa Animal, oznaczało, że liczba całkowita isa wynik lub błąd isa wynik wydają się nieco obce. Chociaż pomysły są dość podobne.

Co do tego, dlaczego języki funkcjonalne mogą preferować pisanie zamknięte niż pisanie otwarte, jednym z możliwych powodów jest to, że preferują dopasowanie wzorca. Jest to przydatne w przypadku polimorfizmu funkcji, ale działa również bardzo dobrze w przypadku typów zamkniętych, ponieważ kompilator może statycznie sprawdzić, czy dopasowanie obejmuje wszystkie podtypy. Może to sprawić, że język będzie bardziej spójny, chociaż nie sądzę, aby przyniosła to jakąś naturalną korzyść (mogę się mylić).

Alex
źródło
BTW: Scala ma zamknięte dziedzictwo. sealedoznacza „można rozszerzyć tylko w tej samej jednostce kompilacji”, co pozwala na zamknięcie zestawu podklas w czasie projektowania.
Jörg W Mittag
-3

Swift chętnie stosuje dyskryminujące związki, z tym że nazywa je „wyliczeniami”. Wyliczenia są jedną z pięciu podstawowych kategorii obiektów w Swift, którymi są klasa, struct, enum, krotka i zamknięcie. Opcjonalne Swift to wyliczenia, które dyskryminują związki i są absolutnie niezbędne dla każdego kodu Swift.

Zatem założenie, że „dyskryminujące związki są związane z programowaniem funkcjonalnym” jest błędne.

gnasher729
źródło