Jak jest obsługiwana reaktywność funkcjonalna w Shiny?

Reaktywność sprawia, że ​​aplikacje Shiny reagują. Wygląda na to, że aplikacja aktualizuje się natychmiast, gdy użytkownik dokona zmiany. Jednak w rzeczywistości Shiny ponownie uruchamia twoje wyrażenia R w dokładnie zaplanowany sposób co kilka mikrosekund, co stwarza iluzję responsywności. Nie musisz wiedzieć, jak zachodzi reaktywność, aby go używać, ale zrozumienie reaktywności sprawi, że będziesz lepszym programistą Shiny.

Pamiętasz, że kiedy uruchomiliśmy naszą bardzo prostą aplikację Shiny w poprzedniej sekcji, konsola R przestała być interaktywna? Cóż, działo się tak, ponieważ wykonywanie funkcji shinyApp() sprawia, że ​​R jest zajęty przez ciągłe monitorowanie i aktualizowanie wyrażeń w razie potrzeby, co tworzy wrażenia responsywne dla użytkowników. Teraz wyobraź sobie, że masz złożoną aplikację z dużą ilością interaktywności, a uruchamianie każdego wyrażenia co kilka mikrosekund całkowicie nasyciłoby twój procesor, a użytkownicy mieliby okropne wrażenia z użytkowania. Dlatego Shiny musi być wystarczająco inteligentny, aby aktualizować tylko te wyrażenia, które tego wymagają. Za każdym razem, gdy użytkownik przesyła akcję (zdarzenie), wyrażenia obsługujące takie zdarzenia zostają unieważnione, co oznacza, że ​​wymagają aktualizacji, a to zachowanie jest propagowane wśród wszystkich wyrażeń, które od nich zależą. Po upływie kilku mikrosekund R sprawdzi, które wyrażenia są oznaczone do aktualizacji i tylko je zaktualizuje. Opisany mechanizm może zmniejszyć liczbę przeliczanych wyrażeń z tysięcy do żadnego, na wypadek gdyby nie było żadnej akcji użytkownika, a najwyżej do kilku, ponieważ użytkownikowi bardzo trudno jest wiele osiągnąć w ciągu kilku mikrosekund co z kolei skutkowałoby kilkoma wymaganymi aktualizacjami, zamiast pełnej aktualizacji aplikacji za każdym razem. Ten mechanizm pozwala R na obsługę złożonych aplikacji Shiny i jest kluczem do reaktywności. Pozwala na jak najszybszą aktualizację aplikacji, dzięki czemu koordynacja wejścia / wyjścia jest niemal natychmiastowa.

Co to jest funkcjonalne programowanie reaktywne i dlaczego jest przydatne?

