Spezielle selbstdefinierte Funktionen in R

Erläutert wird die Syntax, mit der man spezielle Funktionen in R selbst definieren kann: Funktionen mit dem Argument dot-dot-dot ("..."), binäre Operatoren, Funktionen höherer Ordnung und Funktionale, anonyme Funktionen, Listen von Funktionen, replacement-Funktionen, Funktionen mit unsichtbarem Rückgabewert. Um erste Funktionen in R zu implementieren reichen die Kenntnisse aus den Kapiteln Eigenschaften von Funktionen in R und Selbstdefinierte Funktionen in R (UDF = User Defined Functions); möchte man R tatsächlich als funktionale Programmiersprache nutzen, sind die hier vermittelten Kenntnisse unerlässlich.

Einordnung des Artikels

Eigentlich gehören auch Funktionsfabriken in dieses Kapitel. Da dies sehr umfangreich ist und genauere Kenntnisse über die Umgebung einer Funktion (environment) voraussetzt, ist den Funktionsfabriken ein eigenes Kapitel gewidmet: Funktionsfabriken in R.

Einführung

In Selbstdefinierte Funktionen in R (UDF = User Defined Functions) wurde die Syntax vorgestellt, wie man selbst Funktionen definieren kann; für den Großteil der Anwendungen werden die dort beschriebenen Techniken ausreichen. Je tiefer man aber in R einsteigt und im Sinne der strukturierten oder funktionalen Programmierung komplexere Aufgaben in Funktionen auslagert, wird man auch spezielle Funktionen definieren wollen und dazu weitere Konzepte benötigen. Diese werden hier vorgestellt:

  1. Das Argument dot-dot-dot "...": Das Argument ... kann in selbstdefinierten Funktionen eingesetzt werden, indem es an andere Funktionen weitergereicht wird.
  2. Binäre Operatoren: Operatoren mit zwei Eingabewerten wie + , %% und so weiter können auch selbst wie Funktionen definiert und dann wie Operatoren eingesetzt werden (wie x + y oder a %% b ).
  3. Funktion höherer Ordnung: Eine Funktion, die als Eingabewert oder als Rückgabewert eine Funktion besitzt.
  4. Funktional: Eine Funktion, die als Eingabewert eine Funktion und als Rückgabewert einen Vektor besitzt
  5. Funktionaler Parameter: Ist ein Eingabewert einer Funktion selber eine Funktion, wird der Eingabewert als funktionaler Parameter bezeichnet.
  6. Anonyme Funktion: Funktion ohne Namen, nur mit Implementierung; für den einmaligen Gebrauch bestimmt.
  7. Replacement-Funktionen: Es ist möglich, replacement-Funktionen selber zu implementieren: sie verändern ein Objekt und geben dieses unsichtbar (invisible) zurück.

Das Argument "..." (dot-dot-dot)

Es wurden schon zahlreiche Funktionen aus den Standard-Paketen vorgestellt, die das Argument ... (dot-dot-dot) nutzen. Irgendwann möchte man eigene Funktionen implementieren, die dies anbieten. Dazu werden die Eingaben in ... an eine andere Funktion weitergereicht, die ... verarbeiten kann.

Im Folgenden wird dazu eine Variante der Funktion paste0() implementiert; damit sie besser verständlich ist, wird zunächst die Funktion paste0() kurz erklärt, die ebenfalls ... als Eingabewert besitzt.

Eigenschaften der Funktion paste0()

Die Funktion paste0() besitzt zwei Eingabewerte:

paste0(..., collapse = NULL)

Das folgende Beispiel übergibt in ... die Vektoren (1:3) und (4:6) und verknüpft die Komponenten mit dem Trennungszeichen " | " :

paste0((1:3), (4:6), collapse = " | ")
# [1] "14 | 25 | 36"

Die folgenden Anwendungen zeigen, dass man bei paste0() einige Sonderfälle beachten muss:

# recycling-Mechanismus:

paste0((1:3), (4:5), collapse = " | ")
# [1] "14 | 25 | 34"

# Eingabe nur eines Vektors:

paste0((1:3), collapse = " | ")
# [1] "1 | 2 | 3"

paste0(c("x", "y"), collapse = " | ")
# [1] "x | y"

# Eingabe einzelner Zahlen oder Zeichen:

paste0(1, 2, 3, collapse = " | ")
# [1] "123"

paste0("x", "y", collapse = " | ")
# [1] "xy"

Liest man die Dokumentation zu paste0(), wird man in den letzten beiden Beispielen (Zeile 16 und 19) vielleicht die Ausgaben "1 | 2 | 3" oder "x | y" erwarten.

Implementierung einer Funktion paste1()

Das folgende Beispiel definiert eine Funktion paste1(), die genau die Erwartung aus Zeile 16 und 19 des letzten Skriptes erfüllt – dies ist ein häufig benötigter Spezialfall von paste0(): Die in ... eingegebenen Zeichenketten sollen zu einer einzigen Zeichenkette zusammengefasst werden, aber die Bestandteile sollen mit einem Separator sep getrennt werden. Die Eingabewerte der Funktion lauten somit: paste1(..., sep = " ") ; als default-Wert für sep wird das Leerzeichen gesetzt. Und der Aufruf paste1("x", "y", sep = " | ") sollte die Zeichenkette "x | y" erzeugen.

Eine mögliche Implementierung lautet:

paste1 <- function(..., sep = " "){
  args <- list(...) 
  str(args)
  
  nargs <- length(args)
  
  if(nargs == 0L) return("")
  
  return( paste0(args, collapse = sep) )
}

paste1("x", "y", sep = " | ")
# List of 2
# $ : chr "x"
# $ : chr "y"
# [1] "x | y"

paste1(1, 2, 3, sep = " | ")
# List of 3
# $ : num 1
# $ : num 2
# $ : num 3
# [1] "1 | 2 | 3"

