Zoeken…


Invoering

Eindige toestanden Machineconcepten worden meestal geïmplementeerd onder OOP-talen (Object Oriented Programming), bijvoorbeeld met behulp van de Java-taal, gebaseerd op het statuspatroon dat is gedefinieerd in GOF (verwijst naar het boek: "Design Patterns").

R biedt verschillende mechanismen om het OO-paradigma te simuleren, laten we S4 Object System toepassen voor de implementatie van dit patroon.

Lijnen parseren met behulp van State Machine

Laten we het State Machine-patroon toepassen voor het parseren van lijnen met het specifieke patroon met behulp van de S4 Class-functie van R.

PROBLEEM-UITBREIDING

We moeten een bestand parseren waarin elke regel informatie over een persoon geeft, met behulp van een scheidingsteken ( ";" ), maar sommige verstrekte informatie is optioneel en ontbreekt in plaats van een leeg veld. Op elke regel kunnen we de volgende informatie hebben: Name;[Address;]Phone . Waar de adresinformatie optioneel is, hebben we die soms en soms niet, bijvoorbeeld:

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

De tweede regel geeft geen adresinformatie. Daarom kan het aantal scheidingstekens afhankelijk zijn zoals in dit geval met één scheidingsteken en voor de andere lijnen twee scheidingstekens. Omdat het aantal scheidingstekens kan variëren, is een manier om dit probleem aan te pakken het herkennen van de aanwezigheid van een bepaald veld op basis van het patroon. In dat geval kunnen we een reguliere expressie gebruiken om dergelijke patronen te identificeren. Bijvoorbeeld:

  • Naam : "^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$" . Bijvoorbeeld: RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA , enz.
  • Adres : "^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$" . Bijvoorbeeld: 11020 LE JEUNE ROAD , 87 SW 27TH . Omwille van de eenvoud nemen we hier niet de postcode, stad, staat op, maar ik kan in dit veld worden opgenomen of extra velden toevoegen.
  • Telefoon : "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$" . Bijvoorbeeld: 305-123-4567, 305 123 4567, +1-786-123-4567 .

Opmerkingen :

  • Ik overweeg het meest voorkomende patroon van Amerikaanse adressen en telefoons, het kan gemakkelijk worden uitgebreid om meer algemene situaties te overwegen.
  • In R heeft het teken "\" een speciale betekenis voor karaktervariabelen, daarom moeten we eraan ontsnappen.
  • Om het proces van het definiëren van reguliere expressies te vereenvoudigen, is het een goede aanbeveling om de volgende webpagina te gebruiken: regex101.com , zodat u ermee kunt spelen, met een bepaald voorbeeld, totdat u het verwachte resultaat voor alle mogelijke combinaties krijgt.

Het idee is om elk lijnveld te identificeren op basis van eerder gedefinieerde patronen. Het statuspatroon definieert de volgende entiteiten (klassen) die samenwerken om het specifieke gedrag te beheersen (het statuspatroon is een gedragspatroon):

GOF staatspatroon

Laten we elk element beschrijven, rekening houdend met de context van ons probleem:

  • Context : Slaat de contextinformatie van het parseerproces op, dwz de huidige status en verzorgt het gehele statusmachine-proces. Voor elke status wordt een actie uitgevoerd ( handle() ), maar de context delegeert deze, op basis van de status, op de actiemethode die is gedefinieerd voor een bepaalde status ( handle() uit State klasse State ). Het definieert de interface die interessant is voor klanten. Onze Context kan als volgt worden gedefinieerd:
    • Attributen: state
    • Methoden: handle() , ...
  • State : de abstracte klasse die elke staat van de staatsmachine vertegenwoordigt. Het definieert een interface voor het inkapselen van het gedrag geassocieerd met een bepaalde staat van de context. Het kan als volgt worden gedefinieerd:
    • Attributen: name, pattern
    • Methoden: doAction() , isState (controleer met behulp van het pattern of het isState al dan niet tot dit isState behoort), ...
  • Concrete States (subklassen van de State ): elke subklasse van de klasse State die een gedrag implementeert dat verband houdt met een toestand van de Context . Onze subklassen zijn: InitState , NameState , AddressState , PhoneState . Dergelijke klassen implementeren gewoon de generieke methode met behulp van de specifieke logica voor dergelijke toestanden. Er zijn geen aanvullende attributen vereist.

Opmerking: het is een kwestie van voorkeur hoe de methode wordt genoemd die de actie uitvoert, handle() , doAction() of goNext() . De methode naam doAction() kan hetzelfde zijn voor beide klassen ( State of Context ) die we liever als handle() in de klasse Context noemen handle() verwarring te voorkomen bij het definiëren van twee generieke methoden met dezelfde invoerargumenten, maar met verschillende klassen.

PERSOONLIJKE KLASSE

Met behulp van de S4-syntaxis kunnen we een Person-klasse als volgt definiëren:

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