Zacznijmy od reaktywnej części programowania. Programowanie reaktywne to programowanie z asynchronicznymi strumieniami danych. Zaczynamy od zdefiniowania tych terminów na poziomie ogólnym. Strumień to sekwencja trwających wydarzeń uporządkowanych w czasie. W rzeczywistości prawie wszystko można traktować jako strumień, ale prostymi przykładami są odbijające się piłki, gdzie zdarzenie jest brane pod uwagę za każdym razem, gdy piłka uderza w podłogę. Może się to zdarzyć wielokrotnie, wiele razy, bez określonych wzorców, zatrzymać się na chwilę, następnie kontynuować, a następnie ponownie. Użytkownicy klikający w witrynie to także strumień, w którym każde kliknięcie jest zdarzeniem. Jak możesz sobie wyobrazić, strumienie są wszędzie wokół nas. Drugi termin, który należy zdefiniować, jest asynchroniczny, co dosłownie oznacza bez synchronizacji. Zwykle funkcje synchroniczne czekają w wierszu wywołania funkcji do zakończenia wykonywania wywoływanej funkcji, prawdopodobnie zwracając wartość. W ten sposób programowaliśmy do tej pory. Jednak funkcje asynchroniczne niekoniecznie czekają na zakończenie funkcji, które wywołują. Oznacza to, że nasze funkcje muszą reagować na to, gdy tylko nadejdzie. Jeśli połączymy te dwa terminy, zrozumiemy, że programowanie z asynchronicznymi strumieniami danych działa poprzez pisanie kodu, który jest w stanie reagować na zdarzenia w sposób ciągły i losowy. W przypadku tego rozdziału zdarzeniami tymi będą interakcje użytkownika (kliknięcia lub naciśnięcia klawiszy) z naszą aplikacją, co oznacza, że ​​nasz kod R będzie odpowiadał bezpośrednio na te kliknięcia i naciśnięcia klawiszy, gdy się one pojawią. Jeśli nadal trudno jest pojąć ten pomysł, pomyśl o tym jak o arkuszu kalkulacyjnym z formułami. Gdy zmienisz wartość, od której inne komórki zależą lub której nasłuchują (analogicznie do otrzymywania pewnych danych wejściowych od użytkownika w naszej aplikacji), wtedy inne komórki odpowiednio reagują i prezentują nowo obliczoną wartość (która będzie zmianą wyjściową, którą pokazujemy użytkownikowi ). To naprawdę takie proste. Odsłuchiwanie strumienia nazywa się subskrybowaniem. Funkcje, które definiujemy, są obserwatorami, a strumień jest obserwowalną istotą. To jest dokładnie wzorzec projektowy obserwatora. Zapoznaj się z książką Gamma, Helm, Johnson i Vlissides Design Patterns: Elements of Reusable Object-Oriented Software, autorstwa Addison-Wesley, 1994. Ponadto otrzymujesz wspaniały zestaw narzędzi, które pozwalają tworzyć, filtrować i łączyć dowolne z tych strumieni. W tym miejscu wkracza magia programowania funkcjonalnego. Programowanie funkcjonalne pozwala na kompozycję i właśnie do tego będziemy go używać, do komponowania strumieni. Strumień może służyć jako dane wejściowe do innego. Nawet wiele strumieni może być używanych jako dane wejściowe dla wielu innych. Ponadto możesz użyć dowolnego z tych surowych lub przekształconych strumieni w dowolnym miejscu kodu. To naprawdę sprawia, że ​​Shiny jest tak wspaniałym narzędziem.

Funkcjonalne programowanie reaktywne podnosi poziom abstrakcji kodu, dzięki czemu możesz skupić się na współzależności zdarzeń, które definiują logikę aplikacji, zamiast ciągłego majstrowania przy dużej liczbie szczegółów implementacji. Kod funkcjonalny reaktywny będzie również prawdopodobnie bardziej zwięzły. Korzyści są bardziej widoczne w nowoczesnych aplikacjach, które są wysoce interaktywne. W dzisiejszych czasach aplikacje mają mnóstwo zdarzeń w czasie rzeczywistym, które umożliwiają wysoce interaktywne doświadczenia, a funkcjonalne programowanie reaktywne jest do tego doskonałym narzędziem

Przedstawiamy architekturę i reaktywność aplikacji Shiny

W najprostszej formie aplikacja Shiny wymaga serwera i interfejsu użytkownika (UI). Te komponenty tworzą podstawową architekturę wszystkich aplikacji Shiny. Obiekt ui steruje układem i wyglądem aplikacji, a funkcja server zawiera logikę wymaganą przez aplikację. Jeśli wiesz, jak działają aplikacje internetowe, możesz traktować je odpowiednio jako frontend i backend. Funkcja shinyApp() tworzy i uruchamia aplikację Shiny z jawnej pary interfejs użytkownika / serwer. Skompiluje kod R do przyjaznych dla sieci języków HTML, JavaScript i CSS. Poniżej mamy najprostszą możliwą aplikację Shiny, która ma pusty serwer i UI z podstawowym komunikatem. Jeśli jesteś w interaktywnej sesji języka R, twoja przeglądarka internetowa powinna się uruchomić i pokazać aplikację. Jeśli tak się nie stanie, możesz samodzielnie przejść do adresu URL, który znajduje się w formularzu http://127.0.0.1:6924/, gdzie 127.0.0.1 jest IP twojego komputera a 6924 jest portem, którego Shiny używa do nasłuchiwania połączeń. Jak widzisz w swojej przeglądarce, nie jest to nic niesamowitego, ale jest to działająca aplikacja internetowa stworzona wyłącznie przy użyciu R:

library(shiny)

server <- function(input, output) { }

ui <- fluidPage(„This is Shiny application.”)

shinyApp(ui, server)

Posiadanie stałego portu zamiast losowo przypisanego portu, który zmienia się przy każdym wywołaniu sinyApp(), ułatwia programowanie. Aby użyć stałego portu, zmień wywołanie funkcji shinyApp(ui, server. options = list(port = 6924)) na port, który Ci odpowiada.

