6 Programmazione
Per passare dall’usare R come calcolatrice avanzata a vero e proprio strumento a supporto della statistica, abbiamo bisogno di imparare come controllare risultati intermedi in modo automatico e come scrivere funzioni, in modo da rendere il codice più modulare, più leggibile e più facile da mantenere.
6.1 Controlli
Iniziamo dai controlli, ossia quegli strumenti che ci permettono di diversificare
o ripetere certe parti del codice.
Abbiamo due tipi principali di controllo: le strutture di scelta come if
che ci
permettono di eseguire parti diverse di codice a seconda che una condizione sia
verificata o meno e i cicli come for
che ci permettono di ripetere l’esecuzione
di una parte di codice, al variare di un indice.
6.1.1 Scelte
I controlli di tipo scelta sono abbastanza intuitivi come struttura di base e
simili a quelli che si trovano in altri linguaggi di programmazione.
Il primo che incontriamo è lo statement if
: questo ha la seguente struttura,
if (condizione) codice1 else codice2
. Se la condizione è vera (ossia
è un oggetto di tipo Booleano a valore TRUE
), viene eseguito codice1
(che può
essere una singola riga oppure qualcosa di più complesso, racchiuso tra parentesi graffe).
Se la condizione è falsa, viene eseguito codice2
, se presente.
Infatti la parte else codice2
non è sintatticamente richiesta.
Vediamo alcuni esempi
## [1] "numero pari"
Possiamo mettere assieme più if else
## [1] 3
Dobbiamo fare attenzione alla condizione: deve essere un singolo valore Booleano o qualcosa che possa essere interpretato come tale (ad esempio 0 o 1). Questo significa che se mettiamo come condizione un vettore di valori logici, solo il primo verrà interpretato!
## Warning in if (c(TRUE, FALSE)) 78: the condition has length > 1 and only the
## first element will be used
## [1] 78
## Warning in if (c(FALSE, TRUE)) 78 else 19: the condition has length > 1 and only
## the first element will be used
## [1] 19
Tuttavia può succedere che abbiamo un vettore di valori logici e che vogliamo
agire su ogni componente in modo corrispondente. Per questo possiamo usare la
funzione ifelse(test, yes, no)
, che prende in pasto un vettore (o una matrice)
di valori logici test
e restituisce un oggetto della stessa forma i cui elementi
sono determinati da yes
o no
a seconda che la condizione corrispondente
all’elemento in test
sia vera o falsa. Vediamolo con un paio di esempi:
## [1] "0-" "+" "+" "0-" "0-" "0-" "+" "+" "+"
## [,1] [,2] [,3]
## [1,] "-" "-" "0+"
## [2,] "0+" "0+" "0+"
## [3,] "0+" "-" "0+"
Come in C esiste anche lo statement switch
per considerare in modo compatto
più alternative:
## [1] "E"
## [1] "E"
Anche switch
come if
prende un solo valore in input: se proviamo a passargliene
più di uno riceviamo un messaggio di errore.
6.1.2 Cicli
Per ripetere un comando (o una serie di comandi) per ogni elemento di un vettore
possiamo usare il ciclo for
, la cui struttura è for (elemento in vettore) codice
.
Per ogni elemento del vettore viene eseguito (una volta) codice
, poi viene
aggiornato l’elemento con il successivo, fino a esaurire tutti gli elementi del
vettore:
## [1] 2 0
## [1] 3 1
## [1] 4 2
## [1] 5 3
## [1] 6 4
## [1] 7 5
## [1] 8 6
Occorre prestare attenzione al fatto che l’oggetto elemento
(i
nell’esempio
precedente) viene aggiunto all’environment, eventualmente sovrascrivendo un
precedente oggetto col medesimo nome.
Possiamo interrompere un ciclo for
in due modi: mediante il comando break
,
che interrompe l’intero ciclo oppure mediante il comando next
che interrompe
l’iterazione in corso e passa alla successiva (ammesso che ci sia).
v <- c("alice","bob","eve","zelda")
for (i in v){
print(i)
if(i == "bob") next
print(paste("la potente", i, sep = " "))
if(i == "eve") break
}
## [1] "alice"
## [1] "la potente alice"
## [1] "bob"
## [1] "eve"
## [1] "la potente eve"
Il ciclo for
funziona bene quando sappiamo a priori su quali oggetti iterare
(abbiamo il vettore), in alternativa abbiamo altri due possibili cicli in R,
while
e repeat
.
Il ciclo while
ha la struttura while(condizione) codice
e ripete l’esecuzione
di codice
mentre la condizione rimane vera.
Il ciclo repeat
invece ha la struttura repeat(codice)
e ripete l’esecuzione
di codice
finché non viene interrotto, solitamente con un comando break
.
In generale tuttavia non è necessario usare i cicli while
o repeat
, ma i cicli
for
sono sufficienti. Addirittura vedremo più avanti che molto spesso possiamo
evitare anche i cicli for
(che non sono troppo efficienti da un punto di vista
computazionale) usando la vettorizzazione delle funzioni.
6.2 Funzioni in R
Nello svolgere compiti più complessi è molto utile suddividere le istruzioni in funzioni ossia blocchi di codice strutturati che prendono in pasto alcuni oggetti e ne restituiscono altri. Questo modo di affrontare le cose può essere ripetuto ricorsivamente, fino ad avere funzioni molto semplici (solitamente quelle di default).
Ci sono svariati motivi per usare le funzioni in un programma. I principali sono i seguenti: - avere codice facilmente riutilizzabile: se dobbiamo svolgere lo stesso compito più volte è inutile ripetere lo stesso blocco di codice più volte, meglio scriverlo una sola volta e riciclarlo - avere codice più leggibile: dando nomi “parlanti” alle funzioni possiamo scrivere codice molto più chiaro e più le funzioni sono concise, più sono leggibili - avere codice più facile da mantenere: se dobbiamo modificare qualcosa nel codice è più comodo (e meno propenso a errori) cambiarlo una sola volta all’interno di una funzione che non decine di volte in parti diverse del codice.
In R le funzioni sono composte di tre parti: gli argomenti (detti formals
), ossia
gli oggetti che passiamo come input alla funzione, il corpo (body
) della funzione,
che contiene le istruzioni da eseguire a partire dagli argomenti e l’ambiente
(environment
) ossia la struttura dati che associa nomi degli oggetti a valori.
In R le funzioni sono oggetti come gli altri, quindi possono essere assegnate a un nome.
In questo esempio abbiamo definito una funzione che ha come argomento x
e che
ne calcola una trasformazione affine (da cui il nome che le abbiamo dato),
restituendo (con return
) tale valore.
Avendo definito una funzione, vediamo come usarla. Questo è molto semplice (e in realtà l’abbiamo già fatto più volte con le funzioni di default di R): basta chiamare la funzione passandole tra parentesi i valori degli argomenti
## [1] 4
## [1] 31
# naturalmente la variabile che passiamo non deve avere necessariamente lo stesso nome dell'argomento
y <- 33
trasformazione(y)
## [1] 100
## [1] 100
Ci sono un po’ di cose da dire:
- Una funzione vede quello che le passiamo e non modifica al di fuori. Si parla di scope. Vede anche gli argomenti dell’ambiente che l’ha chiamata ma in seconda battuta, solo se non ne ha con quel nome al suo interno (come parametri o definite).
- Se non diamo esplicitamente un
return
, la funzione restituisce l’ultima riga. - Abbiamo chiamato dentro la nostra funzione
*
e+
. In generale possiamo chiamare altre funzioni, anche da noi definite. In particolare possiamo richiamare la funzione stessa! - Possiamo usare
;
al posto di un a capo per separare gli statement, ad esempio
## [1] 235
6.2.1 Osservazioni
Possiamo aggiungere commenti al nostro codice, (utile quando definiamo funzioni), facendoli precedere da #
.
Possiamo anche passare più argomenti e assegnare valori di default per essi, dichiarandoli nella definizione:
trasfParam <- function(x,t = 1){
# Non uso return, restituisce l'ultima riga di codice
3*x+t
}
trasfParam(33)
## [1] 100
## [1] 100
## [1] 1
Che succede se passiamo alla funzione più di un valore, nella fattispecie, un vettore?
## [1] 7 139 127 133 10 64 91 76 49 55
## [1] 10 11 12 13 14
## [1] 7 140 129 136 14 64 92 78 52 59
Sembra essere andato tutto bene, no? Ma cosa ci aspettavamo dall’ultima? Se ci aspettavamo i valori corrispondenti a tutte le possibili coppie \((z,t)\), abbiamo ottenuto qualcosa di diverso… Non c’è messaggio di errore, ma sappiamo che gli elementi del prodotto cartesiano dovrebbero essere \(10\cdot 5=50\), mentre l’ultimo elenco ne conteneva solo 10.
Quello che succede è il tipico comportamento di R: andando a sommare due vettori (\(3\cdot z\) e \(t\)) ricicla quello più corto. Se avessimo preso \(t\) di una lunghezza che non divideva \(z\), avremmo avuto un messaggio di avviso.
In generale, se vogliamo vettorializzare le nostre funzioni, abbiamo a disposizione il comando sapply
(se invece vogliamo “listizzarle” abbiamo lapply
e, più in generale, abbiamo apply
). Vediamo solamente un rapido esempio:
## [1] 0.5558173 -1.1107046 1.1186655 6.4243995 6.7264606 0.5304007
## [7] 3.8549612 4.6513536 3.4240670 0.9324012
## [1] 2.667452 -2.332114 4.355997 20.273198 21.179382 2.591202 12.564883
## [8] 14.954061 11.272201 3.797204
## $z
## [1] 2.710782
##
## $y
## [1] 9.132347
##
## $w
## [1] 39.24017
Quando si usano certe funzioni come apply
può aver senso considerare funzioni
anonime, ossia funzioni cui non viene assegnato un nome (perché non avremo
bisogno di accedervi nuovamente in un secondo momento).
## [1] 1.358519 -4.565778 2.104584 -21.999710 -25.065891 1.309877
## [7] -3.295842 -7.681030 -1.452034 1.927832
6.2.2 Esempi
Vediamo ancora qualche esempio con le funzioni, in particolare per quanto riguarda le chiamate ricorsive.
## [1] 3
## [1] 10
collatz2 <- function(x){
if(x != 1){
print(x)
x <- ifelse(x %% 2 == 0, x/2, 3*x+1)
return(collatz2(x))
}
else
return(x) ## posso trascurare le graffe qui perché si tratta di una sola riga!
}
collatz2(3)
## [1] 3
## [1] 10
## [1] 5
## [1] 16
## [1] 8
## [1] 4
## [1] 2
## [1] 1
Vediamo che possiamo chiamare una funzione ricorsivamente. In questi casi è bene assicurarsi che prima o poi si esca dalla ricorsione (in realtà dopo un po’ R ci butta fuori). Per questa funzione si congettura (congettura di Collatz) che termini per tutti gli interi positivi, anche se questo fatto non è stato ancora completamente dimostrato. Ma nel definire la funzione abbiamo dato per scontato che gli fossero passati interi maggiori di 0, cosa che non è controllata da nessuna parte. Aggiungiamo allora un controllo:
collatz2 <- function(x){
stopifnot(x - as.integer(x) == 0, x > 0)
if(x != 1){
print(x)
x <- ifelse(x %% 2 == 0, x/2, 3*x+1)
return(collatz2(x))
}
else
return(x)
}
# collatz2(0) # questo restituirebbe errore.
# collatz2(3.2) # anche questo dà errore, per stopifnot
collatz2(7)
## [1] 7
## [1] 22
## [1] 11
## [1] 34
## [1] 17
## [1] 52
## [1] 26
## [1] 13
## [1] 40
## [1] 20
## [1] 10
## [1] 5
## [1] 16
## [1] 8
## [1] 4
## [1] 2
## [1] 1