Sök…


Introduktion

Finite States Machine- koncept implementeras vanligtvis under OOP-språk (Object Oriented Programming), till exempel med hjälp av Java-språk, baserat på det statliga mönster som definieras i GOF (hänvisar till boken: "Design Patterns").

R tillhandahåller flera mekanismer för att simulera OO-paradigmet, låt oss tillämpa S4 Object System för att implementera detta mönster.

Analysera linjer med statlig maskin

Låt oss tillämpa State Machine-mönstret för att analysera rader med det specifika mönstret med hjälp av S4 Class-funktionen från R.

PROBLEMMUNICATION

Vi måste analysera en fil där varje rad ger information om en person med hjälp av en avgränsare ( ";" ), men viss information som tillhandahålls är valfri och istället för att tillhandahålla ett tomt fält saknas det. På varje rad kan vi ha följande information: Name;[Address;]Phone . Där adressinformationen är valfri, ibland har vi den och ibland inte, till exempel:

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

Den andra raden ger inte adressinformation. Därför kan antalet avgränsare vara uppskjutande som i detta fall med en avgränsare och för de andra linjerna två avgränsare. Eftersom antalet avgränsare kan variera är ett sätt att klara av detta problem att känna igen förekomsten eller inte av ett givet fält baserat på dess mönster. I sådant fall kan vi använda ett regelbundet uttryck för att identifiera sådana mönster. Till exempel:

  • Namn : "^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$" . Till exempel: RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA , etc.
  • Adress : "^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$" . Till exempel: 11020 LE JEUNE ROAD , 87 SW 27TH . För enkelhetens skull inkluderar vi inte här postnumret, staden, staten, men jag kan inkluderas i det här fältet eller lägga till ytterligare fält.
  • Telefon : "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$" . Till exempel: 305-123-4567, 305 123 4567, +1-786-123-4567 .

Anmärkningar :

  • Jag överväger det vanligaste mönstret för amerikanska adresser och telefoner, det kan vara lätt att utvidga att överväga mer allmänna situationer.
  • I R har tecknet "\" speciell betydelse för teckenvariabler, därför måste vi undgå det.
  • För att förenkla processen att definiera vanliga uttryck är en bra rekommendation att använda följande webbsida: regex101.com , så att du kan spela med det, med ett givet exempel, tills du får det förväntade resultatet för alla möjliga kombinationer.

Tanken är att identifiera varje radfält baserat på tidigare definierade mönster. Statsmönstret definierar följande enheter (klasser) som samarbetar för att kontrollera det specifika beteendet (Statens mönster är ett beteendemönster):

GOF statligt mönster

Låt oss beskriva varje element med tanke på vårt problem:

  • Context : Lagrar sammanhangsinformationen för parsingprocessen, dvs det aktuella tillståndet och hanterar hela State Machine Process. För varje stat utförs en åtgärd ( handle() ), men sammanhanget delegerar den, baserat på staten, på den handlingsmetod som definierats för ett visst tillstånd ( handle() från State ). Den definierar gränssnittet för intressen för kunder. Vår Context kan definieras så här:
    • Attribut: state
    • Metoder: handle() , ...
  • State : Den abstrakta klassen som representerar alla tillstånd i State Machine. Den definierar ett gränssnitt för inkapsling av beteende associerat med ett visst sammanhangstillstånd. Det kan definieras så här:
    • Attribut: name, pattern
    • Metoder: doAction() , isState (med pattern verifiera om ingångsargumentet tillhör detta tillståndsmönster eller inte), ...
  • Concrete States (statliga underklasser): Varje underklass i State som implementerar ett beteende associerat med ett tillstånd i Context . Våra underklasser är: InitState , NameState , AddressState , PhoneState . Sådana klasser implementerar bara den generiska metoden med hjälp av den specifika logiken för sådana tillstånd. Inga ytterligare attribut krävs.

Obs! Det handlar om att namnge metoden som utför åtgärden, handle() , doAction() eller goNext() . doAction() kan vara detsamma för båda klasserna ( State eller Context ) som vi föredrog att namnge som handle() i Context klassen för att undvika en förvirring när vi definierar två generiska metoder med samma inmatningsargument, men olika klass.

PERSONKLASS

Med S4-syntaxen kan vi definiera en personklass som denna:

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

