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.
Por lo tanto, squareit <- function... es una asignación que, sintácticamente, es igual que x <- 42.
R no distingue claramente entre argumentos posicionales y argumentos con 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 es útil para funciones complejas con muchos argumentos, en las que resulta 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 tengan valor predeterminado.
Así, 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 ... (puntos suspensivos) en la definición.
Es posible convertir en un vector los valores adicionales que se pasan en la llamada a la función, pero sigue leyendo para descubrir otra forma de usar estos «dot args» (llamados “varargs” en otros lenguajes).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Antes hemos dicho que tanto los argumentos formales como el cuerpo son 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 ese valor está disponible dentro del cuerpo de esa 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 clausura.
El entorno es especialmente importante con los «dot args», ya que los valores que se pasan de esta forma se pueden reenviar 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 mucho las bibliotecas de Tidyverse como stringr.
Muchas de las funciones de stringr son una envoltura fácil de usar alrededor de funciones de bajo nivel de stringi y del R base.
Los argumentos adicionales que se pasan a las funciones str_*() simplemente se reenvían a esas funciones de nivel inferior.
Una función externa puede definir una función interna (ya sea 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. Sin una vinculación de nombre, se la llama 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 los 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 que quita el hipo: ¡es importante no corromperlos!
La política general (con algunas excepciones) es la copia al modificar. Si se cambia un objeto (como un vector) 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, por lo general, es el mal menor cuando la alternativa es la corrupción de los datos.
Ten cuidado con las operaciones que provocan que se copien repetidamente los mismos datos.
Parafraseando a Hadley Wickham (autor de Tidyverse):
Los bucles no son intrínsecamente lentos, 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 asesino del rendimiento.
Tu empresa ha estado trabajando con datos de mapas para crear páginas web. Tras algunas conversaciones, se ha decidido empezar a usar R para realizar algunos cálculos de forma dinámica.
Intenta trabajar directamente con los puntos vectoriales: no hace falta desglosarlos en coordenadas x e y separadas.
Implementa la función scale(point, s), que recibe un punto con un número arbitrario de dimensiones y lo escala según el valor predefinido s.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Implementa la función translate(point, ...), que devuelve un nuevo punto, con cada coordenada desplazada por los valores de los puntos suspensivos.
Esta función debe poder manejar puntos de cualquier dimensión, con un número de argumentos en los puntos suspensivos que coincida con dicha dimensión.
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
Algunos de tus compañeros tienen menos experiencia con R, así que decides usar una clausura de función para crear transformaciones reutilizables para pares de coordenadas {x, y}.
Implementa la función transform2d(dx, dy, s), que devuelve una función que hace uso de una clausura para realizar una traslación y un escalado repetibles de un punto en 2D.
El argumento de escalado s debe ser opcional, con un valor predeterminado de 1.
La función devuelta debe recibir un punto 2D y, a continuación:
dx y dy
s
# 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
El orden de las operaciones es importante: trasladar y luego escalar da el resultado correcto; escalar y luego trasladar, no. En lenguaje técnico, las operaciones no conmutan.
Los datos de mapas también pueden contener alturas, que se usan para añadir sombreados y contornos en pantalla.
Implementa la función transform3d(dx, dy, dz, s), que devuelve una función.
El argumento de escalado s debe ser opcional, con un valor predeterminado de 1.
La función devuelta debe recibir un punto 3D, trasladarlo según los valores predefinidos y luego escalarlo 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
Regístrate en Exercism para aprender y dominar R con 21 conceptos111 ejercicios y mentoría humana real, todo gratis.