Módulo 5 Programação funcional (purrr)

Script da aula do Módulo IV abaixo. Download Script do módulo 5

Script para acessar informações da basedosdados. Download Base dos dados - exemplo

Programação Funcional (PF) é um estilo de programar que alguns profissionais desenvolveram utilizando a ideia de tratar a programação como funções matemáticas. Esta forma de programar envolve um paradigma de programação com o qual a maior parte dos estatísticos não está familiarizada e muitos tutoriais de R costuma ignorar esta técnica por ela não estar diretamente envolvida com manipulação e visualização de dados.

A forma como o código é organizado, utilizando programação funcional, nos permite criar códigos mais concisos e pipeáveis, que facilita o trabalho de depurar, estender e documentar o trabalho que está sendo desenvolvido. Além disso, códigos funcionais geralmente são paralelizáveis, permitindo que tratemos problemas muito grandes com poucas modificações.

R não é uma linguagem de programação funcional pura, mas é possível, no R base, escrever código usando o paradigma de programação funcional, entretanto é necessário algum esforço. O pacote purrr foi desenvolvido com o objetido de fornecer recursos básicos de programação funcional no R com algumas funções muito interessantes.

Para instalar e carregar o purrr basta executar o código a seguir.

install.packages(c("Rtools", "devtools", "purrr"))
devtools::install_github("jennybc/repurrrsive")
library(tidyverse) # Ou
library(purrr)
library(repurrrsive)

5.1 Iterações básicas

Em vez de usar loops, as linguagens de programação puramente funcionais usam funções que alcançam o mesmo resultado. A primeira família de funções do purrr que veremos também é a mais útil e extensível. As funções map() são muito consistentes e, portanto, mais fáceis de usar. São quase como substitutas para laços for e abstraem a iteração em apenas uma linha. Veja esse exemplo de laço usando for:

soma_um <- function(x) { x + 1 }
obj <- 10:15
for (i in seq_along(obj)) {
  obj[i] <- soma_um(obj[i])
}
obj
## [1] 11 12 13 14 15 16
sw_people[[1]]
## $name
## [1] "Luke Skywalker"
## 
## $height
## [1] "172"
## 
## $mass
## [1] "77"
## 
## $hair_color
## [1] "blond"
## 
## $skin_color
## [1] "fair"
## 
## $eye_color
## [1] "blue"
## 
## $birth_year
## [1] "19BBY"
## 
## $gender
## [1] "male"
## 
## $homeworld
## [1] "http://swapi.co/api/planets/1/"
## 
## $films
## [1] "http://swapi.co/api/films/6/" "http://swapi.co/api/films/3/" "http://swapi.co/api/films/2/" "http://swapi.co/api/films/1/" "http://swapi.co/api/films/7/"
## 
## $species
## [1] "http://swapi.co/api/species/1/"
## 
## $vehicles
## [1] "http://swapi.co/api/vehicles/14/" "http://swapi.co/api/vehicles/30/"
## 
## $starships
## [1] "http://swapi.co/api/starships/12/" "http://swapi.co/api/starships/22/"
## 
## $created
## [1] "2014-12-09T13:50:51.644000Z"
## 
## $edited
## [1] "2014-12-20T21:17:56.891000Z"
## 
## $url
## [1] "http://swapi.co/api/people/1/"

sw_people[1]
## [[1]]
## [[1]]$name
## [1] "Luke Skywalker"
## 
## [[1]]$height
## [1] "172"
## 
## [[1]]$mass
## [1] "77"
## 
## [[1]]$hair_color
## [1] "blond"
## 
## [[1]]$skin_color
## [1] "fair"
## 
## [[1]]$eye_color
## [1] "blue"
## 
## [[1]]$birth_year
## [1] "19BBY"
## 
## [[1]]$gender
## [1] "male"
## 
## [[1]]$homeworld
## [1] "http://swapi.co/api/planets/1/"
## 
## [[1]]$films
## [1] "http://swapi.co/api/films/6/" "http://swapi.co/api/films/3/" "http://swapi.co/api/films/2/" "http://swapi.co/api/films/1/" "http://swapi.co/api/films/7/"
## 
## [[1]]$species
## [1] "http://swapi.co/api/species/1/"
## 
## [[1]]$vehicles
## [1] "http://swapi.co/api/vehicles/14/" "http://swapi.co/api/vehicles/30/"
## 
## [[1]]$starships
## [1] "http://swapi.co/api/starships/12/" "http://swapi.co/api/starships/22/"
## 
## [[1]]$created
## [1] "2014-12-09T13:50:51.644000Z"
## 
## [[1]]$edited
## [1] "2014-12-20T21:17:56.891000Z"
## 
## [[1]]$url
## [1] "http://swapi.co/api/people/1/"

