R
Tutorial R: Scopes y closures
Aprende los conceptos clave de scopes y closures en R para gestionar entornos, variables y funciones con estado persistente.
Aprende R y certifícateEntornos global, local y de paquetes
En R, un entorno es un espacio donde se almacenan y organizan variables, funciones y otros objetos. Comprender cómo funcionan los diferentes tipos de entornos es fundamental para escribir código eficiente y evitar errores comunes relacionados con la visibilidad de las variables.
El entorno global
El entorno global es el espacio de trabajo principal en R donde se crean y almacenan objetos cuando trabajamos directamente en la consola o ejecutamos un script. Cuando iniciamos una sesión de R, automáticamente comenzamos a trabajar en este entorno.
Podemos acceder al entorno global mediante la función globalenv()
o simplemente .GlobalEnv
:
# Creamos una variable en el entorno global
x <- 10
# Verificamos que está en el entorno global
exists("x", envir = globalenv()) # Devuelve TRUE
Las variables definidas en el entorno global están disponibles en toda la sesión y pueden ser accedidas desde cualquier parte del código, incluyendo dentro de funciones (aunque con algunas consideraciones que veremos más adelante).
Para ver todos los objetos en el entorno global, podemos usar:
# Lista todos los objetos en el entorno global
ls()
# Alternativa más explícita
ls(envir = .GlobalEnv)
Entornos locales
Cada vez que ejecutamos una función en R, se crea un entorno local específico para esa ejecución. Este entorno temporal contiene:
- Los argumentos pasados a la función
- Las variables locales creadas dentro de la función
Veamos un ejemplo sencillo:
# Variable en el entorno global
mensaje <- "Estoy en el entorno global"
# Definimos una función que crea su propio entorno local
mi_funcion <- function(x) {
# 'x' es un argumento, existe en el entorno local
y <- "Soy una variable local"
# Podemos acceder a 'mensaje' del entorno global
cat("Dentro de la función:\n")
cat("x:", x, "\n")
cat("y:", y, "\n")
cat("mensaje:", mensaje, "\n")
}
# Llamamos a la función
mi_funcion("Hola")
# Intentamos acceder a 'y' (variable local) desde el entorno global
# Esto generará un error
# print(y) # Error: objeto 'y' no encontrado
En este ejemplo, x
e y
solo existen dentro del entorno local de la función. Una vez que la función termina de ejecutarse, este entorno local se elimina y sus variables dejan de existir.
Las variables locales tienen prioridad sobre las variables globales con el mismo nombre:
valor <- 100 # Variable global
duplicar <- function() {
valor <- 5 # Variable local con el mismo nombre
return(valor * 2) # Usa la variable local
}
resultado <- duplicar()
print(resultado) # Muestra 10, no 200
print(valor) # Muestra 100, la variable global no cambió
Entornos de paquetes
Cuando cargamos un paquete en R usando library()
o require()
, estamos añadiendo un nuevo entorno a la cadena de búsqueda. Cada paquete tiene su propio entorno que contiene todas las funciones, datos y otros objetos que proporciona.
# Cargamos el paquete dplyr
library(dplyr)
# Verificamos que está en la ruta de búsqueda
"package:dplyr" %in% search() # Devuelve TRUE
# Vemos la posición de los entornos en la ruta de búsqueda
search()
La función search()
muestra la cadena de búsqueda actual, que es el orden en que R busca objetos en los diferentes entornos. Típicamente, el entorno global está primero, seguido por los entornos de los paquetes cargados.
Cuando cargamos varios paquetes, el orden de carga determina la prioridad en caso de conflictos de nombres:
# Supongamos que dos paquetes tienen una función con el mismo nombre
library(paquete1) # Contiene función 'procesar()'
library(paquete2) # También contiene función 'procesar()'
# R usará la función del último paquete cargado (paquete2)
# Para usar específicamente la del paquete1:
paquete1::procesar()
El operador ::
nos permite acceder explícitamente a funciones de un paquete específico, incluso sin cargarlo con library()
:
# Usar una función de dplyr sin cargar todo el paquete
resultado <- dplyr::filter(mi_dataframe, columna > 10)
Manipulación de entornos
R nos permite examinar y manipular entornos directamente:
# Crear un nuevo entorno
mi_entorno <- new.env()
# Asignar valores en ese entorno
mi_entorno$variable <- "Hola mundo"
assign("otra_variable", 42, envir = mi_entorno)
# Obtener valores de ese entorno
mi_entorno$variable
get("otra_variable", envir = mi_entorno)
# Listar objetos en ese entorno
ls(envir = mi_entorno)
Esta capacidad de crear y manipular entornos es especialmente útil para desarrollar paquetes o crear estructuras de datos complejas con estado interno.
Entornos anidados y herencia
Los entornos en R forman una estructura jerárquica. Cada entorno (excepto el entorno vacío) tiene un entorno padre. Cuando R no encuentra una variable en el entorno actual, la busca en su entorno padre, luego en el padre de ese entorno, y así sucesivamente.
# Creamos dos entornos anidados
entorno_padre <- new.env()
entorno_hijo <- new.env(parent = entorno_padre)
# Asignamos variables
entorno_padre$x <- "Valor en padre"
entorno_hijo$y <- "Valor en hijo"
# Podemos acceder a 'y' directamente desde el entorno hijo
get("y", envir = entorno_hijo) # "Valor en hijo"
# Podemos acceder a 'x' desde el entorno hijo gracias a la herencia
get("x", envir = entorno_hijo) # "Valor en padre"
Esta estructura jerárquica es fundamental para entender cómo R busca variables y resuelve nombres, tema que se explorará con más detalle en la siguiente sección sobre reglas de búsqueda.
Reglas de búsqueda de variables en R
Cuando trabajamos con R, es fundamental entender cómo y dónde busca el lenguaje las variables que utilizamos en nuestro código. Este proceso sigue un conjunto de reglas específicas que determinan la visibilidad y accesibilidad de los objetos.
El mecanismo de búsqueda lexical scoping
R utiliza un sistema llamado lexical scoping (ámbito léxico) para determinar el valor de una variable. Cuando hacemos referencia a una variable, R sigue una secuencia ordenada de pasos para encontrarla:
- Primero busca en el entorno actual (donde se está ejecutando el código)
- Si no la encuentra, busca en el entorno padre
- Continúa la búsqueda hacia arriba en la jerarquía de entornos
- Finalmente, busca en la ruta de búsqueda (search path)
Veamos un ejemplo sencillo:
x <- 5 # Variable en el entorno global
mi_funcion <- function() {
# R primero busca 'x' en este entorno local
# Como no existe aquí, busca en el entorno padre (global)
return(x + 10)
}
mi_funcion() # Devuelve 15
En este caso, R no encuentra x
en el entorno local de la función, así que la busca en el entorno global donde sí está definida.
La función search()
La ruta de búsqueda es la secuencia de entornos donde R busca objetos cuando no los encuentra en el entorno actual. Podemos examinarla con la función search()
:
search()
# Resultado típico:
# [1] ".GlobalEnv" "package:stats" "package:graphics"
# [4] "package:grDevices" "package:utils" "package:datasets"
# [7] "package:methods" "Autoloads" "package:base"
Esta salida nos muestra que R busca primero en el entorno global (.GlobalEnv
), luego en los paquetes cargados, y finalmente en el paquete base.
Reglas de enmascaramiento (masking)
Cuando existen variables con el mismo nombre en diferentes entornos, R utiliza la primera que encuentra siguiendo su ruta de búsqueda. Esto se conoce como enmascaramiento:
mean <- function(x) {
return("Esta no es la función mean original")
}
# Esta llamada usará nuestra versión de mean, no la del paquete base
mean(c(1, 2, 3)) # Devuelve: "Esta no es la función mean original"
# Para usar la función original debemos especificar el paquete
base::mean(c(1, 2, 3)) # Devuelve: 2
Este ejemplo muestra cómo nuestra función mean
en el entorno global enmascara la función mean
del paquete base.
Reglas dentro de funciones
Dentro de una función, R sigue estas reglas específicas:
- 1. Primero busca en los argumentos de la función
- 2. Luego en las variables locales definidas dentro de la función
- 3. Después en el entorno donde la función fue creada (no donde se ejecuta)
- 4. Finalmente sigue la ruta de búsqueda normal
Este tercer punto es crucial y diferencia a R de otros lenguajes:
x <- 10
crear_funcion <- function() {
x <- 20
# Esta función interna "recuerda" que x=20 en su entorno de creación
function() {
return(x)
}
}
f <- crear_funcion()
f() # Devuelve 20, no 10
La función anónima devuelta por crear_funcion()
"recuerda" el valor de x
en el entorno donde fue creada, no en el entorno global.
La función environment()
Podemos examinar el entorno asociado a una función con environment()
:
mi_var <- 100
f1 <- function() {
return(mi_var)
}
# Veamos el entorno de f1
environment(f1) # Muestra: <environment: R_GlobalEnv>
# Podemos incluso cambiar el entorno de una función
nuevo_entorno <- new.env()
nuevo_entorno$mi_var <- 200
environment(f1) <- nuevo_entorno
f1() # Ahora devuelve 200
Reglas para asignación de variables
Las reglas para asignar valores a variables son diferentes de las reglas para buscarlas:
x <- 5
mi_funcion <- function() {
x <- 10 # Crea una variable local, no modifica la global
print(x)
}
mi_funcion() # Imprime: 10
print(x) # Imprime: 5 (la variable global no cambió)
Para modificar una variable en un entorno superior, necesitamos usar el operador de superasignación <<-
:
x <- 5
mi_funcion <- function() {
x <<- 10 # Modifica la variable en el entorno donde fue definida
print(x)
}
mi_funcion() # Imprime: 10
print(x) # Imprime: 10 (la variable global cambió)
Reglas con funciones anidadas
Las reglas de búsqueda se vuelven más interesantes con funciones anidadas:
outer <- function(x) {
y <- x * 2
inner <- function() {
# Busca primero en el entorno local de inner
# Luego en el entorno de outer donde encuentra 'y'
# Finalmente en el entorno global donde encuentra 'z'
return(y + z)
}
return(inner)
}
z <- 10
f <- outer(5) # y = 10 en el entorno de outer
f() # Devuelve 20 (10 + 10)
En este ejemplo, la función inner
accede a:
y
del entorno deouter
z
del entorno global
Comportamiento con paquetes
Cuando usamos funciones de paquetes, las reglas de búsqueda determinan qué versión de una función se utiliza:
# Supongamos que dplyr y plyr tienen funciones llamadas 'arrange'
library(plyr)
library(dplyr) # Cargado después, tiene prioridad
# Esta llamada usará dplyr::arrange
arrange(mi_dataframe, columna)
# Para usar específicamente la versión de plyr
plyr::arrange(mi_dataframe, columna)
Verificación de existencia de variables
Podemos comprobar si una variable existe en un entorno específico:
# Verificar en el entorno actual
exists("mi_variable")
# Verificar solo en el entorno global (sin buscar en padres)
exists("mi_variable", envir = .GlobalEnv, inherits = FALSE)
# Verificar en un paquete específico
exists("filter", envir = as.environment("package:dplyr"), inherits = FALSE)
La comprensión de estas reglas de búsqueda es esencial para evitar errores sutiles en nuestro código y para aprovechar al máximo las capacidades de R para crear estructuras de datos complejas y funciones reutilizables.
Clausuras (closures) y estado persistente
Las clausuras o closures representan uno de los conceptos más potentes y elegantes en R. Una clausura es simplemente una función que captura y retiene el entorno donde fue creada, permitiéndole acceder a variables que existían en ese momento, incluso después de que la función creadora haya terminado su ejecución.
En términos sencillos, una clausura es una función que "recuerda" el entorno donde nació. Esto permite crear funciones con estado persistente, algo que a primera vista podría parecer contradictorio con la naturaleza funcional de R.
Creación básica de clausuras
Para crear una clausura, definimos una función que devuelve otra función:
crear_contador <- function(inicio = 0) {
contador <- inicio # Variable en el entorno de la función externa
function() {
# Esta función interna tiene acceso a 'contador'
contador <<- contador + 1 # Modificamos la variable del entorno externo
return(contador)
}
}
# Creamos dos contadores independientes
contador1 <- crear_contador(5)
contador2 <- crear_contador(100)
contador1() # Devuelve 6
contador1() # Devuelve 7
contador2() # Devuelve 101
contador1() # Devuelve 8
En este ejemplo, cada llamada a crear_contador()
genera un nuevo entorno con su propia variable contador
. La función anónima devuelta mantiene una referencia a ese entorno específico, permitiéndole acceder y modificar la variable contador
cada vez que se invoca.
Inspección de clausuras
Podemos examinar el entorno capturado por una clausura:
contador <- crear_contador(10)
entorno_clausura <- environment(contador)
# Inspeccionamos el contenido del entorno
ls(entorno_clausura) # Muestra: "contador"
get("contador", envir = entorno_clausura) # Devuelve: 10
# Después de llamar a la función
contador() # Incrementa a 11
get("contador", envir = entorno_clausura) # Ahora devuelve: 11
Esta capacidad de inspección nos permite entender mejor cómo funcionan las clausuras internamente.
Clausuras con múltiples variables
Una clausura puede capturar múltiples variables de su entorno de creación:
crear_calculadora <- function(factor_multiplicacion) {
total <- 0
function(valor) {
if (is.null(valor)) return(total) # Devolver el total acumulado
resultado <- valor * factor_multiplicacion
total <<- total + resultado # Actualizar el acumulador
return(resultado)
}
}
# Calculadora que multiplica por 2 y acumula resultados
calc <- crear_calculadora(2)
calc(5) # Devuelve 10, total = 10
calc(3) # Devuelve 6, total = 16
calc(NULL) # Devuelve 16 (el total acumulado)
En este ejemplo, la clausura captura tanto factor_multiplicacion
como total
, manteniendo ambos valores entre llamadas.
Ventajas de las clausuras
Las clausuras ofrecen varias ventajas importantes:
- Encapsulación de datos: Las variables capturadas están protegidas y solo son accesibles a través de la función devuelta.
- Estado persistente: Permiten mantener estado entre llamadas sin usar variables globales.
- Configuración inicial: Facilitan la creación de funciones pre-configuradas.
Aplicaciones prácticas
1. Funciones de fábrica
Las clausuras son ideales para crear "fábricas de funciones" personalizadas:
crear_potencia <- function(exponente) {
function(x) {
x ^ exponente
}
}
cuadrado <- crear_potencia(2)
cubo <- crear_potencia(3)
raiz_cuadrada <- crear_potencia(0.5)
cuadrado(4) # 16
cubo(3) # 27
raiz_cuadrada(9) # 3
Cada función generada está pre-configurada con un exponente específico.
2. Memoización (caché de resultados)
Las clausuras permiten implementar técnicas como la memoización, que almacena resultados de cálculos previos:
crear_funcion_memoizada <- function(fun) {
cache <- list()
function(...) {
# Convertimos los argumentos en una cadena para usarla como clave
args <- list(...)
clave <- paste(args, collapse = "|")
# Verificamos si el resultado ya está en caché
if (!is.null(cache[[clave]])) {
return(cache[[clave]])
}
# Calculamos y almacenamos el resultado
resultado <- fun(...)
cache[[clave]] <<- resultado
return(resultado)
}
}
# Función costosa que queremos memoizar
calculo_costoso <- function(x) {
Sys.sleep(1) # Simulamos un cálculo que toma tiempo
return(x^2)
}
# Creamos versión memoizada
calculo_rapido <- crear_funcion_memoizada(calculo_costoso)
system.time(calculo_rapido(10)) # Primera llamada: toma ~1 segundo
system.time(calculo_rapido(10)) # Segunda llamada: casi instantánea
3. Generadores de secuencias
Podemos crear generadores que producen secuencias bajo demanda:
crear_generador_fibonacci <- function() {
a <- 0
b <- 1
function() {
resultado <- a
siguiente <- a + b
a <<- b
b <<- siguiente
return(resultado)
}
}
fib <- crear_generador_fibonacci()
fib() # 0
fib() # 1
fib() # 1
fib() # 2
fib() # 3
fib() # 5
Consideraciones importantes
Al trabajar con clausuras, debemos tener en cuenta algunos aspectos:
- Uso de
<<-
: El operador de superasignación es crucial para modificar variables en el entorno capturado. - Consumo de memoria: Las clausuras mantienen todo su entorno en memoria, lo que puede ser costoso si contiene objetos grandes.
- Mutabilidad controlada: Aunque R es principalmente funcional, las clausuras permiten un estilo de programación con estado mutable de forma controlada.
Clausuras vs. Objetos
Las clausuras en R proporcionan una forma de programación orientada a objetos funcional:
crear_cuenta_bancaria <- function(saldo_inicial = 0) {
saldo <- saldo_inicial
list(
depositar = function(cantidad) {
saldo <<- saldo + cantidad
invisible(saldo)
},
retirar = function(cantidad) {
if (cantidad > saldo) {
stop("Fondos insuficientes")
}
saldo <<- saldo - cantidad
invisible(saldo)
},
consultar = function() {
return(saldo)
}
)
}
# Creamos una cuenta
mi_cuenta <- crear_cuenta_bancaria(1000)
mi_cuenta$depositar(500)
mi_cuenta$retirar(200)
mi_cuenta$consultar() # 1300
Este patrón es similar a la creación de objetos con métodos en lenguajes orientados a objetos, pero implementado mediante clausuras.
Las clausuras representan una herramienta fundamental en R que permite combinar la elegancia de la programación funcional con la practicidad del estado persistente, ofreciendo soluciones elegantes para numerosos problemas de programación.
Otros ejercicios de programación de R
Evalúa tus conocimientos de esta lección Scopes y closures con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Todas las lecciones de R
Accede a todas las lecciones de R y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De R Y Rstudio
Introducción Y Entorno
Introducción A R
Introducción Y Entorno
Operadores
Sintaxis
Estructuras De Datos
Sintaxis
Funciones
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Scopes Y Closures
Sintaxis
Estructuras De Control Condicional
Sintaxis
Funciones Anónimas
Sintaxis
Tipos De Datos Y Variables
Sintaxis
Sistema R6: Clases Referenciales Y Encapsulamiento
Programación Orientada A Objetos
Sistema S4: Clases Formales Y Validación
Programación Orientada A Objetos
Herencia Y Polimorfismo En R
Programación Orientada A Objetos
Sistemas De Oop En R
Programación Orientada A Objetos
Sistema S3: Clases Implícitas Y Métodos Genéricos
Programación Orientada A Objetos
Tidyverse Para Transformación De Datos
Manipulación De Datos
Lubridate Para Fechas Y Tiempo
Manipulación De Datos
Group_by Y Summarize Para Agrupación Y Resumen
Manipulación De Datos
Stringr Para Expresiones Regulares
Manipulación De Datos
Tidyr Para Limpieza De Valores Faltantes
Manipulación De Datos
Joins En R Para Combinación Y Relaciones De Tablas
Manipulación De Datos
Pivot_longer Y Pivot_wider Para Reestructuración
Manipulación De Datos
Mutate Y Transmute Para Transformación
Manipulación De Datos
Dplyr Para Filtrado Y Selección
Manipulación De Datos
Readr Y Read.csv Para Importar Datos
Manipulación De Datos
Gráficos Bivariantes En R
Visualización De Datos
Gráficos Univariantes En R
Visualización De Datos
Facetas En Ggplot2
Visualización De Datos
Personalización Y Temas
Visualización De Datos
Ggplot2 Para Visualización De Datos
Visualización De Datos
Gráficos Multivariantes En R
Visualización De Datos
Correlación En R
Estadística
Regresión Lineal En R
Estadística
Pruebas De Hipótesis En R
Estadística
Anova En R
Estadística
Estadística Descriptiva En R
Estadística
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender los diferentes tipos de entornos en R: global, local y de paquetes.
- Entender el mecanismo de búsqueda lexical (scoping) y las reglas de enmascaramiento de variables.
- Aprender a manipular entornos y examinar su jerarquía y herencia.
- Conocer el concepto de clausuras (closures) y cómo permiten mantener estado persistente en funciones.
- Aplicar clausuras para crear funciones con estado, memoización y patrones similares a objetos.