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

Motif d'état GOF

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() de State classe State ). Il définit l'interface d'intérêt pour les clients. Notre classe de Context peut être définie comme ceci:
    • Attributs: state
    • Méthodes: handle() , ...
  • 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'aide pattern attribut pattern vérifier si l'argument d'entrée appartient ou non à ce modèle d'état),…
  • Concrete States (sous-classes d'état): chaque sous-classe de l' State classe qui implémente un comportement associé à un état du Context . 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:

Représentation du diagramme de machine d'état

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

  • 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éthode doAction() particulière de l' state actuel.
  • addPerson : Une fois l'état final atteint, nous devons ajouter une person à la liste des persons analysées.
  • parseLine() : analyse une seule ligne
  • parseLines() : analyse plusieurs lignes (un tableau de lignes)
  • as.df() : extraire les informations de la liste des persons dans un objet de as.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.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow