#状态机 #状态 #机器 #future

state_machine_future

轻松从状态机创建类型安全的Future —— 无需样板代码

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异步

Download history 218/week @ 2024-03-28 162/week @ 2024-04-04 169/week @ 2024-04-11 176/week @ 2024-04-18 205/week @ 2024-04-25 171/week @ 2024-05-02 171/week @ 2024-05-09 174/week @ 2024-05-16 170/week @ 2024-05-23 178/week @ 2024-05-30 139/week @ 2024-06-06 175/week @ 2024-06-13 184/week @ 2024-06-20 121/week @ 2024-06-27 92/week @ 2024-07-04 104/week @ 2024-07-11

543 每月下载次数
12 个crate中(4个直接)使用

Apache-2.0/MIT

62KB
206

state_machine_future

Build Status

轻松从状态机创建类型安全的Future —— 无需样板代码。

state_machine_future会检查状态机和它们的状态转换,然后为你生成Future实现和类型状态0样板代码。

简介

大多数时候,使用Future组合器如mapthen来描述异步计算是一种很好的方式。有时,描述当前过程的最佳方式是一个状态机。

在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