Zauważ, że twoja sesja R będzie zajęta, gdy aplikacja jest aktywna, więc nie będziesz mógł uruchamiać żadnych poleceń R. R monitoruje aplikację i wykonuje jej reakcje. Aby odzyskać sesję R, naciśnij Ctrl + C lub, jeśli używasz RStudio, kliknij ikonkę znaku stopu. Dla uproszczenia tworzymy nasze aplikacje Shiny w jednym pliku. Jednak w przypadku większych aplikacji prawdopodobnie podzielisz komponenty na pliki i (które są standardowymi plikami używanymi w aplikacjach Shiny). Jak zobaczymy, Shiny zapewnia świetny paradygmat do tworzenia aplikacji internetowych, który jest obecnie używany w wielu nowatorskich systemach. Nazywa się to funkcjonalnym programowaniem reaktywnym. Nie jest to prosta koncepcja do zrozumienia, ale jest bardzo potężna i nauczymy się korzystać z jej podstaw. Jednak zanim to zrobimy, spróbuję w prosty sposób wyjaśnić, czym jest i jak działa w Shiny.

Dodawanie interaktywności z pulpitami nawigacyjnymi

Shiny umożliwia pisanie potężnych, interaktywnych aplikacji internetowych w całości w R. Używając języka R, możesz stworzyć interfejs użytkownika i serwer, a Shiny skompiluje Twój kod R do kodu HTML, CSS i JavaScript potrzebnego do wyświetlenia aplikacji w sieci. To, co sprawia, że ​​aplikacja Shiny jest szczególnie wydajna, to fakt, że może ona wykonywać kod R na zapleczu, dzięki czemu aplikacja może wykonywać dowolne obliczenia języka R, które można uruchomić na pulpicie. Możesz chcieć, aby aplikacja przetwarzała niektóre dane na podstawie danych wejściowych użytkownika i zapewniała pewną interaktywność, aby analiza danych była bardziej intuicyjna. W tym rozdziale pokażemy, jak to osiągnąć. Shiny implementuje funkcjonalny paradygmat programowania reaktywnego, który obsługuje wiele z najnowocześniejszych aplikacji internetowych. Wyjaśnimy, co to jest i jak działa w Shiny. Pokażemy, jak pracować ze strumieniami zdarzeń pochodzących od użytkowników aplikacji i jak odpowiednio na nie reagować. Aby to zrobić, przeanalizujemy przykład, który otrzymuje dane wejściowe od użytkowników i dostarcza w zamian dane i wykresy. Pod koniec tego rozdziału zdasz sobie sprawę, jak łatwe może być tworzenie potężnych aplikacji internetowych, które przenoszą Twoje umiejętności R na wyższy poziom. Niektóre z ważnych tematów omawianych w tym rozdziale są następujące:

* Architektura aplikacji internetowych Shiny

* Funkcjonalny paradygmat programowania reaktywnego

* Jak reaktywność jest implementowana w Shiny

* Odbieranie danych wejściowych z interakcji użytkowników

* Wysyłanie wyników w odpowiedziach do przeglądarki internetowej

* Dodawanie interakcji do aplikacji Shiny

Wymagane pakiety

Pracowaliśmy już z pierwszymi dwoma pakietami, a mianowicie ggplot2 i lubridate . Pakiet shiny jest używany do tworzenia aplikacji internetowych bezpośrednio z języka R, a pakiety shinythemes i ggthemr są używane do stosowania motywów w celu nadania stylu naszej aplikacji internetowej. Pakiety wymagane dla tej części przedstawiono  poniżej:

Pakiet : Powód

ggplot2 : Wysokiej jakości wykresy

lubridate : Łatwo zmieniaj daty

shiny : Twórz nowoczesne aplikacje internetowe

ggthemr : Zastosuj motywy do wykresów ggplot2

shinythemes : Zastosuj motywy do aplikacji Shiny

Wyspecjalizowane dystrybucje R

Wreszcie, jeśli żadna z poprzednich opcji nie zadziałała, możesz również użyć wyspecjalizowanych dystrybucji R. Te dystrybucje są utrzymywane niezależnie od zwykłej dystrybucji R i koncentrują się na optymalizacji określonych aspektów w R. Niektóre z nich są zbudowane w celu zapewnienia precyzyjnej kontroli równoległości, a inne robią to automatycznie. Nauka korzystania z tych dystrybucji może wymagać dużo czasu, co może być korzystne lub nie w Twoim konkretnym przypadku.

Podsumowanie

