10个版本
使用旧的Rust 2015
0.2.0 | 2018年11月10日 |
---|---|
0.1.8 | 2018年10月21日 |
0.1.7 | 2018年6月4日 |
0.1.6 | 2018年2月12日 |
0.1.4 | 2017年12月19日 |
#439 在 异步
543 每月下载次数
在 12 个crate中(4个直接)使用
62KB
206 行
state_machine_future
轻松从状态机创建类型安全的Future
—— 无需样板代码。
state_machine_future
会检查状态机和它们的状态转换,然后为你生成Future
实现和类型状态0样板代码。
简介
大多数时候,使用Future
组合器如map
和then
来描述异步计算是一种很好的方式。有时,描述当前过程的最佳方式是一个状态机。
在Rust中编写状态机时,我们希望利用类型系统来强制只发生有效的状态转换。为了做到这一点,我们希望有类型状态0:表示状态机中每个状态的类型,以及签名只允许有效状态转换的方法。但我们也需要每个可能状态的enum
,这样我们可以将整个状态机视为单个实体,并为其实现Future
。但这将导致大量的样板代码...
请看 #[derive(StateMachineFuture)]
。
使用 #[derive(StateMachineFuture)]
,我们描述状态及其之间的可能转换,然后自定义 derive 生成
-
状态机中每个状态的类型状态。
-
一种用于实现
Future
的整体状态机的类型。 -
一个具体的
start
方法,为您构造状态机的Future
,初始化为起始状态。 -
一个状态转换轮询特质,为每个非最终状态
ZeeChoo
提供一个poll_zee_choo
方法。此特质描述了状态机的有效转换,其方法由Future::poll
调用。
然后,我们只需要实现生成的状态转换轮询特质。
此外,#[derive(StateMachineFuture)]
将静态防止在编写状态机时可能出现的某些陷阱。
-
每个状态都是从起始状态可达的:没有无用的状态。
-
没有无法到达最终状态的状态。这些状态否则会导致无限循环。
-
所有状态转换都是有效的。由于生成的类型状态,尝试执行无效的状态转换将无法通过类型检查。
指南
使用 enum
描述状态机的状态,并向其添加 #[derive(StateMachineFuture)]
。
#[derive(StateMachineFuture)]
enum MyStateMachine {
// ...
}
必须有一个 起始 状态,它是构造时的初始状态;一个 就绪 状态,对应于 Future::Item
;以及一个 错误 状态,对应于 Future::Error
。
#[derive(StateMachineFuture)]
enum MyStateMachine {
#[state_machine_future(start)]
Start,
// ...
#[state_machine_future(ready)]
Ready(MyItem),
#[state_machine_future(error)]
Error(MyError),
}
枚举的任何其他变体都是中间状态。
我们使用 #[state_machine_future(transitions(...))]
定义哪些状态到状态转换是有效的。此属性注解了一个状态变体,并列出在此状态之后可以立即转换到的其他状态。
最终状态(无论是 就绪 还是 错误)必须从每个中间状态和 起始 状态可达。最终状态不允许有转换。
#[derive(StateMachineFuture)]
enum MyStateMachine {
#[state_machine_future(start, transitions(Intermediate))]
Start,
#[state_machine_future(transitions(Start, Ready))]
Intermediate { x: usize, y: usize },
#[state_machine_future(ready)]
Ready(MyItem),
#[state_machine_future(error)]
Error(MyError),
}
从这个状态机描述中,自定义 derive 为我们生成样板代码。
对于每个状态,自定义 derive 创建
- 状态的状态类型。类型的名称与变体名称匹配,例如
Intermediate
状态变体的状态类型也命名为Intermediate
。生成的结构类型类型与变体类型匹配:单元样式变体产生单元结构体,元组样式变体产生元组结构体,结构样式变体产生带有字段的正常结构体。
状态 enum 变体 |
生成的状态类型 |
---|---|
enum 状态机 {我的状态, ... } |
结构 我的状态; |
enum 状态机 {我的状态(bool, usize), ... } |
结构 我的状态(bool, usize); |
enum 状态机 {我的状态{x: usize }, ... } |
结构 我的状态 { x: usize }; |
- 表示此状态之后可能出现的状态的枚举。此枚举名为
AfterX
,其中X
为状态名称。对于每个可以从X
状态转换到的Y
状态,都有一个From<Y>
实现。例如,Intermediate
状态将得到
enum AfterIntermediate {
Start(Start),
Ready(Ready),
}
impl From<Start> for AfterIntermediate {
// ...
}
impl From<Ready> for AfterIntermediate {
// ...
}
接下来,对于整个状态机,自定义 derive 生成
-
一个名为
Future
的状态机类型,实际上是一个包含所有不同类型状态的枚举。此类型名为BlahFuture
,其中Blah
是状态机描述枚举的名称。在本例中,状态机描述名为MyStateMachine
,生成的状态机未来类型将命名为MyStateMachineFuture
。 -
一个轮询特质,
PollBordle
,其中Bordle
是此状态机描述的名称。对于每个非最终状态TootWasabi
,此特质有一个方法,poll_toot_wasabi
,类似于Future::poll
,但针对当前状态进行了特殊化。每个方法都对其状态进行条件所有权的获取(通过RentToOwn
),并返回一个futures::Poll<AfterThisState, Error>
,其中Error
是状态机的错误类型。此签名 不允许无效状态转换,这使得尝试非法状态转换无法通过类型检查。以下是一个MyStateMachine
的轮询特质的示例
trait PollMyStateMachine {
fn poll_start<'a>(
start: &'a mut RentToOwn<'a, Start>,
) -> Poll<AfterStart, Error>;
fn poll_intermediate<'a>(
intermediate: &'a mut RentToOwn<'a, Intermediate>,
) -> Poll<AfterIntermediate, Error>;
}
-
为此类型实现的
Future
。此实现根据未来所处的状态将任务调度到相应的轮询特质方法-
如果
Future
处于Start
状态,则它使用<MyStateMachine as PollMyStateMachine>::poll_start
。 -
如果它在
Intermediate
状态,则它使用<MyStateMachine as PollMyStateMachine>::poll_intermediate
。 -
等等...
-
-
描述类型的具体
start
方法(例如本例中的MyStateMachine::start
),它为您在 start 状态中构建一个新的状态机Future
类型。此方法有一个参数,用于 start 状态的每个字段。
开始 enum 变体 |
生成的 start 方法 |
---|---|
MyStart, |
fn start() ->MyStateMachineFuture { ... } |
MyStart(bool, usize), |
fn start(arg0: bool, arg1: usize) ->MyStateMachineFuture { ... } |
MyStart{x: char,y: bool }, |
fn start(x: char, y: bool) ->MyStateMachineFuture { ... } |
有了所有这些生成的类型和特质,我们只需要为我们的状态机 Blah
实现以下内容:impl PollBlah for Blah
。
impl PollMyStateMachine for MyStateMachine {
fn poll_start<'a>(
start: &'a mut RentToOwn<'a, Start>
) -> Poll<AfterStart, MyError> {
// Call `try_ready!(start.inner.poll())` with any inner futures here.
//
// If we're ready to transition states, then we should return
// `Ok(Async::Ready(AfterStart))`. If we are not ready to transition
// states, return `Ok(Async::NotReady)`. If we encounter an error,
// return `Err(...)`.
}
fn poll_intermediate<'a>(
intermediate: &'a mut RentToOwn<'a, Intermediate>
) -> Poll<AfterIntermediate, MyError> {
// Same deal as above...
}
}
上下文
状态机还允许传入上下文,该上下文在所有 poll_*
方法中都是可用的,无需在每一个方法中显式包含它。
可以通过 state_machine_future
属性的 context
参数来指定上下文。这将向 start
方法以及特质中每个 poll_*
方法添加参数。
#[macro_use]
extern crate state_machine_future;
extern crate futures;
use futures::*;
use state_machine_future::*;
struct MyContext {
}
struct MyItem {
}
enum MyError {
}
#[derive(StateMachineFuture)]
#[state_machine_future(context = "MyContext")]
enum MyStateMachine {
#[state_machine_future(start, transitions(Intermediate))]
Start,
#[state_machine_future(transitions(Start, Ready))]
Intermediate { x: usize, y: usize },
#[state_machine_future(ready)]
Ready(MyItem),
#[state_machine_future(error)]
Error(MyError),
}
impl PollMyStateMachine for MyStateMachine {
fn poll_start<'s, 'c>(
start: &'s mut RentToOwn<'s, Start>,
context: &'c mut RentToOwn<'c, MyContext>
) -> Poll<AfterStart, MyError> {
// The `context` instance passed into `start` is available here.
// It is a mutable reference, so are free to modify it.
unimplemented!()
}
fn poll_intermediate<'s, 'c>(
intermediate: &'s mut RentToOwn<'s, Intermediate>,
context: &'c mut RentToOwn<'c, MyContext>
) -> Poll<AfterIntermediate, MyError> {
// The `context` is available here as well.
// It is the same instance. This means if `poll_start` modified it, those
// changes will be visible to this method as well.
unimplemented!()
}
}
fn main() {
let _ = MyStateMachine::start(MyContext { });
}
与状态参数相同,上下文也可以通过 RentToOwn
类型获取!但是请注意,一旦获取了上下文,状态机将 始终 返回 Async::NotReady
,而不再调用 poll_
方法。
这就完了!
示例
下面是一个通过 HTTP 在两个玩家之间进行的简单回合制游戏的示例。
#[macro_use]
extern crate state_machine_future;
#[macro_use]
extern crate futures;
use futures::{Async, Future, Poll};
use state_machine_future::RentToOwn;
/// The result of a game.
pub struct GameResult {
winner: Player,
loser: Player,
}
/// Some kind of simple turn based game.
///
/// ```text
/// Invite
/// |
/// |
/// | accept invitation
/// |
/// |
/// V
/// WaitingForTurn --------+
/// | ^ |
/// | | | receive turn
/// | | |
/// | +-------------+
/// game concludes |
/// |
/// |
/// |
/// V
/// Finished
/// ```
#[derive(StateMachineFuture)]
enum Game {
/// The game begins with an invitation to play from one player to another.
///
/// Once the invited player accepts the invitation over HTTP, then we will
/// switch states into playing the game, waiting to recieve each turn.
#[state_machine_future(start, transitions(WaitingForTurn))]
Invite {
invitation: HttpInvitationFuture,
from: Player,
to: Player,
},
// We are waiting on a turn.
//
// Upon receiving it, if the game is now complete, then we go to the
// `Finished` state. Otherwise, we give the other player a turn.
#[state_machine_future(transitions(WaitingForTurn, Finished))]
WaitingForTurn {
turn: HttpTurnFuture,
active: Player,
idle: Player,
},
// The game is finished with a `GameResult`.
//
// The `GameResult` becomes the `Future::Item`.
#[state_machine_future(ready)]
Finished(GameResult),
// Any state transition can implicitly go to this error state if we get an
// `HttpError` while waiting on a turn or invitation acceptance.
//
// This `HttpError` is used as the `Future::Error`.
#[state_machine_future(error)]
Error(HttpError),
}
// Now, we implement the generated state transition polling trait for our state
// machine description type.
impl PollGame for Game {
fn poll_invite<'a>(
invite: &'a mut RentToOwn<'a, Invite>
) -> Poll<AfterInvite, HttpError> {
// See if the invitation has been accepted. If not, this will early
// return with `Ok(Async::NotReady)` or propagate any HTTP errors.
try_ready!(invite.invitation.poll());
// We're ready to transition into the `WaitingForTurn` state, so take
// ownership of the `Invite` and then construct and return the new
// state.
let invite = invite.take();
let waiting = WaitingForTurn {
turn: invite.from.request_turn(),
active: invite.from,
idle: invite.to,
};
transition!(waiting)
}
fn poll_waiting_for_turn<'a>(
waiting: &'a mut RentToOwn<'a, WaitingForTurn>
) -> Poll<AfterWaitingForTurn, HttpError> {
// See if the next turn has arrived over HTTP. Again, this will early
// return `Ok(Async::NotReady)` if the turn hasn't arrived yet, and
// propagate any HTTP errors that we might encounter.
let turn = try_ready!(waiting.turn.poll());
// Ok, we have a new turn. Take ownership of the `WaitingForTurn` state,
// process the turn and if the game is over, then transition to the
// `Finished` state, otherwise swap which player we need a new turn from
// and request the turn over HTTP.
let waiting = waiting.take();
if let Some(game_result) = process_turn(turn) {
transition!(Finished(game_result))
} else {
let next_waiting = WaitingForTurn {
turn: waiting.idle.request_turn(),
active: waiting.idle,
idle: waiting.active,
};
Ok(Async::Ready(next_waiting.into()))
}
}
}
// To spawn a new `Game` as a `Future` on whatever executor we're using (for
// example `tokio`), we use `Game::start` to construct the `Future` in its start
// state and then pass it to the executor.
fn spawn_game(handle: TokioHandle) {
let from = get_some_player();
let to = get_another_player();
let invitation = invite(&from, &to);
let future = Game::start(invitation, from, to);
handle.spawn(future)
}
属性
这是所有使用 state_machine_future
的属性的列表
-
#[derive(StateMachineFuture)]
:放置在描述状态机的enum
上。 -
#[state_machine_future(derive, Clone, Debug, ...))]
:放置在描述状态机的enum
上。此属性描述了要在生成的Future
类型上放置哪些#[derive(...)]
。 -
#[state_machine_future(start)]
:用于状态机描述enum
的变体。必须有且仅有一个具有此属性的变体。这描述了初始起始状态。生成的start
方法具有参数,每个参数对应于此变体中的每个字段。 -
#[state_machine_future(ready)]
:用于状态机描述enum
的变体。必须有且仅有一个具有此属性的变体。它必须是一个具有一个字段的元组样式变体,例如Ready(MyItemType)
。生成的Future
实现使用字段的类型作为Future::Item
。 -
#[state_machine_future(error)]
:在状态机描述的变体(enum
)中使用。必须恰好有一个带有此属性的变体。它必须是一个只有一个字段的元组风格的变体,例如Error(MyError)
。生成的Future
实现使用字段的类型作为Future::Error
。 -
#[state_machine_future(transitions(OtherState, AnotherState, ...))]
:在状态机描述的变体(enum
)中使用。描述了该状态可以转换到的状态。
宏
提供了一个辅助宏,有助于减少状态转换的样板代码。因此,以下代码
Ok(Ready(NextState(1).转换为()))
可以简化为
transition!(NextState(1))
特性
以下是可以启用的 cargo
功能
debug_code_generation
:将#[derive(StateMachineFuture)]
生成的代码打印到stdout
,用于调试目的。
许可证
根据您的选择,许可方式如下
。
贡献
有关黑客攻击,请参阅 CONTRIBUTING.md。
除非您明确声明,否则任何有意提交以包含在您的工作中的贡献,根据 Apache-2.0 许可证的定义,应按上述方式双许可,不附加任何其他条款或条件。
依赖关系
~4MB
~78K SLoC