Szukaj…


Wprowadzenie

Stany skończone Koncepcje maszynowe są zwykle implementowane w językach programowania obiektowego (OOP), na przykład przy użyciu języka Java, w oparciu o wzorzec stanu zdefiniowany w GOF (odnosi się do książki: „Wzorce projektowe”).

R zapewnia kilka mechanizmów symulujących paradygmat OO, zastosujmy S4 Object System do implementacji tego wzorca.

Parsowanie linii za pomocą automatu stanów

Zastosujmy wzorzec automatu stanów dla parsowania linii o określonym wzorze za pomocą funkcji klasy S4 z R.

ENUNCIATION PROBLEM

Musimy przeanalizować plik, w którym każda linia zawiera informacje o osobie, używając separatora ( ";" ), ale niektóre podane informacje są opcjonalne i zamiast podać puste pole, brakuje go. W każdej linii możemy mieć następujące informacje: Name;[Address;]Phone . Jeśli dane adresowe są opcjonalne, czasami mamy je, a czasem nie, na przykład:

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

Drugi wiersz nie zawiera informacji o adresie. Dlatego liczba ograniczników może być różna, jak w tym przypadku z jednym ogranicznikiem, a dla pozostałych wierszy dwoma ogranicznikami. Ponieważ liczba ograniczników może się różnić, jednym ze sposobów rozwiązania tego problemu jest rozpoznanie obecności lub braku określonego pola na podstawie jego wzorca. W takim przypadku możemy użyć wyrażenia regularnego do identyfikacji takich wzorców. Na przykład:

  • Nazwa : "^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$" . Na przykład: RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA itp.
  • Adres : "^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$" . Na przykład: 11020 LE JEUNE ROAD , 87 SW 27TH . Dla uproszczenia nie podajemy tutaj kodu pocztowego, miasta, województwa, ale mogę dołączyć to pole lub dodać dodatkowe pola.
  • Telefon : "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$" . Na przykład: 305-123-4567, 305 123 4567, +1-786-123-4567 .

Uwagi :

  • Rozważam najczęstszy wzorzec adresów i telefonów w USA, można go łatwo rozszerzyć, aby uwzględnić bardziej ogólne sytuacje.
  • W R znak "\" ma specjalne znaczenie dla zmiennych znakowych, dlatego musimy uciec od niego.
  • Aby uprościć proces definiowania wyrażeń regularnych, dobrą rekomendacją jest skorzystanie z następującej strony internetowej: regex101.com , abyś mógł się nią bawić z podanym przykładem, aż uzyskasz oczekiwany wynik dla wszystkich możliwych kombinacji.

Chodzi o to, aby zidentyfikować każde pole linii na podstawie wcześniej zdefiniowanych wzorców. Wzorzec stanu definiuje następujące jednostki (klasy), które współpracują w celu kontrolowania określonego zachowania (Wzorzec stanu jest wzorcem zachowania):

Wzorzec stanu GOF

Opiszmy każdy element, biorąc pod uwagę kontekst naszego problemu:

  • Context : Przechowuje informacje kontekstowe procesu analizowania, tj. Bieżący stan i obsługuje cały proces automatu stanów. Dla każdego stanu wykonywana jest akcja ( handle() ), ale kontekst deleguje ją, w oparciu o stan, na metodę akcji zdefiniowaną dla określonego stanu ( handle() z klasy State ). Określa interfejs zainteresowania klientów. Nasza klasa Context może być zdefiniowana w następujący sposób:
    • Atrybuty: state
    • Metody: handle() , ...
  • State : Klasa abstrakcyjna reprezentująca dowolny stan automatu stanów. Definiuje interfejs do enkapsulacji zachowania związanego z określonym stanem kontekstu. Można to zdefiniować następująco:
    • Atrybuty: name, pattern
    • Metody: doAction() , isState (przy użyciu atrybutu pattern sprawdź, czy argument wejściowy należy do tego wzorca stanu, czy nie),…
  • Concrete States (podklasy stanu): Każda podklasa stanu klasy State która realizuje zachowanie związane ze stanem Context . Nasze podklasy to: InitState , NameState , AddressState , PhoneState . Takie klasy po prostu implementują ogólną metodę przy użyciu specyficznej logiki dla takich stanów. Nie są wymagane żadne dodatkowe atrybuty.

Uwaga: Preferowaną metodą jest nazwanie metody, która wykonuje akcję, handle() , doAction() lub goNext() . Nazwa metody doAction() może być taka sama dla obu klas ( State lub Context ), którą woleliśmy nazwać handle() w klasie Context aby uniknąć pomyłek przy definiowaniu dwóch ogólnych metod z tymi samymi argumentami wejściowymi, ale inną klasą.

KLASA OSOBOWA

Za pomocą składni S4 możemy zdefiniować klasę Person w następujący sposób:

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

Dobrym zaleceniem jest inicjowanie atrybutów klasy. Dokumentacja setClass sugeruje użycie ogólnej metody oznaczonej jako "initialize" zamiast używania przestarzałych atrybutów, takich jak: 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
    }
)

