#状态机 #Future #状态机Future

derive_state_machine_future

state_machine_future crate 提供自定义 derive 实现。直接使用该 crate 而不是直接使用此实现。

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过程宏 中排名

Download history 229/week @ 2023-11-26 161/week @ 2023-12-03 215/week @ 2023-12-10 228/week @ 2023-12-17 124/week @ 2023-12-24 47/week @ 2023-12-31 205/week @ 2024-01-07 239/week @ 2024-01-14 248/week @ 2024-01-21 85/week @ 2024-01-28 88/week @ 2024-02-04 131/week @ 2024-02-11 173/week @ 2024-02-18 155/week @ 2024-02-25 179/week @ 2024-03-03 70/week @ 2024-03-10

596 每月下载量
13 个 crate 中使用(通过 state_machine_future

Apache-2.0/MIT

69KB
1.5K SLoC

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),
}

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