7 Paradigmas de Programação

este capítulo está em construção. O que segue abaixo é apenas um rascunho.

7.1 Introdução

Referência básica: Advanced R (2e) partes “Functional Programming” + documentação do pacote “purrr”, “Object-Oriented Programming”, “Metaptrogramming” + “R Language Definition” capítulo 6 e do pacote rlang.

Na aula de hoje, aprenderemos construtos de mais alto nível. Falaremos do último tijolinho, as expressões. Por fim, faremos uma revisão.

Haverão três aprendizados principais:

(i) O que é, quais são os construtos, e para que usar programação funcional.

(ii) O que é, e para que usar programação orientada a objetos.

(iii) O que são expressões, e como usá-las para metaprogramação.

Para os exemplos:

7.2 PF: Fundamentos

7.2.1 Programação Funcional

Programação funcional é um termo usado em contextos diferentes, mas no geral, se refere à programas onde as protagonistas são as funções.

Linguagen funcional é um conceito técnico de Ciência da Programação, que normalmente se refere à uma linguagem com funções first-class e pures.

O primeiro termo vocês já conhecem, o segundo quer dizer funções que:

  • O resultado depende apenas dos inputs (runif()?)
  • Funções apenas retornam um resultado, nenhum efeito colateral (<-?).

Ainda assim, linguagens que não são estritamente funcionais, podem tomar decisões de desing que privilegiam um estilo de programação funcional. Um estilo que deriva seus principais construtos através de funções.

Quais são os construtos centrados em funções?

Object Function
Object function function factory
Function functional function operator

7.3 PF: Functionals

7.3.1 Functionals

No dia a dia, o construto mais importante serão os functionals. E dentre eles, o map, que vocês já tiveram uma palinha, será o principal.

O R base implementa vários functionals, mas, na minha opinião, o pacote purrr e suas funções purrr::map() são melhores.

7.3.2 Map

Um map recebe uma função e um vetor, e a aplica em cada um dos elementos dele, retornando os resultados em um novo vetor.

Como foi visto, são similares à loops:

function(x, f, ...) {
  out <- vector("list", length(x))
  for (i in seq_along(x)) out[[i]] <- f(x[[i]], ...)
  out
}

7.3.3 Map 2

Pense sobre o padrão abaixo:

function(x, y, f, ...) {
  out <- vector("list", length(x))
  for (i in seq_along(x)) out[[i]] <- f(x[[i]], y[[i]], ...)
  out
}

É um map, aplica dois vetores, termo-a-termo, em uma função.

No purrr, a função que faz isso é purrr::map2().

7.3.4 Map^2

E se quisermos, para cada elemento de um vetor x, aplicar a função f à esse elemento, e todos os elementos de y?

Isto é, um loop duplo:

function(x, y, f, ...) {
  out <- vector("list", length(x))
  for (i in seq_along(x)) {

    out[[i]] <- vector("list", length(y))
    for (j in seq_along(y)) {
      out[[i]][[j]] <- f(x[[i]], y[[j]], ...)
    }
  }
  out
}

Note que isso é diferente de um map2, isso seria um “map” dentro de outro “map”:

map(x, \(xi) {
  map(y, \(yi) {
    f(xi, yi)
  })
})

7.3.5 Map p

Voltando ao map 2? E se em vez de dois vetores, quero combinar termo-a-termo três vetores? Quatro? Um número \(p\) de vetores?

Aí temos purrr::pmap. A lógica é a mesma, passamos vários vetores, e depois uma função.

Temos que map2(x, y, f) é equivalente à pmap(list(x, y), f). Mas, também podemos fazer pmap(list(x, y, z), f), etc.

Obs: por uma decisão de design, agrupamos esses vários vetores (de mesmo tamanho) em uma lista.

7.3.6 Map^p

E se quisésse-mos fazer um map, dentro de um map, dentro de um map … arbitrárias vezes? A resposta é: não inventa.

Sério, seria estranho ter essa necessidade, e provavelmente era melhor usar um loop while. Mas só de curiosidade, dá pra escrever um map recursivo, que fixa o primeiro vetor e joga o resto pra dentro de um novo map:

rmap <- function(l, f) {
  if (length(l) > 1) {
    map(l[[1]], \(x) rmap(l[-1], \(...) f(x, ...)))
  } else {
    map(l[[1]], f)
  }  
}

7.3.7 Atalhos

Index map: muitas vezes estamos interessados em um caso especial de map2, para qual existe o atalho purrr::imap():