# Eingabe eines Vektors in ...:
paste1((1:3), sep = " | ")
# List of 1
# $ : int [1:3] 1 2 3
# [1] "1:3"

Zeile 2: Die Eingaben aus dem Argument ... werden an die Funktion list() übergeben, die alle Eingabewerte aufsammelt und sie in einer Liste verpackt. Damit hat man sie in eine leicht zugängliche Form gebracht und kann später auf sie zugreifen. Und an den Beispielen oben zu paste0() hat man gesehen, dass nicht einzelne Objekte an paste0() übergeben werden dürfen, wenn das Trennungszeichen dazwischen eingefügt werden soll.

Zeile 3: Die Ausgabe der Struktur dient nur dazu, die Arbeitsweise von Zeile 2 zu "beobachten".

Zeile 5: Die Anweisung aus Zeile 2 ermöglicht zum Beispiel die Anzahl der Argumente in ... festzustellen.

Zeile 7: Falls kein Argument in ... eingegeben wurde, ist der Rückgabewert gleich "" (leere Zeichenkette).

Zeile 9: Andernfalls wird die Funktion paste0() eingesetzt, um den Rückgabewert zu erzeugen.

Zeile 12: Anwendungsbeispiel mit drei Zeilen aus der Ausgabe der Struktur und dem Rückgabewert "x | y" von paste1() (Zeile 16).

Zeile 18: Weiteres Anwendungsbeispiel, das zeigt, dass die Eingabewerte in ... keine Zeichen sein müssen, die Umwandlung wird von Zahlen in Zeichen wird von paste0() vorgenommen.

Zeile 26: Das Beispiel zeigt, dass paste1() nicht das gewünschte Verhalten zeigt, wenn statt einzelner Objekte ein Vektor eingegeben wird.

Man hätte natürlich die Argumente ... direkt an paste0() weiterreichen können. Aber diese Implementierung ist nur auf den ersten Blick identisch zur Implementierung oben.

paste1 <- function(..., sep = " "){
  return( paste0(..., collapse = sep) )
}

paste1("x", "y", sep = " | ")
# [1] "xy"

paste1(c("x", "y"), sep = " | ")
# [1] "x | y"

Da die Funktion paste0() mit einzelnen Zeichen anders verfährt als hier gewünscht, ist diese Implementierung nicht gleichwertig zur oben gezeigten. Aber zumindest erkennt man wiederum das Vorgehen, wie das Argument ... weitergereicht wird.

Man kann die Funktion paste1() natürlich auch mit nur einer Zeile Quelltext implementieren (ohne Ausgabe der Struktur), indem man die Bildung der Liste und den Aufruf von paste0() verkettet; diese Implementierung ist dann gleichwertig zur oben gezeigten Implementierung:

paste1 <- function(..., sep = " "){
  return( paste0(list(...) , collapse = sep) )
}

Da es nicht zwingend vorgeschrieben ist, beim Aufruf einer Funktion die Namen der Argumente anzugeben, ist es vielleicht nicht klar, wie die Zuordnung der Eingabewerte geregelt ist, wenn eine Funktion ... als Eingabewert besitzt. Das folgende Beispiel klärt dies. Es wird eine Funktion f() definiert, die x, y und ... als Eingabewerte besitzt. Der Rückgabewert wird aus x und y berechnet, die Argumente aus ... werden auf der Konsole ausgegeben:

f <- function(x, y, ...){
  cat("...-Argumente: ", ..., "\n")
  return(x + y)
}

f(x = 1, y = 2, 3, 4)
# ...-Argumente:  3 4 
# [1] 3

Zeile 2: Die Eingabewerte von f() aus dem dritten Argument ... werden an die Funktion cat() weitergereicht.

Zeile 6: Beim Aufruf von f() mit Bezeichnung der Argumente ist eindeutig, welche Argumente an cat() weitergegeben werden und wie der Rückgabewert zu berechnen ist.

In den folgenden Aufrufen ist dies nicht so klar:

f(1, 2, 3, 4)
# ...-Argumente:  3 4 
# [1] 3

f(3, x = 1, y = 2, 4)
# ...-Argumente:  3 4 
# [1] 3

f(3, x = 1, 2, 4)
# ...-Argumente:  2 4 
# [1] 4

Zeile 1: Der Aufruf ist identisch zu dem Aufruf oben. Aber wie erfolgt die Zuordnung? Da in der Argument-Liste von f() zuerst x und y kommen und dann erst ... , werden die ersten beiden aufrufenden Argumente als x und y interpretiert und alle anderen Argumente gehören zu ... .

Zeile 5: Sind die Namen der aufrufenden Argumente angegeben, ist ihre Position unerheblich. Daher wird die namenlose 3 – obwohl sie an erster Stelle in f() steht – zu ... gerechnet. Wieder werden 3 und 4 auf der Konsole ausgegeben.

Zeile 9: Hier ist schwer einzusehen, wie die Zuordnung stattfindet. Da ... an letzter Stelle in f() steht, werden die ersten beiden Argumente als x und y interpretiert. Da für das zweite Argument der Name x gesetzt ist, muss das erste Argument y sein.

Das letzte Beispiel soll verdeutlichen, wie wichtig und hilfreich für den Leser eines Programmes es ist, die Argument-Namen anzugeben.

Das dot-dot-dot-Argument bei generisch implementierten Funktionen

Eine weitere Verwendung des Argumentes ... kann nur angedeutet werden: Es ist sehr hilfreich, wenn man selbst generisch implementierte Funktion definieren möchte. Damit ist gemeint, dass man mehrere Varianten einer Funktion definiert, die je nachdem mit welchem Objekt x sie aufgerufen werden anders implementiert sind. Genauer muss man sagen: welche der Implementierungen ausgewählt wird, hängt davon ab, zu welcher Klasse das Objekt x gehört. Wenn dem Objekt keine Implementierung zugeordnet werden kann, wird eine default-Implementierung aufgerufen.

