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

## [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
## [1] 100
## [1] 100

Ci sono un po’ di cose da dire:

  1. 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).
  2. Se non diamo esplicitamente un return, la funzione restituisce l’ultima riga.
  3. Abbiamo chiamato dentro la nostra funzione * e +. In generale possiamo chiamare altre funzioni, anche da noi definite. In particolare possiamo richiamare la funzione stessa!
  4. 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:

## [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
## [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:

## [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