Примеры опасностей глобальных переменных в R и Stata

В недавних беседах с сокурсниками я выступал за то, чтобы избегать глобалов, кроме как хранить константы. Это своего рода типичная прикладная программа типа статистики, где каждый пишет свой собственный код и размеры проекта на небольшой стороне, поэтому людям может быть трудно увидеть проблемы, вызванные неряшливыми привычками.

Говоря об избежании глобальных привязок, я сосредоточен на следующих причинах, по которым глобальные переменные могут вызвать проблемы, но я хотел бы иметь несколько примеров в R и/или Stata, чтобы идти с принципами (и любыми другими принципами, которые вы могли бы найти важными), и мне трудно найти правдоподобных.

  • Нелокальность: глобальные приложения делают отладку сложнее, потому что они лучше понимают поток кода
  • Неявное взаимодействие: глобалы ломают простоту функционального программирования, позволяя сложные взаимодействия между удаленными сегментами кода
  • Коллизии пространства имен: общие имена (x, я и т.д.) повторно используются, вызывая конфликты пространства имен

Полезным ответом на этот вопрос будет воспроизводимый и самодостаточный фрагмент кода, в котором глобальные вызовы вызывают определенный тип проблем, в идеале - с другим фрагментом кода, в котором исправлена ​​проблема. Я могу при необходимости сгенерировать исправленные решения, поэтому пример проблемы более важен.

Соответствующие ссылки:

Глобальные переменные являются плохими

Плохо ли глобальные переменные?

Ответ 1

Я также с удовольствием преподаю R студентам, не имеющим опыта программирования. Проблема, которую я обнаружил, заключается в том, что большинство примеров того, когда глобальные перемены плохи, довольно упрощены и на самом деле не имеют смысла.

Вместо этого я пытаюсь проиллюстрировать принцип наименьшего удивления. Я использую примеры, когда сложно понять, что происходит. Вот несколько примеров:

  • Я прошу класс записать, что, по их мнению, окончательное значение i будет:

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

    Некоторые из классов правильно угадывают. Тогда я спрашиваю, должен ли ты когда-нибудь писать такой код?

    В некотором смысле i - глобальная переменная, которая изменяется.

  • Что возвращает следующий фрагмент кода:

    x = 5:10
    x[x=1]
    

    Проблема заключается в том, что мы имеем в виду под x

  • Возвращает ли следующая функция глобальная или локальная переменная:

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

    Ответ: оба. Опять обсудите, почему это плохо.

Ответ 2

О, замечательный smell глобалы...

Все ответы в этом сообщении приводили примеры R, и OP также нуждался в некоторых примерах Stata. Поэтому позвольте мне перезвонить им.

В отличие от R, Stata заботится о локальности локальных макросов (те, которые вы создаете с помощью команды local), поэтому проблема "Является ли это глобальным z или локальным z, который возвращается?" никогда. (Gosh... как вы, ребята, можете вообще писать код вообще, если местность не применяется?) Stata имеет другую причуду, а именно, что несуществующий локальный или глобальный макрос оценивается как пустая строка, которая может быть или не быть желательным.

Я видел глобальные переменные, используемые по нескольким основным причинам:

  • Глобалы часто используются в качестве ярлыков для списков переменных, как в

    sysuse auto, clear
    regress price $myvars
    

    Я подозреваю, что основное использование такой конструкции для тех, кто переключается между интерактивным вводом и хранением кода в файле do-file при попытке выполнить несколько спецификаций. Скажем, они пытаются регрессировать с гомоскедастическими стандартными ошибками, гетероскедастическими стандартными ошибками и медианной регрессией:

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

    И затем они запускают эти регрессии с другим набором переменных, затем с еще одним, и, наконец, они сдаются и устанавливают это как файл do myreg.do с

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

    чтобы он сопровождался соответствующей настройкой глобального макроса. Все идет нормально; фрагмент

    global myvars mpg foreign
    do myreg
    

    дает желаемые результаты. Теперь позвольте сказать, что они отправляют по электронной почте свой знаменитый файл do-file, который утверждает, что он дает очень хорошие результаты регрессии для коллаборационистов и инструктирует их ввести

    do myreg
    

    Что будут видеть их коллеги? В лучшем случае среднее и медианное значение mpg, если они начали новый экземпляр Stata (сбой связи: myreg.do на самом деле не знал, что вы хотите запустить это с помощью непустого списка переменных). Но если у коллабораторов было что-то в работе, а также было глобальное myvars определенное (столкновение имен)... человек, это было бы катастрофой.

  • Глобалы используются для имен каталогов или файлов, например:

    use $mydir\data1, clear
    

    Бог знает, что будет загружено. Однако в крупных проектах это пригодится. Вы бы хотели определить global mydir где-то в вашем мастер файле, может быть даже как

    global mydir `c(pwd)'
    
  • Глобалы могут использоваться для хранения непредсказуемого дерьма, как целая команда:

    capture $RunThis
    

    Бог знает, что будет выполнено. Это худший случай неявной сильной связи, но, поскольку я даже не уверен, что RunThis будет содержать что-либо значимое, я помещаю перед ним capture и буду готов обработать ненулевой код возврата _rc. (См., Однако, мой пример ниже.)

  • Использование Stap для глобальных целей - это настройки Бога, такие как вероятность вероятности/уверенности в уровне I: глобальный $S_level всегда определен (и вы должны быть полным идиотом, чтобы переопределить это глобальное, хотя, конечно, это технически выполнимо). Это, однако, в основном унаследованная проблема с кодом версии 5 и ниже (примерно), поскольку та же информация может быть получена из менее хрупкой постоянной системы:

    set level 90
    display $S_level
    display c(level)
    

К счастью, globals довольно явны в Stata и поэтому легко отлаживаются и удаляются. В некоторых из вышеперечисленных ситуаций и, конечно же, в первом случае вы хотите передать параметры в do файлы, которые рассматриваются как локальный файл `` 0 'inside the do-file. Instead of using globals in the myreg.do`, я бы, вероятно, закодировал его как

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

Элемент unab будет служить элементом защиты: если вход не является законным varlist, программа остановится с сообщением об ошибке.

В худших случаях, которые я видел, глобальность использовалась только один раз после определения.

Бывают случаи, когда вы хотите использовать глобальные переменные, потому что в противном случае вам придется передавать чертову все в любой файл или программу. Один из примеров, когда я нашел, что глобальные переменные в значительной степени неизбежны, заключался в кодировании оценки максимального правдоподобия, где я не знал заранее, сколько уравнений и параметров у меня было бы. Stata настаивает на том, что оценщик правдоподобия (пользовательский) будет иметь конкретные уравнения. Поэтому мне пришлось аккумулировать свои уравнения в глобалях, а затем вызвать моего оценщика с помощью глобальных переменных в описаниях синтаксиса, которые Stata необходимо будет анализировать:

args lf $parameters

где lf была целевой функцией (лог-правдоподобие). Я столкнулся с этим, по крайней мере, дважды, в обычной упаковке (denormix) и пакете анализа подтверждающих факторов (confa); вы можете findit обе из них, конечно.

Ответ 3

Один пример R глобальной переменной, которая делит мнение, - это проблема stringsAsFactors при чтении данных в R или создании кадра данных.

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

Это не может быть исправлено из-за того, как опции реализованы в R - все может изменить их, если вы этого не знаете, и, таким образом, один и тот же кусок кода не гарантирует возврата точно одного и того же объекта. Джон Чамберс оплакивает эту функцию в своем недавнем book.

Ответ 4

Патологический пример в R - это использование одного из глобалей, доступных в R, pi, для вычисления площади круга.

> 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