map2(x, names(x), \(xi, name) {...})
imap(x, \(xi, name) {...})

Selecionando elementos: outro padrão comum é ter uma lista de listas como list(list(a = 1, b = 2), list(a = 3, b = 4)) e querer selecionar todos os primeiros elementos de cada sub-lista, ou todos os elementos.

Vocês sabem que podem fazer:

  • map(x, ~ .x[[1]]), ou map(x, ~ .x[["a"]]).
  • Mas o purrr tem um atalho: map(x, 1), ou map(x, "a").

Sintaxe lambda: O purrr permite utilizar fórmulas para definir funções, uma sintaxe especial do rlang: \(x, y) x + y é equivalente à ~ .x + .y.

Argumentos fixos: map() pode receber argumentos extras, via ..., passados para a função mapper. Mas, a recomendação é sempre criar uma função anônima:

map(mtcars, mean, na.rm = TRUE)
map(mtcars, ~ mean(.x, na.rm = TRUE))

Piping: as funções do purr também funcionam muito bem com piping:

mtcars[-1] |>
  map(~ lm(mtcars$mpg ~ .x)) |>
  map(coef) |>
  map(2)

mtcars[-1] |>
  map(~ coef(lm(mtcars$mpg ~ .x))[[2]])

7.3.8 Escolhendo o Output

Vimos que map e amigos sempre retornam uma lista, e se eu quiser um vetor simplificado? Podemos coagir na mão: map(mtcars, mean) |> unlist().

Mas, também existem as funções map_*() e amigos:

  • map_dbl(mtcars, mean).
  • purrr::modify() retorna um vetor do mesmo tipo do input.

Lembrando que, para conseguir um vetor simplificado, todos os resultados devem ser do mesmo tipo e ter length 1.

E se eu não me importo com o output, só estou chamando uma função pelos seus efeitos colaterais? Aí existe purrr::walk() e amigos.

7.3.9 E Amigos

List Atomic Same type Nothing
One argument map() map_lgl(), … modify() walk()
Two arguments map2() map2_lgl(), … modify2() walk2()
One argument + index imap() imap_lgl(), … imodify() iwalk()
N arguments pmap() pmap_lgl(), … pwalk()

Alternativas no R base:

7.3.10 Quando Usar um Map?

Lembre-se a escolha entre cada tipo de loop – que inclui maps – é baseada no nível de flexibilidade quista.

O principal ponto de maps é que suas iterações são independentes! Se você quer atualizar uma variável em todas as iterações, deveria estar usando um loop.

7.3.11 Reduce

A operação reduce, implementada em purrr::reduce(), recebe um vetor de tamaho \(n\) e retorna um escalar, aplicando uma operação repetidamente:

reduce(1:4, f)
f(f(f(1, 2), 3), 4)

Reduce é uma ferramenta útil para genralizar uma função que trabalha com dois inputs (uma função binária) para trabalhar com qualquer número de inputs:

  • sum(x) é equivalente à reduce(x, `+`).
  • Às vezes, é melhor criar uma nova função.

7.3.12 Reduce: Início Customizado

reduce() aceita um argumento .init, que é o valor inicial da operação. Se não for fornecido, o primeiro elemento de x é usado.

Normalmente, queremos um elemento “identidade” da operação que estamos fazendo – \(0\) é a identidade da soma, \(1\) é a identidade da multiplicação, etc.

7.3.13 Reduce 2

Não quero perder muito tempo com isso, mas também temos a versão “2” de reduce:

7.3.14 Reduce: Outros

Também poderíamos estar interessados em criar um “reduce p”, “reduce^2”, etc.

Tamém temos purrr::accumulate(), que é como reduce(), mas retorna um vetor do mesmo tamanho de x, com os resultados intermediários.

purrr::accumulate(1:4, `+`) #> 1  3  6 10

7.3.15 Predicate Functionals

Temos alguns funcionais relacionados à testes lógicos:

  • some(): retorna TRUE se algum elemento de .x satisfaz .p.
  • every(): retorna TRUE se todos os elementos de .x satisfazem .p.
  • none(): retorna TRUE se nenhum elemento de .x satisfaz .p.
  • São similares à any(map_lgl(.x, .p)), etc.

Temos também detect() e detect_index(), que retornam o primeiro elemento que satisfaz .p e sua posição, respectivamente.

E keep() e discard(), que mantém ou descartam os elementos que satisfazem .p.

map e modify têm variantes que também aceitam funções de predicado, transformando apenas os elementos que passam uma condição, ou que estão em uma localidade específica: map_if() e map_at.