Det är en bra rekommendation att initialisera klassattributen. Dokumentationen med setClass föreslår att man använder en generisk metod som är märkt som "initialize" , istället för att använda avskrivna attribut som: 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
    }
)

Eftersom initialize metoden är redan en vanlig generisk metod för paket methods måste vi respektera den ursprungliga argumentet definition. Vi kan verifiera att den skriver på R-prompt:

> initialize

Det returnerar hela funktionsdefinitionen, du kan se längst upp vem funktionen definieras som:

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

Därför när vi använder setMethod måste vi följa exaclty samma syntax (. .Object ).

En annan befintlig generisk metod är show , den motsvarar toString() från Java och det är en bra idé att ha en specifik implementering för klassdomän:

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

Obs! Vi använder samma konvention som i standardimplementeringen till toString() .

Låt oss säga att vi vill spara den analyserade informationen (en lista med Person ) i ett datasæt, då ska vi först kunna konvertera en lista med objekt till något som R kan transformera (till exempel tvinga objektet som en lista). Vi kan definiera följande ytterligare metod (för mer information om detta se inlägget )

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 tillhandahåller inte en sockersyntax för OO eftersom språket ursprungligen utformades för att tillhandahålla värdefulla funktioner för statistiker. Därför kräver varje användarmetod två delar: 1) Definitionsdelen (via setGeneric ) och 2) implementeringsdelen (via setMethod ). Som i exemplet ovan.

STATSKLASS

Efter S4-syntax, låt oss definiera den abstrakta State .

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

Varje underklass från State har associerat ett name och ett pattern , men också ett sätt att identifiera om en given ingång tillhör detta tillstånd eller inte ( isState() -metoden), och också implementera motsvarande åtgärder för detta tillstånd ( doAction() metod).

För att förstå processen, låt oss definiera övergångsmatrisen för varje tillstånd baserat på den mottagna ingången:

Inmatning / aktuellt tillstånd I det namn Adress Telefon
namn namn
Adress Adress
Telefon Telefon Telefon
Slutet Slutet

Obs: Cellen [row, col]=[i,j] representerar destinationsläget för det aktuella tillståndet j , när den mottar ingången i .

Det betyder att det under statusnamnet kan ta emot två ingångar: en adress eller ett telefonnummer. Ett annat sätt att representera transaktionstabellen är att använda följande UML State Machine- diagram:

Tillstånd Maskindiagram representation

Låt oss genomföra varje enskild stat som en delstat i State

STAT SUB-KLASSER

Init State :

Det initiala tillståndet kommer att implementeras via följande klass:

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

I R för att ange att en klass är en underklass av andra klasser använder attributet contains och indikerar klassnamnet för föräldraklassen.

Eftersom underklasserna bara genomföra de generiska metoder, utan att tillfoga ytterligare attribut, sedan show metoden, bara ringa den ekvivalenta metoden från den övre klassen (via metoden: callNextMethod() )

Det initiala tillståndet har inte associerat ett mönster, det representerar bara början på processen, sedan initialiserar vi klassen med ett NA värde.

Nu låter vi implementera de generiska metoderna från State :

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

För detta specifika tillstånd (utan pattern ), idén att den initialiserar bara analyseringsprocessen och förväntar sig att det första fältet kommer att vara ett name , annars blir det ett fel.

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

doAction metoden ger övergången och uppdaterar sammanhanget med den extraherade informationen. Här får vi tillgång till kontextinformation via @-operator . Istället kan vi definiera get/set metoder, för att kapsla in denna process (som det krävs i OO: s bästa praxis: inkapsling), men det skulle lägga till ytterligare fyra metoder per get-set utan att lägga till värde för detta exempel.

Det är en bra rekommendation vid all implementering av doAction , att lägga till en skydd när ingångsargumentet inte är korrekt identifierat.

Namn Stat

Här är definitionen av denna klassdefinition:

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

Vi använder funktionen grepl för att verifiera att ingången tillhör ett givet mönster.

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

Nu definierar vi vilken åtgärd som ska utföras för ett givet tillstånd:

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

Här överväger vi möjliga övergångar: en för adresstillstånd och den andra för telefonstatus. I alla fall uppdaterar vi sammanhangsinformationen:

  • person : address eller phone med inmatningsargumentet.
  • Processens state

Sättet att identifiera tillståndet är att åberopa metoden: isState() för ett visst tillstånd. Vi skapar en standardspecifik addressState, phoneState ( addressState, phoneState ) och ber sedan om en viss validering.

