Capitolo 12 Definizione di Funzioni

Nel Capitolo 4.2 abbiamo introdotto il concetto di funzioni e abbiamo visto come queste siano utilizzate per manipolare gli oggetti in R ed eseguire innumerevoli compiti. Nel Capitolo 5.3 abbiamo visto inoltre come sia possibile utilizzare i pacchetti per accedere a nuove funzioni estendendo quindi notevolmente le possibili applicazioni di R. In R, tuttavia, è anche possibile definire le proprie funzioni per eseguire determinati compiti a seconda delle proprie specifiche necessità.

Questo è uno dei vantaggi principali di utilizzare un linguaggio di programmazione, ovvero il poter sviluppare funzioni ad-hoc a seconda delle proprie esigenze e non limitarsi a quelle prestabilite. In questo capitolo descriveremo come creare le proprie funzioni in R.

12.1 Creazione di una Funzione

Il comando usato per creare una funzione in R è function() seguito da una coppia di parentesi graffe { } al cui interno deve essere specificato il corpo della funzione:

nome_funzione <- function( ){
  <corpo-funzione>
}

Nota come sia necessario assegnare la funzione ad un oggetto, ad esempio my_function, che diventerà il nome della nostra funzione. Per eseguire la funzione sarà sufficiente, come per ogni altra funzione, indicare il nome della funzione seguito dalle parentesi tonde, nel nostro caso my_function(). Vediamo alcuni esempi di semplici funzioni:

# Definisco la funzione
my_greetings <- function(){
  print("Hello World!")
}

my_greetings()
## [1] "Hello World!"

# Definisco un'altra funzione
my_sum <- function(){
  x <- 7
  y <- 3
  
  x + y
}

my_sum()
## [1] 10

Quando chiamiamo la nostra funzione, R eseguirà il corpo della funzione e ci restituirà il risultato dell’ultimo comando eseguito. Le funzioni del precedente esempio, tuttavia, si rivelano poco utili poichè eseguono sempre le stesse operazioni senza che ci sia permesso di specificare gli input delle funzioni. Inoltre, sebbene siano funzioni molto semplici, potrebbe non risultare chiaro quale sia effettivamente l’output restituito dalla funzione.

:::{design title=“Nomi Funzioni” data-latex=“[Nomi Funzioni]”} Nel definire il nome di una funzione è utile seguire le stese indicazioni che riguardavano i nomi degli oggetti (vedi Capitolo 4.1.2). In questo caso affinché un nome sia auto-descrittivo si tende ad utilizzare verbi che riassumano l’azione eseguita dalla funzione. :::

12.1.1 Definire Input e Output

Ricordiamo che in generale le funzioni, ricevuti degli oggetti in input, compiono determinate azioni e restituiscono dei nuovi oggetti in output. Input e ouput sono quindi due aspetti aspetti fondamentali di ogni funzione che richiedono particolare attenzione.

  • Input - Abbiamo anche visto che in R gli input vengono specificati attraverso gli argomenti di una funzione (vedi Capitolo 4.2.1). Per definire gli argomenti di una funzione, questi devono essere indicati all’interno delle parentesi tonde al momento della creazione della funzione
  • Output - Per specificare l’output di una funzione si utilizza la funzione return(), indicando tra le parentesi il nome dell’oggetto che si desidera restituire come risultato della funzione.

La definizione di una funzione con tutti i suoi elementi seguirà quindi il seguente schema:

nome_funzione <- function(argument_1, argument_2, ...){
  <corpo-funzione>
  
  return(<nome-output>)
}

Possiamo ora riscrivere le precedenti funzioni permettendo di personalizzare gli input e evidenziando quale sia output che viene restituito.

# Ridefinisco my_greetings()
my_greetings <- function(name){
  greetings <- paste0("Hello ", name, "!")
  
  return(greetings)
}

my_greetings(name = "Psicostat")
## [1] "Hello Psicostat!"

# Ridefinisco my_sum()
my_sum <- function(x, y){
  result <- x + y
  
  return(result)
}

my_sum(x = 8, y = 6)
## [1] 14

Qualora fosse necessario è possibile fare in modo che sia la funzione stessa a chiedere all’utente di inserire delle specifiche informazioni attraverso la funzione readline().