Ponieważ metoda inicjalizacji jest już standardową ogólną metodą methods pakietowych, musimy przestrzegać oryginalnej definicji argumentu. Możemy to zweryfikować, wpisując polecenie R:

> initialize

Zwraca całą definicję funkcji, możesz zobaczyć na górze, kto jest zdefiniowany dla funkcji:

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

Dlatego, gdy używamy setMethod musimy podążać exaclty tej samej składni ( .Object ).

Inną istniejącą metodą ogólną jest show , jest ona równoważna metodzieString toString() z Java i dobrym pomysłem jest mieć konkretną implementację dla domeny klasy:

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

Uwaga : używamy tej samej konwencji, co w domyślnej implementacji Java toString() .

Powiedzmy, że chcemy zapisać przeanalizowane informacje (listę obiektów Person ) w zbiorze danych, a następnie powinniśmy być w stanie najpierw przekonwertować listę obiektów na coś, co R może przekształcić (na przykład wymusić obiekt jako listę). Możemy zdefiniować następującą dodatkową metodę (więcej informacji na ten temat można znaleźć w poście )

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 nie zapewnia składni cukru dla OO, ponieważ początkowo język ten miał zapewniać cenne funkcje dla statystyków. Dlatego każda metoda użytkownika wymaga dwóch części: 1) części Definicja (przez setGeneric ) i 2) części implementacyjnej (przez setMethod ). Jak w powyższym przykładzie.

KLASA STANU

Zgodnie ze składnią S4 zdefiniujmy abstrakcyjną klasę 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"))

Każda podklasa ze State będzie miała powiązaną name i pattern , ale także sposób na identyfikację, czy dane wejście należy do tego stanu, czy nie ( isState() ), a także zaimplementowanie odpowiednich akcji dla tego stanu ( doAction() metoda).

Aby zrozumieć ten proces, zdefiniujmy macierz przejścia dla każdego stanu na podstawie otrzymanych danych wejściowych:

Stan wejściowy / bieżący W tym Nazwa Adres Telefon
Nazwa Nazwa
Adres Adres
Telefon Telefon Telefon
Koniec Koniec

Uwaga: komórka [row, col]=[i,j] reprezentuje stan docelowy dla bieżącego stanu j , gdy odbierze dane wejściowe i .

Oznacza to, że pod nazwą Nazwa może otrzymać dwa dane wejściowe: adres lub numer telefonu. Innym sposobem przedstawienia tabeli transakcji jest użycie następującego schematu maszyny stanów UML :

Reprezentacja schematu automatu stanów

Załóżmy wdrożyć każdy konkretny stan jako sub-stan klasy State

PODKLASY PAŃSTWOWE

Stan początkowy :

Stan początkowy zostanie zaimplementowany za pomocą następującej klasy:

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

W R, aby wskazać klasę jest podklasę innej klasy, używa atrybutu contains i wskazuje nazwę klasy nadrzędnej.

Ponieważ podklasy po prostu implementują metody ogólne, bez dodawania dodatkowych atrybutów, następnie metoda show , wystarczy wywołać równoważną metodę z wyższej klasy (poprzez metodę: callNextMethod() )

Stan początkowy nie ma powiązanego wzorca, reprezentuje jedynie początek procesu, a następnie inicjujemy klasę wartością NA .

Teraz możemy zaimplementować ogólne metody z klasy State :

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

W tym konkretnym stanie (bez pattern ) pomysł, który po prostu inicjuje proces analizowania, oczekując, że pierwsze pole będzie name , w przeciwnym razie będzie to błąd.

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

Metoda doAction zapewnia przejście i aktualizuje kontekst po wyodrębnieniu informacji. Tutaj uzyskujemy dostęp do informacji kontekstowych za pomocą @-operator . Zamiast tego możemy zdefiniować metody get/set celu enkapsulacji tego procesu (tak jak nakazuje to w najlepszych praktykach OO: enkapsulacja), ale dodałoby to cztery kolejne metody na get-set bez dodawania wartości na potrzeby tego przykładu.

Dobrym zaleceniem we wszystkich implementacjach doAction jest dodanie zabezpieczenia, gdy argument wejściowy nie zostanie poprawnie zidentyfikowany.

Imię i nazwisko

Oto definicja tej definicji klasy:

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

Używamy funkcji grepl do sprawdzenia, czy dane wejściowe należą do danego wzorca.

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

Teraz definiujemy akcję do wykonania dla danego stanu:

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

Rozważamy tutaj możliwe przejścia: jedno dla stanu adresu, a drugie dla stanu telefonu. We wszystkich przypadkach aktualizujemy informacje kontekstowe:

  • Dane person : address lub phone z argumentem wejściowym.
  • state procesu

Sposób na identyfikację stanu polega na wywołaniu metody: isState() dla określonego stanu. Tworzymy domyślne określone stany ( addressState, phoneState ), a następnie pytamy o konkretną walidację.

