Wygląda na to, że kod F # często wzoruje dopasowania względem typów. Na pewno
match opt with
| Some val -> Something(val)
| None -> Different()
wydaje się powszechne.
Ale z punktu widzenia OOP wygląda to bardzo podobnie do przepływu kontroli opartego na sprawdzeniu typu środowiska wykonawczego, na którym zwykle nie można się zgodzić. Aby to przeliterować, w OOP prawdopodobnie wolisz użyć przeciążenia:
type T =
abstract member Route : unit -> unit
type Foo() =
interface T with
member this.Route() = printfn "Go left"
type Bar() =
interface T with
member this.Route() = printfn "Go right"
To z pewnością więcej kodu. OTOH, moim zdaniem OOP-y ma zalety strukturalne:
- rozszerzenie do nowej formy
T
jest łatwe; - Nie muszę się martwić znalezieniem duplikatu przepływu sterowania wyborem trasy; i
- wybór trasy jest niezmienny w tym sensie, że kiedy mam
Foo
pod ręką, nigdy nie muszę się martwićBar.Route()
implementacją
Czy są zalety dopasowywania wzorców do typów, których nie widzę? Czy jest to uważane za idiomatyczne, czy może nie jest powszechnie używane?
object-oriented
functional-programming
f#
pattern-matching
Larry OBrien
źródło
źródło
But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.
- brzmi zbyt dogmatyczne. Czasami chcesz oddzielić swoje operacje od swojej hierarchii: może 1) nie możesz dodać operacji do hierarchii b / c, której nie posiadasz; 2) klasy, które chcesz mieć, nie pasują do twojej hierarchii; 3) możesz dodać op do swojej hierarchii, ale nie chcesz b / c, że nie chcesz zaśmiecać interfejsu API swojej hierarchii bzdurami, których większość klientów nie używa.Some
iNone
nie są typami. Obaj są konstruktorami, których typy sąforall a. a -> option a
iforall a. option a
(przepraszam, nie jestem pewien, jaka jest składnia adnotacji typu w F #).Odpowiedzi:
Masz rację, ponieważ hierarchie klas OOP są bardzo ściśle powiązane z dyskryminowanymi związkami w F # i że dopasowanie wzorca jest bardzo ściśle związane z dynamicznymi testami typu. W rzeczywistości tak właśnie F # kompiluje dyskryminowane związki do .NET!
Jeśli chodzi o rozszerzalność, istnieją dwie strony problemu:
To powiedziawszy, F # wyświetli ostrzeżenie, gdy przegapisz przypadek w dopasowaniu wzorca, więc dodanie nowych spraw związkowych nie jest wcale takie złe.
Jeśli chodzi o znajdowanie duplikatów podczas wybierania katalogu głównego - F # wyświetli ostrzeżenie, gdy masz dopasowanie, które jest zduplikowane, np .:
Problemem może być również fakt, że „wybór trasy jest niezmienny”. Na przykład, jeśli chcesz udostępnić implementację funkcji między przypadkami
Foo
iBar
, ale zrobić coś innego dlaZoo
sprawy, możesz to łatwo zakodować za pomocą dopasowania wzorca:Zasadniczo FP skupia się bardziej na projektowaniu typów, a następnie dodawaniu funkcji. Dlatego naprawdę korzysta z faktu, że możesz dopasować swoje typy (model domeny) w kilku liniach w jednym pliku, a następnie łatwo dodać funkcje, które działają na modelu domeny.
Oba podejścia - OO i FP - są dość komplementarne i oba mają zalety i wady. Trudną rzeczą (pochodzącą z perspektywy OO) jest to, że F # zwykle używa stylu FP jako domyślnego. Ale jeśli naprawdę jest więcej potrzeby dodawania nowych podklas, zawsze możesz użyć interfejsów. Ale w większości systemów musisz również dodać typy i funkcje, więc wybór naprawdę nie ma aż tak wielkiego znaczenia - a używanie dyskryminowanych związków w F # jest przyjemniejsze.
Polecam tę wspaniałą serię blogów, aby uzyskać więcej informacji.
źródło
Prawidłowo zaobserwowałeś, że dopasowanie wzorca (zasadniczo instrukcja doładowania przełącznika) i dynamiczne wysyłanie mają podobieństwa. Współistnieją również w niektórych językach, z bardzo przyjemnym rezultatem. Istnieją jednak niewielkie różnice.
Mógłbym użyć systemu typów do zdefiniowania typu, który może mieć tylko określoną liczbę podtypów:
Nie będzie nigdy być inny podtyp
Bool
lubOption
, tak subclassing nie wydaje się być przydatne (niektóre języki, takie jak Scala mieć pojęcie instacji że można sobie z tym poradzić - klasa może być oznaczona jako „ostatecznego” na zewnątrz bieżącej jednostki kompilacji, ale podtypy można zdefiniować w tej jednostce kompilacji).Ponieważ podtypy tego typu
Option
są teraz znane statycznie , kompilator może ostrzec, jeśli zapomnimy obsłużyć wielkość liter w naszym dopasowaniu wzorca. Oznacza to, że dopasowanie wzorca przypomina bardziej specjalną obniżkę, która zmusza nas do obsługi wszystkich opcji.Ponadto dynamiczne wysyłanie metod (które jest wymagane w przypadku OOP) oznacza również sprawdzanie typu środowiska wykonawczego, ale innego rodzaju. Dlatego nie ma znaczenia, czy sprawdzanie tego typu odbywa się jawnie poprzez dopasowanie wzorca lub pośrednio poprzez wywołanie metody.
źródło
Dopasowywanie wzorca F # zwykle wykonuje się przy użyciu dyskryminowanego związku, a nie klas (a zatem technicznie nie jest to w ogóle kontrola typu). Dzięki temu kompilator może ostrzec Cię, gdy nie uwzględniłeś przypadków w dopasowaniu wzorca.
Inną rzeczą wartą uwagi jest to, że w stylu funkcjonalnym organizujesz rzeczy według funkcjonalności, a nie według danych, więc dopasowanie wzorca pozwala zebrać różne funkcje w jednym miejscu, a nie rozproszone po klasach. Ma to również tę zaletę, że można zobaczyć, jak obsługiwane są inne sprawy tuż obok miejsca, w którym należy wprowadzić zmiany.
Dodanie nowej opcji wygląda wtedy następująco:
źródło
Częściowo częściej widzisz to w programowaniu funkcjonalnym, ponieważ częściej używasz typów do podejmowania decyzji. Zdaję sobie sprawę, że prawdopodobnie wybrałeś przykłady mniej więcej przypadkowo, ale OOP odpowiadające przykładowi dopasowania wzorca częściej wyglądałoby:
Innymi słowy, stosunkowo rzadko stosuje się polimorfizm, aby uniknąć rutynowych czynności, takich jak sprawdzanie wartości zerowej w OOP. Podobnie jak programista OO nie tworzy pustego obiektu w każdej małej sytuacji, funkcjonalny programista nie zawsze przeciąża funkcję, zwłaszcza gdy wiesz, że twoja lista wzorców jest wyczerpująca. Jeśli użyjesz systemu typów w większej liczbie sytuacji, zobaczysz, że jest używany w sposób, do którego nie jesteś przyzwyczajony.
I odwrotnie, idiomatyczne programowanie funkcjonalne odpowiadające przykładowi OOP najprawdopodobniej nie użyłoby dopasowania wzorca, ale miałoby
fooRoute
ibarRoute
funkcje, które byłyby przekazywane jako argumenty do kodu wywołującego. Jeśli ktoś użyje dopasowania wzorca w takiej sytuacji, zwykle będzie to uważane za złe, tak jak ktoś, kto włącza typy, będzie uważane za nieprawidłowe w OOP.Kiedy więc dopasowanie wzorca uważa się za dobry kod programowania funkcjonalnego? Gdy robisz więcej niż tylko patrzysz na typy i rozszerzasz wymagania, nie będziesz musiał dodawać więcej przypadków. Na przykład,
Some val
nie tylko sprawdza, czyopt
ma typSome
, ale także wiążeval
się z typem podstawowym w celu bezpiecznego użycia typu po drugiej stronie->
. Wiesz, że najprawdopodobniej nigdy nie będziesz potrzebować trzeciej skrzynki, więc jest to dobre zastosowanie.Dopasowywanie wzorców może powierzchownie przypominać obiektową instrukcję przełączania, ale dzieje się o wiele więcej, szczególnie w przypadku dłuższych lub zagnieżdżonych wzorców. Upewnij się, że bierzesz pod uwagę wszystko, co robi, zanim zadeklarujesz, że jest to odpowiednik źle zaprojektowanego kodu OOP. Często zwięźle radzi sobie z sytuacją, której nie da się w czysty sposób przedstawić w hierarchii dziedziczenia.
źródło
Some
iNone
nie są typami, więc nie dopasowujesz wzorów do typów. Dopasowujesz wzorce do konstruktorów tego samego typu . To nie jest tak, jakby pytać „instanceof”.