Co to jest (funkcjonalne) programowanie reaktywne?

1148

Przeczytałem artykuł w Wikipedii na temat programowania reaktywnego . Przeczytałem również mały artykuł na temat programowania reaktywnego . Opisy są dość abstrakcyjne.

  1. Co w praktyce oznacza funkcjonalne programowanie reaktywne (FRP)?
  2. Z czego składa się programowanie reaktywne (w przeciwieństwie do programowania niereaktywnego?)?

Moje pochodzenie jest w językach imperatywnych / OO, więc docenione zostanie wyjaśnienie dotyczące tego paradygmatu.

JtR
źródło
159
oto facet z aktywną wyobraźnią i dobrymi umiejętnościami w zakresie opowiadania historii. paulstovell.com/reactive-programming
melaos
39
Ktoś naprawdę musi napisać „Funkcjonalne programowanie reaktywne dla manekinów” dla wszystkich nas tutaj samouków. Każdy zasób, który znalazłem, nawet Wiąz, wydaje się zakładać, że uzyskałeś tytuł magistra w dziedzinie CS w ciągu ostatnich pięciu lat. Wydaje się, że ci, którzy mają wiedzę na temat FRP, całkowicie stracili zdolność patrzenia na tę sprawę z naiwnego punktu widzenia, co jest bardzo ważne dla nauczania, szkolenia i ewangelizacji.
TechZen
26
Kolejne doskonałe wprowadzenie do FRP: wprowadzenie do Reactive Programming, którego brakowało mi mojego kolegi André
Jonik
5
Jeden z najlepszych, jakie widziałem, na podstawie przykładu: gist.github.com/staltz/868e7e9bc2a7b8c1f754
Razmig
2
Uważam, że analogia arkusza kalkulacyjnego jest bardzo pomocna jako pierwsze szorstkie wrażenie (patrz odpowiedź Boba: stackoverflow.com/a/1033066/1593924 ). Komórka arkusza kalkulacyjnego reaguje na zmiany w innych komórkach (ściąga), ale nie dociera i nie zmienia innych (nie naciska). Efektem końcowym jest to, że możesz zmienić jedną komórkę, a miliony innych „niezależnie” zaktualizuje swoje własne wyświetlacze.
Jon Coombs

Odpowiedzi:

931

Jeśli chcesz poznać FRP, możesz zacząć od starego samouczka Fran z 1998 roku, który zawiera animowane ilustracje. W przypadku artykułów zacznij od Functional Reactive Animation, a następnie śledź linki w linku do publikacji na mojej stronie głównej oraz link FRP na wiki Haskell .

Osobiście lubię zastanawiać się, co znaczy FRP , zanim zastanowię się, jak można go wdrożyć. (Kod bez specyfikacji jest odpowiedzią bez pytania i dlatego „nawet nie jest zły”). Nie opisuję FRP w kategoriach reprezentacji / implementacji, jak Thomas K w innej odpowiedzi (wykresy, węzły, krawędzie, odpalanie, wykonanie, itp). Istnieje wiele możliwych stylów implementacji, ale żadna implementacja nie mówi, czym jest FRP .

Rezonuję z prostym opisem Laurence'a G, że FRP dotyczy „typów danych reprezentujących wartość„ w czasie ”. Konwencjonalne programowanie imperatywne rejestruje te wartości dynamiczne tylko pośrednio, poprzez stan i mutacje. Pełna historia (przeszłość, teraźniejszość, przyszłość) nie ma reprezentacji pierwszej klasy. Co więcej, tylko dyskretnie ewoluujące wartości mogą być (pośrednio) uchwycone, ponieważ imperatywny paradygmat jest tymczasowo dyskretny. W przeciwieństwie do tego FRP rejestruje te ewoluujące wartości bezpośrednio i nie ma trudności z ciągłą ewolucją wartości.

FRP jest również niezwykły, ponieważ jest zbieżny bez obawy o teoretyczne i pragmatyczne gniazdo szczurów, które nęka imperatywną współbieżność. Semantycznie współbieżność FRP jest drobnoziarnista , zdeterminowana i ciągła . (Mówię o znaczeniu, a nie implementacji. Implementacja może, ale nie musi, obejmować współbieżność lub paralelizm). Określenie semantyczne jest bardzo ważne dla rozumowania, zarówno rygorystycznego, jak i nieformalnego. Podczas gdy współbieżność powoduje ogromną złożoność programowania imperatywnego (z powodu niedeterministycznego przeplatania), w FRP jest on łatwy.