map(sw_people, ~length(.x$starships))
## [[1]]
## [1] 2
## 
## [[2]]
## [1] 0
## 
## [[3]]
## [1] 0
## 
## [[4]]
## [1] 1
## 
## [[5]]
## [1] 0
## 
## [[6]]
## [1] 0
## 
## [[7]]
## [1] 0
## 
## [[8]]
## [1] 0
## 
## [[9]]
## [1] 1
## 
## [[10]]
## [1] 5
## 
## [[11]]
## [1] 3
## 
## [[12]]
## [1] 0
## 
## [[13]]
## [1] 2
## 
## [[14]]
## [1] 2
## 
## [[15]]
## [1] 0
## 
## [[16]]
## [1] 0
## 
## [[17]]
## [1] 1
## 
## [[18]]
## [1] 1
## 
## [[19]]
## [1] 0
## 
## [[20]]
## [1] 0
## 
## [[21]]
## [1] 1
## 
## [[22]]
## [1] 0
## 
## [[23]]
## [1] 0
## 
## [[24]]
## [1] 1
## 
## [[25]]
## [1] 0
## 
## [[26]]
## [1] 0
## 
## [[27]]
## [1] 0
## 
## [[28]]
## [1] 1
## 
## [[29]]
## [1] 0
## 
## [[30]]
## [1] 1
## 
## [[31]]
## [1] 0
## 
## [[32]]
## [1] 0
## 
## [[33]]
## [1] 0
## 
## [[34]]
## [1] 0
## 
## [[35]]
## [1] 0
## 
## [[36]]
## [1] 0
## 
## [[37]]
## [1] 1
## 
## [[38]]
## [1] 0
## 
## [[39]]
## [1] 0
## 
## [[40]]
## [1] 0
## 
## [[41]]
## [1] 0
## 
## [[42]]
## [1] 1
## 
## [[43]]
## [1] 0
## 
## [[44]]
## [1] 0
## 
## [[45]]
## [1] 0
## 
## [[46]]
## [1] 0
## 
## [[47]]
## [1] 0
## 
## [[48]]
## [1] 0
## 
## [[49]]
## [1] 0
## 
## [[50]]
## [1] 0
## 
## [[51]]
## [1] 0
## 
## [[52]]
## [1] 0
## 
## [[53]]
## [1] 0
## 
## [[54]]
## [1] 0
## 
## [[55]]
## [1] 1
## 
## [[56]]
## [1] 0
## 
## [[57]]
## [1] 1
## 
## [[58]]
## [1] 0
## 
## [[59]]
## [1] 0
## 
## [[60]]
## [1] 0
## 
## [[61]]
## [1] 0
## 
## [[62]]
## [1] 0
## 
## [[63]]
## [1] 0
## 
## [[64]]
## [1] 0
## 
## [[65]]
## [1] 0
## 
## [[66]]
## [1] 0
## 
## [[67]]
## [1] 0
## 
## [[68]]
## [1] 0
## 
## [[69]]
## [1] 0
## 
## [[70]]
## [1] 0
## 
## [[71]]
## [1] 0
## 
## [[72]]
## [1] 0
## 
## [[73]]
## [1] 0
## 
## [[74]]
## [1] 0
## 
## [[75]]
## [1] 0
## 
## [[76]]
## [1] 0
## 
## [[77]]
## [1] 1
## 
## [[78]]
## [1] 0
## 
## [[79]]
## [1] 0
## 
## [[80]]
## [1] 0
## 
## [[81]]
## [1] 0
## 
## [[82]]
## [1] 0
## 
## [[83]]
## [1] 0
## 
## [[84]]
## [1] 1
## 
## [[85]]
## [1] 0
## 
## [[86]]
## [1] 0
## 
## [[87]]
## [1] 3

O código acima mostra como é possível extrair informações de forma sequencial sem a necessidade de utilização de laços. A PF, por meio da função map(), permite aplicar uma função ou uma fórmula desejada em cada elemento do objeto dado, dispensando a necessidade de declararmos um iterador auxiliar (i).

Para utilizar fórmulas dentro da map(), basta colocar um til (~) antes da função que será chamada, conforme mostrado no exemplo anterior. Feito isso, podemos utilizar o placeholder .x para indicar onde deve ser colocado cada elemento do objeto.

soma_um <- function(x) { x + 1 }
obj <- 10:15
obj <- map(obj, soma_um)
obj
## [[1]]
## [1] 11
## 
## [[2]]
## [1] 12
## 
## [[3]]
## [1] 13
## 
## [[4]]
## [1] 14
## 
## [[5]]
## [1] 15
## 
## [[6]]
## [1] 16

Você deve ter percebido que o resultado desta última execução não foi exatamente igual à quando utilizamos o loop, isto aconteceu porque a função map() tenta ser mais genérica, retornando por padrão uma lista com um elemento para cada saída.

Mas é possível “achatar” o resultado informando qual será o seu tipo. Isso pode ser feito por meio da utilização das funções pertencentes à família map(): map_chr() (para strings), map_dbl() (para números reais), map_int() (para números inteiros) e map_lgl() (para booleanos).

map_int(sw_people, ~ length(.x[["starships"]]))
##  [1] 2 0 0 1 0 0 0 0 1 5 3 0 2 2 0 0 1 1 0 0 1 0 0 1 0 0 0 1 0 1 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
## [80] 0 0 0 0 1 0 0 3

map_chr(sw_people, ~ .x[["hair_color"]])
##  [1] "blond"         "n/a"           "n/a"           "none"          "brown"         "brown, grey"   "brown"         "n/a"           "black"        
## [10] "auburn, white" "blond"         "auburn, grey"  "brown"         "brown"         "n/a"           "n/a"           "brown"         "brown"        
## [19] "white"         "grey"          "black"         "none"          "none"          "black"         "none"          "none"          "auburn"       
## [28] "brown"         "brown"         "none"          "brown"         "none"          "blond"         "none"          "none"          "none"         
## [37] "brown"         "black"         "none"          "black"         "black"         "none"          "none"          "none"          "none"         
## [46] "none"          "none"          "none"          "white"         "none"          "black"         "none"          "none"          "none"         
## [55] "none"          "none"          "black"         "brown"         "brown"         "none"          "black"         "black"         "brown"        
## [64] "white"         "black"         "black"         "blonde"        "none"          "none"          "none"          "white"         "none"         
## [73] "none"          "none"          "none"          "none"          "none"          "brown"         "brown"         "none"          "none"         
## [82] "black"         "brown"         "brown"         "none"          "unknown"       "brown"

map_lgl(sw_people, ~ .x[["gender"]] == "male")
##  [1]  TRUE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE
## [27] FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE
## [53]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE FALSE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE
## [79]  TRUE FALSE  TRUE  TRUE FALSE  TRUE FALSE FALSE FALSE

map(sw_people, ~ .x[["mass"]])
## [[1]]
## [1] "77"
## 
## [[2]]
## [1] "75"
## 
## [[3]]
## [1] "32"
## 
## [[4]]
## [1] "136"
## 
## [[5]]
## [1] "49"
## 
## [[6]]
## [1] "120"
## 
## [[7]]
## [1] "75"
## 
## [[8]]
## [1] "32"
## 
## [[9]]
## [1] "84"
## 
## [[10]]
## [1] "77"
## 
## [[11]]
## [1] "84"
## 
## [[12]]
## [1] "unknown"
## 
## [[13]]
## [1] "112"
## 
## [[14]]
## [1] "80"
## 
## [[15]]
## [1] "74"
## 
## [[16]]
## [1] "1,358"
## 
## [[17]]
## [1] "77"
## 
## [[18]]
## [1] "110"
## 
## [[19]]
## [1] "17"
## 
## [[20]]
## [1] "75"
## 
## [[21]]
## [1] "78.2"
## 
## [[22]]
## [1] "140"
## 
## [[23]]
## [1] "113"
## 
## [[24]]
## [1] "79"
## 
## [[25]]
## [1] "79"
## 
## [[26]]
## [1] "83"
## 
## [[27]]
## [1] "unknown"
## 
## [[28]]
## [1] "unknown"
## 
## [[29]]
## [1] "20"
## 
## [[30]]
## [1] "68"
## 
## [[31]]
## [1] "89"
## 
## [[32]]
## [1] "90"
## 
## [[33]]
## [1] "unknown"
## 
## [[34]]
## [1] "66"
## 
## [[35]]
## [1] "82"
## 
## [[36]]
## [1] "unknown"
## 
## [[37]]
## [1] "unknown"
## 
## [[38]]
## [1] "unknown"
## 
## [[39]]
## [1] "40"
## 
## [[40]]
## [1] "unknown"
## 
## [[41]]
## [1] "unknown"
## 
## [[42]]
## [1] "80"
## 
## [[43]]
## [1] "unknown"
## 
## [[44]]
## [1] "55"
## 
## [[45]]
## [1] "45"
## 
## [[46]]
## [1] "unknown"
## 
## [[47]]
## [1] "65"
## 
## [[48]]
## [1] "84"
## 
## [[49]]
## [1] "82"
## 
## [[50]]
## [1] "87"
## 
## [[51]]
## [1] "unknown"
## 
## [[52]]
## [1] "50"
## 
## [[53]]
## [1] "unknown"
## 
## [[54]]
## [1] "unknown"
## 
## [[55]]
## [1] "80"
## 
## [[56]]
## [1] "unknown"
## 
## [[57]]
## [1] "85"
## 
## [[58]]
## [1] "unknown"
## 
## [[59]]
## [1] "unknown"
## 
## [[60]]
## [1] "80"
## 
## [[61]]
## [1] "56.2"
## 
## [[62]]
## [1] "50"
## 
## [[63]]
## [1] "unknown"
## 
## [[64]]
## [1] "80"
## 
## [[65]]
## [1] "unknown"
## 
## [[66]]
## [1] "79"
## 
## [[67]]
## [1] "55"
## 
## [[68]]
## [1] "102"
## 
## [[69]]
## [1] "88"
## 
## [[70]]
## [1] "unknown"
## 
## [[71]]
## [1] "unknown"
## 
## [[72]]
## [1] "15"
## 
## [[73]]
## [1] "unknown"
## 
## [[74]]
## [1] "48"
## 
## [[75]]
## [1] "unknown"
## 
## [[76]]
## [1] "57"
## 
## [[77]]
## [1] "159"
## 
## [[78]]
## [1] "136"
## 
## [[79]]
## [1] "79"
## 
## [[80]]
## [1] "48"
## 
## [[81]]
## [1] "80"
## 
## [[82]]
## [1] "unknown"
## 
## [[83]]
## [1] "unknown"
## 
## [[84]]
## [1] "unknown"
## 
## [[85]]
## [1] "unknown"
## 
## [[86]]
## [1] "unknown"
## 
## [[87]]
## [1] "45"