Logika implementacji innych podklas (jednej na państwo) jest bardzo podobna.

Adres państwa

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

Stan telefonu

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

Tutaj dodajemy informacje o osobie do listy persons w context .

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

KLASA KONTEKSTOWA

Teraz wyjaśnijmy implementację klasy Context . Możemy to zdefiniować uwzględniając następujące atrybuty:

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

Gdzie

  • state : bieżący stan procesu
  • person : bieżąca osoba reprezentuje informacje, które już przeanalizowaliśmy z bieżącej linii.
  • persons : lista przetworzonych osób przeanalizowanych.

Uwaga : Opcjonalnie możemy dodać name aby zidentyfikować kontekst po nazwie, w przypadku, gdy pracujemy z więcej niż jednym typem parsera.

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

Za pomocą takich ogólnych metod kontrolujemy całe zachowanie procesu analizowania:

  • handle() : Wywoła określoną doAction() bieżącego state .
  • addPerson : Gdy dojdziemy do stanu końcowego, musimy dodać person do listy persons mamy przeanalizowane.
  • parseLine() : parseLine() pojedynczy wiersz
  • parseLines() : parseLines() wiele linii (tablica linii)
  • as.df() : Wyodrębnij informacje z listy persons do obiektu ramki danych.

Przejdźmy teraz do odpowiednich implementacji:

handle() , deleguje metodę doAction() z bieżącego state 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)
  }
)

Najpierw strsplit() oryginalną linię w tablicy za pomocą separatora, aby zidentyfikować każdy element za pomocą funkcji R strsplit() , a następnie strsplit() dla każdego elementu jako wartość wejściową dla danego stanu. Do handle() metoda powraca ponownie context z aktualnych informacji ( state , person , persons atrybutu).

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

Ponieważ R tworzy kopię argumentu wejściowego, musimy zwrócić kontekst ( 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)
  }
)

Atrybut persons to lista instancji klasy S4 Person . Czegoś takiego nie można wymusić na żadnym typie standardowym, ponieważ R nie wie, aby traktować instancję klasy zdefiniowanej przez użytkownika. Rozwiązaniem jest przekonwertowanie Person na listę przy użyciu wcześniej zdefiniowanej metody as.list . Następnie możemy zastosować tę funkcję do każdego elementu listy persons za pomocą funkcji lapply() . Potem, w następnym wywołaniu do lappy() funkcji, stosuje się obecnie w data.frame funkcję konwersji każdego elementu persons.list w ramce danych. Wreszcie, funkcja rbind() jest wywoływana w celu dodania każdego elementu przekonwertowanego jako nowy wiersz wygenerowanej ramki danych (więcej informacji na ten temat można znaleźć w tym poście )

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

WSZYSTKO RAZEM

Na koniec przetestujmy całe rozwiązanie. Zdefiniuj linie do przeanalizowania, w których dla drugiej linii brakuje informacji o adresie.

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

Teraz inicjalizujemy context i analizujemy linie:

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

Na koniec uzyskaj odpowiedni zestaw danych i wydrukuj go:

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

Przetestujmy teraz metody show :

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

A dla niektórych stanów podrzędnych:

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

Na koniec przetestuj as.list() :

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

$address
[1] "25 NE 25TH"

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

> 

WNIOSEK

Ten przykład pokazuje, jak zaimplementować wzorzec stanu, używając jednego z dostępnych mechanizmów z R do używania paradygmatu OO. Niemniej jednak rozwiązanie R OO nie jest przyjazne dla użytkownika i bardzo różni się od innych języków OOP. Musisz zmienić swój sposób myślenia, ponieważ składnia jest zupełnie inna, bardziej przypomina funkcjonalny paradygmat programowania. Na przykład zamiast: object.setID("A1") jak w Javie / C #, dla R musisz wywołać metodę w ten sposób: setID(object, "A1") . Dlatego zawsze musisz dołączyć obiekt jako argument wejściowy, aby podać kontekst funkcji. W ten sam sposób nie ma specjalnego atrybutu this klasy i ani "." notacja dostępu do metod lub atrybutów danej klasy. Jest to bardziej monit o błąd, ponieważ odwoływanie się do klasy lub metod odbywa się za pomocą wartości atrybutu ( "Person" , "isState" itp.).

Wspomniane powyżej rozwiązanie klasy S4 wymaga do wykonania prostych zadań znacznie więcej linii kodów niż tradycyjne języki Java / C #. W każdym razie wzorzec stanu jest dobrym i ogólnym rozwiązaniem tego rodzaju problemów. Upraszcza to proces delegowania logiki do określonego stanu. Zamiast dużego bloku if-else do kontrolowania wszystkich sytuacji, mamy mniejsze bloki if-else w implementacji każdej podklasy State do implementacji akcji do wykonania w każdym stanie.

Załącznik : tutaj możesz pobrać cały skrypt.

Wszelkie sugestie są mile widziane.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow