Поиск…


Вступление

Конечные состояния. Концепции машин обычно реализуются в рамках языков ООП, например, с использованием языка 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 для реализации действия для выполнения в каждом состоянии.

Приложение : здесь вы можете скачать весь скрипт.

Любое предложение приветствуется.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow