Programação funcional

Você já deve ter ouvido falar que o R é uma linguagem de programação funcional, significanco com isso que no R é possível criar e manusear funções. Essa é uma propriedade importante do R porque reduz tempo e chances de erro, ao evitar duplicações. Isto é, se o mesmo comando ou cadeia de comandos será aplicado a diferentes objetos, é melhor criar ou aplicar de forma iterativa uma função existente a esse conjunto de objetos, em vez de copiar, colar e fazer as modificações necessárias no código para cada situação.

Nesse post iremos trabalhar com loops e funcionais. Funcionais são um especial caso de programação funcional. Elas são funções que têm como argumentos outras funções.

Usando loops

Você também deve ter ouvido falar que no R é preferível não usar loops, porque no R você pode vetorizar as funções, quando elas já não fazem isso por você, o que torna seu código mais eficiente.

No entanto, não há como negar que, para quem está começando a usar o R, loops facilitam o aprendizado, vez que a construção da sequência de comandos é intuitiva, sua visualização facilita a compreensão do que está acontecendo e torna mais simples a correção de erros. Por outro lado, loops podem ser usados para uma infinidade de rotinas, o que dificulta, para o leitor externo, saber de antemão o que se busca com loops.

For loop

Há basicamente dois tipos de loops, sendo o mais conhecido deles o for loop, pelo qual alguns comandos são executados sequencialmente por um pretederminado número de vezes. O exemplo abaixo ilustra o que acaba de ser dito:

a<-0
for(i in 1:10){
  a<-a+i
  }
print(a)
## [1] 55

No exemplo acima, definimos um valor inicial para “a”, a=0. Depois disso, pedimos para “a”" ser somando aos números de 1 a 10, sequencialmente, resultando em 55.

While e Repeat (loops)

O outro tipo de loop leva em conta não um predeterminado de iterações, mas sim uma condição, ou seja, as iterações irão ocorrer enquanto tal condição for satisfeita. O mais conhecido deles é o while. Vejamos como ele opera:

x<-1
while(x< 5){
  x<-x+1
  print(x)
}
## [1] 2
## [1] 3
## [1] 4
## [1] 5

No exemplo acima, inicialmente definimos um valor para “x”, x=1. Depois disso, pedimos para somar sequencialmente x a 1, enquanto x for menor que 5.

A segunda modalidade de loop condicionado é o repeat. Por ela, repete-se a operação até segunda ordem. Esta segunda ordem é geralmente determinada pelas funções if e break:

x<-0
repeat{
   x<-x+1
   print(x)
  if(x==5){
    break
  }
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5

No exemplo acima, inicialmente definimos um valor para “x”, x=0. Depois disso, pedimos para somar sequencialmente x a 1, até que x alcance o valor 5.

Família apply

Uma alternativa ao loop, principalmente para a manipulação de listas e dataframes, é usar a família de funções apply. Delas, as mais comuns são apply, pela qual aplica-se uma função às linhas ou às colunas, ou ambas, de uma matriz, e lapply, pela qual se aplica a função a todos os elementos de uma lista e retorna-se uma lista.

Função apply

Primeiramente, vamos criar uma matriz bem simples.

minhaMatriz<-matrix( 
  c(2, 4, 3, 1, 5, 7),  nrow=3,  ncol=2)
print(minhaMatriz)
##      [,1] [,2]
## [1,]    2    1
## [2,]    4    5
## [3,]    3    7

Agora vamos somar todos os elementos da mesma linha dessa matriz:

apply(minhaMatriz,1,sum)
## [1]  3  9 10

Agora vamos somar todos os elementos da mesma coluna dessa matriz.

apply(minhaMatriz,2,sum)
## [1]  9 13

Nos exemplo acima, criamos uma matriz: minhaMatriz, e aplicamos a função sum a todas as linhas da matriz. O número 1 no primeiro exemplo indica que iremos executar a função sobre as linhas da matriz. O número 2 indica que iremos fazer o mesmo com as colunas.

Função lapply

Imagine agora você tem uma lista de elementos e quer aplicar uma função a todos esses elementos. A função lapply faz isso por você.

Exemplo 1:

minhaLista<-list(e1=c(1,2,3),e2=c(3,4,5,10),e3=c(6,7,8,11,13))
lapply(minhaLista,mean)
## $e1
## [1] 2
## 
## $e2
## [1] 5.5
## 
## $e3
## [1] 9

No exemplo acima, criamos uma lista com três elementos. Cada elemento é um vetor com diferentes tamanhos. Depois disso, com a ajuda da função lapply, calculamos a média dos valores de cada vetor.

Exemplo 2:

No primeiro exemplo, lapply aplica a função mean utilizando o primeiro argumento da função mean. Se você quer utilizar outros argumentos, você deve criar uma função anônima, e.g, function(x). Veja como fica isso:

lapply(minhaLista,function(x) mean(x, trim=.5))
## $e1
## [1] 2
## 
## $e2
## [1] 4.5
## 
## $e3
## [1] 8

Função sapply

Esta função é similar à função lapply, com a diferença de que retorna um vetor em vez de uma lista.

sapply(minhaLista,mean)
##  e1  e2  e3 
## 2.0 5.5 9.0

Função vapply

A função vapply opera como sapply, mas recebe um argumento adicional especificando o tipo de retorno esperado, o que a torna mais segura e mais rápida que sapply, principalmente quando utilizada para criar outra função.

Exemplo:

vapply(minhaLista,mean,double(1))
##  e1  e2  e3 
## 2.0 5.5 9.0

No exemplo acima, determinamos que o resultado deve ser do tipo double. Se colocássemos integer, o resultado seria um erro informando que se esperava integer, mas o resultado é do tipo double.

Função tapply

Esta função é bastante utilizada para aplicar uma função a subconjuntos de um vetor conforme uma variável fator. Vamos utilizar o dataset iris como exemplo para calcular a média do comprimento da sépala (sepal) com base na espécie.

data(iris)
tapply(iris$Sepal.Length,iris$Species,mean)
##     setosa versicolor  virginica 
##      5.006      5.936      6.588

Função mapply

Uma função pouco utilizada e, como veremos adiante, pouco útil porque há opções melhores, é a função mapply, a qual admite mais de um argumento. Basicamente, mapply é uma versão multivariada de sapply. Por ela, é possível incluir mais de uma variável como argumento.

l1 <- list(a = c(1:10), b = c(11:20))
l2 <- list(c = c(21:30), d = c(31:40))
mapply(sum, l1$a, l1$b, l2$c, l2$d)
##  [1]  64  68  72  76  80  84  88  92  96 100

No exemplo acima, foi aplicada a função sum (soma) aos elementos correspondentes das duas listas. Ou seja, somaram-se o primeiro valor de cada um dos elementos das duas listas, fez-se o mesmo com o segundo e assim por diante. Como se pode verificar, no caso dos primeiros elementos, temos: 1+11+21+31=64.

Função rapply

Esta função aplica uma função recursivamente a uma lista. Ela pode retornar tanto um vetor quanto uma lista.

Exemplo 1 - retornando um vetor

l <- list(a = 1:10, b = 11:20)
rapply(l,mean)
##    a    b 
##  5.5 15.5

Exemplo 2 - Retornando uma lista

rapply(l,mean,how="list")
## $a
## [1] 5.5
## 
## $b
## [1] 15.5

Função do.call

Outra função bastante útil é a função do.call. Diferentemente da família apply, que aplica uma função a cada elemento da lista, do.call reconstrói a função e a executa sobre todos os elementos da lista, tratando cada elemento como um argumento da função. Ela é especialmente útil quando queremos juntar várias dataframes contidos numa lista com as funções rbind e cbind.

Eu particularmente uso esta função na manipulação de dados longitudinais, ou seja, quando tenho várias tabelas de valores correspondentes a diferentes anos e/ou unidades federativas. Ou quando extraio de uma página web várias tabelas numa lista, as quais precisam ser juntadas numa única tabela. O exemplo abaixo ilustra como usar do.call. Criamos três dataframes com a coluna ano, unidade federativa e uma terceira coluna com valores simulados. Depois disso, pusemos todos numa única lista. Se queremos juntar todos esses dataframes num único dataframe, bastará chamar a função rbind dentro da função do.call.

library(tibble)

df1<-tibble(ano=1992,uf=rep(c("SP","RJ"),each=2),valores=rnorm(4))
df2<-tibble(ano=1993,uf=rep(c("SP","RJ"),each=2),valores=rnorm(4))
df3<-tibble(ano=1994,uf=rep(c("SP","RJ"),each=2),valores=rnorm(4))

lista<-list(df1,df2,df3)

do.call(rbind,lista)
## # A tibble: 12 x 3
##      ano uf    valores
##    <dbl> <chr>   <dbl>
##  1  1992 SP      0.351
##  2  1992 SP      0.311
##  3  1992 RJ     -0.378
##  4  1992 RJ      0.141
##  5  1993 SP     -1.35 
##  6  1993 SP      0.363
##  7  1993 RJ     -0.903
##  8  1993 RJ     -0.441
##  9  1994 SP      0.309
## 10  1994 SP     -0.183
## 11  1994 RJ     -0.703
## 12  1994 RJ      0.533

Funções Reduce, Filter, Find, Map, Negate e Position

Por fim, o pacote base do R possui cinco funções extremamente úteis quando se trata de programação funcional: Reduce, Filter, Find, Map, Negate e Position. Vamos dar exemplos de cada uma delas.

Reduce

Esta função aplica uma função de forma recursiva sobre os elementos de uma lista. Quando usada com o argumento accumulate=TRUE, cada combinação será retornada. Por padrão, somente a último resultado é retornado.

Exemplo 1

Multiplicando os valores de uma lista e mostrando todas as sucessivas combinações.

Reduce(`*`, x=list(5,4,3,2), accumulate=TRUE)
## [1]   5  20  60 120

Exemplo 2

Multiplicando os valores de uma lista e mostrando o resultado final

Reduce(`*`, x=list(5,4,3,2))
## [1] 120

Exemplo 3

A função Reduce é particularmente útil no exemplo abaixo, quando queremos dar um merge em vários dataframes, mas a função merge só permite dois de cada vez.

d1<-tibble(ano=1991:1994,sexo=c("m","f","f","m"))
d2<-tibble(ano=1991:1994,cor=c("branco","negro","amarelo","indígena"))
d3<-tibble(ano=1991:1994,religiao_igreja=c("católico","evangélico","budista","matriz_afro"))

Reduce(function(x,y) merge(x,y,all=T),list(d1,d2,d3))
##    ano sexo      cor religiao_igreja
## 1 1991    m   branco        católico
## 2 1992    f    negro      evangélico
## 3 1993    f  amarelo         budista
## 4 1994    m indígena     matriz_afro

As três funções a seguir são chamadas de funcionais predicados, isto porque elas aplicam um predicado uma lista ou dataframe. Predicados são funções que retornam Verdadeiro ou Falso (TRUE ou FALSE), e.g. is.na(), is.character(). Vejamos o seu funcionamento com exemplos.

Filter

Filter extrai os elementos de uma lista com base numa função. No exemplo abaixo, temos uma lista com letras e números. Iremos extrair somente as letras.

s<-list(1,2,"a","c")
Filter(is.character,s)
## [[1]]
## [1] "a"
## 
## [[2]]
## [1] "c"

Find

Find é simplesmente uma forma truncada de Filter. Em vez de extrair todos os elementos conforme o predicado, extrai apenas o primeiro elemento. Usando o mesmo exemplo acima:

Find(is.character,s)
## [1] "a"

Position

Position é similar a Find com a diferença de que retorna a posição do valor e não o próprio valor, como o fez Find.

Position(is.character, s)
## [1] 3

Negate

Negate cria a negação de uma função. Usando a mesma lista, podemos utilizar a função Negate combinada com a função Filter para extrair os elementos da lista que não são letras:

Filter(Negate(is.character),s)
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 2

Map

Map é similar a mapply, com a diferença de que não força a simplificação do resultado. Vamos utilizar o mesmo exemplo usado para mapply um pouco acima para ilustrar a diferença.

l1 <- list(a = c(1:10), b = c(11:20))
l2 <- list(c = c(21:30), d = c(31:40))
Map(sum, l1$a, l1$b, l2$c, l2$d)
## [[1]]
## [1] 64
## 
## [[2]]
## [1] 68
## 
## [[3]]
## [1] 72
## 
## [[4]]
## [1] 76
## 
## [[5]]
## [1] 80
## 
## [[6]]
## [1] 84
## 
## [[7]]
## [1] 88
## 
## [[8]]
## [1] 92
## 
## [[9]]
## [1] 96
## 
## [[10]]
## [1] 100

Como se verifica, o resultado é uma lista e não um vetor.