Reaktivní funkce a reaktivní proměnné v Shiny

Jednoduché situace

V jednoduchých Shiny aplikacích vytváříme bloky kódu, které reagují na změny hodnot ovládacích prvků uživatele (UI).

Například následující aplikace má tři vstupy, čísla stranaA, stranaB a vek.

output$vysledek <- renderText({      # blok 1
    x <- input$stranaA * input$stranaB
    return( paste0("Vypočtený obsah obdélníku = ", x)  )
})

output$vekTazatele <- renderText({   # blok 2
    x <- input$vek
    return( paste0("Váš věk je ", x)  )
})

Na rozdíl od klasických skriptů, kde se postupně provádějí všechny příkazy řádek po řádku, se Shiny aplikace chovají poněkud jinak.

Program v souboru server.R jakoby „spí“. Teprve pokud uživatel změní nějaký ze vstupů, server se „probudí“. Ale jen ten blok, který daný vstup používá.

Schématicky můžeme situaci znázornit tímto obrázkem:

Pokud uživatel změní stranaA nebo stranaB, provede se jen první blok, protože ten tyto proměnné používá. Druhý blok se neprovádí.

Naopak, pokud uživatel změní vek, provede se jen druhý blok a první se neprovádí.

Pro tyto jednoduché aplikace se vše chová dle očekávání a programátor si ani mnohdy neuvědomí, jak provádění programu interně funguje. Protože vše funguje, tak proč to řešit?

Složitější situace

Co když ale chceme na základě jednoho vstupu nastavit několik výstupů? Například uživatel stiskne tlačítko s odpovědí a my chceme zároveň vykreslit obrázek vyhodnocující správnost odpovědi a zároveň vygenerovat a zobrazit text nové otázky.

Zde nastává problém, protože zobrazení textu a zobrazení obrázku musíme dle logiky Shiny aplikací řešit ve dvou samostatných blocích.

Představme si situaci aplikace na zkoušení. Zobrazí se text otázky, na kterou jsou možné odpovědi ano / ne, k čemuž jsou přítomná tlačítka. Po jejich stisknutí se odpověd vyhodnotí ve formě obrázku (usmívající se / mračící se panďulák) a zároveň se vybere nová otázka, jejíž text se opět ihned zobrazí.

Naivní schéma takového programu by vypadalo následovně:

Problém je, že nikde není zaručeno, který ze dvou bloků (vyhodnoť / vymysli) zareaguje rychleji. Ideálně by asi v naší představě měly reagovat současně, ale reálně bude vždy jeden na serveru vyhodnocen trochu později. A nikdo nezaručí, který to zrovna bude. V praxi je to skutečně náhodné. Jednou zareaguje dříve blok pro text, jindy pro obrázek.

A teď to domysleme do detailů. Pokud zareaguje dříve blok s vymýšlením nové otázky a zobrazením textu, tak ihned následující blok s vyhodnocením a zobrazováním obrázku bude porovnávat uživatelovu odpověď s již nově vytvořenou otázkou, což je nesmysl! A skutečně, taková situace nastává velice často, ale ne vždy. Na pohled se takový program chová naprosto zmateně a nesprávně. Tudy tedy cesta nevede.

Řešením jsou reaktivní proměnné.

Můžeme totiž vytvářet programové bloky, které poslouchají změny v reaktivních proměnných.

Ukažme schéma a celé tělo funkce shinyServer hypotetické aplikace, kde uživatel odpovídá tlačítky Ano a Ne. Nyní již nezáleží na tom, zda se dříve zavolá renderText či renderUI, v obou variantách se aplikace bude chovat správně.

shinyServer(function(input, output) {
    rv <- reactiveValues()    # zatím prázdný seznam (list) reaktivních proměnných
    rv$vyhodnoceni <- "nic"   # vytvoření jedné konkrétní reaktivní proměnné, kterou použijeme pro příkaz
                              # ke kreslení obrázku
    rv$otazka <- ""       # další reaktivní proměnná pro uchování a sledování otázky
    odpoved <- ""         # správná odpověď z poslední vygenerované otázky, ta nemusí být reaktivní
    
    observe({
        input$tlacitkoAno   # posloucháme stisk tlačítka Ano
        
        if (odpoved == "ano") { # tedy: stiskli jsme Ano a správná odpověď je také "ano"
            rv$vyhodnoceni <- "ok"    # nastavení reaktivní proměnné
            vymysliOtazku()
        } else if (odpoved == "") {
            # nic nědělat, nastává po startu programu
        } else {                # tedy: stiskli jsme Ano a správná odpověď není "ano"
            rv$vyhodnoceni <- "ne"    # nastavení reaktivní proměnné
        }
    })
    
    observe({
        input$tlacitkoNe   # posloucháme stisk tlačítka Ne
        
        if (odpoved == "ne") {  # stisklo se Ne a správná odpověď je "ne"
            rv$vyhodnoceni <- "ok"
            vymysliOtazku()
        } else if (odpoved == "") {
            # nic, nastává po startu programu
        } else {                # stisklo se Ne a správná odpověď není "ne"
            rv$vyhodnoceni <- "ne"
        }
    })
    
    vymysliOtazku <- function() {
        index <- sample(1:nrow(data), size = 1)  # náhodné generování nové otázky
        odpoved <<- data$odpověď[index]          # uložení správné odpovědi do "globální" proměnné odpoved
        rv$otazka <<- data$otázka[index]
    }
        
    # A nyní konečně naše dvě renderovací funkce, jedna pro text a jedna pro obrázek
    # Zde se již nic nevyhodnocuje, tudíž není důležité, která z nich zareaguje o milisekundu rychleji.
    # Vyhodnocení proběhlo zaručeně dříve ve funkcích observe(), na jejichž důsledky zde čekáme.
    
    # renderování textu
    output$textOtazky <- renderText({
        return(rv$otazka)  # protože je rv$otazka reaktivní, tak se tato renderovací funkce
    })                     # automaticky zavolá při každé její změně
    
    # renderování obrázku
    output$obrazek <- renderUI({
        if (rv$vyhodnoceni == "ok") {     # posloucháme změny reaktivní proměnné rv$vyhodnoceni
            img(src = "dobre.png")
        } else if (rv$vyhodnoceni == "ne") {
            img(src = "spatne.png")
        } else {
            # bez obrázku: nastává po startu programu
        }
    })
    
})

Při návrhu programu je skutečně výhodné nakreslit si nejdříve na papír schéma, kdo na co reaguje, a promyslet, zda nejsou přítomny paralelní souběhy, kde by náhodné pořadí provedení mohlo způsobit problémy.

Oproti jiným programovacím jazykům je nezvyk, že renderování textu a obrázku není možné provést v rámci jedné funkce ošetřující např. událost stisku tlačítka. Pomocí funkcí observe() a reaktivních proměnných je ale možné tento problém vyřešit.

Příklad zkoušení vyjmenovaných slov

Po stisknutí tlačítka se zároveň do jednoho pole napíše vyhodnocení a současně do druhého pole napíše text nové otázky. Kvůli tomuto souběhu je nutné situaci řešit pomocí reaktivních proměnných, což naštěstí není nic složitého.

V zipu je k dispozici i alternativní řešení bez reaktivních proměnných, kde se odpověď vyhodnocuje okamžitě během psaní a tlačítko slouží k vygenerování nové otázky. Ovšem z uživatelského hlediska se nejedná o příliš příjemný zážitek, jak si můžete vyzkoušet.

shiny_slova_hure_a_lepe.zip


© 5. 1. 2016 a 24. 11. 2020 Tomáš Bořil,