R Language
Implementar patrón de máquina de estado usando la clase S4
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):
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()
deState
claseState
). Define la interfaz de interés para los clientes. Nuestra clase deContext
se puede definir así:- Atributos:
state
- Métodos:
handle()
, ...
- Atributos:
-
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 depattern
verifique si el argumento de entrada pertenece o no a este patrón de estado), ...
- Atributos:
-
Concrete States
(subclases de estado): cada subclase de la claseState
que implementa un comportamiento asociado con un estado delContext
. 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 :
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
ophone
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á eldoAction()
particulardoAction()
delstate
actual. -
addPerson
: Una vez que alcancemos el estado final, necesitamos agregar unaperson
a la lista depersons
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 depersons
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.