Da man für unterschiedliche Varianten der Funktion unterschiedliche formale Argumente anbieten möchte, kann man das Argument ... einsetzen, um Argumente weiterzureichen. Als Beispiel betrachte man dazu etwa die Funktionen plot() und plot.default() im Paket graphics. Die Funktion plot() besitzt nur drei Eingabewerte, nämlich x, y und ... (siehe Zeile 1):

plot(x, y, ...)

plot(x, y = NULL, type = "p", xlim = NULL, ylim = NULL,
log = "", main = NULL, sub = NULL, xlab = NULL, ylab = NULL,
ann = par("ann"), axes = TRUE, frame.plot = axes,
panel.first = NULL, panel.last = NULL, asp = NA, ...)

Dagegen hat die default-Implementierung von plot() eine fast unüberschaubare Anzahl von weiteren Eingabewerten, mit denen sich eine Graphik konfigurieren lässt (siehe Zeile 3 bis 6). Mit dem Argument ... in plot(x, y, ...) kann man diese Vielzahl ansprechen, ohne sie in der Argument-Liste von plot(x, y, ...) ausdrücklich aufzuführen.

Das Thema soll hier nicht weiter verfolgt werden; um selber generisch implementierte Funktionen anzubieten, sind Kenntnisse zur objekt-orientierten Programmierung nötig.

Selbstdefinierte binäre Operatoren

Eine Operation wie x + y könnte man natürlich auch durch eine Funktion ersetzen, etwa:

add <- function(x, y) return(x + y)

In vielen Fällen sorgen derartige binäre Operatoren für leichter lesbare Quelltexte als die entsprechenden Funktionen – dies gilt auf jeden Fall, wenn ein treffendes Symbol gefunden werden kann. (Mit der Bezeichnung binärer Operator ist gemeint, dass zwei Objekte miteinander verknüpft werden.)

Als einfaches Beispiel soll die Addition der Katheten im rechtwinkligen Dreieck durch eine binäre Operation definiert werden. Damit ist folgendes gemeint: Hat ein rechtwinkliges Dreieck die Kathetenlängen a und b, so berechnet sich die Länge der Hypothenuse c durch:

a2 + b2 = c2,

wobei man noch die Wurzel ziehen muss.

Die "Addition" von a und b zu c soll durch eine binäre Operation ausgedrückt werden: c <- a %++% b .

In R kann man eine binäre Operation wie eine Funktion definieren, das folgende Beispiel zeigt, wie man den Operator %++% als Funktion mit zwei Eingabewerten implementiert:

"%++%" <- function(a, b) return(sqrt(a*a + b*b)) 

3 %++% 4
# [1] 5

3 %++% 4 %++% 12
# [1] 13

5 * 3 %++% 4
# [1] 25

3 %++% c(4, 5)
# [1] 5.000000 5.830952

Man muss dazu lediglich einige Bedingungen beachten (siehe Zeile 1):

Zeile 3: In Berechnungen kann der Operator jetzt wie jeder andere binäre Operator eingesetzt werden.

Zeile 6: Da die Operation %++% assoziativ ist, muss man bei der "Addition" von drei Summanden keine Klammern setzen. Umgekehrt sollte man binäre Operatoren nur bei assoziativen Operationen einsetzen.

Zeile 9: Welche Operation Vorrang hat, wenn in einem Ausdruck verschiedene binäre Operationen eingesetzt werden, kann leicht zu Verwirrung führen: zur besseren Nachvollziehbarkeit sollte man Klammern einsetzen. Im Beispiel aus Zeile 9 ist zu sehen, dass die selbstdefinierte Operation gegenüber der Multiplikation Vorrang hat; man hätte besser schreiben sollen: 5 * (3 %++% 4) .

Zeile 12: Die binäre Operation ist selbstverständlich vektorisiert. Man muss dazu nur die Implementierung aus Zeile 1 betrachten: für a und b können Vektoren eingesetzt werden und der Rückgabewert wird sinnvoll berechnet. Möchte man umgekehrt ausschließen, dass Vektoren verknüpft werden, müsste man in der Implementierung mit stopifnot() arbeiten.

Aufgabe:

Implementieren Sie eine binäre Operation, die nicht assoziativ ist und stellen Sie damit fest, ob ein Ausdruck wie

3 %++% 4 %++% 12

Funktion als Eingabewert einer Funktion

Funktionen höherer Ordnung und Funktionale

In mehreren der bisherigen Kapitel wurde die Funktion outer() verwendet, die folgende Besonderheit besitzt: Einer ihrer Eingabewerte ist selber eine Funktion (als default-Wert wird die Multiplikation verwendet):

outer(X, Y, FUN = "*", ...)

Funktionen, die als Eingabewerte Funktionen zulassen und eventuell als Rückgabewert wiederum eine Funktion erzeugen, nennt man Funktionen höherer Ordnung. Ein Eingabewert wie FUN in outer() wird als funktionaler Parameter bezeichnet. Als Rückgabewert erzeugt outer() ein Feld (array). Im Spezialfall, dass der Rückgabewert ein Vektor ist, nennt man eine Funktion höherer Ordnung auch Funktional.

Mit der Familie der apply()-Funktionen werden später weitere Funktionen höherer Ordnung vorgestellt. Hat man mit derartigen Funktionen öfter gearbeitet, wird man sie auch selber implementieren wollen. Hier wird kurz vorgestellt, welche Besonderheiten dabei zu beachten sind.

Dazu gibt es kaum etwas zu erklären:

Die Neuerung, die dadurch entsteht, dass man Funktionen höherer Ordnung zulässt, bezieht sich eher auf die Vorgehensweise beim Programmieren: Bisher wurden Funktionen im Sinne der strukturierten Programmierung eingesetzt: Funktionen fassen Anweisungen zusammen, so dass sie

Der Einsatz von Funktionen höherer Ordnung ist typisch für die sogenannte funktionale Programmierung:

Beispiele für Funktionale: Würfelspiel