Utilizzando la funzione readline() comparirà nella console il messaggio che abbiamo impostato nell’argomento prompt (ricordati di concludere lasciando uno spazio). All’utente sarà richiesto di digitare una qualche sequenza alfanumeirca e premere successivamente invio. I valori inseriti dell’utente saranno salvati come una variabile caratteri nell’oggetto che abbiamo indicato e potranno essere utilizzati sucessivamente nella funzione.

happy_birthday <- function(){
  name <- readline(prompt = "Inserisci il tuo nome: ")
  message <- paste0("Buon Compleanno ", name, "!")
  
  return(message)
}

happy_birthday()

Nota che questa funzione possona essere usata solo in sessioni interattive di R poichè è richiesta un ’azione diretta da parte dell’utente.

Default Argomenti

Abbiamo visto come aggiungere degli argomenti che devono essere definiti dall’utente nell’utilizzare la funzione. Qualora l’utente non specifichi uno o più argomenti R riporterà un messaggio di errore indicando come ci siano degli argomenti non specificati e senza valori di default.

my_sum(x = 5)
## Error in my_sum(x = 5): argument "y" is missing, with no default

Per assegnare dei valori di default agli argomenti di una funzione, è sufficiente indicare al momento della creazione all’interno delle parentesi tonde nome_argomento = <valore-default>. Se non altrimenti specificati, gli argomenti assumeranno i loro valori di default. Tuttavia, gli utenti sono liberi di specificare gli argomenti della funzione a seconda delle loro esigenze. Ad esempio impostiamo nella funzione my_sum() il valore di default y = 10.

my_sum <- function(x, y = 10){
  result <- x + y
  
  return(result)
}

# Utilizzo il valore di default di y
my_sum(x = 5)
## [1] 15

# Specifico il valore id y
my_sum(x = 5,  y = 8)
## [1] 13

Questa pratica è molto usata per specificare comportamenti particolari delle funzioni. In genere le funzioni sono definite con un funzionameto di default ma alcuni argomenti possono essere specificati per particolari essigenze solo quando necessario.

Per imporre la scelta di un argomento tra una limitata serie di valori è possibile indicare nella definizione dell’argomento un vettore con le possibili entrate. Successivamente la scelta deve essere validata attraverso la funzione match.arg() come nell esempio successivo:

# Ridefinisco my_greetings()
my_greetings <- function(name, type = c("Hello", "Goodbye")){
  
  type <- match.arg(type)
  greetings <- paste0(type," ", name, "!")
  
  return(greetings)
}

# Scelta type
my_greetings(name = "Psicostat", type = "Goodbye")
## [1] "Goodbye Psicostat!"

# Valore default
my_greetings(name = "Psicostat")
## [1] "Hello Psicostat!"

# Valore non ammesso
my_greetings(name = "Psicostat", type = "Guten Tag")
## Error in match.arg(type): 'arg' should be one of "Hello", "Goodbye"

La funzione match.arg() permette di confrontare il valore specificato risspetto a quelli indicati nella definizione dell’argomento, riportando un errore in mancanza di un match. Osserva come se non specificato il valore di default sia il primo indicato nella definizione.

Esercizi

Esegui i seguenti esercizi:

  1. definisci una funzione che trasformi la temperatura da Celsius a Fahrenheit \[ Fahrenheit = Celsius *1.8 + 32 \]
  2. Definisci una funzione che permetta di fare gli auguri di buon natale e buona pasqua ad una persona.
  3. Definisci una funzione che, dato un vettore di valori numerici, calcoli il numero di elementi e la loro media.
  4. Definisci una funzione iterattiva che calcoli il prodotto di due valori. Gli input devono essere ottenuti conlla funzione readline().
  5. Definisci una funzione che calcoli lo stipendio mensile sulla base delle ore svolte nel mese e la paga oraria.

12.2 Lavorare con le Funzioni

Le funzioni sono sicuramente l’aspetto più utile e avanzato del linguaggio R e in generale dei linguaggi di programmazione. I pacchetti che sono sviluppati in R non sono altro che insieme di funzioni che lavorano assieme per uno scopo preciso. Oltre allo scopo della funzione è importante capire come gestire gli errori e gli imprevisti. Se la funzione infatti accetta degli argomenti, l’utente finale o noi stessi che la utilizziamo possiamo erroneamente usarla nel modo sbagliato. E’ importante quindi capire come leggere gli errori ma sopratutto creare messaggi di errore o di avvertimento utili per l’utilizzo della funzione.

Prendiamo ad esempio la funzione somma +, anche se non sembra infatti l’operatore + è in realtà una funzione. Se volessimo scriverlo come una funzione simile a quelle viste in precedenza possiamo:

my_sum <- function(x, y){
  res <- x + y
  return(res)
}

my_sum(1, 5)
## [1] 6

Abbiamo definito una (abbastanza inutile) funzione per calcolare la somma tra due numeri. Cosa succede se proviamo a sommare un numero con una stringa? Ovviamente è un’operazione che non ha senso e ci aspettiamo un qualche tipo di errore:

my_sum("stringa", 5)
## Error in x + y: non-numeric argument to binary operator

In questo caso infatti, vediamo un messaggio denominato Error... con l’utile informazione che uno degli argomenti utilizzati risulta essere non-numeric. E’ un messaggio semplice, mirato e sopratutto non fornisce un risultato perchè una condizione fondamentale (la somma vale solo per i numeri) non è rispettata. La funzione + ha già al suo interno questo controllo, ma se noi volessimo implementare un controllo e fornire un messaggio abbiamo a disposizione diverse opzioni:

  • stop(<message>, .call = TRUE): Se inserito all’interno di una funzione, interrompe l’esecuzione e fornisce il messaggio specificato denominato come Error.
  • stopifnot(expr1, expr2, ...): Se inserito all’interno di una funzione interrompe l’esecuzione se almeno una di una serie di condizioni risulta come non VERA.
  • warning(): Restituisce un messaggio all’utente senza tuttavia interrompere l’esecuzione ma fornendo informazioni su un possibile problema che deriva dal tipo di input o da un possibile effetto collaterale della funzione.
  • message(): Fornisce un semplice messaggio senza alterare l’esecuzione della funzione che può essere utile per informare rispetto ad operazioni eseguite

Tornando all’esempio della somma, immaginiamo di voler scrivere una funzione che somma solo numeri positivi. In altri termini vogliamo che i valori x e y in input siano solo posiviti per poter eseguire la funzione. Possiamo quindi inserire un controllo condizionale e usare la funzione stop() nel caso in cui la condizione non sia rispettata:

my_positive_sum <- function(x, y){
  if(x < 0 | y < 0){
    stop("Gli argomenti devono essere numeri positivi!")
  }
  res <- x + y
  return(res)
}

my_positive_sum(10, 5)
## [1] 15
my_positive_sum(10, -5)
## Error in my_positive_sum(10, -5): Gli argomenti devono essere numeri positivi!

Un modo più rapido di gestire l’arresto dell’esecuzione è usare stopifnot(). La logica tuttavia è leggermente diversa rispetto ad usare un if + stop(). Nell’esempio precedente la logica è: “se x oppure y sono minori di 0, stop”. Con stopifnot() utilizziamo una logica inversa, ovvero inseriamo quello che vogliamo sia VERO e fermiamo l’esecuzione se è FALSO. Nel nostro caso usiamo stopifnot(x > 0, y > 0) ovvero fermati se x oppure y NON SONO maggiori di 0. Rispetto a stop() non fornisce un messaggio personalizzato ma restituisce la prima delle condizioni specificate che non è rispettata:

my_positive_sum <- function(x, y){
  stopifnot(x > 0, y > 0)
  res <- x + y
  return(res)
}
my_positive_sum(10, -5)
## Error in my_positive_sum(10, -5): y > 0 is not TRUE

Allo stesso modo immaginiamo (con molta immaginazione) che la nostra funzione non sia affidabile con numeri minori di 10, nel senso che qualche volta potrebbe sbagliare il risultato. In questo caso non vogliamo interrompere l’esecuzione ma fornire un messaggi di warning:

my_positive_sum <- function(x, y){
  # Error
  if(x < 0 | y < 0){
    stop("Gli argomenti devono essere numeri positivi!")
  }
  # Warning
  if(x < 10 | y < 10){
    warning("Per qualche strano motivo la funzione non gestisce bene i numeri minori di 10, attenzione!! :)")
  }
  res <- x + y
  return(res)
}

my_positive_sum(15, 4)
## Warning in my_positive_sum(15, 4): Per qualche strano motivo la funzione non
## gestisce bene i numeri minori di 10, attenzione!! :)
## [1] 19

Come vedete, abbiamo il risultato (ovviamente corretto) ma anche un messaggio di warning che ci avverte di questa possibile (ma non critica) problematica.

Infine possiamo accompagnare il risultato alcune condizioni che si realizzano con un semplice messaggio che fornisce ulteriori informazioni con la funzione message():

my_positive_sum <- function(x, y){
  # Error
  if(x < 0 | y < 0){
    stop("Gli argomenti devono essere numeri positivi!")
  }
  # Warning
  if(x < 10 | y < 10){
    warning("Per qualche strano motivo la funzione non gestisce bene i numeri minori di 10, attenzione!! :)")
  }
  res <- x + y
  message("Ottimo lavoro! :)")
  return(res)
}

my_positive_sum(12, 10)
## Ottimo lavoro! :)
## [1] 22

Un ultimo aspetto importante riguarda cosa avviene se assegniamo il risultato di una funzione in presenza di errori, warning o messaggi. In generale, tranne che per la presenza di errori e quindi usando la funzione stop(), l’output rimane lo stesso e il messaggio è solo stampato nella console:

res1 <- my_positive_sum(10,5)
## Warning in my_positive_sum(10, 5): Per qualche strano motivo la funzione non
## gestisce bene i numeri minori di 10, attenzione!! :)
## Ottimo lavoro! :)
res2 <- my_positive_sum(10,23)
## Ottimo lavoro! :)
res3 <- my_positive_sum(10,-1)
## Error in my_positive_sum(10, -1): Gli argomenti devono essere numeri positivi!

res1
## [1] 15
res2
## [1] 33
res3 # nessun output
## Error in eval(expr, envir, enclos): object 'res3' not found

Come vedete infatti, quando abbiamo un errore e fermiamo l’esecuzione, la funzione pur prevedendo un output, non fornisce risultato perchè è stata interrotta.

12.3 Ambiente della funzione

Il concetto di ambiente in R è abbastanza complesso2. In parole semplici, tutte le operazioni che normalmente eseguiamo nella console o in uno script, avvengono in quello che si chiama global environment. Quando scriviamo ed eseguiamo una funzione, stiamo creando un oggetto funzione (nel global environment) che a sua volta crea un ambiente interno per eseguire le operazioni previste. Immaginiamo di avere questa funzione my_fun() che riceve un valore x e lo somma ad un valore y che non è un argomento.

my_fun <- function(x){
  return(x + y)
}

my_fun(10)
## Error in my_fun(10): object 'y' not found

Chiaramente otteniamo un errore perchè l’oggetto y non è stato creato. Se però creiamo l’oggetto y all’interno della funzione, questa esegue regolarmente la somma MA non crea l’oggetto y nell’ambiente globale.

my_fun <- function(x){
  y <- 1
  return(x + y)
}

my_fun(10)
## [1] 11
ls() # abbiamo solo la nostra funzione come oggetto
## [1] "my_fun"          "my_greetings"    "my_positive_sum" "my_sum"         
## [5] "res1"            "res2"

Da qui è chiaro che quello che avviene all’interno della funzione è in qualche modo compartimentalizzato rispetto all’ambiente globale. L’unico modo per influenzare l’ambiente globale è quello di assegnare il risultato della funzione, creando quindi un nuovo oggetto:

res <- my_fun(10)
ls()
## [1] "my_fun"          "my_greetings"    "my_positive_sum" "my_sum"         
## [5] "res"             "res1"            "res2"

Altra cosa importante, sopratutto per gestire effetti collaterali riguarda il fatto che la funzione NON modifica gli oggetti presenti nell’ambiente globale:

y <- 10 # ambiente globale

my_fun <- function(x){
  y <- 1 # ambiente funzione
  return(x + y) # questo si basa su y funzione
}

my_fun(1)
## [1] 2
y
## [1] 10

Come vedete, abbiamo creato un oggetto y dentro la funzione. Se eseguito nello stesso ambiente questo avrebbe sovrascritto il precedente valore. Il risultato si basa sul valore di y creato nell’ambiente funzione e l’y globale non è stato modificato.

