Transparentné reaktívne programovanie
Reaktívne programovanie je mätúci pojem, pretože každý ho chápe inak. Pokrýva tak širokú škálu programovacích paradigiem a architektúr, že takmer nemá konkrétny zmysel. Preto je dôležité objasniť, ktoré reaktívne programovanie má človek na mysli. Tu sa pokúsim jasne definovať jeden druh reaktívneho programovania, ktoré sa zvyčajne nazýva transparentné reaktívne programovanie.
Táto téma ma obzvlášť zaujíma, pretože vyvíjam Hookless, javovskú knižnicu pre transparentné reaktívne programovacie. V rámci jeho vývoja som skúmal rôzne koncepty a prístupy reaktívneho programovania. Tu je to, čo som zistil.
Dávno, pradávno ľudia väčšinou písali dávkové programy (batche). Interaktívne používateľské rozhrania, najmä GUI, si však vyžadovali veľmi odlišný spôsob štruktúrovania programov, ktorý sa stal známym ako eventové programovanie (event-driven programming). Udalosťami riadený program väčšinou pozostáva z rutín reagujúcich na udalosti, z ktorých každá vykonáva činnosti v reakcii na nejakú udalosť. Bola to prvá paradigma, ktorá bola do istej miery reaktívna. Programovanie riadené udalosťami však vytvára zložitosť a chyby. Ako sa program zväčšuje a jeho dátové štruktúry sú komplikovanejšie a redundantné, rutiny ošetrujúce udalosti sú dlhšie a komplikovanejšie, ako sa snažia aktualizovať všetky dátové štruktúry, ktoré sú ovplyvnené udalosťou. Zložitosť týchto rutín sa nevyhnutne stane úkrytom pre nekonečné chyby synchronizácie údajov a používateľského rozhrania.
Základnou myšlienkou reaktívneho programovania bolo modelovať aplikáciu ako tok informácií od normalizovaných základných dátových štruktúr cez vypočítané redundantné dátové štruktúry až po formátované dáta zobrazené v používateľskom rozhraní. Stále ste to mohli implementovať pomocou udalostí, ale teraz všetky udalosti signalizovali zmenu dát. Pri reaktívnom programovaní sa redundantné údaje neaktualizujú priamo udalosťami reagujúcimi na činnosť používateľa, ale predovšetkým následnými udalosťami signalizujúcimi zmeny v dátach. Dokonca aj vrchnú vrstvu používateľského rozhrania možno považovať za redundantnú dátovú štruktúru odvodenú zo základného dátového modelu. Rôzne časti programu spolu tvoria graf závislostí, ktorý je charakteristickým znakom reaktívneho programovania.
Existuje súvisiaci koncept dataflow programovania (dataflow programming), ktorý tiež organizuje výpočty okolo grafu závislostí. Ak si chcete predstaviť dataflow program, predstavte si hardvérové obvody alebo mergesort na veľkých súboroch. Niektorí ľudia vidia reaktívne programovanie ako podmnožinu dataflow programovania, ale tieto dva pojmy sa veľmi prekrývajú a rozdiel je skôr v zameraní a aplikáciách. Dataflow programovanie sa zameriava na priepustnosť a paralelizmus a nachádza uplatnenie v hardvéri a klastroch, kým reaktívne programovanie sa zameriava na odozvu a najčastejšie sa používa na používateľské rozhrania.
MVVM (model-view-viewmodel) štruktúruje programy ako graf závislosti, ale nemusí byť nutne reaktívny. Normalizované základné dátové štruktúry sú model, vypočítané dátové štruktúry sú viewmodel a vrchná vrstva užívateľského rozhrania je view. Keďže MVVM program je už beztak graf závislostí, MVVM programy sú vhodné pre reaktívne programovanie. Data binding je zvyčajne len špecializované reaktívne prepojenie medzi užívateľským rozhraním a viewmodelom v MVVM. Reaktívne programovanie ale môže byť použité aj na iné veci než programovanie používateľských rozhraní. A aj keď vezmeme do úvahy iba používateľské rozhrania, existuje oveľa viac spôsobov, ako štruktúrovať reaktívne používateľské rozhrania, než len MVVM.
Keď počujete niekoho hovoriť o vysokovýkonnom reaktívnom programovaní, s najväčšou pravdepodobnosťou hovoria o streamovo orientovanom (stream-oriented) reaktívnom programovaní, v ktorom každý uzol v grafe závislosti je tokom dát prichádzajúcich v priebehu času. RxJava a podobné Rx knižnice pre iné jazyky sú príkladom streamového reaktívneho programovania. Výhodou streamov je, že vám umožňujú vykonávať elegantné transformácie, najmä časové pravidlá ako je oneskorenie alebo obmedzenie rýchlosti, a umožňujú vám spoľahlivo spracovať všetky hodnoty, ktoré prechádzajú streamom, pokiaľ ste ich výslovne neodfiltrovali.
Streamy však pridávajú do programu veľa redundantného kódu, obzvlášť ak vás zaujíma iba posledná hodnota v streame. O tom je stavovo orientované (sync-oriented, state-oriented) reaktívne programovanie. V každom uzle grafu závislostí nájdete len najnovšiu hodnotu. Časové pravidlá sú stále možné, ale už nie sú tak prirodzené. A stavovo orientované reaktívne knižnice často nemilosrdne vyhodia neprečítané hodnoty, ak konzument dát nie je dostatočne rýchly, čo znamená, že už nemôžete spoľahlivo spracovať všetky vyprodukované dáta. Stavovo orientované reaktívne programovanie je však veľmi praktické v používateľských rozhraniach, kde najnovšia hodnota je zvyčajne to, čo chce používateľ vidieť.
Reaktívne programovanie môže buď využívať push propagáciu zmien, pri ktorej sa zmeny slepo tlačia od producentov k prihláseným konzumentom, alebo push-pull propagáciu, čo znamená, že spotrebitelia preberajú zmeny kedykoľvek po ľahkej notifikácii od producentov. Niektoré push systémy používajú spätný tlak (backpressure), čo je v podstate len inak spravený push-pull. Stavovo orientované reaktívne programovanie nepotrebuje spätný tlak, pretože môže jednoducho prepísať staršie dáta, čo zaisťuje, že spotreba pamäte je vždy limitovaná. Napriek tomu mu prospieva push-pull, pretože spracovanie každej zmeny môže stále zaťažovať CPU.
Aby sme zhrnuli to, o čom sme doteraz diskutovali, programovanie používateľského rozhrania vo všeobecnosti benefituje zo stavovo orientovaného reaktívneho programovania s push-pull propagáciou zmien. Tak vyzerá všeobecná architektúra, ale ako ju implementovať bez toho, aby sme museli písať príliš veľa kódu?
Tu prichádza na rad funkčné reaktívne programovanie (functional reactive programming, FRP). Vo funkčnom reaktívnom programovaní je graf závislostí vytvorený pomocou preddefinovaných funkčných operátorov (napr. map, filter, ...), ktoré akceptujú jeden alebo viac reaktívnych vstupných uzlov a vrátia nový reaktívny výstupný uzol. Operátory je možné dodatočne parametrizovať pomocou čistých funkcií (pure functions), ktoré implementujú aplikačnú logiku. Vytváranie programov týmto spôsobom je dosť obmedzujúce, ale výhodou je, že reaktivita je plne zapuzdrená v operátoroch a graf závislostí je implicitne skonštruovaný pri použití operátorov. Funkčné reaktívne programovanie možno použiť so streamovým aj stavovým reaktívnym programovaním, ale je bežnejšie v streamovom reaktívnom programovaní.
Funkčné reaktívne programovanie, okrem toho, že je výrazne obmedzujúce v tom, ako je program štruktúrovaný, zvyčajne tiež podporuje len statický graf závislostí, ktorý sa vytvorí raz a potom sa nechá bežať tak, ako je. Aplikácie, najmä používateľské rozhrania, však často prepínajú medzi zobrazeniami (okná, formuláre, stránky, karty) a dokonca menia štruktúru pohľadu, ako používateľ vyberá rôzne možnosti. Takéto aplikácie majú prirodzene dynamický graf závislostí, ktorý sa môže zmeniť po každej akcii používateľa.
Asi najznámejším reaktívnym používateľským rozhraním je tabuľkový kalkulátor. Bunky tabuľky tvoria graf závislostí. Vypočítané bunky sú reaktívne závislé od všetkých buniek použitých v ich výrazoch. Keď sa jedna bunka zmení, všetky závislé bunky sa okamžite aktualizujú. Na základe toho, o čom sme doteraz hovorili, môžeme tabuľkový kalkulátor ľahko klasifikovať ako stavovo orientovanú reaktívnu aplikáciu. Ľudia často nevedia, že pozície buniek použité vo výrazoch sa dajú vypočítať. To znamená, že tabuľkové kalkulátory majú dynamický graf závislostí, ktorý sa môže zmeniť vždy, keď sa zmenia akékoľvek údaje v tabuľke. Môžeme tiež povedať, že tabuľkový kalkulátor nepoužíva funkčné reaktívne programovanie. Bunkové výrazy nie sú čisté funkcie. Čítajú iné bunky a dokonca si môžu vybrať, ktoré bunky budú čítať.
To, čo tabuľkový kalkulátor implementuje, sa všeobecne nazýva transparentné reaktívne programovanie. Závislosti sa zisťujú automaticky pozorovaním, k akým údajom sa pristupuje. Graf závislostí sa vytvorí pri prvom spustení kódu a pri každom nasledujúcom spustení sa zahodí a znova vytvorí. Transparentné reaktívne programovanie preto prirodzene podporuje dynamický graf závislosti. Uzly v grafe závislosti sú definované ako bežné funkcie, ktoré čítajú hodnoty iných uzlov a vrátia hodnotu aktuálneho uzla. Tieto čítania sa starajú iba o najnovšiu hodnotu, takže transparentné reaktívne programovanie je stavovo orientované reaktívne programovanie.
Transparentné reaktívne programovanie je preto ideálne pre používateľské rozhrania. Niet divu, že ho používajú všetky populárne reaktívne knižnice pre JavaScript: Vue.js, React, Knockout.js, Svetle, Meteor. Knižnice podporujúce transparentné reaktívne programovanie sú dostupné aj v iných jazykoch. Čo si pamätám: Assisticant v NET-e a Streamlit v Pythone. Java, ktorá ma obzvlášť zaujíma, má pokiaľ viem iba Quasar dataflow a ten je príliš jednoduchý a vyžaduje inštrumentáciu bytekódu. Preto vyvíjam Hookless, transparentnú reaktívnu programovaciu knižnicu pre Javu.
Jedným z najjednoduchších spôsobov implementácie transparentnej reaktivity je vykonať úplný refresh po každej zmene. Videohry to robia, keď len prekreslia celú obrazovku zakaždým, keď ju potrebujú aktualizovať. Je to jednoduché a veľa aplikácií nepotrebuje viac. Úplný refresh je extrémnym koncom na škále granularity reaktívneho programovania. Na druhom konci škály sú experimentálne programovacie jazyky, v ktorých je každá premenná reaktívna. Reaktívne programovanie však prináša určitú neefektívnosť, výpočtovú aj v zložitosti kódu, čo robí reaktivitu na úrovni premenných nepraktickou. Aplikácie musia byť navrhnuté pre určitú primeranú granularitu. Zvyčajne sa prepočíta jeden malý dokument (asi ako JSON súbor) naraz.
Aby som to zhrnul, tu je to, čo charakterizuje transparentné reaktívne programovanie (TRP):
- TRP sa zvyčajne používa v používateľských rozhraniach a aplikačných front-endoch.
- Najznámejšími predstaviteľmi sú knižnice používateľského rozhrania pre JavaScript: React, Vue.js a ďalšie.
- MVVM a data binding sú len špeciálne a obmedzené prípady TRP.
- Výpočty sa vykonávajú podľa grafu závislosti, ktorý je dynamický a automaticky odvodený z ľubovoľného kódu na rozdiel od funkčného reaktívneho programovania s jeho statickou sieťou reaktívnych operátorov.
- TRP je stavovo orientovaný, kým väčšina reaktívneho programovonia je streamová. Propagácia zmien v TRP je prirodzene push-pull.
- Reaktivita má určitú granularitu, zvyčajne okolo veľkosti malého dokumentu alebo veľkého objektu.
Pravdepodobne najjednoduchší spôsob, ako opísať transparentné reaktívne programovanie, je prirovnať ho k tabuľkovým procesorom. Podobne ako v tabuľkovom kalkulátore, ak zmeníte jednu hodnotu, všetky hodnoty z nej vypočítané sa automaticky aktualizujú. Až na to, že transparentné reaktívne programovanie vám umožňuje použiť ľubovoľný kód na výpočet hodnôt.