As funções foram apresentadas lá no conceito Básico, 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.
Assim, squareit <- function... é uma atribuição que, sintaticamente, é igual a x <- 42.
O 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 de forma posicional ou por nome. Esta última é ú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 devem vir depois de todos os argumentos sem valor padrão.
Aí, podemos escolher entre aceitar o padrão 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, use um ... (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"
Antes, dissemos que os argumentos formais e o corpo são ambos componentes de uma função.
Na verdade, há 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 da 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.
Dito de outra forma, x = 5 está no ambiente da função interna.
Tecnicamente, isso é conhecido como closure.
O ambiente é especialmente importante com os dot args, já que quaisquer valores fornecidos dessa forma podem ser repassados para chamadas de função no 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 é muito usada por bibliotecas do Tidyverse, como stringr.
Muitas das funções de stringr são um wrapper amigável em torno de funções de baixo nível do stringi e do R base.
Os argumentos extras fornecidos às funções str_*() são simplesmente repassados para 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 essa vinculação não é necessária. Uma função curta, usada uma única vez, pode ser útil no contexto imediato. Sem a vinculação a um nome, 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.
O R permite atribuir 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
O 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 política geral (com algumas exceções) é copiar ao modificar. Se um objeto (como um vetor) é alterado de uma forma que poderia causar problemas depois, o R retorna uma cópia modificada e deixa o original intacto.
Copiar grandes estruturas de dados pode ser computacionalmente caro, mas geralmente isso é o mal menor quando a alternativa é a corrupção dos dados.
Cuidado com operações que levam à cópia repetida dos mesmos dados.
Parafraseando Hadley Wickham (autor do Tidyverse):
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 funções de ordem superior podem ajudar a proteger você desse tipo de problema que mata o desempenho.
Sua empresa vem trabalhando com dados de mapeamento para construir páginas web. Depois de algumas discussões, foi tomada a decisão de começar a usar R para fazer alguns cálculos dinamicamente.
Procure trabalhar diretamente com os pontos vetoriais: não é preciso separá-los em coordenadas x e y distintas.
Implemente a função scale(point, s), que recebe um ponto com um número arbitrário de dimensões e o escala pelo s predefinido.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Implemente a função translate(point, ...), que retorna um novo ponto, com cada coordenada deslocada pelos valores nas reticências.
Essa função precisa dar conta de pontos de qualquer dimensão, com o número de argumentos das reticências correspondendo a essa dimensão.
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
Alguns dos seus colegas de equipe têm menos experiência com R, então você decide usar um closure de função para criar transformações reutilizáveis para pares de coordenadas {x, y}.
Implemente a função transform2d(dx, dy, s), que retorna uma função que usa um closure para realizar, de forma repetível, a translação e a escala de um ponto 2D.
O argumento de escala s deve ser opcional, com valor padrão 1.
A função retornada deve receber um ponto 2D e então:
dx e dy predefinidoss
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
A ordem das operações é importante: transladar e depois escalar dá o resultado correto; escalar e depois transladar, não. Em linguagem técnica, as operações não comutam.
Os dados de mapeamento também podem conter alturas, usadas para adicionar sombreamento e contornos na tela.
Implemente a função transform3d(dx, dy, dz, s), que retorna uma função.
O argumento de escala s deve ser opcional, com valor padrão 1.
A função retornada deve receber um ponto 3D, transladá-lo pelos valores predefinidos e então escalá-lo por s.
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Crie sua conta no Exercism para aprender e dominar R com 21 conceitos111 exercícios e mentoria humana de verdade, tudo de graça.