Präsentation für MHP

Shiny Projektstruktur

An die empfohlene Struktur eines R-Pakets anlehnen (vgl. Buch R Packages).

Basisdateien und Ordner eines R-Pakets:

  • Datei DESCRIPTION: Basis-Informationen zur App
  • Datei NAMESPACE: Definition des Namensraums
  • Ordner R: Quellcode, wird automatisch geladen
  • Ordner man: Hilfe/Benutzerhandbuch (automatisch mit dem Paket Roxygen2 erstellen)
  • Ordner test: Unit Tests

Ein Shiny-Projekt sollte zusätzlich mindestens folgende Dateien enthalten:

  • server.R
  • ui.R
  • global.R

Vorteile dieser Struktur

  • Shiny erkennt alle relevanten Quelldateien automatisch und lädt sie in einer bestimmten Reihenfolge, d.h. Entwickler müssen sich nicht um das Sourcen-Management kümmern.
  • In der DESCRIPTION wird die App versioniert und es erfolgt die Definition der Dependencies und der jeweiligen Minimal-Versionen der Pakete.
  • Die Struktur ist allgemeingültig, d.h. neue Entwickler finden sich sofort zurecht.
  • Die App kann optional als echtes R Paket released werden.
  • Die App kann mit verschiedenen Tools automatisch auf ihre Qualität überprüft werden, z.B. devtools::check oder devtools::test.

Beim Start der App werden die Dateien und Ordner in folgender Reihenfolge geladen:

  1. Datei global.R
  2. Alle R-Dateien im Ordner R
  3. Datei ui.R
  4. Datei server.R

Das automatische Laden des R-Ordners kann mit dem Befehl options(shiny.autoload.r = FALSE) deaktiviert werden.

Server und UI immer in zwei getrennten Dateien ablegen!

Code Style

Im Buch Advanced R werden Richtlinien für gutes Code Styling gegeben. Diese Richtlinien sind als tidyverse style guide auch online verfügbar.

Dateien benennen

Grundsätzlich müssen alle Dateien logisch benannt werden:

Dateinamen im R-Ordner

Dateinamen im R-Ordner

Dateinamen (genau wie Code) immer auf Englisch benennen! Notfalls kurz Google-Translator bemühen. Das erleichtert die Arbeit für andere Entwickler ungemein.

Bei Shiny Modulen wird die UI immer in einer vom Server getrennten Datei abgelegt. Die UI-Code-Datei endet immer mit _ui.R; die Server-Code-Datei endet immer mit _server.R.

Für allgemeine Regelen wie Schreibweise der Dateien siehe The tidyverse style guide - Files.

Syntax

Grundsätzlich gilt: Immer selbsterklärende Namen benutzen.

Hadley Wickham empfielt in seinem Buch Advanced R, Variablen und Funktionen klein zu schreiben und alle Worte mit einem Unterstrich zu trennen (siehe The tidyverse style guide - Syntax).

Beispiele:

day_one <- 1
increment_value <- function(value) {value + 1}

In anderen Programmiersprachen wie Java hat sich allerdings Camel Case durchgesetz:

Camel Case

Camel Case

Beispiele:

dayOne <- 1
incrementValue <- function(value) {value + 1}
getIncrementedValue <- function(value) {value + 1}

Meine Empfehlung ist ganz klar Camel Case!

Konstanten werden übrigens in beiden Fällen groß geschrieben und Worte werden mit Unterstrich getrennt, z.B. C_START_VALUE <- 1.

Dokumentation

Sofern selbsterklärende Namen benutzt werden, sind zusätzliche Kommentare bei ausschließlich intern verwendeten Funktionen und Variablen in der Regel nicht nötig.

Wenn etwas im Code dokumentiert werden muss, dann im Roxygen2-Stil, siehe The tidyverse style guide - Documentation.

Code Styler in RStudio

RStudio: Addins ansehen und starten

RStudio: Addins ansehen und starten

RStudio: Sytler mit einem Tastenkürzel verknüpfen

RStudio: Sytler mit einem Tastenkürzel verknüpfen

Clean Code

Wichtigste Regel: Don’t repeat yourself!

Lösung mit Hilfe von

  • Konstanten
  • Funktionen

Performance

eval(parse(text = …))