Un ultimo punto importante riguarda invece il legame tra ambiente funzione e quello globale. Abbiamo visto la loro indipendenza che però non è totale. Se infatti all’interno della funzione utilizziamo una variabile definita solamente nell’ambiente globale, la funzione in automatico userà quel valore (se non specificato internamente). Questo è utile per far lavorare funzioni e variabili globali MA è sempre preferibile creare un ambiente funzione indipendente e fornire come argomenti tutte gli oggetti necessari.

y <- 10

my_fun <- function(x){
  return(x + y) # viene utilizzato y globale
}

my_fun(1)
## [1] 11

Le cose importanti da ricordare quando si definiscono e utilizzano funzioni sono:

  • Ogni volta che una funzione viene eseguita, l’ambiente interno viene ricreato e quindi è come ripartire da zero
  • Gli oggetti creati all’interno della funzione hanno priorità rispetto a quelli nell’ambiente esterno
  • Se la funzione utilizza un oggetto non definito internamente, automaticamente cercherà nell’ambiente principale

12.4 Best practice

Scrivere funzioni è sicuramente l’aspetto più importante quando si scrive del codice. Permette di automatizzare operazioni, ridurre la quantità di codice, rendere più chiaro il nostro script e riutilizzare una certa porzione di codice in altri contesti. Ci sono tuttavia delle convenzioni e degli accorgimenti per scrivere delle ottime e versatili funzioni:

  • Quando serve una funzione?
  • Scegliere il nome
  • Semplificare la quantita di operazioni e output
  • Commentare e documentare

12.4.1 Quando serve una funzione?

Hadley Wickam suggerisce che se ripetiamo una serie di operazioni più di 2 volte, forse è meglio scrivere una funzione. Immaginiamo di avere una serie di oggetti e voler eseguire la stessa operazione in tutti. Ad esempio vogliamo centrare (ovvero sottrarre a tutti i valori di un vettore la loro media) un vettore numerico:

vec1 <- runif(10)
vec2 <- runif(10)
vec3 <- runif(10)

Abbiamo visto nei capitoli precedenti l’utilizzo dell’apply family e come si possa applicare una funzione ad una serie di oggetti. Pensiamo però ad un caso dove abbiamo uno script molto lungo e in diversi momenti eseguiamo una certa operazione:

# Centriamo
vec1 - mean(vec2)
##  [1]  0.12729618 -0.40531801  0.02733898 -0.58379772  0.02399610 -0.19690274
##  [7] -0.37155398 -0.50369717 -0.39027445 -0.42332292
vec2 - mean(vec2)
##  [1]  0.32642244 -0.14348476 -0.47156559 -0.28923355 -0.09984746 -0.22709173
##  [7]  0.31085942  0.19301046  0.25388859  0.14704217
vec3 - mean(vec3)
##  [1]  0.10201844  0.04060439 -0.15203127 -0.18919809  0.13046704 -0.02452261
##  [7]  0.44904462 -0.36330584 -0.08103922  0.08796253

L’operazione viene eseguita correttamente ed è anche di facile comprensione. Tuttavia stiamo eseguendo sempre la stessa cosa, semplicemente cambiando un input (proprio la definizione di funzione) quindi possiamo:

my_fun <- function(x){
  return(x - mean(x))
}

my_fun(vec1)
##  [1]  0.39691976 -0.13569444  0.29696255 -0.31417415  0.29361968  0.07272083
##  [7] -0.10193041 -0.23407360 -0.12065088 -0.15369934
my_fun(vec2)
##  [1]  0.32642244 -0.14348476 -0.47156559 -0.28923355 -0.09984746 -0.22709173
##  [7]  0.31085942  0.19301046  0.25388859  0.14704217
my_fun(vec3)
##  [1]  0.10201844  0.04060439 -0.15203127 -0.18919809  0.13046704 -0.02452261
##  [7]  0.44904462 -0.36330584 -0.08103922  0.08796253

Il codice non è molto cambiato rispetto a prima in termini di numero di righe o complessità. Immaginate però di esservi resi conto di un errore o di voler cambiare o estendere le operazioni suvec1, vec2 e vec3. Nel primo caso dovreste andare linea per linea dello script e modificare il codice. Nel caso di una funzione, semplicemente cambiando le operazioni queste verranno applicate ogni volta che quella funzione è chiamata. Immaginiamo di voler anche standardizzare (sottrarre la media e dividere per la deviazione standard) i nostri vettori:

my_fun <- function(x){
  res <- (x - mean(x)) / sd(x)
  return(res)
}

my_fun(vec1)
##  [1]  1.5944801 -0.5451028  1.1929386 -1.2620799  1.1795098  0.2921294
##  [7] -0.4094682 -0.9403052 -0.4846708 -0.6174310
my_fun(vec2)
##  [1]  1.1583673 -0.5091809 -1.6734332 -1.0263960 -0.3543262 -0.8058748
##  [7]  1.1031392  0.6849315  0.9009682  0.5218049
my_fun(vec3)
##  [1]  0.4632282  0.1843696 -0.6903180 -0.8590788  0.5924027 -0.1113481
##  [7]  2.0389462 -1.6496379 -0.3679692  0.3994055

Ovviamente la combinazione di funzione e apply family permette di rendere il tutto ancora più compatto ed efficiente:

my_list <- list(vec1, vec2, vec3)
lapply(my_list, my_fun)
## [[1]]
##  [1]  1.5944801 -0.5451028  1.1929386 -1.2620799  1.1795098  0.2921294
##  [7] -0.4094682 -0.9403052 -0.4846708 -0.6174310
## 
## [[2]]
##  [1]  1.1583673 -0.5091809 -1.6734332 -1.0263960 -0.3543262 -0.8058748
##  [7]  1.1031392  0.6849315  0.9009682  0.5218049
## 
## [[3]]
##  [1]  0.4632282  0.1843696 -0.6903180 -0.8590788  0.5924027 -0.1113481
##  [7]  2.0389462 -1.6496379 -0.3679692  0.3994055

12.4.2 Scegliere il nome

Questo potrebbe sembrare un argomento marginale tuttavia la scelta dei nomi sia per le variabili ma sopratutto per le funzioni è estremamente importante. Permette di:

  • leggere chiaramente il nostro codice e renderlo comprensibile ad altri
  • organizzare facilmente un gruppo di funzioni. Quando avete più funzioni, usare una giusta denominazione permette di sfruttare i suggerimenti di RStudio in modo più efficace. Il pacchetto stringr and esempio che fornisce strumenti per lavorare con stringhe, utilizza tutte le funzioni denominate come str_ permettendo di cercare facilmente quella desiderata.

E’ utile utilizzare verbi per nominare le funzioni mentre nomi per nominare argomenti. Ad esempio un nome adatto alla nostra ultima funzione potrebbe essere center_var() mentre il nome del nuovo vettore centered_vec o c_vec. Se troviamo center_var all’interno di uno script è subito chiaro il compito di quella funzione, anche senza guardare il codice al suo interno.

12.4.3 Semplificare la quantita di operazioni e output

Questo è un punto molto importante ma allo stesso tempo variegato. Ci sono diversi stili di programmazione e quindi non ci sono regole fisse oppure delle pratiche migliori di altre. Abbiamo detto che una funzione è un modo per astrarre, riutilizzare e semplificare una serie di operazioni. Possiamo quindi scrivere funzioni molto complesse che ricevono diversi input, eseguono diverse operazioni e restituiscono diversi output. E’ buona pratica però scrivere funzioni che:

  • riducono il numero di operazioni interne
  • forniscono un singolo (o limitati) output
  • hanno un numero di input limitato

Se quindi abbiamo pensato ad una funzione che ha troppi output, è troppo complessa oppure ha troppi input magari possiamo valutare di scomporre la funzione in sotto-funzioni.

Facciamo un esempio con la nostra center_vec(). Possiamo pensare a diverse alternative ed estensioni di questa funzione. Ad esempio possiamo pensare di creare una funzione che centra oppure standardizza il vettore. Possiamo inoltre scegliere se centrare usando la media oppure la mediana. Quindi possiamo pensare a una macro funzione trans_vec() che in base agli argomenti trasforma il vettore:

trans_vec <- function(x, what = c("center_mean", "center_median", "standardize")){
  if(match.arg(what) == "center_mean"){
    res <- x - mean(x)
  }else if(match.arg(what) == "center_median"){
    res <- x - median(x)
  }else if(match.arg(what) == "standardize"){
    res <- (x - mean(x))/sd(x)
  }
  return(res)
}