An einem konkreten Beispiel sollen sowohl die Syntax als auch der Einsatz eines Funktionals veranschaulicht werden.

Dazu wird ein Würfelspiel zwischen zwei Spielern A und B betrachtet, bei dem jeder Spieler zweimal würfelt und das nach verschiedenen Regeln gespielt werden kann. Die Spielregeln werden in einer Funktion rule() festgelegt.

Weiter soll ein Funktional result() definiert werden:

result(a, b, rule)

Es erhält als Eingabewerte:

  1. Die von A gewürfelten Zahlen a (als Vektor der Länge 2).
  2. Die von B gewürfelten Zahlen b (als Vektor der Länge 2).
  3. Die Spielregeln in Form der Funktion rule() (siehe unten).

Der Rückgabewert von result() ist gleich 1, 0 oder -1, je nachdem ob A gewinnt, das Spiel unentschieden endet oder B gewinnt.

Für die Spielregeln sollen 2 Varianten existieren (man kann später problemlos weitere Regeln implementieren und einsetzen):

1. Variante: höhere Augensumme gewinnt

Es gewinnt derjenige Spieler, der bei seinen Würfen die höhere Augensumme erzielt. Ist die Augensumme identisch, endet das Spiel unentschieden. Die Funktion, die entscheidet, ob B gewinnt, wird als less.sum(a, b) bezeichnet. Sie hat die beiden Vektoren a und b als Eingabewerte und sie liefert TRUE, wenn b die höhere Augensumme hat.

2. Variante: beide Würfe mit höherer Augenzahl

In der zweiten Variante gewinnt derjenige Spieler, der in beiden Würfen die höhere Augenzahl erzielt. Jetzt gibt es viele Kombinationen von Ergebnissen, die unentschieden enden, zum Beispiel a = (3, 5) und b (4, 2). Die entsprechende Funktion zur Entscheidung, ob B gewinnt, wird als less.all(a, b) bezeichnet.

Die Implementierung der Spielregeln könnte wie folgt aussehen:

less.sum <- function(a, b){
  return( sum(a) < sum(b) )
}

less.all <- function(a, b){
  return( all(a < b) )
}

Da die Funktionen einen logischen Wert zurückgeben, ist folgende Besonderheit zu beachten: Die Funktionen erlauben es nur festzustellen, ob B gewinnt (der Rückgabewert ist gleich TRUE). Ist der Rückgabewert gleich FALSE, sind noch die beiden Fälle möglich: A gewinnt und unentschieden.

Die Implementierung des Funktionals result() muss diese Besonderheit beachten:

# Rückgabewert:
#  1: A gewinnt
# -1: B gewinnt
#  0: unentschieden

result <- function(a, b, rule){
  if(rule(a, b)){
    return(-1)
  } else {
    if(rule(b, a)){
      return(1)
    } else return(0)
  }
}

Zeile 7: Liefert die Spielregel TRUE, gewinnt B (denn B hat die größere Augensumme oder alle seine gewürfelten Zahlen sind echt größer).

Zeile 9: Andernfalls gewinnt entweder A oder das Spiel ist unentschieden.

Zeile 10: Indem man die Eingabewerte in rule() vertauscht, kann man dies weiter untersuchen. Liefert jetzt die Spielregel TRUE, gewinnt A (Zeile 11), andernfalls unentschieden (Zeile 12).

Das folgende Skript zeigt einige Aufrufe der Funktion result():

a = c(1, 3)
b <- c(2, 3)

result(a, b, rule = less.sum)    # -1
result(b, a, rule = less.sum)    # 1
result(a, a, rule = less.sum)    # 0

result(a, b, rule = less.all)    # 0

Aufgabe:

1. Drei Würfe:

Diskutieren Sie: Wie muss man die Implementierung der Spielregeln und der Funktion result() abändern, wenn dreimal gewürfelt wird (und die Spielregeln entsprechend angepasst werden).

2. Neue Spielregeln bei 2 Würfen:

Implementieren Sie folgende Spielregeln:

♦ ♦ ♦ ♦

Weiter soll ein Funktional outcomes() implementiert werden, das zu einem gegebenen Würfel-Ergebnis von A diejenigen Ergebnisse berechnet, bei denen das Spiel

Das Ergebnis des Würfelns wird in zwei Vektoren a und b der Länge 2 festgehalten: die erste Komponente von a ist das Ergebnis des ersten Wurfes von A, die zweite Komponente von a das Ergebnis des zweiten Wurfes von A und so weiter.

Die Funktion outcomes() erhält dann als Eingabewerte:

  1. Einen Vektor der Länge 2 für die Ergebnisse von A.
  2. Die Funktion rule(), die die Spielregeln festlegt.

Allerdings enthält die Implementierung von outcomes() die Funktion apply(), die noch nicht erklärt wurde; dies wird in einem eigenen Kapitel geschehen). Alles vorerst Wissenswerte über apply() wird unten kurz erklärt.

Zur Vorbereitung zeigt das folgende Skript wie man leicht alle möglichen Kombinationen beim zweimaligen Würfeln als Dataframe darstellen kann:

v <- (1:6)
results <- expand.grid(b1 = v, b2 = v)

results
#    b1 b2
# 1   1  1
# 2   2  1
# 3   3  1
# 4   4  1
# 5   5  1
# 6   6  1
# 7   1  2
# 8   2  2
# 9   3  2
# 10  4  2
# 11  5  2
# 12  6  2
# 13  1  3
# 14  2  3
# 15  3  3
# 16  4  3
# 17  5  3
# 18  6  3
# 19  1  4
# 20  2  4
# 21  3  4
# 22  4  4
# 23  5  4
# 24  6  4
# 25  1  5
# 26  2  5
# 27  3  5
# 28  4  5
# 29  5  5
# 30  6  5
# 31  1  6
# 32  2  6
# 33  3  6
# 34  4  6
# 35  5  6
# 36  6  6