map_dbl(sw_people, ~ as.numeric(.x[["mass"]]))
## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção

## Warning in .f(.x[[i]], ...): NAs introduzidos por coerção
##  [1]  77.0  75.0  32.0 136.0  49.0 120.0  75.0  32.0  84.0  77.0  84.0    NA 112.0  80.0  74.0    NA  77.0 110.0  17.0  75.0  78.2 140.0 113.0  79.0  79.0  83.0
## [27]    NA    NA  20.0  68.0  89.0  90.0    NA  66.0  82.0    NA    NA    NA  40.0    NA    NA  80.0    NA  55.0  45.0    NA  65.0  84.0  82.0  87.0    NA  50.0
## [53]    NA    NA  80.0    NA  85.0    NA    NA  80.0  56.2  50.0    NA  80.0    NA  79.0  55.0 102.0  88.0    NA    NA  15.0    NA  48.0    NA  57.0 159.0 136.0
## [79]  79.0  48.0  80.0    NA    NA    NA    NA    NA  45.0

map_chr(sw_people, ~ .x[["mass"]]) %>%
  readr::parse_number(na = "unknown")
##  [1]   77.0   75.0   32.0  136.0   49.0  120.0   75.0   32.0   84.0   77.0   84.0     NA  112.0   80.0   74.0 1358.0   77.0  110.0   17.0   75.0   78.2  140.0
## [23]  113.0   79.0   79.0   83.0     NA     NA   20.0   68.0   89.0   90.0     NA   66.0   82.0     NA     NA     NA   40.0     NA     NA   80.0     NA   55.0
## [45]   45.0     NA   65.0   84.0   82.0   87.0     NA   50.0     NA     NA   80.0     NA   85.0     NA     NA   80.0   56.2   50.0     NA   80.0     NA   79.0
## [67]   55.0  102.0   88.0     NA     NA   15.0     NA   48.0     NA   57.0  159.0  136.0   79.0   48.0   80.0     NA     NA     NA     NA     NA   45.0

O purrr também nos fornece outra ferramenta interessante para achatar listas: a família flatten(). No fundo, map_chr() é quase um atalho para map() %>% flatten_chr()! Algo bastante útil da família map() é a possibilidade de passar argumentos fixos para a função que será aplicada. A primeira forma de fazer isso envolve fórmulas:

A outra forma de passar argumentos para a função é através das reticências da map(). Desta maneira precisamos apenas dar o nome do argumento e seu valor logo após a função soma_n().

soma_n <- function(x, n = 1) { x + n }
obj <- 10:15
map_dbl(obj, soma_n, n = 2)
## [1] 12 13 14 15 16 17

Usando fórmulas temos maior flexibilidade, pois podemos declarar, por exemplo, funções anônimas como ~.x+2, ao invés de soma_dois, por exemplo), enquanto com as reticências temos maior legibilidade.

5.2 Iterações intermediárias

Em algumas situações é necessário realizar operações com mais de um objeto. O objetivo desta seção é apresentar a função map2(), mas antes disso vamos mostrar duas funções (walk() e modify()) que irão nos ajudar a apresentar a map2() mais adiante.

5.2.1 Andar e modificar

walk() e modify() são pequenas alterações da família map() que vêm a calhar em diversas situações. A primeira destas funciona exatamente igual à map() mas não devolve resultado, apenas efeitos colaterais, quando não existe a necessidade de utilizar um valor de retorno. Normalmente utilizamos ela quando desejamos renderizar a saída na tela ou salvar arquivos no disco - o importante é a ação, não o valor de retorno. Aqui está um exemplo simples:

x <- list(1, "a", 3)

x %>% 
  walk(print)
## [1] 1
## [1] "a"
## [1] 3

Já a a segunda, não muda a estrutura do objeto sendo iterado, ela substitui os próprios elementos da entrada, aplicando a função em cada elemento.

x <- list(1, 2, 3)

modify(x, ~.+2)
## [[1]]
## [1] 3
## 
## [[2]]
## [1] 4
## 
## [[3]]
## [1] 5

A maior utilidade de walk é quando precisamos salvar múltiplas tabelas. Para fazer isso, podemos usar algo como walk(tabelas, readr::write_csv). Um caso de uso interessante da modify() é ao lado do sufixo _if(), combinação que nos permite iterar nas colunas de uma tabela e aplicar transformações de tipo apenas quando um atributo for verdade (geralmente de queremos transformar as colunas de fator para caractere).

A função map2() é uma generalização da map() para mais de um argumento e nos permite reproduzir o laço acima em apenas uma linha. A map2() abstrai a iteração em paralelo, aplica a função em cada par de elementos das entradas e pode achatar o objeto retornado com os sufixos _chr, _dbl, _int e _lgl.

O termo “paralelo” neste capítulos se refere a laços em mais de uma estrutura e não a paralelização de computações em mais de uma unidade de processamento.

gap_split_small <- gap_split[1:10]
countries <- names(gap_split_small)

ggplot(gap_split_small[[1]], aes(year, lifeExp)) +
  geom_line() +
  labs(title = countries[[1]])


plots <- map2(gap_split_small, countries,
              ~ ggplot(.x, aes(year, lifeExp)) +
                geom_line() +
                labs(title = .y))

plots[[1]]


walk(plots, print)


walk2(.x = plots, .y = countries,
      ~ ggsave(filename = paste0(.y, ".pdf"), plot = .x))
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image
## Saving 7 x 5 in image

file.remove(paste0(countries, ".pdf"))
##  [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE

Como o pacote purrr é extremamente consistente, a map2() também funciona com reticências, fórmulas e dá acesso ao placeholder .y para indicar onde os elementos do segundo vetor devem ir.

Para não precisar oferecer uma função para cada número de argumentos, o pacote purrr fornece a pmap(). Para esta função devemos passar uma lista em que cada elemento é um dos objetos a ser iterado:

x <- list(1, 1, 1)
y <- list(10, 20, 30)
z <- list(100, 200, 300)

pmap(list(x, y, z), sum)
## [[1]]
## [1] 111
## 
## [[2]]
## [1] 221
## 
## [[3]]
## [1] 331

Infelizmente a pmap() não nos permite utilizar fórmulas. Se quisermos usar uma função anônima com ela, precisamos declará-la no seu corpo:

pmap(list(x, y, z), function(first, second, third) (first + third) * second)
## [[1]]
## [1] 1010
## 
## [[2]]
## [1] 4020
## 
## [[3]]
## [1] 9030

l <- list(a = x, b = y, c = z)
pmap(l, function(c, b, a) (a + c) * b)
## [[1]]
## [1] 1010
## 
## [[2]]
## [1] 4020
## 
## [[3]]
## [1] 9030

A última função que veremos nessa seção é a imap(). No fundo ela é um atalho para map2(x, names(x), ...) quando x tem nomes e para map2(x, seq_along(x), ...) caso contrário:

objeto <- 10:20
imap_dbl(objeto, ~.x+.y)
##  [1] 11 13 15 17 19 21 23 25 27 29 31

Como podemos observar, agora .y é o placeholder para o índice atual (equivalente ao i no laço com for). Naturalmente, assim como toda a família map(), a imap() também funciona com os sufixos de achatamento.

5.3 Iterações avançadas

Agora que já tivemos uma noção da sintaxe da PF com a família map(), para substituição de laços, podemos passar para os tipos de laços que envolvem condicionais. Como o objetivo deste módulo é mostrar uma “nova forma” de manipular dados, recomendamos fortemente que vocês não fiquem restritos a este material e também leiam a documentação de cada função aqui abordada.

