R Language
Implementare lo schema della macchina a stati usando la classe S4
Ricerca…
introduzione
Stati finiti I concetti di macchina sono solitamente implementati in linguaggi di programmazione orientata agli oggetti (OOP), ad esempio utilizzando il linguaggio Java, in base al modello di stato definito in GOF (si riferisce al libro: "Design Patterns").
R fornisce diversi meccanismi per simulare il paradigma OO, applichiamo S4 Object System per l'implementazione di questo modello.
Parsing Lines using State Machine
Applichiamo il pattern State Machine per analizzare le linee con il pattern specifico usando la feature Class S4 di R.
PROBLEMA ENUNCIATION
È necessario analizzare un file in cui ogni riga fornisce informazioni su una persona, utilizzando un delimitatore ( ";"
), ma alcune informazioni fornite sono facoltative e, invece di fornire un campo vuoto, non è presente. Su ogni riga possiamo avere le seguenti informazioni: Name;[Address;]Phone
. Dove le informazioni sull'indirizzo sono opzionali, a volte lo abbiamo e altre volte no, ad esempio:
GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553
La seconda riga non fornisce informazioni sull'indirizzo. Pertanto il numero di delimitatori può essere differito come in questo caso con un delimitatore e per le altre due delimitatori. Poiché il numero di delimitatori può variare, un modo per attentare a questo problema è riconoscere la presenza o meno di un dato campo in base al suo modello. In tal caso, possiamo usare un'espressione regolare per identificare tali modelli. Per esempio:
- Nome :
"^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$"
. Ad esempio:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
, ecc. - Indirizzo :
"^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$"
. Ad esempio:11020 LE JEUNE ROAD
,87 SW 27TH
. Per semplicità non includiamo qui il codice postale, la città, lo stato, ma posso essere incluso in questo campo o aggiungere campi aggiuntivi. - Telefono :
"^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$"
. Ad esempio:305-123-4567, 305 123 4567, +1-786-123-4567
.
Note :
- Sto considerando il modello più comune di indirizzi e telefoni degli Stati Uniti, può essere facilmente esteso per prendere in considerazione situazioni più generali.
- In R il segno
"\"
ha un significato speciale per le variabili di carattere, quindi abbiamo bisogno di evitarlo. - Per semplificare il processo di definizione delle espressioni regolari, si consiglia di utilizzare la seguente pagina Web: regex101.com , in modo da poter giocare con esso, con un determinato esempio, fino a ottenere il risultato previsto per tutte le combinazioni possibili.
L'idea è di identificare ciascun campo di linea in base a modelli precedentemente definiti. Il pattern State definisce le seguenti entità (classi) che collaborano per controllare il comportamento specifico (The State Pattern è un modello di comportamento):
Descriviamo ogni elemento considerando il contesto del nostro problema:
-
Context
: memorizza le informazioni di contesto del processo di analisi, ovvero lo stato corrente e gestisce l'intero processo della macchina di stato. Per ogni stato, viene eseguita un'azione (handle()
), ma il contesto lo delega, in base allo stato, sul metodo di azione definito per uno stato particolare (handle()
dalla classeState
). Definisce l'interfaccia di interesse per i clienti. La nostra classeContext
può essere definita in questo modo:- Attributi:
state
- Metodi:
handle()
, ...
- Attributi:
-
State
: la classe astratta che rappresenta uno stato della macchina di stato. Definisce un'interfaccia per incapsulare il comportamento associato a un particolare stato del contesto. Può essere definito in questo modo:- Attributi:
name, pattern
- Metodi:
doAction()
,isState
(usando l'attributopattern
verifica se l'argomento di input appartiene o meno a questo pattern di stato), ...
- Attributi:
-
Concrete States
(sottoclassi di stato): ogni sottoclasse della classeState
che implementa un comportamento associato a uno stato delContext
. I nostri sottoclassi sono:InitState
,NameState
,AddressState
,PhoneState
. Tali classi implementano semplicemente il metodo generico utilizzando la logica specifica per tali stati. Non sono richiesti attributi aggiuntivi.
Nota: è una questione di preferenza come nominare il metodo che esegue l'azione, handle()
, doAction()
o goNext()
. Il nome del metodo doAction()
può essere lo stesso per entrambe le classi ( State
o Context
) che abbiamo preferito nominare come handle()
nella classe Context
per evitare confusione quando si definiscono due metodi generici con gli stessi argomenti di input, ma una classe diversa.
PERSONA CLASSE
Usando la sintassi S4 possiamo definire una classe Person come questa:
setClass(Class = "Person",
slots = c(name = "character", address = "character", phone = "character")
)
È un buon consiglio per inizializzare gli attributi della classe. La documentazione di setClass
suggerisce di utilizzare un metodo generico etichettato come "initialize"
, invece di utilizzare attributi deprecati come: 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
}
)
Poiché il metodo di inizializzazione è già un metodo generico standard dei methods
del pacchetto, è necessario rispettare la definizione dell'argomento originale. Possiamo verificarlo digitando il prompt R:
> initialize
Restituisce l'intera definizione della funzione, puoi vedere in cima a chi la funzione è definita come:
function (.Object, ...) {...}
Pertanto, quando usiamo setMethod
dobbiamo seguire esattamente la stessa sintassi ( .Object
).
Un altro metodo generico esistente è show
, è equivalente al metodo toString()
di Java ed è una buona idea avere un'implementazione specifica per il dominio di classe:
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 : utilizziamo la stessa convenzione nell'implementazione Java toString()
predefinita.
Diciamo che vogliamo salvare le informazioni analizzate (una lista di oggetti Person
) in un set di dati, quindi dovremmo essere in grado prima di convertire una lista di oggetti in qualcosa che la R può trasformare (ad esempio costringere l'oggetto come una lista). Possiamo definire il seguente metodo aggiuntivo (per maggiori dettagli su questo vedi il post )
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 non fornisce una sintassi dello zucchero per OO perché inizialmente il linguaggio era concepito per fornire preziose funzioni agli statistici. Pertanto ciascun metodo utente richiede due parti: 1) la parte Definizione (tramite setGeneric
) e 2) la parte implementazione (tramite setMethod
). Come nell'esempio sopra.
CLASSE DI STATO
Seguendo la sintassi S4, definiamo la classe State
astratta.
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"))
Ogni sottoclasse di State
avrà associato un name
e un pattern
, ma anche un modo per identificare se un dato input appartiene o meno a questo stato isState()
metodo isState()
) e implementare anche le azioni corrispondenti per questo stato ( doAction()
metodo).
Per comprendere il processo, definiamo la matrice di transizione per ogni stato in base all'input ricevuto:
Input / Current State | Dentro | Nome | Indirizzo | Telefono |
---|---|---|---|---|
Nome | Nome | |||
Indirizzo | Indirizzo | |||
Telefono | Telefono | Telefono | ||
Fine | Fine |
Nota: la cella [row, col]=[i,j]
rappresenta lo stato di destinazione per lo stato corrente j
, quando riceve l'input i
.
Significa che sotto lo stato Nome può ricevere due ingressi: un indirizzo o un numero di telefono. Un altro modo per rappresentare la tabella delle transazioni è l'utilizzo del seguente diagramma della macchina dello stato UML :
Diamo implementare ogni particolare stato come un sub-stato della classe State
Sottoclassi dello Stato
Stato iniziale :
Lo stato iniziale verrà implementato tramite la seguente classe:
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()
}
)
In R per indicare una classe è una sottoclasse di un'altra classe sta usando l'attributo contains
e indica il nome della classe della classe genitore.
Poiché le sottoclassi implementano solo i metodi generici, senza aggiungere ulteriori attributi, quindi il metodo show
, basta chiamare il metodo equivalente dalla classe superiore (tramite metodo: callNextMethod()
)
Lo stato iniziale non ha associato uno schema, rappresenta solo l'inizio del processo, quindi inizializziamo la classe con un valore NA
.
Ora consente di implementare i metodi generici dalla classe State
:
setMethod(f = "isState", signature = "InitState",
definition = function(obj, input) {
nameState <- new("NameState")
result <- isState(nameState, input)
return(result)
}
)
Per questo particolare stato (senza pattern
), l'idea che inizializza il processo di analisi che prevede il primo campo sarà un name
, altrimenti sarà un errore.
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)
}
)
Il metodo doAction
fornisce la transizione e aggiorna il contesto con le informazioni estratte. Qui stiamo accedendo alle informazioni di contesto tramite @-operator
. Invece, possiamo definire i metodi get/set
, per incapsulare questo processo (come è richiesto nelle migliori pratiche OO: incapsulamento), ma aggiungerebbe altri quattro metodi per get-set
senza aggiungere valore per lo scopo di questo esempio.
È una buona raccomandazione in tutte le implementazioni doAction
, per aggiungere una salvaguardia quando l'argomento di input non è correttamente identificato.
Nome Stato
Ecco la definizione di questa definizione di classe:
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()
}
)
Usiamo la funzione grepl
per verificare che l'input appartenga a un dato pattern.
setMethod(f="isState", signature="NameState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
Ora definiamo l'azione da eseguire per un determinato stato:
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)
}
)
Qui consideriamo le possibili transizioni: una per lo stato degli indirizzi e l'altra per lo stato del telefono. In tutti i casi aggiorniamo le informazioni di contesto:
- Le informazioni sulla
person
:address
ophone
con l'argomento di input. - Lo
state
del processo
Il modo per identificare lo stato è di invocare il metodo: isState()
per un particolare stato. Creiamo uno stato specifico predefinito ( addressState, phoneState
) e poi chiediamo una convalida particolare.
La logica per l'implementazione delle altre sottoclassi (uno per stato) è molto simile.
Indirizzo Stato
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)
}
)
Stato del telefono
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)
}
)
Qui è dove aggiungiamo le informazioni sulla persona nella lista delle persons
del context
.
setMethod(f = "doAction", "PhoneState",
definition = function(obj, input, context) {
context <- addPerson(context, context@person)
context@state <- new("InitState")
return(context)
}
)
CLASSE CONTEMPORANEA
Ora è possibile illustrare l'implementazione della classe Context
. Possiamo definirlo considerando i seguenti attributi:
setClass(Class = "Context",
slots = c(state = "State", persons = "list", person = "Person")
)
Dove
-
state
: lo stato attuale del processo -
person
: la persona attuale, rappresenta le informazioni che abbiamo già analizzato dalla riga corrente. -
persons
: l'elenco delle persone analizzate elaborate.
Nota : facoltativamente, possiamo aggiungere un name
per identificare il contesto per nome nel caso in cui stiamo lavorando con più di un tipo di parser.
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 tali metodi generici, controlliamo l'intero comportamento del processo di analisi:
-
handle()
: invoca il particolare metododoAction()
dellostate
corrente. -
addPerson
: Una volta raggiunto lo stato finale, dobbiamo aggiungere unaperson
all'elenco dellepersons
che abbiamo analizzato. -
parseLine()
:parseLine()
una singola riga -
parseLines()
:parseLines()
più righe (una serie di linee) -
as.df()
:as.df()
le informazioni dalla listapersons
in un oggetto frame dati.
Andiamo avanti ora con le corrispondenti implementazioni:
metodo handle()
, delega sul metodo doAction()
dallo state
corrente 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)
}
)
Per prima cosa, dividiamo la linea originale in una matrice usando il delimitatore per identificare ogni elemento tramite la funzione R strsplit()
, quindi iteriamo per ciascun elemento come valore di input per un determinato stato. Il metodo handle()
restituisce nuovamente il context
con l'informazione aggiornata ( state
, person
, attributo 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 fa una copia dell'argomento di input, dobbiamo restituire il contesto ( 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)
}
)
L'attributo persons
è una lista di istanze della classe S4 Person
. Questo qualcosa non può essere forzato a nessun tipo standard perché R non sa di trattare un'istanza di una classe definita dall'utente. La soluzione è convertire una Person
in una lista, usando il metodo as.list
precedentemente definito. Quindi possiamo applicare questa funzione a ciascun elemento della lista persons
, tramite la funzione lapply()
. Quindi nella prossima lappy()
alla funzione lappy()
, ora si applica la funzione data.frame
per convertire ciascun elemento di persons.list
in un frame di dati. Infine, la funzione rbind()
viene chiamata per aggiungere ogni elemento convertito come una nuova riga del frame di dati generato (per maggiori dettagli su questo vedi questo post )
# 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)
}
)
METTENDO TUTTI INSIEME
Infine, consente di testare l'intera soluzione. Definire le linee per analizzare dove per la seconda riga mancano le informazioni sull'indirizzo.
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"
)
Ora inizializziamo il context
e analizziamo le linee:
context <- new("Context")
context <- parseLines(context, s)
Finalmente ottieni il set di dati corrispondente e stampalo:
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
Proviamo ora i metodi dello show
:
> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']
E per alcuni sotto-stati:
>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']
Infine, prova il metodo as.list()
:
> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"
$address
[1] "25 NE 25TH"
$phone
[1] "+1-786-987-6543"
>
CONCLUSIONE
Questo esempio mostra come implementare il pattern State, usando uno dei meccanismi disponibili da R per usare il paradigma OO. Tuttavia, la soluzione R OO non è user-friendly e differisce molto dalle altre lingue OOP. Hai bisogno di cambiare mentalità perché la sintassi è completamente diversa, ricorda più il paradigma della programmazione funzionale. Ad esempio invece di: object.setID("A1")
come in Java / C #, per R devi invocare il metodo in questo modo: setID(object, "A1")
. Pertanto è sempre necessario includere l'oggetto come argomento di input per fornire il contesto della funzione. Allo stesso modo, non esiste un attributo speciale di this
classe e un "."
notazione per accedere a metodi o attributi della classe data. È più una richiesta di errore perché per riferire una classe o metodi viene fatto tramite il valore dell'attributo ( "Person"
, "isState"
, ecc.).
Detto quanto sopra, la soluzione di classe S4, richiede molte più linee di codici rispetto ai tradizionali linguaggi Java / C # per svolgere semplici compiti. Ad ogni modo, lo State Pattern è una soluzione buona e generica per questo tipo di problemi. Semplifica il processo delegando la logica in uno stato particolare. Invece di avere un grosso blocco if-else
per il controllo di tutte le situazioni, all'interno di ogni sottoclasse State
blocchi if-else
più piccoli per implementare l'azione da eseguire in ogni stato.
Allegato : qui puoi scaricare l'intero script.
Qualsiasi suggerimento è benvenuto.