Ejemplos de los peligros de los globales en R y Stata

En conversaciones recientes con mis compañeros, he estado abogando por evitar los globales excepto para almacenar constantes. Este es un tipo de progtwig típico de estadísticas aplicadas donde todos escriben su propio código y los tamaños de los proyectos son pequeños, por lo que puede ser difícil para las personas ver los problemas causados ​​por los hábitos descuidados.

Al hablar sobre evitar los globales, me estoy enfocando en las siguientes razones por las que los globales pueden causar problemas , pero me gustaría tener algunos ejemplos en R y / o Stata para ir con los principios (y cualquier otro principio que pueda encontrar importante ), y estoy teniendo dificultades para encontrar creíbles.

  • No localidad: los Globals dificultan la depuración porque dificultan la comprensión del flujo de código
  • Acoplamiento implícito: los Globals rompen la simplicidad de la progtwigción funcional al permitir interacciones complejas entre segmentos de código distantes
  • Colisiones del espacio de nombres: los nombres comunes (x, i, etc.) se vuelven a usar, lo que provoca colisiones en el espacio de nombres

Una respuesta útil a esta pregunta sería un fragmento de código reproducible y autónomo en el que los globales causan un tipo específico de problema, idealmente con otro fragmento de código en el que se corrige el problema. Puedo generar las soluciones corregidas si es necesario, por lo que el ejemplo del problema es más importante.

Enlaces relevantes:

Las variables globales son malas

¿Son malas las variables globales?

También tengo el placer de enseñar R a estudiantes de pregrado que no tienen experiencia en progtwigción. El problema que encontré fue que la mayoría de los ejemplos de cuando los globales son malos, son bastante simplistas y realmente no logran entenderse.

En cambio, trato de ilustrar el principio del menor asombro . Utilizo ejemplos en los que es difícil averiguar qué estaba pasando. Aquí hay unos ejemplos:

  1. Pido a la clase que escriba lo que piensan que será el valor final de:

     i = 10 for(i in 1:5) i = i + 1 i 

    Algunos de la clase adivinan correctamente. Entonces pregunto si alguna vez escribir código como este?

    En cierto sentido, i soy una variable global que está siendo modificada.

  2. ¿Qué devuelve el siguiente fragmento de código?

     x = 5:10 x[x=1] 

    El problema es a qué nos referimos exactamente con x

  3. La siguiente función devuelve una variable global o local:

      z = 0 f = function() { if(runif(1) < 0.5) z = 1 return(z) } 

    Respuesta: ambos. Nuevamente discuta por qué esto es malo.

Oh, el maravilloso olor de los globals …

Todas las respuestas en esta publicación dieron ejemplos de R, y el OP también quería algunos ejemplos de Stata. Así que déjame entrar con esto.

A diferencia de R, Stata se ocupa de la localidad de sus macros locales (las que usted crea con local comando local ), por lo que el problema de “¿Es esto una Z global o una Z local que se devuelve?” nunca viene. (Gosh … ¿cómo pueden ustedes R escribir ningún código en absoluto si la localidad no se aplica?) Stata tiene una peculiaridad diferente, a saber, que una macro local o global inexistente se evalúa como una cadena vacía, que puede o no ser deseable.

He visto que los globales se usan por varias razones principales:

  1. Los Globals se usan a menudo como accesos directos para listas de variables, como en

     sysuse auto, clear regress price $myvars 

    Sospecho que el uso principal de tal construcción es para alguien que cambia entre el tipeo interactivo y el almacenamiento del código en un do-archivo, ya que intentan múltiples especificaciones. Supongamos que intentan la regresión con errores estándar homoscedásticos, errores estándar heteroscedásticos y regresión mediana:

     regress price mpg foreign regress price mpg foreign, robust qreg price mpg foreign 

    Y luego ejecutan estas regresiones con otro conjunto de variables, luego con otro, y finalmente se dan por vencidos y lo configuran como un archivo do myreg.do con

     regress price $myvars regress price $myvars, robust qreg price $myvars exit 

    para ser acompañado con un ajuste apropiado de la macro global. Hasta aquí todo bien; el fragmento

     global myvars mpg foreign do myreg 

    produce los resultados deseables. Ahora digamos que envían por correo electrónico su famoso do-file que dice producir muy buenos resultados de regresión a los colaboradores, y les piden que escriban

     do myreg 

    ¿Qué verán sus colaboradores? En el mejor de los casos, la media y la mediana de mpg si comenzaron una nueva instancia de Stata (acoplamiento fallido: myreg.do realmente no sabía que tenía la intención de ejecutar esto con una lista de variables no vacía). Pero si los colaboradores tenían algo en myvars , y también tenía un myvars global definido (colisión de nombre) … hombre, sería un desastre.

  2. Globals se utilizan para nombres de directorios o archivos, como en:

     use $mydir\data1, clear 

    Dios solo sabe lo que se cargará. En proyectos grandes, sin embargo, es útil. Usted querrá definir global mydir en algún lugar en su do-archivo maestro, puede ser incluso como

     global mydir `c(pwd)' 
  3. Globals se puede utilizar para almacenar una basura impredecible, como un comando completo:

     capture $RunThis 

    Dios solo sabe lo que se ejecutará. Este es el peor caso de acoplamiento fuerte implícito, pero dado que ni siquiera estoy seguro de que RunThis contenga algo significativo, puse una capture frente a él y estaré preparado para tratar el código de retorno distinto de cero _rc . (Ver, sin embargo, mi ejemplo a continuación).

  4. El propio uso de Stata de los valores globales es para los entornos de Dios, como el nivel de probabilidad / confianza de error tipo I: el $S_level global siempre está definido (y debes ser un $S_level idiota para redefinir este global, aunque por supuesto es técnicamente posible). Sin embargo, esto es principalmente un problema heredado con el código de la versión 5 y siguientes (más o menos), ya que se puede obtener la misma información de la constante del sistema menos frágil:

     set level 90 display $S_level display c(level) 

Afortunadamente, los globales son bastante explícitos en Stata, y por lo tanto son fáciles de depurar y eliminar. En algunas de las situaciones anteriores, y ciertamente en la primera, querría pasar parámetros a hacer-archivos que se ven como el `0' local `0' local dentro del archivo-do. En lugar de usar globales en el archivo myreg.do , probablemente lo codificaría como

  unab varlist : `0' regress price `varlist' regress price `varlist', robust qreg price `varlist' exit 

La unab cosa servirá como un elemento de protección: si la entrada no es una varlist legal, el progtwig se detendrá con un mensaje de error.

En los peores casos que he visto, el global se usó solo una vez después de haber sido definido.

Hay ocasiones en las que desea utilizar globales, porque de lo contrario tendría que pasar la maldita cosa a cualquier otro archivo do o un progtwig. Un ejemplo en el que encontré que los globales eran bastante inevitables fue la encoding de un estimador de máxima verosimilitud en el que no sabía de antemano cuántas ecuaciones y parámetros tendría. Stata insiste en que el evaluador de probabilidad (proporcionado por el usuario) tendrá ecuaciones específicas. Así que tuve que acumular mis ecuaciones en los globales, y luego llamar a mi evaluador con los globales en las descripciones de la syntax que Stata necesitaría analizar:

 args lf $parameters 

donde lf era la función objective (la logaritmo-verosimilitud). Me encontré con esto al menos dos veces, en el paquete de mezcla normal ( denormix ) y en el paquete de análisis de factores confirmatorios ( confa ); puedes encontrarlos a los dos, por supuesto.

Un ejemplo de R de una variable global que divide la opinión es el problema stringsAsFactors sobre la lectura de datos en R o la creación de un dataframe.

 set.seed(1) str(data.frame(A = sample(LETTERS, 100, replace = TRUE), DATES = as.character(seq(Sys.Date(), length = 100, by = "days")))) options("stringsAsFactors" = FALSE) set.seed(1) str(data.frame(A = sample(LETTERS, 100, replace = TRUE), DATES = as.character(seq(Sys.Date(), length = 100, by = "days")))) options("stringsAsFactors" = TRUE) ## reset 

Esto realmente no se puede corregir debido a la forma en que se implementan las opciones en R, cualquier cosa podría cambiarlas sin que usted lo sepa y, por lo tanto, no se garantiza que el mismo fragmento de código devuelva exactamente el mismo objeto. John Chambers lamenta esta característica en su libro reciente.

Un ejemplo patológico en R es el uso de uno de los valores globales disponibles en R, pi , para calcular el área de un círculo.

 > r <- 3 > pi * r^2 [1] 28.27433 > > pi <- 2 > pi * r^2 [1] 18 > > foo <- function(r) { + pi * r^2 + } > foo(r) [1] 18 > > rm(pi) > foo(r) [1] 28.27433 > pi * r^2 [1] 28.27433 

Por supuesto, uno puede escribir la función foo() defensivamente forzando el uso de base::pi pero tal recurso puede no estar disponible en el código de usuario normal a menos que esté empaquetado y usando un NAMESPACE :

 > foo <- function(r) { + base::pi * r^2 + } > foo(r = 3) [1] 28.27433 > pi <- 2 > foo(r = 3) [1] 28.27433 > rm(pi) 

Esto resalta el desorden al que puede acceder confiando en cualquier cosa que no esté únicamente dentro del scope de su función o que se transmita explícitamente como argumento.

Aquí hay un ejemplo patológico interesante que involucra funciones de reemplazo, la asignación global y x definida tanto global como localmente …

 x <- c(1,NA,NA,NA,1,NA,1,NA) local({ #some other code involving some other x begin x <- c(NA,2,3,4) #some other code involving some other x end #now you want to replace NAs in the the global/parent frame x with 0s x[is.na(x)] <<- 0 }) x [1] 0 NA NA NA 0 NA 1 NA 

En lugar de devolver [1] 1 0 0 0 1 0 1 0 , la función de reemplazo usa el índice devuelto por el valor local de is.na(x) , aunque esté asignando el valor global de x. Este comportamiento está documentado en R Language Definition.

Un ejemplo rápido pero convincente en R es ejecutar la línea como:

 .Random.seed <- 'normal' 

Elegí 'normal' como algo que alguien podría elegir, pero podría usar cualquier cosa allí.

Ahora ejecute cualquier código que use números aleatorios generados, por ejemplo:

 rnorm(10) 

Entonces puede señalar que lo mismo podría suceder para cualquier variable global.

También uso el ejemplo de:

 x <- 27 z <- somefunctionthatusesglobals(5) 

Luego pregunte a los estudiantes cuál es el valor de x ; la respuesta es que no sabemos.

A través del método de prueba y error, aprendí que tengo que ser muy explícito al nombrar los argumentos de mi función (y asegurarme de que haya suficientes comprobaciones al inicio y a lo largo de la función) para que todo sea lo más robusto posible. Esto es especialmente cierto si tiene variables almacenadas en un entorno global, pero luego intenta depurar una función con objetos de valor personalizados, ¡y algo no cuadra! Este es un ejemplo simple que combina verificaciones incorrectas y llamar a una variable global.

 glob.arg <- "snake" customFunction <- function(arg1) { if (is.numeric(arg1)) { glob.arg <- "elephant" } return(strsplit(glob.arg, "n")) } customFunction(arg1 = 1) #argument correct, expected results customFunction(arg1 = "rubble") #works, but may have unexpected results 

Un boceto de ejemplo que surgió al intentar enseñar esto hoy. Específicamente, esto se enfoca en tratar de dar intuición sobre por qué los globales pueden causar problemas, por lo que se abstrae tanto como sea posible en un bash por establecer lo que puede y no puede concluirse simplemente desde el código (dejando la función como una caja negra).

La puesta en marcha

Aquí hay un código. Decida si devolverá un error o no según los criterios dados.

El código

 stopifnot( all( x!=0 ) ) y <- f(x) 5/x 

El criterio

Caso 1: f() es una función que se comporta correctamente, que usa solo variables locales.

Caso 2: f() no es necesariamente una función de comportamiento correcto, que podría utilizar una asignación global.

La respuesta

Caso 1: El código no arrojará un error, ya que la línea uno verifica que no hay x 's igual a cero y la línea tres se divide por x .

Caso 2: El código podría devolver un error, ya que f() podría, por ejemplo, restar 1 de x y asignarlo nuevamente a la x en el entorno padre, donde cualquier elemento x igual a 1 podría establecerse en cero y la tercera línea devolvería una división por error cero.

Aquí hay un bash de respuesta que tendría sentido para los tipos de estadísticas.

  • Colisiones del espacio de nombres: los nombres comunes (x, i, etc.) se vuelven a usar, lo que provoca colisiones en el espacio de nombres

Primero definimos una función de verosimilitud de registro,

 logLik <- function(x) { y <<- x^2+2 return(sum(sqrt(y+7))) } 

Ahora escribimos una función no relacionada para devolver la sum de cuadrados de una entrada. Debido a que somos perezosos lo haremos pasándolo como una variable global,

 sumSq <- function() { return(sum(y^2)) } y <<- seq(5) sumSq() [1] 55 

Nuestra función de verosimilitud de registro parece comportarse exactamente como cabría esperar, tomar un argumento y devolver un valor,

 > logLik(seq(12)) [1] 88.40761 

¿Pero qué pasa con nuestra otra función?

 > sumSq() [1] 633538 

Por supuesto, este es un ejemplo trivial, como lo será cualquier ejemplo que no exista en un progtwig complejo. Pero con suerte, provocará una discusión sobre cuánto más difícil es hacer un seguimiento de los globales que los locales.

En R , también puede intentar mostrarles que a menudo no es necesario utilizar variables globales, ya que puede acceder a las variables definidas en el scope de la función desde dentro de la función misma cambiando solo el entorno. Por ejemplo, el código a continuación

 zz="aaa" x = function(y) { zz="bbb" cat("value of zz from within the function: \n") cat(zz , "\n") cat("value of zz from the function scope: \n") with(environment(x),cat(zz,"\n")) }