O que é programação funcional?
Essa é uma pergunta ampla, que merece uma resposta longa (a Wikipédia oferece uma, o Advanced R oferece outra).
Para os nossos propósitos, estas são algumas características-chave:
No entanto, R não é uma linguagem funcional pura (ao contrário, por exemplo, de Haskell).
Veja o R como uma linguagem pragmática, que (cada vez mais) oferece recursos funcionais quando 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 R base na v4.1.0, e você pode vê-lo chamado de "pipe nativo".
Antes, o encadeamento era feito com %>%, do pacote magrittr (o nome é um trocadilho para os amantes de arte).
O "pipe do magrittr" ainda está disponível, mas os pipes nativos dispensam a necessidade de um pacote externo, e a implementação é mais simples em alguns detalhes.
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 serã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, elas são chamadas com frequência de "funções de ordem superior" pelos programadores e de "funcionais" pelos matemáticos, porque recebem funções como argumentos.
Por ora, considere lapply() e sapply().
lapply()O "List-apply" foi projetado para trabalhar com listas como entrada, mas vetores serão convertidos silenciosamente 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 Switch podem notar que poderíamos, como alternativa, substituir o switch escalar pela recode_values() vetorizada 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()O "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, o uso de unname() pode ser necessário:
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 R base também tem várias outras funções apply:
vapply()é parecida com sapply(), mas também tem um parâmetro obrigatório para especificar o formato da saída.apply() para matrizes e arrays de dimensões superiores.mapply() para vários argumentos de lista ou vetor em paralelo.tapply() é mais difícil de explicar, mas funciona com operações de agrupamento e arrays irregulares.vectorize() é uma função um tanto misteriosa, que parece ser um wrapper para mapply().Já chega!
Oferecer um substituto mais simples e mais consistente para esse tipo de confusão é um dos principais objetivos do Tidyverse.
Agora existe 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 retroativa do que o R base, esse pacote aproxima bastante o R da programação funcional moderna encontrada em outras linguagens.
Há muitas funções diversas no purrr, que desafiam um resumo fácil.
Entre os recursos que ajudam a navegar pela biblioteca estão:
A biblioteca purrr opera quase inteiramente sobre dados unidimensionais: vetores e listas.
Veremos em um conceito posterior que a biblioteca dplyr oferece funcionalidade equivalente para dataframes 2D.
A função map() aplica uma função a cada elemento da entrada e sempre retorna uma lista.
Até aqui, isso soa como lapply(), só que renomeada, e talvez com mensagens de erro melhores.
No entanto, map() é apenas um membro de uma grande família.
Algumas perguntas para pensar 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 no início dos 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, o paralelismo em nível de hardware é possível.map_if() para aplicar um filtro de predicado (boolean), map_at() para especificar critérios de índice posicional, 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 exigente que o R base. 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 de redução de dimensão arbitrárias em 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(), correspondente ao framework MapReduce, bem conhecido (entre os entusiastas de algoritmos).
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 desde o início ou desde o 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, há um argumento opcional .dir.
# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5
Assim como com map() e map2(), também há uma 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 a de 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 suas próprias funções.
O Conceito de Funções Vetoriais discutiu cumsum() e cumprod() no R base, e a biblioteca dplyr tem cummean() para médias numéricas, cumall() e cumany() para vetores lógicos.
Com frequência, queremos incluir (ou excluir) elementos da entrada que correspondam a algum predicado.
No Conceito de Filtragem de Vetores 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 oferece 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.
A maioria das linguagens funcionais usa a recursão como alternativa preferida aos laços.
R tem outras opções, muitas vezes melhores: funções vetorizadas e funções de ordem superior.
No entanto, a recursão é suportada em R e pode ser útil em contextos como percorrer estruturas de árvore.
A profundidade máxima de recursão é 5000 por padrão, o que é relativamente generoso (o Python usa 1000 por padrão). É possível alterar o padrão.
A otimização de chamada de cauda não é padrão em R, embora algumas soluções alternativas tenham sido desenvolvidas.
A tradição exige que mostremos um exemplo de fatorial aqui, como o exemplo clássico de definição recursiva.
fact <- function(n) ifelse(n == 0, 1, n * fact(n - 1))
fact(5)
#> [1] 120
# some more idiomatic R:
factorial(5)
#> [1] 120
prod(1:5)
#> [1] 120
Você pode usar recursão em R, e às vezes ela é valiosa, mas muitas vezes há abordagens mais simples.