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:
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”:
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:
7.3.7 Atalhos
Index map: muitas vezes estamos interessados em um caso especial de map2
, para qual existe o atalho purrr::imap()
:
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]])
, oumap(x, ~ .x[["a"]])
. - Mas o purrr tem um atalho:
map(x, 1)
, oumap(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:
Piping: as funções do purr também funcionam muito bem com piping:
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:
- Considere
lapply()
como substituto demap()
. - Considere
vapply()
como substituto demap_*()
(mais consistente quesapply()
). - Considere
Map()
como substituto depmap()
. Não existe uma versão generalizada devapply()
, apenas desapply()
, amapply()
. -
apply()
é interessante para operar com matrizes ou linhas de um dataframe.
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.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()
: retornaTRUE
se algum elemento de.x
satisfaz.p
. -
every()
: retornaTRUE
se todos os elementos de.x
satisfazem.p
. -
none()
: retornaTRUE
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:
-
integrate()
encontra a área sob a curva definida porf
. -
uniroot()
encontra ondef
atinge zero. -
optimise()
encontra o mínimo/máximo def
.
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.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:
- Podemos capturar código como expressões (e amigos). Tanto de modo interativo, mas também passando-o para funções.
- Podemos criar e alterar código programaticamente.
- 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:
-
expression()
, que sempre retorna uma expression. -
bquote()
erlang::expr()
retornam uma das “sub-expression”.
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.9 Metaprogramação: Alterando
7.9.1 2. Criando e Modificando Calls
Podemos criar calls com call()
e rlang::call2()
:
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 !!
:
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!!!
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:
E para fazer algo como o abaixo, precisamos de uma syntaxe especial para o R não brigar com a gente:
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)
.
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.
Acontece que esse é um padrão muito comum, e temos um atalho:
mutate_squared <- function(data, col) {
dplyr::mutate(data, {{col}} := {{col}} ^ 2)
}