5.3.1 Iterações com condicionais

Imagine que seja necessário aplicar uma função somente em alguns elementos de um vetor. Sabemos que com a utilização de um laço isso é uma tarefa fácil, no entanto 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 uma condição é satisfeita. Esta condição pode ser a saída de uma função ou uma fórmula (que serão aplicadas em cada elemento da entrada e devem retornar TRUE ou FALSE).

A função map_if(), diferente das apresentadas anteriormente, não funciona com sufixos, isto implica que devemos achatar o resultado:

eh_par <- function(x) { x%%2 == 0}
raiz <- function(x) { sqrt(x) }
numeros <- 2:20
map_if(numeros, eh_par, raiz) %>% flatten_dbl()
##  [1]  1.414214  3.000000  2.000000  5.000000  2.449490  7.000000  2.828427  9.000000  3.162278 11.000000  3.464102 13.000000  3.741657 15.000000  4.000000
## [16] 17.000000  4.242641 19.000000  4.472136

A utilização de fórmulas permite eliminar completamente a necessidade de funções declaradas:

map_if(numeros, ~.x%%2 == 0, ~sqrt(.x)) %>% flatten_dbl()
##  [1]  1.414214  3.000000  2.000000  5.000000  2.449490  7.000000  2.828427  9.000000  3.162278 11.000000  3.464102 13.000000  3.741657 15.000000  4.000000
## [16] 17.000000  4.242641 19.000000  4.472136
map_if(numeros, ~.x%%2 == 0, ~sqrt(.x), .else = ~.x/2) %>% flatten_dbl()
##  [1] 1.414214 1.500000 2.000000 2.500000 2.449490 3.500000 2.828427 4.500000 3.162278 5.500000 3.464102 6.500000 3.741657 7.500000 4.000000 8.500000 4.242641
## [18] 9.500000 4.472136

Também é possível especificar a condição else, como mostra o comando a seguir:

map_if(numeros, ~.x%%2 == 0, ~sqrt(.x), .else = ~.x/2) %>%
  flatten_dbl()
##  [1] 1.414214 1.500000 2.000000 2.500000 2.449490 3.500000 2.828427 4.500000 3.162278 5.500000 3.464102 6.500000 3.741657 7.500000 4.000000 8.500000 4.242641
## [18] 9.500000 4.472136

A segunda dessas funções é a map_at(), que funciona de forma muito semelhante à map_if(). Para map_at() devemos passar um vetor de nomes ou índices onde a função deve ser aplicada:

map_at(numeros, c(2, 4, 6), ~sqrt(.x)) %>% flatten_dbl()
##  [1]  2.000000  1.732051  4.000000  2.236068  6.000000  2.645751  8.000000  9.000000 10.000000 11.000000 12.000000 13.000000 14.000000 15.000000 16.000000
## [16] 17.000000 18.000000 19.000000 20.000000

5.3.2 Iterações com tabelas e funções

Ainda dentro da família map() existem duas funções, que são menos utilizadas, map_dfc() e map_dfr(), que retornam um dataframe criado por vinculação de linha e vinculação de coluna, respectivamente. Isto é equivalente a um map() seguido de um dplyr::bind_cols() ou de um dplyr::bind_rows(), nesta ordem.

A maior utilidade dessas funções é quando precisamos estruturar uma tabela com informações espalhadas 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). Outra função do pacote purrr pouco utilizada é a invoke_map(). Antes de apresentar a funcionalidade da invoke_map(), vamos demonstrar o que faz a invoke() sozinha:

invoke(runif, list(n = 10))
##  [1] 0.9874303 0.1619048 0.8137891 0.5729209 0.5568814 0.2426378 0.6535431 0.2069450 0.1776256 0.3173730

invoke(runif, n = 10)
##  [1] 0.2805692 0.3659802 0.6621373 0.9436000 0.2310623 0.9782688 0.8417631 0.2663436 0.1333008 0.5405801

A invoke recebe uma função e uma lista de argumentos já a invoke_map() pode recerber tanto uma única função com uma lista de argumentos quanto uma lista de funções com uma lista de argumentos, sendo uma generalização da primeira. Assim como a família map(), a família invoke() também aceita os sufixos, como veremos a seguir:

invoke_map(list(runif, rnorm), list(list(n = 10), list(n = 5)))
## [[1]]
##  [1] 0.61275601 0.35777281 0.44360537 0.65858583 0.55333403 0.15429852 0.65016731 0.03282468 0.27493851 0.60644473
## 
## [[2]]
## [1] -0.9446061  0.5223384 -0.4046952  1.7690264 -0.3571692

