R-Vektoren ohne Schleifen

18.05.2023, von Christian Glahn

Bei R-Bloggers bin ich heute auf den Beitrag von Steven Sanderson II gestossen, welcher die Funktion strftime() erklärt. Ich habe den Artikel eigentlich nur gelesen, um festzustellen, was die R-Community gerade so beschäftigt. Weil der gezeigte Code auf mich geradezu antiquiert wirkte, musste ich das Datum des Beitrags kontrollieren und habe festgestellt, dass der Beitrag erst einen Tag alt war 😱.

Hier ist der anstössige Code aus dem Originalbeitrag:

RightNow <- Sys.time()

# all of the modifiers
for (formatter in sort(c(letters, LETTERS))) {
  modifier <- paste0("%", formatter)
  print(
    paste0(
      modifier, 
      " used on: ",
      RightNow,
      " will give: ",
      strftime(RightNow, modifier)
    )
  )
}

Das verwendete Programmierparadigma ist imperativ, was nicht mehr ganz zu modernem R passt. Ich habe mich gefragt, wie aufwändig eine Lösung im funktionalen Programmierparadigma ausfallen würde, um den sprachlichen Eigenschaften von modernen R-Versionen gerechtzuwerden. Weil der Code im Originalbeitrag ohne zusätzliche Bibliotheken auskommt, musste die funktionale Lösung ebenfalls in Base R umgesetzt werden.

Hier ist meine Lösung:

c(letters, LETTERS) |> 
    sort() |>
    (\(formatter) paste0("%", formatter))() |> 
    (\(modifier, RightNow) paste0(
        modifier, 
        " used on: ", 
        RightNow, 
        " will give: ", 
        strftime(RightNow, modifier)
    ))(Sys.time())

Diese Lösung kommt ohne globale Variablen und Schleifen aus und ist ausserdem kompakter als die ursprüngliche Variante.

Meine Lösung verwendet den native Pipe-Operator (|>) sowie die Kurzform für anonyme Funktionsdeklarationen (\()). Beide Operatoren sind seit zwei Jahren bzw. seit Version 4.1.0 Teil von Base R und gehören somit zum sprachlichen Kern von R.

Der Anfang des Codes ist bis zum Aufruf von sort() identisch mit der Logik des ursprünglichen Codes: Das Ergebnis ist ein Vektor mit der sortierten Kombination der beiden Basis-Vektoren letters und LETTERS. Ab diesem Punkt spare ich mir die for-Schleife und nutze aus, dass R-Funktionen immer für alle Elemente eines Vektors als implizite Map-Funktion ausgeführt werden.

Die Code-Zeile (\(formatter) paste0("%", formatter))() erstellt eine anonyme Hilfsfunktion, welche vor jeden übergebenen Parameterwert ein Prozentzeichen einfügt, und führt diese Funktion sofort aus. Diese Hilfsfunktion wird nur deshalb benötigt, weil der Pipe-Operator die Werte ausschliesslich als ersten Parameter an die nachfolgende Funktion übergeben kann. Die Funktion paste0() kann aber die übergebenen Zeichenketten nur in dieser Reihenfolge aneinanderfügen. Die Hilfsfunktion löst dieses Problem, indem die Funktion den Datenfluss der Pipe aufnimmt und dann für die Funktion paste0() korrekt arrangiert.

Damit die Hilfsfunktion sofort ausgeführt werden kann, muss die Funktionsdeklaration geklammert werden. Dieser Klammer folgen dann die Funktionsparameter. In diesem Fall steht für die Parameterliste nur (), weil der einzige Parameter von der Pipe eine Zeile darüber weitergereicht und deshalb nicht angegeben wird.

Das letzte Fragment erstellt die Ausgabezeichenkette und ist ebenfalls als anonyme Hilfsfunktion umgesetzt. Diesmal hat die Funktion zwei Parameter, weil für die Ausgabe zwei Werte von Bedeutung sind. Der erste Parameter modifier wird von der Pipe befüllt und erhält so den Ergebnisvektor der ersten Hilfsfunktion. Der zweite Parameter RightNow nimmt das Ergebnis der Funktion Sys.time() auf. Weil diese Parameterübergabe nur eine Ausführung dieser Funktion erfordert, entfällt die globale Variable RightNow des ursprünglichen Codes und die damit verbundenen potenziellen Seiteneffekte.

Die beiden Hilfsfunktionen haben einen positiven Nebeneffekt: Die Variablennamen der ursprünglichen Lösung bleiben für die Lesbarkeit erhalten.

Weil die Lesbarkeit durch die Kurzform der Funktionsdeklaration und die vielen Zeilenumbrüche für die Ausgabe leidet, hier noch eine Variante in der funktionalen Langform:

c(letters, LETTERS) |> 
    sort() |>
    (function (formatter) paste0("%", formatter))() |> 
    (function (modifier, RightNow) paste0(
        modifier, " used on: ", RightNow, " will give: ", strftime(RightNow, modifier)
    ))(Sys.time())

Das sieht doch gleich viel besser aus. 😎

Für die Funktionsweise dieser Logik empfielt sich die Lektüre des Beitrags von Steven Sanderson II.

– EDIT 22. Mai 2023 –

Ich wurde gefragt, wie eine tidyverse-Lösung aussehen würde. Meine Lösung orientiert sich wieder an der ursprünglichen Lösung und verwendet keine zwischengeschalteten komplexen Objekte.

Die einfachste Lösung ist die naive Umsetzung meiner Schlusslösung.

c(letters, LETTERS) %>% 
    sort() %>%
    str_c("%", .) %>% 
    (function (modifier, RightNow) str_c(
        modifier, " used on: ", RightNow, " will give: ", strftime(RightNow, modifier)
    ))(Sys.time())

In der tidyverse-Philosophie ist ein explizites map() aber vorzuziehen. Also hier die purrr-Variante mit map():

library(tidyverse)

c(letters, LETTERS) %>% 
    sort() %>%
    str_c("%", .) %>% 
    map(function(modifier, RightNow) 
            str_c(modifier, " used on: ", RightNow, " will give: ", strftime(RightNow, modifier)),
        Sys.time())

Die Alternative mit einem Dataframe/tibble wäre komplexer und gibt das Ergebnis als Tabelle und nicht als Liste von Zeichenketten aus. Deshalb wäre das im Sinne der Übung keine richtige Lösung. Zur Vollständigkeit aber trotzdem:

library(tidyverse)

tibble(modifier = c(letters, LETTERS)) %>%
    arrange(modifier, .locale = "en") %>%
    mutate(
        modifier = str_c("%", modifier),
        RightNow = Sys.time(),
        result = strftime(RightNow, modifier)
    ) %>%
    rename(
        "with timestamp"= RightNow,
        "will give" = result
    )
funktionale Programmierung R Map-Reduce Style No Loops

Share