Czym jest FRP? Mógłbyś to sam wymyślić. Zacznij od tych pomysłów:

  • Wartości dynamiczne / ewoluujące (tj. Wartości „w czasie”) same w sobie są wartościami pierwszej klasy. Możesz je definiować i łączyć, przekazywać i wyłączać z funkcji. Nazwałam te rzeczy „zachowaniami”.

  • Zachowania budowane są z kilku prymitywów, takich jak stałe (statyczne) zachowania i czas (jak zegar), a następnie kombinacja sekwencyjna i równoległa. n zachowań łączy się, stosując funkcję n-ary (na wartościach statycznych), „punktowo”, tj. ciągle w czasie.

  • Aby uwzględnić zjawiska dyskretne, należy mieć inny typ (rodzinę) „zdarzeń”, z których każde ma strumień (skończony lub nieskończony). Każde wystąpienie ma powiązany czas i wartość.

  • Aby wymyślić słownictwo kompozytorskie, z którego można zbudować wszystkie zachowania i zdarzenia, zagraj z kilkoma przykładami. Rozbijaj dalej na części, które są bardziej ogólne / proste.

  • Abyś wiedział, że jesteś na solidnym gruncie, nadaj całemu modelowi podstawę kompozycyjną, używając techniki semantyki denotacyjnej, co oznacza po prostu, że (a) każdy typ ma odpowiadający prosty i precyzyjny matematyczny typ „znaczeń” oraz ( b) każdy prymityw i operator ma proste i precyzyjne znaczenie w zależności od znaczeń składników. Nigdy nie mieszaj kwestii związanych z implementacją z procesem eksploracji. Jeśli ten opis jest bełkotliwy, skonsultuj się z (a) Projektem denotacyjnym z morfizmami klas typu , (b) Funkcjonalnym programowaniem reaktywnym push-pull (ignorując bity implementacyjne) oraz (c) stroną Wikipedii Denotational Semantics Haskell. Strzeż się, że semantyka denotacyjna składa się z dwóch części, od jej dwóch założycieli, Christophera Stracheya i Dany Scott: łatwiejszej i bardziej użytecznej części Strachey oraz trudniejszej i mniej użytecznej (do projektowania oprogramowania) części Scott.

Jeśli będziesz przestrzegać tych zasad, spodziewam się, że dostaniesz coś mniej więcej w duchu FRP.

Skąd wziąłem te zasady? W projektowaniu oprogramowania zawsze zadaję to samo pytanie: „co to znaczy?”. Semantyka denotacyjna dała mi dokładne ramy dla tego pytania i takie, które pasują do mojej estetyki (w przeciwieństwie do semantyki operacyjnej lub aksjomatycznej, które pozostawiają mnie niezadowoloną). Więc zadałem sobie pytanie, co to jest zachowanie? Wkrótce zdałem sobie sprawę, że czasowo dyskretna natura obliczeń imperatywnych jest dostosowaniem do określonego stylu maszyny , a nie naturalnym opisem samego zachowania. Najprostszy precyzyjny opis zachowań, jaki mogę wymyślić, to po prostu „funkcja (ciągłego) czasu”, więc to mój model. Cudownie, ten model obsługuje ciągłą, deterministyczną współbieżność z łatwością i wdziękiem.

Prawidłowe i wydajne wdrożenie tego modelu było dużym wyzwaniem, ale to już inna historia.

Conal
źródło
78
Byłem świadomy programowania funkcjonalnego reaktywnego. Wydaje się, że jest to związane z moimi własnymi badaniami (w interaktywnej grafice statystycznej) i jestem pewien, że wiele pomysłów byłoby przydatnych w mojej pracy. Trudno mi jednak przejść obok języka - czy naprawdę muszę się uczyć o „semantyce denotacyjnej” i „morfizmach klas typów”, aby zrozumieć, co się dzieje? Przydałoby się ogólne wprowadzenie do tematu przez publiczność.
hadley
212
@Conal: wyraźnie wiesz, o czym mówisz, ale twój język zakłada, że ​​mam doktorat z matematyki obliczeniowej, czego nie mam. Mam doświadczenie w inżynierii systemów i ponad 20-letnie doświadczenie w komputerach i językach programowania, ale nadal czuję, że twoja reakcja wprawia mnie w zakłopotanie. Wzywam cię do
ponownego opublikowania
50
@ minplay.dk: Twoje uwagi nie dają mi wiele do powiedzenia na temat tego, czego w szczególności nie rozumiesz, i nie jestem skłonny zgadywać, jaki konkretny podzbiór języka angielskiego szukasz. Zachęcam jednak do powiedzenia konkretnie, jakie aspekty mojego wyjaśnienia powyżej się potknęły, abyśmy ja i inni mogli ci pomóc. Na przykład, czy są określone słowa, które chcesz zdefiniować, lub pojęcia, do których chcesz dodać odniesienia? Naprawdę lubię poprawiać przejrzystość i dostępność mojego pisania - nie stępiając go.
Conal,
27
„Determinacja” / „determinacja” oznacza, że ​​istnieje jedna, dobrze zdefiniowana poprawna wartość. W przeciwieństwie do tego prawie wszystkie formy imperatywnej współbieżności mogą dawać różne odpowiedzi, w zależności od harmonogramu lub tego, czy szukasz, czy nie, a nawet mogą się zaciąć. „Semantyczny” (a dokładniej „denotacyjny”) odnosi się do wartości („denotacji”) wyrażenia lub reprezentacji, w przeciwieństwie do „operatywnego” (jak obliczana jest odpowiedź lub ile miejsca i / lub czasu zajmuje to, co rodzaj maszyny).
Conal
18
Zgadzam się z @ mindplay.dk, chociaż nie mogę się chwalić, że byłem w terenie bardzo długo. Chociaż wydawało się, że wiesz, o czym mówisz, nie dało mi to szybkiego, krótkiego i prostego zrozumienia, co to jest, ponieważ jestem wystarczająco rozpieszczony, aby spodziewać się SO. Ta odpowiedź doprowadziła mnie przede wszystkim do mnóstwa nowych pytań, ale tak naprawdę nie odpowiedziałem na moje pierwsze. Mam nadzieję, że dzielenie się doświadczeniem bycia względnie ignorantem w terenie może dać ci wgląd w to, jak proste i krótkie musisz być. Pochodzę z podobnego pochodzenia jak OP, btw.
Aske B.,
739

