O que é a programação funcional?
Para os nossos objetivos, estas são algumas características fundamentais:
No entanto, o R não é uma linguagem funcional pura (ao contrário de, por exemplo, Haskell).
Vê o R como uma linguagem pragmática, que (cada vez mais) oferece capacidades funcionais quando estas são uma forma limpa e fácil de fazer o trabalho. Em geral, o objetivo é a análise de dados, e a programação é apenas um meio para esse fim.
A utilização de pipes |> para ligar funções é uma técnica cada vez mais comum em várias linguagens.
Em resumo:
Alguns programadores acham útil pronunciar o pipe como «then» ao rever código: faz o passo 1, depois faz o passo 2...
O exemplo abaixo ilustra a sintaxe.
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"
Este tipo de pipe foi adicionado ao Base R na v4.1.0, e podes vê-lo referido como o «pipe nativo».
O editor RStudio (entre outros) usa Ctrl-Shift-M como atalho de teclado para adicionar um pipe com espaços adequados.
Vimos anteriormente que muitas funções em R são «vetorizadas»: operam sobre vetores inteiros, sem necessidade de escrever ciclos explícitos.
As funções que usam apenas funções vetorizadas são elas próprias provavelmente vetorizadas automaticamente. Num exemplo trivial:
triple <- function(x) return(3 * x)
triple(1:10)
[1] 3 6 9 12 15 18 21 24 27 30
No entanto, funções mais complexas podem não aceitar entrada vetorial, por exemplo se contiverem ciclos ou blocos if-else.
Precisamos de uma forma mais geral de aplicar funções a vetores (semelhante a map() nalgumas outras linguagens).
Como o R tem uma grande variedade de estruturas de dados na linguagem base, também tem toda uma família de funções *apply() para operar sobre elas.
Mais tecnicamente, são frequentemente chamadas «funções de ordem superior» pelos programadores e «funcionais» pelos matemáticos, porque recebem funções como argumentos.
Por agora, considera lapply() e sapply().
lapply()O «List-apply» foi concebido para trabalhar com entradas de listas, mas os vetores serão silenciosamente convertidos em listas quando necessário.
O resultado é uma lista, pelo que pode ser necessário desempacotá-la para obter um vetor:
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
Os estudantes familiarizados com o Conceito Switch podem notar que podíamos, em alternativa, substituir o switch escalar pela função vetorizada recode_values() da biblioteca dplyr.
A vetorização é boa, e o R moderno fornece várias formas de a conseguir.
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()O «Simplified-apply» faz algo semelhante ao lapply, mas também tenta simplificar o valor devolvido: por exemplo, um vetor com nomes em vez de uma lista.
Dependendo do contexto, pode ser necessário usar unname():
sapply(v, h)
small large medium
6 10 8
sapply(v, h) |> unname()
#> [1] 6 10 8
purrr
lapply e sapply são simples, familiares para a maioria dos utilizadores de R, e ainda bastante populares para vetores e listas simples.
O Base R também tem muitas outras funções de aplicação. Fornecer uma alternativa mais simples e consistente para este tipo de confusão é um dos principais objetivos do Tidyverse.
Existe agora o pacote purrr, que fornece várias opções adicionais como map(), melhorando a consistência do design e a clareza das mensagens de erro.
Menos limitado pela compatibilidade retroativa do que o Base R, este pacote aproxima significativamente o R da programação funcional moderna encontrada noutras linguagens.
Há muitas funções diversificadas no purrr, que não se resumem facilmente.
Os recursos que ajudam a navegar na biblioteca incluem:
A função map() aplica uma função a cada elemento da entrada e devolve sempre uma lista.
Até aqui, isto parece o lapply(), apenas com outro nome, e talvez com melhores mensagens de erro.
No entanto, map() é apenas um membro de uma grande família.
Algumas perguntas a considerar ao escolher uma função:
map() devolve sempre uma lista, mas se quiseres um tipo específico de vetor atómico usa map_chr() para strings, map_int() para inteiros, etc.i nos nomes das funções, como em imap().map por map2 no nome, como em map2_dbl() para duas entradas (por exemplo, valores e pesos, para calcular uma média ponderada)pmap para uma lista de entradas paralelas, como em pmap_chr(). E sim, é possível paralelismo ao nível do hardware.map_if() para aplicar um filtro de predicado (booleano), map_at() para especificar critérios de índice posicional, head_while() para processar a entrada até um elemento falhar um predicado.modify() em vez de map, ou modify_tree() para aplicação recursiva.Estas funções têm os dados de entrada como primeiro argumento sempre que possível, para garantir que funcionam bem com pipes.
Além disso, o purrr é (deliberadamente) mais pedante do que o Base R. Se pedires algo específico, as funções devolvem exatamente isso ou param com uma mensagem de erro.
Duro, mas muito eficaz a reduzir bugs difíceis de encontrar.
As funções map têm geralmente um valor devolvido com o mesmo comprimento da entrada.
Algumas outras funções reduzem a dimensionalidade dos dados, por exemplo sum().
sum(1:100)
#> [1] 5050
No caso acima, convertemos um vetor de comprimento 100 num inteiro de comprimento 1. Veremos num futuro Conceito sobre matrizes e arrays que o conceito de redução de dimensionalidade é mais geral.
A função sum() é incorporada (tal como muitas outras funções estatísticas).
No entanto, precisamos de uma forma de aplicar funções arbitrárias de redução de dimensionalidade a uma estrutura de dados, usando alguma função de ordem superior equivalente a map().
Noutras linguagens, usam-se nomes de funções como fold, foldl e foldr. Em R, a função relevante é reduce(), que corresponde à conhecida (entre entusiastas de algoritmos) framework MapReduce.
O primeiro argumento é, como habitualmente, os dados de entrada. O segundo argumento é uma função de 2 argumentos: muitas vezes uma função anónima, mas possivelmente tão simples como um operador aritmético como +.
# 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
Importa se aplicamos a função a partir do início ou do fim da entrada?
Não com operadores associativos como + ou *.
Claramente, (1 + 2) + 3 == 1 + (2 + 3).
Em contrapartida, - e / são não associativos:
(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2
Para funções em que a direção importa, existe um argumento opcional .dir.
# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5
Tal como com map() e map2(), existe também uma função reduce2() para operar sobre dois vetores de entrada em paralelo.
Em R, gostamos de trabalhar com vetores. Imagina que, em vez de um único valor final, queres ver todos os passos intermédios.
Para isso, as funções correspondentes são accumulate() e accumulate2().
A sintaxe é semelhante a reduce(), com alguns argumentos opcionais extra para especificar o tipo de resultado.
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
Alguns tipos comuns de accumulate têm as suas próprias funções.
O Conceito Funções de Vetores abordou cumsum() e cumprod() no Base R, e a biblioteca dplyr tem cummean() para médias numéricas, cumall() e cumany() para vetores lógicos.
Frequentemente, queremos incluir (ou excluir) elementos da entrada que correspondem a um determinado predicado.
No Conceito Filtragem de Vetores vimos uma forma idiomática de fazer isto.
v <- 1:3
v[v >= 2]
#> [1] 2 3
Isto é muito usado no R tradicional, mas encaixa mal nos pipelines funcionais modernos.
Consequentemente, o purrr fornece um conjunto de funções de predicado, que seguem a convenção habitual de argumentos f(input, func, ...) para funcionar com pipes.
Os nomes são um pouco diferentes da maioria das outras linguagens (nomes como filter e drop já significam algo completamente diferente em R).
As funções correspondentes do purrr incluem keep() e discard(), além de várias outras.
Temos também every(), some() e none(), para substituir all() e any() ao devolver um único valor lógico.
Estamos a criar um clube de queijo, que vai usar ML para selecionar novos queijos a oferecer aos nossos clientes amantes de queijo, com base no seu historial e nos seus gostos.
Os novos membros têm de preencher um inquérito inicial para reunirmos alguns dados básicos com que começar. A partir daí, verificou-se que existe um subconjunto de clientes enfáticos cujas críticas carecem de subtileza. Como isto pode acabar por enviesar de forma irrevogável um algoritmo mais subtil, existe um algoritmo à parte, criado para responder às suas necessidades. Pedem-te que forneças algumas funções auxiliares para tratar os seus dados.
Embora possa haver diferentes formas de resolver as tarefas seguintes, cada uma pode ser resolvida com, no máximo, duas funções de ordem superior.
O sistema de avaliação tem uma base de cinco estrelas, que consiste simplesmente nos números inteiros 1:5.
Os clientes enfáticos só dão avaliações de 1 ou 5, e queremos saber se um cliente tem este comportamento.
Implementa all_15(), que recebe um vetor de avaliações e devolve true se todas as avaliações forem 1 ou 5, e false caso contrário.
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
Precisamos de associar o nome (ou ID) do cliente às suas avaliações.
Implementa name_customers(), que recebe um vetor de nomes e uma lista de avaliações e os "junta" numa lista de listas.
Os elementos internos das listas devem ter os nomes 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
Precisamos de separar os clientes mais enfáticos dos restantes.
Implementa emphatics(), que recebe os clientes e as avaliações.
Devolve uma lista restrita aos clientes que usam apenas avaliações de 1 ou 5 estrelas.`
# names and ratings as in the task 2 example
emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"
#> [[1]]$rating
#> [1] 1 1 5
Como os clientes enfáticos só usam avaliações de 1 e 5, será computacionalmente mais conveniente converter estas em 0 e 1.
Implementa to_binary(), que recebe um vetor de avaliações enfáticas.
Devolve avaliações binárias, em que 1 foi convertido em 0 e 5 foi convertido em 1.
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
Queres continuar a melhorar as recomendações para todos os clientes individualmente (não apenas para os enfáticos), à medida que compreendes melhor os seus gostos.
Implementa satisfactions(), que recebe um vetor de avaliações.
Devolve um vetor de avaliações médias acumuladas até esse momento.
Arredonda os resultados a 2 casas decimais.
Esperemos que os números continuem a crescer, para clientes felizes!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Inscreve-te no Exercism para aprenderes e dominares R com 21 conceitos111 exercícios, e mentoria humana real, tudo grátis.