As funções foram apresentadas lá no conceito Básicos, com exemplos como este:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Olhando mais de perto a definição de squareit, podemos identificar várias partes:
x.{ }.squareit.Uma função é um objeto de primeira classe em R, assim como números e strings.
Portanto, squareit <- function... é uma atribuição sintaticamente igual a x <- 42.
Acessar os componentes de uma função é raro no uso normal, mas bem fácil.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Os argumentos obtidos com formals() se parecem com uma lista, com a sintaxe $x.
Na verdade, o tipo é pairlist, um tipo específico de lista que contém pares de chave-valor.
O body é código executável, do tipo language (provavelmente não é algo com que você precise se preocupar).
R não faz uma distinção clara entre argumentos posicionais e argumentos nomeados, ao contrário de outras linguagens de script como Python e Julia.
As chamadas de função podem passar valores por posição ou por nome. Esta última forma é útil para funções complexas com muitos argumentos, em que é difícil lembrar a ordem deles.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
Valores padrão para argumentos podem ser especificados na definição da função, mas precisam vir depois de todos os argumentos sem valor padrão.
Aí podemos escolher entre aceitar o padrão ou sobrescrevê-lo.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Para aceitar um número arbitrário de argumentos adicionais, use ... (reticências) na definição.
É possível converter quaisquer valores extras da chamada de função em um vetor, mas continue lendo para conhecer uma forma alternativa de usar esses "dot args" (chamados de "varargs" em várias outras linguagens).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Vimos antes que os argumentos formais e o corpo são ambos componentes de uma função.
Na verdade, existe um terceiro componente: o ambiente em que a função é definida.
Isso pode ser ilustrado com o caso de funções aninhadas:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
A chamada de função passa x = 5 para a função externa, e esse valor fica disponível dentro do corpo dessa função.
A função interna é parte do corpo da função externa e tem acesso ao valor de x.
Em outras palavras, x = 5 está no ambiente da função interna.
Tecnicamente, isso é conhecido como closure.
O ambiente é especialmente importante com os dot args, pois quaisquer valores fornecidos dessa forma podem ser repassados para chamadas de função dentro do corpo da função. A função externa não precisa saber nem se importar com o que os dot args significam.
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
Essa técnica é usada amplamente por bibliotecas do Tidyverse, como stringr.
Muitas das funções do stringr são uma camada amigável sobre funções de baixo nível do stringi e do R base.
Os argumentos extras fornecidos às funções str_*() são simplesmente repassados a essas funções de baixo nível.
Uma função externa pode definir uma função interna (nomeada ou anônima, como descrito abaixo) e usá-la como valor de retorno.
A função retornada incluirá o ambiente em que foi definida.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Quando definimos uma função, normalmente vinculamos o objeto de função resultante a uma variável:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Isso facilita usar a função mais tarde no script, mas esse vínculo não é necessário. Uma função curta, usada uma única vez, pode ser útil no contexto imediato. Sem um nome vinculado, ela é chamada de função anônima.
O uso de funções anônimas é tão comum que (desde o R v4.1.0) existe uma sintaxe abreviada para defini-las: substitua a palavra function por uma barra invertida \.
Um exemplo disso foi usado na seção anterior, sobre valores de retorno.
R permite atribuir valores a elementos individuais de um vetor. Se passarmos um vetor como argumento de uma função e o modificarmos no corpo da função antes de retorná-lo, obtemos um vetor modificado.
Mas o que aconteceu com o vetor original?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R é uma linguagem projetada para ciência de dados. Coletar esses dados pode custar muito tempo, esforço e, potencialmente, uma quantia exorbitante de dinheiro: é importante não corrompê-los!
A regra geral (com algumas exceções) é cópia na modificação. Se um objeto (como um vetor) é alterado de uma forma que poderia causar problemas depois, R retorna uma cópia modificada e deixa o original intacto.
Copiar grandes estruturas de dados pode ser computacionalmente caro, mas isso geralmente é o mal menor quando a alternativa é a corrupção dos dados.
Cuidado com operações que levam à cópia repetida dos mesmos dados.
Parafraseando (o autor do Tidyverse) Hadley Wickham:
Os laços não são lentos por natureza, mas tornam perigosamente fácil incluir operações lentas dentro do laço.
A vetorização ou as funções de ordem superior podem ajudar a proteger você desse tipo de problema de desempenho.