Эффективный способ определить класс с несколькими, необязательно пустыми слотами в S4 R?

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

Подход 1:

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

Подход 2:

Второй подход заключается в создании единого «мастер-класса», включающего слот для всех 4 типов данных. Чтобы слоты были NULL для экземпляров отсутствующих данных, представляется необходимым сначала определить объединение виртуальных классов между классом NULL и классом нового типа данных, а затем использовать объединение виртуальных классов в качестве ожидаемого класса для соответствующий слот в мастер-классе. Вот пример (при условии, что каждый класс типа данных уже определен):

################################################################################
# Use setClassUnion to define the unholy NULL-data union as a virtual class.
################################################################################    
setClassUnion("dataClass1OrNULL", c("dataClass1", "NULL"))
setClassUnion("dataClass2OrNULL", c("dataClass2", "NULL"))
setClassUnion("dataClass3OrNULL", c("dataClass3", "NULL"))
setClassUnion("dataClass4OrNULL", c("dataClass4", "NULL"))
################################################################################
# Now define the master class with all 4 slots, and 
# also the possibility of empty (NULL) slots and an explicity prototype for
# slots to be set to NULL if they are not provided at instantiation.
################################################################################
setClass(Class="theMasterClass", 
    representation=representation(
        slot1="dataClass1OrNULL",
        slot2="dataClass2OrNULL",
        slot3="dataClass3OrNULL",
        slot4="dataClass4OrNULL"),
    prototype=prototype(slot1=NULL, slot2=NULL, slot3=NULL, slot4=NULL)
)
################################################################################

Поэтому вопрос можно перефразировать так:

Существуют ли более эффективные и/или гибкие альтернативы любому из этих подходов?

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


r oop s4
person Paul McMurdie    schedule 30.11.2011    source источник
comment
Хороший обзор S4 в R можно найти по адресу: github.com/hadley/devtools/wiki/ S4   -  person Paul McMurdie    schedule 01.12.2011


Ответы (1)


На мой взгляд...

Подход 2

Это как бы противоречит цели принятия формальной системы классов, а затем создания класса, который содержит неточно определенные слоты («A» или NULL). Как минимум, я бы попытался сделать так, чтобы DataClass1 имел значение по умолчанию, подобное NULL. В качестве простого примера здесь по умолчанию используется числовой вектор нулевой длины.

setClass("DataClass1", representation=representation(x="numeric"))
DataClass1 <- function(x=numeric(), ...) {
    new("DataClass1", x=x, ...)
}

потом

setClass("MasterClass1", representation=representation(dataClass1="DataClass1"))
MasterClass1 <- function(dataClass1=DataClass1(), ...) {
    new("MasterClass1", dataClass1=dataClass1, ...)
}

Одним из преимуществ этого является то, что методам не нужно проверять, является ли экземпляр в слоте NULL или «DataClass1».

setMethod(length, "DataClass1", function(x) length(x@x))
setMethod(length, "MasterClass1", function(x) length(x@dataClass1))

> length(MasterClass1())
[1] 0
> length(MasterClass1(DataClass1(1:5)))
[1] 5

В ответ на ваш комментарий о предупреждении пользователей, когда они обращаются к «пустым» слотам, и помня, что пользователи обычно хотят, чтобы функции что-то делали, а не сообщали им, что они делают что-то неправильно, я, вероятно, вернул бы пустой объект DataClass1(), который точно отражает состояние объекта. Возможно, метод show предоставит обзор, который укрепит статус слота — DataClass1: none. Это кажется особенно уместным, если MasterClass1 представляет собой способ координации нескольких различных анализов, из которых пользователь может выполнять только некоторые.

Ограничение этого подхода (или вашего подхода 2) заключается в том, что вы не получаете диспетчеризацию метода - вы не можете писать методы, которые подходят только для экземпляра с DataClass1 экземплярами, имеющими ненулевую длину, и вынуждены выполнять своего рода ручная диспетчеризация (например, с if или switch). Это может показаться ограничением для разработчика, но оно также относится и к пользователю — пользователь не понимает, какие операции однозначно подходят для экземпляров MasterClass1, которые имеют экземпляры DataClass1 ненулевой длины.

Подход 1

Когда вы говорите, что имена классов в иерархии будут сбивать с толку вашего пользователя, кажется, что это, возможно, указывает на более фундаментальную проблему — вы слишком стараетесь сделать всестороннее представление типов данных; пользователь никогда не сможет отслеживать ClassWithMatrixDataFrameAndTree, потому что он не представляет способ просмотра данных. Возможно, это возможность уменьшить ваши амбиции, чтобы по-настоящему заняться только самыми важными частями области, которую вы исследуете. Или, возможно, возможность переосмыслить, как пользователь может думать и взаимодействовать с данными, которые они собрали, и использовать разделение интерфейса (то, что видит пользователь) от реализации (как вы решили представлять данные в классы), предоставляемые системами классов, чтобы более эффективно инкапсулировать действия пользователя.

Если оставить в стороне названия и количество классов, когда вы говорите «сложно расширить для дополнительных типов данных в будущем», это заставляет меня задуматься, не смущают ли вас какие-то нюансы классов S4? Короткое решение состоит в том, чтобы не писать свои собственные методы initialize и полагаться на конструкторы, выполняющие сложную работу, в соответствии с

setClass("A", representation(x="numeric"))
setClass("B", representation(y="numeric"), contains="A")

A <- function(x = numeric(), ...) new("A", x=x, ...)
B <- function(a = A(), y = numeric(), ...) new("B", a, y=y, ...)

а потом

> B(A(1:5), 10)
An object of class "B"
Slot "y":
[1] 10

Slot "x":
[1] 1 2 3 4 5
person Martin Morgan    schedule 01.12.2011
comment
Мартин, спасибо за подробный ответ. Вы точно решили мою дилемму. Мой первый подход допускает четкую диспетчеризацию с существующей инфраструктурой S4, но дополнительные типы данных (которые мы предвидим) комбинаторно увеличивают необходимое количество классов. К сожалению, в этом случае от пользователей можно ожидать практически всех комбинаций типов данных, так что очевидного сокращения здесь нет. Между тем, мне не нравится идея отказа от стандартной диспетчеризации S4 в пользу ручного переключения или повторения if-теста во многих методах. - person Paul McMurdie; 02.12.2011
comment
Вы не собираетесь писать комбинаторное количество разных вещей, которые нужно делать с вашими классами, поэтому кажется, что вы не идентифицируете представление данных, которое позволяет вам делать то, что вы хотите, с одним (или двумя) классами. Возможно, вы ищете шаблон, в котором вы берете множество различных типов входных данных, и вместо того, чтобы точно передавать нюансы каждого вперед, вы выполняете некоторую начальную обработку, чтобы прийти к стандартному представлению. - person Martin Morgan; 02.12.2011
comment
Согласитесь, не будет комбинаторного количества разных вещей, которые можно делать с классами. Если мы примем как должное, что 4 основных класса данных, которые я первоначально описал, являются ортогональными описаниями одного и того же эксперимента и не могут быть объединены в дальнейшем, и мы также предположим, что пользователи часто будут отсутствовать данные для одного или нескольких из этих классов, то это звучит как ваше предложение касается Подхода 2 с некоторыми изменениями, чтобы лучше обрабатывать экземпляры с пустыми слотами. Моя мысль заключалась в том, чтобы включить автоматические и информативные ошибки в средствах доступа в качестве ответа на отсутствие необходимых данных. - person Paul McMurdie; 02.12.2011