W czystym programowaniu funkcjonalnym nie ma skutków ubocznych. W przypadku wielu rodzajów oprogramowania (na przykład cokolwiek z interakcją użytkownika) działania niepożądane są konieczne na pewnym poziomie.

Jednym ze sposobów uzyskania efektu podobnego do efektu ubocznego przy jednoczesnym zachowaniu funkcjonalnego stylu jest zastosowanie funkcjonalnego programowania reaktywnego. Jest to połączenie programowania funkcjonalnego i programowania reaktywnego. (Artykuł w Wikipedii, do którego linkujesz, dotyczy tego drugiego.)

Podstawową ideą programowania reaktywnego jest to, że istnieją pewne typy danych, które reprezentują wartość „w czasie”. Obliczenia obejmujące te wartości zmieniające się w czasie same będą miały wartości, które zmieniają się w czasie.

Na przykład, możesz przedstawić współrzędne myszy jako parę liczb całkowitych w czasie. Powiedzmy, że mieliśmy coś takiego (to jest pseudo-kod):

x = <mouse-x>;
y = <mouse-y>;

W dowolnym momencie xiy miałyby współrzędne myszy. W przeciwieństwie do programowania niereaktywnego, musimy przypisać to zadanie tylko raz, a zmienne xiy pozostaną „aktualne” automatycznie. Dlatego programowanie reaktywne i programowanie funkcjonalne działają tak dobrze razem: programowanie reaktywne eliminuje potrzebę mutacji zmiennych, jednocześnie pozwalając ci robić wiele z tego, co można osiągnąć dzięki mutacjom zmiennych.

Jeśli następnie wykonamy pewne obliczenia na tej podstawie, wówczas otrzymane wartości będą również wartościami, które zmieniają się w czasie. Na przykład:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

W tym przykładzie minXzawsze będzie o 16 mniej niż współrzędna x wskaźnika myszy. Za pomocą bibliotek świadomych reaktywnie możesz powiedzieć coś takiego:

rectangle(minX, minY, maxX, maxY)

Wokół wskaźnika myszy zostanie narysowane pole o wymiarach 32 x 32, które będzie śledzić go w dowolnym miejscu.

Oto całkiem niezły artykuł na temat programowania funkcjonalnego .

Laurence Gonsalves
źródło
25
Czyli więc programowanie reaktywne jest formą programowania deklaratywnego?
troelskn
31
> Czy zatem programowanie reaktywne jest formą programowania deklaratywnego? Funkcjonalne programowanie reaktywne jest formą programowania funkcjonalnego, które jest formą programowania deklaratywnego.
Conal
7
@ user712092 Nie bardzo, nie. Na przykład, jeśli wywołam sqrt(x)C z twoim makrem, to po prostu oblicza sqrt(mouse_x())i daje mi podwójne. W prawdziwym funkcjonalnym systemie reaktywnym sqrt(x)zwróciłby nowe „podwójne z czasem”. Jeśli spróbujesz symulować system FR, #definebędziesz musiał przysiąc zmienne na korzyść makr. Systemy FR zazwyczaj również ponownie obliczają rzeczy tylko wtedy, gdy trzeba je ponownie obliczyć, podczas gdy używanie makr oznaczałoby, że ciągle dokonujesz ponownej oceny wszystkiego, aż do podwyrażeń.
Laurence Gonsalves
4
„W przypadku wielu rodzajów oprogramowania (na przykład cokolwiek z interakcją użytkownika) działania niepożądane są konieczne na pewnym poziomie”. I może tylko na poziomie wdrażania. Istnieje wiele efektów ubocznych przy wdrażaniu czystego, leniwego programowania funkcjonalnego, a jednym z sukcesów tego paradygmatu jest utrzymanie wielu z tych efektów poza modelem programowania. Moje własne testy funkcjonalnych interfejsów użytkownika sugerują, że można je również programować całkowicie bez skutków ubocznych.
Conal
4
@tieTYT x nigdy nie jest ponownie przypisany / zmutowany. Wartość x to ciąg wartości w czasie. Innym sposobem na to jest to, że zamiast x o wartości „normalnej”, takiej jak liczba, wartość x jest (koncepcyjnie) funkcją, która wymaga czasu jako parametru. (Jest to trochę nadmierne uproszczenie. Nie można tworzyć wartości czasu, które pozwoliłyby przewidzieć przyszłość takich rzeczy, jak pozycja myszy.)
Laurence Gonsalves
144

Łatwym sposobem osiągnięcia pierwszej intuicji na temat tego, jak to jest, jest wyobrażenie sobie, że twój program jest arkuszem kalkulacyjnym, a wszystkie zmienne są komórkami. Jeśli dowolna komórka w arkuszu kalkulacyjnym ulegnie zmianie, zmieniają się również wszystkie komórki, które odnoszą się do tej komórki. Tak samo jest z FRP. Teraz wyobraź sobie, że niektóre komórki zmieniają się same (a raczej są pobierane ze świata zewnętrznego): w sytuacji GUI, pozycja myszy byłaby dobrym przykładem.

