Las funciones ya se presentaron en el concepto Fundamentos, con ejemplos como este:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Si nos fijamos más de cerca en la definición de squareit, podemos identificar varias partes:
x.{ }.squareit.Una función es un objeto de primera clase en R, igual que los números y los strings. Así, squareit <- function... es una asignación que, sintácticamente, es igual que x <- 42.
Acceder a los componentes de una función es poco habitual en el uso normal, pero bastante fácil.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Los argumentos que se obtienen con formals() se parecen a un array, con la sintaxis $x. De hecho, el tipo es pairlist, un tipo particular de array que contiene pares clave-valor.
El body es código ejecutable, con el tipo language (algo de lo que probablemente no necesites preocuparte).
R no hace una distinción clara entre argumentos posicionales y argumentos por nombre, a diferencia de otros lenguajes de scripting como Python y Julia.
Las llamadas a funciones pueden pasar valores de forma posicional o por nombre. Esto último resulta útil en funciones complejas con muchos argumentos, donde es difícil recordar su orden.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
Los valores predeterminados de los argumentos se pueden especificar en la definición de la función, pero deben ir después de todos los argumentos que no tienen valor predeterminado.
Después podemos elegir entre aceptar el valor predeterminado o sobrescribirlo.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Para aceptar un número arbitrario de argumentos adicionales, usa un ... (puntos suspensivos) en la definición. Es posible convertir en un vector los valores extra de la llamada a la función, pero sigue leyendo para ver una forma alternativa de usar estos «dot args» (llamados «varargs» en otros muchos lenguajes).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Antes hemos dicho que los argumentos formales y el cuerpo son ambos componentes de una función.
De hecho, hay un tercer componente: el entorno en el que se define la función.
Esto se puede ilustrar con el caso de las funciones anidadas:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
La llamada a la función pasa x = 5 a la función externa, y este valor está disponible dentro de ese cuerpo de función.
La función interna es parte del cuerpo de la función externa y tiene acceso al valor de x. Dicho de otro modo, x = 5 está en el entorno de la función interna.
Técnicamente, esto se conoce como closure.
El entorno es especialmente importante con los «dot args», ya que cualquier valor que se pase de esta forma se puede transmitir a las llamadas a funciones dentro del cuerpo de la función. La función externa no necesita saber qué significan los «dot args», ni le importa.
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 la usan profusamente las librerías de Tidyverse, como stringr. Muchas de las funciones de stringr son un envoltorio fácil de usar alrededor de funciones de bajo nivel de stringi y de R base. Los argumentos adicionales que se pasan a las funciones str_*() simplemente se transmiten a esas funciones de nivel inferior.
Una función externa puede definir una función interna (con nombre o anónima, como se describe más abajo) y usarla como valor devuelto.
La función devuelta incluirá el entorno en el que se definió.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Cuando definimos una función, normalmente vinculamos el objeto función resultante a una variable:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Esto facilita usar la función más adelante en el script, pero esa vinculación no es necesaria. Una función corta que se usa una sola vez puede ser útil en el contexto inmediato. Si no se vincula a un nombre, se denomina función anónima.
El uso de funciones anónimas es tan común que (desde R v4.1.0) existe una sintaxis abreviada para definirlas: sustituye la palabra function por una barra invertida \.
En la sección anterior, sobre valores devueltos, se usó un ejemplo de esto.
R permite asignar valores a elementos individuales de un vector. Si pasamos un vector como argumento de una función y lo modificamos en el cuerpo de la función antes de devolverlo, obtenemos un vector modificado.
Pero, ¿qué ha pasado con el vector 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 es un lenguaje diseñado para la ciencia de datos. Recopilar esos datos puede costar mucho tiempo, esfuerzo y, potencialmente, una cantidad de dinero desorbitada: ¡es importante no corromperlos!
La política general (con algunas excepciones) es la copia al modificar. Si un objeto (como un vector) se cambia de una forma que podría causar problemas más adelante, R devuelve una copia modificada y deja el original intacto.
Copiar estructuras de datos grandes puede ser costoso desde el punto de vista computacional, pero en general es el mal menor cuando la alternativa es la corrupción de los datos.
Ten cuidado con las operaciones que provocan copias repetidas de los mismos datos.
Parafraseando al autor de Tidyverse, Hadley Wickham:
Los bucles no son lentos por naturaleza, pero hacen que sea peligrosamente fácil incluir operaciones lentas dentro del bucle.
La vectorización o las funciones de orden superior pueden ayudarte a protegerte de este tipo de lastre para el rendimiento.