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.

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

Problém je, že nikde není zaručeno, který ze dvou bloků zareaguje rychleji. Ideálně by měly reagovat současně. Ale jeden bude asi vždy trošku rychlejší. 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í funkce a 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.

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
    
    odpoved <- ""         # správná odpověď z poslední vygenerované otázky, zatím prázdná
    
    vyhodnoceni1 <- reactive({  # reaktivní funkce "vyhodnoceni1"
        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é
        } 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é
        }
    })
    
    vyhodnoceni2 <- reactive({  # reaktivní funkce "vyhodnoceni2"
        input$tlacitkoNe   # posloucháme stisk tlačítka Ne
        
        if (odpoved == "ne") {  # stisklo se Ne a správná odpověď je "ne"
            rv$vyhodnoceni <- "ok"
        } else if (odpoved == "") {
            # nic, nastává po startu programu
        } else {                # stisklo se Ne a správná odpověď není "ne"
            rv$vyhodnoceni <- "ne"
        }
    })
    
    
    # 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 v reaktivních funkcích, na které zde čekáme.
    
    # renderování textu
    output$textOtazky <- renderText({
        vyhodnoceni1()   # posloucháme volání reaktivních funkcí vyhodnoceni1() nebo vyhodnoceni2()
        vyhodnoceni2()
        
        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
        
        return(data$otázka[index])               # nastavení textu otázky
    })
    
    # 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í reaktivních funkcí a proměnných je ale možné tento problém vyřešit.


© 5. 1. 2016 Tomáš Bořil, borilt@gmail.com