3个不稳定版本

使用旧的Rust 2015

0.2.1 2017年1月12日
0.2.0 2017年1月11日
0.1.0 2016年12月16日

算法类别中排名第1773

Apache-2.0

20KB
192

Build Status

API文档

描述

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 所需要的一切!

无运行时依赖