To niekoniecznie bardzo tęskni. Metafora rozpada się dość szybko, kiedy faktycznie używasz systemu FRP. Po pierwsze, zwykle próbuje się również modelować zdarzenia dyskretne (np. Kliknięcie myszą). Kładę to tutaj, aby dać ci wyobrażenie, jak to jest.


źródło
3
Niezwykle trafny przykład. Wspaniale jest mieć teoretykę i być może niektórzy ludzie wyciągają z tego wnioski bez uciekania się do gruntownego przykładu, ale muszę zacząć od tego, co robi dla mnie, a nie abstrakcyjnego. Niedawno dostałem (z rozmów Rx Netflix!), Że RP (lub Rx, tak czy inaczej), czyni te „zmieniające się wartości” pierwszą klasą i pozwala ci je uzasadnić lub napisać funkcje, które z nimi robią. Pisz funkcje, aby tworzyć arkusze kalkulacyjne lub komórki, jeśli chcesz. I obsługuje się, gdy wartość kończy się (znika) i umożliwia automatyczne czyszczenie.
Benjohn
W tym przykładzie podkreślono różnicę między programowaniem opartym na zdarzeniu a podejściem reaktywnym, w którym deklaruje się tylko zależności do korzystania z inteligentnego routingu.
kinjelom
131

Dla mnie jest to około 2 różnych znaczeń symbolu =:

  1. W matematyce x = sin(t)oznacza, że xjest inna nazwa dla sin(t). Więc pisanie x + yjest tym samym sin(t) + y. Pod tym względem funkcjonalne programowanie reaktywne przypomina matematykę: jeśli piszesz x + y, jest ono obliczane na podstawie dowolnej wartości tw momencie jego użycia.
  2. W językach programowania podobnych do C (językach imperatywnych) x = sin(t)jest przypisanie: oznacza, że xprzechowuje wartość sin(t) wziętą w momencie przypisania.
użytkownik712092
źródło
5
Dobre wytłumaczenie. Myślę, że można również dodać, że „czas” w znaczeniu FRP to zwykle „jakakolwiek zmiana w stosunku do danych zewnętrznych”. Za każdym razem, gdy siła zewnętrzna zmienia dane wejściowe FRP, przesuwasz „czas” do przodu i ponownie oblicz wszystko, na co zmiana ma wpływ.
Didier A.
4
W matematyce x = sin(t)oznacza xwartość sin(t)dla danego t. To nie inna nazwa sin(t)w funkcji. W przeciwnym razie byłoby x(t) = sin(t).
Dmitri Zaitsev,
+ Znak równości Dmitrija Zaitseva ma kilka znaczeń w matematyce. Jednym z nich jest to, że ilekroć zobaczysz lewą stronę, możesz zamienić ją na prawą stronę. Na przykład 2 + 3 = 5lub a**2 + b**2 = c**2.
user712092,
71

OK, z wiedzy w tle i po przeczytaniu strony w Wikipedii, na którą wskazałeś, wydaje się, że programowanie reaktywne przypomina obliczanie przepływu danych, ale z określonymi zewnętrznymi „bodźcami” wyzwalającymi zestaw węzłów do uruchamiania i wykonywania ich obliczeń.

Jest to dość dobrze dostosowane do projektu interfejsu użytkownika, na przykład w którym dotknięcie kontrolki interfejsu użytkownika (powiedzmy, regulacji głośności w aplikacji do odtwarzania muzyki) może wymagać aktualizacji różnych elementów wyświetlania i rzeczywistej głośności wyjścia audio. Gdy zmodyfikujesz głośność (powiedzmy suwak), która odpowiada modyfikacji wartości powiązanej z węzłem na ukierunkowanym wykresie.

Różne węzły posiadające krawędzie z tego węzła „wartość objętości” byłyby automatycznie uruchamiane, a wszelkie niezbędne obliczenia i aktualizacje naturalnie falowałyby przez aplikację. Aplikacja „reaguje” na bodziec użytkownika. Funkcjonalne programowanie reaktywne byłoby po prostu implementacją tego pomysłu w funkcjonalnym języku lub ogólnie w funkcjonalnym paradygmacie programowania.

Aby uzyskać więcej informacji na temat „przetwarzania danych”, wyszukaj te dwa słowa w Wikipedii lub użyj swojej ulubionej wyszukiwarki. Ogólna idea jest następująca: program jest ukierunkowanym wykresem węzłów, z których każdy wykonuje pewne proste obliczenia. Węzły te są połączone ze sobą za pomocą łączy graficznych, które zapewniają wyjścia niektórych węzłów z wejściami innych.

Gdy węzeł odpala lub wykonuje obliczenia, węzły podłączone do jego wyjść mają odpowiadające im wejścia „wyzwalane” lub „oznaczone”. Każdy węzeł posiadający wszystkie wejścia wyzwolone / oznaczone / dostępne automatycznie uruchamia się. Wykres może być niejawny lub jawny w zależności od tego, w jaki sposób wdrażane jest programowanie reaktywne.

Węzły można traktować jako odpalane równolegle, ale często są one wykonywane szeregowo lub z ograniczoną równoległością (na przykład może być wykonywanych przez kilka wątków). Słynny przykład to Manchester Dataflow Machine , która (IIRC) zastosowała oznakowaną architekturę danych do zaplanowania wykonania węzłów na wykresie przez jedną lub więcej jednostek wykonawczych. Obliczenia przepływu danych są dość dobrze dostosowane do sytuacji, w których wyzwalanie obliczeń asynchronicznie, powodujące kaskady obliczeń, działa lepiej niż próba kierowania wykonaniem zegarem (lub zegarami).

Programowanie reaktywne importuje tę ideę „kaskady wykonania” i wydaje się, że myśli o programie w sposób podobny do przepływu danych, ale pod warunkiem, że niektóre węzły są podłączone do „świata zewnętrznego”, a kaskady wykonania są uruchamiane, gdy te sensoryczne podobne zmiany węzłów. Wykonanie programu wyglądałoby wówczas jak coś analogicznego do złożonego łuku refleksowego. Program może, ale nie musi, być w zasadzie bezczynny między bodźcami lub może zasadniczo ustabilizować się pomiędzy bodźcami.

programowanie „niereaktywne” byłoby programowaniem z zupełnie innym spojrzeniem na przepływ wykonania i związek z zewnętrznymi danymi wejściowymi. To może być nieco subiektywne, ponieważ ludzie będą skłonni powiedzieć wszystko, co reaguje na zewnętrzne sygnały wejściowe „reaguje” na nie. Ale patrząc na ducha tej rzeczy, program, który odpytuje kolejkę zdarzeń w ustalonym odstępie czasu i wywołuje wszelkie zdarzenia znalezione w funkcjach (lub wątkach), jest mniej reaktywny (ponieważ bierze udział tylko w danych wprowadzanych przez użytkownika w ustalonych odstępach czasu). Ponownie, jest to sedno sprawy: można sobie wyobrazić umieszczenie implementacji odpytywania z szybkim interwałem odpytywania w systemie na bardzo niskim poziomie i programowanie na nim reaktywnie.

Thomas Kammeyer
źródło
1
OK, powyżej jest kilka dobrych odpowiedzi. Czy powinienem usunąć mój post? Jeśli zobaczę, że dwie lub trzy osoby mówią, że nic nie dodaje, usunę je, chyba że zwiększy się liczba pomocnych użytkowników. Nie ma sensu zostawiać go tutaj, chyba że doda coś wartościowego.
Thomas Kammeyer
3
wspomniałeś o przepływie danych, co dodaje pewnej wartości IMHO.
Rainer Joswig
Wydaje się, że właśnie taki powinien być QML;)
mlvljr
3
Dla mnie ta odpowiedź była najłatwiejsza do zrozumienia, szczególnie dlatego, że użycie naturalnych analogów, takich jak „falowanie przez aplikację” i „węzły czuciowe”. Świetny!
Akseli Palén
1
niestety łącze Manchester Dataflow Machine nie działa.
Pac0
65

Po przeczytaniu wielu stron o FRP w końcu natknąłem się na to oświecające pismo o FRP, w końcu zrozumiałem, na czym tak naprawdę polega FRP.

Cytuję poniżej Heinricha Apfelmusa (autora reaktywnego banana).

Jaka jest istota funkcjonalnego programowania reaktywnego?

Częstą odpowiedzią byłoby, że „FRP polega na opisaniu systemu w kategoriach funkcji zmieniających się w czasie zamiast stanu zmiennego”, a to z pewnością nie byłoby błędne. To jest semantyczny punkt widzenia. Ale moim zdaniem głębszej, bardziej satysfakcjonującej odpowiedzi udziela następujące czysto składniowe kryterium:

Istotą funkcjonalnego programowania reaktywnego jest całkowite określenie dynamicznego zachowania wartości w momencie deklaracji.

Weźmy na przykład licznik: masz dwa przyciski oznaczone „W górę” i „W dół”, których można użyć do zwiększenia lub zmniejszenia licznika. Koniecznie należy najpierw określić wartość początkową, a następnie ją zmienić po każdym naciśnięciu przycisku; coś takiego:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)

Chodzi o to, że w momencie deklaracji podana jest tylko wartość początkowa licznika; dynamiczne zachowanie licznika jest niejawne w pozostałej części tekstu programu. Natomiast funkcjonalne programowanie reaktywne określa całe zachowanie dynamiczne w momencie deklaracji, takie jak:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)

Ilekroć chcesz zrozumieć dynamikę licznika, musisz tylko spojrzeć na jego definicję. Wszystko, co może się przydarzyć, pojawi się po prawej stronie. Jest to bardzo sprzeczne z imperatywnym podejściem, w którym kolejne deklaracje mogą zmienić dynamiczne zachowanie wcześniej zadeklarowanych wartości.

Tak więc, moim zdaniem, program FRP jest zbiorem równań: wprowadź opis zdjęcia tutaj

j jest dyskretny: 1,2,3,4 ...

fzależy od ttego, co obejmuje możliwość modelowania bodźców zewnętrznych

cały stan programu jest zamknięty w zmiennych x_i

Biblioteka FRP dba o postęp czasu, innymi słowy, jdo j+1.

Wyjaśniam te równania bardziej szczegółowo w tym filmie.

EDYTOWAĆ:

Około 2 lata po oryginalnej odpowiedzi doszedłem niedawno do wniosku, że implementacje FRP mają jeszcze jeden ważny aspekt. Muszą (i zwykle robią) rozwiązać ważny problem praktyczny: unieważnienie pamięci podręcznej .

Równania dla x_i-s opisują wykres zależności. Kiedy niektóre x_izmiany w danym momencie, jnie wszystkie pozostałe x_i'wartości j+1muszą zostać zaktualizowane, więc nie wszystkie zależności muszą zostać ponownie obliczone, ponieważ niektóre x_i'mogą być niezależne x_i.

Ponadto x_i-s, które się zmieniają, można stopniowo aktualizować. Dla przykładu rozważmy operację mapę f=g.map(_+1)w Scala, gdzie fi gListod Ints. Tutaj fodpowiada x_i(t_j)i gjest x_j(t_j). Teraz, jeśli przygotuję do tego element g, byłoby niepotrzebnie przeprowadzić mapoperację dla wszystkich elementów w g. Niektóre implementacje FRP (na przykład reflex-frp ) mają na celu rozwiązanie tego problemu. Ten problem jest również znany jako obliczanie przyrostowe.

Innymi słowy, zachowania ( x_i-s) we FRP można traktować jako obliczenia buforowane. Mechanizm FRP ma za zadanie skutecznie unieważnić i ponownie obliczyć te pamięci podręczne ( x_i-y), jeśli niektóre z f_inich się zmienią.

Jhegedus
źródło
4
Byłem tam z tobą, dopóki nie poszedłeś z dyskretnymi równaniami. Ideą FRP był ciągły czas , w którym nie ma „ j+1”. Zamiast tego pomyśl o funkcjach ciągłego czasu. Jak pokazali nam Newton, Leibniz i inni, często bardzo przydatne (i „naturalne” w dosłownym znaczeniu) jest opisanie tych funkcji w różny sposób, ale w sposób ciągły, przy użyciu całek i układów ODE. W przeciwnym razie opisujesz algorytm aproksymacji (i kiepski) zamiast samej rzeczy.
Conal
Layx w języku szablonów i szablonów HTML wydaje się wyrażać elementy FRP.
@Conal To mnie zastanawia, czym różni się FRP od ODE. Czym się różnią?
jhegedus
@jhegedus W tej integracji (być może rekurencyjnej, tj. ODE) stanowi jeden z elementów składowych FRP, a nie całość. Każdy element słownictwa FRP (w tym m.in. integracja) jest dokładnie wyjaśniony w kategoriach ciągłego czasu. Czy to wyjaśnienie pomaga?
Conal,
29

Zastrzeżenie: moja odpowiedź jest w kontekście rx.js - biblioteki „programowania reaktywnego” dla Javascript.

W programowaniu funkcjonalnym zamiast iteracji po każdym elemencie kolekcji, stosujesz funkcje wyższego rzędu (HoF) do samej kolekcji. Zatem idea FRP polega na tym, że zamiast przetwarzać każde pojedyncze zdarzenie, utwórz strumień zdarzeń (zaimplementowany z możliwym do zaobserwowania *) i zastosuj do tego HoF. W ten sposób możesz wizualizować system jako potoki danych łączące wydawców z subskrybentami.

Głównymi zaletami używania obserwowalnego są:
i) abstrahowanie stanu od twojego kodu, np. Jeśli chcesz, aby moduł obsługi zdarzeń był uruchamiany tylko dla każdego n-tego zdarzenia lub przestał strzelać po pierwszych zdarzeniach „n”, lub zacznij strzelać dopiero po pierwszych zdarzeniach „n”, możesz po prostu użyć HoF (odpowiednio filtr, takeUntil, pomiń) zamiast ustawiania, aktualizowania i sprawdzania liczników.
ii) poprawia lokalizację kodu - jeśli masz 5 różnych procedur obsługi zdarzeń zmieniających stan komponentu, możesz scalić ich obserwowalne i zdefiniować jedną procedurę obsługi zdarzeń na scalonej obserwowalnej zamiast tego, skutecznie łącząc 5 procedur obsługi zdarzeń w 1. To sprawia, że ​​jest to bardzo łatwo zrozumieć, jakie zdarzenia w całym systemie mogą wpłynąć na komponent, ponieważ wszystkie są obecne w jednym module obsługi.

  • Obserwowalny to dwoistość Iterowalnego.

Iterable to leniwie zużyta sekwencja - każdy element jest ciągnięty przez iterator, ilekroć chce go użyć, a zatem wyliczenie jest sterowane przez konsumenta.

Obserwowalna jest leniwie wytwarzana sekwencja - każdy przedmiot jest wypychany do obserwatora za każdym razem, gdy jest dodawany do sekwencji, a zatem wyliczenie jest sterowane przez producenta.

tldr
źródło
1
Dziękuję bardzo za tę prostą definicję obserwowalnego i jego odróżnienie od iterowalnych. Myślę, że często bardzo pomocne jest porównanie złożonej koncepcji ze znaną podwójną koncepcją, aby uzyskać prawdziwe zrozumienie.
2
„Więc idea FRP polega na tym, że zamiast przetwarzać każde pojedyncze zdarzenie, utwórz strumień zdarzeń (zaimplementowany z możliwym do zaobserwowania *) i zastosuj do tego HoF.” Mogę się mylić, ale uważam, że nie jest to tak naprawdę FRP, ale raczej ładna abstrakcja nad wzorcem projektowym Observer, który pozwala na funkcjonalne operacje za pośrednictwem HoF (co jest świetne!), A jednocześnie jest przeznaczony do użycia z kodem imperatywnym. Dyskusja na ten temat - lambda-the-ultimate.org/node/4982
nqe
18

Koleś, to cholernie genialny pomysł! Dlaczego nie dowiedziałem się o tym w 1998 roku? Tak czy inaczej, oto moja interpretacja samouczka Fran . Sugestie są mile widziane, myślę o uruchomieniu silnika gry opartego na tym.

import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy

pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')

class Time:
    def __float__(self):
        return epoch_delta()
time = Time()

class Function:
    def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
        self.var = var
        self.func = func
        self.phase = phase
        self.scale = scale
        self.offset = offset
    def copy(self):
        return copy(self)
    def __float__(self):
        return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
    def __int__(self):
        return int(float(self))
    def __add__(self, n):
        result = self.copy()
        result.offset += n
        return result
    def __mul__(self, n):
        result = self.copy()
        result.scale += n
        return result
    def __inv__(self):
        result = self.copy()
        result.scale *= -1.
        return result
    def __abs__(self):
        return Function(self, abs)

def FuncTime(func, phase = 0., scale = 1., offset = 0.):
    global time
    return Function(time, func, phase, scale, offset)

def SinTime(phase = 0., scale = 1., offset = 0.):
    return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()

def CosTime(phase = 0., scale = 1., offset = 0.):
    phase += pi / 2.
    return SinTime(phase, scale, offset)
cos_time = CosTime()

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    @property
    def size(self):
        return [self.radius * 2] * 2
circle = Circle(
        x = cos_time * 200 + 250,
        y = abs(sin_time) * 200 + 50,
        radius = 50)

class CircleView(Sprite):
    def __init__(self, model, color = (255, 0, 0)):
        Sprite.__init__(self)
        self.color = color
        self.model = model
        self.image = Surface([model.radius * 2] * 2).convert_alpha()
        self.rect = self.image.get_rect()
        pygame.draw.ellipse(self.image, self.color, self.rect)
    def update(self):
        self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)

sprites = Group(circle_view)
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            running = False
    screen.fill((0, 0, 0))
    sprites.update()
    sprites.draw(screen)
    pygame.display.flip()
pygame.quit()

W skrócie: Jeśli każdy element można traktować jak liczbę, cały system można traktować jak równanie matematyczne, prawda?

Dan Ross
źródło
1
Jest trochę późno, ale w każdym razie ... Frag to gra wykorzystująca FRP .
arx
14

Książka Paula Hudaka, The Haskell School of Expression , jest nie tylko świetnym wprowadzeniem do Haskell, ale także spędza sporo czasu na FRP. Jeśli dopiero zaczynasz przygodę z FRP, gorąco polecam, abyś wiedział, jak działa FRP.

Jest też coś, co wygląda jak nowe przepisanie tej książki (wydanej w 2011 r., Zaktualizowanej w 2014 r.), The Haskell School of Music .

cjs
źródło
10

Zgodnie z poprzednimi odpowiedziami wydaje się, że matematycznie myślimy po prostu w wyższej kolejności. Zamiast myśleć o wartości x mającej typ X , myślimy o funkcji x : TX , gdzie T jest rodzajem czasu, czy to liczbami naturalnymi, liczbami całkowitymi czy kontinuum. Teraz, gdy piszemy y : = x + 1 w języku programowania, faktycznie mamy na myśli równanie y ( t ) = x ( t ) + 1.

Yuning
źródło
9

Jak wspomniano, działa jak arkusz kalkulacyjny. Zwykle oparty na frameworku sterowanym zdarzeniami.

Jak w przypadku wszystkich „paradygmatów”, jego nowość jest dyskusyjna.

Z mojego doświadczenia z rozproszonymi przepływami aktorów, może łatwo paść ofiarą ogólnego problemu spójności stanu w sieci węzłów, tj. Kończy się to dużą oscylacją i pułapką w dziwnych pętlach.

Trudno tego uniknąć, ponieważ niektóre semantyki sugerują pętle referencyjne lub transmisje, i mogą być dość chaotyczne, ponieważ sieć aktorów zbiega się (lub nie) w jakimś nieprzewidywalnym stanie.

Podobnie niektóre stany mogą nie zostać osiągnięte, mimo że mają dobrze zdefiniowane krawędzie, ponieważ stan globalny odwraca się od rozwiązania. 2 + 2 może, ale nie musi, wynosić 4, w zależności od tego, kiedy 2 stało się 2 i czy tak pozostały. Arkusze kalkulacyjne mają synchroniczne zegary i wykrywanie pętli. Rozproszeni aktorzy na ogół nie.

Cała dobra zabawa :).

