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, è identica a x <- 42.
A differenza di altri linguaggi di scripting come Python e Julia, R non fa una distinzione netta tra argomenti posizionali e argomenti con nome.
Le chiamate di funzione possono passare i valori in modo posizionale oppure per nome. Quest'ultimo è 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 che non hanno un valore predefinito.
A quel punto possiamo 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 i valori in eccesso passati nella chiamata, 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"
In precedenza abbiamo detto che i parametri 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 del corpo di quella 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é qualsiasi valore fornito in questo modo può essere inoltrato alle chiamate di funzione all'interno del corpo della funzione. Alla funzione esterna non serve sapere cosa significano i dot args, né le importa.
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 è usata ampiamente dalle librerie del Tidyverse, come stringr.
Molte funzioni di stringr sono un wrapper amichevole attorno a funzioni di basso livello di stringi e di R di base.
Gli argomenti aggiuntivi forniti alle funzioni str_*() vengono semplicemente inoltrati a quelle funzioni di livello inferiore.
Una funzione esterna può definire una funzione interna (con un nome oppure anonima, come spiegato più avanti) e usarne il valore come valore restituito.
La funzione restituita comprenderà 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 assegniamo 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'associazione del genere non è necessaria. Una funzione breve, usata una sola volta, può essere utile nel contesto immediato. Senza un nome associato, si chiama funzione anonima.
L'uso delle funzioni anonime è così comune che (a partire da R v4.1.0) esiste una sintassi abbreviata per definirle: sostituisci la parola function con una barra rovesciata \.
Un esempio di questa sintassi è stato usato nella sezione precedente, sui valori restituiti.
R consente di assegnare un valore 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 che fine ha fatto il 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 pensato 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 regola 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 dispendioso dal punto di vista computazionale, ma di solito è il male minore quando l'alternativa è la corruzione dei dati.
Fai attenzione alle operazioni che portano a copiare più volte gli stessi dati.
Parafrasando (l'autore del Tidyverse) Hadley Wickham:
I cicli non sono lenti di per sé, ma rendono pericolosamente facile includere operazioni lente al loro interno.
La vettorizzazione o le funzioni di ordine superiore possono aiutarti a difenderti da questo tipo di problema che penalizza le prestazioni.
La tua azienda lavora con dati di mappatura per costruire pagine web. Dopo qualche discussione, si è deciso di iniziare a usare R per eseguire alcuni calcoli in modo dinamico.
Cerca di lavorare direttamente con i punti come vettori: non c'è bisogno di scomporli in coordinate x e y separate.
Implementa la funzione scale(point, s), che prende un punto con un numero arbitrario di dimensioni e lo scala in base al valore predefinito s.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Implementa la funzione translate(point, ...), che restituisce un nuovo punto in cui ogni coordinata è spostata dei valori contenuti nei puntini di sospensione.
Questa funzione deve saper gestire punti di qualsiasi dimensione, con un numero di argomenti passati attraverso i puntini di sospensione pari alla dimensione del punto.
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
Alcuni dei tuoi colleghi hanno meno esperienza con R, così decidi di usare una closure per creare trasformazioni riutilizzabili per coppie di coordinate {x, y}.
Implementa la funzione transform2d(dx, dy, s), che restituisce una funzione basata su una closure per eseguire in modo ripetibile la traslazione e la scalatura di un punto 2D.
L'argomento di scalatura s deve essere opzionale, con valore predefinito 1.
La funzione restituita deve prendere un punto 2D e poi:
dx e dy
s
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
L'ordine delle operazioni è importante: traslare e poi scalare dà il risultato corretto, mentre scalare e poi traslare no. In termini tecnici, le operazioni non commutano.
I dati di mappatura possono anche contenere le altezze, usate per aggiungere ombreggiature e curve di livello sullo schermo.
Implementa la funzione transform3d(dx, dy, dz, s), che restituisce una funzione.
L'argomento di scalatura s deve essere opzionale, con valore predefinito 1.
La funzione restituita deve prendere un punto 3D, traslarlo dei valori predefiniti e poi scalarlo di s.
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Iscriviti a Exercism per imparare e padroneggiare R con 21 concetti111 esercizi e il mentoring di persone reali, tutto gratis.