Logiken för implementering av andra underklasser (en per stat) är mycket lik.

Adressstat

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

Telefonstat

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

Här lägger vi till personinformationen i listan över persons i context .

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

KONTEXTKLASS

Nu kan vi förklara implementeringen av Context klass. Vi kan definiera det med tanke på följande attribut:

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

Var

  • state : Processens nuvarande tillstånd
  • person : Den aktuella personen, den representerar den information som vi redan har analyserat från den aktuella raden.
  • persons : Listan över behandlade personer.

Obs! Valfritt kan vi lägga till ett name att identifiera sammanhanget efter namn i fall vi arbetar med mer än en parsertyp.

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

Med sådana generiska metoder kontrollerar vi hela beteendet hos analysprocessen:

  • handle() : Åkallar den specifika doAction() metoden för det aktuella state .
  • addPerson : När vi når addPerson måste vi lägga till en person i listan över persons vi har analyserat.
  • parseLine() : Analysera en enda rad
  • parseLines() : Analysera flera rader (en rad rader)
  • as.df() : Extrahera informationen från persons till ett dataramobjekt.

Låt oss fortsätta nu med motsvarande implementeringar:

handle() metoden, delegater på doAction() metoden från det aktuella state av 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)
  }
)

Först delade vi den ursprungliga linjen i en matris med hjälp av avgränsaren för att identifiera varje element via R-funktionen strsplit() , sedan iterera för varje element som ett ingångsvärde för ett givet tillstånd. De handle() metoden returnerar igen context med den uppdaterade informationen ( state , person , persons attribut).

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 gör en kopia av inmatningsargumentet, vi måste returnera sammanhanget ( 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)
  }
)

Attribut persons är en lista över instans av S4 Person klass. Detta kan inte tvingas till någon standardtyp eftersom R inte känner till att behandla en instans av en användardefinierad klass. Lösningen är att konvertera en Person till en lista med den tidigare definierade metoden as.list . Då kan vi tillämpa denna funktion till varje element i listan persons via lapply() funktion. Sedan tillämpar data.frame funktionen data.frame i nästa kallelse till lappy() -funktion för att konvertera varje element i persons.list till en dataram. Slutligen rbind() funktionen rbind() för att lägga till varje element som konverteras som en ny rad i den genererade dataramen (för mer information om detta se detta inlägg )

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

SÄTT ALLA SAMMAN

Slutligen låter vi testa hela lösningen. Definiera raderna för att analysera var adressinformationen saknas för den andra raden.

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 initialiserar vi context och analyserar raderna:

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

Slutligen få motsvarande datasats och skriv ut det:

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

Låt oss testa nu är det show metoder:

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

Och för en del delstat:

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

as.list() metoden as.list() :

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

$address
[1] "25 NE 25TH"

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

> 

SLUTSATS

Detta exempel visar hur man implementerar statsmönstret med en av de tillgängliga mekanismerna från R för att använda OO-paradigmet. Ändå är R OO-lösningen inte användarvänlig och skiljer sig så mycket från andra OOP-språk. Du måste byta tankesätt eftersom syntaxen är helt annorlunda, den påminner mer om det funktionella programmeringsparadigmet. Till exempel istället för: object.setID("A1") som i Java / C #, för R måste du åberopa metoden på detta sätt: setID(object, "A1") . Därför måste du alltid inkludera objektet som ett inmatningsargument för att ge funktionens sammanhang. På samma sätt finns det inget speciellt this klassattribut och varken ett "." notation för åtkomst till metoder eller attribut för den givna klassen. Det är mer felmeddelande eftersom att hänvisa till en klass eller metoder görs via attributvärde ( "Person" , "isState" , etc.).

Nämnda ovan, S4-klasslösning, kräver mycket fler koderader än ett traditionellt Java / C # -språk för att utföra enkla uppgifter. Hur som helst är statsmönstret en bra och generisk lösning för sådana problem. Det förenklar processen att delegera logiken till ett visst tillstånd. Istället för att ha en stor if-else blocket för att styra alla situationer, vi har mindre if-else block inne på varje State underklass implementering för att genomföra åtgärder för att genomföra i varje stat.

Bilaga : Här kan du ladda ner hela skriptet.

Alla förslag är välkomna.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow