Trilhas
/
R
R
/
Programa
/
Programação funcional
Pr

Programação funcional em R

1 exercício

Sobre Programação funcional

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:

  • Funções puras, que mapeiam entradas para saídas de forma consistente e sem efeitos colaterais.
  • Dados imutáveis.
  • Funções de primeira classe, que podem ser tratadas como valores, como qualquer outro tipo.
  • Funções de ordem superior, que recebem outras funções como argumentos ou como valores de retorno.

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.

Pipes

O uso de pipes |> para conectar funções é uma técnica cada vez mais comum em várias linguagens.

Em resumo:

  • Um resultado à esquerda do pipe se torna o primeiro argumento da função à direita (em que "à direita" ignora quebras de linha opcionais).
  • Os argumentos restantes podem ser incluídos na chamada da função.
  • Em R, precisamos encadear o pipe até uma chamada de função, não apenas um nome, então os parênteses são sempre obrigatórios (isso é diferente de algumas outras linguagens).

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.

Funções apply

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

A biblioteca 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:

Note

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.

As muitas funções map

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:

  • O que você quer retornar? 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.
  • Você precisa de cada índice posicional da entrada? Coloque um i no início dos nomes das funções, como em imap().
  • Você quer processar várias entradas em paralelo?
    • Substitua map por map2 no nome, como em map2_dbl() para duas entradas (por exemplo, valores e pesos, para calcular uma média ponderada)
    • Substitua por pmap para uma lista de entradas paralelas, como em pmap_chr(). E sim, o paralelismo em nível de hardware é possível.
  • Você quer processar todos os elementos da entrada? Se não, pense em 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.
  • Os dados de entrada são profundamente aninhados? Existem funções para essa situação.
  • Você quer que o valor de retorno seja sempre do mesmo tipo que a entrada? Use as funções 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.

Funções reduce

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.

Funções de filtro

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.

Recursão

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.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Programação funcional