Le funzioni sono state introdotte nel concetto Basi, con esempi come questo:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Guardando più da vicino la definizione di squareit, possiamo individuare varie parti:
x.{ }.squareit.In R una funzione è un oggetto di prima classe, proprio come i numeri e le stringhe.
Quindi squareit <- function... è un'assegnazione che, dal punto di vista sintattico, è del tutto simile a x <- 42.
Accedere ai componenti di una funzione è raro nell'uso normale, ma è piuttosto semplice.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Gli argomenti ottenuti con formals() assomigliano a una lista, con la sintassi $x.
In realtà il tipo è pairlist, un particolare tipo di lista che contiene coppie chiave-valore.
Il body è codice eseguibile, con il tipo language (probabilmente non è qualcosa di cui devi preoccuparti).
A differenza di altri linguaggi di scripting come Python e Julia, R non fa una distinzione netta tra argomenti posizionali e argomenti per parola chiave.
Le chiamate di funzione possono passare i valori in modo posizionale oppure per nome. Quest'ultimo modo è utile per funzioni complesse con molti argomenti, dove è difficile ricordarne l'ordine.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
I valori predefiniti degli argomenti si possono specificare nella definizione della funzione, ma devono venire dopo tutti gli argomenti senza valore predefinito.
Possiamo poi scegliere se accettare il valore predefinito o sovrascriverlo.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Per accettare un numero arbitrario di argomenti aggiuntivi, usa un ... (ellissi) nella definizione.
È possibile convertire in un vettore tutti i valori extra della chiamata di funzione, ma continua a leggere per scoprire un modo alternativo di usare questi «dot args» (chiamati «varargs» in diversi altri linguaggi).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Abbiamo detto prima che gli argomenti formali e il corpo sono entrambi componenti di una funzione.
In realtà c'è una terza componente: l'ambiente in cui la funzione è definita.
Lo si può illustrare con il caso delle funzioni annidate:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
La chiamata di funzione passa x = 5 alla funzione esterna, e questo valore è disponibile all'interno di quel corpo della funzione.
La funzione interna è parte del corpo della funzione esterna e ha accesso al valore di x.
Detto in altri termini, x = 5 si trova nell'ambiente della funzione interna.
Tecnicamente, questo è noto come closure.
L'ambiente è particolarmente importante con i dot args, perché tutti i valori forniti in questo modo possono essere passati alle chiamate di funzione nel corpo della funzione. La funzione esterna non ha bisogno di sapere né di preoccuparsi di cosa significhino i dot args.
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
Questa tecnica è ampiamente usata da librerie del Tidyverse come stringr.
Molte funzioni di stringr sono un wrapper comodo attorno a funzioni di basso livello di stringi e di R base.
Gli argomenti extra forniti alle funzioni str_*() vengono semplicemente passati a quelle funzioni di livello inferiore.
Una funzione esterna può definire una funzione interna (con un nome oppure anonima, come descritto più avanti) e usarla come valore restituito.
La funzione restituita includerà l'ambiente in cui è stata definita.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Quando definiamo una funzione, di solito leghiamo l'oggetto funzione risultante a una variabile:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Questo rende facile usare la funzione più avanti nello script, ma un legame del genere non è necessario. Una funzione breve, usata una sola volta, può essere utile nel contesto immediato. Senza un nome a cui legarla, si parla di funzione anonima.
L'uso delle funzioni anonime è così comune che (da R v4.1.0) esiste una sintassi abbreviata per definirle: sostituisci la parola function con una barra rovesciata \.
Un esempio è stato usato nella sezione precedente, sui valori restituiti.
R permette di assegnare valori ai singoli elementi di un vettore. Se passiamo un vettore come argomento di una funzione e lo modifichiamo nel corpo della funzione prima di restituirlo, otteniamo un vettore modificato.
Ma cosa è successo al vettore originale?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R è un linguaggio progettato per la data science. Raccogliere quei dati può costare molto in termini di tempo, di impegno e potenzialmente di una quantità di denaro da capogiro: è importante non corromperli!
La politica generale (con qualche eccezione) è la copia alla modifica. Se un oggetto (come un vettore) viene modificato in un modo che potrebbe causare problemi in seguito, R restituisce una copia modificata e lascia l'originale intatto.
Copiare grandi strutture dati può essere costoso dal punto di vista computazionale, ma in genere è il male minore quando l'alternativa è la corruzione dei dati.
Attenzione alle operazioni che portano a copiare ripetutamente gli stessi dati.
Parafrasando (l'autore del Tidyverse) Hadley Wickham:
I cicli non sono intrinsecamente lenti, ma rendono pericolosamente facile inserire operazioni lente al loro interno.
La vettorizzazione o le funzioni di ordine superiore possono aiutarti a proteggerti da questo tipo di operazione che distrugge le prestazioni.