Что такое "{" класс в R?

Вот код:

mf = function(..., expr) {
    expr = substitute(expr)
    print(class(expr))
    print(str(expr))
    expr
}
mf(a = 1, b = 2, expr = {matrix(NA, 4, 4)})

Вывод:

[1] "{"
length 2 {  matrix(NA, 4, 4) }
 - attr(*, "srcref")=List of 2
  ..$ :Class 'srcref'  atomic [1:8] 1 25 1 25 25 25 1 1
  .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fbcdbce3860> 
  ..$ :Class 'srcref'  atomic [1:8] 1 26 1 41 26 41 1 1
  .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fbcdbce3860> 
 - attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fbcdbce3860> 
 - attr(*, "wholeSrcref")=Class 'srcref'  atomic [1:8] 1 0 1 42 0 42 1 1
  .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fbcdbce3860> 
NULL
{
matrix(NA, 4, 4)
}

По-видимому, результат substitute(expr) создает что-то из класса "{". Что это за класс? Почему {matrix(NA, 4, 4)} длины 2? Что означают эти странные attrs?

Ответ 1

{ - это класс для блока кода. Просто взглянув на классы, обратите внимание на разницу между этими

mf(a = 1, b = 2, expr = {matrix(NA, 4, 4)})
# [1] "{"
mf(a = 1, b = 2, expr = matrix(NA, 4, 4))
# [1] "call"

Класс { может содержать несколько операторов. length() указывает, сколько операторов находится в блоке (включая начало блока). Например

length(quote({matrix(NA, 4, 4)}))
# [1] 2
length(quote({matrix(NA, 4, 4); matrix(NA,3,3)}))
# [1] 3
length(quote({}))
# [1] 1

Атрибуты "srcref" и "srcfile" - это то, как R-треки, где определены функции, для предоставления информационных сообщений об ошибках. Вы можете увидеть справочную страницу ?srcfile для получения дополнительной информации об этом.

Ответ 2

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

Функция substitute() возвращает дерево разбора выражения R. Дерево разбора - это дерево элементов языка. Они могут включать в себя литеральные значения, символы (в основном имена переменных), вызовы функций и блокированные блоки. Здесь показана демонстрация всех элементов языка R, возвращаемых substitute(), показывая их типы во всех классификационных схемах типа R:

tmc <- function(x) c(typeof(x),mode(x),class(x));
tmc(substitute(TRUE));
## [1] "logical" "logical" "logical"
tmc(substitute(4e5L));
## [1] "integer" "numeric" "integer"
tmc(substitute(4e5));
## [1] "double"  "numeric" "numeric"
tmc(substitute(4e5i));
## [1] "complex" "complex" "complex"
tmc(substitute('a'));
## [1] "character" "character" "character"
tmc(substitute(somevar));
## [1] "symbol" "name"   "name"
tmc(substitute(T));
## [1] "symbol" "name"   "name"
tmc(substitute(sum(somevar)));
## [1] "language" "call"     "call"
tmc(substitute(somevec[1]));
## [1] "language" "call"     "call"
tmc(substitute(somelist[[1]]));
## [1] "language" "call"     "call"
tmc(substitute(somelist$x));
## [1] "language" "call"     "call"
tmc(substitute({blah}));
## [1] "language" "call"     "{"

Примечания:

  • Обратите внимание, что все схемы классификации трех типов очень похожи, но тонко отличаются. Это может быть источником путаницы. typeof() предоставляет тип хранилища объекта, иногда называемый "внутренним" типом (честно говоря, его, вероятно, не следует называть "внутренним", потому что он часто отображается непосредственно пользователю на уровне R, но он часто описывается таким образом, я бы назвал его "фундаментальным" или "базовым" типом), mode() дает аналогичную схему классификации, которую каждый должен, вероятно, игнорировать, а class() дает неявный (если нет атрибута class) или явный (если есть) класс объекта, который используется для поиска метода S3 (и, надо сказать, иногда проверяется непосредственно кодом R, независимо от процесса поиска S3).
  • Обратите внимание, что TRUE - это логический литерал, но T - это символ, как и любое другое имя переменной, и просто назначается TRUE по умолчанию (и ditto для F и FALSE). Вот почему иногда люди рекомендуют не использовать T и F в пользу использования TRUE и FALSE, потому что T и F могут быть переназначены (но лично я предпочитаю использовать T и F для конкретизации, никто не должен переизбирать их!).
  • Проницательный читатель заметит, что в моей демонстрации литералов я опустил необработанный тип. Это связано с тем, что в Р. не существует такого понятия, как исходный литерал. На самом деле, существует очень мало способов получить необработанные векторы в R; raw(), as.raw(), charToRaw() и rawConnectionValue() - это единственные способы, о которых я знаю, и если бы я использовал эти функции в вызове substitute(), они были бы возвращены как объекты "call" как в примере sum(somevar), а не буквальных необработанных значений. То же самое можно сказать и о типе списка; нет такой вещи, как листинг списка (хотя есть много способов получить список через вызовы функций). Обычные необработанные векторы возвращают 'raw' для всех трех типов классификаций, а простые списки возвращают 'list' для всех трех типов классификаций.

Теперь, когда у вас есть дерево синтаксического анализа, которое более сложное, чем простое литеральное значение или символ (что означает, что это должен быть вызов функции или сжатое выражение), вы можете в целом проверить содержимое этого дерева синтаксического анализа, принудительно перечислив его. Именно так R предоставляет свою внутреннюю структуру языка через свои собственные структуры данных.

Дайвинг в вашем примере:

pt <- as.list(substitute({matrix(NA,4,4)}));
pt;
## [[1]]
## `{`
##
## [[2]]
## matrix(NA, 4, 4)

Это дает понять, почему length() возвращает 2: длина списка, представляющего дерево разбора. Как правило, привязка выражения транслируется в первый компонент списка, а остальные компоненты списка построены из операторов, разделенных точкой с запятой, в фигурных скобках:

as.list(substitute({}));
## [[1]]
## `{`
##
as.list(substitute({a}));
## [[1]]
## `{`
##
## [[2]]
## a
##
as.list(substitute({a;b}));
## [[1]]
## `{`
##
## [[2]]
## a
##
## [[3]]
## b
##
as.list(substitute({a;b;c}));
## [[1]]
## `{`
##
## [[2]]
## a
##
## [[3]]
## b
##
## [[4]]
## c

Обратите внимание, что это идентично тому, как работают вызовы функций, за исключением той разницы, что для вызовов функций компоненты списка формируются из аргументов, разделенных запятыми, на вызов функции:

as.list(substitute(sum()));
## [[1]]
## sum
##
as.list(substitute(sum(1)));
## [[1]]
## sum
##
## [[2]]
## [1] 1
##
as.list(substitute(sum(1,3)));
## [[1]]
## sum
##
## [[2]]
## [1] 1
##
## [[3]]
## [1] 3
##
as.list(substitute(sum(1,3,5)));
## [[1]]
## sum
##
## [[2]]
## [1] 1
##
## [[3]]
## [1] 3
##
## [[4]]
## [1] 5

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

a <- 4:6;
a[2];
## [1] 5
`[`(a,2);
## [1] 5
{1;2};
## [1] 2
`{`(1,2);
## [1] 2

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

unwrap <- function(x) if (typeof(x) == 'language') lapply(as.list(x),unwrap) else x;
unwrap(substitute(3));
## [1] 3
unwrap(substitute(a));
## a
unwrap(substitute(a+3));
## [[1]]
## `+`
##
## [[2]]
## a
##
## [[3]]
## [1] 3
##
unwrap(substitute({matrix(NA,4,4)}));
## [[1]]
## `{`
##
## [[2]]
## [[2]][[1]]
## matrix
##
## [[2]][[2]]
## [1] NA
##
## [[2]][[3]]
## [1] 4
##
## [[2]][[4]]
## [1] 4

Как вы можете видеть, сжатое выражение превращается в обычный вызов функции функции `{`(), принимая один аргумент, который является единственным выражением, которое вы закодировали в нем. Этот оператор состоит из одного вызова функции matrix(), используя три аргумента, каждое из которых является буквальным значением: NA, 4 и 4. И что все дерево синтаксического разбора.

Итак, теперь мы можем понять смысл класса "{" на глубоком уровне: он представляет собой элемент дерева синтаксического анализа, который является вызовом функции функции `{`(). Это, по-видимому, классифицируется иначе, чем другие вызовы функций ("{" вместо "call"), но, насколько я могу судить, это нигде не имеет значения. Также обратите внимание, что typeof() и mode() идентичны ("language" и "call" соответственно) между всеми элементами дерева синтаксиса, представляющими вызовы функций, как для `{`(), так и для других.