Het is een goede aanbeveling om de klassenkenmerken te initialiseren. De documentatie van setClass suggereert het gebruik van een generieke methode gelabeld als "initialize" , in plaats van het gebruik van verouderde attributen zoals: 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
    }
)

Omdat de initialisatiemethode al een standaard generieke methode voor methods , moeten we de oorspronkelijke argumentdefinitie respecteren. We kunnen controleren of het op de R-prompt typt:

> initialize

Het geeft de volledige functiedefinitie terug, u kunt bovenaan zien wie de functie is gedefinieerd als:

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

Daarom moeten we bij het gebruik van setMethod exact dezelfde syntaxis ( .Object ) volgen.

Een andere bestaande generieke methode is show , deze is equivalent aan de toString() van Java en het is een goed idee om een specifieke implementatie voor klassedomein te hebben:

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

Opmerking : we gebruiken dezelfde conventie als in de standaard Java-implementatie toString() .

Laten we zeggen dat we de ontlede informatie (een lijst met Person objecten) in een dataset willen opslaan, dan moeten we eerst een lijst met objecten kunnen omzetten in iets dat de R kan transformeren (bijvoorbeeld het object als een lijst dwingen). We kunnen de volgende aanvullende methode definiëren (zie het bericht voor meer informatie hierover)

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 biedt geen suikersyntaxis voor OO omdat de taal aanvankelijk was bedoeld om waardevolle functies voor statistici te bieden. Daarom vereist elke gebruikersmethode twee delen: 1) het Definitiedeel (via setGeneric ) en 2) het implementatiedeel (via setMethod ). Zoals in het bovenstaande voorbeeld.

STAATSKLASSE

Laten we na de S4-syntaxis de abstracte klasse State definiëren.

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

Elke subklasse van State heeft een name en pattern gekoppeld, maar ook een manier om te bepalen of een bepaalde invoer tot deze status behoort of niet ( isState() -methode), en ook de bijbehorende acties voor deze status te doAction() methode).

Om het proces te begrijpen, laten we de overgangsmatrix voor elke status definiëren op basis van de ontvangen invoer:

Ingang / Huidige status In het Naam Adres Telefoon
Naam Naam
Adres Adres
Telefoon Telefoon Telefoon
Einde Einde

Opmerking: De cel [row, col]=[i,j] vertegenwoordigt de bestemmingstoestand voor de huidige status j , wanneer deze de invoer i ontvangt.

Het betekent dat het onder de status Naam twee ingangen kan ontvangen: een adres of een telefoonnummer. Een andere manier om de transactietabel weer te geven, is met behulp van het volgende UML State Machine- diagram:

Staatsmachinediagram weergave

Laten we elke specifieke staat implementeren als een substaat van de klasse State

SUBKLASSEN VAN DE STAAT

Oorspronkelijke staat :

De initiële status wordt geïmplementeerd via de volgende klasse:

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 om aan te geven dat een klasse een subklasse is van een andere klasse, gebruikt het attribuut contains en geeft de klassenaam van de bovenliggende klasse aan.

Omdat de subklassen gewoon de generieke methoden implementeren, zonder extra attributen toe te voegen, dan de show methode, roept u gewoon de equivalente methode uit de hogere klasse aan (via methode: callNextMethod() )

Aan de begintoestand is geen patroon gekoppeld, het vertegenwoordigt slechts het begin van het proces, daarna initialiseren we de klasse met een NA waarde.

Laten we nu de generieke methoden uit de klasse State implementeren:

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

Voor deze specifieke status (zonder pattern ), zal het idee dat het parseerproces gewoon wordt geïnitieerd en het eerste veld verwacht, een name , anders zal het een fout zijn.

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

De doAction methode zorgt voor de overgang en werkt de context bij met de geëxtraheerde informatie. Hier hebben we toegang tot contextinformatie via de @-operator . In plaats daarvan kunnen we get/set methoden definiëren get/set dit proces in te kapselen (zoals voorgeschreven in OO best practices: inkapseling), maar dat zou nog vier methoden per get-set toevoegen zonder waarde toe te voegen voor het doel van dit voorbeeld.

Het is een goede aanbeveling bij alle doAction implementatie om een beveiliging toe te voegen wanneer het doAction niet correct is geïdentificeerd.

Naam staat

Hier is de definitie van deze klassedefinitie:

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

We gebruiken de functie grepl om te controleren of de invoer bij een bepaald patroon hoort.

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

Nu definiëren we de actie die moet worden uitgevoerd voor een bepaalde staat:

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

Hier houden we rekening met mogelijke overgangen: één voor adresstatus en de andere voor telefoonstatus. In alle gevallen werken we de contextinformatie bij:

  • De person : address of phone met het invoerargument.
  • De state van het proces

De manier om de status te identificeren is door de methode: isState() voor een bepaalde status aan te roepen. We maken een standaardspecifieke addressState, phoneState ( addressState, phoneState ) en vragen vervolgens om een bepaalde validatie.

