Niektórzy mówią, że jeśli doprowadzisz ich ekstremalne zasady do SOLID, skończysz na programowaniu funkcjonalnym . Zgadzam się z tym artykułem, ale myślę, że niektóre semantyki zostały utracone podczas przejścia z interfejsu / obiektu do funkcji / zamknięcia i chcę wiedzieć, w jaki sposób programowanie funkcjonalne może złagodzić utratę.
Z artykułu:
Ponadto, jeśli rygorystycznie zastosujesz zasadę segregacji interfejsów (ISP), zrozumiesz, że powinieneś faworyzować interfejsy ról zamiast interfejsów nagłówka.
Jeśli nadal będziesz kierować swoim projektem w kierunku coraz mniejszych interfejsów, ostatecznie dojdziesz do ostatecznego interfejsu roli: interfejsu z jedną metodą. To mi się często zdarza. Oto przykład:
public interface IMessageQuery
{
string Read(int id);
}
Jeśli wezmę zależność od IMessageQuery
, częścią niejawnej umowy jest to, że wywołanie Read(id)
wyszuka i zwróci wiadomość o podanym identyfikatorze.
Porównajmy to do robienia zależność od jego odpowiednik podpisu funkcjonalnej int -> string
. Bez dodatkowych wskazówek ta funkcja może być prosta ToString()
. Jeśli wdrożysz IMessageQuery.Read(int id)
za pomocą ToString()
I, mogę cię oskarżyć o celową wywrotę!
Co zatem mogą zrobić funkcjonalni programiści, aby zachować semantykę dobrze nazwanego interfejsu? Czy konwencjonalne jest na przykład tworzenie typu rekordu z jednym elementem?
type MessageQuery = {
Read: int -> string
}
źródło
Without any additional clues
... może dlatego dokumentacja jest częścią umowy ?Odpowiedzi:
Jak mówi Telastyn, porównanie statycznych definicji funkcji:
do
Tak naprawdę nie straciłeś niczego, przechodząc od OOP do FP.
Jest to jednak tylko część historii, ponieważ funkcje i interfejsy są wymienione nie tylko w ich statycznych definicjach. Są także przekazywane . Powiedzmy, że nasz
MessageQuery
został odczytany przez inny fragment kodu, aMessageProcessor
. Potem będzie:Teraz nie możemy bezpośrednio zobaczyć nazwy metody
IMessageQuery.Read
ani jej parametruint id
, ale bardzo łatwo możemy się do niej dostać dzięki naszemu IDE. Mówiąc bardziej ogólnie, fakt, że przekazujemyIMessageQuery
raczej niż dowolny interfejs za pomocą metody funkcji od int do string oznacza, że zachowujemyid
metadane nazwy parametru związane z tą funkcją.Z drugiej strony, dla naszej wersji funkcjonalnej mamy:
Co więc zatrzymaliśmy i straciliśmy? Cóż, nadal mamy nazwę parametru
messageReader
, co prawdopodobnie sprawia, że nazwa typu (odpowiednikIMessageQuery
) jest niepotrzebna. Ale teraz straciliśmy nazwę parametruid
w naszej funkcji.Są na to dwa główne sposoby:
Po pierwsze, po przeczytaniu tego podpisu możesz już całkiem dobrze zgadnąć, co się dzieje. Dzięki krótkim, prostym i spójnym funkcjom oraz dobrym nazewnictwu łatwiej jest intuicyjnie znaleźć te informacje. Gdy tylko zaczniemy czytać samą funkcję, będzie to jeszcze prostsze.
Po drugie, w wielu językach funkcjonalnych uważa się projektowanie idiomatyczne za tworzenie małych typów do zawijania prymitywów. W tym przypadku dzieje się odwrotnie - zamiast zamiany nazwy typu na nazwę parametru (
IMessageQuery
domessageReader
) możemy zastąpić nazwę parametru nazwą typu. Na przykładint
może być zawinięty w typ o nazwieId
:Teraz nasz
read
podpis staje się:Co jest tak samo pouczające jak to, co mieliśmy wcześniej.
Na marginesie, zapewnia to nam również pewną ochronę kompilatora, którą mieliśmy w OOP. Podczas gdy wersja OOP zapewniła, że wybraliśmy konkretną,
IMessageQuery
a nie jakąkolwiek starąint -> string
funkcję, tutaj mamy podobną (ale inną) ochronę, którą przyjmujemyId -> string
raczej niż jakąkolwiek starąint -> string
.Nie chciałbym powiedzieć ze 100% pewnością, że te techniki zawsze będą tak samo dobre i pouczające, jak posiadanie pełnych informacji dostępnych w interfejsie, ale myślę, że z powyższych przykładów można powiedzieć, że przez większość czasu prawdopodobnie wykona równie dobrą robotę.
źródło
Podczas wykonywania FP mam tendencję do używania bardziej specyficznych typów semantycznych.
Na przykład twoja metoda dla mnie mogłaby wyglądać następująco:
To komunikuje o wiele więcej niż
ThingDoer.doThing()
styl OO (/ java)źródło
read: MessageId -> Message
ci tostring MessageReader.GetMessage(int messageId)
mówi?Używaj dobrze nazwanych funkcji.
IMessageQuery::Read: int -> string
po prostu staje sięReadMessageQuery: int -> string
lub coś podobnego.Najważniejszą rzeczą do zapamiętania jest to, że nazwy są umowami tylko w najluzszym tego słowa znaczeniu. Działają tylko wtedy, gdy ty i inny programista wywnioskujecie te same implikacje z nazwy, i będziecie ich przestrzegać. Z tego powodu możesz naprawdę użyć dowolnej nazwy, która komunikuje to dorozumiane zachowanie. OO i programowanie funkcjonalne mają swoje nazwy w nieco innych miejscach i w nieco różnych kształtach, ale ich funkcja jest taka sama.
Nie w tym przykładzie. Jak wyjaśniłem powyżej, pojedyncza klasa / interfejs z pojedynczą funkcją nie jest znacznie bardziej pouczająca niż podobnie nazwana samodzielna funkcja.
Gdy uzyskasz więcej niż jedną funkcję / pole / właściwość w klasie, możesz wnioskować o nich więcej informacji, ponieważ możesz zobaczyć ich relację. Jest to dyskusyjne, jeśli jest to bardziej informacyjne niż samodzielne funkcje, które przyjmują te same / podobne parametry lub samodzielne funkcje zorganizowane według przestrzeni nazw lub modułu.
Osobiście nie sądzę, że OO jest znacznie bardziej pouczające, nawet w bardziej złożonych przykładach.
źródło
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
Theid
parametr jest po prostuint
. Myślę, że jednym argumentem jest to, że powinieneś zdaćId
, a nieint
. W rzeczywistości byłaby to przyzwoita odpowiedź sama w sobie.Nie zgadzam się, że żadna funkcja nie może mieć „kontraktu semantycznego”. Rozważ te prawa dla
foldr
:W jakim sensie nie jest to semantyczna ani umowa? Nie musisz definiować typu „fałdera”, zwłaszcza, że
foldr
jest to jednoznacznie określone przez te prawa. Wiesz dokładnie, co to zrobi.Jeśli chcesz przyjąć jakąś funkcję, możesz zrobić to samo:
Musisz tylko nazwać i uchwycić ten typ, jeśli potrzebujesz tej samej umowy wiele razy:
Sprawdzanie typów nie będzie wymuszać jakiejkolwiek semantyki przypisanej do typu, więc tworzenie nowego typu dla każdego kontraktu jest po prostu wzorcem.
źródło
foldr
. Wskazówka: definicjafoldr
ma dwa równania (dlaczego?), A powyższa specyfikacja ma trzy.Prawie wszystkie statycznie wpisane języki funkcjonalne mają sposób na alias podstawowych typów w sposób, który wymaga jawnego zadeklarowania swojego semantycznego zamiaru. Niektóre inne odpowiedzi podały przykłady. W praktyce doświadczeni programiści funkcjonalni potrzebują bardzo dobrych powodów, aby korzystać z tych rodzajów opakowań, ponieważ szkodzą one możliwości łączenia i ponownego użycia.
Załóżmy na przykład, że klient chciał implementacji zapytania dotyczącego wiadomości, które zostało poparte listą. W Haskell implementacja może być tak prosta:
Korzystanie z
newtype Message = Message String
tego byłoby o wiele mniej proste, a implementacja wyglądałaby mniej więcej tak:Może to nie wydawać się wielkim problemem, ale albo musisz przeprowadzić konwersję tego typu tam i z powrotem wszędzie , albo ustawić warstwę graniczną w kodzie, w której wszystko powyżej jest
Int -> String
następnie konwertowane,Id -> Message
aby przejść do warstwy poniżej. Powiedzmy, że chcę dodać internacjonalizację lub sformatować ją wielkimi literami, dodać kontekst rejestrowania lub cokolwiek innego. Wszystkie te operacje są bardzo proste do skomponowania za pomocąInt -> String
i denerwujące za pomocąId -> Message
. Nie jest tak, że nigdy nie ma przypadków, w których pożądane są zwiększone ograniczenia typów, ale irytacja powinna być bardziej warta wymiany.Możesz użyć synonimu typu zamiast opakowania (w Haskell
type
zamiastnewtype
), co jest znacznie bardziej powszechne i nie wymaga konwersji wszędzie, ale nie zapewnia statycznych typów gwarancji, takich jak wersja OOP, tylko trochę enapsulacji. Owijarki typów są najczęściej używane tam, gdzie klient nie powinien w ogóle manipulować wartością, wystarczy ją zapisać i przekazać z powrotem. Na przykład uchwyt pliku.Nic nie stoi na przeszkodzie, aby klient był „wywrotowy”. Po prostu tworzysz obręcze do przeskoczenia dla każdego klienta. Prosta próbka do wspólnego testu jednostkowego często wymaga dziwnego zachowania, które nie ma sensu w produkcji. Twoje interfejsy powinny być napisane, aby ich to nie obchodziło, jeśli to w ogóle możliwe.
źródło
Id
iMessage
są prostymi opakowaniami dlaInt
iString
, konwersja między nimi jest banalna .