R Language
Внедрить шаблон государственного устройства с использованием класса S4
Поиск…
Вступление
Конечные состояния. Концепции машин обычно реализуются в рамках языков ООП, например, с использованием языка Java, на основе шаблона State, определенного в GOF (относится к книге «Шаблоны проектирования»).
R предоставляет несколько механизмов для моделирования парадигмы OO, давайте применим S4 Object System для реализации этого шаблона.
Разбор строк с использованием машины состояния
Применим шаблон State Machine для разбора строк с определенным шаблоном с использованием функции класса S4 от R.
ПРОБЛЕМА
Нам нужно проанализировать файл, в котором каждая строка содержит информацию о человеке, используя разделитель ( ";"
), но предоставленная информация является необязательной, и вместо предоставления пустого поля она отсутствует. В каждой строке мы можем получить следующую информацию: Name;[Address;]Phone
. Если информация адреса не является обязательной, иногда мы ее имеем, а иногда нет, например:
GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553
Вторая строка не содержит адресной информации. Поэтому число разделителей может быть отклонено, как в этом случае с одним разделителем, а для других линий - двумя разделителями. Поскольку количество разделителей может варьироваться, одним из способов устранить эту проблему является распознавание присутствия или отсутствия заданного поля на основе его шаблона. В этом случае мы можем использовать регулярное выражение для идентификации таких паттернов. Например:
- Имя :
"^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$"
. Например:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
и т. Д. - Адрес :
"^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$"
. Например:11020 LE JEUNE ROAD
,87 SW 27TH
. Для простоты здесь мы не включаем zipcode, city, state, но я могу быть включен в это поле или добавлять дополнительные поля. - Телефон :
"^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$"
. Например:305-123-4567, 305 123 4567, +1-786-123-4567
.
Примечания :
- Я рассматриваю наиболее распространенную схему американских адресов и телефонов, ее можно легко расширить, чтобы рассмотреть более общие ситуации.
- В R знак
"\"
имеет особое значение для символьных переменных, поэтому нам нужно его избежать. - Чтобы упростить процесс определения регулярных выражений, хорошей рекомендацией является использование следующей веб-страницы: regex101.com , поэтому вы можете играть с ней в данном примере, пока не получите ожидаемый результат для всех возможных комбинаций.
Идея состоит в том, чтобы идентифицировать каждое поле линии на основе ранее определенных шаблонов. Шаблон состояния определяет следующие сущности (классы), которые взаимодействуют для управления конкретным поведением (шаблон состояния - шаблон поведения):
Опишем каждый элемент, рассматривая контекст нашей проблемы:
-
Context
: хранит контекстную информацию процесса синтаксического анализа, то есть текущее состояние и обрабатывает весь процесс машинного процесса. Для каждого состояния выполняется действие (handle()
), но контекст делегирует его на основе состояния на метод действия, определенный для определенного состояния (handle()
из классаState
). Он определяет интерфейс, представляющий интерес для клиентов. Наш классContext
можно определить следующим образом:- Атрибуты:
state
- Методы:
handle()
, ...
- Атрибуты:
-
State
: абстрактный класс, представляющий состояние State Machine. Он определяет интерфейс для инкапсуляции поведения, связанного с конкретным состоянием контекста. Его можно определить следующим образом:- Атрибуты:
name, pattern
- Методы:
doAction()
,isState
(с использованием атрибутаpattern
проверить, принадлежит ли входной аргумент этому шаблону состояния), ...
- Атрибуты:
-
Concrete States
(государственные подклассы): Каждый подкласс классаState
, реализующее поведение , связанное с состояниемContext
. Наши подклассы:InitState
,NameState
,AddressState
,PhoneState
. Такие классы просто реализуют общий метод, используя определенную логику для таких состояний. Дополнительные атрибуты не требуются.
Примечание. Это вопрос предпочтения, как назвать метод, который выполняет действия, handle()
, doAction()
или goNext()
. Имя метода doAction()
может быть одинаковым для обоих классов ( State
или Context
), которые мы предпочитали называть как handle()
в классе Context
чтобы избежать путаницы при определении двух общих методов с одинаковыми входными аргументами, но с другим классом.
ПЕРСОНАЛЬНЫЙ КЛАСС
Используя синтаксис S4, мы можем определить класс Person следующим образом:
setClass(Class = "Person",
slots = c(name = "character", address = "character", phone = "character")
)
Это хорошая рекомендация для инициализации атрибутов класса. Документация setClass
предлагает использовать общий метод, обозначенный как "initialize"
, вместо использования устаревших атрибутов, таких как: prototype, representation
.
setMethod("initialize", "Person",
definition = function(.Object, name = NA_character_,
address = NA_character_, phone = NA_character_) {
.Object@name <- name
.Object@address <- address
.Object@phone <- phone
.Object
}
)
Поскольку метод initialize уже является стандартным универсальным методом пакетных methods
, нам необходимо соблюдать определение исходного аргумента. Мы можем проверить его, набрав на подсказке R:
> initialize
Он возвращает полное определение функции, вы можете видеть вверху, кто определен как функция:
function (.Object, ...) {...}
Поэтому, когда мы используем setMethod
мы должны следовать exaccty того же синтаксиса ( .Object
).
Другой существующий общий метод - это show
, он эквивалентен методу toString()
из Java, и неплохо иметь конкретную реализацию для домена класса:
setMethod("show", signature = "Person",
definition = function(object) {
info <- sprintf("%s@[name='%s', address='%s', phone='%s']",
class(object), object@name, object@address, object@phone)
cat(info)
invisible(NULL)
}
)
Примечание . Мы используем то же соглашение, что и в реализации Java toString()
по умолчанию.
Предположим, мы хотим сохранить анализируемую информацию (список объектов Person
) в наборе данных, тогда мы должны сначала преобразовать список объектов в нечто, что может преобразовать R (например, принудить объект как список). Мы можем определить следующий дополнительный метод (более подробно об этом см. Сообщение )
setGeneric(name = "as.list", signature = c('x'),
def = function(x) standardGeneric("as.list"))
# Suggestion taken from here:
# http://stackoverflow.com/questions/30386009/how-to-extend-as-list-in-a-canonical-way-to-s4-objects
setMethod("as.list", signature = "Person",
definition = function(x) {
mapply(function(y) {
#apply as.list if the slot is again an user-defined object
#therefore, as.list gets applied recursively
if (inherits(slot(x,y),"Person")) {
as.list(slot(x,y))
} else {
#otherwise just return the slot
slot(x,y)
}
},
slotNames(class(x)),
SIMPLIFY=FALSE)
}
)
R не обеспечивает синтаксис сахара для OO, потому что язык изначально был задуман, чтобы обеспечить ценные функции для статистиков. Поэтому для каждого пользовательского метода требуются две части: 1) часть определения (через setGeneric
) и 2) часть реализации (через setMethod
). Как в приведенном выше примере.
СОСТОЯНИЕ
Следуя синтаксису S4, давайте определим абстрактный класс State
.
setClass(Class = "State", slots = c(name = "character", pattern = "character"))
setMethod("initialize", "State",
definition = function(.Object, name = NA_character_, pattern = NA_character_) {
.Object@name <- name
.Object@pattern <- pattern
.Object
}
)
setMethod("show", signature = "State",
definition = function(object) {
info <- sprintf("%s@[name='%s', pattern='%s']", class(object),
object@name, object@pattern)
cat(info)
invisible(NULL)
}
)
setGeneric(name = "isState", signature = c('obj', 'input'),
def = function(obj, input) standardGeneric("isState"))
setGeneric(name = "doAction", signature = c('obj', 'input', 'context'),
def = function(obj, input, context) standardGeneric("doAction"))
Каждый подкласс из State
будет ассоциирован с name
и pattern
, но также может определить, принадлежит ли данный вход этому состоянию или нет ( isState()
), а также реализовать соответствующие действия для этого состояния ( doAction()
метод).
Чтобы понять процесс, давайте определим матрицу перехода для каждого состояния на основе полученного ввода:
Входное / текущее состояние | В этом | название | Адрес | Телефон |
---|---|---|---|---|
название | название | |||
Адрес | Адрес | |||
Телефон | Телефон | Телефон | ||
Конец | Конец |
Примечание: ячейка [row, col]=[i,j]
представляет состояние назначения для текущего состояния j
, когда оно принимает вход i
.
Это означает, что под именем состояния он может принимать два входа: адрес или номер телефона. Другой способ представления таблицы транзакций - использовать следующую диаграмму состояний машины UML :
Давайте реализуем каждое конкретное государство как к югу от состояния класса State
ГОСУДАРСТВЕННЫЕ СУБ-КЛАССЫ
Init State :
Начальное состояние будет реализовано с помощью следующего класса:
setClass("InitState", contains = "State")
setMethod("initialize", "InitState",
definition = function(.Object, name = "init", pattern = NA_character_) {
.Object@name <- name
.Object@pattern <- pattern
.Object
}
)
setMethod("show", signature = "InitState",
definition = function(object) {
callNextMethod()
}
)
В R, чтобы указать, что класс является подклассом другого класса, использует атрибут contains
и указывает имя класса родительского класса.
Поскольку подклассы просто реализуют общие методы, не добавляя дополнительных атрибутов, тогда метод show
просто вызовет эквивалентный метод из верхнего класса (с помощью метода: callNextMethod()
)
Начальное состояние не связано с шаблоном, оно просто представляет собой начало процесса, затем мы инициализируем класс значением NA
.
Теперь позволяет реализовать общие методы из класса State
:
setMethod(f = "isState", signature = "InitState",
definition = function(obj, input) {
nameState <- new("NameState")
result <- isState(nameState, input)
return(result)
}
)
Для этого конкретного состояния (без pattern
) идея, что он просто инициализирует процесс синтаксического анализа, ожидая первого поля, будет name
, иначе это будет ошибка.
setMethod(f = "doAction", signature = "InitState",
definition = function(obj, input, context) {
nameState <- new("NameState")
if (isState(nameState, input)) {
person <- context@person
person@name <- trimws(input)
context@person <- person
context@state <- nameState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
Метод doAction
обеспечивает переход и обновляет контекст с извлеченной информацией. Здесь мы обращаемся к контекстной информации через @-operator
. Вместо этого мы можем определить методы get/set
, чтобы инкапсулировать этот процесс (как это предусмотрено в лучших практиках OO: инкапсуляция), но это добавит еще четыре метода для каждого get-set
без добавления значения для этого примера.
Это хорошая рекомендация во всей реализации doAction
, чтобы добавить защиту, если входной аргумент не определен правильно.
Имя государства
Вот определение этого определения класса:
setClass ("NameState", contains = "State")
setMethod("initialize","NameState",
definition=function(.Object, name="name",
pattern = "^([A-Z]'?\\s+)* *[A-Z]+(\\s+[A-Z]{1,2}\\.?,? +)*[A-Z]+((-|\\s+)[A-Z]+)*$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "NameState",
definition = function(object) {
callNextMethod()
}
)
Мы используем функцию grepl
для проверки того, что вход принадлежит данному шаблону.
setMethod(f="isState", signature="NameState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
Теперь мы определяем действие, выполняемое для данного состояния:
setMethod(f = "doAction", signature = "NameState",
definition=function(obj, input, context) {
addressState <- new("AddressState")
phoneState <- new("PhoneState")
person <- context@person
if (isState(addressState, input)) {
person@address <- trimws(input)
context@person <- person
context@state <- addressState
} else if (isState(phoneState, input)) {
person@phone <- trimws(input)
context@person <- person
context@state <- phoneState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
Здесь мы рассмотрим возможные переходы: один для состояния адреса, а другой для состояния телефона. Во всех случаях мы обновляем контекстную информацию:
- Информация о
person
:address
илиphone
с аргументом ввода. -
state
процесса
Способ идентификации состояния заключается в вызове метода: isState()
для определенного состояния. Мы создаем стандартные состояния по умолчанию ( addressState, phoneState
), а затем запрашиваем конкретную проверку.
Логика для реализации других подклассов (по одному на состояние) очень похожа.
Состояние адреса
setClass("AddressState", contains = "State")
setMethod("initialize", "AddressState",
definition = function(.Object, name="address",
pattern = "^\\s[0-9]{1,4}(\\s+[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,2}|[A-Z\\s0-9]+)$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "AddressState",
definition = function(object) {
callNextMethod()
}
)
setMethod(f="isState", signature="AddressState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
setMethod(f = "doAction", "AddressState",
definition=function(obj, input, context) {
phoneState <- new("PhoneState")
if (isState(phoneState, input)) {
person <- context@person
person@phone <- trimws(input)
context@person <- person
context@state <- phoneState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
Состояние телефона
setClass("PhoneState", contains = "State")
setMethod("initialize", "PhoneState",
definition = function(.Object, name = "phone",
pattern = "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "PhoneState",
definition = function(object) {
callNextMethod()
}
)
setMethod(f = "isState", signature = "PhoneState",
definition = function(obj, input) {
result <- grepl(obj@pattern, input, perl = TRUE)
return(result)
}
)
Здесь мы добавляем информацию человека в список persons
context
.
setMethod(f = "doAction", "PhoneState",
definition = function(obj, input, context) {
context <- addPerson(context, context@person)
context@state <- new("InitState")
return(context)
}
)
КОНТЕКСТНЫЙ КЛАСС
Теперь давайте объясним реализацию класса Context
. Мы можем определить его, учитывая следующие атрибуты:
setClass(Class = "Context",
slots = c(state = "State", persons = "list", person = "Person")
)
куда
-
state
: Текущее состояние процесса -
person
: Текущее лицо, оно представляет информацию, которую мы уже проанализировали из текущей строки. -
persons
: список обработанных обработанных лиц.
Примечание . При желании мы можем добавить name
чтобы идентифицировать контекст по имени, если мы работаем с несколькими типами парсера.
setMethod(f="initialize", signature="Context",
definition = function(.Object) {
.Object@state <- new("InitState")
.Object@persons <- list()
.Object@person <- new("Person")
return(.Object)
}
)
setMethod("show", signature = "Context",
definition = function(object) {
cat("An object of class ", class(object), "\n", sep = "")
info <- sprintf("[state='%s', persons='%s', person='%s']", object@state,
toString(object@persons), object@person)
cat(info)
invisible(NULL)
}
)
setGeneric(name = "handle", signature = c('obj', 'input', 'context'),
def = function(obj, input, context) standardGeneric("handle"))
setGeneric(name = "addPerson", signature = c('obj', 'person'),
def = function(obj, person) standardGeneric("addPerson"))
setGeneric(name = "parseLine", signature = c('obj', 's'),
def = function(obj, s) standardGeneric("parseLine"))
setGeneric(name = "parseLines", signature = c('obj', 's'),
def = function(obj, s) standardGeneric("parseLines"))
setGeneric(name = "as.df", signature = c('obj'),
def = function(obj) standardGeneric("as.df"))
Используя такие общие методы, мы контролируем все поведение процесса синтаксического анализа:
-
handle()
: вызывается конкретныйdoAction()
текущегоstate
. -
addPerson
: Как только мы достигнем конечного состояния, нам нужно добавитьperson
в списокpersons
мы проанализировали. -
parseLine()
: проанализировать одну строку -
parseLines()
: Разбор нескольких строк (массив строк) -
as.df()
: Извлечь информацию из спискаpersons
в объект фрейма данных.
Перейдем теперь к соответствующим реализациям:
handle()
, делегаты метода doAction()
из текущего state
context
:
setMethod(f = "handle", signature = "Context",
definition = function(obj, input) {
obj <- doAction(obj@state, input, obj)
return(obj)
}
)
setMethod(f = "addPerson", signature = "Context",
definition = function(obj, person) {
obj@persons <- c(obj@persons, person)
return(obj)
}
)
Сначала мы разделим исходную строку в массиве с помощью разделителя, чтобы идентифицировать каждый элемент через R-функцию strsplit()
, а затем итерации для каждого элемента в качестве входного значения для данного состояния. handle()
метод снова возвращает context
с обновленной информацией ( state
, person
, persons
, атрибут).
setMethod(f = "parseLine", signature = "Context",
definition = function(obj, s) {
elements <- strsplit(s, ";")[[1]]
# Adding an empty field for considering the end state.
elements <- c(elements, "")
n <- length(elements)
input <- NULL
for (i in (1:n)) {
input <- elements[i]
obj <- handle(obj, input)
}
return(obj@person)
}
)
Becuase R делает копию входного аргумента, нам нужно вернуть контекст ( obj
):
setMethod(f = "parseLines", signature = "Context",
definition = function(obj, s) {
n <- length(s)
listOfPersons <- list()
for (i in (1:n)) {
ipersons <- parseLine(obj, s[i])
listOfPersons[[i]] <- ipersons
}
obj@persons <- listOfPersons
return(obj)
}
)
persons
атрибута - это список экземпляров класса S4 Person
. Это не может быть принуждено к любому стандарту, потому что R не знает, как обрабатывать экземпляр класса, определенного пользователем. Решение состоит в том, чтобы преобразовать Person
в список, используя ранее описанный метод as.list
. Тогда мы можем применить эту функцию к каждому элементу списка persons
, через lapply()
функцию. Затем в следующем вызове функции lappy()
теперь применяется функция data.frame
для преобразования каждого элемента persons.list
в фрейм данных. Наконец, rbind()
вызывается для добавления каждого элемента, преобразованного в новую строку генерируемого кадра данных (более подробно об этом см. В этом сообщении )
# Sugestion taken from this post:
# http://stackoverflow.com/questions/4227223/r-list-to-data-frame
setMethod(f = "as.df", signature = "Context",
definition = function(obj) {
persons <- obj@persons
persons.list <- lapply(persons, as.list)
persons.ds <- do.call(rbind, lapply(persons.list, data.frame, stringsAsFactors = FALSE))
return(persons.ds)
}
)
ВСТУПЛЕНИЕ ВМЕСТЕ
Наконец, позволяет протестировать все решение. Определите строки для разбора, где для второй строки отсутствует адресная информация.
s <- c(
"GREGORY BROWN; 25 NE 25TH; +1-786-987-6543",
"DAVID SMITH;786-123-4567",
"ALAN PEREZ; 25 SE 50TH; +1-786-987-5553"
)
Теперь мы инициализируем context
и анализируем строки:
context <- new("Context")
context <- parseLines(context, s)
Наконец, получите соответствующий набор данных и напечатайте его:
df <- as.df(context)
> df
name address phone
1 GREGORY BROWN 25 NE 25TH +1-786-987-6543
2 DAVID SMITH <NA> 786-123-4567
3 ALAN PEREZ 25 SE 50TH +1-786-987-5553
Давайте проверим теперь методы show
:
> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']
И для некоторого суб-состояния:
>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']
Наконец, проверьте метод as.list()
:
> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"
$address
[1] "25 NE 25TH"
$phone
[1] "+1-786-987-6543"
>
ЗАКЛЮЧЕНИЕ
В этом примере показано, как реализовать шаблон состояния, используя один из доступных механизмов из R для использования парадигмы OO. Тем не менее, решение R OO не является удобным для пользователя и сильно отличается от других языков ООП. Вам нужно переключить свое мышление, потому что синтаксис совершенно другой, он напоминает больше парадигму функционального программирования. Например, вместо: object.setID("A1")
как в Java / C #, для R вы должны вызвать метод таким образом: setID(object, "A1")
. Поэтому вам всегда нужно включить объект в качестве входного аргумента, чтобы обеспечить контекст функции. На том же пути, нет никакого специального this
класс атрибута и либо "."
нотация для доступа к методам или атрибутам данного класса. Это скорее запрос ошибки, потому что для ссылки на класс или методы выполняется через значение атрибута ( "Person"
, "isState"
и т. Д.).
Сказанное выше, решение класса S4, требует гораздо больше строк кода, чем традиционные языки Java / C # для выполнения простых задач. Во всяком случае, государственный шаблон является хорошим и универсальным решением для таких проблем. Это упрощает процесс делегирования логики в конкретное состояние. Вместо того, чтобы иметь большой блок if-else
для управления всеми ситуациями, у нас есть меньшие блоки if-else
внутри каждой реализации подкласса State
для реализации действия для выполнения в каждом состоянии.
Приложение : здесь вы можете скачать весь скрипт.
Любое предложение приветствуется.