Понимание кода для пользовательской функции модификации на месте?

Я наткнулся на этот пост: http://r.789695.n4.nabble.com/speeding-up-perception-tp3640920p3646694.html от Мэтта Доула, обсуждая некоторые ранние? идеи data.table пакета data.table.

Он использует следующий код:

x = list(a = 1:10000, b = 1:10000) 
class(x) = "newclass" 
"[<-.newclass" = function(x,i,j,value) x      # i.e. do nothing 
tracemem(x)
x[1, 2] = 42L 

В частности, я смотрю:

"[<-.newclass" = function(x,i,j,value) x

Я пытаюсь понять, что там сделано и как я могу использовать это обозначение.

Мне кажется:

  • я - индекс строки
  • j - индекс столбца
  • значение - это значение, которое необходимо присвоить
  • x - рассматриваемый объект

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

[<-.newclass находится в модификации класса для класса newclass.

Понимание того, что происходит: Обычно следующий код должен возвращать ошибку:

x = list(a = 1:10000, b = 1:10000) 
x[1, 2] = 42L 

поэтому я думаю, что пример кода не имеет практического использования.

Попытка использовать логику:

Простая не-смысловая попытка заключалась бы в том, чтобы скопировать значение, которое нужно вставить:

x[i, j] <- value^2

Полная попытка:

> x = matrix(1:9, 3, 3)
> class(x) = "newclass"
> "[<-.newclass" = function(x, i, j, value) x[i, j] <- value^2 # i.e. do something
> x[1, 2] = 9
Error: C stack usage  19923536 is too close to the limit

Кажется, это не работает.

Мои вопросы):

"[<-.newclass" = function(x,i,j,value) x 

Как именно эта нотация работает и как я буду ею пользоваться?

(Я добавляю тег data.table, так как связанное обсуждение касается "ссылки" на модификацию места в data.table, я думаю).

Ответы

Ответ 1

Функция '[<-'() (традиционно) используется для переназначения и, в более широком смысле, является функцией замены. Он также является общим (более конкретно, внутренним родовым), который позволяет вам писать собственные методы для него, как вы правильно поняли.

Запасные функции

В общем, когда вы вызываете функцию замены, например...

foo(x) <- bar(y)

... выражение в правой части <- (поэтому здесь bar(y)) передается как аргумент именованного value в 'foo<-'() с x в качестве первого аргумента, а объект x переназначается с помощью результат: т.е. указанный вызов эквивалентен написанию:

x <- 'foo<-'(x, value = bar(y))

Поэтому для того, чтобы работать вообще, все функции замены должны принимать как минимум два аргумента, один из которых должен иметь именованное value. Большинство функций замены имеют только эти два аргумента, но есть и исключения: например 'attr<-' и, как правило, subassignment.

Subassignment

Когда у вас есть вызов subassignment, такой как x[i, j] <- y, i и j передаются как дополнительные аргументы функции '[<-'() с x и y в качестве первого и аргументов value соответственно:

x <- '[<-'(x, i, j, value = y) # x[i, j] <- y

В случае matrix или data.frame, i и j будут использоваться для выбора строк и столбцов; но в целом это не обязательно. Метод для пользовательского класса мог бы что-либо сделать с аргументами. Рассмотрим этот пример:

x <- matrix(1:9, 3, 3)
class(x) <- "newclass" 

'[<-.newclass' <- function(x, y, z, value) {
  x + (y - z) * value # absolute nonsense
}

x[1, 2] <- 9
x
#>      [,1] [,2] [,3]
#> [1,]   -8   -5   -2
#> [2,]   -7   -4   -1
#> [3,]   -6   -3    0
#> attr(,"class")
#> [1] "newclass"

Является ли это полезным или разумным? Возможно нет. Но действительно ли это R-код? Абсолютно!

Менее распространено видеть пользовательские методы переназначения в реальных приложениях, поскольку '[<-'() обычно "просто работает", как вы могли ожидать, на основе базового объекта вашего класса. Заметным исключением является '[<-.data.frame', где базовый объект является списком, но subassignment ведет себя как матричный. (С другой стороны, многим классам нужен собственный метод подмножества, так как метод по умолчанию '['() уменьшает большинство атрибутов, включая атрибут class, см. ?'[' Для деталей).


Что касается того, почему ваш пример не работает: помните, что вы пишете метод для общей функции, и применяются все обычные правила. Если мы используем функциональную форму '[<-'() и расширяем отправку метода в вашем примере, мы сразу видим, почему это не удается:

'[<-.newclass' <- function(x, i, j, value) {
  x <- '[<-.newclass'(x, i, j, value = value^2)  # x[i, j] <- value^2
}

То есть, функция была определена рекурсивно, без базового случая, что привело к бесконечному циклу. Один из способов обойти это можно было бы unclass(x) до вызова следующего метода:

'[<-.newclass' <- function(x, i, j, value) {
  x <- unclass(x)
  x[i, j] <- value^2
  x # typically you would also add the class back here
}

(Или, используя несколько более продвинутую технику, тело можно также заменить следующим явным следующим методом: NextMethod(value = value^2). Это играет лучше с наследованием и суперклассами.)

И просто чтобы убедиться, что он работает:

x <- matrix(1:9, 3, 3)
class(x) <- "newclass" 

x[1, 2] <- 9
x
#>      [,1] [,2] [,3]
#> [1,]    1   81    7
#> [2,]    2    5    8
#> [3,]    3    6    9

Совершенно смущает!


Что касается контекста примера подавления Dowle "ничего не делать", я считаю, что это должно было проиллюстрировать это в R 2.13.0, пользовательский метод переназначения всегда будет вызывать глубокую копию объекта, даже если сам метод ничего. (Это уже не так, поскольку я считаю, что 3.1.0.)

Создано в 2018-08-15 пакетом reprex (v0.2.0).