Reputation: 31
I am creating a state machine with the Boost::Ext SML library. I have a number of states (A, B, C, D...) and in most cases, states will transition to another subject to common events. For instance, if in state A or B or C and the event "GoToD" is processed, the state will transition to D. To my knowledge, there is no way to encode this commonality using the aforementioned library, and as my number of states increases, my transition table becomes very large and fragile. It's already looking something like this:
struct MyStateMachine {
auto operator()() const noexcept {
return sml::make_transition_table(
*sml::state<A> + sml::event<GoToB> = sml::state<B>,
sml::state<A> + sml::event<GoToC> = sml::state<C>,
sml::state<A> + sml::event<GoToD> = sml::state<D>,
sml::state<B> + sml::event<GoToA> = sml::state<A>,
sml::state<B> + sml::event<GoToC> = sml::state<C>,
sml::state<B> + sml::event<GoToD> = sml::state<D>,
sml::state<C> + sml::event<GoToA> = sml::state<A>,
sml::state<C> + sml::event<GoToB> = sml::state<B>,
sml::state<C> + sml::event<GoToD> = sml::state<D>,
sml::state<D> + sml::event<GoToA> = sml::state<A>,
sml::state<D> + sml::event<GoToB> = sml::state<B>,
sml::state<D> + sml::event<GoToC> = sml::state<C>,
}
};
There are nuances I've omitted above (there are some guards on common transitions, for instance), but this gets to the root of the issue I'm encoutering. I'm wondering if there is either a programatic way to commonize these transitions, or a better way of thinking about the fundamental design of this state machine.
I have tried writing helper functions (see below) to encode common transitions, but I'm not sure how to do so such that they can be amalgamated into a single transition table using this library. I've also considered using pre-processor macros, but I don't like that solution.
template <typename From>
auto make_common_transitions() {
return sml::make_transition_table(sml::state<From> + sml::event<GoToA> = sml::state<A>,
sml::state<From> + sml::event<GoToB> = sml::state<B>,
sml::state<From> + sml::event<GoToC> = sml::state<C>,
sml::state<From> + sml::event<GoToD> = sml::state<D>);
}
Upvotes: 1
Views: 343
Reputation: 1457
This answer proposes to make a basic wrapper around the library using templates, to make a function similar to OP's auto make_common_transitions()
implementable.
The basic idea is to define a custom TransitionTable
class that wraps the parameters that will be passed to sml::make_transition_table
into a std::tuple
:
#include <tuple>
#include <utility>
#include <https://raw.githubusercontent.com/boost-ext/sml/master/include/boost/sml.hpp>
namespace sml = boost::sml;
// framework
template<typename T>
concept cTransitionable = sml::concepts::transitional<T>().value;
struct SmlTableMaker {
template<typename... BasicTransitions>
constexpr auto operator()(BasicTransitions&&... basicTransitions) {
return sml::make_transition_table(std::forward<BasicTransitions>(basicTransitions)...);
}
};
template<cTransitionable... Transitions>
class TransitionTable {
public:
constexpr TransitionTable(Transitions const&... basicTransitions)
: _basicTransitions{ basicTransitions... }
{}
explicit constexpr TransitionTable(std::tuple<Transitions...> const& basicTransitions)
: _basicTransitions{ basicTransitions }
{}
// concatenate 2 TransitionTable.
template<typename... RhsTransitions>
constexpr TransitionTable<Transitions..., RhsTransitions...> operator+(TransitionTable<RhsTransitions...> const& rhs) const& {
return TransitionTable<Transitions..., RhsTransitions...>(std::tuple_cat(_basicTransitions, rhs.basicTransitions()));
}
// tuple containing all the basic sml transition rules.
constexpr std::tuple<Transitions...> const& basicTransitions() const& {
return _basicTransitions;
}
// generate a sml transition table from the stored basic transitions.
constexpr auto makeSmlTable() const& {
return std::apply(SmlTableMaker{}, _basicTransitions);
}
private:
std::tuple<Transitions...> _basicTransitions;
};
The key API points:
TransitionTable + TransitionTable
concatenates 2 tables.TransitionTable::makeSmlTable()
creates the table required by sml's API.Now it's possible to define generic helper functions that generate one or several basic rules at a time:
// CTAD rules
template<cTransitionable... BasicTransitions>
TransitionTable(BasicTransitions...) -> TransitionTable<BasicTransitions...>;
// wraps a single sml transition rule into a TransitionTable
template<cTransitionable BasicTransition>
constexpr TransitionTable<BasicTransition> basicTransition(BasicTransition&& tr) {
return { std::forward<BasicTransition>(tr) };
}
// generates a TransitionTable with the rules 'src + event = destState' for each 'src' in 'srcStates'.
template<typename... SrcStates>
constexpr auto multiSourceTransition(std::tuple<SrcStates...> const& srcStates, auto event, auto destState) {
auto transformOne = [&destState,&event](auto src) {
return src + event = destState;
};
auto transformAll = [&transformOne](auto... src) {
return std::make_tuple(transformOne(src)...);
};
return TransitionTable{ std::apply(transformAll, srcStates) };
}
Usage:
struct MyStateMachine {
struct A {};
struct B {};
struct C {};
struct D {};
static constexpr auto a = sml::state<A>;
static constexpr auto b = sml::state<B>;
static constexpr auto c = sml::state<C>;
static constexpr auto d = sml::state<D>;
struct GoToA {};
struct GoToB {};
struct GoToC {};
struct GoToD {};
struct LoopA {};
static constexpr auto multiSources = std::make_tuple(a,b,c,d);
constexpr auto operator()() const {
constexpr auto trTable = basicTransition(*a + sml::event<LoopA> = a)
+ multiSourceTransition(multiSources, sml::event<GoToA>, a)
+ multiSourceTransition(multiSources, sml::event<GoToB>, b)
+ multiSourceTransition(multiSources, sml::event<GoToC>, c)
+ multiSourceTransition(multiSources, sml::event<GoToD>, d);
return trTable.makeSmlTable();
}
};
Upvotes: 0