Wiem, że koncepcja niezmienników istnieje w wielu paradygmatach programowania. Na przykład niezmienniki pętli są istotne w OO, programowaniu funkcjonalnym i proceduralnym.
Jednak jednym bardzo przydatnym rodzajem znalezionym w OOP jest niezmiennik danych określonego typu. To właśnie nazywam w tytule „niezmiennikami opartymi na typie”. Na przykład Fraction
typ może mieć numerator
a denominator
, przy niezmienniku, że ich gcd wynosi zawsze 1 (tzn. Ułamek jest w postaci zredukowanej). Mogę to zagwarantować tylko poprzez pewnego rodzaju enkapsulację tego typu, nie pozwalając na swobodne ustawianie danych. W zamian nigdy nie muszę sprawdzać, czy jest zmniejszone, więc mogę uprościć algorytmy, takie jak kontrole równości.
Z drugiej strony, jeśli po prostu zadeklaruję Fraction
typ bez zapewnienia tej gwarancji poprzez enkapsulację, nie mogę bezpiecznie napisać żadnych funkcji tego typu, które zakładają, że ułamek jest zmniejszony, ponieważ w przyszłości ktoś inny mógłby przyjść i dodać sposób zdobycia niezredukowanej frakcji.
Zasadniczo brak tego rodzaju niezmiennika może prowadzić do:
- Bardziej złożone algorytmy, ponieważ warunki wstępne muszą być sprawdzane / zapewniane w wielu miejscach
- Naruszenia OSUSZANIA, ponieważ te powtarzające się warunki wstępne reprezentują tę samą wiedzę podstawową (że niezmiennik powinien być prawdziwy)
- Konieczność egzekwowania warunków wstępnych przez awarie środowiska wykonawczego zamiast gwarancji czasu kompilacji
Moje pytanie brzmi więc, jaka jest funkcjonalna odpowiedź programowa na tego rodzaju niezmiennik. Czy istnieje funkcjonalnie-idiomatyczny sposób osiągnięcia mniej więcej tego samego? Czy jest jakiś aspekt programowania funkcjonalnego, który sprawia, że korzyści są mniej istotne?
źródło
PrimeNumber
klasę. Wykonanie wielu redundantnych kontroli pierwszeństwa dla każdej operacji byłoby zbyt drogie, ale nie jest to rodzaj testu, który można wykonać w czasie kompilacji. (Wiele operacji, które chciałbyś wykonać na liczbach pierwszych, powiedzmy, mnożenie, nie tworzy zamknięcia , tzn. Wyniki prawdopodobnie nie są gwarantowane jako liczby pierwsze. (Publikowanie jako komentarz, ponieważ sam nie znam programowania funkcjonalnego.)Odpowiedzi:
Niektóre języki funkcjonalne, takie jak OCaml, mają wbudowane mechanizmy implementujące abstrakcyjne typy danych, które wymuszają niektóre niezmienniki . Języki, które nie mają takich mechanizmów, polegają na tym, że użytkownik „nie zagląda pod dywan” w celu wymuszenia niezmienników.
Abstrakcyjne typy danych w OCaml
W OCaml moduły są używane do struktury programu. Moduł ma implementację i podpis , przy czym ten ostatni jest rodzajem podsumowania wartości i typów zdefiniowanych w module, podczas gdy ten pierwszy zawiera rzeczywiste definicje. Można to luźno porównać do dyptyku
.c/.h
znanego programistom C.Jako przykład możemy zaimplementować
Fraction
moduł w następujący sposób:Tej definicji można teraz używać w następujący sposób:
Każdy może wytworzyć wartości frakcji typu bezpośrednio, omijając wbudowaną siatkę bezpieczeństwa
Fraction.make
:Aby temu zapobiec, możliwe jest ukrycie konkretnej definicji typu
Fraction.t
:Jedynym sposobem na utworzenie
AbstractFraction.t
jest użycieAbstractFraction.make
funkcji.Abstrakcyjne typy danych w schemacie
Język schematu nie ma tego samego mechanizmu abstrakcyjnych typów danych, co OCaml. Polega ona na tym, że użytkownik „nie patrzy pod dywan”, aby uzyskać hermetyzację.
W schemacie zwyczajowo definiuje się predykaty, takie jak
fraction?
rozpoznawanie wartości, dające możliwość weryfikacji danych wejściowych. Z mojego doświadczenia wynika, że dominującym zastosowaniem jest umożliwienie użytkownikowi sprawdzania poprawności danych wejściowych, jeśli wykuwa wartość, zamiast sprawdzania poprawności danych wejściowych w każdym wywołaniu biblioteki.Istnieje jednak kilka strategii wymuszających abstrakcję zwracanych wartości, takich jak zwracanie zamknięcia, które daje wartość po zastosowaniu lub zwracanie odwołania do wartości w puli zarządzanej przez bibliotekę - ale nigdy nie widziałem żadnej z nich w praktyce.
źródło
Hermetyzacja nie jest funkcją dostarczaną z OOP. Ma go każdy język, który obsługuje odpowiednią modularyzację.
Oto w przybliżeniu, jak to robisz w Haskell:
Teraz, aby utworzyć produkt Rational, użyj funkcji ratio, która wymusza niezmiennik. Ponieważ dane są niezmienne, nie można później naruszyć niezmiennika.
To cię jednak kosztuje: użytkownik nie może już używać tej samej deklaracji dekonstrukcyjnej, co w mianowniku i liczniku.
źródło
Robisz to w ten sam sposób: stwórz konstruktor, który wymusza ograniczenie, i zgódź się na użycie tego konstruktora za każdym razem, gdy utworzysz nową wartość.
Ale Karl, w OOP nie musisz zgadzać się na użycie konstruktora. Naprawdę?
W rzeczywistości możliwości tego rodzaju nadużyć są mniejsze w FP. Ci mają umieścić konstruktor ostatni, ze względu na niezmienność. Chciałbym, żeby ludzie przestali myśleć o enkapsulacji jako o jakiejś ochronie przed niekompetentnymi kolegami lub jako o unikaniu konieczności komunikowania się z ograniczeniami. To nie robi tego. To ogranicza miejsca, które musisz sprawdzić. Dobrzy programiści FP również używają enkapsulacji. Po prostu występuje w postaci przekazywania kilku preferowanych funkcji w celu dokonania pewnych modyfikacji.
źródło