Bei der Implementierung von outcomes() geht man jetzt folgendermaßen vor:

outcomes <- function(a, rule){
  v <- (1:6)
  results <- expand.grid(b1 = v, b2 = v)
  
  # B gewinnt:
  idx.b <- apply(X = as.matrix(results), MARGIN = 1, FUN = rule, a = a)
  res.b <- results[idx.b, ]

  # A gewinnt:
  # Beachte: aufgerufen wird rule(b, a), da b die Zeile der Matrix ist
  idx.a <- apply(X = as.matrix(results), MARGIN = 1, FUN = rule, b = a)
  res.a <- results[idx.a, ]
 
  # unentschieden:
  idx.0 <- (!idx.a) & (!idx.b)
  res.0 <- results[idx.0, ]

  return(list(LESS = res.a, GREATER = res.b, EQUAL = res.0))
}

# Test:
outcomes(a = c(4, 2), rule = less.sum)
# $LESS
#    b1 b2
# 1   1  1
# 2   2  1
# 3   3  1
# 4   4  1
# 7   1  2
# 8   2  2
# 9   3  2
# 13  1  3
# 14  2  3
# 19  1  4
# 
# $GREATER
#    b1 b2
# 6   6  1
# 11  5  2
# 12  6  2
# 16  4  3
# 17  5  3
# 18  6  3
# 21  3  4
# 22  4  4
# 23  5  4
# 24  6  4
# 26  2  5
# 27  3  5
# 28  4  5
# 29  5  5
# 30  6  5
# 31  1  6
# 32  2  6
# 33  3  6
# 34  4  6
# 35  5  6
# 36  6  6
# 
# $EQUAL
#    b1 b2
# 5   5  1
# 10  4  2
# 15  3  3
# 20  2  4
# 25  1  5

outcomes(a = c(4, 2), rule = less.all)
# $LESS
#   b1 b2
# 1  1  1
# 2  2  1
# 3  3  1
# 
# $GREATER
#    b1 b2
# 17  5  3
# 18  6  3
# 23  5  4
# 24  6  4
# 29  5  5
# 30  6  5
# 35  5  6
# 36  6  6
# 
# $EQUAL
#    b1 b2
# 4   4  1
# 5   5  1
# 6   6  1
# 7   1  2
# 8   2  2
# 9   3  2
# 10  4  2
# 11  5  2
# 12  6  2
# 13  1  3
# 14  2  3
# 15  3  3
# 16  4  3
# 19  1  4
# 20  2  4
# 21  3  4
# 22  4  4
# 25  1  5
# 26  2  5
# 27  3  5
# 28  4  5
# 31  1  6
# 32  2  6
# 33  3  6
# 34  4  6

Die entscheidenden Befehle sind in Zeile 6 und 11: Dort wird jeweils die Funktion apply() aufgerufen; dabei handelt es sich ebenfalls um ein Funktional.

Die Funktion apply() besitzt folgende Eingabewerte:

apply(X, MARGIN, FUN, ...)

Dabei ist X eine Matrix, MARGIN gibt an, ob die Matrix zeilen- oder spaltenweise verarbeitet wird, FUN ist die Funktion, die auf jeweils eine Zeile (oder Spalte) der Matrix angewendet wird und hinter ... verbergen sich weitere Argumente, die an die Funktion FUN übergeben werden.

Salopp kann man sich die Arbeitsweise von apply() folgendermaßen vorstellen:

In Zeile 11 wird dazu b = a als weiteres Argument übergeben, da jetzt die Rollen von a und b vertauscht werden müssen.

