7 个版本

0.1.6 2021 年 9 月 2 日
0.1.5 2021 年 9 月 2 日
0.1.3 2021 年 8 月 31 日

#24#状态转换


2 crates 中使用

MIT/Apache

68KB
990

有限状态机

有限状态机可以看作是从 {S, E} 的二维空间到 {S} 的转换,其中 S 和 E 分别是与状态和事件相关的集合。Rust 提供了涵盖此模式的语法

  let next_state =
    match (state, event) {
      (StateA, Event1) => StateB,
      // and generally on:
      //(State#i, Event#j) => State#k,
      (_, _) => SomeVariant
    };

从程序员的视角来看,有限状态机可以被视为一个黑盒,它根据其当前状态对外部世界的事件做出反应。这些“反应”或转换是定义机器模板后需要编程的内容。这个 crate 以过程宏的形式尝试为这种模式提供一些糖。由于除了用户定义的状态上下文之外不涉及动态数据操作,因此每个转换应该与两次方法调用一样快。要使用它,请将以下内容包含在您的代码中

#[macro_use]
extern crate fsm;

定义机器类型的宏看起来是这样的

fsm!( UselessMachine [
  StateA on Signal1 => StateB,
  StateB on Signal2 => StateC,
  StateB on Signal3 => StateC,
  StateC on Signal2 => StateA,
  Idle on Start => StateA,
  _ on Reset => Idle,
]);

这等价于更紧凑的链式表示法

fsm!( UselessMachine [
  StateA on Signal1 => StateB on (Signal2 | Signal3) => StateC on Signal2 => StateA,
  _ on Reset => Idle on Start => StateA
]);

从这个声明中,宏将推断出以下内容

  pub enum UselessMachineStates {
    StateA,
    StateB,
    StateC,
    Idle,
  }

  pub enum UselessMachineSignals {
    Start,
    Reset,
    Signal1,
    Signal2,
    Signal3,
  }
  
  pub struct UselessMachine {
    ...
  }

我之所以称这个机器为“无用”,是因为它从外部世界接收的信息很少,无法产生有意义的反应,因为与其状态关联的用户数据为零。从这样的机器中获得有用信息的唯一方法是通过重复匹配其状态。

下面是如何扩展机器定义的示例

  // user data associated with state
 struct Count {
  a_count: usize,
  b_count: usize,
  other_count: usize,
}
// this will count letters only, discriminating A & B from the rest
// the rest of the states is introduced just for presentation purpose
fsm!( LetterCounter<Count> [
  Idle on Start => Started,
  _ on LetterA => IncA on Other(char) => OtherAfterA,
  _ on LetterB => IncB,
  _ on Other => IncOther,
  _ on (Number(u32) | Float(f64)) => NumberSt,
  _ on Reset => Idle,
]);

生成了一个名为 LetterCounter 的结构体,并关联了类型 Count。每个单独的信号都可以携带类型为括号中的类型输入。 (注意这里的细节:一旦声明了 Other(char),就不需要在声明中重复 (char) - 将在类型冲突时引发恐慌)。现在,由于我们缺少本质的东西 - 转换,所以这里是我们的声明中的第二部分 - 为每个箭头定义一个方法

// this reads: do sth when Start signal received while in Idle state
transition_handler!( Idle on Start for LetterCounter {
});
// this reads: do sth when LetterA signal received no matter in which actual state
from_any_state_transition_handler!(LetterA for LetterCounter {
  data.a_count += 1;
});
from_any_state_transition_handler!(LetterB for LetterCounter {
  data.b_count += 1;
});
from_any_state_transition_handler!(Other(char) for LetterCounter {
  data.other_count += 1;
  println!("Unrecognized counted: {}", input);
});
transition_handler!(IncA on Other(char) for LetterCounter {
  data.other_count += 1;
  println!("Unrecognized counted: {}", input);
});
from_any_state_transition_handler!(Reset for LetterCounter {
  data.a_count = 0;
  data.b_count = 0;
  data.other_count = 0;
});
from_any_state_transition_handler!(Number(u32) for LetterCounter {
  // do sth with Numbers
});
from_any_state_transition_handler!(Float(f64) for LetterCounter {
  // do sth with Floats
});