R Code sollte bis auf ganz wenige Ausnahmen nicht mit einem eval(parse(text = "myRCodeAsCharacterString") Aufruf ausgeführt werden.

Das erschwert den Navigation im Code für alle Entwickler unnötig, macht den Code undurchsichig und fehleranfällig.

Außerdem ist die Performance schlecht. Das lässt sich z.B. anhand des folgenden einfachen Beispiels zeigen:

library(rbenchmark)

evalParseDemo <- function(dynamicList) {
    eval(parse(text = "param <- paste0(sample(5), collapse = '')"))
    eval(parse(text = "value <- paste0(sample(5), collapse = '')"))
    eval(parse(text = paste0("dynamicList$p", param, " <- ", value)))
    return(dynamicList)
}

rDemo <- function(dynamicList) {
    param <- paste0(sample(5), collapse = '')
    value <- paste0(sample(5), collapse = '')
    dynamicList[[paste0("p", param)]] <- value
    return(dynamicList)
}

dynamicList1 <- list()
dynamicList2 <- list()
print(kable(benchmark(
    dynamicList1 <- rDemo(dynamicList1),
    dynamicList2 <- evalParseDemo(dynamicList2),
    replications = 100000
), format = "pipe"))
test replications elapsed relative user.self sys.self user.child sys.child
dynamicList1 <- rDemo(dynamicList1) 1e+05 1.95 1.00 1.95 0 NA NA
dynamicList2 <- evalParseDemo(dynamicList2) 1e+05 11.33 5.81 11.22 0 NA NA

eval-parse-Code ist in diesem Beispiel etwa um den Faktor 5 langsamer als die gleichen Operationen in reinem R.

Rcpp

R ist bei Schleifen extrem langsam.

library(Rcpp)

cppFunction(
"
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector longRunningFunctionCpp(int size = 1000) {
    NumericVector result = NumericVector(size, NA_REAL);
    for (int i = 0; i < size; i++) {
        result[i] = sqrt(i + pow(i, 2));
    }
    return result;
} 
"
)

longRunningFunctionRLoop <- function(size) {
    result <- c()
    for (i in 1:size) {
        result <- c(result, sqrt(i + i^2))
    }
    return(result)
}

longRunningFunctionSApply <- function(size) {
    return(sapply(1:size, function(i) {sqrt(i + i^2)}))
}

Schleife mit 100 Iterationen:

print(kable(benchmark(
    longRunningFunctionCpp(100),
    longRunningFunctionRLoop(100),
    longRunningFunctionSApply(100),
    replications = 200
), format = "pipe"))
test replications elapsed relative user.self sys.self user.child sys.child
longRunningFunctionCpp(100) 200 0.00 NA 0.00 0 NA NA
longRunningFunctionRLoop(100) 200 0.01 NA 0.02 0 NA NA
longRunningFunctionSApply(100) 200 0.03 NA 0.04 0 NA NA

Schleife mit 1.000 Iterationen:

print(kable(benchmark(
    longRunningFunctionCpp(1000),
    longRunningFunctionRLoop(1000),
    longRunningFunctionSApply(1000),
    replications = 100
), format = "pipe"))
test replications elapsed relative user.self sys.self user.child sys.child
longRunningFunctionCpp(1000) 100 0.00 NA 0.00 0 NA NA
longRunningFunctionRLoop(1000) 100 0.22 NA 0.21 0 NA NA
longRunningFunctionSApply(1000) 100 0.10 NA 0.10 0 NA NA

Schleife mit 10.000 Iterationen:

print(kable(benchmark(
    longRunningFunctionCpp(10000),
    longRunningFunctionRLoop(10000),
    longRunningFunctionSApply(10000),
    replications = 10
), format = "pipe"))
test replications elapsed relative user.self sys.self user.child sys.child
longRunningFunctionCpp(10000) 10 0.00 NA 0.00 0 NA NA
longRunningFunctionRLoop(10000) 10 1.61 NA 1.55 0 NA NA
longRunningFunctionSApply(10000) 10 0.11 NA 0.11 0 NA NA

Benchmark Tools

Siehe Efficient optimisation im Buch Efficient R programming.

Validierung / Unit Tests

Siehe z.B. Testing R Code.

Werkzeuge

Entwicklungsumgebung

Refactoring

Unbenutzte Funktionen identifizieren:

#install.packages("mvbutils")
library(mvbutils)

setwd("D:/R/external/Otartq_einzelansicht")

# start the app to load all functions into the workspace

# analyse functions
result <- foodweb(plotting = FALSE)

# get the number of times each function is called
res <- sapply(rownames(result$funmat), function(n) try(length(callers.of(n, fw = result))))

# get those functions that are never called
names(res[res==0])
 

Copyright © 2022 by Dr. Friedrich Pahlke. All rights reserved.