В одном из упражнений Advanced R, раздел 2.5, посвященном изменению на месте, задается вопрос, почему следующий код не создает циклический список:
x <- list()
x[[1]] <- x
Advanced R Solutions говорит, что это не циклично из-за запуска копирования при изменении. Однако в начале раздела Уикхем сказал: «Если к объекту привязано одно имя, R изменит его на месте». Почему здесь не применимо изменение на месте? Я думал, что, возможно, когда вы устанавливаете x[[1]] <- x
, вы привязываете второе имя к объекту, но кажется, что это также одновременно отменяет привязку x
к пустому списку, потому что он переопределяет x
как список, содержащий пустой список в качестве своего первого элемента. Это потому, что R каким-то образом не регистрирует отвязку x
до тех пор, пока не произойдет привязка к x[[1]]
, или мне здесь чего-то не хватает?
Оператор [[<-
привязывает объект к второму имени. Согласно определению языка ваш код идентичен этому:
`*tmp*` <- x #binding to a second name
x <- "[[<-"(`*tmp*`, 1, value = x)
rm(`*tmp*`)
Мы можем показать, что он действительно создает и удаляет это временное имя:
`*tmp*` <- 1
`*tmp*`
#[1] 1
x <- list()
x[[1]] <- x
`*tmp*`
#Error: object '*tmp*' not found
Это правда, но я считаю, что это отвлекающий маневр. Учтите: x = list(1:10); x[[2L]] = x[[1L]]
. Один и тот же вектор теперь связан с двумя подвыражениями x
. Изменение одного не приведет к изменению другого, даже если выражение никогда не привязано более чем к одному имени (а именно, параметру value
в [[<-
).
@KonradRudolph Есть вторая привязка: вектор привязан к списку.
Конечно, он привязан к индексу в списке. Но оно не связано с именем, разве что транзитивно.
@KonradRudolph Я не думаю, что различие имеет значение (или должно иметь значение) для запуска копирования при изменении.
Вот почему я имею в виду «отвлекающий маневр»: настаивание на привязке ценностей к именам вводит в заблуждение. Что на самом деле имеет значение, так это то, из скольких мест в памяти ссылается значение, независимо от того, связаны ли эти места с именем. Даже если бы <-
был реализован по-другому (без обхода через *tmp*
), это не изменило бы поведение CoW. Если говорить более решительно: ответ на вопрос о *tmp*
, кстати, верен, но совершенно не имеет значения.
@KonradRudolph Я согласен, что речь идет о том, «из скольких мест в памяти ссылается значение» (я бы назвал это привязками). Я не согласен, что *tmp*
неактуален, потому что это дополнительная привязка.
Ладно, может быть, слово «нерелевантно» звучит слишком сильно, очевидно, что его присутствие увеличивает количество ссылок. Но, опять же, даже если бы *tmp*
не фигурировало в картине, поведение было бы таким же, потому что (как отмечается в моем комментарии под вопросом), «из-за CoW» на самом деле не является ответом. CoW — это деталь реализации для оптимизации семантики значений. Но это не причина, по которой списки в R имеют семантику значений.
На самом деле, если я правильно читаю код интерпретатора R, наличие *tmp*
все-таки не имеет значения, поскольку R явно отключает подсчет ссылок для этого значения. — Смотрите сноски со ссылками в моем ответе. (Я по-прежнему считаю, что ваш ответ ценен как объяснение среднего уровня: не слишком высокого и не слишком низкого уровня.)
Я несколько не согласен с «правильным» ответом, данным «Advanced R Solutions». Ответ имеет два аспекта: определенное поведение и детали реализации низкого уровня.
Основная причина, по которой ваш код не создает циклическую ссылку, заключается в том, что R имеет семантику значений, а не семантику ссылок. В R, если вы привязываете значение (посредством присваивания или путем передачи его функции), это значение копируется. И вот что здесь происходит: назначение x[[1]] <- x
создает копию x
, назначенную x[[1]]
.
И это ответ. Все остальное — детали реализации. Но давайте углубимся:
Проблема с «наивной» реализацией семантики значений заключается в том, что она очень неэффективна. Фактически, большой объем кода постоянно копирует, а копирование больших объектов по-прежнему очень неэффективно даже на современных архитектурах ЦП. Обычно мы хотим максимально избежать копирования памяти.
Таким образом, R реализует оптимизацию: вместо копирования значения, как только оно присваивается чему-то другому, R просто создает дополнительную ссылку на существующее значение и увеличивает его счетчик ссылок.
Счетчик ссылок — это буквально целое число, которое сообщает R, из скольких мест происходит ссылка на значение. При попытке изменить значение R проверяет счетчик ссылок. И если счетчик ссылок равен 1, R выполняет модификацию на месте. Но если счетчик ссылок >1, R сначала создает копию значения, а затем вместо этого изменяет эту копию. Эта оптимизация известна как копирование при записи (CoW).
Обратите внимание, что выше не говорится об «именах»: потому что не имеет значения, привязаны ли эти ссылки к именам. Важно количество ссылок на значение, а не количество имен, с которыми оно связано.
Для иллюстрации вот ситуация перед заданием:
+---+ [C]-[T]----.
| x | ---> | 1 | list |
+---+ ·----------·
x
— переменная, которая ссылается на значение. Значения в R несут некоторую дополнительную информацию, включая счетчик ссылок (C
) и флаг, определяющий тип (T
) — это радикально упрощено, но здесь это все, что имеет значение.
Теперь, как только мы присваиваем значение другому имени, мы увеличиваем счетчик ссылок; например: y <- x
вызовет такую ситуацию:
+---+
| x | --.
+---+ | [C]-[T]----.
+-> | 2 | list |
+---+ | ·----------·
| y | --·
+---+
Теперь счетчик ссылок равен 2, и любое изменение значения потребует копирования, например. x[[1]] = 5
:
+---+ [C]-[T]----.---.
| x | ---> | 1 | list | 5 |
+---+ ·----------·---·
+---+ [C]-[T]----.
| y | ---> | 1 | list |
+---+ ·----------·
(Обратите внимание, что после копирования счетчик ссылок возвращается к 1.)
Но ситуация, о которой мы здесь говорим, более сложная. Во-первых, назначение вызывает увеличение количества ссылок, прежде чем что-либо еще будет сделано:1
+---+ [C]-[T]----.
| x | ---> | 2 | list |
+---+ ·----------·
На этом этапе счетчик ссылок увеличивается, но ссылка еще не назначена, поскольку для того, чтобы присвоить ее чему-то (x[[1]]
), сначала необходимо изменить значение. Это происходит дальше и запускает копирование, поскольку счетчик ссылок >1:
[C]-[T]----.
| 1 | list |
·----------·
+---+ [C]-[T]----.
| x | ---> | 1 | list |
+---+ ·----------·
Вновь созданная копия будет изменена и присвоена x
, как указано стрелкой выше. Исходная версия значения остается неизмененной и пока не упоминается в коде R; но у интерпретатора R есть внутренняя переменная, которая содержит эту ссылку.2
Теперь происходит изменение скопированного значения. Сначала список расширяется, чтобы освободить место для нового значения:
[C]-[T]----.
| 1 | list |
·----------·
+---+ [C]-[T]----.---.
| x | ---> | 1 | list | |
+---+ ·----------·---·
И затем, наконец, происходит фактическое присвоение, и ссылка на исходное значение x
помещается во вновь созданный элемент:
[C]-[T]----.
| 1 | list |-\
·----------· |
v
+---+ [C]-[T]----.---.
| x | ---> | 1 | list | |
+---+ ·----------·---·
3Кроме окружения.
1 Это происходит глубоко внутри интерпретатора R, и это произойдет независимо от того, как присвоение реализовано в R. То есть промежуточное присваивание `*tmp*`
— которое действительно происходит, как показано в ответе Роланда — не требуется для запуска этого. Где и как именно это происходит, немного сложно, потому что в зависимости от конкретной ситуации для этого может быть несколько мест. Я считаю, что в этой конкретной ситуации это происходит в applydefine , который вызывается, когда левая часть присваивания представляет собой сложное выражение (как в данном случае). И что интересно, это происходит до того, как `*tmp*`
определен, а затем R отключает подсчет ссылок для `*tmp*`, так что наличие `*tmp*`
фактически не влияет на то, создаем ли мы копию!
2 Фактически, после создания копии ей присваивается `*tmp*`
. Я пропускаю этот шаг, чтобы избежать необходимости рисовать еще три диаграммы, и это не меняет результата.
Фантастический вопрос. Формулировка немного неудачная: имена не привязаны к значениям, значения привязаны к именам, но на самом деле они также могут быть привязаны к подобъектам, а не только к именам, и это также вызовет CoW. Но, более того, я не уверен, что «из-за CoW» — это, строго говоря, правильный ответ на поставленный здесь вопрос: ответ более высокого уровня заключается в том, что объекты, не являющиеся объектами среды, в R имеют семантику значений. CoW — это деталь реализации на совершенно другом уровне абстракции.