De logica voor de implementatie van de andere subklassen (één per staat) lijkt erg op elkaar.

Adres staat

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

Telefoon staat

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

Hier voegen we de persoonsinformatie toe aan de lijst met persons uit de context .

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

CONTEXT KLASSE

Nu is het verhuurt aan de uit te leggen Context class-implementatie. We kunnen het definiëren rekening houdend met de volgende kenmerken:

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

Waar

  • state : de huidige status van het proces
  • person : de huidige persoon, het vertegenwoordigt de informatie die we al hebben ontleed uit de huidige regel.
  • persons : de lijst met verwerkte geparseerde personen.

Opmerking : Optioneel kunnen we een name toevoegen om de context op name te identificeren als we met meer dan één parsertype werken.

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

Met dergelijke generieke methoden beheersen we het volledige gedrag van het parsproces:

  • handle() : roept de specifieke doAction() -methode van de huidige state .
  • addPerson : Zodra we de eindstatus hebben bereikt, moeten we een person aan de lijst met persons we hebben ontleed.
  • parseLine() : Parseer een enkele regel
  • parseLines() : meerdere regels parseLines() een reeks regels)
  • as.df() : extraheer de informatie uit de persons in een dataframe-object.

Laten we nu verder gaan met de bijbehorende implementaties:

handle() methode, delegeert op doAction() methode uit de huidige state van de 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)
  }
)

Eerst splitsen we de oorspronkelijke regel in een array met behulp van het scheidingsteken om elk element te identificeren via de R-functie strsplit() , en itereren vervolgens voor elk element als een invoerwaarde voor een bepaalde status. De methode handle() retourneert opnieuw de context met de bijgewerkte informatie ( state , person , kenmerk 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)
  }
)

Omdat R een kopie van het invoerargument maakt, moeten we de context teruggeven ( 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)
  }
)

Het kenmerk persons is een lijst met exemplaren van de klasse S4 Person . Dit iets kan niet worden gedwongen tot een standaardtype omdat R niet weet of een instantie van een door de gebruiker gedefinieerde klasse moet worden behandeld. De oplossing is om een Person om te zetten in een lijst, met behulp van de eerder gedefinieerde as.list methode. Dan kunnen we deze functie toepassen op elk element van de lijst van persons , via de lapply() functie. Vervolgens past in de data.frame functie voor het aanroepen van lappy() functie data.frame voor het converteren van elk element van de persons.list in een gegevensframe. Ten slotte wordt de functie rbind() aangeroepen voor het toevoegen van elk geconverteerd element als een nieuwe rij van het gegenereerde dataframe (zie voor meer informatie hierover dit bericht )

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

SAMEN ZETTEN

Laten we ten slotte de hele oplossing testen. Definieer de te parseren lijnen waar voor de tweede regel de adresinformatie ontbreekt.

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

Nu initialiseren we de context en ontleden we de lijnen:

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

Eindelijk de bijbehorende dataset verkrijgen en afdrukken:

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

Laten we nu de show testen:

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

En voor een deelstaat:

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

Test ten slotte de methode as.list() :

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

$address
[1] "25 NE 25TH"

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

> 

CONCLUSIE

Dit voorbeeld laat zien hoe het statuspatroon moet worden geïmplementeerd, met behulp van een van de beschikbare mechanismen van R voor het gebruik van het OO-paradigma. Desondanks is de R OO-oplossing niet gebruiksvriendelijk en verschilt hij zoveel van andere OOP-talen. U moet uw mindset veranderen, omdat de syntaxis totaal anders is, het herinnert meer aan het functionele programmeerparadigma. Bijvoorbeeld in plaats van: object.setID("A1") zoals in Java / C #, voor R moet u de methode op deze manier setID(object, "A1") : setID(object, "A1") . Daarom moet u het object altijd opnemen als invoerargument om de context van de functie te bieden. Op dezelfde manier is er geen speciaal kenmerk van this klasse en ook geen "." notatie voor toegang tot methoden of attributen van de gegeven klasse. Het is meer foutprompt omdat het verwijzen naar een klasse of methoden gebeurt via kenmerkwaarde ( "Person" , "isState" , etc.).

De bovengenoemde oplossing uit de S4-klasse vereist veel meer coderegels dan traditionele Java / C # -talen voor het uitvoeren van eenvoudige taken. Hoe dan ook, het staatspatroon is een goede en generieke oplossing voor dergelijke problemen. Het vereenvoudigt het proces waarbij de logica in een bepaalde staat wordt gedelegeerd. In plaats van het hebben van een grote if-else blok voor het regelen van alle situaties, we hebben kleinere if-else blokken binnen op iedere State sub-klasse implementatie voor de uitvoering van de actie om in elke staat uit te voeren.

Bijlage : hier kunt u het volledige script downloaden.

Elke suggestie is welkom.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow