As funções foram introduzidas no conceito Noções básicas, com exemplos como este:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Se olharmos com mais atenção para a definição de squareit, conseguimos identificar várias partes:
x.{ }.squareit.Uma função é um objeto de primeira classe em R, tal como os números e as strings.
Assim, squareit <- function... é uma atribuição sintaticamente igual a x <- 42.
Aceder aos componentes de uma função é raro no uso normal, mas bastante fácil.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Os argumentos obtidos com formals() parecem uma lista, com a sintaxe $x.
Na verdade, o tipo é pairlist, um tipo particular de lista que contém pares chave-valor.
O body é código executável, com o tipo language (provavelmente não precisas de te preocupar com isto).
O R não faz uma distinção clara entre argumentos posicionais e argumentos nomeados, ao contrário de outras linguagens de scripting como Python e Julia.
As chamadas a funções podem passar valores de forma posicional ou pelo nome. Esta última é útil para funções complexas com muitos argumentos, em que é difícil lembrarmo-nos da ordem.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
Os valores predefinidos dos argumentos podem ser especificados na definição da função, mas têm de vir depois de todos os argumentos sem valor predefinido.
Podemos então escolher aceitar o valor predefinido ou substituí-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, usa ... (reticências) na definição.
É possível converter quaisquer valores extra da chamada à função num vetor, mas continua a ler para descobrires uma forma alternativa de usar estes «dot args» (chamados «varargs» em várias outras linguagens).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Anteriormente, dissemos 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.
Isto 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 à função passa x = 5 à função exterior, e esse valor fica disponível dentro do corpo dessa função.
A função interior é parte do corpo da função exterior e tem acesso ao valor de x.
Por outras palavras, x = 5 está no ambiente da função interior.
Tecnicamente, isto é conhecido como um fecho.
O ambiente é particularmente importante com os «dot args», uma vez que quaisquer valores fornecidos desta forma podem ser passados para chamadas a funções dentro do corpo da função. A função exterior não precisa de saber nem de se preocupar com o significado dos «dot args».
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
Esta técnica é amplamente usada pelas bibliotecas do Tidyverse, como a stringr.
Muitas das funções da stringr são uma camada amigável por cima de funções de baixo nível da stringi e do R base.
Os argumentos extra fornecidos às funções str_*() são simplesmente passados para essas funções de nível inferior.
Uma função exterior pode definir uma função interior (com nome ou anónima, como se descreve a seguir) e usá-la como valor devolvido.
A função devolvida 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 ligamos o objeto função resultante a uma variável:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Isto torna mais fácil usar a função mais adiante no script, mas essa ligação não é necessária. Uma função curta, usada uma única vez, pode ser útil no contexto imediato. Sem uma ligação a um nome, chama-se 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 as definir: substitui a palavra function por uma barra invertida \.
Foi usado um exemplo disto na secção anterior, sobre valores devolvidos.
O 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 o devolver, obtemos um vetor modificado.
Mas o que aconteceu ao 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
O R é uma linguagem concebida para a ciência de dados. Recolher esses dados pode custar muito tempo, esforço e, potencialmente, uma quantia de dinheiro assustadora: é importante não os corromper!
A política geral (com algumas exceções) é a cópia na modificação. Se um objeto (como um vetor) for alterado de uma forma que possa causar problemas mais tarde, o R devolve uma cópia modificada e deixa o original intacto.
Copiar grandes estruturas de dados pode ser computacionalmente dispendioso, mas, em geral, é o mal menor quando a alternativa é a corrupção dos dados.
Tem cuidado com as operações que levam à cópia repetida dos mesmos dados.
Parafraseando (o autor do Tidyverse) Hadley Wickham:
Os ciclos não são lentos por natureza, mas tornam perigosamente fácil incluir operações lentas dentro do ciclo.
A vetorização ou as funções de ordem superior podem ajudar a proteger-te deste tipo de problema de desempenho.