Zobaczyliśmy najważniejsze przyczyny powolnego kodu R: programowanie bez zrozumienia niezmienności obiektów, charakter interpretowanych typów dynamicznych, procesy związane z pamięcią i procesy jednowątkowe. Dowiedzieliśmy się, że pierwszy można zredukować za pomocą odpowiedniego R, drugi można zredukować, delegując do języków statystycznie typowanych, takich jak Fortran lub C ++, trzeci można zredukować za pomocą mocniejszych komputerów (szczególnie z większą ilością pamięci RAM), oraz wreszcie czwarty można zredukować za pomocą równoległości. Wspomnieliśmy również o niektórych zmiennych, które możemy chcieć wziąć pod uwagę przy podejmowaniu decyzji, czy optymalizować nasze implementacje, jak mała różnica we wdrożeniu może skutkować dużymi ulepszeniami wydajności oraz jak wzrost wydajności wynikający z tych ulepszeń może rosnąć wraz ze wzrostem rozmiaru nakładów wzrasta. Na koniec dowiedzieliśmy się, jak profilować i porównywać w celu ulepszenia naszych wdrożeń.

Elastyczność i moc dzięki przetwarzaniu w chmurze

Czasami nie potrzebujesz nawet większej mocy obliczeniowej ani efektywnego wykorzystania zasobów. Czasami wystarczy uruchomić R na innym komputerze bez wiązania własnego przez wiele godzin lub dni. W takich przypadkach korzystanie z zasobów przetwarzania w chmurze może być bardzo przydatne. Zasoby przetwarzania w chmurze są przydatne nie tylko wtedy, gdy chcesz użyć dodatkowej maszyny, ale są bardzo wydajnym sposobem na pozyskanie superkomputerów do wykonania pracy za Ciebie. Bardzo łatwo jest zbudować maszynę z 64 rdzeniami procesora i 512 GB pamięci RAM. Korzystanie z takiego systemu może być tańsze niż myślisz i może być wykorzystane do bardzo kosztownych obliczeń, które zajęłyby zbyt dużo czasu w standardowym sprzęcie

Korzystanie ze specjalistycznych pakietów do wydajności

Innym dobrym sposobem na zwiększenie wydajności implementacji jest wyszukanie wyspecjalizowanych funkcji opublikowanych w pakietach CRAN lub gdzie indziej. Zanim przejdziesz do zmiany własnego kodu, spójrz i zobacz, czy możesz znaleźć bardzo wydajną implementację w innym miejscu. Istnieją ogromne różnice w jakości i szybkości pakietów CRAN, ale ich wykorzystanie może zdecydowanie zaoszczędzić sporo czasu. Dwa bardzo wydajne pakiety, które pomogą Ci opracować wydajne implementacje, to pakiety dta.table i dplyr. Mogą zapewnić wydajne sposoby radzenia sobie z ramkami danych, a także w przypadku dplyr innych obiektów. Biblioteka podprogramów podstawowej algebry liniowej (BLAS) może być również bardzo pomocna podczas wykonywania operacji algebry liniowej. Jest napisany przy użyciu języka Fortran i jest wysoce zoptymalizowany.

Poprawa zarządzania danymi i pamięcią

