R Language
Zaimplementuj wzorzec automatu stanów za pomocą klasy S4
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):
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 klasyState
). Określa interfejs zainteresowania klientów. Nasza klasaContext
może być zdefiniowana w następujący sposób:- Atrybuty:
state
- Metody:
handle()
, ...
- Atrybuty:
-
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 atrybutupattern
sprawdź, czy argument wejściowy należy do tego wzorca stanu, czy nie),…
- Atrybuty:
-
Concrete States
(podklasy stanu): Każda podklasa stanu klasyState
która realizuje zachowanie związane ze stanemContext
. 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 :
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
lubphone
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żącegostate
. -
addPerson
: Gdy dojdziemy do stanu końcowego, musimy dodaćperson
do listypersons
mamy przeanalizowane. -
parseLine()
:parseLine()
pojedynczy wiersz -
parseLines()
:parseLines()
wiele linii (tablica linii) -
as.df()
: Wyodrębnij informacje z listypersons
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.