すべての人間を猫で殺すか、Akka.FSMのステートマシンで殺す

最初の記事で書いたように、さほど前ではなく、C ++からScalaに切り替えました。 これに伴い、私はアッカが演じる俳優のモデルを研究し始めました。 私にとって最も鮮明な印象は、このライブラリが提供する有限状態マシン(FSM)の実装とテストの容易さでした。 Akkaには他にもすばらしい便利なものがたくさんあるので、なぜこれが起こったのかはわかりません。 しかし、今では、最初のScalaプロジェクトで、都合の良いことに裏付けられたあらゆる機会にステートマシンを使用しています(心から願っています)。 それで、私はAkka.FSMについての知識だけでなく、私が蓄積したいくつかのトリックと個人的なベストプラクティスをコミュニティと共有する準備ができていると判断しました。 私はハブで同様のトピックを見つけませんでした(そして、一般的にScalaとAkkaについての記事では、それはどういうわけかあまりありませんでした)。 そして、それが退屈しないように-私は一緒に本物の電子猫の行動を実装することを提案します。 私の記事に触発された孤独なロマンティックな魂が、宿題として本格的な「たまこっち」に提供する機能を洗練させると信じたい。 主なことは、コメントでコミュニティと結果を共有した後、そのような魂は忘れないということです。 理想的には、共有アクセスを備えたgithubでプロジェクトを作成し、誰もがトランスヒューマニズムのアイデアの開発に自分の個人的な貢献をもたらすことができるようにします。 そして今-冗談と空想の方向で、私たちは袖をまくります。 最初から始めます。7Dとプレゼンスの効果を高めるために、私はあなたとすべてのステップを踏みます。 TDD添付:認証されていないロボコットがあれば、それは確かに冗談ではありません。

この記事の情報は、すでにScalaに少なくとも少し慣れており、少なくとも俳優のモデルについて表面的な理解を持っている人を対象としています。 知り合いになりたいが、どこから始めればいいのか分からない人のために、ボーナスとして、小さな開始指示を書いて、それがネタバレしないようにスポイラーの下に隠しました。 必要なすべてのライブラリを使用して、あまり手間をかけずにクリーンなScalaプロジェクトを作成する方法について説明します。


そのため、既に理解しているように、最初にakka-actor、akka-testkitおよびscalatestライブラリの最新バージョンを使用したクリーンなプロジェクトが必要です(この記事の執筆時点ではakka 2.3.4およびscalatest 2.1.6です。

''うーん...しかし、これはどのようなゴミですか? ''、または対象外の人のために
警告#1:素手でScalaをまったく感じず、鍵穴からもScalaを覗き見しなかった場合、この記事で後述するすべての特定の部分を理解することはできないでしょう。 しかし、最も頑固な人(私はこれを自分で承認します)のために、ファッショナブルで光沢のあるTypesafe Activatorバンを使用して、Scalaで不必要な困難なく新しいプロジェクトを作成する方法を説明します。

警告#2:次のコマンドラインアクションは、OS LinuxおよびMac OS Xに有効です。Windowsに必要なアクションは、説明されているものと似ていますが、異なります(少なくともProjectsディレクトリ名の前にチルダがないこと、バックスラッシュ、「フォルダ」という語「ディレクトリ」または「ディレクトリ」という言葉の代わりに、Windows用に設計された特別なactivate.batファイルのアーカイブ内の存在)。

プロジェクトを作成する


行きましょう。 私が個人的に新しいプロジェクトを作成する最も簡単な方法は、前述のタイプセーフアクティベーターを公式ウェブサイトからダウンロードすることです。 執筆時点でサイトで宣言されているライブラリのバージョンは、Activator 1.2.10、Akka 2.3.4、Scala 2.11.1です。 すべてがZIPアーカイブとしてダウンロードされます。 ダウンロード中は、オーブンを230℃に予熱する必要があります。 それまでの間、「なぜオーブンが必要なのですか? o_0”-352MBのアーカイブが既にダウンロードされています。 このすべてをディスクのどこかに展開します。 〜/ Projectsディレクトリですべての操作を行います。 だから:

$ mkdir ~/Projects $ cd ~/Projects $ unzip ~/Downloads/typesafe-activator-1.2.10.zip 

アーカイブを開梱したら、パンにオイルを塗ることを忘れないでください。 すべて、私は約束します、そして、すべては非常に深刻になります。 プロジェクトを作成するには、グラフィカルインターフェイスを使用する方法とコマンドラインを使用する方法の2つがあります。 労働者ジェダイとして、もちろん、電力の経路を選択します(さらに、ターミナルはすでに開いています-何らかのUIがあるため、ターミナルを閉じないでください)。

 $ activator-1.2.10/activator new kote hello-akka 

この簡単な行を使用して、アクティベーターに、 hello-akkaというテンプレートから、現在のフォルダーに( 新しいkoteプロジェクトを作成するよう指示します(覚えているように、〜/ Projectsに残ります)。 このテンプレートには、必要なライブラリ用に構成されたbuild.sbtファイルが既に含まれてます。 ダークサイドの可能性は、いつものように、より簡単で魅力的なので、誰かがコマンドラインで成功しない場合は、。 ./activator ui (または既にアクティベーターコンソールにいる場合は単にui )を入力して、開くブラウザーですべてを行うことができます。 そこはすべてとてもきれいです。少なくとも楽しみのためだけに見てください-あなたがそれを好きになることを約束します。 プロジェクトが作成されたら、そのディレクトリに移動します。

 $ cd kote 

IDEまたは非IDE


さらに、各ジェダイは、ed、vi、vim、emacs、Sublime、TextMate、Atom、他の何か、または本格的なIDEを使用して自分の強さを決定します。 個人的には、Scalaに切り替えたときにIntelliJ IDEAを使い始めたので、すぐにこの環境のプロジェクトファイルを生成します。 動作させるには、 addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")をproject / plugins.sbtファイルに追加する必要があります。

 $ echo "addSbtPlugin(\"com.github.mpeltonen\" % \"sbt-idea\" % \"1.5.2\")" > project/plugins.sbt 

その後、アクティベータを起動すると、彼は必要なすべてをコマンドで実行します。

 $ $ ./activator > gen-idea sbt-classifiers 

これで、IDEAでプロジェクトを開くことができます。

それともIDEではありませんか?


IDEがフォースのダークサイド(またはその逆)であり、ジェダイが価値がないと思うなら、これがあなたの完全な権利です。 この場合、アクティベーターのコマンドラインに留まり、任意の便利な方法でファイルを編集できます。 そして、猫の将来の運命を決定するのは、2つのアクティベーターチームだけです。
  1. コンパイル -プロジェクトのコンパイル
  2. test-すべてのテストを実行します。 必要に応じてコンパイルを呼び出しますので、私は嘘をついたので、このコマンドだけで対応できます。

この記事の一部として本番環境でkoteを起動しませんが、Tamagotchiの最終バージョンの潜在的な開発者はrunコマンドを使用してこれを行うことができます。

こてのために場所を掃除します


ご存知のように、すべての猫はきちんとした物足りないです。 したがって、私たちは将来のペットのために清潔で整頓された家を準備することから始めます。 つまり、hello-akkaテンプレートの一部として新しく作成されたプロジェクトに付属するすべての余分なファイルを削除します。 個人的には、src / main / java、src / test / javaディレクトリとすべてのコンテンツ、および不要なすべての.scalaファイルを考慮する必要があります:src / main / scala / HelloAkkaScala.scalaとsrc / test / scala /HelloAkkaSpec.scala。 さて、これで先へ進む準備ができました。


最初のステップ


最初はテストがありました。 そして、テストはコンパイルされませんでした。 ご存知のように、この声明がTDDの基本的な仮定であり、現在私がコミットしています。 したがって、説明はマシン自体ではなく、Akka TestKitライブラリが提供するテスト機能を実証するための最初のテストの作成から始めます。 私が使用するアクティベーターに加えて、すでにテストフレームワークであるscalatestがあります。 彼は私にとても似合っており、私たちのプロジェクトでそれを使わない理由はないと思います。 一般に、Akka TestKitはフレームワークに依存しないため、spec2などで使用できます。 テストパッケージの名前を気にしないために、ファイルを直接src / test / scala / KoteSpec.scalaに配置します

 import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestFSMRef, TestKit} import org.scalatest.{BeforeAndAfterAll, FreeSpecLike, Matchers} class KoteSpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender with Matchers with FreeSpecLike with BeforeAndAfterAll { def this() = this(ActorSystem("KoteSpec")) import kote.Kote._ override def afterAll(): Unit = { system.shutdown() system.awaitTermination(10.seconds) } "A Kote actor" - { // All future tests go here } } 

さらに、コメントのすぐ下にあるすべてのテストをこのクラスの本体に追加することを想定しています。 たとえば、FlatSpecLikeではなく、FreeSpecLikeを使用します。これは、さまざまな状態とオートマトンの遷移に対する多くのテストを明確に構成する方が個人的にはるかに便利だからです。 最初のテストの作成を開始する準備ができたので、猫は何よりもやりたいこと-睡眠という事実から始めることを提案します。 したがって、TDDの原則を考慮して、新しく生まれた猫が最初から寝ていることを確認するテストを作成します。

 "should sleep at birth" in { val kote = TestFSMRef(new Kote) kote.stateName should be(State.Sleeping) kote.stateData should be(Data.Empty) } 


順番に理解してみましょう。 TestFSMRefは、FSMクラスを使用して実装されたステートマシンのテストを簡素化するためにAkka TestKitフレームワークが提供するクラスです。 より正確に言うと、TestFSMRefは、applyメソッドを呼び出すコンパニオンオブジェクトを持つクラスです。 そしてこのメ​​ソッドは、最も一般的なActorRefの子孫であるTestFSMRefクラスのインスタンスを返します。つまり、単純なアクターとしてメッセージをマシンに送信できます。 ただし、TestFSMRefの機能は、単純なActorRefと比較して多少拡張されており、これらの拡張機能はテスト専用に設計されています。 これらの拡張機能の1つは、テストした子猫の現在の状態へのアクセスを提供するstateNameとstateDataの2つの関数です。 なぜ2つの機能、1つの状態があるのですか? 実際、通常の理解では、状態はオートマトンの内部パラメーターの現在の値のセットです。 2つの変数はどこから来たのですか? 事実は、オートマトンの現在の状態を説明するために、Akka.FSM(Erlangのオートマトンの設計の原則に基づく)は、状態の「名前」とそれに関連付けられた「データ」の概念を分けているということです。 さらに、Akkaはオートマトンクラスで可変プロパティ(var) 使用を避けることをお勧めします。これにより、プログラムコード内のオートマトンの状態は、事前定義された少数のよく知られた場所でのみ変更でき、明白で暗黙的な変更を避けることができるという利点があります。 さらに、将来のクラス内からこれらの2つの変数に直接アクセスすることはできません。これらは、FSM基本クラスでprivateとして宣言されています。 ただし、TestFSMRefはテストのためにそれらへのアクセスを提供します。 そして、マシン自体のクラスからそれらに到達する方法はさらに明らかになります。

それで、私たちの睡眠状態を私は睡眠と呼びました。 そして、それを補助オブジェクトStateに押し込みます。これは、コードを明確にし、混乱を避けるために、これからステートの名前をすべて保存します。 データに関しては、この段階ではまだ何になるかわかりません。 ただし、マシンをデータとして「フィード」する必要があります。そうしないと機能しません。 したがって、Emptyという名前で変数に名前を付けることにしました。これは私の個人的な選択であり、何も強制するものではありません。 別の方法で呼び出すことができます:Nothing、Undefined。 私に関しては、Emptyは短く、十分な情報を提供します。 また、データと呼ばれる特別に割り当てられたオブジェクトにデータを保存することにも慣れています。 さまざまな種類のデータの「戦闘」アサルトライフルには、州名よりも少ない、またはそれ以上の名前がある場合があるため、常に専用の場所に保管します。カツレツを別々に、別々に飛ぶ。

さて、コンパイルしていますか? テストで参照する型と変数がないため、コンパイルが失敗することは明らかです。 これは、TDDサイクルの次の段階に進む準備ができていることを意味します。

オートマトンのクラスを宣言するには、状態の名前とそのデータを記述するすべてのクラスとオブジェクトが継承される2つの基本タイプが必要です。 環境を散らかさないために、子猫の生活に必要なすべての定義を保存するコンパニオンオブジェクトを作成します。 これはScalaの世界では一般的な基準であり、誰も私たちを責めることはありません。 パッケージの名前を使用したテストでわずらわしくない場合は、プロジェクト自体についても作成します。 彼をkoteと呼びましょう。 そして、ペットの実装ファイルをそれぞれsrc / main / scala / kote / Kote.scalaに配置します。 それでは、始めましょう:

 package kote import akka.actor.FSM import scala.concurrent.duration._ /** Kote companion object */ object Kote { sealed trait State sealed trait Data } 

これらの定義は、子猫クラスを宣言するのに十分です:

 /** Kote Tamakotchi mimimi njawka! */ class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ } 

クラス内に、さらにアクセスしやすくするために、補助オブジェクトでさらに宣言されるすべてのもののインポートを追加しました。 最初の「眠い」状態の名前とデータの値のみを宣言できます。

 /** Kote companion object */ object Kote { sealed trait State sealed trait Data object State { case object Sleeping extends State } object Data { case object Empty extends Data } } 

テストをコンパイルする前に、最後のステップが残っていました。 クラス自体からと同じように簡単かつ単純にKoteオブジェクトの内部を参照する(そして今後参照したい)ため、KoteSpecクラスの本体にインポートを追加すると便利です。 代替コンストラクタを宣言した直後にできます:

 ... def this() = this(ActorSystem("KoteSpec")) import Kote._ ... 

さて、KoteSpec.scalaファイルのimportセクションにimport kote.Koteを追加することを忘れないでください。 これでプロジェクトが正常にコンパイルされ、テストを実行できます。 なに? 赤 NullPointerException? そして、あなたは考えました-新しい子猫を作るのはとても簡単ですか? 自然は何百万年もの進化を台無しにしてきました! まあ、パニックはありません。 おそらく問題は、出生直後に何をすべきかを動物に伝えなかったことです。 とても簡単です:

 class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ startWith(State.Sleeping, Data.Empty) } 

テストを開始します-出来上がり! 私の大好きな緑! 子猫は生き返ったように見えましたが、どういうわけか退屈なものです。それ自体は愚かに眠ります-それがすべてです。 悲しいです。 彼を起こしましょう。

「眠り、私の喜び!」、または初期状態での行動の実現方法


これをどのように行うのでしょうか? テストの実行中にモニターの速度を落とさないでください? 建設的に考えてみましょう。子猫が俳優である場合、彼と通信する唯一の方法はメッセージを送信することです。 そのような重要なコテ官僚、あなたは彼に秘書を雇うだけでよいので、彼は通信を整理します。 彼が目を覚ますために、彼はどんなメッセージを送るべきですか? 私たちは彼を簡単に書くことができました:kote! 「プロニス」! 起きろ!」 しかし、私は個人的に行の中でメッセージを送信するのは悪いマナーだと考えています。なぜなら、あなたはいつでもある文字で間違いを犯すことができ、コンパイラはそれに気付かず、デバッグするのが非常に難しいからです。 そして、あなたが空想した場合、私たちの新生児のコテはまだ人間の言語を理解していないはずです。 チームの特別な猫の言語を開発することを提案します。彼はそれを誕生から学び始めているようです。 まあ、本能的に、または何か。 そして、私たちは彼の本能の発展に貢献します。 彼を訓練する最初のチームはWakeUpと呼ばれます。 そして、補助オブジェクトのCommandsサブオブジェクトに配置します。

 object Kote { ... object Commands { case object WakeUp } } 

それではテストを始めましょう:

 "should wake up on command" in { val kote = TestFSMRef(new Kote) kote ! Commands.WakeUp kote.stateName should be (State.Awake) } 

もちろん、テストはコンパイルされません。 条件の名前を宣言するのを忘れました:

  case object Awake extends State 

テストはコンパイルされましたが、どうやらそれは私たちのために運命づけられたもので、別の例外: NoSuchElementException:key not found:Sleepingでクラッシュします。 これらすべての野barな文章はどういう意味ですか? 量子実験の若い恋人に、彼は眠るべきだと言いました、そして彼は本当に素直に眠りますが、彼はまだ何を眠りどのようにそれをするかを知りません。 さらに、この不確実性の状態で彼にメッセージを送ろうとしています。 しかし、猫の有名な拷問者や中毒者のようになり、貧しい動物を絶望的な無知に保ち、その行動を簡単に説明しましょう:

 when(State.Sleeping, Data.Empty) { FSM.NullFunction } 

はじめに、悪くない。 whenは、2組の角かっこを持つ最も一般的なscala関数です。 つまり、()()の場合です。 まず、行動を記述したい状態の名前を示し、次に(この場合scalaがそれらを示すことができないため、2番目の括弧は表示されません)、動物の行動を特徴付ける部分関数この状態。 ファンキーな振る舞いです。 そして、行動はさまざまな外部刺激に対する反応です。 単に-着信メッセージで。 通常の反応には3つのタイプがあります-マシンが現在の状態のまま(滞在)、新しい状態に移行(goto)、または作業を停止(停止)します。 4番目のオプション-「異常な」反応-は、マシンが問題に対処できず、例外をスローする場合です(そして、通常のアクターの場合のように、スーパーバイザーが現在の監督の戦略に従ってそれをどうするかを決定します)。 例外のトピックについては後ほど触れます。

FSM.NullFunctionは、この状態の猫はまったく何もせず、何にも反応せず、すべての着信メッセージを耳に通すことを伝える、Akkaライブラリによって有用に提供される関数です。 {case _ =>}と書くこともできますが、それはまったく同じではありません。これについては後で説明します。 NullFunctionを将来の状態を記述するための「ギャグ」として使用すると便利です。詳細はこの段階では重要ではありませんが、それらへの移行をテストする必要があります。

「ウェイクアップ、レイジービースト!」、または新しい状態への遷移によってイベントに応答する方法


それでは、今すぐテストを実行しましょう-そして今、転倒の理由は完全に異なっています:睡眠は目覚めと等しくありませんでした。 もちろん、結局のところ、私たちの猫は眠ることを学びましたが、私たちはまだ彼にウェイクアップチームへの対応方法を教えていません。 少しかき混ぜてみましょう:

 when(State.Sleeping) { case Event(Commands.WakeUp, Data.Empty) => goto(State.Awake) } 

前述したように、状態とデータの名前を持つ変数に直接アクセスすることはできません。 メッセージがマシンに到着したときにのみ、それらにアクセスできます。 FSMはこのメッセージをケースクラスイベントにラップし、そこに現在のステータスデータを追加します。 これで、パターンマッチングを適用し、必要なものをすべて「到着」イベントから分離できます。 この場合、Sleepingという名前の状態でWakeUpコマンドを受け取り、データがData.Emptyであることを確認します。 そして、私たちはこのビネグレット全体に反応して、新しい状態、つまり目覚めへと移行します。 動作を記述するこのアプローチにより、状態名と現在のデータを組み合わせるためのさまざまなオプションを処理できます。 つまり、同じ状態で見つけるために、現在のデータに応じて同じメッセージに異なる反応をすることができます。

ここで、状態間の遷移関数であるgotoとstayの機能に注目したいと思います。 単独では、これらは副作用のない「純粋な」関数です。 これは、彼らの呼び出しの事実が現在の状態の変化につながらないことを意味します。 これらは、必要な状態値(gotoの場合はユーザーが指定し、stayの場合はcurrentで指定された)のみを返し、FSMが理解できる型に変換されます。 変更が発生するには、動作関数から変更を返す必要があります。

私たちはそれを理解しました。 ここでテストを実行します-ただし、再び失敗します。次の状態のAwakeは存在しません。 次の状態が宣言されていない場合、遷移が発生せず、マシンが同じ状態のままである場合、次の状態が宣言された場合に何が起こるかを意図的に示したいと思いました。 開始状態で発生した例外もスローされません。 多くの場合、開発の一環として、私はこれを忘れて、移行が発生せず、テストがクラッシュする理由を理解するために時間をかけました。 ログ内の非自明なテストの「次の状態の目覚めは存在しません」というメッセージは、とりわけ気付くのが簡単ではありません。 しかし、時間が経つにつれて、この機能に慣れ始めます。

したがって、次の状態としてnull関数を宣言すると、テストが緑色に変わります。

  when(State.Awake)(FSM.NullFunction) 


「猫をなでて!」、または揺るぎないままイベントに対応する方法


さて、今、あなたは彼が目覚めたという事実を利用して、子猫をstrokeでることができます。 私はそれとどこに追加することを願っています-あなたはまだそれを理解しましたか?

チーム:
  case object Stroke 

テスト:
 "should purr on stroke" in { val kote = TestFSMRef(new Kote) kote ! Commands.WakeUp kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

コテ:
 when(State.Awake) { case Event(Commands.Stroke, Data.Empty) => sender() ! "purrr" stay() } 


同じことをより簡潔に書くことができます:
 when(State.Awake) { case Event(Commands.Stroke, Data.Empty) => stay() replying "purrr" } 

「猫を二度起こさないでください!」、または繰り返しずに繰り返しずにテストする方法


やめろ! さて、テストで猫をpetでるために、私たちは最初に彼を起こしてから、彼をpetでますか? 優れた、つまり、テストされた状態にまだ10-15の中間のものがある場合(そして100-150の場合)、正しいものに入るために、単一のエラーを犯さずにすべてを正しく実行する必要がありますか? まだ間違いであり、私たちが考えている場所にいない場合はどうなりますか? または、時間の経過とともに中間状態間の遷移に何か変化がありましたか? この場合、TestFSMRefを使用すると、すべての中間手順を実行することなく、setState関数を使用して必要な状態とデータを保証できます。 それでは、テストを変更しましょう。

 "should purr on stroke" in { val kote = TestFSMRef(new Kote) kote.setState(State.Awake, Data.Empty) kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

さて、いくつかの異なる刺激物に対して同じ状態をテストするために、私は個人的に重複コードを取り除くこの方法を発明しました:

 class TestedKote { val kote = TestFSMRef(new Kote) } 

そして今、私はすべてのテストを安全に置き換えることができます:

 "should sleep at birth" in new TestedKote { kote.stateName should be (State.Sleeping) kote.stateData should be (Data.Empty) } "should wake up on command" in new TestedKote { kote ! Commands.WakeUp kote.stateName should be (State.Awake) } "should purr on stroke" in new TestedKote { kote.setState(State.Awake, Data.Empty) kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

同じ非開始状態を数回テストすることに関して、私は次の簡単なトリックを思いつきました:

 "while in Awake state" - { trait AwakeKoteState extends TestedKote { kote.setState(State.Awake, Data.Empty) } "should purr on stroke" in new AwakeKoteState { kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be(State.Awake) } } 

ご覧のとおり、すべての「覚醒」テスト用に「覚醒状態にある」というサブタイトルのフレームを作成し、その中に特性AwakeKoteStateを配置します(ポイントではなくクラス化することもできます)。 この状態のすべてのテストは、その助けを借りて宣言します。

「もっと命を吹き込む」、つまり意味のあるデータを州に追加する方法


私たちの猫が何が欠けているのか考えてくださいまあ、私は-その空腹感。私は猫を飼っていました、私は私が話していることを知っています!彼らは常に食べたいです!彼らが眠らないならもちろんですそして、夢の中で、あなたは、彼らが愛するグラブと素晴らしく健康的なボウルを見るのを見ます!しかし、猫をどこに飢えさせるのでしょうか?彼の生きている親relativeの正確な位置はわかりませんが、私たちのマシンでは、州の名前に加えて、これらの目的のために、現在空のデータがあります。私は少し考えることを提案します。出生時、普通の猫はすぐにおっぱいを探します。だから、彼はすでに少し空腹に生まれています。そして、あなたが彼を養うならば、彼は満腹になります、すなわち、空腹になります。彼が眠り、走り、さらには食べさえすれば、常に空腹感/満腹感があります。つまり、自分で想像できるどの状態でも、データを空にすることはできません。それはつまり他のデータを発表し、これらを捨てて忘れるときです。彼らの時間は過ぎ、進化はそのように決定しました、そして私たちは彼らのために悲しむことはありません。したがって、飢レベルを変数hunger:Intで表し、レベル100は子猫の飢dies死を意味し、レベル0以下は過食を意味します(これは家族が飢excessiveの過剰なレベルと呼んでいるものです)。そして、彼は、例えば60のレベルで生まれます-つまり、すでに少し空腹ですが、まだ耐えられます。ケースクラスVitalSignsに新しい変数を押し込み、ケースオブジェクトEmptyを削除します。 Dataオブジェクトにデータの説明を保存し続けます。レベル0以下-過食から(私たちの家族が飢levelの欠如の過度のレベルを呼び出すように)。そして、彼は、例えば60のレベルで生まれます-つまり、すでに少し空腹ですが、まだ耐えられます。ケースクラスVitalSignsに新しい変数を押し込み、ケースオブジェクトEmptyを削除します。 Dataオブジェクトにデータの説明を保存し続けます。レベル0以下-過食から(私たちの家族が飢levelの欠如の過度のレベルを呼び出すように)。そして、彼は、例えば60のレベルで生まれます-つまり、すでに少し空腹ですが、まだ耐えられます。ケースクラスVitalSignsに新しい変数を押し込み、ケースオブジェクトEmptyを削除します。 Dataオブジェクトにデータの説明を保存し続けます。だから:

 ... object Data { case class VitalSigns(hunger: Int) extends Data } ... 

当然、プロジェクト全体で、Data.EmptyをData.VitalSignsに変更する必要があります。startWith行から開始:

  startWith(State.Sleeping, Data.VitalSigns(hunger = 60)) 

実際、既に説明した状態での子猫の既存の動作では、私たち(もちろん彼)はその重要なインジケーターを気にしないので、ここでData.EmptyをVitalSignsではなくアンダースコアで安全に置き換えることができます。

 when(State.Sleeping) { case Event(Commands.WakeUp, _) => goto(State.Awake) } when(State.Awake) { case Event(Commands.Stroke, _) => stay() replying "purrr" } 

今、私たちの子猫はさらに進化しており、その行動を複雑にし、十分にうんざりしている場合にのみwhenでるときに鳴り響きます。

 when(State.Awake) { case Event(Commands.Stroke, Data.VitalSigns(hunger)) if hunger < 30 => stay() replying "purrr" case Event(Commands.Stroke, Data.VitalSigns(hunger)) => stay() replying "miaw!!11" } 


そしてテスト:

 "while in Awake state" - { trait AwakeKoteState extends TestedKote { def initialHunger: Int kote.setState(State.Awake, Data.VitalSigns(initialHunger)) } trait FullUp { def initialHunger: Int = 15 } trait Hungry { def initialHunger: Int = 75 } "should purr on stroke if not hungry" in new AwakeKoteState with FullUp { kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be(State.Awake) } "should miaw on stroke if hungry" in new AwakeKoteState with Hungry { kote ! Commands.Stroke expectMsg("miaw!!11") kote.stateName should be(State.Awake) } } 

「動物は飢えている!」、またはイベントの計画方法


子猫は時間の経過とともに空腹のレベルを「獲得」する必要があります(何ですか?残酷 これが人生です!

メッセージ:
  case class GrowHungry(by: Int) 

コテ:
 class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ import context.dispatcher startWith(State.Sleeping, Data.VitalSigns(hunger = 60)) val hungerControl = context.system.scheduler.schedule(5.minutes, 5.minutes, self, Commands.GrowHungry(3)) override def postStop(): Unit = { hungerControl.cancel() } ... 

«» , «» ( +3 5 ) , . hungerControl Cancellable, postStop, , dispatcher , , , , . : implicit ExecutionContext, import context.dispatcher.

« !», ,


記事を引きずらないために、私はすぐに空腹からの猫の死(空腹> = 100)と、特に空腹状態(空腹> 85)への移行を実感します。アッカは彼女のプランナーをテストし、メッセージは時間通りに到着すると信じ、猫がそれにどのように反応するかを書きます。猫が眠っている、目を覚ましている、食べ物を要求している、食べている、マウスで遊んでいるなど、すべての条件で「自然な脂肪燃焼」が発生することに注意してください。この場合の対処方法すべての可能な状態に対して同じ動作を説明しますか?テストと一緒に?そして、ある時点でテストを書くのを忘れて、そのようなボールを見つけた猫が1つの状態でフリーズして永遠の満腹を楽しんだら?はい、彼と一緒に地獄に行き、彼に楽しんでもらい、気にしませんしかし、結局のところ、このヒゲ野郎は、古い習慣に従って、定期的に食事をし、脂肪は燃えません-そして結局、彼は体内の食物の過剰摂取で死ぬでしょう!この場合、FSMは、子猫を心から思いやり、whenUnhandled関数を使用することを提案します。この関数は、現在の状態の動作のオプションのいずれとも一致しないメッセージの任意の状態で機能します。覚えておいて、私はそれを書いたFSM.NullFunction{_ =>}とは異なりますか?今、あなたは正確に何を推測していると思います。最初のケースでアクターに届いたメッセージを処理せず、それらすべてがwhenUnhandled関数に該当する場合、2番目のケースでは状況は逆です。すべてのメッセージはビヘイビアー関数によって単に「吸収」され、whenUnhandledに到達しません。

 whenUnhandled { case Event(Commands.GrowHungry(by), Data.VitalSigns(hunger)) => val newHunger = hunger + by if (newHunger < 85) stay() using Data.VitalSigns(newHunger) else if (newHunger < 100) goto(State.VeryHungry) using Data.VitalSigns(newHunger) else throw new RuntimeException("They killed the kitty! Bastards!") } 

ここでは、別の関数の出現を見ることができます-使用しています。コンテキストから、移行中に特定のデータを状態にバインドでき、stayとgotoの両方で使用できることは明らかです。つまり、usingを使用すると、現在の状態のままで新しいデータを使用したり、新しいデータを使用して新しい状態に移動したりできます。コードの以前のすべてのバージョンのようにusingが指定されていない場合、データは変更されず同じままです。

「生命を与える病理学」、または例外的な状況をテストする方法


時間とスペースを節約するために、すべてのテストについては説明しません。これらは以前のものと大差ありません。興味深いことに、スローされた例外をテストする方法に言及する必要があると思います。実際、FSMの場合、これは通常のアクターに適用可能な方法と違いはありません。

 "should die of hunger" in new AwakeKoteState with Hungry { intercept[RuntimeException] { kote.receive(Commands.GrowHungry(1000)) // headshot } } 

メッセージを送信する代わりに(テストではなく、この場合ユーザーガーディアンであるスーパーバイザーアクターに受信を配信し、テストは単にパスしません)、アクターのreceiveメソッド呼び出しを直接使用します。これは、特定の状況でマシンが正しい例外をスローすることを確認するために必要です。確かに、私たちの貧しい動物が全能の監督者を蘇生させるかどうかは、これに依存し続け、スローされた例外に対する彼の戦略で適切なマークを見つけました。猫の死の原因として例外を使用したのは、このテストを実証するためでした。または、単にstop()を返すこともできます-しかし、通常の猫は空腹ではなく老齢で死にます。

「すでに呼吸を止めて、眠そう!」、またはある状態での滞在時間を制限する方法


私たち自身が眠りに落ちないように、バイパスしたくない最後の機能についてお話します。これは、状態の最大タイムアウトを設定する機能です。単純に設定されます:when関数の小数点の後に、状態の名前の直後に示されます。たとえば、3時間の睡眠の後、猫は目を覚ますので、目を覚ます必要はありません。

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp, _) => goto(State.Awake) } 

どう思いますか、起きて?いや。それ自体では、タイムアウトは状態もデータも変更しません。これは猫自身、彼自身の意思決定によってのみ行うことができます(まあ、ネオ、しかし彼はもう戻らないと聞きました;そして古いチャックはもはやケーキではありません)。そして、単一の場所で-行動の機能で(これはネオには当てはまりません、彼は若い頃のチャックのようにどこでもできます)。しかし、今では、指定された期間の後、誰も猫を起こさない場合、StateTimeoutメッセージを受け取ります。そして、それにどのように反応するかは、彼が決めることです。

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp | StateTimeout, _) => goto(State.Awake) } 

今、彼は2つの理由から目覚めることができます:彼が十分に長く眠り、眠った場合、または彼が力で起こされた場合。これらの2つのイベントを分離して、異なる反応をすることができます。1つはアクティブで遊び心があり、もう1つは邪悪な悪臭を放ち、絶えず鳴き声を上げて、みんなを動揺させます。いずれにせよ、猫が夢の状態を抜けると、タイムアウトは自動的にキャンセルされ(愚かなスケジューラーはそれ自体でキャンセルする必要があります)、超自然的なことは何も起こりません。ちなみに、猫がタイムアウトを受信したが、スリープ状態を続けた場合(滞在()を返す)、予想どおり3時間後に再び猫が受信します。つまり、明示的にキャンセルまたは再割り当てされることなく(stay()を使用します。ForMax(20.hours)、さらに)、動作機能に捕捉され、stay()応答を伴うタイムアウト一定の時間が経過すると、再び「シュート」します。

when関数で状態タイムアウトを指定できるという事実に加えて(そして、この状態に入るたびに動作します)、goto関数に渡すときに直接指定することもできますし、前述のforMax関数(たとえばstay()。forMax(1.minute)またはgoto(State.Sleeping).using(Data.Something).forMax(1.minute))、このようなタイムアウトはこの特定の遷移でのみ機能します(値を置き換えます)そこに示されている場合):

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp, _) => goto(State.Awake).forMax(3.hours) case Event(StateTimeout, _) => goto(State.Awake).forMax(5.hours) } 

今、私たちの猫は力で目覚め、3時間目を覚まし続け、通常は5時間眠ります。もちろん、Awake状態でStateTimeoutイベントを処理することを条件とします。

「何?!彼はいびきをかきますか?!!」、または状態間の遷移中に自動アクションを設定する方法


そして最後に、最後に、私は約束します。Akka FSMには、onTransitionメソッドという別の便利な機能があります。状態から状態への遷移中にいくつかのアクションを設定できます。次のように使用します。

 onTransition { case State.Sleeping -> State.Awake => log.warning("Meow!") case _ -> State.Sleeping => log.info("Zzzzz...") } 

すべてが明らかなように思えますが、念のために説明します。睡眠状態から覚醒状態に移行する瞬間に、子猫が特別な方法で一度だけ鳴きます。任意の状態から睡眠状態に移行すると、いびきが1つだけ発せられます(英語では、これがどのように聞こえます。

これらのアクションは、FSMActorRef.setState関数を使用してテストで状態を設定した場合でも機能します(もちろん、現在の状態からターゲット状態への遷移がonTransitionで説明されているもののいずれかに一致する場合)。したがって、それらをテストできます。さて、ここでgoto関数を使用しても意味がないことに注意してください。これは、状態遷移中に頑固にデータを変更しようとしたことがあり、長い間データが機能しない理由を理解できなかった人としてあなたに伝えます。私が発見した別のニュアンス:遷移トリガーは、動作関数からstay()を返すことで現在の状態にとどまる場合でも機能します。これはAkkaの将来のバージョンで修正されると約束されていましたが、現時点では、Sleeping状態から何かに反応したときにstay()を返すと、onTransitionが機能し、子猫がいびきをかくことになります。

終わり


今日はこれだけについて話したかったのです。そして、独立した研究のタスクとして、質問に答えることを提案します。同じ状態名に対してwhen関数が連続して数回呼び出されるとどうなりますか?または、連続してwhenUnhandled関数を数回呼び出します。ご清聴ありがとうございました。

PSこの記事を書いている過程で、生きていても死んでもいないネコは一人もいなかった。

Source: https://habr.com/ru/post/J237657/


All Articles