среда, 4 ноября 2015 г.

Феерическая расстановка точек над окружениями и областями видимости в R

Феерическая расстановка точек над окружениями и областями видимости в R
Спешу поделиться достигнутым просветлением на тему окружений (environments) и областей видимости (scope) в языке R. Эту тему в книгах обычно обделяют вниманием или упоминают вскользь, а почему - непонятно, ведь это базовые понятия при изучении любого языка программирования. Обстоятельное (рассчитанное на программистов, потому несколько перегруженное техническими деталями) изложение можно найти в Advanced R, менее подробное, но достаточное для понимания - в Art of R Programming. A Tour of Statistical Software Design. Ниже тезисно постарался раскрыть самую суть.

1. Иерархия наследования окружений

Она существует и имеет линейный вид. Окружения, находящиеся ниже, имеют доступ ко всем окружениям, находящимся выше (и объектам в них, включая функции, которые тоже являются обычными объектами).
На самом верху сидит пустое окружение emptyenv(), в котором нет ничего и которое играет роль прародителя для всех остальных окружений.
Ниже находится базовое окружение baseenv(), которое соответствует пакету base.
Ниже одно за одним идут окружения, соответствующие остальным пакетам в порядке их загрузки.
Еще ниже находится глобальное окружение globalenv() - то самое, где мы работаем, создаем новые функции (которые тоже формируют свои окружения) или непосредственно новые окружения. Если в ходе работы загружается новый пакет, он автоматически помещается выше глобального окружения.

2. Области видимости

Из дочернего (находящего ниже в иерархии наследования) окружения видно все, что находится выше, и не видно ничего, что находится ниже. Если создать в глобальном окружении новое окружение e <- new.env(), то доступ к объектам в нем осуществляется при помощи e$.
Поиск начинается в текущем окружении и продолжается в объемлющих (находящихся выше) до тех пор, пока не будет найдено соответствие имени или не будет возвращена ошибка вида Error: object 'a' not found, если объект не будет найден. Так происходит при поиске переменных, передаваемых в качестве аргументов функциям. Поиск переменных в теле функций происходит иначе! Об этом - ниже.
Такое поведение обеспечивает одновременно доступность всех объектов из “вышестоящих” окружений и возможность перегрузки функций. Это бывает небезопасно, ведь появляется риск использования переопределенной функции, которая делает совсем не то, что исходная с тем же именем. С другой стороны, если быть внимательным, нежелательных ситуаций легко избежать. В том числе обращаем внимание на предупреждения, которые появляются при загрузке пакетов - они содержат информацию о том, что то или иное имя из одного пакета “маскируется” именем из другого.

3. Определение функций и лексическая область видимости

Есть два подхода к областям видимости в контексте определения новых функций: динамическая область видимости и лексическая (она же статическая) область видимости. Первый вариант используется редко, второй наиболее распространен, в том числе он используется в R.
Динамическая область видимости позволяет функции искать все требуемые значения (значения переменных, чьи имена используются в теле функции, но которые там не определены) в окружении вызова. Вызываем в глобальном окружении - значения берутся из глобального окружения, вызываем внутри другой функции - берутся оттуда, если найдены, или из объемлющего окружения, если не найдены. Принцип тот же, что при поиске аргументов. Таким образом поведение функции является менее предсказуемым, чем при использовании лексической области видимости.
Использование лексической области видимости означает, что функция при любом вызове ищет значения переменных, используемых в теле функции, в одном и том же окружении - в окружении, где функция была определена:
a <- 1

f1 <- function(x) {
    return(x+a)
}

f2 <- function() {
    a <- 200
    return(f1)
}

f3 <- f2() 
f3(1)

#> 2
a всегда будет иметь значение 1 для функции f1(). Иначе это можно записать так:
a <- 1

f1 <- function(x) {
    return(x+a)
}

f2 <- function(x) {
    a <- 200
    f1(x)
}

f2(1)

#> 2
Если переменная a не будет найдена непосредственно в окружении, где функция определена, то поиск будет происходить только выше, в объемлющих окружениях. Та переменная a, которая равна 200, будет недоступна и никак ни на что не повлияет.
Другой пример:
y <- 100
x <- 100
h <- function() {
    gg <- function(x) {
        x + y
    }
    gg(x = 1)
}
h()
#> 101
Значение x передается как аргумент и равно 1, значение y не найдено в окружении, где функция определена (вызывается она в данном случае оттуда же), но найдено в вышестоящем.
y <- 100
x <- 100
h <- function() {
    gg <- function(x) {
        x + y
    }
    y <- 100500
    gg(x = 1)
}
h()
#> 100501
Теперь значение y найдено в окружении, где функция определена.
y <- 100
x <- 100
h <- function() {
    gg <- function(x) {
        x + y
    }
    gg(x = x)
}
h()
#> 200
И x, и y берутся из вышестоящего окружения.
И последний пример:
f <- function(x) {
    b <- 111
    x + b
}

x <- 100
x + b
#> Error: object 'b' not found
Переменна b определена в нижестоящем окружении, и к ней нет доступа извне функции f().

4. Замыкания (closures)

Замыкание - функция, которая возвращает другую функцию. Это работает и является полезным благодаря описанному выше поведению. Реализуются “фабрики функций”, когда возвращаемая функция (fun2 в примере ниже) “помнит” свое состояние в момент, когда она была определена, а определяется она при каждом вызове fun1:
expon <- 23.356
fun1 <- function(expon) {
    fun2 <- function(x) {
        x^expon
    }
    return(fun2)
}

power4 <- fun1(4)
power4(10)
#> 1000

power5 <- fun1(5)
power5(10)
#> 10000
Подавляющее большинство примеров в книгах и в жизни принципиально не сложнее описанных выше. Не были рассмотрены особенности, связанные с пространствами имен пакетов, но они не противоречат описанному выше и нужны в основном разработчикам этих самых пакетов.

Комментариев нет:

Отправить комментарий