Tracce
/
R
R
/
Programma
/
Funzioni
Fu

Funzioni in R

1 esercizio

Informazioni su Funzioni

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:

  • La funzione prende un argomento formale x.
  • C'è un corpo della funzione, di solito tra parentesi graffe { }.
  • Una volta creato l'oggetto funzione, lo assegniamo a una variabile 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.

Advanced

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

Argomenti

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

Argomenti opzionali

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

Argomenti aggiuntivi

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"

L'ambiente della funzione

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.

Le funzioni come valori restituiti

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

Le funzioni anonime

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.

Copia alla modifica

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.

Caution

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.

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Funzioni