cesarz
źródło
7

Ten artykuł Andre Staltza jest najlepszym i najbardziej zrozumiałym wyjaśnieniem, jakie do tej pory widziałem.

Niektóre cytaty z artykułu:

Programowanie reaktywne to programowanie z asynchronicznymi strumieniami danych.

Ponadto otrzymujesz niesamowity zestaw funkcji do łączenia, tworzenia i filtrowania dowolnego z tych strumieni.

Oto przykład fantastycznych diagramów, które są częścią tego artykułu:

Kliknij schemat strumienia zdarzeń

Zielony gigant
źródło
5

Chodzi o matematyczne transformacje danych w czasie (lub ignorowanie czasu).

W kodzie oznacza to czystość funkcjonalną i programowanie deklaratywne.

Błędy stanu są ogromnym problemem w standardowym paradygmacie imperatywu. Różne fragmenty kodu mogą zmieniać stan współdzielenia w różnych „momentach” podczas wykonywania programów. Trudno sobie z tym poradzić.

We FRP opisujesz (jak w programowaniu deklaratywnym), w jaki sposób dane przekształcają się z jednego stanu do drugiego i co je wyzwala. Pozwala to zignorować czas, ponieważ twoja funkcja po prostu reaguje na dane wejściowe i wykorzystuje ich bieżące wartości do utworzenia nowej. Oznacza to, że stan jest zawarty na wykresie (lub drzewie) węzłów transformacji i jest funkcjonalnie czysty.

To znacznie zmniejsza złożoność i czas debugowania.

Pomyśl o różnicy między A = B + C w matematyce i A = B + C w programie. W matematyce opisujesz związek, który nigdy się nie zmieni. W programie jest napisane, że „W tej chwili” A to B + C. Ale następnym poleceniem może być B ++, w którym to przypadku A nie jest równe B + C. W matematyce lub programowaniu deklaratywnym A zawsze będzie równe B + C, bez względu na to, o który moment poprosisz.

Usuwając złożoność wspólnego stanu i zmieniając wartości w czasie. Twój program jest o wiele łatwiejszy do uzasadnienia.

EventStream to EventStream + jakaś funkcja transformacji.

Zachowanie to EventStream + Pewna wartość w pamięci.

Po uruchomieniu zdarzenia wartość jest aktualizowana przez uruchomienie funkcji transformacji. Wytworzona wartość jest przechowywana w pamięci zachowań.

Zachowania można komponować w celu uzyskania nowych zachowań, które są transformacją N innych zachowań. Ta skomponowana wartość zostanie ponownie obliczona podczas uruchamiania zdarzeń wejściowych (zachowań).

„Ponieważ obserwatorzy są bezstanowi, często potrzebujemy kilku z nich, aby symulować maszynę stanów, jak w przykładzie przeciągania. Musimy zapisać stan, w którym jest dostępny dla wszystkich zaangażowanych obserwatorów, na przykład na ścieżce zmiennej powyżej”.

Cytat z - Przestarzałe wzorce obserwatora http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf

Jay Shepherd
źródło
Tak właśnie myślę o programowaniu deklaratywnym, a ty po prostu lepiej opisujesz ten pomysł niż ja.
neevek
2

Krótkie i jasne wyjaśnienie dotyczące programowania reaktywnego pojawia się w Cyclejs - Programowanie reaktywne , wykorzystuje proste i wizualne próbki.

[Moduł / komponent / obiekt] jest reaktywny, co oznacza, że ​​jest w pełni odpowiedzialny za zarządzanie swoim własnym stanem poprzez reagowanie na zdarzenia zewnętrzne.

Jakie są zalety tego podejścia? Jest to odwrócenie kontroli , głównie dlatego, że [moduł / komponent / obiekt] jest odpowiedzialny za siebie, poprawiając enkapsulację metodami prywatnymi w stosunku do metod publicznych.

To dobry punkt startowy, a nie pełne źródło wiedzy. Stamtąd możesz przejść do bardziej złożonych i głębokich dokumentów.

pdorgambid
źródło
0

Sprawdź Rx, Reactive Extensions dla .NET. Wskazują, że w IEnumerable w zasadzie „wyciągasz” ze strumienia. Zapytania Linq nad IQueryable / IEnumerable to operacje na zestawach, które „wysysają” wyniki z zestawu. Ale z tymi samymi operatorami w IObservable możesz pisać zapytania Linq, które „reagują”.

Na przykład, możesz napisać zapytanie Linq jak (z mw MyObservableSetOfMouseMovements, gdzie mX <100 i mY <100 wybierz nowy punkt (mX, mY)).

a dzięki rozszerzeniom Rx to wszystko: masz kod interfejsu, który reaguje na nadchodzący strumień ruchów myszy i rysuje, gdy jesteś w polu 100 100 ...

strażnik
źródło
0

FRP to połączenie programowania funkcjonalnego (paradygmat programowania zbudowany na idei wszystkiego jest funkcją) i reaktywnego paradygmatu programowania (zbudowany na idei, że wszystko jest strumieniem (obserwator i obserwowalna filozofia)). To ma być najlepszy ze światów.

Na początek sprawdź post Andre Andreta na temat programowania reaktywnego.

Krishna Ganeriwal
źródło