O que é programação funcional?
Para os nossos propósitos, estas são algumas características fundamentais:
No entanto, R não é uma linguagem funcional pura (ao contrário de, por exemplo, Haskell).
Encare R como uma linguagem pragmática, que (cada vez mais) oferece recursos funcionais onde eles são uma forma limpa e fácil de fazer o trabalho. Em geral, a análise de dados é o objetivo, e a programação é apenas um meio para alcançá-lo.
O uso de pipes |> para conectar funções é uma técnica cada vez mais comum em várias linguagens.
Em resumo:
Alguns programadores acham útil pronunciar o pipe como "então" ao revisar código: faça o passo 1 ENTÃO faça 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"
Esse tipo de pipe foi adicionado ao Base R na versão 4.1.0, e você pode vê-lo referido como "pipe nativo".
O editor RStudio (entre outros) usa Ctrl-Shift-M como atalho de teclado para adicionar um pipe com os espaços adequados.
Vimos antes que muitas funções em R são "vetorizadas": elas operam sobre vetores inteiros, sem precisar escrever laços explícitos.
Funções que usam apenas funções vetorizadas provavelmente também são vetorizadas automaticamente. Em um 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 laços ou blocos if-else.
Precisamos de uma forma mais geral de aplicar funções a vetores (parecida com map() em algumas outras linguagens).
Como R tem uma grande variedade de estruturas de dados na linguagem base, ele também tem toda uma família de funções *apply() para operar sobre elas.
Mais tecnicamente, programadores costumam chamá-las de "funções de ordem superior" e matemáticos de "funcionais", porque elas recebem funções como argumentos.
Por ora, considere lapply() e sapply().
lapply()"List-apply" foi projetada para trabalhar com listas como entrada, mas vetores serão silenciosamente convertidos em listas quando necessário.
A saída é uma lista, então pode ser preciso 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
Estudantes familiarizados com o Conceito de Switch podem notar que poderíamos, alternativamente, substituir o switch escalar pela função vetorizada recode_values() da biblioteca dplyr.
A vetorização é boa, e o R moderno oferece várias formas de alcançá-la.
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" faz algo parecido com lapply, mas também tenta simplificar o valor de retorno: por exemplo,
um vetor nomeado 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 usuários de R e ainda bastante populares para vetores e listas simples.
O Base R também tem muitas outras funções de aplicação. Oferecer uma alternativa mais simples e mais consistente para esse tipo de confusão é um dos principais objetivos do Tidyverse.
Existe agora o pacote purrr, que oferece várias opções adicionais, como map(), além de melhorar a consistência do design e a clareza das mensagens de erro.
Menos limitado pela compatibilidade com versões anteriores do que o Base R, esse pacote aproxima bastante o R da programação funcional moderna encontrada em outras linguagens.
Há muitas funções diversas no purrr, que não se resumem facilmente.
Alguns recursos para ajudar a explorar a biblioteca incluem:
A função map() aplica uma função a cada elemento da entrada e sempre retorna uma lista.
Até aqui, isso soa como lapply(), apenas renomeada e, talvez, com mensagens de erro melhores.
No entanto, map() é apenas um membro de uma família grande.
Algumas perguntas para considerar ao escolher uma função:
map() sempre retorna uma lista, mas se você quer um tipo específico de vetor atômico, use map_chr() para strings, map_int() para inteiros etc.i, como 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, o paralelismo em nível de hardware é possível.map_if() para aplicar um filtro de predicado (booleano), map_at() para especificar critérios de índice de posição, head_while() para processar a entrada até que um elemento falhe em um predicado.modify() em vez de map, ou modify_tree() para aplicação recursiva.Essas funções têm os dados de entrada como primeiro argumento sempre que possível, para garantir que funcionem bem com pipes.
Além disso, o purrr é (deliberadamente) mais rígido do que o Base R. Se você pedir algo específico, as funções retornarão exatamente aquilo ou pararão com uma mensagem de erro.
Duro, mas muito eficaz para reduzir bugs difíceis de encontrar.
As funções map geralmente têm um valor de retorno do mesmo comprimento que a 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 em um inteiro de comprimento 1. Veremos em um Conceito futuro sobre matrizes e arrays que o conceito de redução de dimensionalidade é mais geral.
A função sum() é nativa (assim como muitas outras funções estatísticas).
No entanto, precisamos de uma forma de aplicar funções arbitrárias de redução de dimensionalidade sobre uma estrutura de dados, usando alguma função de ordem superior equivalente a map().
Em várias outras linguagens, são usados nomes de funções como fold, foldl e foldr. Em R, a função relevante é reduce(), que corresponde ao bem conhecido (entre entusiastas de algoritmos) framework MapReduce.
O primeiro argumento é, como de costume, os dados de entrada. O segundo argumento é uma função de 2 argumentos: muitas vezes uma função anônima, mas possivelmente algo tão simples quanto 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
Faz diferença se aplicamos a função do início ou do fim da entrada?
Não com operadores associativos como + ou *.
Claramente, (1 + 2) + 3 == 1 + (2 + 3).
Em contraste, - e / não sã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
Assim como em map() e map2(), também existe a função reduce2() para operar sobre dois vetores de entrada em paralelo.
Em R, gostamos de trabalhar com vetores. Suponha que, em vez de um único valor final, você queira ver todos os passos intermediários.
Para isso, as funções correspondentes são accumulate() e accumulate2().
A sintaxe é parecida com reduce(), com alguns argumentos opcionais extras para especificar o tipo de saída.
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
Alguns tipos comuns de accumulate têm funções próprias.
O Conceito de Funções Vetoriais discutiu 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 correspondam a algum predicado.
No Conceito de Filtragem Vetorial vimos uma forma idiomática de fazer isso.
v <- 1:3
v[v >= 2]
#> [1] 2 3
Isso é muito usado no R tradicional, mas se encaixa mal nos pipelines funcionais modernos.
Por isso, o purrr fornece um conjunto de funções de predicado, que seguem a convenção usual 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.
Também temos every(), some() e none(), para substituir all() e any() ao retornar um único valor lógico.
Estamos abrindo um clube do queijo, que vai usar aprendizado de máquina (ML) para selecionar novos queijos a serem oferecidos aos nossos clientes amantes de queijo, com base no histórico e nos gostos de cada um.
Os novos membros precisam responder a um questionário inicial para que possamos reunir alguns dados básicos para começar. Com isso, descobrimos que existe um subconjunto de clientes enfáticos que não têm nuance nas críticas. Como isso pode acabar enviesando de forma irreversível um algoritmo mais sutil, há um algoritmo separado para atender às necessidades deles. Pedimos que você forneça algumas funções auxiliares para lidar com os dados deles.
Embora possa haver maneiras diferentes de resolver as tarefas a seguir, cada uma pode ser resolvida com no máximo duas funções de ordem superior.
O sistema de avaliação tem 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 apresenta esse comportamento.
Implemente all_15(), que recebe um vetor de avaliações e retorna 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 associar o nome (ou ID) do cliente às suas avaliações.
Implemente name_customers(), que recebe um vetor de nomes e uma lista de avaliações e os "emparelha" em uma lista de listas.
Os elementos das listas internas 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 separar os clientes mais enfáticos dos demais.
Implemente emphatics(), que recebe clientes e avaliações.
Retorna uma lista restrita àqueles que só dão 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 1 e 5, será mais conveniente do ponto de vista computacional mudá-las para 0 e 1.
Implemente to_binary(), que recebe um vetor de avaliações enfáticas.
Retorna avaliações binárias, em que 1 foi mudado para 0 e 5 foi mudado para 1.
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
Você quer continuar melhorando as recomendações para cada cliente individual (não só para os enfáticos) com o tempo, à medida que entende melhor o gosto deles.
Implemente satisfactions(), que recebe um vetor de avaliações.
Retorna um vetor de médias acumuladas até esse ponto.
Arredonde os resultados para 2 casas decimais.
Tomara que os números continuem crescendo, para clientes felizes!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Crie sua conta no Exercism para aprender e dominar R com 21 conceitos111 exercícios e mentoria humana de verdade, tudo de graça.