R Language
S4クラスを使用したステートマシンパターンの実装
サーチ…
前書き
有限状態マシン概念は、通常、オブジェクト指向プログラミング(OOP)言語、例えばGOFで定義された状態パターンに基づいてJava言語を使用して実装されている(本書「デザインパターン」を参照)。
Rはオブジェクト指向のパラダイムをシミュレートするいくつかのメカニズムを提供します。このパターンを実装するためにS4 Object Systemを適用しましょう。
ステートマシンを使用したラインの解析
RのS4 Classの機能を使用して、特定のパターンの行を解析するためのState Machineパターンを適用しましょう。
問題の取り組み
区切り文字( ";"
)を使用して、各行が人に関する情報を提供するファイルを解析する必要がありますが、提供される情報の中にはオプションで、空のフィールドを提供する代わりに欠落しています。各行には、 Name;[Address;]Phone
情報があります。アドレス情報がオプションの場合、時にはアドレス情報を持っていることもあれば、しないこともあります。
GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553
2行目はアドレス情報を提供しません。したがって、区切り文字の数は、この場合のように、1つの区切り文字と他の2つの区切り文字とでは異なる場合があります。区切り文字の数が変わる可能性があるため、この問題を解決する方法の1つは、パターンに基づいてフィールドの有無を認識することです。そのような場合、そのようなパターンを識別するために正規表現を使用することができます。例えば:
- 名前 :
"^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$"
ます。たとえば:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
など - アドレス :
"^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$"
ます。例:11020 LE JEUNE ROAD
、87 SW 27TH
。わかりやすくするために、ここには郵便番号、市区町村、州は含まれていませんが、私はこのフィールドに含めるか、追加のフィールドを追加することができます。 - 電話 :
"^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$"
。例えば:305-123-4567, 305 123 4567, +1-786-123-4567
。
注 :
- 私は米国のアドレスと電話の最も一般的なパターンを検討していますが、より一般的な状況を検討するのは簡単に拡張できます。
- Rでは、記号
"\"
は文字変数にとって特別な意味を持ちますので、それをエスケープする必要があります。 - 正規表現を定義するプロセスを簡略化するため、次のWebページregex101.comを使用することをお勧めします。可能なすべての組み合わせについて期待される結果が得られるまで、与えられた例でそれを再生できます。
アイデアは、以前に定義されたパターンに基づいて各ラインフィールドを識別することです。 Stateパターンは、特定の動作を制御するために共同作業する次のエンティティ(クラス)を定義します(Stateパターンは動作パターンです)。
問題のコンテキストを考慮して各要素を説明しましょう。
-
Context
:解析プロセスのコンテキスト情報、すなわち現在の状態を格納し、状態機械プロセス全体を処理する。各状態について、アクション(handle()
)が実行されますが、コンテキストは特定の状態(State
クラスのhandle()
に対して定義されたアクションメソッドに対して、状態に基づいてそれを委譲します。これは、クライアントにとって関心のあるインターフェースを定義します。私たちのContext
クラスは次のように定義することができます:- 属性:
state
- メソッド:
handle()
、...
- 属性:
-
State
:State Machineの状態を表す抽象クラス。これは、コンテキストの特定の状態に関連付けられた動作をカプセル化するためのインタフェースを定義します。それは次のように定義することができます:- 属性:
name, pattern
- メソッド:
doAction()
、isState
(pattern
属性を使用して、入力引数がこの状態パターンに属しているかどうかを検証する)、...
- 属性:
-
Concrete States
(状態サブクラス):クラスの各サブクラスState
の状態に関連付けられた動作を実装Context
。私たちのサブクラスは:InitState
、NameState
、AddressState
、PhoneState
です。そのようなクラスは、そのような状態に特定のロジックを使用して汎用メソッドを実装するだけです。追加の属性は必要ありません。
注意: action、 handle()
、 doAction()
またはgoNext()
を実行するメソッドに名前を付ける方法は、優先事項です。メソッド名doAction()
は、入力引数は同じでクラスは異なる2つのジェネリックメソッドを定義する際の混乱を避けるため、 Context
クラスのhandle()
として指定した両方のクラス( State
またはContext
)で同じにすることができます。
パーソナルクラス
S4構文を使用して、Personクラスを次のように定義できます。
setClass(Class = "Person",
slots = c(name = "character", address = "character", phone = "character")
)
クラス属性を初期化することをお勧めします。 setClass
ドキュメントでは、 prototype, representation
などの非推奨の属性を使用する代わりに、 "initialize"
というラベルの付いた汎用メソッドを使用することを提案しています。
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
}
)
initializeメソッドはすでにパッケージmethods
標準汎用メソッドであるため、元の引数定義を尊重する必要があります。 Rプロンプトで入力を確認できます:
> initialize
これは関数定義全体を返します。関数の定義の先頭に次のようなものがあります。
function (.Object, ...) {...}
したがって、 setMethod
を使用するときは、同じ構文( .Object
)でexacltyを実行する必要があります。
別の既存の一般的な方法があるshow
それは同等である、 toString()
Javaからメソッドと、クラスドメインの特定の実装を持つことは良いアイデアです。
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)
}
)
注意 :デフォルトのtoString()
Java実装と同じ規約を使用します。
解析された情報( Person
オブジェクトのリスト)をデータセットに保存したい場合、まずオブジェクトのリストをRが変換できるものに変換する必要があります(たとえば、オブジェクトをリストとして強制的に変換する)。以下の追加のメソッドを定義することができます(詳細はpostを参照してください)
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は統計学者にとって貴重な機能を提供するために最初に考案されたものであるため、Oは砂糖構文を提供しません。したがって、各ユーザメソッドは、1)定義部分( setGeneric
経由)と2)実装部分( setMethod
経由)の2つの部分を必要とします。上記の例のように。
状態クラス
S4の構文に従って、抽象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"))
State
からのすべてのサブクラスにはname
とpattern
が関連付けられているだけでなく、指定された入力がこの状態に属しているかどうかを識別する方法( isState()
メソッド)、およびこの状態( doAction()
方法)。
このプロセスを理解するために、受け取った入力に基づいて各状態の遷移行列を定義しましょう:
入力/現在の状態 | その中に | 名 | 住所 | 電話 |
---|---|---|---|---|
名 | 名 | |||
住所 | 住所 | |||
電話 | 電話 | 電話 | ||
終わり | 終わり |
注: cell [row, col]=[i,j]
は、入力i
を受け取ったときの現在の状態j
宛先状態を表します。
州の名前の下では、住所または電話番号の2つの入力を受け取ることができます。トランザクションテーブルを表すもう1つの方法は、次のUMLステートマシンの図を使用する方法です。
クラスのサブ状態として、それぞれ特定の状態を実装してみましょうState
州のサブクラス
初期状態 :
初期状態は、次のクラスを介して実装されます:
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()
}
)
Rで、クラスが他のクラスのサブクラスであることを示すには、 contains
属性を使用し、親クラスのクラス名を示します。
サブクラスは、追加の属性を追加せずに汎用メソッドを実装するだけなので、 show
メソッドは、上のクラスから同等のメソッドを呼び出すだけcallNextMethod()
メソッド: callNextMethod()
経由)
初期状態にはパターンが関連付けられておらず、プロセスの開始点を表しているだけで、クラスをNA
値で初期化します。
次に、 State
クラスからジェネリックメソッドを実装することができます。
setMethod(f = "isState", signature = "InitState",
definition = function(obj, input) {
nameState <- new("NameState")
result <- isState(nameState, input)
return(result)
}
)
この特定の状態( pattern
なし)では、最初のフィールドを期待している解析プロセスを初期化するというアイデアはname
になります。それ以外の場合はエラーになります。
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
メソッドは、遷移を提供し、抽出された情報でコンテキストを更新します。ここでは、 @-operator
介してコンテキスト情報にアクセスしています。代わりに、このプロセスをカプセル化するget/set
メソッド(OOベストプラクティスではカプセル化)が定義できますget/set
、この例の目的では値を追加せずにget-set
ごとに4つのメソッドを追加します。
すべてのdoAction
実装では、入力引数が正しく識別されない場合に保護機能を追加することをお勧めします。
名前の状態
このクラス定義の定義は次のとおりです。
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()
}
)
入力が与えられたパターンに属することを検証するために関数grepl
を使います。
setMethod(f="isState", signature="NameState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
次に、特定の状態に対して実行するアクションを定義します。
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)
}
)
ここでは、可能な遷移を考慮する.1つはアドレス状態用であり、もう1つは電話状態用である。どのような場合でも、コンテキスト情報を更新します。
-
person
情報:入力引数を持つaddress
またはphone
。 - プロセスの
state
状態を識別する方法は、特定の状態のisState()
メソッドを呼び出すことです。デフォルトの特定の状態( addressState, phoneState
)を作成し、特定の検証を依頼します。
他のサブクラス(1つの状態ごとに1つ)の実装のロジックは非常に似ています。
住所の状態
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)
}
)
電話の状態
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)
}
)
ここでは、個人情報をcontext
のpersons
のリストに追加しcontext
。
setMethod(f = "doAction", "PhoneState",
definition = function(obj, input, context) {
context <- addPerson(context, context@person)
context@state <- new("InitState")
return(context)
}
)
コンテクストクラス
ここで、 Context
クラスの実装について説明します。次の属性を考慮して定義できます。
setClass(Class = "Context",
slots = c(state = "State", persons = "list", person = "Person")
)
どこで
-
state
:プロセスの現在の状態 -
person
:現在の人物。現在の行からすでに解析した情報を表します。 -
persons
:処理された解析済み人物のリスト。
注 :オプションで、複数のパーサータイプを扱う場合に備えて、名前を付けてコンテキストを識別するためのname
を追加できます。
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"))
このような一般的なメソッドでは、解析プロセスの全体的な動作を制御します。
-
handle()
:現在のstate
の特定のdoAction()
メソッドを呼び出します。 -
addPerson
:我々は終了状態に到達すると、我々は、追加する必要がありperson
リストにpersons
我々が解析されてきました。 -
parseLine()
:1行を解析する -
parseLines()
:複数の行を解析する(行の配列) -
as.df()
:persons
リストから情報をデータフレームオブジェクトに抽出します。
対応する実装について、今説明します。
handle()
メソッドは、 context
現在のstate
からdoAction()
handle()
メソッドに委譲し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)
}
)
まず、元の行を区切り文字を使って配列に分割し、R関数strsplit()
を介して各要素を識別した後、各要素について、与えられた状態の入力値として反復処理を行います。 handle()
メソッドは、更新された情報( state
、 person
、 persons
属性)を持つcontext
を再び返します。
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)
}
)
Rが入力引数のコピーを作成すると、コンテキスト( 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)
}
)
属性persons
は、S4 Person
クラスのインスタンスのリストです。 Rはユーザー定義のクラスのインスタンスを扱うことを知らないので、この何かを標準型に強制することはできません。解決策は、前に定義したas.list
メソッドを使用してPerson
をリストに変換することです。その後、我々は、リストの各要素に、この機能を適用することができpersons
を経由して、 lapply()
関数。次にlappy()
関数の次の呼び出しで、 persons.list
各要素をデータフレームに変換するdata.frame
関数を適用するdata.frame
にpersons.list
ました。最後に、 rbind()
されたデータフレームの新しい行として変換された各要素を追加するためにrbind()
関数が呼び出されます(詳細はこの記事を参照してください)
# 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)
}
)
一緒にすべてを託す
最後に、ソリューション全体をテストできます。 2行目のアドレス情報がない行を解析する行を定義します。
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"
)
次に、 context
を初期化し、行を解析します。
context <- new("Context")
context <- parseLines(context, s)
最後に、対応するデータセットを取得して印刷します。
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
show
メソッドをテストしましょう:
> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']
そして、いくつかのサブ状態について:
>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']
最後に、 as.list()
メソッドをテストしas.list()
。
> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"
$address
[1] "25 NE 25TH"
$phone
[1] "+1-786-987-6543"
>
結論
この例は、OOパラダイムを使用するためにRから利用可能なメカニズムの1つを使用して、Stateパターンを実装する方法を示しています。それにもかかわらず、R OOソリューションはユーザーフレンドリーではなく、他のOOP言語とは大きく異なります。構文がまったく違うので、あなたの考え方を変える必要があります。それは機能プログラミングのパラダイムを思い起こさせます。たとえば、Java / C#のようにobject.setID("A1")
代わりに、Rの場合はsetID(object, "A1")
ようにメソッドを呼び出す必要があります。したがって、関数のコンテキストを提供するためには、常にオブジェクトを入力引数として含める必要があります。同様に、 this
クラス属性も特別なものもなく、 "."
指定されたクラスのメソッドまたは属性にアクセスするための表記法。クラスまたはメソッドを参照するためには、属性値( "Person"
、 "isState"
など)を使用するため、これはエラープロンプトの多くです。
上記のS4クラスのソリューションには、単純なタスクを実行するための従来のJava / C#言語よりも多くのコード行が必要です。とにかく、ステートパターンは、このような種類の問題に対する優れた汎用ソリューションです。これは、ロジックを特定の状態に委譲するプロセスを簡素化します。すべての状況を制御するための大きなif-else
ブロックを持つ代わりに、各状態で実行するアクションを実装するために、各State
サブクラス実装の内側に小さなif-else
ブロックを配置しています。
添付ファイル : ここでは、スクリプト全体をダウンロードできます。
どんな提案も大歓迎です。