Che cos'è la programmazione funzionale?
Per i nostri scopi, ecco alcune caratteristiche fondamentali:
Tuttavia, R non è un linguaggio funzionale puro (a differenza, per esempio, di Haskell).
Considera R un linguaggio pragmatico, che (sempre più spesso) offre strumenti funzionali là dove questi rappresentano un modo pulito e semplice per portare a termine un lavoro. In genere l'obiettivo è l'analisi dei dati, e la programmazione è solo un mezzo per raggiungerlo.
L'uso delle pipe |> per collegare le funzioni è una tecnica sempre più comune in diversi linguaggi.
In breve:
Alcuni programmatori trovano utile leggere la pipe come «poi» quando rileggono il codice: fai il passo 1 POI fai il passo 2...
L'esempio qui sotto illustra la sintassi.
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
Questo tipo di pipe è stato aggiunto al Base R nella versione 4.1.0, e potresti vederla chiamata «native pipe».
L'editor RStudio (tra gli altri) usa Ctrl-Shift-M come scorciatoia da tastiera per inserire una pipe con gli spazi adatti.
Abbiamo visto in precedenza che molte funzioni in R sono «vettorizzate»: operano su interi vettori, senza bisogno di scrivere cicli espliciti.
Le funzioni che usano solo funzioni vettorizzate sono probabilmente anch'esse vettorizzate automaticamente. In un esempio banale:
triple <- function(x) return(3 * x)
triple(1:10)
[1] 3 6 9 12 15 18 21 24 27 30
Tuttavia, funzioni più complesse potrebbero non accettare input vettoriali, per esempio se contengono cicli o blocchi if-else.
Ci serve un modo più generale per applicare le funzioni ai vettori (simile a map() in alcuni altri linguaggi).
Poiché R ha una grande varietà di strutture dati nel linguaggio di base, ha anche un'intera famiglia di funzioni *apply() per operare su di esse.
Più tecnicamente, i programmatori le chiamano spesso «funzioni di ordine superiore» e i matematici «funzionali», perché prendono funzioni come argomenti.
Per ora, consideriamo lapply() e sapply().
lapply()«List-apply» è progettata per lavorare su input di tipo lista, ma i vettori verranno silenziosamente convertiti in liste quando necessario.
L'output è una lista, quindi potrebbe servire aprirla per ottenere un vettore:
h <- function(size) {
switch(size,
"small" = 6,
"medium" = 8,
"large" = 10
)
}
v <- c("small", "large", "medium")
h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) :
#> EXPR must be a length 1 vector
lapply(v, h) # output is a list
#> [[1]]
#> [1] 6
#> [[2]]
#> [1] 10
#> [[3]]
#> [1] 8
lapply(v, h) |> unlist() # output is a vector
#> [1] 6 10 8
Chi ha già familiarità con il concetto Switch noterà che potremmo in alternativa sostituire lo switch scalare con la versione vettorizzata di recode_values() della libreria dplyr.
La vettorizzazione è una buona cosa, e il R moderno offre diversi modi per ottenerla.
library(dplyr)
h <- function(size) {
recode_values(size,
"small" ~ 6,
"medium" ~ 8,
"large" ~ 10
)
}
v <- c("small", "large", "medium")
h(v)
#> [1] 6 10 8
sapply()«Simplified-apply» fa qualcosa di simile a lapply, ma cerca anche di semplificare il valore restituito: per esempio un vettore con nomi invece di una lista.
A seconda del contesto, potrebbe essere necessario usare unname():
sapply(v, h)
small large medium
6 10 8
sapply(v, h) |> unname()
#> [1] 6 10 8
purrr
lapply e sapply sono semplici, familiari alla maggior parte degli utenti di R, e ancora piuttosto popolari per vettori e liste semplici.
Il Base R ha anche molte altre funzioni apply. Offrire un'alternativa più semplice e più coerente a questo tipo di confusione è uno degli obiettivi principali del Tidyverse.
Ora c'è il pacchetto purrr, che offre diverse opzioni aggiuntive come map(), migliorando al tempo stesso la coerenza del design e la chiarezza dei messaggi di errore.
Meno vincolato del Base R dalla retrocompatibilità, questo pacchetto avvicina R in modo significativo alla programmazione funzionale moderna che si trova in altri linguaggi.
In purrr ci sono funzioni molteplici e diverse, difficili da riassumere in poche parole.
Tra le risorse utili per orientarsi nella libreria ci sono:
La funzione map() applica una funzione a ogni elemento dell'input e restituisce sempre una lista.
Fin qui sembra lapply(), solo ribattezzata, e magari con messaggi di errore migliori.
Tuttavia, map() è solo un membro di una grande famiglia.
Alcune domande da porsi quando si sceglie una funzione:
map() restituisce sempre una lista, ma se vuoi un tipo specifico di vettore atomico usa map_chr() per le stringhe, map_int() per gli interi, ecc.i ai nomi delle funzioni, come in imap().map con map2 nel nome, come in map2_dbl() per due input (ad esempio valori e pesi, per calcolare una media ponderata)pmap per una lista di input paralleli, come in pmap_chr(). E sì, il parallelismo a livello hardware è possibile.map_if() per applicare un filtro tramite predicato (booleano), a map_at() per specificare criteri sugli indici di posizione, a head_while() per elaborare l'input finché un elemento non fallisce un predicato.modify() invece di map, oppure modify_tree() per l'applicazione ricorsiva.Queste funzioni hanno i dati di input come primo argomento quando possibile, così da funzionare bene con le pipe.
Inoltre, purrr è (volutamente) più pignolo del Base R. Se chiedi qualcosa di specifico, le funzioni restituiranno esattamente quello, oppure si fermeranno con un messaggio di errore.
Severo, ma molto efficace nel ridurre bug difficili da individuare.
Le funzioni map in genere restituiscono un valore della stessa lunghezza dell'input.
Altre funzioni invece riducono la dimensionalità dei dati, per esempio sum().
sum(1:100)
#> [1] 5050
Nel caso precedente, convertiamo un vettore di lunghezza 100 in un intero di lunghezza 1. Vedremo in un prossimo concetto su matrici e array che il concetto di riduzione di dimensionalità è più generale.
La funzione sum() è integrata (come molte altre funzioni statistiche).
Tuttavia, ci serve un modo per applicare funzioni arbitrarie di riduzione della dimensionalità su una struttura dati, usando una funzione di ordine superiore equivalente a map().
In diversi altri linguaggi si usano nomi di funzione come fold, foldl e foldr. In R la funzione in questione è reduce(), che corrisponde al framework MapReduce, ben noto (agli appassionati di algoritmi).
Il primo argomento è, come al solito, i dati di input. Il secondo argomento è una funzione a 2 argomenti: spesso una funzione anonima, ma talvolta semplice come un operatore aritmetico, per esempio +.
# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050 # same as sum(1:100)
# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7
Fa differenza se applichiamo la funzione dall'inizio o dalla fine dell'input?
Non con operatori associativi come + o *.
Chiaramente, (1 + 2) + 3 == 1 + (2 + 3).
Al contrario, - e / non sono associativi:
(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2
Per le funzioni in cui la direzione conta, esiste un argomento facoltativo .dir.
# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5
Come per map() e map2(), esiste anche una funzione reduce2() per operare in parallelo su due vettori di input.
In R ci piace lavorare con i vettori. Supponi di voler vedere tutti i passaggi intermedi invece di un unico valore finale.
Per questo, le funzioni corrispondenti sono accumulate() e accumulate2().
La sintassi è simile a reduce(), con alcuni argomenti facoltativi aggiuntivi per specificare il tipo di output.
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
Alcuni tipi comuni di accumulo hanno funzioni dedicate.
Il concetto Vector Functions ha trattato cumsum() e cumprod() nel Base R, e la libreria dplyr ha cummean() per le medie numeriche, cumall() e cumany() per i vettori logici.
Spesso vogliamo includere (o escludere) gli elementi dell'input che soddisfano un certo predicato.
Nel concetto Vector Filtering abbiamo visto un modo idiomatico per farlo.
v <- 1:3
v[v >= 2]
#> [1] 2 3
È molto utilizzato nel R tradizionale, ma si integra male nelle moderne pipeline funzionali.
Di conseguenza, purrr fornisce un insieme di funzioni di predicato, che seguono la solita convenzione sugli argomenti f(input, func, ...) per funzionare con le pipe.
I nomi sono un po' diversi da quelli della maggior parte degli altri linguaggi (nomi come filter e drop hanno già un significato del tutto diverso in R).
Le funzioni purrr corrispondenti includono keep() e discard(), più diverse altre.
Abbiamo anche every(), some() e none(), che sostituiscono all() e any() nel restituire un singolo valore logico.
Stiamo avviando un club del formaggio, che userà il machine learning per selezionare nuovi formaggi da offrire ai nostri clienti amanti del formaggio, in base al loro storico e ai loro gusti.
I nuovi membri devono compilare un sondaggio iniziale, così possiamo raccogliere alcuni dati di base per iniziare. Da questo è emerso che esiste un sottoinsieme di clienti enfatici che mancano di sfumature nelle loro critiche. Poiché questo può finire per distorcere irreversibilmente un algoritmo più raffinato, è stato creato un algoritmo separato per gestire le loro esigenze. Ti viene chiesto di fornire alcune funzioni ausiliarie per gestire i loro dati.
Sebbene ci siano diversi modi per risolvere i compiti seguenti, ognuno può essere risolto con non più di due funzioni di ordine superiore.
Il sistema di valutazione si basa su cinque stelle e consiste semplicemente in numeri interi 1:5.
I clienti enfatici danno solo valutazioni di 1 o 5, e vogliamo sapere se un cliente mostra questo comportamento.
Implementa all_15() che prende un vettore di valutazioni e restituisce true se tutte le valutazioni sono 1 o 5, e false altrimenti.
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
Dobbiamo associare il nome del cliente (o l'ID) alle sue valutazioni.
Implementa name_customers(), che prende un vettore di nomi e un array di valutazioni, e li «zippa» in un array di array.
Gli elementi interni dell'array devono avere i nomi name e rating.
names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"
#> [[1]]$rating
#> [1] 2 3 5
#> [[2]]
#> [[2]]$name
#> [1] "c2"
#> [[2]]$rating
#> [1] 1 1 5
Dobbiamo separare i clienti più enfatici dagli altri.
Implementa emphatics() che prende clienti e valutazioni.
Restituisce un array limitato a coloro che usano solo valutazioni di 1 o 5 stelle.`
# names and ratings as in the task 2 example
emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"
#> [[1]]$rating
#> [1] 1 1 5
Poiché i clienti enfatici usano solo valutazioni 1 e 5, sarà più conveniente dal punto di vista computazionale cambiarle in 0 e 1.
Implementa to_binary() che prende un vettore di valutazioni enfatiche.
Restituisce valutazioni binarie, in cui 1 è stato cambiato in 0 e 5 è stato cambiato in 1.
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
Vuoi continuare a migliorare le raccomandazioni per tutti i singoli clienti (non solo gli enfatici) nel tempo, man mano che comprendi meglio i loro gusti.
Implementa satisfactions() che prende un vettore di valutazioni.
Restituisce un vettore di valutazioni medie cumulative fino a questo punto.
Arrotonda i risultati a 2 cifre decimali.
Speriamo che i numeri continuino a crescere, per clienti felici!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Iscriviti a Exercism per imparare e padroneggiare R con 21 concetti111 esercizi e il mentoring di persone reali, tutto gratis.