R Language
Implémenter un modèle de machine d'état à l'aide de la classe S4
Recherche…
Introduction
États finis Les concepts de machine sont généralement implémentés sous les langages de programmation orientée objet (OOP), par exemple en langage Java, en fonction du modèle d'état défini dans GOF (fait référence au livre: "Design Patterns").
R fournit plusieurs mécanismes pour simuler le paradigme OO, appliquons S4 Object System pour implémenter ce modèle.
Lignes d'analyse utilisant State Machine
Appliquons le modèle Machine d'état pour analyser les lignes avec le modèle spécifique à l'aide de la fonctionnalité Classe S4 de R.
ENUNCIATION DE PROBLÈME
Nous devons analyser un fichier où chaque ligne fournit des informations sur une personne, en utilisant un délimiteur ( ";"
), mais certaines informations fournies sont facultatives et, au lieu de fournir un champ vide, elles sont manquantes. Sur chaque ligne, nous pouvons avoir les informations suivantes: Name;[Address;]Phone
. Lorsque les informations d'adresse sont facultatives, nous les avons parfois et parfois, par exemple:
GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553
La deuxième ligne ne fournit pas d'informations sur l'adresse. Par conséquent, le nombre de délimiteurs peut différer comme dans le cas présent avec un délimiteur et pour les autres lignes deux délimiteurs. Comme le nombre de délimiteurs peut varier, une façon de résoudre ce problème consiste à reconnaître la présence ou non d’un champ donné en fonction de son modèle. Dans ce cas, nous pouvons utiliser une expression régulière pour identifier de tels motifs. Par exemple:
- Nom :
"^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$"
. Par exemple:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
, etc. - Adresse :
"^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$"
. Par exemple:11020 LE JEUNE ROAD
,87 SW 27TH
. Par souci de simplicité, nous n'incluons pas ici le code postal, la ville, l'état, mais je peux être inclus dans ce champ ou ajouter des champs supplémentaires. - Téléphone :
"^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$"
. Par exemple:305-123-4567, 305 123 4567, +1-786-123-4567
.
Notes :
- Je considère le modèle le plus courant d'adresses et de téléphones américains, il peut être facilement étendu pour considérer des situations plus générales.
- Dans R, le signe
"\"
a une signification particulière pour les variables de caractères, nous devons donc y échapper. - Afin de simplifier le processus de définition des expressions régulières, il est recommandé d'utiliser la page Web suivante: regex101.com , afin de pouvoir jouer avec un exemple donné, jusqu'à obtenir le résultat attendu pour toutes les combinaisons possibles.
L'idée est d'identifier chaque champ de ligne en fonction de modèles préalablement définis. Le modèle d'état définit les entités (classes) suivantes qui collaborent pour contrôler le comportement spécifique (le modèle d'état est un modèle de comportement):
Décrivons chaque élément en tenant compte du contexte de notre problème:
-
Context
: Stocke les informations de contexte du processus d'analyse, c'est-à-dire l'état actuel et gère l'intégralité du processus de la machine d'état. Pour chaque état, une action est exécutée (handle()
), mais le contexte le délègue, en fonction de l'état, à la méthode d'action définie pour un état particulier (handle()
deState
classeState
). Il définit l'interface d'intérêt pour les clients. Notre classe deContext
peut être définie comme ceci:- Attributs:
state
- Méthodes:
handle()
, ...
- Attributs:
-
State
: classe abstraite qui représente n'importe quel état de la machine d'état. Il définit une interface pour encapsuler le comportement associé à un état particulier du contexte. Il peut être défini comme ceci:- Attributs:
name, pattern
- Méthodes:
doAction()
,isState
(à l'aidepattern
attributpattern
vérifier si l'argument d'entrée appartient ou non à ce modèle d'état),…
- Attributs:
-
Concrete States
(sous-classes d'état): chaque sous-classe de l'State
classe qui implémente un comportement associé à un état duContext
. Nos sous-classes sont:InitState
,NameState
,AddressState
,PhoneState
. De telles classes implémentent simplement la méthode générique en utilisant la logique spécifique de ces états. Aucun attribut supplémentaire n'est requis.
Remarque: Il est préférable de nommer la méthode qui exécute l'action, handle()
, doAction()
ou goNext()
. Le nom de la méthode doAction()
peut être le même pour les deux classes ( State
ou Context
). Nous avons préféré nommer handle()
dans la classe Context
pour éviter toute confusion lors de la définition de deux méthodes génériques avec les mêmes arguments
Classe de personne
En utilisant la syntaxe S4, nous pouvons définir une classe de personne comme ceci:
setClass(Class = "Person",
slots = c(name = "character", address = "character", phone = "character")
)
Il est recommandé d’initialiser les attributs de classe. La documentation de setClass
suggère d'utiliser une méthode générique appelée "initialize"
, au lieu d'utiliser des attributs obsolètes tels que: 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
}
)
Parce que la méthode initialize est déjà une méthode générique standard paquet methods
, nous devons respecter la définition de l' argument d' origine. Nous pouvons le vérifier en tapant sur l'invite R:
> initialize
Il retourne la définition de la fonction entière, vous pouvez voir en haut qui la fonction est définie comme:
function (.Object, ...) {...}
Par conséquent , lorsque nous utilisons setMethod
nous devons suivre exaclty la même syntaxe ( .Object
).
Une autre méthode générique existante est show
, elle est équivalente à la toString()
de Java et c'est une bonne idée d'avoir une implémentation spécifique pour le domaine de 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)
}
)
Note : Nous utilisons la même convention que dans l'implémentation par défaut de toString()
Java.
Supposons que nous voulions enregistrer les informations analysées (une liste d'objets Person
) dans un ensemble de données, puis nous devrions d'abord pouvoir convertir une liste d'objets en quelque chose que le R peut transformer (par exemple, contraindre l'objet en tant que liste). Nous pouvons définir la méthode supplémentaire suivante (pour plus de détails à ce sujet, voir l' article )
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 ne fournit pas de syntaxe de sucre pour OO car le langage a été initialement conçu pour fournir des fonctions précieuses aux statisticiens. Par conséquent, chaque méthode utilisateur nécessite deux parties: 1) la partie Définition (via setGeneric
) et 2) la partie implémentation (via setMethod
). Comme dans l'exemple ci-dessus.
CLASSE D'ÉTAT
Suivant la syntaxe S4, définissons la classe d' State
abstraite.
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"))
Chaque sous-classe de State
aura associé un name
et un pattern
, mais aussi un moyen d'identifier si une entrée donnée appartient à cet état ou non (méthode isState()
), et implémente également les actions correspondantes pour cet état ( doAction()
méthode).
Pour comprendre le processus, définissons la matrice de transition pour chaque état en fonction des entrées reçues:
État d'entrée / actuel | Init | prénom | Adresse | Téléphone |
---|---|---|---|---|
prénom | prénom | |||
Adresse | Adresse | |||
Téléphone | Téléphone | Téléphone | ||
Fin | Fin |
Remarque: La cellule [row, col]=[i,j]
représente l'état de destination pour l'état actuel j
, lorsqu'elle reçoit l'entrée i
.
Cela signifie que sous Nom, il peut recevoir deux entrées: une adresse ou un numéro de téléphone. Une autre manière de représenter la table de transaction consiste à utiliser le diagramme de machine d'état UML suivant:
Implémentons chaque état particulier comme un sous-état de la classe State
SOUS-CLASSES D'ÉTAT
Etat d'initialisation :
L'état initial sera implémenté via la classe suivante:
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()
}
)
Dans R, pour indiquer qu'une classe est une sous-classe d'une autre classe, utilisez l'attribut contains
et indiquez le nom de la classe parente.
Comme les sous-classes implémentent simplement les méthodes génériques, sans ajouter d'attributs supplémentaires, alors la méthode show
appelle simplement la méthode équivalente de la classe supérieure (via la méthode: callNextMethod()
)
L'état initial n'a pas de motif associé, il représente simplement le début du processus, puis nous initialisons la classe avec une valeur NA
.
Maintenant, permet d'implémenter les méthodes génériques de la classe State
:
setMethod(f = "isState", signature = "InitState",
definition = function(obj, input) {
nameState <- new("NameState")
result <- isState(nameState, input)
return(result)
}
)
Pour cet état particulier (sans pattern
), l'idée d'initialiser le processus d'analyse en attendant le premier champ sera un name
, sinon ce sera une erreur.
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)
}
)
La méthode doAction
fournit la transition et met à jour le contexte avec les informations extraites. Ici, nous accédons aux informations de contexte via l' @-operator
. Au lieu de cela, nous pouvons définir des méthodes get/set
pour encapsuler ce processus (comme c'est le cas dans les meilleures pratiques OO: encapsulation), mais cela ajouterait quatre méthodes supplémentaires par get-set
sans ajouter de valeur pour cet exemple.
C'est une bonne recommandation dans toutes les implémentations de doAction
, d'ajouter une sauvegarde lorsque l'argument d'entrée n'est pas correctement identifié.
Nom de l'Etat
Voici la définition de cette définition de 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()
}
)
Nous utilisons la fonction grepl
pour vérifier que l'entrée appartient à un modèle donné.
setMethod(f="isState", signature="NameState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
Maintenant, nous définissons l’action à mener pour un état donné:
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)
}
)
Nous considérons ici les transitions possibles: une pour l'état de l'adresse et l'autre pour l'état du téléphone. Dans tous les cas, nous mettons à jour les informations de contexte:
- Les informations sur la
person
:address
ouphone
avec l'argument de saisie. - L'
state
du processus
La façon d'identifier l'état consiste à appeler la méthode: isState()
pour un état particulier. Nous créons des états spécifiques par défaut ( addressState, phoneState
) et demandons ensuite une validation particulière.
La logique de l'implémentation des autres sous-classes (un par état) est très similaire.
Etat de l'adresse
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)
}
)
Etat du téléphone
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)
}
)
Voici où nous ajoutons les informations sur la personne dans la liste des persons
du context
.
setMethod(f = "doAction", "PhoneState",
definition = function(obj, input, context) {
context <- addPerson(context, context@person)
context@state <- new("InitState")
return(context)
}
)
CLASSE DE CONTEXTE
Maintenant, permet d'expliquer l'implémentation de la classe Context
. Nous pouvons le définir en tenant compte des attributs suivants:
setClass(Class = "Context",
slots = c(state = "State", persons = "list", person = "Person")
)
Où
-
state
: l'état actuel du processus -
person
: La personne actuelle, elle représente les informations que nous avons déjà analysées à partir de la ligne actuelle. -
persons
: la liste des personnes analysées traitées.
Remarque : facultativement, nous pouvons ajouter un name
pour identifier le contexte par son nom si nous travaillons avec plus d’un type d’analyseur.
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"))
Avec de telles méthodes génériques, nous contrôlons l'intégralité du comportement du processus d'analyse:
-
handle()
: invoquera la méthodedoAction()
particulière de l'state
actuel. -
addPerson
: Une fois l'état final atteint, nous devons ajouter uneperson
à la liste despersons
analysées. -
parseLine()
: analyse une seule ligne -
parseLines()
: analyse plusieurs lignes (un tableau de lignes) -
as.df()
: extraire les informations de la liste despersons
dans un objet deas.df()
données.
Allons maintenant avec les implémentations correspondantes:
Méthode handle()
, délègue la méthode doAction()
partir de l' state
actuel du 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)
}
)
Premièrement, nous divisons la ligne d'origine dans un tableau en utilisant le délimiteur pour identifier chaque élément via la fonction R- strsplit()
, puis nous itérons pour chaque élément en tant que valeur d'entrée pour un état donné. La handle()
méthode retourne à nouveau le context
de l'information mise à jour ( state
, person
, persons
attribuent).
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)
}
)
Parce que R fait une copie de l'argument d'entrée, il faut retourner le contexte ( 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'attribut persons
est une liste d'instance de classe S4 Person
. Ce quelque chose ne peut être contraint à aucun type standard car R ne sait pas pour traiter une instance d'une classe définie par l'utilisateur. La solution consiste à convertir une Person
en une liste en utilisant la méthode as.list
précédemment définie. Ensuite, nous pouvons appliquer cette fonction à chaque élément de la liste des persons
, via la fonction lapply()
. Ensuite, dans la fonction suivante d'invocation à lappy()
, applique maintenant la fonction data.frame
pour convertir chaque élément de persons.list
en un data.frame
données. Enfin, la fonction rbind()
est appelée pour ajouter chaque élément converti en nouvelle ligne du rbind()
de données généré (pour plus de détails à ce sujet, voir cet article )
# 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)
}
)
TOUT ENSEMBLE
Enfin, permet de tester la solution complète. Définir les lignes à analyser où pour la deuxième ligne les informations d'adresse sont manquantes.
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"
)
Maintenant, nous initialisons le context
et analysons les lignes:
context <- new("Context")
context <- parseLines(context, s)
Enfin, obtenez le jeu de données correspondant et imprimez-le:
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
Testons maintenant les méthodes show
:
> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']
Et pour certains sous-états:
>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']
Enfin, testez la méthode as.list()
:
> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"
$address
[1] "25 NE 25TH"
$phone
[1] "+1-786-987-6543"
>
CONCLUSION
Cet exemple montre comment implémenter le modèle d'état, en utilisant l'un des mécanismes disponibles de R pour utiliser le paradigme OO. Néanmoins, la solution R OO n’est pas conviviale et diffère beaucoup des autres langages OOP. Vous devez changer votre mentalité car la syntaxe est complètement différente, cela rappelle davantage le paradigme de la programmation fonctionnelle. Par exemple au lieu de: object.setID("A1")
comme dans Java / C #, pour R vous devez appeler la méthode de cette manière: setID(object, "A1")
. Par conséquent, vous devez toujours inclure l'objet en tant qu'argument d'entrée pour fournir le contexte de la fonction. De la même façon, il n'y a pas spécial this
attribut de classe et soit un "."
notation pour accéder aux méthodes ou aux attributs de la classe donnée. C'est plus un message d'erreur car faire référence à une classe ou à des méthodes se fait via la valeur de l'attribut ( "Person"
, "isState"
, etc.).
Ce qui précède, la solution de classe S4, nécessite beaucoup plus de lignes de codes que les langages Java / C # traditionnels pour effectuer des tâches simples. Quoi qu’il en soit, le State Pattern est une bonne solution générique pour ce type de problèmes. Cela simplifie le processus de délégation de la logique dans un état particulier. Au lieu d'avoir un gros bloc if-else
pour contrôler toutes les situations, nous avons à l'intérieur de chaque sous-classe State
blocs if-else
plus petits pour mettre en œuvre l'action à effectuer dans chaque état.
Pièce jointe : Ici, vous pouvez télécharger le script entier.
Toute suggestion est la bienvenue.