4.1 Funciones e iteración
“To understand computations in R, two slogans are helpful:
* Everything that exists is an object.
* Everything that happens is a function call.”
— John Chambers
Funciones
En R todas las operaciones son producto de la llamada a una función, esto
incluye operaciones como +
, operadores que controlan flujo como for
, if
y
while
, e incluso operadores para obtener subconjuntos como [ ]
y $
.
a <- 3
b <- 4
a + b
#> [1] 7
`+`(a, b)
#> [1] 7
for (i in 1:2) print(i)
#> [1] 1
#> [1] 2
`for`(i, 1:2, print(i))
#> [1] 1
#> [1] 2
Para escribir código eficiente y fácil de leer es importante saber escribir funciones, se dice que si hiciste copy-paste de una sección de tu código 3 o más veces es momento de escribir una función.
Escribimos una función para calcular un promedio ponderado:
Notemos que esta función recibe hasta dos argumentos:
x
: el vector a partir del cual calcularemos el promedio ywt
: un vector de ponderadores para cada componente del vectorx
.
Notemos además que al segundo argumento le asignamos un valor predeterminado, esto implica que si no especificamos los ponderadores la función usará el valor predeterminado y promediara con mismo peso a todas las componentes.
Veamos como escribir una función que reciba un vector y devuelva el mismo vector centrado en cero.
- Comenzamos escribiendo el código para un caso particular, por ejemplo, reescalando el vector \((0, 5, 10)\).
Una vez que lo probamos lo convertimos en función:
Ejercicio
Escribe una función que reciba un vector y devuelva el
mismo vector reescalado al rango 0 a 1. Comienza escribiendo el código para un
caso particular, por ejemplo, empieza reescalando el vector
. Tip: la función range()
devuelve el rango de un
vector.
Estructura de una función
Las funciones de R tienen tres partes:
- El cuerpo: el código dentro de la función
- Los formales: la lista de argumentos que controlan como puedes llamar a la función,
- El ambiente: el mapeo de la ubicación de las variables de la función, cómo busca la función cada función el valor de las variables que usa.
Veamos mas ejemplos, ¿qué regresan las siguientes funciones?
# 1
x <- 5
f <- function(){
y <- 10
c(x = x, y = y)
}
rm(x, f)
# 2
x <- 5
g <- function(){
x <- 20
y <- 10
c(x = x, y = y)
}
rm(x, g)
# 3
x <- 5
h <- function(){
y <- 10
i <- function(){
z <- 20
c(x = x, y = y, z = z)
}
i()
}
# 4 ¿qué ocurre si la corremos por segunda vez?
j <- function(){
if (!exists("a")){
a <- 5
} else{
a <- a + 1
}
print(a)
}
x <- 0
y <- 10
# 5 ¿qué regresa k()? ¿y k()()?
k <- function(){
x <- 1
function(){
y <- 2
x + y
}
}
Las reglas de búsqueda determinan como se busca el valor de una variable libre en una función. A nivel lenguaje R usa lexical scoping, esto implica que en R los valores de los símbolos se basan en como se anidan las funciones cuando fueron creadas y no en como son llamadas.
Las reglas de bússqueda de R, lexical scoping, son:
- Enmascaramiento de nombres: los nombres definidos dentro de una función enmascaran aquellos definidos fuera.
Si un nombre no está definido R busca un nivel arriba,
Y lo mismo ocurre cuando una función está definida dentro de una función.
x <- 5
h <- function(){
y <- 10
i <- function(){
z <- 20
c(x = x, y = y, z = z)
}
i()
}
h()
#> x y z
#> 5 10 20
Y cuando una función crea otra función:
- Funciones o variables: en R las funciones son objetos, sin embargo una función y un objeto no-función pueden llamarse igual. En estos casos usamos un nombre en el llamado de una función se buscará únicamente entre los objetos de tipo función.
- Cada vez que llamamos una función es un ambiente limpio, es decir, los objetos que se crean durante la llamada de la función no se pasan a las llamadas posteriores.
# 4 ¿qué ocurre si la corremos por segunda vez?
j <- function(){
if (!exists("a")) {
a <- 5
} else{
a <- a + 1
}
print(a)
}
j()
#> [1] 4
j()
#> [1] 4
- Búsqueda dinámica: la búsqueda lexica determina donde se busca un valor más no determina cuando. En el caso de R los valores se buscan cuando la función se llama, y no cuando la función se crea.
Las reglas de búsqueda de R lo hacen muy flexible pero también propenso a
cometer errores. Una función que suele resultar útil para revisar las
dependencias de nuestras funciones es findGlobals()
en el paquete codetools
,
esta función enlista las dependencias dentro de una función:
Observaciones del uso de funciones
- Cuando llamamos a una función podemos especificar los argumentos en base a posición, nombre completo o nombre parcial:
f <- function(abcdef, bcde1, bcde2) {
c(a = abcdef, b1 = bcde1, b2 = bcde2)
}
# Posición
f(1, 2, 3)
#> a b1 b2
#> 1 2 3
f(2, 3, abcdef = 1)
#> a b1 b2
#> 1 2 3
# Podemos abreviar el nombre de los argumentos
f(2, 3, a = 1)
#> a b1 b2
#> 1 2 3
# Siempre y cuando la abreviación no sea ambigua
f(1, 3, b = 1)
#> Error in f(1, 3, b = 1): argument 3 matches multiple formal arguments
- Los argumentos de las funciones en R se evalúan conforme se necesitan (lazy evaluation),
La función anterior nunca utiliza el argumento b, de tal manera que f(2)
no produce ningún error.
- Funciones con el mismo nombre en distintos paquetes:
La función filter()
(incluida en R base) aplica un filtro lineal a una serie
de tiempo de una variable.
x <- 1:100
filter(x, rep(1, 3))
#> Error in UseMethod("filter_"): no applicable method for 'filter_' applied to an object of class "c('integer', 'numeric')"
Ahora cargamos dplyr
.
library(dplyr)
filter(x, rep(1, 3))
#> Error in UseMethod("filter_"): no applicable method for 'filter_' applied to an object of class "c('integer', 'numeric')"
R tiene un conflicto en la función a llamar, nosotros requerimos usar
filter
de stats y no la función filter
de dplyr
. R utiliza por default
la función que pertenece al último paquete que se cargó.
La función search()
nos enlista los paquetes cargados y el orden.
search()
#> [1] ".GlobalEnv" "package:dplyr" "package:forcats"
#> [4] "package:stringr" "package:purrr" "package:readr"
#> [7] "package:tidyr" "package:tibble" "package:ggplot2"
#> [10] "package:tidyverse" "package:stats" "package:graphics"
#> [13] "package:grDevices" "package:utils" "package:datasets"
#> [16] "package:methods" "Autoloads" "package:base"
Una opción es especificar el paquete en la llamada de la función:
stats::filter(x, rep(1, 3))
#> Time Series:
#> Start = 1
#> End = 100
#> Frequency = 1
#> [1] NA 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54
#> [19] 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99 102 105 108
#> [37] 111 114 117 120 123 126 129 132 135 138 141 144 147 150 153 156 159 162
#> [55] 165 168 171 174 177 180 183 186 189 192 195 198 201 204 207 210 213 216
#> [73] 219 222 225 228 231 234 237 240 243 246 249 252 255 258 261 264 267 270
#> [91] 273 276 279 282 285 288 291 294 297 NA
Como alternativa surge el paquete conflicted que alerta cuando hay conflictos y tiene funciones para especificar a que paquete se desea dar preferencia en una sesión de R.