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日 |
1542 在 过程宏 中排名
596 每月下载量
在 13 个 crate 中使用(通过 state_machine_future)
69KB
1.5K SLoC
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),
}
enum
的其他变体是中间状态。
我们使用 #[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 变体 |
生成的状态类型 |
---|---|
枚举 状态机 {MyState, ... } |
结构体 MyState; |
枚举 状态机 {MyState(布尔类型, 无符号整数类型), ... } |
结构体 MyState(布尔类型, 无符号整数类型); |
枚举 状态机 {MyState{x: 无符号整数类型 }, ... } |
结构体 MyState { x: 无符号整数类型 }; |
- 一个枚举类型,用于表示该状态之后可能出现的所有状态。此枚举类型命名为
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
),该方法为您构造一个新状态机的Future
类型,其处于 start 状态。此方法为 start 状态的每个字段都有一个参数。
Start enum 变体 |
生成的 start 方法 |
---|---|
MyStart, |
fn start() ->MyStateMachineFuture { ... } |
MyStart(布尔类型, 无符号整数类型), |
fn start(arg0: 布尔类型, arg1: 无符号整数类型) ->MyStateMachineFuture { ... } |
MyStart{x: 字符,y: 布尔类型 }, |
fn start(x: 字符, y: 布尔类型) ->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)]
:用于状态机描述枚举的一个变体。必须有恰好一个带有此属性的变体。这描述了初始启动状态。生成的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)]
生成的代码,以进行调试。
许可协议
根据您的选择,许可为以下之一
。
贡献
有关黑客信息,请参阅CONTRIBUTING.md。
除非您明确表示,否则根据 Apache-2.0 许可证定义的您提交的任何贡献,都应如上所述双重许可,不附加任何其他条款或条件。
依赖关系
~3.5MB
~75K SLoC