Buscar..


Introducción

Los conceptos de máquina de estados finitos generalmente se implementan en lenguajes de programación orientada a objetos (OOP), por ejemplo, utilizando el lenguaje Java, según el patrón de estado definido en GOF (se refiere al libro: "Patrones de diseño").

R proporciona varios mecanismos para simular el paradigma OO, apliquemos S4 Object System para implementar este patrón.

Analizando líneas usando State Machine

Apliquemos el patrón de la máquina de estados para analizar líneas con el patrón específico usando la función de clase S4 de R.

PROBLEMA ENUNCIACIÓN

Necesitamos analizar un archivo donde cada línea proporciona información sobre una persona, usando un delimitador ( ";" ), pero parte de la información proporcionada es opcional y, en lugar de proporcionar un campo vacío, falta. En cada línea podemos tener la siguiente información: Name;[Address;]Phone . Cuando la información de la dirección es opcional, a veces la tenemos y otras no, por ejemplo:

GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553

La segunda línea no proporciona información de dirección. Por lo tanto, el número de delimitadores puede ser diferente como en este caso con un delimitador y para las otras líneas dos delimitadores. Debido a que el número de delimitadores puede variar, una forma de atacar este problema es reconocer la presencia o no de un campo determinado en función de su patrón. En tal caso, podemos usar una expresión regular para identificar dichos patrones. Por ejemplo:

  • Nombre : "^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$" . Por ejemplo: RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA , etc.
  • Dirección : "^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$" . Por ejemplo: 11020 LE JEUNE ROAD , 87 SW 27TH . Para simplificar, no incluimos aquí el código postal, ciudad, estado, pero puedo incluirme en este campo o agregar campos adicionales.
  • Teléfono : "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$" . Por ejemplo: 305-123-4567, 305 123 4567, +1-786-123-4567 .

Notas :

  • Estoy considerando el patrón más común de direcciones y teléfonos en los EE. UU., Se puede extender fácilmente para considerar situaciones más generales.
  • En R, el signo "\" tiene un significado especial para las variables de caracteres, por lo tanto necesitamos escapar de él.
  • Para simplificar el proceso de definir expresiones regulares, una buena recomendación es usar la siguiente página web: regex101.com , para que pueda jugar con ella, con un ejemplo dado, hasta que obtenga el resultado esperado para todas las combinaciones posibles.

La idea es identificar cada campo de línea en base a patrones definidos previamente. El patrón de estado define las siguientes entidades (clases) que colaboran para controlar el comportamiento específico (el patrón de estado es un patrón de comportamiento):

Patrón de estado GOF

Describamos cada elemento considerando el contexto de nuestro problema:

  • Context : Almacena la información de contexto del proceso de análisis, es decir, el estado actual y maneja todo el Proceso de Máquina de Estado. Para cada estado, se ejecuta una acción ( handle() ), pero el contexto la delega, en función del estado, en el método de acción definido para un estado particular ( handle() de State clase State ). Define la interfaz de interés para los clientes. Nuestra clase de Context se puede definir así:
    • Atributos: state
    • Métodos: handle() , ...
  • State : la clase abstracta que representa cualquier estado de la Máquina del Estado. Define una interfaz para encapsular el comportamiento asociado con un estado particular del contexto. Se puede definir así:
    • Atributos: name, pattern
    • Métodos: doAction() , isState (usando el atributo de pattern verifique si el argumento de entrada pertenece o no a este patrón de estado), ...
  • Concrete States (subclases de estado): cada subclase de la clase State que implementa un comportamiento asociado con un estado del Context . Nuestros sub-clases son: InitState , NameState , AddressState , PhoneState . Tales clases simplemente implementan el método genérico utilizando la lógica específica para tales estados. No se requieren atributos adicionales.

Nota: es una cuestión de preferencia cómo nombrar el método que lleva a cabo la acción, handle() , doAction() o goNext() . El nombre del método doAction() puede ser el mismo para ambas clases ( State o Context ) que preferimos nombrar como handle() en la clase Context para evitar confusiones al definir dos métodos genéricos con los mismos argumentos de entrada, pero una clase diferente.

Clase de persona

Usando la sintaxis de S4 podemos definir una clase de persona como esta:

setClass(Class = "Person",
    slots = c(name = "character", address = "character", phone = "character")
)

Es una buena recomendación para inicializar los atributos de la clase. La documentación de setClass sugiere usar un método genérico etiquetado como "initialize" , en lugar de usar atributos obsoletos como: 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
    }
)

Debido a que el método initialize ya es un método genérico estándar del paquete methods , hay que respetar la definición argumento original. Podemos verificarlo escribiendo en el indicador de R:

