6 Funções e Ambientes
este capítulo está em construção. O que segue abaixo é apenas um rascunho.
6.1 Introdução
Referência básica: Advanced R (2e) cap. 6, cap. 7, and cap. 13.
Na aula de hoje, aprenderemos alguns dos conceitos mais técnicos do curso. Mas, é a aula de funções, vocês ja aprenderam objetos, então é isso, acabou.
Haverão três aprendizados principais:
(i) o que, no detalhe, são funções;
(ii) o que são ambientes;
(iii) como funções buscam por valores;
(iv) o que são funções genéricas, a OOP do R.
6.2 Fundamentos das Funções
6.2.1 Criação
Funções no R são expressões que salvamos como um objeto, para utilizá-las repetidas vezes, variando seus argumentos.
Elas são criadas com a seguinte sintaxe:
fun_name <- function(x, y) {x + y}
fun_name <- \(x, y) {x + y}
Para acessar o código fonte de uma função, digite seu nome (sem parênteses) no console.
6.2.2 Funções de Primeira Classe
Funções de primeira classe
No R, funções são objetos como qualquer outro, e são tratados da mesma forma. Tem seu tipo, podem ser salvas à variáveis ou em listas, podem receber atributos, etc.
Exemplo de funções em listas:
funs <- list(
halve = function(x) x / 2,
double = function(x) x * 2
)
funs$double(10) #> 20
6.2.3 Tipos
Funções tem tipo “closure”.
As únicas exceções existem no R base, e são escritas em C em vez de R, por performance. Elas são as funções primitivas, e seu tipo é ou “builtin” ou “primitive”.
6.2.4 Componentes
Funções têm três componentes:
- Seus argumentos (
x
ey
), acessados comformals()
- Seu corpo (
x + y
), acessado combody()
- Seu environment, um objeto que determina onde a função deve procurar os valores das variáveis relevantes, acessado com
environment()
. Mais sobre em breve.
As funções primitivas não tem os componentes: sum #> .Primitive("sum")
.
Funções closures
No R, funções são closures, porque enclose seus ambientes. O lugar onde buscaram valores necessários é definido na hora da criação.
6.2.5 Formatos
No R, “Everything that exists is an object. Everything that happens is a function call”. Já tatuaram?
A segunda parte do slogan fica clara ao saber que operadores e keywords também são funções, apenas com outro formato.
Quais são os formatos possíveis?
Prefix: o formato “padrão”
sum(x, y)
.-
Infix: a função vai no meio de seus argumentos,
x + y
.- Modo prefix:
`+`(x, y)
. - Para definir use um nome que comece e termine com “%”:
`%&%` <- \(x, y) paste(x, y)
.
- Modo prefix:
-
Replacement: funções que alterem valores de um objeto:
names(df) <- ...
.- Modo prefix:
`names<-`(df, ...)
. - Veja como definir.
- Modo prefix:
-
Special: funções muito importantes para o R, cada uma com uma forma própria, como
[[
,if
, andfor
.-
`if`(x > 0, x, x * (-1))
. -
`for`(var, seq, action)
. -
`next`()
, e`break`()
.
-
6.2.6 Encadeando Funções
Existem três maneiras de combinar funções:
-
Nesting:
g(f(x))
,f
realiza sua operação emx
, e o resultado vira argumento parag
.- É enxuto para pequenas expressões, mas pode ficar confuso em expressões muito longas.
-
Objetos intermediários:
y <- f(x); z <- g(y)
.- Bom quando os valores intermediários são importantes, mas desnecessário quando não são.
-
Piping: os operadores
|>
emagrittr::`%>%`
chamam “pipe” (cano). Eles transformam a avaliação de “dentro para fora” em “da esquerda pra direita”:f(x) %>% g()
.- Facilita a leitura de expressões longas, mas só serve quando é apenas um objeto que irá “avançar” ao longo da função.
- Ideal quando as funções recebem e retornam a mesma classe de objeto.
6.2.7 Resultados
Todas as funções retornam algo:
-
return()
é usado para definir o resultado explicitamente, e encerrar a execução da função. Útil em conjunto com if & else. - Caso contrário, a função retorna o valor de sua última linha.
-
invisible()
pode ser usado para retornar um valor sem imprimi-lo.
Isso mesmo, funções que você achava que só realizavam um efeito colateral (print()
), retornam algo invisivelmente – (a <- 2) #> [1] 2
.
6.2.8 Resultados - Exit Handlers
Uma função termina sua execução se:
- Encontrar um
return()
ou rodar sua última linha. - Encontrar um erro (
stop()
) não handled, ou encontrar avisos e mensagens lidadas portryCatch()
.
Podemos usar a função on.exit()
para rodar uma expressão quando uma função finalizar, independente do motivo.
6.2.9 Argumentos
Funções podem ter argumentos padrão: f <- \(x, a = 2) x^a
. Mais sobre as capacidades disso à frente.
É muito comum usar NULL
para um argumento padrão, junto com %||%
.
Funções podem ter o argumento especial ...
pronunciado “dot-dot-dot”. Ele é útil em vários casos:
- Sua função é um wrapper de outra função –
\(x, ...) <- replicate(rnorm(...), x)
. - Sua função recebe uma função como variável –
lapply()
. - Funções genéricas (mais sobre isso no futuro).
6.2.10 Argumentos Em Listas
Muitas vezes temos argumentos em uma lista, que queremos passar para uma função. Podemos fazer isso com a função do.call()
:
Note que podemos passar argumentos nomeados e não nomeados.
Similar ao **
do python, o pacote rlang oferece um operador !!!
de desempacotar/injetar argumentos em uma call de função:
Vamos falar mais sobre isso no futuro.
6.2.11 Pacotes
Um package é um conjunto de funções com um tema/objetivo específico. Eles contém uma documentação explicativa e as vezes datasets para exemplos.
O CRAN armazena pacotes que passam testes específicos, mas qualquer um pode criar e postar um pacote no GitHub. Vide install.packages()
e o pacote “devtools”.
Após instalar, para carregar as funções do pacote, use library(pkg_name)
.
Para aprender como criar seu próprio pacote, vide este tutorial do MIT e o livro R Packages, 2nd Edition, também de Hadley Wickham.
O R base conta com 7 pacotes built-in, que estão sempre carregadas no R:
- “base” (a fundação do R, que inicia todos os outros pacotes).
- “graphics” e “grDevices” (funções gráficas).
- “utils” (funções utilitárias).
- “methods” (funções para OOP).
- “datasets” (bases de dados para treino).
- “stats” (funções estatísticas).
6.3 Ambientes
6.3.1 Definição
Ambientes
Ambientes são um tipo de objeto no R, que armazenam variáveis, e empoderam o sistema de scoping (i.e. como funções buscam por valores).
Eles são similares à listas nomeadas, onde:
- Todos os nomes devem ser únicos.
- Não existe uma ordem nos elementos.
- Todo ambiente tem um um “ambiente pai” (como uma lista dentro de outra).
- São o único tipo de objeto que não é copied when modified.
Podemos criar um ambiente com rlang::env()
. Podemos “printar” os conteúdos de um ambiente com rlang::env_print()
. rlang::new_environment()
permite escolher o pai.
6.3.2 Ambientes Importantes
A maioria dos ambientes são criados pelo R. Em ordem de senioridade:
Ambiente vazio – chamado como R_EmptyEnv, é o único ambiente que não tem pai, serve basicamente para denotar o começo da ordem.
Ambiente base – o ambiente do pacote base. As funções definidas nele são utilizadas, inclusive para iniciar o resto do R.
Ambientes de pacotes – cada pacote armazena funções em seu próprio ambiente. A ordem é a ordem dos
library()
s.
A maioria dos ambientes são criados pelo R. Em ordem de senioridade:
Ambiente global – chamado como R_GlobalEnv, é o ambiente onde o usuário define suas variáveis.
Ambientes de funções – quando uma função executa, ela cria um ambiente temporário para salvar suas variáveis. Mais sobre isso à frente.
6.3.3 Outros Tópicos
Podemos utilizar o operador <<-
“super assignment” para salvar uma variável no ambiente pai. Assim como a função assign()
para salvar uma variável em qualquer ambiente. O uso disso é raro.
Ambientes são objetos, como qualquer outro, e seus conteúdos podem ser acessados e alterados por [[
e $
.
A relação entre um nome e seu valor é chamada de bind. Afora a relação que vocês ja conhecem de variáveis, podem haver outras duas mais avançadas.
Os pacotes, além do ambiente que armazena as funções, também tem um ambiente especial chamado de “namespace”, que descreve como cada função deve procurar suas variáveis. Mais sobre isso a frente.
6.3.4 Funções Úteis
Use rlang::env_parent()
para achar o pai de um ambiente, e rlang::current_env()
para achar o ambiente atual.
6.4 Scoping e Evaluation
Na aula 1, falamos sobre como o R associa valores à um nome (atribuição), agora, falaremos sobre como o R acha o valor associado à um nome (scoping). No R, utilizamos o lexical scoping.
Ao encontrar um nome em seu corpo, como uma função procura por seu valor? Quando esse valor é acessado?
Adicionalmente, como, especialmente, os argumentos serão avaliados?
6.4.1 Como
Princípios do “como”:
Funções procuram nomes em ambientes. Começam de um específico (já te conto qual), se não acham, buscam no ambiente pai, e vão subindo até o último ambiente (
R_EmptyEnv
).Essa lógica é utilizada para procurar todos os nomes, inclusive funções.
e1 = new_environment(data = list(x = 1), parent = empty_env())
e2 = new_environment(data = list(), parent = e1)
e3 = new_environment(data = list(), parent = e2)
f1 <- \() x
environment(f1) #<environment: R_GlobalEnv>
environment(f1) <- e3
f1()
env = new_environment(data = list(`+` = \(a, b) paste0(a, b)))
f1 <- \(a, b) a + b
environment(f1) <- env
f1("oi", "olá")
6.4.2 Onde
Princípios do “onde”:
Static scope – o ambiente pai do primeiro é o ambiente que a função carrega. Não se usa o ambiente de onde a função foi chamada.
Name masking – o primeiro ambiente é criado pela função, onde serão salvos seus argumentos e variáveis definidas em seu corpo. Não se usa o ambiente que a função carrega.
Fresh start – esse ambiente de execução é temporário, a função nunca “lembra” nada da última vez que foi chamada.
Podemos utilizar o operador <<-
para a função gerar um efeito colateral no ambiente que carrega, “alterando” (em termos práticos) o ponto 3. Isso não costuma ser uma boa prática.
a <- 1
f1 <- function() a
f2 <- function() {
a <- 2
f1()
}
f2()
i <- 1
f1 <- function() {
i <- i + 1
i
}
f1()
f1()
6.4.3 Quando
Princípios do “quando”:
Dynamic lookup – funções buscam os valores na hora da chamada, não quando são criadas. Só salvamos o ambiente, não “congelamos” seu conteúdo.
Lazy evaluation – argumentos são salvados como uma expressão, que só é avaliada quando acessada.
Lazy evaluation é empoderada por um tipo de dado chamado promessa: uma expressão (que carrega o ambiente onde deve ser avaliada, similar às closures), mas que, uma vez acessada, colapsa e “vira” o valor.
a <- 1
f1 <- function() a
f1()
a <- 2
f1()
f <- function(x = 1, y = x * 2, z = a + 2) {
a <- 3
c(x, y, z)
}
6.5 Funções Genéricas (OOP)
6.5.1 Funções Genéricas
Vamos falar mais sobre o sistema de OOP no R, S3, na aula que vem, mas vou contar o principal conceito de OOP no R: funções genéricas.
Algumas funções no R assumem uma forma, um método, diferente a depender da classe do seu argumento (polimorfismo). Ex: summary()
.
Os métodos são funções cujo nome junta o nome da genérica e da classe, funname.classname
. Note que .
não é um caracter especial no R, mas é utilizado para denotar métodos.
6.5.2 Method Dispatch
Method dispatch: Essas funções genéricas são apenas um intermediário, que recebem argumentos, descobrem qual é o método adequado, e passam os argumentos a diante.
Quem faz o method dispatch é a função UseMethod()
. Ela olha para a classe de x
, e busca o método mean.classofx
.
mean
#> function (x, ...) UseMethod("mean")
No S3, o método “mora” na função, e não na classe!
6.5.3 Default e Herança
Se não há nenhum método para a classe “classofx”, existe a pseudo-classe “default”, que é um fallback para todas as classes.
O R também tem um sistema de herança. Bem simples, classes podem ser vetores:
UseMethod()
vai procurar por funname.tbl_df
, depois funname.tbl
, depois, funname.data.frame
, e por fim, funname.default
. É como se todos os objetos herdassem “default”. Nesse processo, também é utilizada a função NextMethod()
6.6 Outros Tópicos em Ambientes
6.6.1 Caller Environment
Ok, funções carregam seu próprio ambiente, independente de onde foram chamadas. Ainda assim, podemos estar interessados no caller environment.
Dentro de uma função, podemos acessar o ambiente onde ela foi chamada com rlang::caller_env()
.
Podemos utilizar isso para criar funções que dependam de onde foram chamadas, funções com dynamic scope.
Haverá um material explicando o que empodera rlang::caller_env()
no livro. Saber isso não é súper prático, mas ensina um conceito de programação importante, o de Call Stack (e Call Stack Trees).
Apenas note que, para denotar calls, e o fato de que uma é “pai” de outra, existe um conceito similar ao de ambientes, chamado “frame”.
6.6.2 Ambientes como Dado
Na aula 1, comentamos que utilizar copy-on-modify era uma escolha de design, mas as vezes é útil ter um dado que modifies-in-place.
Como ambientes tem essa característica, existe o pacote r6 que implementa uma classe criada em cima dos ambientes. Também é um pacote sobre OOP, e será explicado no futuro.
Complemento
Recapitulando
Funções:
- São expressões que dependem de argumentos.
- No R, funções são de primeira classe, são objetos como qualquer outro
- No R, funções são closures, carregam um ambiente onde deve-se avaliar suas variáveis.
- Temos também as funções primitivas.
- Têm três componentes, argumentos, corpo, e ambiente.
Funções:
- Podem vir em três formatos, prefix, infix, replacement, e special.
- Podem ser encadeadas com nesting, objetos intermediários, ou piping.
- Sempre retornam algo.
- Encerram ao encontrar sua última linha, um return(), ou em certos contextos de condições.
Funções:
- Podem ter argumentos padrões, que podem depender de outros argumentos.
- Podem recever varargs ``.
- Podem receber argumentos em listas via
do.call()
ourlang::`!!!`
. - São aglomeradas em pacotes.
- Podem ser anônimas.
- Podem ser recursivas.
Ambientes:
- Ambientes são um tipo de objeto no R, que armazenam variáveis.
- Similares à listas, mas com nomes únicos, sem ordem, carregam um ambiente pai, e são modified-in-place.
- Temos, em ordem: o ambiente vazio, base, de pacotes, global, de funções.
Scoping e Evaluation:
- Scoping é o processo de encontrar o valor de um nome.
- Como: procura-se em ambientes, subindo até achar; vale para tudo, inclusive funções.
- Onde: static scope (o ambiente pai é o que a função carrega); name masking (mas o primeiro é criado pela função, com seus argumentos e corpo); fresh start (o ambiente de execução é temporário).
- Quando: dynamic lookup (só quando acessados); lazy evaluation (o mesmo vale para argumentos, através de promessas).
- Podemos criar uma função com dynamic scope acessando o ambiente de onde a função foi chamada, o
rlang::caller_env()
.
Funções Genéricas:
- Algumas funções no R assumem uma forma, um método, diferente a depender da classe do seu argumento.
- Os métodos são funções cujo nome junta o nome da genérica e da classe, separados por um ponto
.
. - O método adequado é encontrado no processo chamado method dispatch, feito pela função
UseMethod()
. - Objetos podem ter sub-classes (herança), e existe a pseudo-classe “default”, fallback de todos os objetos.
-
UseMethod()
vai procurar por todas as classes (e “default”), até achar um método, e dar erro caso não ache nenhum.
Dicionário de Funções
Categoria | Função | Descrição |
---|---|---|
functions |
function() , formals() , body() , environment()
|
function creation and components |
functions |
return() , invisible() , on.exit()
|
function results |
operadores |
|> , magrittr::`%>%`
|
pipes |
outros |
do.call()
|
calling functions |
functions |
library() , install.packages()
|
installing and loading packages |
ambientes |
rlang::env() , rlang::new_environment() , rlang::env_print()
|
creating and printing environments |
ambientes |
<<- , assign()
|
assignment with envrironments |
generics |
UseMethod() , NextMethod() , sloop::s3_dispatch()
|
method dispatch |
generics |
sloop::s3_methods_generic() , sloop::s3_methods_class()
|
finding methods |
generics |
sloop::s3_get_method()
|
finding source code of method |
ambientes |
rlang::env_parent() , rlang::current_env() , rlang::caller_env()
|
finding environments from context |