Blog

Module in R

07.05.2019 10:31
von Sebastian Warnholz

Wenn unsere R-Projekte größer werden, dann müssen wir anfangen sie besser zu organisieren. Das passiert oft dadurch, dass wir den Programm-Code in verschiedene Dateien aufteilen, Funktionen verwenden und mit fortgeschrittenen Fähigkeiten sollten wir natürlich auf Pakete zurückgreifen. Es ist dabei etwas schade, dass die Basisfunktionalität in R keine Abstraktionsebene bereitstellt, die sich zwischen Funktionen und Paketen einsortieren lässt. Mit dem Paket modules kann diese Lücke gefüllt werden. Das Paket liefert lokale und geschützte Namensraumdefinitionen innerhalb oder außerhalb von R-Paketen.

In vielen Sprachen ist es ganz normal verschiedene Wege für lokale Namensräume bereitzustellen. Diese Eigenschaften finden sich wieder in Klassen, Funktionen, Paketen und manchmal auch Modulen. Python, Julia, F#, Scala und Erlang zum Beispiel sind Sprachen die explizit Module verwenden, in vielen Fällen parallel zu Klassen und Paketen.

Mehr als eine Funktion, weniger als ein Paket

Genau wie Klassen und Objekte bieten Module eine Möglichkeit, Funktionen zu einem Objekt zusammenzufassen. Sie sind in dem Sinne Objekte erster Klasse, als dass sie wie jede andere Datenstruktur in R behandelt werden können:

  • Module können überall definiert werden, auch in einem anderen Modul,
  • sie können als Input an Funktionen übergeben werden,
  • und von Funktionen als Output zurückgegeben werden.

Zusätzlich bieten sie:

  • Lokale Namensräume mit der Möglichkeit zur Deklaration von importierten und exportierten Objekten,
  • Kapselung,
  • Austauschbarkeit mit anderen Modulen, die dieselbe Schnittstelle implementiert haben.

Die wichtigste Eigenschaft dabei ist wohl die Freiheit Namen in unterschiedlichen Kontexten wiederverwenden zu können. Das kann in R ein Problem sein, weil in einer R Sitzung oder einem Paket standardmäßig immer nur mit einem Namensraum gearbeitet wird (dem .GlobalEnv oder der Umgebung in einem Paket). Module sollen aber keinesfalls Pakete ersetzen. Stattdessen können wir sie verwenden, um einen lokalen Namensraum innerhalb oder außerhalb eines Pakets zu erstellen. Auf einer Abstraktionsskala befinden sie sich zwischen Funktionen und Paketen: Module können Funktionen und manchmal auch Daten umfassen. Pakete können Funktionen, Daten und Module umfassen (außerdem Dokumentation, Tests und sie sind außerdem wichtig um Code zu teilen).

Eine einfache Moduldefinition

Schauen wir uns ein Beispiel an:

# install.packages("modules")
# vignette("modulesInR", "modules")
graphics <- modules::module({
  modules::import("ggplot2")
  barplot <- function(df) {
    ## my example barplot
    ## df (data.frame) a data frame with group and count columns
    ggplot(df, aes(group, count)) +
      geom_bar(stat = "identity") +
      labs(x = "Some group variable", y = "Absolute frequency")
  }
})
df <- data.frame(
  # Just some sample data
  group = sample(LETTERS, 5),
  count = round(runif(5, max = 100))
)
graphics$barplot(df)

Zuerst erstellen wir ein neues Modul mit modules::module(<Körper>). Im Modulkörper können beliebige Ausdrücke enthalten sein, in diesem Falle wird eine Funktion definiert. Die Funktion barplot kann dann mit graphics$barplot aufgerufen werden. Das graphics-Modul ist ganz einfach eine Liste:

class(graphics) ## [1] "module" "list" graphics ## barplot: ## function(df) ## ## my example barplot ## ## df (data.frame) a data frame with group and count columns

Es ist wichtig zu verstehen, dass Module einen lokalen Namensraum erzeugen. Hier wird die Definition von Importen und Exporten unterstützt. Bisher haben wir alle Objekte, die von ggplot2 exportiert werden, innerhalb des Moduls verfügbar gemacht. Die Kontrolle über die im Modul verfügbaren Objekte kann aber noch weiter angepasst werden:

graphics <- modules::module({ modules::import("ggplot2", "aes", "geom_bar", "ggplot", "labs") modules::export("barplot") barplot <- function(df) { ## my example barplot ## df (data.frame) a data frame with a group and count column ggplot(df, aes(group, count)) + geom_bar(stat = "identity") + labs(x = "Some group variable", y = "Absolute frequency") } })

Um ein Modul zu erstellen, muss nicht zwangsläufig die Funktion modules::module verwendet werden. Beim Programmieren ist es hilfreich die Code-Basis auf mehrere Dateien aufzuteilen. Jede Datei kann dann als Modul behandelt werden. Wenn man dieses Vorgehen mit source vergleicht, ist ein enormer Vorteil, dass jede Datei über eigene Importe verfügen kann, ohne dass Namenskonflikte entstehen. Weiterhin exportieren Module nur was notwendig ist und wir können vermeiden, den globalen Zustand der R Sitzung mit der Definition von Hilfsfunktionen zu überschütten:

 

graphics.R

modules::import("ggplot2", "aes", "geom_bar", "ggplot", "labs") modules::export("barplot") barplot <- function(df) { ## my example barplot ## df (data.frame) a data frame with a group and count column ggplot(df, aes(group, count)) + geom_bar(stat = "identity") + labs(x = "Some group variable", y = "Absolute frequency") }

Dann laden wir das Modul folgendermaßen:

graphics <- modules::use("graphics.R") graphics$barplot

Wann sind Module nützlich?

Allzu oft verbringen wir mehr Zeit damit schlechten Code zu pflegen als wir eigentlich sollten. Und während die Lösung oft lautet: Schreiben Sie ein Paket!, oder verwenden Sie einen Styleguide!, oder Struktur! - neigen wir doch häufig dazu zu antworten Ich mach das Ganze später hübsch. Erstmal muss es funktionieren. Module zielen nun darauf ab hier früh im Prozess Struktur zu schaffen. Sie bieten eine relativ einfache Lösung für ein paar Probleme, die wir oft in Projekten sehen:

Stellen Sie sich vor, Sie schreiben einen Bericht der eine Datenanalyse enthält. Natürlich wird dafür Knitr oder Sweave verwendet, denn so können Sie den Code und den Text in einer Datei behalten. Nach einer Weile wächst der Bericht über die Länge eines Blogbeitrags hinaus und ist für eine Datei zu lang. Als erfahrener R Nutzer, der Sie sind, beginnen Sie also Funktionen zu schreiben. Vielleicht haben Sie jetzt auch mehrere R-Skripte oder mehrere Rmd-Dateien, in denen Ihre Funktionsdefinitionen leben. Natürlich sind die Aufrufe von library zum Laden von Zusatzpaketen in einer Art Präambel, oder? Und die Funktionen, die dann diese Pakete nutzen, werden in Dateien aufgeteilt.

Nach einiger Zeit und mit dem der gemeinsamen Leistung mehrerer Kollegen stellen Sie fest, dass Ihr Bericht nur in einer neuen R Sitzung kompiliert werden kann. Aber nie zweimal hintereinander, da dann unterschiedliche Ergebnisse herauskommen. Einer der Autoren beschloss, Pakete in verschiedenen Skripten zu laden. Wenn Sie diese entfernen und in einer Präambel platzieren, wo sie hingehören, bekommen Sie Fehler. Wenn Sie einige Variablen entfernen, die gar nicht mehr benötigt werden, bekommen Sie auch Fehler. Sie können nicht richtig nachvollziehen weshalb, aber heute Nachmittag ist die Deadline um ein Update des Berichtes vorzulegen. Also lassen Sie lieber alles unverändert und versuchen es zum Laufen zu bekommen, irgendwie.

In solchen Situationen haben wir den Zeitpunkt verpasst die Code-Basis zu organisieren. Der Wildwuchs ist uns langsam aber sicher über den Kopf gewachsen. Es kommt öfter vor, als wir zugeben möchten. Und es ist vermeidbar. Abgesehen von allen bewährten Standards, die Sie anwenden können und sollten, bieten die Module darüber hinaus Folgendes:

  • Eine sichere(re) Möglichkeit, Dateien in einer R Sitzung auszuführen
  • Vermeiden Sie Namenskonflikte zwischen Paketen: (a) durch das Importieren einzelner Funktionen und (b) indem Sie diese Funktionen nur lokal importieren
  • Vermeiden Sie Namenskonflikte zwischen verschiedenen Teilen (möglicherweise Dateien) Ihrer Code-Basis. In vielen Fällen sind Sie auf Objekte in Ihrer Globalen Sitzung angewiesen. Mit Modulen können Sie diese lokal machen.
  • Stellen Sie nur eine Schnittstelle bereit und verstecken Sie die Implementierung

Ein Werkzeug um Wildwuchs von Code zu vermeiden

Was können wir tun um nicht in solche Situationen zu geraten? Wir müssen Grenzen schaffen. Wir müssen Abhängigkeiten definieren und auch wo diese genau benötigt werden. Außerdem muss klar sein, welche Objekte zur späteren Analyse verwendet werden dürfen. Sobald wir es schaffen zwischen den verschiedenen Teilen unseres Codes oder unseres Berichts Grenzen zu ziehen, reduzieren wir den Aufwand beim Schreiben, Pflegen oder Erweitern des Codes enorm. Wir haben die volle Kontrolle darüber, wie viel Abhängigkeit wir zwischen den einzelnen Teilen zulassen. Grenzen sind etwas Gutes, zumindest wenn es ums Programmieren geht.

Es gibt einige Werkzeuge und Konzepte die zum Aufbau von Grenzen genutzt werden können. Module können eines sein. Sie erlauben lokale Importe: ggplot2 war in dem obigen Beispiel nie im Suchpfad der R Sitzung. Module erlauben uns nur das Weiterzugeben, was auch wirklich benötigt wird und helfen uns so private Objekte unter Verschluss zu halten. Module können in ihren eigenen Dateien und Ordnern leben und dann eingeladen werden wenn sie benötigt werden, ohne irgendetwas am globalen Zustand der R Sitzung zu ändern. Module sind Container in denen wir Dinge zusammenfassen können, die miteinander in Verbindung stehen. Sie helfen uns Projekte ordentlich zu halten und eine saubere Struktur zu etablieren. Wenn ich Funktionen für ein Paket schreibe fühlt es sich für mich oft so an als würde ich alle Dinge in einem Lagerraum unterbringen. In dieser Analogie stellen Module zusätzliche Kisten dar, die zum Organisieren einladen.

Finale Anmerkungen

Nur weil wir all unseren Code in Klassendefinitionen stecken, heißt das nicht, dass wir objekt-orientiert programmieren. Nur weil wir viele Funktionen schreiben, heißt das nocht nicht, dass wir funktional programmieren. Und nur weil sich all Funktionen in einem Modul wiederfinden, lösen sich nicht einfach alle Probleme in Luft auf. Nur wenn wir die Probleme, die wir versuchen zu lösen kennen und verstehen, können wir das richtige Werkzeug auswählen. Die Ordnung in einem Projekt vereinfacht das Wachstum, die Umstrukturierung und die Arbeit im Team enorm. Ihr zukünftiges Ich wird Ihnen danken.

Zurück