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.
Noch keine Stimmen abgegeben
Noch keine Kommentare

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 Argument ... , mit dem mehrere Objekte (meist Vektoren) eingegeben werden; jedes dieser Objekte wird in einen character-Vektor umgewandelt. Entsprechende Komponenten dieser Vektoren werden aneinandergeh√§ngt. Es entsteht ein character-Vektor, dessen L√§nge durch den l√§ngsten Vektor der Eingabewerte bestimmt ist.
  • Wird das Argument collapse gesetzt (als Zeichenkette), werden die Komponenten dieses character-Vektors zu einer Zeichenkette vereinigt, wobei collapse als Trennungszeichen verwendet wird.

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):

  • Der Name des Operators muss mit % beginnen und enden.
  • In der Definition des Operators wird der Name in Anf√ľhrungsstriche eingeschlossen; bei der Verwendung des Operators werden die Anf√ľhrungsstriche nicht geschrieben (siehe Zeile 3 und 6).
  • Fasst man "%++%" als Namen einer Funktion auf, ist der Rest der Definition identisch zur Implementierung einer Funktion: man verwendet function(), allerdings mit der Einschr√§nkung, dass nur zwei Eingabewerte erlaubt sind (genauer: nur die beiden ersten Eingabewerte werden verwendet).

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

  • von links nach rechts, also wie (3 %++% 4) %++% 12 ,
  • oder von rechts nach links ausgewertet wird, also 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:

  • Eine Funktion kann wie jedes andere Objekt behandelt werden und so wie √ľblicherweise einem Eingabewert ein Objekt √ľbergeben wird, muss jetzt eine Funktion √ľbergeben werden. Dazu muss nat√ľrlich die Implementierung der Funktion vorliegen.
  • Damit die Funktion sp√§ter richtig eingesetzt werden kann, muss sie geeignete Argument-Liste und den geeigneten R√ľckgabewert besitzen. Da Datentypen in R immer nur dynamisch erkannt werden, entscheidet sich dies erst beim Einsatz der Funktion in der Implementierung der Funktion h√∂herer Ordnung.

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

  • leicht durch den Namen der Funktion aufgerufen werden k√∂nnen (und somit wiederverwendbar sind),
  • durch Eingabewerte konfigurierbar sind (wodurch man an Flexibilit√§t gewinnt).

Der Einsatz von Funktionen h√∂herer Ordnung ist typisch f√ľr die sogenannte funktionale Programmierung:

  • Durch Funktionen als Eingabewerte kann man leichter das Verhalten einer anderen Funktion steuern.
  • Indem man Funktionen als R√ľckgabewert zul√§sst, kann man dies leichter vorbereiten.

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:

  • Es wird zweimal gew√ľrfelt.
  • Sind bei einem Spieler die beiden Ergebnisse identisch, bei dem anderen Spieler nicht, gewinnt der Spieler mit den identischen Ergebnissen.
  • In allen anderen F√§llen entscheidet die Augensumme wie bei less.sum().

‚ô¶ ‚ô¶ ‚ô¶ ‚ô¶

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

  • B gewinnt,
  • B verliert,
  • unentschieden endet.

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:

  • Zuerst sucht man zu gegebenem a diejenigen Kombinationen f√ľr b aus obigem Dataframe, f√ľr die B gewinnt (die Funktion rule() liefert TRUE, siehe Zeile 6 und 7 unten).
  • Bei den restlichen Kombinationen gewinnt entweder A oder das Spiel endet unentschieden.
  • Um festzustellen, ob A gewinnt, muss man die Argumente in rule() vertauschen (Zeile 11 und 12).
  • Alle jetzt noch verbleibenden Kombinationen f√ľhren zu einem Unentschieden (Zeile 15 und 16).
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:

  • Sie ersetzt eine Iteration √ľber die Zeilen des Dataframes, das mit expand.grid() erzeugt wurde (genauer muss es dazu in eine Matrix verwandelt werden, siehe Zeile 6: Aufruf von as.matrix()).
  • Durch das Argument MARGIN = 1 wird die Matrix zeilenweise abgearbeitet.
  • Auf jede Zeile der Matrix wird die Funktion angewendet, die mit FUN = rule √ľbergeben wird. Eine Zeile der Matrix ist ein Vektor der L√§nge 2. Da die Funktion rule() zwei Eingabe-Vektoren der L√§nge 2 besitzt, wird in dem weiteren Argument a = a das zus√§tzliche Argument von rule() gesetzt (Zeile 6). Da f√ľr das weitere Argument ausdr√ľcklich der Name a gesetzt ist, ist die Zeile der Matrix der Eingabewert b f√ľr rule().
  • Der R√ľckgabewert von apply() ist f√ľr jede Zeile ein logischer Wert, so dass insgesamt ein logischer Vektor der L√§nge 36 entsteht. Dessen TRUE-Eintr√§ge werden verwendet, um aus dem Dataframe die gesuchten Zeilen auszuw√§hlen (Zeile 7 und 12).

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

  • kleinerer Augensumme,
  • gr√∂√üerer Augensumme und
  • identischer Augensumme.

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:

  • Die Funktion wird nur einmalig eingesetzt, so dass man sie nicht unter einem Namen abspeichern muss.
  • Die Implementierung besteht nur aus ein oder h√∂chstens zwei Zeilen, so dass der Quelltext der aufrufenden Funktion nicht √ľberladen wird.

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:

  • Man definiert sich eine Funktion, etwa mit Namen analyse(), die s√§mtliche relevanten Funktionen aufruft, die zur statistischen Auswertung ben√∂tigt werden.
  • Eingabewert der Funktion analyse() ist die Zahlenfolge.
  • Die R√ľckgabewerte der Funktionen werden zu einer Liste zusammengefasst und diese Liste wird von analyse() zur√ľckgegeben.

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:

  • Man bildet zuerst eine Liste der relevanten statistischen Funktionen: analysingFunctions.
  • Die Funktion lapply() sorgt daf√ľr, dass die Zahlenfolge an die Komponenten der Liste √ľbergeben wird und die Funktionen ausgef√ľhrt werden.
  • Das Ergebnis ist dann automatisch in einer Liste verpackt, da schon die statistischen Funktionen in einer Liste enthalten waren.

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