R, jak każdy język programowania, jest ograniczony przez procesor, pamięć RAM i wejścia / wyjścia, a my skupiliśmy się na zwiększeniu szybkości części procesora. Jednak znaczny wzrost wydajności można osiągnąć, zwiększając efektywność wykorzystania pamięci RAM i we / wy. Pomiar wykorzystania pamięci RAM (pamięci) najlepiej wykonywać poza językiem R przy użyciu narzędzi dostarczonych przez system operacyjny do tego właśnie celu. Informacje, które raportują te narzędzia, różnią się w zależności od systemu operacyjnego, ale oto kluczowe wskaźniki, na które należy zwrócić uwagę: użycie procesora, wolna pamięć, pamięć fizyczna, rozmiar wymiany i bajty odczytu / zapisu na sekundę. Jeśli napotkasz wysokie wykorzystanie procesora, procesor jest prawdopodobnie głównym wąskim gardłem dla wydajności R. Użyj technik profilowania opisanych w tym rozdziale, aby zidentyfikować, które części kodu zajmują większość czasu procesora. Jeśli napotkasz wystarczającą ilość wolnej pamięci systemowej z wysokim poziomem operacji we / wy dysku, kod prawdopodobnie wykonuje wiele operacji odczytu / zapisu na dysku. Usuń wszystkie niepotrzebne operacje we / wy i przechowuj dane pośrednie w pamięci, jeśli jest wystarczająca ilość pamięci. Jeśli napotkasz niskie wykorzystanie procesora i małą ilość wolnej pamięci systemowej przy dużym rozmiarze wymiany, prawdopodobnie systemowi zabraknie pamięci fizycznej i tym samym wymienia pamięć na dysk. W takim przypadku sprawdź, czy masz wystarczająco dużo zasobów, aby obsłużyć obciążenia, które wysyłasz do R, a jeśli to zrobisz, spróbuj użyć funkcji rm(), aby usunąć nieużywane obiekty, które czekają na pamięć z sesji R. Jeśli napotkasz scenariusz podobny do poprzedniego, ale wiesz, że nie masz wystarczającej ilości pamięci, aby obsłużyć pełne dane, z którymi pracujesz, nawet jeśli zrobiłeś to skutecznie, możesz spróbować podzielić swoje dane na partycje. Czy możesz pracować z podzbiorem danych według części, a następnie połączyć wyniki? Jeśli tak, powinieneś spróbować. Na przykład, jeśli pełne dane nie mieszczą się w pamięci i próbujesz znaleźć maksymalną wartość, możesz podzielić dane na cztery części, załadować je po kolei, obliczyć maksimum dla każdej z nich, i usuń je z pamięci po wykonaniu tej czynności, zachowując maksimum, a następnie uzyskując maksimum z czterech maksimów obliczonych oddzielnie. Inną możliwością dla scenariusza takiego jak poprzedni jest po prostu migracja obsługi danych do bazy danych. Bazy danych są wyspecjalizowanymi narzędziami do przetwarzania danych i pozwalają uniknąć wąskiego gardła w R, ponieważ tylko wstępnie przetworzony podzbiór potrzebnych danych jest wprowadzany do R. Większość baz danych obecnie wykonuje również bardzo wydajne proste operacje, takie jak znajdowanie maksimum.

Korzystanie z zapamiętywania lub warstw pamięci podręcznej

Jeśli masz algorytmy deterministyczne, za każdym razem, gdy zapewniasz równe dane wejściowe, powinieneś otrzymywać równe wyniki, a jeśli tak jest, a proces przejścia od danych wejściowych do wyjściowych jest bardzo czasochłonny, możesz użyć zapamiętywania lub warstw pamięci podręcznej. Podstawową ideą jest to, że przechowujesz kilka kopii danych wejściowych i wyjściowych, a za każdym razem, gdy dane wejściowe są wysyłane do funkcji, przed obliczeniem danych wyjściowych, sprawdzasz, czy dane wyjściowe tego konkretnego wejścia zostały wcześniej obliczone. Jeśli tak, wyślij to, zamiast wykonywać całą pracę ponownie. Oznacza to, że dane wyjściowe dla każdego wejścia należy obliczać tylko raz. Powinieneś spróbować zaimplementować taką warstwę w funkcji  fibonacci_recursive() utworzoną na początku , aby zobaczyć, jak duży wpływ mogą mieć tego rodzaju techniki, nawet przy użyciu powolnych algorytmów. Czasami tego typu techniki są również używane, nawet jeśli dane wyjściowe dla danego wkładu zmieniają się w czasie. Wszystko, co musisz zrobić w takich przypadkach, to zapewnić mechanizm, który unieważni lub usunie przechowywaną relację wejście / wyjście po określonym czasie, tak aby została ponownie obliczona następnym razem, gdy dane wejściowe zostaną użyte.

Kompilacja Just-in-time (JIT) kodu R

R obsługuje również kompilację Just-in-Time (JIT). Gdy kompilacja JIT jest włączona, język R automatycznie kompiluje bajtami kod, który jest wykonywany bez jawnego wywoływania jednej z funkcji kompilacji. Aby aktywować kompilację JIT, użyj funkcji enableJIT(). Argument level mówi R, ile kodu należy skompilować przed wykonaniem; 0 wyłącza JIT, 1 kompiluje funkcje przed ich pierwszym użyciem, 2 kompiluje także funkcje przed ich zduplikowaniem, a 3 także kompiluje pętle przed ich wykonaniem:

library(compiler)

enableJIT(level=3)

Kompilację JIT można również włączyć, ustawiając środowisko R_ENABLE_JIT w systemie operacyjnym przed uruchomieniem R. Wartość R_ENABLE_JIT należy ustawić na wartość argumentu level.