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 e y), acessados com formals()
  • Seu corpo (x + y), acessado com body()
  • 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).
  • Replacement: funções que alterem valores de um objeto: names(df) <- ....

  • Special: funções muito importantes para o R, cada uma com uma forma própria, como [[, if, and for.

    • `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 em x, e o resultado vira argumento para g.
    • É 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 |> e magrittr::`%>%` 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 por tryCatch().

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():

do.call(sum, list(1, 4, 6, NA, 5, na.rm = TRUE)) #> 16

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:

rlang::inject(sum(!!!list(1, 4, 6, NA, 5, na.rm = TRUE))) #> 16

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.2.12 Outros Tópicos

Funções anônimas: note que não precisamos atribuídas um nome à uma função. Muito comum ao passar funções à outras – lapply(mtcars, \(x) x + 1).

Recursividade: funções no R podem chamar elas mesmas. Muitas vezes, usar uma função recursiva é similar à usar um loop while.

recursive_factorial <- function(x) {
  if (x == 0) return(1) else return(x * recursive_factorial(x - 1))
}

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:

  1. Ambiente vazio – chamado como R_EmptyEnv, é o único ambiente que não tem pai, serve basicamente para denotar o começo da ordem.

  2. Ambiente base – o ambiente do pacote base. As funções definidas nele são utilizadas, inclusive para iniciar o resto do R.

  3. 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:

  1. Ambiente global – chamado como R_GlobalEnv, é o ambiente onde o usuário define suas variáveis.

  2. 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”:

  1. 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).

  2. 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”:

  1. 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.

  2. 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.

  3. 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”:

  1. 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.

  2. 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)
}
mean_unif <- \(a, b, sim = FALSE, x = runif(10000, a, b)) {
  if (sim) mean(x) else (a + b) / 2
}

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:

class(tibble::as_tibble(mtcars)) #> "tbl_df" "tbl" "data.frame"

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.5.4 Funções Úteis

  • Para achar os métodos de uma generic, use sloop::s3_methods_generic().
  • Para achar os métodos de uma classe, use sloop::s3_methods_class().
  • Para ver o código fonte de um método, use sloop::s3_get_method().
  • Para ver o processo de dispatch, use sloop::s3_dispatch().

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() ou rlang::`!!!`.
  • 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

Referências

Referência básica: Advanced R (2e) cap. 6, cap. 7, and cap. 13.

@@bs4-math@@