Zeile 15: Wenn rule(a, b) und zugleich rule(b, a den Wert FALSE liefern, endet das Spiel unentschieden.

In Zeile 18 werden die Teil-Dataframes in eine Liste verpackt und zurückgegeben.

Zeile 22: Test des Funktionals mit a = (4, 2). Man erkennt nacheinander diejenigen Kombinationen mit

Zeile 68: Der entsprechende Test mit rule = less.all .

Hinweis: In Die Teilbarkeitsrelation als Ordnungsrelation wird die hier als Spielregel verwendete Funktion less.all() als Ordnungsrelation aufgefasst und zum Erstellen eines Hasse-Diagramms verwendet. Auch dort werden typische Methoden der funktionalen Programmierung eingesetzt. Man beachte, dass dort die Spielregeln leicht anders formuliert sind.

Aufgabe:

Versuchen Sie die Ausgaben obiger Tests nachzuvollziehen.

Testen Sie den Aufruf von outcomes() mit den entsprechenden Würfelspielen, bei denen dreimal gewürfelt wird. An welchen Stellen muss man dazu die Quelltexte abändern? Kann man die Anzahl der Würfe geeignet als Eingabewert setzen? In welchen Funktionen ist dies nötig?

♦ ♦ ♦ ♦ ♦

Das folgende Skript erzeugt eine geeignete Ausgabe der Ergebnisse beim zweimaligen Würfeln und damit sollten – zumindest für die Spielregel gemäß rule = less.sum – die Tests leichter nachvollziehbar sein.

v <- (1:6)

results <- rep("-----", times = 11*11)
m <- matrix(data = results, nrow = 11)

for(i in v){
  for(j in v){
    m[i + j - 1, i - j + 6] <- paste0(c(i, j), collapse = " | ")
  }
}

print(m, quote = FALSE, print.gap = 2)
#        [,1]   [,2]   [,3]   [,4]   [,5]   [,6]   [,7]   [,8]   [,9]   [,10]  [,11]
#  [1,]  -----  -----  -----  -----  -----  1 | 1  -----  -----  -----  -----  -----
#  [2,]  -----  -----  -----  -----  1 | 2  -----  2 | 1  -----  -----  -----  -----
#  [3,]  -----  -----  -----  1 | 3  -----  2 | 2  -----  3 | 1  -----  -----  -----
#  [4,]  -----  -----  1 | 4  -----  2 | 3  -----  3 | 2  -----  4 | 1  -----  -----
#  [5,]  -----  1 | 5  -----  2 | 4  -----  3 | 3  -----  4 | 2  -----  5 | 1  -----
#  [6,]  1 | 6  -----  2 | 5  -----  3 | 4  -----  4 | 3  -----  5 | 2  -----  6 | 1
#  [7,]  -----  2 | 6  -----  3 | 5  -----  4 | 4  -----  5 | 3  -----  6 | 2  -----
#  [8,]  -----  -----  3 | 6  -----  4 | 5  -----  5 | 4  -----  6 | 3  -----  -----
#  [9,]  -----  -----  -----  4 | 6  -----  5 | 5  -----  6 | 4  -----  -----  -----
# [10,]  -----  -----  -----  -----  5 | 6  -----  6 | 5  -----  -----  -----  -----
# [11,]  -----  -----  -----  -----  -----  6 | 6  -----  -----  -----  -----  -----

Die Ausgabe kann auch leicht in eine Tabelle verwandelt werden:

1 | 1
1 | 2 2 | 1
1 | 3 2 | 2 3 | 1
1 | 4 2 | 3 3 | 2 4 | 1
1 | 5 2 | 4 3 | 3 4 | 2 5 | 1
1 | 6 2 | 5 3 | 4 4 | 3 5 | 2 6 | 1
2 | 6 3 | 5 4 | 4 5 | 3 6 | 2
3 | 6 4 | 5 5 | 4 6 | 3
4 | 6 5 | 5 6 | 4
5 | 6 6 | 5
6 | 6

Anonyme Funktionen

Anonyme Funktionen wurden bereits in Eigenschaften von Funktionen in R und Faktoren in R: Anwendungen verwendet und kurz erklärt. Mit den Kenntnissen, wie man selber Funktionen implementiert, kann man ihre Rolle in der strukturierten oder funktionalen Programmierung besser verstehen.

Im Sinne der strukturierten Programmierung kann eine Funktion beliebig wiederverwendet werden; ist sie einmal implementiert, kann sie jederzeit durch ihren Namen aufgerufen werden (der Fall, dass sich die Funktion in einer anderer Datei befindet, soll jetzt noch ausgeschlossen werden). Manchmal werden Funktionen nur einmal benötigt, so dass ihr Name irrelevant ist. Sie können dann sofort mit Hilfe von function() definiert werden und sind nur unmittelbar in der aufrufenden Funktion verfügbar. Derartige Funktionen bezeichnet man als anonyme Funktionen (manchmal auch als disposable function wegen ihrer unmittelbaren Verfügbarkeit). Sie sind ein wichtiges Hilfsmittel der funktionalen Programmierung, da sie meist eingesetzt werden, wenn eine "kleine" Funktion Eingabewert einer anderen Funktion ist.

Das folgende Beispiel verwendet das Funktional result() aus dem letzten Abschnitt, jetzt wird aber die Spielregel als anonyme Funktion implementiert:

a = c(1, 3)
b <- c(2, 3)

result( a, b, rule = function(a, b){ return( sum(a) < sum(b) ) } )    # -1
result( b, a, rule = function(a, b){ return( sum(a) < sum(b) ) } )    # 1
result( a, a, rule = function(a, b){ return( sum(a) < sum(b) ) } )    # 0

Man erkennt: Alles, das eigentlich auf der rechten Seite der Definition der Funktion less.sum <- steht, wird jetzt direkt an das Argument rule der Funktion result() übergeben.

Als Faustregeln, wann man anonyme Funktionen einsetzen soll, gelten:

Man erkennt auch den Nachteil der anonymen Funktion: Wird sie mehrfach eingesetzt, muss man die Quelltexte wiederholen; bei einer Änderung der Implementierung der Spielregel können sich leicht Fehler einschleichen.

Listen von Funktionen

Da Funktionen wie Objekte behandelt werden können, ist es auch möglich sie zu einer Liste zusammenzufassen; zu einem Vektor können sie nicht zusammengefasst werden, da dies der Modus function nicht zulässt.

Das folgende Beispiel soll zeigen, wie man dies geschickt einsetzen kann. Allerdings benötigt man zur Realisierung die Funktion lapply(), die bisher noch nicht erklärt wurde. Kurz: Sie sorgt dafür, dass eine Funktion auf alle Komponenten einer Liste angewendet wird; sie ersetzt somit eine Iteration über die Komponenten.

Eine statistische Auswertung einer Zahlenfolge könnte wie folgt aussehen:

Eine simple Realisierung zeigt folgendes Skript, dabei enthält die Auswertung die Berechnung des Mittelwertes, der Varianz und der Standardabweichung.

analyse <- function(x){
  return( list(E = mean(x), VAR = var(x), SD = sd(x)) )
}

# Test:
v <- (1:9)

a <- analyse(x = v)
a
# $E
# [1] 5
# 
# $VAR
# [1] 7.5
# 
# $SD
# [1] 2.738613

Die drei Anforderungen an die statistische Auswertung geschehen alle in Zeile 2.

Man kann die Reihenfolge auch umdrehen:

Das folgende Skript zeigt diese Realisierung:

analysingFunctions <- list(MEAN = mean, VAR = var, SD = sd)

# Test:
v <- (1:9)

lapply(X = analysingFunctions, FUN = function(f){f(v)})
# $MEAN
# [1] 5
# 
# $VAR
# [1] 7.5
# 
# $SD
# [1] 2.738613

Hier wird innerhalb der Funktion lapply() eine anonyme Funktion eingesetzt, die dem Argument FUN übergeben wird: FUN erhält eine Funktion f(), nämlich nacheinander die Komponenten der Funktions-Liste, und wendet diese auf den Vektor v an.

Replacement-Funktionen

Was ist eine replacement-Funktion?

In früheren Kapiteln wurden schon mehrere sogenannte replacement-Funktionen besprochen, zum Beispiel die Funktion names(), die in zwei Versionen existiert:

  1. Als Funktion zur Abfrage des Attributes names: Wird sie etwa für einen Vektor x aufgerufen, ist der Rückgabewert ein character-Vektor mit den Namen der Komponenten (sind die Namen nicht gesetzt, erhält man NULL).
  2. Als replacement-Funktion, um die Namen der Komponenten zu setzen.

Das folgende Beispiel initialisiert einen Vektor (ohne Namen), fragt das Attribut names ab, setzt die Namen und fragt sie wiederum ab:

v <- c(1, 1, 0)
names(v)
# NULL

names(v) <- c("x", "y", "z")

v
# x y z 
# 1 1 0 

names(v)
# [1] "x" "y" "z"

Wie man das Attribut names setzt, wurde in Vektoren in R: der Datentyp vector ausführlich besprochen, daher sollte das Skript eigentlich keine Fragen aufwerfen.

Aber: Wie kann man eine replacement-Funktion wie in Zeile 5 selbst implementieren? Ist das überhaupt möglich oder kann man nur die in R vorbereiteten replacement-Funktionen einsetzen?

Die Funktionen zun Abfragen beziehungsweise zum Setzen der Namen eines Objektes x sind:

names(x)
names(x) <- value

Dabei ist x ein Objekt und value der character-Vektor der neuen Namen. Funktionen wie names() sollten inzwischen verständlich sein: Sie besitzt ein Objekt x als Eingabewert, eine spezielle Eigenschaft des Objektes wird zurückgegeben.

Aber ist auch die Arbeitsweise der replacement-Version von names() verständlich? Sie greift auf ein Objekt x zu und verändert eine Eigenschaft von x, indem sie es gleich value setzt. Wie soll man das selbst implementieren?

Man muss nur x und value als Eingabewerte betrachten und das veränderte Objekt als Rückgabewert. Der folgende Unterabschnitt zeigt die Syntax einer selbstdefinierten replacement-Funktion.

Die Syntax einer replacement-Funktion

Das folgende Beispiel implementiert eine replacement-Funktion, die aus einem Vektor x sämtliche NA-Werte durch einen Wert value ersetzt:

"replace.NA<-" <- function(x, value){
  x[which(is.na(x))] <- value
  return(x)
}

v <- c(1, NA, 2, NA)
v
# [1]  1 NA  2 NA

replace.NA(x = v) <- 0

v
# [1] 1 0 2 0

Zeile 1: Der Name der replacement-Funktion kann wieder frei gewählt werden – hier wird replace.NA() gewählt. Damit eine replacement-Funktion gebildet wird, muss der Name mit dem Zuordnungsoperator <- abschließen und dies muss insgesamt in Anführungsstriche geschrieben werden.

Wie üblich wird die Funktion function() zur Definition der Funktion verwendet; sie erhält die beiden Eingabewerte x und value. Dabei ist natürlich x das Objekt an dem die Veränderung vorgenommen werden soll und value der Wert, der neu gesetzt werden soll.

Wie gewohnt steht die Implementierung der Funktion in geschweiften Klammern und in der letzten Anweisung der Implementierung wird das veränderte Objekt x zurückgegeben (Zeile 3).

Zeile 2: Die Funktion which() wird eingesetzt, um die Indizes der NA-Werte zu bestimmen. Die NA-Werte werden durch value ersetzt.

Zeile 6: Test: Es wird ein Vektor v initialisiert, der zwei NA-Werte enthält.

Zeile 10: Die replacement-Funktion wird aufgerufen und die NA-Werte werden gleich 0 gesetzt. Bemerkenswert ist, dass der Aufruf der Funktion replace.NA(x = v) <- 0 zu keiner Konsolen-Ausgabe führt; und dies, obwohl der Rückgabewert gleich x ist, der bei "üblichen" Funktionen ausgegeben wird (mehr dazu im nächsten Abschnitt).

Zeile 12: Die Ausgabe bestätigt, das der Vektor v wie gewünscht verändert wurde.

Die folgenden zwei Beispiele zeigen, dass man an die replacement-Funktion sogar weitere Argumente übergeben kann; die weiteren Argumente müssen nur in der Argument-Liste zwischen x und value stehen.

  1. Das erste Beispiel implementiert eine Variante der Funktion replace.NA(x) <- value von oben: Die Ersetzung wird nur vorgenommen, wenn der Modus von x mit dem eingegebenen Modus übereinstimmt (zusätzliches Argument mode); andernfalls wird die Funktion verlassen.
  2. Im zweiten Beispiel werden in einem Vektor alle Komponenten, deren Betrag kleiner ist als eine gewisse Schranke tol, auf den Wert value gesetzt.

1. Beispiel:

# Variante von replace.NA(x) <- value:

"replace.NA<-" <- function(x, mode, value){
  stopifnot(identical(mode(x), mode))
  x[which(is.na(x))] <- value
  return(x)
}

# Test mit numeric-Vektor und Modus numeric:
v <- c(1, NA, 2, NA)
v
# [1]  1 NA  2 NA

replace.NA(x = v, mode = "numeric") <- 0

v
# [1] 1 0 2 0

# Test mit character-Vektor und Modus numeric:
v <- c("A", NA, "C", "D")
v
# [1] "A" NA  "C" "D"

replace.NA(x = v, mode = "numeric") <- 0
# Error: identical(mode(x), mode) is not TRUE

2. Beispiel:

"replace<-" <- function(x, tol, value){
  x[which( abs(x) < tol )] <- value
  return(x)
}

v <- c(1, 0.05, -0.05, 0.1)
v
# [1]  1.00  0.05 -0.05  0.10

replace(x = v, tol = 0.1) <- 0

v
# [1] 1.0 0.0 0.0 0.1

v <- c(1, 0.05, -0.05, 0.1)

replace(x = v, tol = 0.1) <- NA

v
# [1] 1.0  NA  NA 0.1

Aufgabe:

Testen Sie, ob die Funktion aus dem zweiten Beispiel auch auf Matrizen (allgemein Felder) oder Dataframes angewendet werden kann oder ob dazu die Implementierung abgeändert werden muss.

Der Rückgabewert ist unsichtbar: invisible()

Im letzten Abschnitt wurde auf eine Besonderheit der replacement-Funktion replace.NA(x) <- hingewiesen: Laut ihrer Implementierung ist der Rückgabewert das Objekt x, aber beim Aufruf der Funktion wird der Rückgabewert nicht ausgegeben. Dieses Verhalten gilt auch für sämtliche replacement-Funktionen aus den Basis-Paketen.

Die Ursache ist am Quelltext nicht abzulesen: im Beispiel von replace.NA(x)<- steht ausdrücklich return(x) im Quelltext, die Funktionen aus den Basis-Paketen sind meist primitive Funktionen, so dass man ihren Quelltext nicht lesen kann.

In R gibt es die Möglichkeit, einen Rückgabewert unsichtbar zu machen, das heißt es wird zwar ein Rückgabewert berechnet und man kann ihn wie gewohnt für eine Zuweisung verwenden, aber beim Aufruf der Funktion wird der Rückgabewert nicht ausgegeben. Dies geschieht indem man die Funktion invisible() einsetzt. Bei replacement-Funktionen ist dies nicht nötig: sie haben immer einen unsichtbaren Rückgabewert.

Das folgende Skript implementiert zweimal die Funktion add(), die lediglich zwei Zahlen addiert, in der zweiten Version ist der Rückgabewert unsichtbar:

# herkömmliche Implementierung:
add <- function(x, y){
  return(x + y)
}

# Test:
add(5, 17)
# [1] 22

# Implementierung mit unsichtbarem Rückgabewert:
add <- function(x, y){
  return(invisible(x + y))
}

# Test:
add(5, 17)
# kein Ausgabe

y <- add(5, 17)
y
# [1] 22

Zeile 2 bis 8: Die erste Version von add() bedarf keiner Erklärung.

Zeile 12: In der zweiten Version wird invisible() eingesetzt, um den Rückgabewert unsichtbar zu machen.

Zeile 16: Der Aufruf liefert jetzt keine Konsolen-Ausgabe.

Zeile 19: Aber die Zuweisung kann dennoch vorgenommen werden.

Möchte man dennoch den Rückgabewert als Konsolen-Ausgabe sehen, muss man den Funktions-Aufruf in runde Klammern setzen:

(add(5, 17))
# [1] 22

Damit ist aber noch nicht erklärt, warum eine replacement-Funktion wie replace.NA(x) <- den Rückgabewert nicht anzeigt. Die Lösung des Problems ist auch nicht offensichtlich: Bei replacement-Funktionen wird intern der Rückgabewert immer unsichtbar gemacht – auch wenn wie in der selbstdefinierten replacement-Funktion wie oben ausdrücklich return(x) in den Quelltext geschrieben wird..

Zusammenfassung: Spezielle Funktionen

Definition eines binären Operators

Der Name des binären Operators kann frei gewählt werden, er muss nur mit dem Symbol % beginnen und enden. Zur Definition wird der Name in Anführungsstriche geschrieben. Der Rest der Definition ist wie der einer Funktion mit zwei Eingabewerten:

"%++%" <- function(a, b){
  return(sqrt(a*a + b*b)) 
} 

3 %++% 4
# [1] 5

Definition einer anonymen Funktionen

Eine anonyme Funktion wird dort eingesetzt, wo eine andere Funktion als Eingabewert eine Funktion erwartet. Bis auf die Tatsache, dass sie keinen Namen hat, gleicht die Definition der einer üblichen Funktion (mit function(), sowie der Implementierung in geschweiften Klammern).

v <- (1:5)

outer(X = v, Y = v, FUN = function(a, b){return(sqrt(a*a + b*b)) })
#     [,1]     [,2]     [,3]     [,4]     [,5]
# [1,] 1.414214 2.236068 3.162278 4.123106 5.099020
# [2,] 2.236068 2.828427 3.605551 4.472136 5.385165
# [3,] 3.162278 3.605551 4.242641 5.000000 5.830952
# [4,] 4.123106 4.472136 5.000000 5.656854 6.403124
# [5,] 5.099020 5.385165 5.830952 6.403124 7.071068

Definition einer replacement-Funktion

Der Name einer replacement-Funktion ist frei wählbar, er muss aber mit <- enden. Und er wird zur Definition in Anführungsstriche geschrieben. Die Argument-Liste muss x und value enthalten; dabei ist x das Objekt, das verändert werden soll, und value der Wert, der neu zugewiesen wird. Die Implementierung muss dann x zurückgeben.

"replace.NA<-" <- function(x, value){
  x[which(is.na(x))] <- value
  return(x)
}

v <- c(1, NA, 2, NA)
replace.NA(x = v) <- 0

v
# [1] 1 0 2 0

Soll es weitere Argumente geben, werden sie zwischen x und value geschrieben:

"replace.NA<-" <- function(x, mode, value){
  stopifnot(identical(mode(x), mode))
  x[which(is.na(x))] <- value
  return(x)
}

v <- c("A", NA, "C", "D")

replace.NA(x = v, mode = "numeric") <- 0
# Error: identical(mode(x), mode) is not TRUE

Der Rückgabewert einer replacement-Funktion ist unsichtbar.

Funktionen mit unsichtbarem Rückgabewert

Der Rückgabewert einer Funktion kann mit invisible() unsichtbar gemacht werden, das heißt, er wird beim Aufruf der Funktion nicht angezeigt. Die Funktion kann aber wie üblich für Zuweisungen eingesetzt werden.

add <- function(x, y){
  return(invisible(x + y))
}

# Test:
add(5, 17)
# kein Ausgabe

y <- add(5, 17)
y
# [1] 22