invoke_map(list(runif, rnorm), list(list(n = 5)))
## [[1]]
## [1] 0.5582334 0.7905379 0.1011621 0.6367221 0.7404948
## 
## [[2]]
## [1] -1.19410087  0.92130208  0.59587783 -0.49267655 -0.09128766
invoke_map(list(runif, rnorm), n = 5)
## [[1]]
## [1] 0.03004588 0.88975103 0.68595225 0.08546755 0.88595884
## 
## [[2]]
## [1]  0.611715694 -0.006528331  0.489441734  0.324943537 -0.965504968

invoke_map("runif", list(list(n = 5), list(n = 10)))
## [[1]]
## [1] 0.92227008 0.56318437 0.61233982 0.06952834 0.37853198
## 
## [[2]]
##  [1] 0.6502889 0.3310032 0.8295486 0.1865933 0.5490760 0.9131637 0.2568578 0.1696646 0.2614180 0.1519546

5.3.3 Redução e acúmulo

Reduzir é outro conceito importante na programação funcional. Permite partir de uma lista com alguns elementos e obter um objeto com um único elemento, combinando os elementos de alguma forma. Sendo assim o pacote purrr possui duas funções que ajudam a realizar essa transformaçã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

A soma cumulativa utilizando um laço é bastante simples, mas também é muito fácil de fazer confusão entre os índices e o bug acabar passando desapercebido. Para evitar esse tipo de situação, podemos utilizar accumulate() ou reduce (tanto com uma função quanto com uma fórmula). A diferença entre elas é que a primeira mantém os resultados intermediários, enquanto a segunda retorna o resultado final. Primeiramente vamos mostrar a utilização da accumulate():

1:5 %>% accumulate(`+`)
## [1]  1  3  6 10 15
1:5 %>% accumulate(`+`, .dir = "backward")
## [1] 15 14 12  9  5

rerun(5, rnorm(100)) %>%
  set_names(paste0("sim", 1:5)) %>%
  map(~ accumulate(., ~ .05 + .x + .y)) %>%
  map_dfr(~ tibble(value = .x, step = 1:100), .id = "simulation") %>%
  ggplot(aes(x = step, y = value)) +
    geom_line(aes(color = simulation)) +
    ggtitle("Simulations of a random walk with drift")

DICA: 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:

1:5 %>% reduce(`+`)
## [1] 15

Essas duas funções também têm variedades paralelas (accumulate2() e reduce2()), assim como variedades invertidas accumulate_right() e reduce_right()).

5.4 Miscelânea

A Programação Funcional não facilita apenas para evitar o uso de laços, o purrr possui algumas funções que não relacionadas com laços, mas que são bastante úteis quando utilizamos as funções apresentadas até o momento.

5.4.1 Manter e descartar

Se tivermos o objetivo de filtrar elementos de um vetor ou lista, podemos usar as funções keep(), seleciona elementos que passam por um teste lógico, e discard(), seleciona elementos que não passam por um teste lógico. Elas funcionam com fórmulas e podem ser extremamente úteis em situações que dplyr::select() e magrittr::extract() não conseguem cobrir:

rep(10, 10) %>%
  map(sample, 5) %>%
  keep(~ mean(.x) > 6)
## [[1]]
## [1] 6 8 7 9 3
## 
## [[2]]
## [1]  1  7  9 10  6

No exemplo acima estamos mantendo todos os vetores da lista com média maior que 6.

x <- rerun(5, a = rbernoulli(1), b = sample(10))
x
## [[1]]
## [[1]]$a
## [1] TRUE
## 
## [[1]]$b
##  [1]  4  7  1  9 10  6  8  5  3  2
## 
## 
## [[2]]
## [[2]]$a
## [1] TRUE
## 
## [[2]]$b
##  [1] 10  3  7  8  5  2  1  4  9  6
## 
## 
## [[3]]
## [[3]]$a
## [1] FALSE
## 
## [[3]]$b
##  [1]  5  1  9  3  2  4 10  8  7  6
## 
## 
## [[4]]
## [[4]]$a
## [1] TRUE
## 
## [[4]]$b
##  [1]  9  1  3  6  7  5 10  4  8  2
## 
## 
## [[5]]
## [[5]]$a
## [1] FALSE
## 
## [[5]]$b
##  [1]  4  7  5  8 10  1  9  2  6  3
x %>% keep("a")
## [[1]]
## [[1]]$a
## [1] TRUE
## 
## [[1]]$b
##  [1]  4  7  1  9 10  6  8  5  3  2
## 
## 
## [[2]]
## [[2]]$a
## [1] TRUE
## 
## [[2]]$b
##  [1] 10  3  7  8  5  2  1  4  9  6
## 
## 
## [[3]]
## [[3]]$a
## [1] TRUE
## 
## [[3]]$b
##  [1]  9  1  3  6  7  5 10  4  8  2
x %>% discard("a")
## [[1]]
## [[1]]$a
## [1] FALSE
## 
## [[1]]$b
##  [1]  5  1  9  3  2  4 10  8  7  6
## 
## 
## [[2]]
## [[2]]$a
## [1] FALSE
## 
## [[2]]$b
##  [1]  4  7  5  8 10  1  9  2  6  3

