Czytam o programowaniu funkcjonalnym i zauważyłem, że dopasowywanie wzorców jest wymieniane w wielu artykułach jako jedna z podstawowych cech języków funkcyjnych.
Czy ktoś może wyjaśnić programistom Java / C ++ / JavaScript, co to oznacza?
Czytam o programowaniu funkcjonalnym i zauważyłem, że dopasowywanie wzorców jest wymieniane w wielu artykułach jako jedna z podstawowych cech języków funkcyjnych.
Czy ktoś może wyjaśnić programistom Java / C ++ / JavaScript, co to oznacza?
Odpowiedzi:
Zrozumienie dopasowania wzorców wymaga wyjaśnienia trzech części:
Algebraiczne typy danych w pigułce
Języki funkcyjne podobne do ML umożliwiają definiowanie prostych typów danych zwanych „rozłącznymi związkami” lub „algebraicznymi typami danych”. Te struktury danych są prostymi kontenerami i można je definiować rekurencyjnie. Na przykład:
definiuje strukturę danych podobną do stosu. Potraktuj to jako odpowiednik tego C #:
Tak więc identyfikatory
Cons
iNil
definiują prostą prostą klasę, w którejof x * y * z * ...
definiuje konstruktor i niektóre typy danych. Parametry konstruktora są nienazwane, są identyfikowane przez pozycję i typ danych.Tworzysz instancje swojej
a list
klasy jako takie:Co jest tym samym, co:
Dopasowanie wzorców w pigułce
Dopasowywanie wzorców to rodzaj testowania typu. Powiedzmy, że utworzyliśmy obiekt stosu podobny do powyższego, możemy zaimplementować metody podglądania i zdejmowania stosu w następujący sposób:
Powyższe metody są równoważne (chociaż nie są zaimplementowane jako takie) dla następującego języka C #:
(Prawie zawsze języki ML implementują dopasowywanie wzorców bez testów typu lub rzutowania w czasie wykonywania, więc kod C # jest nieco zwodniczy. Pomińmy szczegóły implementacji, machając ręką :))
Rozkład struktury danych w pigułce
Ok, wróćmy do metody podglądu:
Sztuczka polega na zrozumieniu, że identyfikatory
hd
itl
są zmiennymi (errm ... ponieważ są niezmienne, tak naprawdę nie są „zmiennymi”, ale „wartościami”;)). Jeślis
ma typCons
, to wyciągniemy jego wartości z konstruktora i powiążemy je ze zmiennymi o nazwachhd
itl
.Dopasowywanie wzorców jest przydatne, ponieważ pozwala nam rozłożyć strukturę danych na podstawie jej kształtu zamiast zawartości . Wyobraź sobie więc, że zdefiniujemy drzewo binarne w następujący sposób:
Możemy zdefiniować niektóre obroty drzew w następujący sposób:
(
let rotateRight = function
Konstruktor to cukier składniowylet rotateRight s = match s with ...
).Więc oprócz powiązania struktury danych ze zmiennymi, możemy również przejść do niej. Powiedzmy, że mamy węzeł
let x = Node(Nil, 1, Nil)
. Jeśli wywołaszrotateLeft x
, sprawdzamyx
pierwszy wzorzec, który nie pasuje, ponieważ właściwe dziecko ma typNil
zamiastNode
. Przeniesie się do następnego wzorca,x -> x
który dopasuje dowolne dane wejściowe i zwróci je niezmodyfikowane.Dla porównania napisalibyśmy powyższe metody w C # jako:
Na poważnie.
Dopasowywanie wzorów jest niesamowite
Możesz zaimplementować coś podobnego do dopasowywania wzorców w C # przy użyciu wzorca gościa , ale nie jest on tak elastyczny, ponieważ nie można skutecznie rozłożyć złożonych struktur danych. Ponadto, jeśli używasz dopasowywania wzorców, kompilator poinformuje Cię, czy pominąłeś przypadek . Jakie to niesamowite?
Pomyśl o tym, jak zaimplementowałbyś podobną funkcjonalność w C # lub językach bez dopasowywania wzorców. Pomyśl, jak byś to zrobił bez testów i rzutów w czasie wykonywania. Z pewnością nie jest twardy , po prostu uciążliwy i nieporęczny. I nie musisz sprawdzać kompilatora, aby upewnić się, że uwzględniłeś każdy przypadek.
Tak więc dopasowanie wzorców pomaga dekomponować i nawigować po strukturach danych w bardzo wygodnej, zwartej składni, pozwala kompilatorowi przynajmniej trochę sprawdzić logikę kodu. To naprawdę jest cechą zabójca.
źródło
Krótka odpowiedź: Dopasowanie wzorców pojawia się, ponieważ języki funkcjonalne traktują znak równości jako potwierdzenie równoważności zamiast przypisania.
Długa odpowiedź: Dopasowywanie wzorców jest formą wysyłki opartą na „kształcie” podanej wartości. W języku funkcjonalnym typy danych, które definiujesz, to zwykle tak zwane związki rozłączne lub algebraiczne typy danych. Na przykład, co to jest lista (połączona)? Połączona lista
List
rzeczy pewnego typua
jest albo pustą listąNil
albo jakimś elementema
Cons
wpisanym naList a
(listę elementówa
). Piszemy to w języku Haskell (języku funkcjonalnym, który jest mi najbardziej znany)data List a = Nil | Cons a (List a)
Wszystkie związki rozłączne są definiowane w ten sposób: pojedynczy typ ma określoną liczbę różnych sposobów jego tworzenia; twórcy, jak
Nil
iCons
tutaj, nazywani są konstruktorami. Oznacza to, że wartość typuList a
mogłaby zostać utworzona za pomocą dwóch różnych konstruktorów - mogłaby mieć dwa różne kształty. Załóżmy więc, że chcemy napisaćhead
funkcję, która pobierze pierwszy element listy. W Haskell napisalibyśmy to jako-- `head` is a function from a `List a` to an `a`. head :: List a -> a -- An empty list has no first item, so we raise an error. head Nil = error "empty list" -- If we are given a `Cons`, we only want the first part; that's the list's head. head (Cons h _) = h
Od
List a
wartości mogą być dwóch różnych rodzajów, musimy traktować każdą z nich osobno; to jest dopasowanie wzorców. Whead x
, jeślix
pasuje do wzorcaNil
, uruchamiamy pierwszy przypadek; jeśli pasuje do wzorcaCons h _
, uruchamiamy drugi.Krótka odpowiedź, wyjaśniona: Myślę, że jednym z najlepszych sposobów myślenia o tym zachowaniu jest zmiana sposobu myślenia o znaku równości. W językach z nawiasami klamrowymi,
=
ogólnie rzecz biorąc , oznacza przypisanie:a = b
oznacza „przekształcića
wb
”. Jednak w wielu językach funkcjonalnych=
oznacza stwierdzenie równości:let Cons a (Cons b Nil) = frob x
stwierdza, że rzecz po lewej stronieCons a (Cons b Nil)
jest równoważna rzeczy po prawej stroniefrob x
; ponadto wszystkie zmienne użyte po lewej stronie stają się widoczne. To samo dzieje się z argumentami funkcji: zapewniamy, że pierwszy argument wygląda jakNil
, a jeśli nie, sprawdzamy dalej.źródło
Cons
znaczy?Cons
to cons tructor, który buduje (połączoną) listę z głowy (thea
) i tail (theList a
). Nazwa pochodzi od Lispa. W Haskell, dla wbudowanego typu listy, jest to:
operator (nadal wymawiany jako „minusy”).To znaczy, że zamiast pisać
Możesz pisać
Hej, C ++ obsługuje również dopasowywanie wzorców.
źródło
Dopasowywanie wzorców jest czymś w rodzaju przeciążonych metod stosowanych na sterydach. Najprostszy przypadek byłby z grubsza taki sam, jak ten, który widziałeś w java, argumenty to lista typów z nazwami. Prawidłowa metoda do wywołania jest oparta na przekazanych argumentach i podwaja się jako przypisanie tych argumentów do nazwy parametru.
Wzorce idą o krok dalej i mogą jeszcze bardziej zniszczyć przekazywane argumenty. Potencjalnie może również używać strażników do rzeczywistego dopasowania na podstawie wartości argumentu. Aby to zademonstrować, będę udawać, że JavaScript ma dopasowanie do wzorca.
W foo2 oczekuje, że a będzie tablicą, rozbija drugi argument, oczekując obiektu z dwoma właściwościami (prop1, prop2) i przypisuje wartości tych właściwości do zmiennych d i e, a następnie oczekuje, że trzeci argument będzie 35.
W przeciwieństwie do JavaScript, języki z dopasowywaniem wzorców zwykle dopuszczają wiele funkcji o tej samej nazwie, ale różnych wzorcach. W ten sposób jest to jak przeładowanie metody. Podam przykład w języku erlang:
Rozmyj trochę oczy i możesz to sobie wyobrazić w javascript. Może coś takiego:
Chodzi o to, że kiedy wywołujesz fibo, implementacja, której używa, jest oparta na argumentach, ale tam, gdzie Java jest ograniczona do typów jako jedynego sposobu na przeciążenie, dopasowanie wzorców może zdziałać więcej.
Oprócz przeciążania funkcji, jak pokazano tutaj, ta sama zasada może być zastosowana w innych miejscach, takich jak opisy przypadków lub oceny destrukturyzujące. JavaScript ma to nawet w wersji 1.7 .
źródło
Dopasowanie wzorców umożliwia dopasowanie wartości (lub obiektu) do niektórych wzorców w celu wybrania gałęzi kodu. Z punktu widzenia C ++ może to brzmieć trochę podobnie do
switch
stwierdzenia. W językach funkcjonalnych dopasowywanie wzorców może służyć do dopasowywania standardowych wartości pierwotnych, takich jak liczby całkowite. Jest to jednak bardziej przydatne w przypadku typów złożonych.Najpierw pokażmy dopasowywanie wzorców na wartościach pierwotnych (używając rozszerzonego pseudo-C ++
switch
):Drugie zastosowanie dotyczy funkcjonalnych typów danych, takich jak krotki (które umożliwiają przechowywanie wielu obiektów w jednej wartości) i rozłącznych związków, które pozwalają na utworzenie typu, który może zawierać jedną z kilku opcji. Brzmi to trochę jak
enum
z wyjątkiem tego, że każda etykieta może również zawierać pewne wartości. W składni pseudo-C ++:Wartość typu
Shape
może teraz zawieraćRectangle
wszystkie współrzędne lub aCircle
ze środkiem i promieniem. Dopasowanie wzorców pozwala napisać funkcję do pracy zShape
typem:Wreszcie, możesz również użyć zagnieżdżonych wzorców, które łączą obie te funkcje. Na przykład, możesz użyć
Circle(0, 0, radius)
do dopasowania dla wszystkich kształtów, które mają środek w punkcie [0, 0] i mają dowolny promień (wartość promienia zostanie przypisana do nowej zmiennejradius
).Może to zabrzmieć trochę obco z punktu widzenia C ++, ale mam nadzieję, że moje pseudo-C ++ wyjaśni wyjaśnienie. Programowanie funkcjonalne opiera się na zupełnie innych koncepcjach, więc ma sens w języku funkcjonalnym!
źródło
Dopasowywanie wzorców polega na tym, że interpreter Twojego języka wybierze określoną funkcję na podstawie struktury i treści argumentów, które mu podajesz.
Jest to nie tylko funkcjonalna funkcja języka, ale jest dostępna dla wielu różnych języków.
Po raz pierwszy wpadłem na ten pomysł, kiedy nauczyłem się prologu, w którym jest to naprawdę kluczowe dla języka.
na przykład
Powyższy kod da ostatnią pozycję z listy. Argument wejściowy jest pierwszym, a wynikiem jest drugi.
Jeśli na liście jest tylko jedna pozycja, interpreter wybierze pierwszą wersję, a drugi argument zostanie ustawiony na równy pierwszej, tj. Do wyniku zostanie przypisana wartość.
Jeśli lista ma zarówno nagłówek, jak i koniec, tłumacz wybierze drugą wersję i będzie powtarzał, dopóki na liście nie zostanie tylko jedna pozycja.
źródło
Dla wielu osób wybór nowej koncepcji jest łatwiejszy, jeśli podano kilka łatwych przykładów, więc zaczynamy:
Powiedzmy, że masz listę trzech liczb całkowitych i chcesz dodać pierwszy i trzeci element. Bez dopasowania wzorców można to zrobić w ten sposób (przykłady w Haskell):
Teraz, chociaż jest to przykład zabawki, wyobraź sobie, że chcielibyśmy powiązać pierwszą i trzecią liczbę całkowitą ze zmiennymi i zsumować je:
To wyodrębnianie wartości ze struktury danych jest tym, co robi dopasowywanie wzorców. Zasadniczo "odzwierciedlasz" strukturę czegoś, dając zmienne do powiązania z interesującymi miejscami:
Gdy wywołasz tę funkcję z [1,2,3] jako argumentem, [1,2,3] zostanie ujednolicone z [pierwszy
_
, trzeci], wiążąc najpierw z 1, od trzeciego do 3 i odrzucając 2 (_
jest symbolem zastępczym rzeczy, na których nie zależy).Teraz, jeśli chcesz dopasować tylko listy z 2 jako drugim elementem, możesz to zrobić w następujący sposób:
To zadziała tylko dla list z 2 jako drugim elementem, aw przeciwnym razie zgłosi wyjątek, ponieważ nie podano definicji addFirstAndThird dla niepasujących list.
Do tej pory używaliśmy dopasowania wzorców tylko do wiązania destrukturyzującego. Ponadto możesz podać wiele definicji tej samej funkcji, w przypadku gdy używana jest pierwsza definicja dopasowania, dlatego dopasowanie wzorca przypomina trochę „instrukcję przełączającą na stereoidach”:
addFirstAndThird z radością doda pierwszy i trzeci element list z 2 jako drugim elementem, aw przeciwnym razie „spadnie” i „zwróci” 0. Ta funkcja „podobna do przełącznika” może być używana nie tylko w definicjach funkcji, np .:
Co więcej, nie jest ograniczony do list, ale może być również używany z innymi typami, na przykład dopasowywaniem konstruktorów wartości Just i Nothing typu Maybe w celu „rozpakowania” wartości:
Jasne, to były zwykłe przykłady zabawek i nawet nie próbowałem podać formalnego ani wyczerpującego wyjaśnienia, ale powinny one wystarczyć, aby uchwycić podstawową koncepcję.
źródło
Powinieneś zacząć od strony Wikipedii, która daje całkiem dobre wyjaśnienie. Następnie przeczytaj odpowiedni rozdział w książce Haskell wikibook .
To jest fajna definicja z powyższego wikibooka:
źródło
Oto naprawdę krótki przykład, który pokazuje przydatność dopasowywania wzorców:
Powiedzmy, że chcesz posortować element na liście:
do (posortowałem „Nowy Jork”)
w bardziej imperatywnym języku napisałbyś:
Zamiast tego w języku funkcjonalnym napisałbyś:
Jak widać, rozwiązanie z dopasowanym wzorcem ma mniej hałasu, możesz wyraźnie zobaczyć, jakie są różne przypadki i jak łatwo jest podróżować i usuwać strukturę naszej listy.
Napisałem na ten temat bardziej szczegółowy wpis na blogu tutaj .
źródło