Решение может заключаться в использовании S4 методов и разрешении работы внутреннего диспетчера R за вас (см. Пример ниже). Таким образом, вы несколько «пуленепробиваемы» в том, что можете систематически обновлять ваш код, не рискуя что-то сломать.
Основные преимущества
Ключевым моментом здесь является то, что методы S4 поддерживают многократную отправку .
Таким образом, ваша функция всегда будет foo
(в отличие от необходимости отслеживать foo1
, foo2
и т. Д.), В то время как новые функциональные возможности могут быть легко реализованы (путем добавления соответствующих методов), не касаясь «старых» методы (на которые могут полагаться другие люди / пакеты).
Основные функции, которые вам понадобятся:
setGeneric
setMethod
setRefClass
(справочные классы S4; личная рекомендация) или setClass
(класс S4; я не буду использовать их по причине, описанной в «Дополнительные замечания» в самом конце)
"Минусы"
Вам необходимо переключиться с логики S3 на S4
Это означает, что вам нужно написать немного больше кода, чем то, к чему вы могли бы привыкнуть (общие определения методов, определения методов и, возможно, собственные определения классов (см. Пример ниже). Но это «покупает» вас и ваш код намного более структурирован и делает его более устойчивым.
Это может также подразумевать, что вы в конечном итоге будете копать все глубже и глубже в мир объектно-ориентированного программирования или объектно-ориентированного проектирования, Хотя я лично считаю, что это хорошо (мое личное эмпирическое правило: чем сложнее / распределено ваше приложение, тем лучше вы отказываетесь от использования ООП), некоторые считают эти подходы R-нетипичными (я категорически не согласен с R имеет превосходные OO-функции, которые поддерживаются Core Team ) или "не подходит" для R (это может быть правдой в зависимости от того, насколько вы полагаетесь на "не-OOP") пакеты / код). Если вы хотите пойти по этому пути, возможно, вы захотите ознакомиться с SOLID принципами объектно-ориентированного проектирования. Вы также можете проверить следующие книги: Чистый кодер и Прагматичный программист .
Если эффективность вычислений (например, при оценке статистических моделей) действительно важна, использование методов S4 и эталонных классов S4 может немного замедлить работу. В конце концов, здесь больше кода по сравнению с S3. Но я бы рекомендовал проверить влияние этого от случая к случаю через system.time()
и / или microbenchmark::microbenchmark()
вместо выбора «идеологических» сторон (S3 против S4).
Пример
Начальная функция
Предположим, вы находитесь в отделе A , и кто-то в вашей команде начал с создания функции с именем foo()
foo <- function(x, y) {
x + y
}
foo(x=10, y=20)
Первый запрос на изменение
Вы хотели бы иметь возможность расширять его, не нарушая "старый" код, основанный на foo()
.
Теперь, я думаю, мы все согласны с тем, что это может быть довольно сложно сделать.
Вам либо нужно явно изменить исходный код foo()
(каждый раз, когда существует риск того, что вы сломаете что-то, что уже использовалось для работы; это нарушает "O" в SOLID : Откройте Closed-Principle ) или вам нужно указать альтернативные имена, такие как foo1
, foo2
и т. Д. (Очень сложно отследить, какая функция выполняет что-либо).
foo <- function(x, y, type=c("old", "new")) {
type <- match.arg(type, choices=c("old", "new"))
if (type == "old") {
x + y
} else if (type == "new") {
x * y
}
}
foo(x=10, y=20)
[1] 30
foo(x=10, y=20, type="new")
[1] 200
foo1 <- function(x, y) {
x * y
}
foo1(x=10, y=20)
[1] 200
Давайте посмотрим, как методы S4 и множественная диспетчеризация могут действительно помочь нам здесь.
Общий метод
Вам нужно начать с превращения foo()
в универсальный метод.
setGeneric(
name="foo",
signature=c("x", "y", ".ctx", ".ns"),
def=function(x, y, ..., .ctx, .ns) {
standardGeneric("foo")
}
)
Проще говоря: сам универсальный метод еще ничего не делает. Это просто предварительное условие для того, чтобы иметь возможность указывать «фактические» методы для своих аргументов подписи, которые делают что-то полезное.
Аргументы подписи
Степень гибкости по отношению к исходной проблеме напрямую связана с числом аргументов подписи, которые вы объявляете (signature=c("x", "y", ".ctx", ".ns")
): чем больше аргументов подписи, тем больше гибкости у вас есть, но чем сложнее может быть код хорошо (относительно того, сколько кода вы должны написать).
Опять же, в упрощенных словах: аргументы подписи (и ее классы) используются диспетчером метода для получения правильного метода, который выполняет фактическую работу.
Представьте себе, что диспетчер методов похож на клерка в бизнесе проката лыж: вы даете ему произвольный большой набор информации о подписи (то есть информации, которая «четко отличает вас от других»: ваш возраст, рост размер обуви и уровень квалификации), и он использует эту информацию, чтобы предоставить вам необходимое снаряжение для катания по склонам. Думайте о диспетчере методов R как о клерке, у которого есть доступ к камере хранения лыжного проката. Но вместо лыжного снаряжения он вернет методы.
Обратите внимание, что мы сказали, что наши "старые" аргументы x
и y
отныне должны быть аргументами подписи, а также есть два новых аргумента: .ctx
и .ns
. Я доберусь до них через минуту. Именно эти аргументы обеспечат нам гибкость, к которой мы стремимся.
Начальное определение метода
Теперь мы определим «вариант» (метод) универсального метода для следующего «сценария подписи»:
x
является numeric
y
is numeric
.ctx
просто не будет предоставляться при вызове метода и поэтому будет missing
.ns
просто не будет предоставляться при вызове метода и поэтому будет missing
Думайте об этом как о регистрации информации о подписи с явным оборудованием проката лыж. Как только вы это сделаете и попросите ваше оборудование, единственное, что должен сделать клерк, - это пойти в хранилище и посмотреть, какое оборудование связано с вашей личной информацией.
setMethod(
f="foo",
signature=signature(x="numeric", y="numeric", .ctx="missing", .ns="missing"),
definition=function(x, y, ..., .ctx, .ns) {
x + y
}
)
Когда мы вызываем foo
с этим «сценарием подписи» (запрашивая метод, который мы зарегистрировали для этого сценария), диспетчер методов точно знает, какой фактический метод ему нужно выбрать из хранилища:
foo(x=10, y=20)
[1] 30
Первое обновление
Теперь кто-то из отдела B приходит, смотрит на foo()
, любит его, но решает, что foo()
необходимо обновить (x * y
вместо x + y
), если он должен быть используется в его отделе.
Именно тогда в игру вступает .ctx
(сокращение от context ): это аргумент, по которому мы можем различить контексты приложения .
Определение класса, который представляет новый контекст приложения
setRefClass("ApplicationContextDepartmentB")
При вызове foo()
мы предоставим ему экземпляр этого класса
(.ctx=new("ApplicationContextDepartmentB")
)
Определение нового метода для нового контекста приложения
Обратите внимание, как мы регистрируем аргумент подписи .ctx
в нашем новом классе ApplicationContextDepartmentB
:
setMethod(
f="foo",
signature=signature(x="numeric", y="numeric",
.ctx="ApplicationContextDepartmentB", .ns="missing"),
definition=function(x, y, ..., .ctx, .ns) {
out <- x * y
attributes(out)$description <- "I'm different from the original foo()"
return(out)
}
)
Таким образом, диспетчер методов точно знает, что он должен возвращать «новый» метод вместо «старого» метода, когда мы вызываем foo()
следующим образом:
foo(x=1, y=10, .ctx=new("ApplicationContextDepartmentB"))
[1] 10
attr(,"description")
[1] "I'm different from the original foo()"
На "старый" метод это никак не влияет:
foo(x=1, y=10)
[1] 30
Второе обновление
Предположим, что кто-то из отдела C приходит и предлагает еще одну "конфигурацию" или версию для foo()
. Вы можете легко это сделать, не нарушая ничего, что вы реализовали для отделов A и B до настоящего момента, выполнив ту же процедуру, что и для отдела B .
Но мы даже сделаем еще один шаг: мы определим два дополнительных класса, которые позволят нам различать разные «пространства имен» (вот где .ns
вступает в игру).
Думайте о пространствах имен как о способе различения различных сценариев времени выполнения для конкретного метода для конкретного контекста приложения (т. Е. «Тестирование» и «продуктивный режим»).
Определение классов
setRefClass("ApplicationContextDepartmentC")
setRefClass("TestNamespace")
setRefClass("ProductionNamespace")
Определение нового метода для нового контекста приложения и «тестового» сценария
Обратите внимание, как мы регистрируем аргументы подписи .ctx
для нашего нового класса ApplicationContextDepartmentC
и .ns
для нашего нового класса TestNamespace
:
setMethod(
f="foo",
signature=signature(x="character", y="numeric",
.ctx="ApplicationContextDepartmentC", .ns="TestNamespace"),
definition=function(x, y, ..., .ctx, .ns) {
data.frame(x, y, test.ok=rep(TRUE, length(x)))
}
)
Опять же, диспетчер методов будет искать правильный метод при вызове foo()
следующим образом:
foo(x=letters[1:5], y=11:15, .ctx=new("ApplicationContextDepartmentC"),
.ns=new("TestNamespace"))
x y test.ok
1 a 11 TRUE
2 b 12 TRUE
3 c 13 TRUE
4 d 14 TRUE
5 e 15 TRUE
Определение нового метода для нового контекста приложения и «производительного» сценария
setMethod(
f="foo",
signature=signature(x="character", y="numeric",
.ctx="ApplicationContextDepartmentC", .ns="ProductionNamespace"),
definition=function(x, y, ..., .ctx, .ns) {
data.frame(x, y)
}
)
Мы сообщаем диспетчеру методов, что теперь хотим, чтобы метод был зарегистрирован для этого сценария или пространства имен следующим образом:
foo(x=letters[1:5], y=11:15, .ctx=new("ApplicationContextDepartmentC"),
.ns=new("ProductionNamespace"))
x y
1 a 11
2 b 12
3 c 13
4 d 14
5 e 15
Обратите внимание, что вы можете использовать классы TestNamespace
и ProductionNamespace
где угодно. Эти классы никак не связаны с ApplicationContextDepartmentC
, так что вы можете, например, также использовать для всех других ваших сценариев применения.
Дополнительные замечания к определениям методов
Часто полезно использовать метод, который принимает классы ANY
для аргументов сигнатуры и определяет более строгие методы по мере развития вашего программного обеспечения:
setMethod(
f="foo",
signature=signature(x="ANY", y="ANY", .ctx="missing", .ns="missing"),
definition=function(x, y, ..., .ctx, .ns) {
message("Value of x:")
print(x)
message("Value of y:")
print(y)
}
)
foo(x="Hello World!", y=rep(TRUE, 3))
Value of x:
[1] "Hello World!"
Value of y:
[1] TRUE TRUE TRUE
Дополнительные замечания к определениям классов
Я предпочитаю ссылочные классы S4, а не классы S4 из-за возможностей самоссылки эталонных классов S4:
setRefClass(
Class="A",
fields=list(
x1="numeric",
x2="logical"
),
methods=list(
getX1=function() {
.self$x1
},
getX2=function() {
.self$x2
},
setX1=function(x) {
.self$x1 <- x
},
setX2=function(x) {
.self$field("x2", x)
},
addX1AndX2=function() {
.self$getX1() + .self$getX2()
}
)
)
x <- new("A", x1=10, x2=TRUE)
x$getX1()
[1] 10
x$getX2()
[1] TRUE
x$addX1AndX2()
[1] 11
S4 Классы не имеют этой функции.
Последующие модификации значений полей:
x$setX1(100)
x$addX1AndX2()
[1] 101
x$x1 <- 1000
x$addX1AndX2()
[1] 1001
Дополнительные замечания по документированию методов и классов
Я настоятельно рекомендую использовать пакеты roxygen2
и devtools
для документирования ваших методов и классов. Возможно, вы также захотите посмотреть на пакет roxygen3
.
Документирование общих методов с помощью roxygen2
:
#' Foo
#'
#' This method takes \code{x} and \code{y} and adds them.
#'
#' Some details here
#'
#' @param x \strong{Signature argument}.
#' @param y \strong{Signature argument}.
#' @param ... Further arguments to be passed to subsequent functions.
#' @param .ctx \strong{Signature argument}.
#' Application context.
#' @param .ns \strong{Signature argument}.
#' Application namespace. Usually used to distinguish different context
#' versions or configurations.
#' @author Janko Thyson \email{john.doe@@something.com}
#' @references \url{http://www.something.com/}
#' @example inst/examples/foo.R
#' @docType methods
#' @rdname foo-methods
#' @export
setGeneric(
name="foo",
signature=c("x", "y", ".ctx", ".ns"),
def=function(x, y, ..., .ctx, .ns) {
standardGeneric("foo")
}
)
Методы документирования с помощью roxygen2
:
#' @param x \code{\link{character}}. Character vector.
#' @param y \code{\link{numeric}}. Numerical vector.
#' @param .ctx \code{\link{ApplicationContextDepartmentC}}.
#' @param .ns \code{\link{ProductionNamespace}}.
#' @return \code{\link{data.frame}}. Some data frame.
#' @rdname foo-methods
#' @aliases foo,character,numeric,missing,missing-method
#' @export
setMethod(
f="foo",
signature=signature(x="character", y="numeric",
.ctx="ApplicationContextDepartmentC", .ns="ProductionNamespace"),
definition=function(x, y, ..., .ctx, .ns) {
data.frame(x, y)
}
)