> initialize

Devuelve la definición de la función completa, puede ver en la parte superior la función definida como:

function (.Object, ...) {...}

Por lo tanto, cuando usamos setMethod debemos seguir exactamente la misma sintaxis ( .Object ).

Otro método genérico existente es show , es equivalente al método toString() de Java y es una buena idea tener una implementación específica para el dominio de clase:

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)
  }
)

Nota : usamos la misma convención que en la implementación Java de toString() predeterminada.

Digamos que queremos guardar la información analizada (una lista de objetos Person ) en un conjunto de datos, luego deberíamos poder convertir primero una lista de objetos en algo que R pueda transformar (por ejemplo, forzar el objeto como una lista). Podemos definir el siguiente método adicional (para más detalles sobre esto, vea la publicación )

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 no proporciona una sintaxis de azúcar para OO porque el lenguaje se concibió inicialmente para proporcionar funciones valiosas para los estadísticos. Por lo tanto, cada método de usuario requiere dos partes: 1) la parte de Definición (a través de setGeneric ) y 2) la parte de implementación (a través de setMethod ). Como en el ejemplo anterior.

Clase de estado

Siguiendo la sintaxis de S4, definamos la clase State abstracta.

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"))

Cada subclase del State tendrá asociado un name y un pattern , pero también una manera de identificar si una entrada dada pertenece o no a este estado isState() método de estado de estado) y también implementará las acciones correspondientes para este estado ( doAction() método).

Para comprender el proceso, definamos la matriz de transición para cada estado en función de la entrada recibida:

Entrada / estado actual En eso Nombre Dirección Teléfono
Nombre Nombre
Dirección Dirección
Teléfono Teléfono Teléfono
Fin Fin

Nota: la celda [row, col]=[i,j] representa el estado de destino para el estado actual j , cuando recibe la entrada i .

Significa que bajo el nombre del estado puede recibir dos entradas: una dirección o un número de teléfono. Otra forma de representar la tabla de transacciones es mediante el siguiente diagrama de máquina de estado UML :

Representación del diagrama de la máquina de estado

Implementemos cada estado particular como un subestado de la clase State

Subclases del estado

Estado de inicio :

El estado inicial se implementará a través de la siguiente clase:

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()
  }
)

En R para indicar que una clase es una subclase de otra clase, está usando el atributo que contains e indica el nombre de la clase de la clase principal.

Debido a que las subclases solo implementan los métodos genéricos, sin agregar atributos adicionales, el método show , simplemente llama al método equivalente de la clase superior (a través del método: callNextMethod() )

El estado inicial no tiene asociado un patrón, solo representa el comienzo del proceso, luego inicializamos la clase con un valor de NA .

Ahora vamos a implementar los métodos genéricos de la clase State :

setMethod(f = "isState", signature = "InitState",
  definition = function(obj, input) {
      nameState <- new("NameState")
      result <- isState(nameState, input)
      return(result)
  }
)

Para este estado en particular (sin pattern ), la idea que simplemente inicializa el proceso de análisis esperando que el primer campo sea un name , de lo contrario será un error.

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)
    }
)

El método doAction proporciona la transición y actualiza el contexto con la información extraída. Aquí estamos accediendo a la información de contexto a través del @-operator . En su lugar, podemos definir los métodos de get/set , para encapsular este proceso (como se exige en las mejores prácticas de OO: encapsulación), pero eso agregaría cuatro métodos más por get-set sin agregar valor para el propósito de este ejemplo.

Es una buena recomendación en toda la implementación de doAction , agregar una salvaguarda cuando el argumento de entrada no se identifica correctamente.

Nombre estado

Aquí está la definición de esta definición de clase:

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()
  }
)

Usamos la función grepl para verificar que la entrada pertenece a un patrón dado.

setMethod(f="isState", signature="NameState",
  definition=function(obj, input) {
      result <- grepl(obj@pattern, input, perl=TRUE)
      return(result)
  }
)

Ahora definimos la acción a realizar para un estado dado:

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)
  }
)

Aquí consideramos posibles transiciones: una para el estado de la dirección y otra para el estado del teléfono. En todos los casos actualizamos la información de contexto:

  • La información de la person : address o phone con el argumento de entrada.
  • El state del proceso.

La forma de identificar el estado es invocar el método: isState() para un estado particular. Creamos un estado específico predeterminado ( addressState, phoneState ) y luego pedimos una validación particular.

La lógica para la implementación de otras subclases (una por estado) es muy similar.

Dirección Estado

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)
    }
)

Estado del teléfono

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)
    }
)