Конечно, можно защищать функцию foo(), заставляя использовать base::pi, но такой регресс может быть недоступен в обычном коде пользователя, если он не упакован и не использует NAMESPACE:

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

Это подчеркивает беспорядок, с которым вы можете попасть, полагаясь на все, что не входит в сферу действия вашей функции, или явно передано в качестве аргумента.

Ответ 5

Вот интересный патологический пример с замещающими функциями, глобальным назначением и х, определяемым как глобально, так и локально...

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

Вместо возврата [1] 1 0 0 0 1 0 1 0 функция замены использует индекс, возвращаемый локальным значением is.na(x), даже если вы назначаете глобальное значение x. Это поведение задокументировано в определении языка R.

Ответ 6

Одним быстрым, но убедительным примером в R является запуск строки, например:

.Random.seed <- 'normal'

Я выбрал "нормальный", как что-то, что мог бы выбрать, но вы могли бы что-то там использовать.

Теперь запустите любой код, который использует сгенерированные случайные числа, например:

rnorm(10)

Затем вы можете указать, что то же самое может произойти для любой глобальной переменной.

Я также использую пример:

x <- 27
z <- somefunctionthatusesglobals(5)

Затем спросите учащихся, что такое значение x; ответ заключается в том, что мы не знаем.

Ответ 7

Через пробную версию и ошибку я узнал, что мне нужно очень четко указывать аргументы моей функции (и обеспечивать достаточное количество проверок в начале и по функции), чтобы сделать все максимально надежным. Это особенно верно, если у вас есть переменные, хранящиеся в глобальной среде, но затем вы пытаетесь отлаживать функцию с помощью специальных ценностей - и что-то не складывается! Это простой пример, который объединяет ошибки и вызовы глобальной переменной.

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

Ответ 8

Примерный эскиз, который появился, пытаясь научить этому сегодня. В частности, это сосредоточено на попытке дать интуицию относительно того, почему глобальные переменные могут вызвать проблемы, поэтому он максимально абстрагируется в попытке указать, что можно и не может быть заключено только из кода (оставив функцию как черный ящик).

Настройка

Вот какой код. Определите, вернет ли она ошибку или не будет основано только на указанных критериях.

Код

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

Критерии

Случай 1: f() - это функция с корректным поведением, которая использует только локальные переменные.

Случай 2: f() не обязательно является должным образом выполненной функцией, которая потенциально может использовать глобальное назначение.

Ответ

Случай 1: код не будет возвращать ошибку, так как первая строка проверяет, что нет x, равного нулю, а строка три делит на x.

Случай 2: Код может потенциально вернуть ошибку, так как f() может, например, вычитайте 1 из x и верните его в x в родительской среде, где любой элемент x, равный 1, может быть установлен на ноль, а третья строка вернет деление на нулевую ошибку.

Ответ 9

Вот одна попытка ответа, которая имела бы смысл для статистических типов.

  • Коллизии пространства имен: общие имена (x, я и т.д.) повторно используются, вызывая конфликты пространства имен

Сначала мы определяем функцию логарифмического правдоподобия,

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

Теперь мы пишем несвязанную функцию для возврата суммы квадратов ввода. Поскольку мы ленивы, мы сделаем это, передав его y как глобальную переменную,

sumSq <- function() {
   return(sum(y^2))
}

y <<- seq(5)
sumSq()
[1] 55

Наша функция правдоподобия журнала ведет себя точно так, как мы ожидали, принимая аргумент и возвращая значение,

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

Но что с нашей другой функцией?

> sumSq()
[1] 633538

Конечно, это тривиальный пример, как и любой пример, который не существует в сложной программе. Но, надеюсь, это вызовет дискуссию о том, насколько сложнее следить за глобалами, чем локальные жители.

Ответ 10

В R вы также можете показать им, что часто использовать не нужно для использования глобальных переменных, поскольку вы можете получить доступ к указанным переменным в области функций от внутри самой функции, только изменяя среду. Например, код ниже

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"))
}