10.3 Iterações avançadas
Agora que já vimos como substituir iterações de nível básico e de nível
intermediário com a família map(), podemos passar para os tipos mais obscuros
de laços. Cada item desta seção será mais denso do que os das passadas, por isso
encorajamos todos os leitores para que também leiam a documentação de cada função
aqui abordada.
10.3.1 Iterações com condicionais
Imagine que precisamos aplicar uma função somente em alguns elementos de um vetor.
Com um laço isso é uma tarefa fácil, mas com as funções da família map()
apresentadas até agora isso seria extremamente difícil. Veja o trecho de código
a seguir por exemplo:
dobra <- function(x) { x*2 }
obj <- 10:15
for (i in seq_along(obj)) {
if (obj[i] %% 2 == 1) { obj[i] <- dobra(obj[i]) }
else { obj[i] <- obj[i] }
}
obj## [1] 10 22 12 26 14 30
No exemplo acima, aplicamos a função dobra() apenas nos elementos ímpares do
vetor obj. Com o pacote purrr temos duas maneiras de fazer isso: com
map_if() ou map_at().
A primeira dessas funções aplica a função dada apenas quando um predicado é
TRUE. Esse predicado pode ser uma função ou uma fórmula (que serão aplicadas
em cada elemento da entrada e devem retornar TRUE ou FALSE). Infelizmente
a map_if() não funciona com sufixos, então devemos achatar o resultado:
eh_impar <- function(x) { x%%2 == 1 }
dobra <- function(x) { x*2 }
obj <- 10:15
map_if(obj, eh_impar, dobra) %>% flatten_dbl()## [1] 10 22 12 26 14 30
Com fórmulas poderíamos eliminar completamente a necessidade de funções declaradas:
obj <- 10:15
map_if(obj, ~.x%%2 == 1, ~.x*2) %>% flatten_dbl()## [1] 10 22 12 26 14 30
A segunda dessas funções é a irmã gêmea de map_if() e funciona de forma muito
semelhante. Para map_at() devemos passar um vetor de nomes ou índices onde a
função deve ser aplicada:
obj <- 10:15
map_at(obj, c(2, 4, 6), ~.x*2) %>% flatten_dbl()## [1] 10 22 12 26 14 30
10.3.2 Iterações com tabelas e funções
Duas funções menos utilizadas da família map() são map_dfc() e map_dfr(),
que equivalem a um map() seguido de um dplyr::bind_cols() ou de um
dplyr::bind_rows() respectivamente.
A maior utilidade dessas funções é quando temos uma tabela espalhada em muitos arquivos. Se elas estiverem divididas por grupos de colunas, podemos usar algo como
map_dfc(arquivos, readr::read_csv)e se elas estiverem divididas por grupos de linhas,map_dfr(arquivos, readr::read_csv)
Outro membro obscuro da família map() é a invoke_map(). Na verdade essa
função pode ser considerada um membro da família invoke(), mas vamos ver
que as semelhanças são muitas. Primeiramente, vamos demonstrar o que faz a
invoke() sozinha:
soma_ambos <- function(x, y) { x + y }
invoke(soma_ambos, list(x = 10, y = 15))## [1] 25
É fácil de ver que essa função recebe uma função e uma lista de argumentos para
usar em uma chamada desta. Agora generalizando esta lógica temos invoke_map(),
que chama uma mesma função com uma lista de listas de argumentos ou uma lista
de funções com uma lista de argumentos. A família invoke() também aceita os
sufixos como veremos a seguir:
soma_ambos <- function(x, y) { x + y }
soma_um <- function(x) { x + 1 }
soma_dois <- function(x) { x + 2 }
invoke_map_dbl(soma_ambos, list(list(x = 10, y = 15), list(x = 20, y = 25)))## [1] 25 45
invoke_map_dbl(list(soma_um, soma_dois), list(x = 10))## [1] 11 12
10.3.3 Redução e acúmulo
Outras funções simbólicas de programação funcional além da map() são reduce
e accumulate, que aplicam transformações em valores acumulados. Observe o laço
a seguir:
soma_ambos <- function(x, y) { x + y }
obj <- 10:15
for (i in 2:length(obj)) {
obj[i] <- soma_ambos(obj[i-1], obj[i])
}
obj## [1] 10 21 33 46 60 75
Essa soma cumulativa é bastante simples, mas não é difícil imaginar uma situação
em que um programador desavisado confunde um índice com o outro e o bug acaba
passando desapercebido. Para evitar esse tipo de situação, podemos utilizar
accumulate() (tanto com uma função quanto com uma fórmula):
soma_ambos <- function(x, y) { x + y }
obj <- 10:15
accumulate(obj, soma_ambos)## [1] 10 21 33 46 60 75
accumulate(obj, ~.x+.y)## [1] 10 21 33 46 60 75
Obs.: Nesse caso, os placeholders têm significados ligeiramente diferentes.
Aqui, .x é o valor acumulado e .y é o valor “atual” do objeto sendo iterado.
Se não quisermos o valor acumulado em cada passo da iteração, podemos usar
reduce():
obj <- 10:15
reduce(obj, ~.x+.y)## [1] 75
Para a nossa comodidade, essas duas funções também têm variedades paralelas
(accumulate2() e reduce2()), assim como variedades invertidas
accumulate_right() e reduce_right()).