R

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ícate

Entornos 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:

  1. Primero busca en el entorno actual (donde se está ejecutando el código)
  2. Si no la encuentra, busca en el entorno padre
  3. Continúa la búsqueda hacia arriba en la jerarquía de entornos
  4. 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 de outer
  • 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.

Aprende R online

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

R

Introducción Y Entorno

Introducción A R

R

Introducción Y Entorno

Operadores

R

Sintaxis

Estructuras De Datos

R

Sintaxis

Funciones

R

Sintaxis

Estructuras De Control Iterativo

R

Sintaxis

Scopes Y Closures

R

Sintaxis

Estructuras De Control Condicional

R

Sintaxis

Funciones Anónimas

R

Sintaxis

Tipos De Datos Y Variables

R

Sintaxis

Sistema R6: Clases Referenciales Y Encapsulamiento

R

Programación Orientada A Objetos

Sistema S4: Clases Formales Y Validación

R

Programación Orientada A Objetos

Herencia Y Polimorfismo En R

R

Programación Orientada A Objetos

Sistemas De Oop En R

R

Programación Orientada A Objetos

Sistema S3: Clases Implícitas Y Métodos Genéricos

R

Programación Orientada A Objetos

Tidyverse Para Transformación De Datos

R

Manipulación De Datos

Lubridate Para Fechas Y Tiempo

R

Manipulación De Datos

Group_by Y Summarize Para Agrupación Y Resumen

R

Manipulación De Datos

Stringr Para Expresiones Regulares

R

Manipulación De Datos

Tidyr Para Limpieza De Valores Faltantes

R

Manipulación De Datos

Joins En R Para Combinación Y Relaciones De Tablas

R

Manipulación De Datos

Pivot_longer Y Pivot_wider Para Reestructuración

R

Manipulación De Datos

Mutate Y Transmute Para Transformación

R

Manipulación De Datos

Dplyr Para Filtrado Y Selección

R

Manipulación De Datos

Readr Y Read.csv Para Importar Datos

R

Manipulación De Datos

Gráficos Bivariantes En R

R

Visualización De Datos

Gráficos Univariantes En R

R

Visualización De Datos

Facetas En Ggplot2

R

Visualización De Datos

Personalización Y Temas

R

Visualización De Datos

Ggplot2 Para Visualización De Datos

R

Visualización De Datos

Gráficos Multivariantes En R

R

Visualización De Datos

Correlación En R

R

Estadística

Regresión Lineal En R

R

Estadística

Pruebas De Hipótesis En R

R

Estadística

Anova En R

R

Estadística

Estadística Descriptiva En R

R

Estadística

Accede GRATIS a R y certifícate

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.