5.4.2 Verificações

Quando há necessidade de verificar o tipo de um ou mais objetos, existe, no pacote purrr, uma outra família que ajuda a realizar essas verificações, que é a is(). Esta família possui uma série de funções que nos permite fazer verificações extremamente estritas em objetos dos mais variados tipos. Seguem alguns poucos exemplos:

is_scalar_integer(10:15)
## [1] FALSE
is_bare_integer(10:15)
## [1] TRUE
is_atomic(10:15)
## [1] TRUE
is_vector(10:15)
## [1] TRUE

5.4.3 Transposição e indexação profunda

Quando acontece de estarmos trabalhando com listas complexas e profundas, às vezes é necessário acessar algum elemento da mesma ou transpô-la para facilitar a manipulação ou análise. O purrr nos fornece duas funções extremamente úteis para facilitar o nosso trabalho: pluck() e transpose(), respectivamente. A primeira possibilita o acesso de elementos profundos de uma lista sem a necessidade de colchetes, enquanto a segunda transpõe a lista.

obj <- list(list(a = 1, b = 2, c = 3), list(a = 4, b = 5, c = 6))
str(obj)
## List of 2
##  $ :List of 3
##   ..$ a: num 1
##   ..$ b: num 2
##   ..$ c: num 3
##  $ :List of 3
##   ..$ a: num 4
##   ..$ b: num 5
##   ..$ c: num 6
pluck(obj, 2, "b")
## [1] 5
str(transpose(obj))
## List of 3
##  $ a:List of 2
##   ..$ : num 1
##   ..$ : num 4
##  $ b:List of 2
##   ..$ : num 2
##   ..$ : num 5
##  $ c:List of 2
##   ..$ : num 3
##   ..$ : num 6

DICA: Se você estiver com muitos problemas com listas profundas, dê uma olhada nas funções relacionadas a depth() pois elas podem ser muito úteis.

5.4.4 Aplicação parcial

Se o nosso objetivo for definir valor para os arhgumentos de uma função (seja para usá-la em uma pipeline ou com alguma função do próprio purrr), podemos utilizar a partial(). Ela funciona nos moldes da família invoke() e pode ser bastante útil para tornar suas pipelines mais enxutas:

soma_varios <- function(x, y, z) { x + y + z }
nova_soma <- partial(soma_varios, x = 5, y = 10)
nova_soma(3)
## [1] 18

No exemplo anterior, fixamos o valor de x e y da função soma_varios, sendo necessário definir posteriormente apenas o valor de z.

5.4.5 Execução segura

É bem comum criarmos uma função e, quando a executamos, temos um erro como retorno. Isto pode ser contornado com facilidade em um laço com um condicional, mas se trata de uma tarefa mais complexa quando está trabalhando com programação funcional. Desta forma, o purrr possui algumas funções que encapsulam as saídas, e quando esta possui um erro, o silenciam e retornam um valor padrão em seu lugar.

A função quietly() retorna uma lista com resultado, saída, mensagem e alertas, já a safely() retorna uma lista com resultado e erro (um destes sempre é NULL), e a possibly() silencia o erro e retorna um valor dado pelo usuário.

soma_um <- function(x) { x + 1 }
s_soma_um <- safely(soma_um, 0)
obj <- c(10, 11, "a", 13, 14, 15)
s_soma_um(obj)
## $result
## [1] 0
## 
## $error
## <simpleError in x + 1: argumento não-numérico para operador binário>

5.5 Exercícios

1. Utilize a função map() para calcular a média de cada coluna da base mtcars.

2. Use a função map() para testar se cada elemento do vetor letters é uma vogal ou não. Dica: você precisará criar uma função para testar se é uma letra é vogal. Faça o resultado ser (a) uma lista de TRUE/FALSE e (b) um vetor de TRUE/FALSE.

3 Faça uma função que divida um número por 2 se ele for par ou multiplique ele por 2 caso seja ímpar. Utilize uma função map para aplicar essa função ao vetor 1:100. O resultado do código deve ser um vetor numérico.