7.3.16 Outros

Vocês talvez não perceberam, mas esta foi a primeira aula sobre o tidyverse! Vocês aprenderam o básico do pacote purrr.

Ele é muito grande, e existem muitas ourtas variações de map e predicates para facilitar sua vida. Foquem que aprender map antes de ir para o resto.

Por fim, outras funções são functionals e vocês não sabiam:

7.4 PF: Function Factories

7.4.1 Function Factories

Uma function factory é uma função que faz funções.

power1 <- function(exp) {
  function(x) x ^ exp
}

square <- power1(2)
cube <- power1(3)
square(3) #> 9

7.4.2 Function Factories’ Envs

Porque funciona? Onde square vai procurar exp?

square #> function(x) x ^ exp
environment(square) #> <environment: 0x0000017235c2c3b0>
environment(cube) #> <environment: 0x0000017235150760>
environment(square)$exp #> 2
environment(cube)$exp #> 3

Cuidado com:

e <- 2
square <- power1(e)
e <- 3
square(2) #> 8

Porque x é lazily evaluated. Por isso, é bom forcar sua avaliação antes de criar a função, com force(x):

power1 <- function(exp) {
  force(exp)
  function(x) x ^ exp
}

7.4.3 Function Factories’ Envs

Adicionalmente, podemos criar funções “manufaturadas” que alterem seu ambiente de encapsulação – que é único.

power1_count <- function(exp) {
  force(exp)
  i <- 0

  function(x) {
    assign("i", i + 1, envir = rlang::env_parent()) #ou i <<- i + 1
    list(res = x ^ exp, count = i)
  }
}

7.5 PF: Function Operators

7.5.1 Function Operators

Uma function operator é uma função que recebe uma ou mais funções como input e retorna uma função como output.

Por exemplo, usar Sys.time() para transformar uma função em uma função que também retorna seu tempo de execução.

timeit <- function(f) {
  function(...) {
    start <- Sys.time()
    res <- f(...)
    end <- Sys.time()
    list(res = res, time = end - start)
  }
}

sum_time <- timeit(sum)
sum_time(runif(10000))

Outros dois exemplos úteis são purrr::safely() e memoise::memoise().

  • Memoise é uma função que guarda o resultado de uma função, para que não seja recalculado.
  • Safely é uma função que captura erros, ao invés de parar a execução do programa.

Safely está no purr porque é útil para map e amigos:

x <- list(1:4, 5:8, "erro")
str(map(x, safely(sum)))
#> List of 3
#>  $ :List of 2
#>   ..$ result: int 10
#>   ..$ error : NULL
#>  $ :List of 2
#>   ..$ result: int 26
#>   ..$ error : NULL
#>  $ :List of 2
#>   ..$ result: NULL
#>   ..$ error :List of 2
#>   .. ..$ message: chr "invalid 'type' (character) of argument"
#>   .. ..$ call   : language .Primitive("sum")(..., na.rm = na.rm)
#>   .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

7.6 OOP

7.6.1 Definição

Da wikipédia:

Object-oriented programming (OOP) is a programming paradigm based on the concept of objects, which can contain data and code: data in the form of attributes, and code in the form of methods. In OOP, computer programs are designed by making them out of objects that interact with one another.

Muitas linguagens são multi-paradigma, o próprio R, por exemplo. O que temos, são diferenças de estilo de resolução de problemas: podemos centrar nosso estilo em funções, ou em objetos.

7.6.2 Sistemas de OOP no R

Vocês já conhecem o principal, S3. Lembrem que ele era diferente do python, era um sistema OOP de generic functions. Mas existem outros:

  • S4, que é o S3 mas de uma maneira mais formal e complicada.
  • RC, que é um sistema de OOP mais tradicional.
  • R6, que é o RC mas melhor.
  • Entre outros como R.oo e proto.

7.6.3 Sistemas de OOP no R

S3 é importante de aprender porque é muito utilizado, inclusive no R base. Os outros são mais raros. Do Advanced R:

S4 is a rigorous system that forces you to think carefully about program design. It’s particularly well-suited for building large systems that evolve over time and will receive contributions from many programmers.

R6 provides a standardised way to escape R’s copy-on-modify semantics. This is particularly important if you want to model objects that exist independently of R.

7.6.4 Quando Usar OOP

O sistema S3 faz parte integral do R, e você utilizará ele para:

  • Criar um método de uma classe não suportada por uma função genérica de seu interesse.
  • Criar funções genéricas quando criar código a ser compartilhado com outros (pacotes, principalmente).

Para além disso, o R não é uma linguagem orientada a objetos, se seu problema demanda isso, pode ser melhor usar outra linguagem. Ainda assim:

  • O sistema R6 é útil para criar objetos que existem independentemente do R.
  • Muitas aplicações fora da realidade da ciência de dados se dão bem com abordagem de OOP.

7.7 Expressões

7.7.1 Definição

Vocês viram sobre o estilo de resolução de problemas que coloca objetos no centro, funções no centro, e em breve, verão o estilo que coloca expressões no centro.

Mas antes, vamos estudar esse tipo de objeto.

7.7.2 Tipos de Expressões

Vocês viram que expressões são “código congelado”.

Uma expressão tem tipo expression, mas existem “subtipos” a despeito da sua natureza/objetivo:

  • Call, ou language, se é uma chamada de função.
  • Symbol, ou name, se é um nome de objeto.
  • Constant, se é um “dado cru” (1).
  • Pairlist, se é uma lista de argumentos – algo vestigial.

Os mais importantes são Call e Symbol:

  • Um symbol é uma expressão limitada, que segue as regras de nome.
  • Uma call tem uma estrutura específica, é como uma lista com: o nome da função, e cada argumentos (nomeados ou não). Conseguiremos alterar cada componente.

7.7.3 Tipos Baseados em Expressões

Uma expressão pode ser “detalhada”, unida com outros objetos para se tornar alguns dos objetos:

  • Quosure, se unida com um ambiente onde deve ser avaliada.
  • Closure, se unida com um ambiente e uma lista de argumentos.
  • Promise, se unida com um ambiente e o comportamento de colapsar após ser avaliada.
  • Formula, uma call à função ~ com um ambiente.

Obs: quosure e formulas não são tipos de verdade. Quosures são construídos em cima de promises, e formulas em cima de calls.

7.7.4 AST

Por último, vou apresentar como visualizamos expressões – e por consequência, scripts: as abstract syntax trees.

lobstr::ast(f1(f2(a, b), f3(1, f4(2))))
#> █─f1 
#> ├─█─f2 
#> │ ├─a 
#> │ └─b 
#> └─█─f3 
#>   ├─1 
#>   └─█─f4 
#>     └─2
lobstr::ast(1 + 2 * 3)
#> █─`+` 
#> ├─1 
#> └─█─`*` 
#>   ├─2 
#>   └─3

7.8 Metaprogramação: Capturando

7.8.1 Disclaimer

Não é muito útil para a maioria dos problemas de ciência de dados. Mas é muito útil para criar pacotes, e para resolver problemas mais complexos.

Tema difícil, mas útil para fixar alguns conceitos de CC, e está muito bem mastigado.

Few modern popular languages expose the level of metaprogramming that R provides.

Não gastem seu cérebro de mais, mas não gastem de menos também.

7.8.2 Definição

Como dito, pode ser entendido como o estilo que coloca expressões no centro.

Existem três pilares:

  1. Podemos capturar código como expressões (e amigos). Tanto de modo interativo, mas também passando-o para funções.
  2. Podemos criar e alterar código programaticamente.
  3. Podemos avaliar código em momentos oportunos, e escolhendo o ambiente de avaliação.

7.8.3 1. Quoting

O ato de capturar/congelar código é chamado de quoting, uma vez que é similar à transformar código em uma string. Mas não é a mesma coisa.

Podemos congelar código com:

expr(10 + 1) #> 10 + 1

7.8.4 1. Parsing

Não é para você se encontrar nessa situação, mas se você tiver um código em uma string, você pode usar parse() ou rlang::parse_expr() para transformar em uma expressão.

O inverso de parsing é deparsing, conseguir uma string que geraria uma dada expressão. Use deparse() ou rlang::expr_text().

7.8.5 1. Enriched Quoting

E se o código que quero congelar veio de um argumento de uma função?

f <- function(x){
  expr(x)
}

f(10 + 1) #> ?

Então, eu preciso de uma função mágica, que inspecione a promessa, e me retorne a expressão não avaliada. A função mágica é enexpr().

It’s called “en”-expr() by analogy to enrich. Enriching someone makes them richer; enexpr()ing a argument makes it an expression.

exprs() e enexprs() criam listas de expressões. Obs: diferente de um vetor do tipo expression.

7.8.6 1. Criando Symbols

Para ser mais restritivo, e exigir que um código esteja associado apenas a um símbolo, use sym(), ensym(), syms(), e ensyms().

7.8.7 1. Criando Quosures

Já falamos que elas existem. Não falei por que elas são úteis, mas calma lá.

Ainda assim, como são criadas? Com a função quo(). E se eu quiser salvar a expressão e o ambiente de uma promessa, em uma quosure? Com enquo().

7.9 Metaprogramação: Alterando

7.9.1 2. Criando e Modificando Calls

Podemos criar calls com call() e rlang::call2():

call2("f", 1, 2, 3)
#> f(1, 2, 3)

call2("+", 1, call2("*", 2, 3))
#> 1 + 2 * 3

E se eu quiser alterar um componente específico de uma call? Com [[ ou rlang::call_modify():

x <- call2("mean", c(1, 2, NA), na.rm = TRUE)
x$na.rm <- FALSE
x #> mean(c(1, 2, NA), na.rm = FALSE)

call_modify(x, trim = 0.1) #> mean(c(1, 2, NA), na.rm = FALSE, trim = 0.1)

7.9.2 2. Criando e Modificando Expressions

Podemos seletivamente “unquote” e “quote” partes de código, usando o operador do rlang !!:

xx <- expr(x + x)
yy <- expr(y + y)
expr(!!xx / !!yy) #> (x + x)/(y + y)

Temos também !!!:

!! is a one-to-one replacement. !!! (called “unquote-splice”, and pronounced bang-bang-bang) is a one-to-many replacement. It takes a list of expressions and inserts them at the location of the !!!

xs <- exprs(x, x, x)
expr(sum(!!!xs)) #> sum(x, x, x)

7.9.3 2. Criando e Modificando Functions

Também podemos modificar os componentes de funções com formals(), body(), environment(). E podemos criar funções com rlang::new_function().

7.10 Metaprogramação: Avaliando

7.10.1 3. Avaliação

Podemos escolher quando e onde (em qual ambiente) avaliar uma expressão, via eval().

Especialmente, podemos:

  • Data mask: avaliar expressões no contexto de um dataframe – tratando as colunas como variáveis.
  • Alterar a definição de operadores temporariamente.
eval(expr(x + y), envir = env(x = 1, y = 10)) #> 11

eval(expr(mean(mpg)), envir = mtcars) #> 20.09062

`+` <- function(a, b) {paste0(a, b)}
eval(expr(x + y), envir = env(x = 1, y = 10, `+` = `+`)) #> 110

Podemos usar rlang::exec() para criar uma call igual à call2() e avaliá-la imediatamente.

7.10.2 3. Tidy Evaluation

Essas ferramentas de avaliação do rlang formam o tidy evaluation framework, que é a base do tidyverse. É uma maneira de avaliar código non-standard.

Por isso, não são todas as funções que a suportam. Use exec(), inject(), list2(), e eval_tidy() para trazer suporte a tidy evaluation.

eval_tidy() está presente na maior parte das funções do tidyverse, e é dessa forma que pegamos uma expressão, avaliamos ela no contexto de um dataframe e seu ambiente pai também é definido.

Podemos usar pronomes .data$ e .env$ para clarificar de qual dessas duas coisas queremos tirar as variáveis.

7.10.3 3. Outros Pontos

É muito comum usar !!! em conjunto com ..., similar à ** do python:

dfs <- list(mtcars, mtcars)
dplyr::bind_rows(!!!dfs)

E para fazer algo como o abaixo, precisamos de uma syntaxe especial para o R não brigar com a gente:

a <- sym(column1)
tibble(!!a = 1:10) #> erro
tibble(!!a := 1:10)

Para suportar ambos esses comportamentos, funções precisam colocar ... em list2(), aí dizemos que suportam “tidy dots”.

7.10.4 Estudo de Caso

Como usar isso para resolver problemas?

Vamos criar uma função a la dplyr::filter(), como filter2(mtcars, cyl == 4).

filter2 <- function(data, condition) {
  keep <- eval_tidy(enquo(condition), data)
  data[keep, ]
}

7.10.5 Estudo de Caso

Como usar isso para resolver problemas?

Vamos criar um wrapper de dplyr::mutate(), que pegue uma coluna e eleve ao quadrado.

mutate_squared <- function(data, col) {
  col <- ensym(col)
  dplyr::mutate(data, !!col := !!col ^ 2)
}

Acontece que esse é um padrão muito comum, e temos um atalho:

mutate_squared <- function(data, col) {
  dplyr::mutate(data, {{col}} := {{col}} ^ 2)
}

Complemento

Recapitulando

[a fazer]


Dicionário de Funções

[a fazer]


Referências

[a fazer]

@@bs4-math@@