
Несколько примеров читателей, которые могли бы извлечь пользу из этой книги:
- Аналитик (допустим, работающий в больнице или в правительственном учреждении), которому приходится регулярно выдавать статистические отчеты и разрабатывать программы для этой цели.
- Научный работник, занимающийся разработкой статистической методологии — новой или объединяющей существующие методы в интегрированные процедуры. Методологию нужно закодировать, чтобы она могла использоваться в сообществе исследователей.
- Специалисты по маркетингу, судебному сопровождению, журналистике, издательскому делу и т. д., занимающиеся разработкой кода для построения сложных графических представлений данных.
- Профессиональные программисты с опытом разработки программного обеспечения, назначенные в проекты, связанные со статистическим анализом.
- Студенты, изучающие статистику и обработку данных.
Таким образом, эта книга не является справочником по бесчисленным статистическим методам замечательного пакета R. На самом деле она посвящена программированию и в ней рассматриваются вопросы программирования, редко встречающиеся в других книгах о R. Даже основополагающие темы рассматриваются под углом программирования. Несколько примеров такого подхода:
- В этой книге встречаются разделы «Расширенные примеры». Обычно в них представлены полные функции общего назначения вместо изолированных фрагментов кода, основанных на конкретных данных. Более того, некоторые из этих функций могут пригодиться в вашей повседневной работе с R. Изучая эти примеры, вы не только узнаете, как работают конкретные конструкции R, но и научитесь объединять их в полезные программы. Во многих случаях я привожу описания альтернативных решений и отвечаю на вопрос: «Почему это было сделано именно так?»
- Материал излагается с учетом восприятия программиста. Например, при описании кадров данных я не только утверждаю, что кадр данных в R представляет собой список, но и указываю на последствия этого факта с точки зрения программирования. Также в тексте R сравнивается с другими языками там, где это может быть полезно (для читателей, владеющих этими языками).
- Отладка играет важнейшую роль в программировании на любом языке, однако в большинстве книг о R эта тема практически не упоминается. В этой книге я посвятил средствам отладки целую главу, воспользовался принципом «расширенных примеров» и представил полностью проработанные демонстрации того, как происходит отладка программ в реальности.
- В наши дни многоядерные компьютеры появились во всех домах, а программирование графических процессоров (GPU) производит незаметную революцию в области научных вычислений. Все больше приложений R требует очень больших объемов вычислений, и параллельная обработка стала актуальной для программистов на R. В книге этой теме посвящена целая глава, в которой помимо описания механики также приводятся расширенные примеры.
- Отдельная глава рассказывает о том, как использовать информацию о внутренней реализации и других аспектах R для ускорения работы кода R.
- Одна из глав посвящена интерфейсу R с другими языками программирования, такими как C и Python. И снова особое внимание уделяется расширенным примерам и рекомендациям по выполнению отладки.
Отрывок. 7.8.4. Когда следует использовать глобальные переменные ?
По поводу использования глобальных переменных в сообществе программистов нет единого мнения. Очевидно, на вопрос, вынесенный в название этого раздела, нет правильного ответа, поскольку это вопрос личных предпочтений и стиля. Тем не менее многие программисты считают, что полный запрет на глобальные переменные, за который выступают многие преподаватели программирования, был бы излишне жестким. В этом разделе мы исследуем возможную пользу глобальных переменных в контексте структур R. Термином «глобальная переменная» будет обозначаться любая переменная, находящаяся в иерархии окружений выше уровня кода, представляющего интерес.
Использование глобальных переменных в R более распространено, чем можно было бы ожидать. Как ни удивительно, R очень широко использует глобальные переменные в своей внутренней реализации (и в коде C, и в функциях R). Так, оператор суперприсваивания << — используется во многих библиотечных функциях R (хотя обычно для записи в переменную, находящуюся всего одним уровнем выше в иерархии переменных). В многопоточном коде и коде графического процессора, используемом для написания быстрых программ (см. главу 16), обычно широко задействованы глобальные переменные, обеспечивающие основной механизм взаимодействия между параллельными исполнителями.
А теперь для конкретности вернемся к более раннему примеру из раздела 7.7:
f <- function(lxxyy) { # lxxyy — список, содержащий x и y
...
lxxyy$x <- ...
lxxyy$y <- ...
return(lxxyy)
}
# Задать x и y
lxy$x <- ...
lxy$y <- ...
lxy <- f(lxy)
# Использовать новые x и y
... <- lxy$x
... <- lxy$y
Как упоминалось ранее, этот код может стать громоздким, особенно если x и y сами являются списками.
С другой стороны, взгляните на альтернативную схему с использованием глобальных переменных:
f <- function() {
...
x <<- ...
y <<- ...
}
# Задать x и y
x <-...
y <-...
f() # Здесь x и y изменяются
# Использовать новые x и y
... <- x
... <- y
Пожалуй, вторая версия намного чище, менее громоздка и не требует манипуляций со списками. Понятный код обычно создает меньше проблем в написании, отладке и сопровождении.
По этим причинам — для упрощения и снижения громоздкости кода — мы решили использовать глобальные переменные вместо возвращения списков в коде DES, приведенном ранее. Рассмотрим этот пример чуть подробнее.
Использовались две глобальные переменные (обе представляют собой списки, содержащие разную информацию): переменная sim связана с библиотечным кодом, а переменная mm1glbls связана с кодом конкретного применения M/M/1. Начнем с sim.
Даже программисты, сдержанно относящиеся к глобальным переменным, согласны с тем, что применение таких переменных может быть оправданно в том случае, если они действительно глобальны — в том смысле, что они широко используются в программе. Все это относится к переменной sim из примера DES: она используется как в коде библиотеки (в schedevnt(), getnextevnt() и dosim()) и в коде M/M/1 (в mm1reactevnt()). В этом конкретном примере последующие обращения к sim ограничиваются чтением, но в некоторых ситуациях возможна запись. Типичный пример такого рода — возможная реализация отмены событий. Например, такая ситуация может встретиться при моделировании принципа «более раннее из двух»: планируются два события, и когда одно из них происходит, другое должно быть отменено.
Таким образом, использование sim в качестве глобальной переменной выглядит оправданным. Тем не менее, если бы мы решительно отказались от использования глобальных переменных, sim можно было бы поместить в локальную переменную внутри dosim(). Эта функция будет передавать sim в аргументе всех функций, упоминаемых в предыдущем абзаце (schedevnt(), getnextevnt() и т. д.), и каждая из этих функций будет возвращать измененную переменную sim.
Например, строка 94:
reactevnt(head)
преобразуется к следующему виду:
sim <- reactevnt(head)
После этого в функцию mm1reactevnt(), связанную с конкретным приложением, необходимо добавить следующую строку:
return(sim)
Нечто похожее можно сделать и с mm1glbls, включив в dosim() локальную переменную с именем (например) appvars. Но если это делается с двумя переменными, то их необходимо поместить в список, чтобы обе переменные можно было вернуть из функции, как в приведенном выше примере функции f(). И тогда возникает громоздкая структура списков внутри списков, о которой говорилось выше, а вернее, списков внутри списков внутри списков.
С другой стороны, противники использования глобальных переменных замечают, что простота кода не дается даром. Их беспокоит то, что в процессе отладки возникают трудности с поиском мест, в которых глобальная переменная изменяет значение, поскольку изменение может произойти в любой точке программы. Казалось бы, в мире современных текстовых редакторов и интегрированных средств разработки, которые помогут найти все вхождения переменной, проблема уходит на второй план (исходная статья, призывающая отказаться от использования глобальных переменных, была опубликована в 1970 году!). И все же этот фактор необходимо учитывать.
Другая проблема, о которой упоминают критики, встречается при вызове функции из нескольких несвязанных частей программы с разными значениями. Например, представьте, что функция f() вызывается из разных частей программы, причем каждый вызов получает собственные значения x и y вместо одного значения для каждого. Проблему можно решить созданием векторов значений x и y, в которых каждому экземпляру f() в вашей программе соответствует отдельный элемент. Однако при этом будет утеряна простота от использования глобальных переменных.
Указанные проблемы встречаются не только в R, но и в более общем контексте. Впрочем, в R использование глобальных переменных на верхнем уровне создает дополнительную проблему, так как у пользователя на этом уровне обычно существует множество переменных. Появляется опасность того, что код, использующий глобальные переменные, может случайно заменить совершенно постороннюю переменную с тем же именем.
Конечно, проблема легко решается — достаточно выбирать для глобальных переменных длинные имена, привязанные к конкретному применению. Однако окружения также предоставляют разумный компромисс, как в следующей ситуации для примера DES.
Внутри функции dosim() строка
sim <<- list()
может быть заменена строкой
assign("simenv",new.env(),envir=.GlobalEnv)
Она создает новое окружение, на которое ссылается переменная simenv на верхнем уровне. Это окружение служит контейнером для инкапсуляции глобальных переменных, к которым можно обращаться вызовами get() и assign(). Например, строки
if (is.null(sim$evnts)) {
sim$evnts <<- newevnt
в schedevnt() принимают вид
if (is.null(get("evnts",envir=simenv))) {
assign("evnts",newevnt,envir=simenv)
Да, это решение тоже получается громоздким, но по крайней мере оно не такое сложное, как списки внутри списков внутри списков. И оно защищает от случайной записи в постороннюю переменную на верхнем уровне. Использование оператора суперприсваивания по-прежнему дает менее громоздкий код, но этот компромисс следует принять во внимание.
Как обычно, не существует единого стиля программирования, который бы обеспечивал лучшие результаты во всех ситуациях. Решение с глобальными переменными — еще один вариант, который стоит включить в ваш арсенал средств программирования.
7.8.5. Замыкания
Напомню, что замыкания (closure) R состоят из аргументов и тела функции в совокупности с окружением на момент вызова. Факт включения окружения задействован в парадигме программирования, которая использует концепцию, также называемую замыканием (здесь наблюдается некоторая перегрузка терминологии).
Замыкание представляет собой функцию, которая создает локальную переменную, а затем создает другую функцию, обращающуюся к этой переменной. Описание получается слишком абстрактным, поэтому я лучше приведу пример.
1 > counter
2 function () {
3 ctr <- 0
4 f <- function() {
5 ctr <<- ctr + 1
6 cat("this count currently has value",ctr,"\n")
7 }
8 return(f)
9 }
Проверим, как работает этот код, прежде чем погружаться в подробности реализации:
> c1 <- counter()
> c2 <- counter()
> c1
function() {
ctr <<- ctr + 1
cat("this count currently has value",ctr,"\n")
}
<environment: 0x8d445c0>
> c2
function() {
ctr <<- ctr + 1
cat("this count currently has value",ctr,"\n")
}
<environment: 0x8d447d4>
> c1()
this count currently has value 1
> c1()
this count currently has value 2
> c2()
this count currently has value 1
> c2()
this count currently has value 2
> c2()
this count currently has value 3
> c1()
this count currently has value 3
Здесь функция counter() вызывается дважды, а результаты присваиваются c1 и c2. Как и ожидалось, эти две переменные состоят из функций, а именно копий f(). Тем не менее f() обращается к переменной ctr через оператор суперприсваивания, и эта переменная будет переменной с указанным именем, локальной по отношению к counter(), так как она будет первой на пути по иерархии окружений. Она является частью окружения f(), и как таковая упаковывается в то, что возвращается на сторону вызова counter(). Ключевой момент заключается в том, что при разных вызовах counter() переменная ctr будет находиться в разных окружениях (в примере окружения хранились в памяти по адресам 0x8d445c0 и 0x8d447d4). Иначе говоря, разные вызовы counter() будут создавать физически разные экземпляры ctr.
В результате функции c1() и c2() работают как полностью независимые счетчики. Это видно из примера, где каждая функция вызывается по несколько раз.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — R
По факту оплаты бумажной версии книги на e-mail высылается электронная версия книги.