这些宏隐藏了两个引用:一个是通过 'data' 公开的与状态相关的数据的可变引用,另一个是与信号一起提供的不可变 'input'。

由于 LetterCounter 是一个结构体,如果您想的话可以扩展其 API

impl LetterCounter {
  pub fn start(&mut self) {
    self.signal(&Start);
  }
}

LetterCounter::signal(...) 方法封装了转换匹配器。

from_any_state_transition_handler!(Signal for Machine { // ... 
});

是等价表示法的替代方案

transition_handler!(_ on Signal for Machine { // ... 
});

现在定义实例

let mut counter = LetterCounter::new(Idle, Count { a_count: 0, b_count: 0, other_count: 0 });

并使用它

counter.start();
counter.signal(&LetterA);
counter.signal(&Other('x'));
counter.signal(&LetterA);
counter.signal(&LetterB);
counter.signal(&Number(8));
counter.signal(&LetterB);
counter.signal(&Other('x'));

let n = counter.data();
println!("counted: a -> {}, b -> {}, others -> {}", n.a_count, n.b_count, n.other_count);

在这个示例中,没有代码直接引用当前 fsm 的状态(尽管可以通过 state() 方法访问它)。

我认为当前状态是机器黑盒的一个实现细节,它并没有提供关于过去足够的信息,也就是说,不了解某个转换的上下文,知道当前状态并没有太大意义。

尽管如此,还有另一种打破黑盒的方法。考虑这个愚蠢的计数器需要区分进入特定状态时计数的字母超过5B的情况,我们可以称之为MoreThan5B。

可以这样做定义它

Started on LetterB => IncB_1 on LetterB => IncB_2 on LetterB... and so forth.

这被称为转换爆炸。

对于这种情况,还有一个替代的分支模式

 fsm!( LetterCounter<Count> [
    Idle on Start => Started,
    _ on LetterA => IncA on Other(char) => OtherAfterA,
    _ on LetterB => ?, // <-------- 
    _ on Other => IncOther,
    _ on Reset => Idle,
]);

这个疑问号声明了LetterCounter将结果状态的确定委托给了转换处理程序本身。而这里就是这个宏跳出来的地方

conditional_handler!(_ on LetterB for LetterCounter {  
  data.b_count += 1;

  if data.b_count < 5 {
    IncB
  } else {
    MoreThan5B
  }
});

现在,这不会编译,机器不知道IncB和MoreThan5B是什么,所以让我们在我们的机器中添加一些愚蠢的转换

 fsm!( LetterCounter<Count> [
    Idle on Start => Started on LetterB => IncB, // <-- here
    _ on LetterA => IncA on Other(char) => OtherAfterA,
     // and here
     MoreThan5B on LetterB => MoreThan5B,
    _ on LetterB => ?,
    _ on Other => IncOther,
    _ on Reset => Idle,
]);

以及它们相应的处理程序

transition_handler!( Started on LetterB for LetterCounter {
  data.b_count += 1;
});
transition_handler!( MoreThan5B on LetterB for LetterCounter {
 data.b_count += 1;
 println!("On MoreThan5B");
});

这种设计定义了一个松散的机器,因为它不会在没有对应当前状态的转换的情况下改变状态。

摩尔状态机

在定义一个动作以执行状态进入和/或退出,无论导致状态变化的转换是否存在时,这通常很有用。这可以用于初始化等常见场景,并可以节省一些代码重复。以下是机器声明语法的扩展,以涵盖此类情况

// using the previous example, take the reset code out of Transition handler to Idle State Entry handler
fsm!( LetterCounter<Count> [
  Idle on Start => Started,
  _ on LetterA => IncA on Other(char) => OtherAfterA,
  _ on LetterB => IncB,
  _ on Other => IncOther,
  _ on (Number(u32) | Float(f64)) => NumberSt,
  _ on Reset => Idle,
  ],
  [Idle entry, Start exit]
);
on_state_handler!( Idle entry for LetterCounter {
  data.a_count = 0;
  data.b_count = 0;
  data.other_count = 0;
});
on_state_handler!( Start exit for LetterCounter {
// do sth here
});

这个语法只识别两个词:'entry'和'exit'。

下压自动机

向方程引入了第三个变量,即堆栈,它与状态和输入信号一起提供了一个比常规状态机更强大的转换模型。例如,它能够解析嵌套模式。请注意,根据正式定义,信号和堆栈符号字母表不必相同。

在这个crate中,堆栈被实现为没有封装的标准Vec,而PDA堆栈的正式定义可能对允许的堆栈操作更为严格。所以这里是一个声明示例

    fsm!( LittleParser<ParserCtx, StackCtx> [ // ...
// state context ---------^          ^---------------- stack context (impls PartialEq) 
    // ...
    ]);

一旦声明如此,'stack'可变引用就可在转换处理程序中访问。这个堆栈可以被轮询、推送以及以任何方式操作,以确定状态转换的条件分支。

作为一个实验,我引入了转换签名扩展语法的扩展语法,以追求一些常见的确定性自动机模式,目前有

'cmp_pop expr' - 表示比较堆栈的顶部与给定的表达式,如果为真,则弹出它;

'cmp_pop_if_last expr' - 与上面相同,但仅当堆栈中只有一个项目时才应用;

'peek_cmp expr' - 比较堆栈的顶部元素与给定的表达式而不修改堆栈;

'pop' - 在转换时无条件地从堆栈中弹出;

'empty' - 堆栈为空的条件转换;

'!empty' - 上面的否定;

使用这种语法,可以在更复杂的条件下引入一系列确定性转换,这些条件直接编码在转换签名中,而不是在代码中。

例如

    fsm!( LittleParser<ParserCtx, StackCtx> [ 
        // code omitted ...
        // TextSegment is a State, RightBracket is a current symbol delivered with a Signal
        // and (Left)Bracket is a symbol that can possibly be on top of the Stack...
        // if so recognize it as a valid pattern and transtion to the same State 
        // then stack popping can be performed in the handler    
        TextSegment on RightBracket peek_cmp Bracket 
        // next possible situation is that we have the same State and Signal, but the Stack contains
        // other symbol so treat this as an unbalanced segment
        => TextSegment on RightBracket => InvalidSegment, 
        // more code omitted ...
  ]);

所以这里有两个不同的确定性转换,而不是一个条件分支

transition_handler!(TextSegment on RightBracket peek_cmp Bracket for LittleParser {
// ...
});
transition_handler!(TextSegment on RightBracket for LittleParser {
// ...
});

内部匹配是有序的,更具体的模式优先于更常见的模式,所以在上面的例子中,首先检查第一个条件,无论其在机器声明中的视觉位置如何(至少这是我的意图)。

meta_iter功能

该库定义了一个名为 "meta_iter" 的特性,当启用时,会将机器的迭代器引入作用域。请注意,没有动态集合可以迭代,这个迭代器在机器枚举声明产生的元数据上操作。这个特性向代码中添加了一些文本,仅用于调试时枚举转换,例如。结果可以发送到图形可视化软件或直接打印出来。

目前,只有FSM可以获得完整的输出,PDA将产生与FSM相同的输出。

它的工作原理是这样的

let mut chains = LetterCounter::chains();
while let Some(mut chain) = chains.next() {
 while let Some(item) = chain.next() {
   let s = if let Some(to) = item.to {
       format!("{} -> {} by {}\n", item.from, to, item.on)
     } else {
       format!("{} -> {} by {}\n", item.from, "ConditionalMultichoice", item.on)   
     };
     println!("{}", s);
 }
}

打印结果将是机器转换的列表。请注意,该列表打印结果几乎不可能与您在源代码中的机器布局视图一致。

fsm_gen_code 特性

当启用此特性时,将生成一个包含宏生成的Rust文本的字符串。字符串的格式是FSM名称与_GEN_CODE标识符连接。例如,对于MyMachine,可以通过以下方式访问该字符串:

MY_MACHINE_GEN_CODE 

常量。

注意事项

用户数据类型的生命周期解析未涵盖,因此这将无法编译

 fsm!( LetterCounter<WithLifetime<'a>> [...]);

依赖项

~3.5–4.5MB
~89K SLoC