Путаница с изменением на месте в R

В одном из упражнений Advanced R, раздел 2.5, посвященном изменению на месте, задается вопрос, почему следующий код не создает циклический список:

x <- list()
x[[1]] <- x

Advanced R Solutions говорит, что это не циклично из-за запуска копирования при изменении. Однако в начале раздела Уикхем сказал: «Если к объекту привязано одно имя, R изменит его на месте». Почему здесь не применимо изменение на месте? Я думал, что, возможно, когда вы устанавливаете x[[1]] <- x, вы привязываете второе имя к объекту, но кажется, что это также одновременно отменяет привязку x к пустому списку, потому что он переопределяет x как список, содержащий пустой список в качестве своего первого элемента. Это потому, что R каким-то образом не регистрирует отвязку x до тех пор, пока не произойдет привязка к x[[1]], или мне здесь чего-то не хватает?

Фантастический вопрос. Формулировка немного неудачная: имена не привязаны к значениям, значения привязаны к именам, но на самом деле они также могут быть привязаны к подобъектам, а не только к именам, и это также вызовет CoW. Но, более того, я не уверен, что «из-за CoW» — это, строго говоря, правильный ответ на поставленный здесь вопрос: ответ более высокого уровня заключается в том, что объекты, не являющиеся объектами среды, в R имеют семантику значений. CoW — это деталь реализации на совершенно другом уровне абстракции.

Konrad Rudolph 31.07.2024 16:32
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
1
103
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Оператор [[<- привязывает объект к второму имени. Согласно определению языка ваш код идентичен этому:

`*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 в [[<-).

Konrad Rudolph 31.07.2024 16:43

@KonradRudolph Есть вторая привязка: вектор привязан к списку.

Roland 31.07.2024 16:51

Конечно, он привязан к индексу в списке. Но оно не связано с именем, разве что транзитивно.

Konrad Rudolph 31.07.2024 20:27

@KonradRudolph Я не думаю, что различие имеет значение (или должно иметь значение) для запуска копирования при изменении.

Roland 01.08.2024 06:45

Вот почему я имею в виду «отвлекающий маневр»: настаивание на привязке ценностей к именам вводит в заблуждение. Что на самом деле имеет значение, так это то, из скольких мест в памяти ссылается значение, независимо от того, связаны ли эти места с именем. Даже если бы <- был реализован по-другому (без обхода через *tmp*), это не изменило бы поведение CoW. Если говорить более решительно: ответ на вопрос о *tmp*, кстати, верен, но совершенно не имеет значения.

Konrad Rudolph 01.08.2024 10:24

@KonradRudolph Я согласен, что речь идет о том, «из скольких мест в памяти ссылается значение» (я бы назвал это привязками). Я не согласен, что *tmp* неактуален, потому что это дополнительная привязка.

Roland 01.08.2024 10:29

Ладно, может быть, слово «нерелевантно» звучит слишком сильно, очевидно, что его присутствие увеличивает количество ссылок. Но, опять же, даже если бы *tmp* не фигурировало в картине, поведение было бы таким же, потому что (как отмечается в моем комментарии под вопросом), «из-за CoW» на самом деле не является ответом. CoW — это деталь реализации для оптимизации семантики значений. Но это не причина, по которой списки в R имеют семантику значений.

Konrad Rudolph 01.08.2024 10:34

На самом деле, если я правильно читаю код интерпретатора R, наличие *tmp* все-таки не имеет значения, поскольку R явно отключает подсчет ссылок для этого значения. — Смотрите сноски со ссылками в моем ответе. (Я по-прежнему считаю, что ваш ответ ценен как объяснение среднего уровня: не слишком высокого и не слишком низкого уровня.)

Konrad Rudolph 01.08.2024 11:45
Ответ принят как подходящий

Я несколько не согласен с «правильным» ответом, данным «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*`. Я пропускаю этот шаг, чтобы избежать необходимости рисовать еще три диаграммы, и это не меняет результата.

Другие вопросы по теме