Aquí es donde agregamos la información de la persona a la lista de persons del context .

setMethod(f = "doAction", "PhoneState",
    definition = function(obj, input, context) {
        context <- addPerson(context, context@person)
        context@state <- new("InitState")
        return(context)
    }   
)

Clase de contexto

Ahora permite explicar la implementación de la clase de Context . Podemos definirlo considerando los siguientes atributos:

setClass(Class = "Context",
     slots = c(state = "State", persons = "list", person = "Person")
)

Dónde

  • state : el estado actual del proceso
  • person : la persona actual, representa la información que ya hemos analizado de la línea actual.
  • persons : La lista de personas analizadas procesadas.

Nota : Opcionalmente, podemos agregar un name para identificar el contexto por nombre en caso de que estemos trabajando con más de un tipo de analizador.

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"))

Con tales métodos genéricos, controlamos todo el comportamiento del proceso de análisis:

  • handle() : invocará el doAction() particular doAction() del state actual.
  • addPerson : Una vez que alcancemos el estado final, necesitamos agregar una person a la lista de persons que hemos analizado.
  • parseLine() : analizar una sola línea
  • parseLines() : analizar múltiples líneas (una matriz de líneas)
  • as.df() : Extraiga la información de la lista de persons en un objeto de marco de datos.

Continuemos ahora con las implementaciones correspondientes:

handle() método handle() , delega el método doAction() del state actual del 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)
  }
)

Primero, dividimos la línea original en una matriz utilizando el delimitador para identificar cada elemento a través de la función R strsplit() , luego iteramos para cada elemento como un valor de entrada para un estado dado. El método handle() devuelve nuevamente el context con la información actualizada ( state , person , atributo de 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 hace una copia del argumento de entrada, necesitamos devolver el contexto ( 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)
  }
)

El atributo persons es una lista de instancias de la clase S4 Person . Este algo no se puede coaccionar a ningún tipo estándar porque R no sabe cómo tratar una instancia de una clase definida por el usuario. La solución es convertir una Person en una lista, utilizando el método as.list previamente definido. Luego podemos aplicar esta función a cada elemento de la lista de persons , a través de la función lapply() . Luego, en la siguiente invocación a la función lappy() , ahora aplica la función data.frame para convertir cada elemento de la lista persons.list en un marco de datos. Finalmente, se llama a la función rbind() para agregar cada elemento convertido como una nueva fila del marco de datos generado (para más detalles sobre esto, vea esta publicación )

# 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)
  }
)

PONIENDO TODOS JUNTOS

Finalmente, permite probar toda la solución. Defina las líneas a analizar donde falta la información de la dirección para la segunda línea.

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"
)

Ahora inicializamos el context , y analizamos las líneas:

context <- new("Context")
context <- parseLines(context, s)

Finalmente obtenga el conjunto de datos correspondiente e imprímalo:

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

Probemos ahora los métodos de show :

> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']

Y para algunos subestados:

>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']

Finalmente, prueba el método as.list() :

> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"

$address
[1] "25 NE 25TH"

$phone
[1] "+1-786-987-6543"

> 

CONCLUSIÓN

Este ejemplo muestra cómo implementar el patrón de estado, utilizando uno de los mecanismos disponibles de R para usar el paradigma OO. Sin embargo, la solución R OO no es fácil de usar y difiere mucho de otros idiomas OOP. Debe cambiar su forma de pensar porque la sintaxis es completamente diferente, recuerda más el paradigma de la programación funcional. Por ejemplo, en lugar de: object.setID("A1") como en Java / C #, para R tienes que invocar el método de esta manera: setID(object, "A1") . Por lo tanto, siempre debe incluir el objeto como un argumento de entrada para proporcionar el contexto de la función. De la misma manera, no hay ningún atributo especial de this clase y tampoco un "." Notación para acceder a los métodos o atributos de la clase dada. Es un mensaje de error más porque la referencia a una clase o métodos se realiza a través del valor del atributo ( "Person" , "isState" , etc.).

Dicho lo anterior, la solución de clase S4, requiere muchas más líneas de códigos que los lenguajes tradicionales Java / C # para realizar tareas simples. De todos modos, el patrón estatal es una solución buena y genérica para este tipo de problemas. Simplifica el proceso delegando la lógica en un estado particular. En lugar de tener un gran bloque if-else para controlar todas las situaciones, tenemos bloques if-else pequeños if-else dentro de cada implementación de subclase State para implementar la acción que se debe llevar a cabo en cada estado.

Adjunto : Aquí puedes descargar el script completo.

Cualquier sugerencia es bienvenida.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow