3个不稳定版本
使用旧的Rust 2015
0.2.1 | 2017年1月12日 |
---|---|
0.2.0 | 2017年1月11日 |
0.1.0 | 2016年12月16日 |
在算法类别中排名第1773
20KB
192 行
描述
funfsm
是一个有限状态机库,其中每个状态在状态机中都是函数。每个FSM还有一个相应的context
,它维护FSM的数据状态。在收到每条消息时,代表当前状态的函数会调用消息和一个对上下文的可变引用。状态函数根据需要修改上下文,并返回下一个状态函数以及任何发往其他FSM的输出消息。
用法
将以下内容添加到您的Cargo.toml
[dependencies]
funfsm = "0.2"
将此添加到您的crate根目录
extern crate funfsm;
以下小节都使用了bowl_fsm测试代码作为示例。
创建FSM
使用funfsm的第一步是确定您的fsms使用的上下文类型、输入消息和输出消息的类型。这些类型通过实现FsmTypes
特质来声明。
以一个喂猫状态机的例子为例,其中状态是猫食碗的状态,我们定义类型如下
use funfsm::FsmTypes;
const MAX_RESERVES: u8 = 10;
const REFILL_THRESHOLD: u8 = 9;
#[derive(Debug, Clone)]
pub struct Context {
pub contents: u8, // % of the bowl that is full
pub reserves: u8, // The amount of bowls of food left in the bag
}
impl Context {
pub fn new() -> Context {
Context {
contents: 0, // The bowl starts off empty
reserves: MAX_RESERVES,
}
}
}
#[derive(Debug, Clone)]
pub enum CatMsg {
Meow,
Eat(u8) // % of food to eat
}
#[derive(Debug, Clone)]
pub enum StoreReq {
Buy(u8)
}
#[derive(Debug, Clone)]
pub enum StoreRpy {
Bowls(u8)
}
#[derive(Debug, Clone)]
pub enum BowlMsg {
CatMsg(CatMsg),
StoreRpy(StoreRpy)
}
#[derive(Debug)]
pub struct BowlTypes;
impl FsmTypes for BowlTypes {
type Context = Context;
type Msg = BowlMsg;
type Output = StoreReq;
}
我们可以看到Context
维护有关猫食碗和供应的信息,这有助于确定何时在状态之间进行转换。我们看到使用BowlMsg
来指示状态机进行转换,它可以由猫的动作或宠物店的回复组成。最后,我们可能会在某一点上食物不足,因此需要从宠物店重新订购。因此,由状态转换引起的任何输出消息都是StoreReq
消息。
现在我们已经声明了类型,我们需要创建表示状态机的实际状态函数。在我们的例子中,碗可以处于两种状态:empty
(空)或full
(满)。注意,这里的full
指的是碗内有食物的情况。再次提醒,每个状态都接受一个可变参数Context
和一个BowlMsg
,并返回一个包含下一个状态和包含任何输出消息的Vec的元组。为了从返回类型签名中移除一些冗余,使用了类型StateFn
来表示下一个状态。它只是一个包含表示状态函数名称的&'static str
和一个指向实际状态函数的函数指针的两个元组的包装器。
use funfsm::{Fsm, StateFn};
pub fn empty(ctx: &mut Context, msg: BowlMsg) -> (StateFn<BowlTypes>, Vec<StoreReq>) {
if let BowlMsg::CatMsg(CatMsg::Meow) = msg {
if ctx.reserves > 0 {
// Fill the bowl
ctx.contents = 100;
ctx.reserves -= 1;
if ctx.reserves <= REFILL_THRESHOLD {
let output = vec![StoreReq::Buy(10)];
return next!(full, output);
}
return next!(full);
} else {
return next!(empty);
}
}
if let BowlMsg::StoreRpy(StoreRpy::Bowls(num)) = msg {
ctx.reserves += num-1;
ctx.contents = 100;
return next!(full);
}
next!(empty)
}
pub fn full(ctx: &mut Context, msg: BowlMsg) -> (StateFn<BowlTypes>, Vec<StoreReq>) {
if let BowlMsg::CatMsg(CatMsg::Eat(pct)) = msg {
if pct >= ctx.contents {
ctx.contents = 0;
return next!(empty)
} else {
ctx.contents -= pct;
return next!(full)
}
}
if let BowlMsg::StoreRpy(StoreRpy::Bowls(num)) = msg {
ctx.reserves += num;
}
next!(full)
}
上面的代码展示了我们的状态函数。我们可以看到,根据当前状态、接收到的消息和上下文,可以确定下一个状态和任何输出消息。next!
宏是驱动状态之间转换的因素,因此用户不需要手动创建StateFn
值。这个宏有两种形式:一种用于没有输出消息的转换,另一种用于有输出消息的转换。
使用fsm
现在我们已经编写了fsm的代码,我们可以用它做什么呢?首先,我们必须实例化一个实例
let mut fsm = Fsm::<BowlTypes>::new(Context::new(), state_fn!(empty));
上面的代码展示了如何创建一个新的Fsm
,该实例由适当的类型参数化。构造函数接受一个新的Context
和fsm的初始状态,该状态是通过给state_fn!
宏的。
现在我们的fsm处于活动状态,我们可以向它发送消息并检查其状态。注意,在这个例子中,我们没有创建宠物店来发送消息,所以我们忽略fsm返回的任何消息。
// Let's ensure we start off in the empty state. The extra block used here is because `get_state`
// calls return a reference to the internal context of the fsm and we want to limit it's scope so
// we can mutate the fsm with further send calls.
{
let (name, ctx) = fsm.get_state();
assert_eq!(name, "empty");
assert_eq!(ctx.contents, 0);
}
// Send a message
let _ = fsm.send(BowlMsg::CatMsg(CatMsg::Meow));
// Our bowl was empty, but we have some reserves at home. When the cat meows, the bowl gets
// refilled. Let's ensure this actually happens.
let (name, ctx) = fsm.get_state();
assert_eq!(name, "full");
assert_eq!(ctx.contents, 100);
这就是操作fsm的全部内容!简单,对吧?
不变性和测试
您有一个fsm。它看起来相当不错,但您怎么知道它是正确的呢?显然,您需要测试它!在这种情况下,建议的测试应生成消息,将它们通过状态机运行,并确保fsm在状态之间的转换过程中保持某些不变性。funfsm
提供了一个Constraints
类型来简化此类测试。fsm处理每个消息时都会检查3种类型的约束。Preconditions
在将消息发送到fsm之前在特定状态下进行检查,invariants
在所有状态下进行检查,而transitions
在消息发送到fsm并从一个状态转换到另一个状态后进行检查。每个约束都通过宏添加,以便简化添加新不变性的过程,并允许测试失败时报告失败的具体约束。
use funfsm::constraints::Constraints;
// Create a new constraints object
let mut c = Constraints::new();
// Add some preconditions. Each precondition only runs when the state machine is in that state.
// These particular preconditions simply check that when in the given state, the contents of the
// bowl is correct. Note that each state can have multiple preconditions, although only one for each
// state is shown here. In fact, complex state machines should have multiple preconditions rather
// than one large predicate function. This makes it easier to add new ones and enables better error
// reporting.
precondition!(c, "empty", |ctx: &Context| ctx.contents == 0);
precondition!(c, "full", |ctx: &Context| ctx.contents > 0 && ctx.contents <= 100);
// Add an invariant that gets checked in every state after the message is sent to the fsm and the
// transition occurs. Like preconditions, each state can have multiple invariants, and the same
// recommendations to writing them apply.
invariant!(c, |ctx: &Context| ctx.contents <= 100);
// Add some transition constraints. Transition constraints are only checked when the fsm transitions
//from one given state to another given state. Note that because transitions take so many input
//parameters (making it overly verbose to use closures), they are written differently from both
//preconditions and invariants. A single function is associated with a state transition. Inside that
//transition function, individual assertions/invariants are verified using the "check!" macro. This
//allows the same benefits of small predicates as used in both preconditions and invariants, namely
//ease of adding new checks and error reporting. The `&'static str s` passed to "check!" is there to
// indicate which transition a check failed in.
transition!(c, "empty" => "full", empty_to_full);
transition!(c, "full" => "empty", full_to_empty);
// The actual transition constraint functions. Note that these functions only illustrate some
// possibilities of error checks and don't check all invariants.
fn empty_to_full(init_ctx: &Context,
final_ctx: &Context,
msg: &BowlMsg,
_output: &Vec<StoreReq>) -> Result<(), String>
{
let s = "Transition from empty to full";
check!(s, init_ctx.contents == 0);
check!(s, final_ctx.contents == 100);
check!(s, match *msg {
BowlMsg::StoreRpy(_) => true,
BowlMsg::CatMsg(CatMsg::Meow) => true,
_ => false
});
Ok(())
}
fn full_to_empty(init_ctx: &Context,
final_ctx: &Context,
msg: &BowlMsg,
_output: &Vec<StoreReq>) -> Result<(), String>
{
let s = "Transition from full to empty";
check!(s, init_ctx.contents > 0);
check!(s, final_ctx.contents == 0);
check!(s, { if let BowlMsg::CatMsg(CatMsg::Eat(_)) = *msg {
true
} else {
false
}});
Ok(())
}
现在我们已经定义了测试约束,我们如何使用这些约束来验证我们的fsm呢?我们使用一个Checker
对象。该Checker
检查先决条件,向fsm发送消息,然后检查其不变性和转换约束。在成功的情况下,它返回一个Ok()
,在失败的情况下,返回一个包含失败检查的字符串化版本、行号和文件名的错误字符串。
推荐生成消息的方式是使用 Quickcheck
,但暂时我们只使用一个普通的静态消息列表。
use funfsm::fsm_check::Checker;
// The checker must be created with a new context and initial state of the FSM. The "c" in the third
// paramater is the `Constraints` object defined above.
let mut checker = Checker::<BowlTypes>::new(Context::new(), state_fn!(empty), c);
for msg in msgs {
assert_matches!(checker.check(msg), Ok(_));
}
就这样。你现在已经拥有了创建和测试 Fun FSM 所需要的一切!