Interfejsy API i programowanie funkcjonalne

15

Z mojej (co prawda ograniczonej) ekspozycji na funkcjonalne języki programowania, takie jak Clojure, wydaje się, że enkapsulacja danych ma mniej istotną rolę. Zwykle różne typy rodzime, takie jak mapy lub zestawy, są preferowaną walutą reprezentowania danych nad obiektami. Ponadto dane te są zasadniczo niezmienne.

Oto na przykład jeden z bardziej znanych cytatów ze sławy Richa Hickeya z Clojure w wywiadzie na ten temat :

Fogus: Zgodnie z tym pomysłem - niektórzy ludzie są zaskoczeni faktem, że Clojure nie angażuje się w enkapsulację ukrywania danych na swoich typach. Dlaczego zdecydowałeś się zrezygnować z ukrywania danych?

Hickey: Wyjaśnijmy, że Clojure zdecydowanie kładzie nacisk na programowanie na abstrakcje. W pewnym momencie ktoś będzie musiał mieć dostęp do danych. A jeśli masz pojęcie „prywatny”, potrzebujesz odpowiednich pojęć przywileju i zaufania. A to dodaje całej tony złożoności i małej wartości, stwarza sztywność w systemie i często zmusza rzeczy do życia w miejscach, w których nie powinny. Jest to dodatek do innych strat, które występują, gdy proste informacje są wprowadzane do klas. W zakresie, w jakim dane są niezmienne, istnieje niewiele szkód, które mogą wyniknąć z zapewnienia dostępu, poza tym, że ktoś może polegać na czymś, co może się zmienić. No dobrze, ludzie robią to przez cały czas w prawdziwym życiu, a kiedy wszystko się zmienia, dostosowują się. A jeśli są racjonalni, wiedzą, kiedy podejmują decyzję na podstawie czegoś, co może się zmienić, co w przyszłości mogą wymagać dostosowania. Jest to więc decyzja dotycząca zarządzania ryzykiem, którą moim zdaniem programiści powinni mieć swobodę. Jeśli ludzie nie mają wrażliwości, aby chcieć programować abstrakcje i nieufnie podchodzić do szczegółów implementacji, to nigdy nie będą dobrymi programistami.

Pochodzący ze świata OO wydaje się, że komplikuje to niektóre z ugruntowanych zasad, których nauczyłem się przez lata. Należą do nich Ukrywanie informacji, prawo Demeter i zasada jednolitego dostępu, by wymienić tylko kilka. Powszechnym wątkiem jest to, że enkapsulacja pozwala nam zdefiniować interfejs API, aby inni wiedzieli, co powinni, a czego nie powinni dotykać. Zasadniczo, tworzenie kontraktu, który pozwala opiekunowi jakiegoś kodu na swobodne wprowadzanie zmian i refaktoryzacji bez obawy o to, jak może wprowadzić błędy w kodzie konsumenta (zasada Otwarta / Zamknięta). Zapewnia również czysty, wyrafinowany interfejs dla innych programistów, aby wiedzieć, jakich narzędzi mogą użyć, aby uzyskać dane lub je wykorzystać.

Gdy dostęp do danych jest możliwy bezpośrednio, umowa API zostaje zerwana i wydaje się, że wszystkie zalety enkapsulacji znikają. Również dane ściśle niezmienne sprawiają, że przekazywanie struktur specyficznych dla domeny (obiektów, struktur, rekordów) jest znacznie mniej przydatne w sensie reprezentowania stanu i zestawu działań, które można wykonać w tym stanie.

W jaki sposób funkcjonalne bazy kodu rozwiązują te problemy, które wydają się pojawiać, gdy rozmiar bazy kodu rośnie, tak że należy zdefiniować interfejsy API i wielu programistów jest zaangażowanych w pracę z określonymi częściami systemu? Czy są dostępne przykłady tej sytuacji, które pokazują, jak to jest obsługiwane w tego rodzaju bazach kodów?

jameslk
źródło
2
Możesz zdefiniować formalny interfejs bez pojęcia obiektów. Wystarczy utworzyć funkcję interfejsu dokumentującego je. Nie dostarczaj dokumentacji zawierającej szczegółowe informacje dotyczące implementacji. Właśnie utworzyłeś interfejs.
Scara95
@ Scara95 Czy to nie znaczy, że muszę wykonać pracę, aby zarówno zaimplementować kod interfejsu, jak i napisać o nim wystarczającą ilość dokumentacji, aby ostrzec konsumenta, co robić, a czego nie robić? Co się stanie, jeśli kod się zmieni, a dokumentacja stanie się nieaktualna? Z tego powodu generalnie wolę kod samodokumentujący.
jameslk
W każdym razie musisz udokumentować interfejs.
Scara95
3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.Nie całkiem. Jedyną zmianą jest to, że zmiany kończą się na nowym obiekcie. To ogromna wygrana, jeśli chodzi o rozumowanie na temat kodu; przekazywanie zmiennych obiektów oznacza konieczność śledzenia, kto może je mutować, problem ten rośnie wraz z rozmiarem kodu.
Doval,

Odpowiedzi:

10

Przede wszystkim zajmę się komentarzami Sebastiana na temat tego, co jest prawidłowe, a co dynamicznego pisania. Mówiąc bardziej ogólnie, Clojure jest jednym ze smaków funkcjonalnego języka i społeczności i nie powinieneś uogólniać zbytnio na nim. Zrobię kilka uwag z bardziej perspektywy ML / Haskell.

Jak wspomina Basile, koncepcja kontroli dostępu istnieje w ML / Haskell i jest często stosowana. „Faktoring” różni się nieco od konwencjonalnych języków OOP; w OOP pojęcie klasy odgrywa jednocześnie rolę typu i modułu , podczas gdy funkcjonalne (i tradycyjne procedury proceduralne) traktują je ortogonalnie.

Inną kwestią jest to, że ML / Haskell są bardzo obciążone lekami generycznymi z usuwaniem typu i że można tego użyć, aby zapewnić inny smak „ukrywania informacji” niż kapsułkowanie OOP. Gdy składnik zna tylko typ elementu danych jako parametr typu, można mu bezpiecznie przekazać wartości tego typu, a mimo to nie będzie mógł wiele z nimi zrobić, ponieważ nie zna i nie zna ich konkretnego typu ( instanceofw tych językach nie ma przesyłania uniwersalnego ani runtime). Ten wpis na blogu jest jednym z moich ulubionych wstępnych przykładów tych technik.

Następnie: w świecie FP bardzo często stosuje się przezroczyste struktury danych jako interfejsy do nieprzezroczystych / kapsułkowanych komponentów. Na przykład wzorce interpretera są bardzo powszechne w FP, gdzie struktury danych są używane jako drzewa składniowe opisujące logikę i podawane do kodu, który je „wykonuje”. Stan, właściwie powiedziane, wtedy istnieje efemerycznie, gdy działa interpreter, który zużywa struktury danych. Również implementacja interpretera może ulec zmianie, o ile nadal komunikuje się z klientami w zakresie tych samych typów danych.

Ostatni i najdłuższy: enkapsulacja / ukrywanie informacji to technika , a nie koniec. Pomyślmy trochę o tym, co zapewnia. Enkapsulacja to technika uzgadniania kontraktu i wdrożenia jednostki oprogramowania. Typowa sytuacja jest taka: wdrożenie systemu uznaje wartości lub stwierdza, że ​​zgodnie z umową nie powinno istnieć.

Kiedy spojrzysz na to w ten sposób, możemy zauważyć, że FP zapewnia, oprócz enkapsulacji, szereg dodatkowych narzędzi, których można użyć do tego samego celu:

  1. Niezmienność jako wszechobecna wartość domyślna. Możesz przekazać przezroczyste wartości danych kodowi strony trzeciej. Nie mogą ich modyfikować i wprowadzać w nieprawidłowe stany. (Odpowiedź Karla na to wskazuje.)
  2. Wyrafinowane systemy typów z algebraicznymi typami danych, które pozwalają dokładnie kontrolować strukturę typów bez pisania dużej ilości kodu. Korzystając z tych funkcji rozsądnie, często można projektować typy, w których „złe stany” są po prostu niemożliwe. (Slogan: „Uczyń nielegalne stany niereprezentatywnymi.” ) Zamiast używać enkapsulacji do pośredniej kontroli zestawu dopuszczalnych stanów klasy, wolałbym po prostu powiedzieć kompilatorowi, jakie to są, i mieć to dla mnie!
  3. Wzorzec tłumacza, jak już wspomniano. Jednym z kluczy do zaprojektowania dobrego abstrakcyjnego typu drzewa składni jest:
    • Spróbuj zaprojektować abstrakcyjny typ danych drzewa składni, aby wszystkie wartości były „prawidłowe”.
    • W przeciwnym razie interpreter wyraźnie wykrywa nieprawidłowe kombinacje i czysto je odrzuca.

Ta seria „Projektowanie z typami” F # zapewnia całkiem przyzwoitą lekturę na niektóre z tych tematów, w szczególności nr 2. (Stąd pochodzi link „uczyń nielegalne stany niereprezentatywnymi” z góry.) Jeśli przyjrzysz się uważnie, zauważysz, że w drugiej części pokazują one, jak używać enkapsulacji do ukrywania konstruktorów i uniemożliwiać klientom tworzenie nieprawidłowych instancji. Jak powiedziałem powyżej, jest to część zestawu narzędzi!

sacundim
źródło
9

Naprawdę nie mogę przecenić stopnia, w jakim zmienność powoduje problemy w oprogramowaniu. Wiele praktyk wbijanych w nasze głowy rekompensuje problemy powodowane przez zmienność. Kiedy zabierasz zmienność, nie potrzebujesz tych praktyk tak bardzo.

Gdy masz niezmienność, wiesz, że twoja struktura danych nie zmieni się niespodziewanie pod tobą podczas działania, więc możesz tworzyć własne pochodne struktury danych do własnego użytku, dodając funkcje do swojego programu. Oryginalna struktura danych nie musi nic wiedzieć o tych pochodnych strukturach danych.

Oznacza to, że Twoje podstawowe struktury danych wydają się być wyjątkowo stabilne. Nowe struktury danych są pobierane z nich wokół krawędzi w razie potrzeby. Naprawdę trudno to wytłumaczyć, dopóki nie wykonasz znaczącego programu funkcjonalnego. Po prostu coraz mniej dbasz o prywatność i coraz częściej myślisz o tworzeniu trwałych ogólnych struktur danych publicznych.

Karl Bielefeldt
źródło
Chciałbym dodać, że niezmienna zmienna powoduje, że programiści trzymają się rozproszonej i rozproszonej struktury danych, jeśli w ogóle istnieje taka struktura. Wszystkie dane są skonstruowane w taki sposób, aby tworzyły grupę logiczną, dla łatwego wyszukiwania i przechodzenia, a nie transportu. Jest to logiczny postęp, który wykonasz, gdy wykonasz wystarczająco dużo programowania funkcjonalnego.
Xephon
8

Skłonność Clojure do używania skrótów i prymitywów nie jest, moim zdaniem, częścią jego dziedzictwa funkcjonalnego, ale częścią jego dynamicznego dziedzictwa. Widziałem podobne tendencje w Pythonie i Ruby (zarówno obiektowe, imperatywne, jak i dynamiczne, chociaż oba mają dość dobrą obsługę funkcji wyższego rzędu), ale nie, powiedzmy, w Haskell (który jest typowo statyczny, ale czysto funkcjonalny , ze specjalnymi konstrukcjami potrzebnymi do uniknięcia niezmienności).

Pytanie, które musisz zadać, nie brzmi: w jaki sposób języki funkcjonalne obsługują duże interfejsy API, ale jak to robią języki dynamiczne. Odpowiedź brzmi: dobra dokumentacja i wiele testów jednostkowych. Na szczęście współczesne dynamiczne języki zazwyczaj mają bardzo dobre wsparcie dla obu; na przykład zarówno Python, jak i Clojure potrafią osadzać dokumentację w samym kodzie, a nie tylko komentarze.

Sebastian Redl
źródło
Jeśli chodzi o statycznie wpisane (czysto) funkcjonalne języki, nie ma (prostego) sposobu na przeniesienie funkcji o typie danych, jak w programowaniu OO. Tak więc dokumentacja ma znaczenie. Chodzi o to, że nie potrzebujesz obsługi języka, aby zdefiniować interfejs.
Scara95
5
@ Scara95 Czy możesz wyjaśnić, co rozumiesz przez „nieść funkcję z typem danych”?
Sebastian Redl,
6

Niektóre języki funkcjonalne dają możliwość kapsułkowania lub ukrywania szczegółów implementacji w abstrakcyjnych typach danych i modułach .

Na przykład OCaml ma moduły zdefiniowane przez zbiór nazwanych typów abstrakcyjnych i wartości (w szczególności funkcje działające na tych typach abstrakcyjnych). W pewnym sensie moduły Ocaml są interfejsami API. Ocaml ma również funktory, które przekształcają niektóre moduły w inne, zapewniając w ten sposób ogólne programowanie. Moduły są więc złożone.

Basile Starynkevitch
źródło