vec <- runif(10)
trans_vec(vec, "center_mean")
##  [1]  0.01889254  0.34048030 -0.18737537 -0.04253962 -0.49197728 -0.44405130
##  [7]  0.41703036  0.34001520 -0.34639237  0.39591756
trans_vec(vec, "center_mean")
##  [1]  0.01889254  0.34048030 -0.18737537 -0.04253962 -0.49197728 -0.44405130
##  [7]  0.41703036  0.34001520 -0.34639237  0.39591756
trans_vec(vec, "standardize")
##  [1]  0.05265361  0.94892037 -0.52221613 -0.11855816 -1.37114326 -1.23757329
##  [7]  1.16226578  0.94762414 -0.96539736  1.10342430

La funzione è molto chiara ma comunque contiene dei margini di fragilità. L’utente deve inserire una per stringa eseguire l’operazione. Ci sono diversi if e lo scopo della funzione è forse troppo generico. Una migliore soluzione sarebbe quella di scrivere 3 funzioni più semplici, mirate e facili da mantenere e leggere:

center_vec_mean <- function(x){
  return(x - mean(x))
}

center_vec_median <- function(x){
  return(x - median(x))
}

standardize_vec <- function(x){
  return((x - mean(x)) / sd(x))
}

vec <- runif(10)
center_vec_mean(vec)
##  [1] -0.07657349 -0.36426804  0.07096932  0.13685800  0.18662668  0.17932600
##  [7] -0.44729573  0.35991605  0.16049363 -0.20605242
center_vec_median(vec)
##  [1] -0.18048716 -0.46818170 -0.03294434  0.03294434  0.08271302  0.07541233
##  [7] -0.55120939  0.25600239  0.05657997 -0.30996608
standardize_vec(vec)
##  [1] -0.2900803 -1.3799422  0.2688503  0.5184537  0.7069905  0.6793336
##  [7] -1.6944727  1.3634557  0.6079917 -0.7805802

In questo modo il codice è molto più leggibile e chiaro sia dentro le funzioni che quando le funzioni vengono utilizzate. Un’ulteriore alternativa sarebbe quella di raggruppare le funzione di “centramento” specificando se utilizzare media o mediana e separare quella di standardizzazione.

12.4.4 Commentare e documentare

La documentazione è forse la parte di più importante della scrittura del codice. Possiamo classificarla in documentazione formale e informale in base allo scopo. La documentazione formale è quella che troviamo facendo help(funzione) oppure ?funzione. E’ una documentazione standardizzata e necessaria quando si creano delle funzioni in un pacchetto che altri utenti devono utilizzare. La documentazione informale è quella che mettiamo nei nostri script e all’interno delle funzioni come # commento. Entrambe sono molto importati e permettono di descrivere lo scopo generale della funzione, i singoli argomenti e i passaggi eseguiti.

12.5 Importare una funzione

Abbiamo già visto che il comando library() carica un certo pacchetto, rendendo le funzioni contenute disponibili all’utilizzo. Senza la necessità di creare un pacchetto, possiamo comunque organizzare le nostre funzioni in modo efficace. Abbiamo 2 opzioni:

  • scrivere le funzioni nello stesso script dove esse vengono utilizzate
  • scrivere uno script separato e importare tutte le funzioni contenute

Anche in questo caso è una questione di stile e comodità, in generale:

  • Se abbiamo tante funzioni, è meglio scriverle in uno o più file separati e poi importarle all’inizio dello script principale
  • Se abbiamo poche funzioni possiamo tenerle nello script principale, magari in una sezione apposita nella parte iniziale

Nel secondo caso è sufficiente quindi scrivere la funzione e questa sarà salvata come oggetto nell’ambiente principale. Riguardo il primo scenario si può utilizzare la funzione source("percorso/script.R"). La funzione source() accetta il percorso di uno script R che verrà poi eseguito in background. Quindi se la vostra directory è organizzata in questo modo:

- working directory/
|-- main_script.R
|-- functions/
    |-- my_functions.R

Dove lo script my_functions.R è uno script dove sono dichiarare tutte le funzioni:

fun1 <- function(x){
  # do
}

fun2 <- function(x){
  # do
}

fun3 <- function(x){
  # do
}

...

Scrivendo all’inizio del nostro script principale source("functions/my_functions.R"), tutte le funzioni saranno caricate nel ambiente di lavoro.


  1. per una non semplice trattazione dell’argomento, il capitolo 7 del libro “Advanced R” di Hadley Wickam è un ottima risorsa.↩︎