1

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>);
}
UpAndAdam
  • 4,515
  • 3
  • 28
  • 46
jlev
  • 21
  • 2
  • 2
    I have a lot of experience with statemodels, but having an event "go to state xyz" seems like a modeling issue. Transitions should be actions not descriptions of state destinations. I don't know the boost state engine but does it allow nesting of states? (Like Harel Statecharts) – Pepijn Kramer Aug 31 '23 at 18:05
  • Just looking at the docs, https://www.boost.org/doc/libs/1_72_0/libs/statechart/doc/tutorial.html and it seems nesting is allowed : you could put A,B,C in a parent state ABC and add a transition from that to D (this will allow exit from all child states based on one event) – Pepijn Kramer Aug 31 '23 at 18:07
  • @PepijnKramer In hindsight, "GoToX" was a poor choice of terms for the events -- was just meant to simplify the problem. Imagine it instead as "UserRequestsX", which may not be respected because MyStateMachine is in fact a sub-state machine of a larger one, and we're not in it; or there's a guard to transition to a state X that isn't satisfied. I'm not familiar with Harel Statecharts, though the library does allow nesting of state machines (i.e. sub-state machines), if that's what you're referring to? Though it's not clear to me how that could address the problem. – jlev Aug 31 '23 at 18:12
  • 1
    This is the paper [harel statecharts](https://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf) which also explains nesting. I don't know if this exact model is used by boost but it should get the idea across. Nesting helps to reduce complexity – Pepijn Kramer Aug 31 '23 at 18:15
  • @PepijnKramer wrong library (see here: https://boost-ext.github.io/sml/examples.html#composite) but your comment is understood nonetheless (this lib can do nesting as well). If A, B, C can all get to D via the same transition (the same event + destination pair), that would help. But if A, B, C, and D can all get to one another with the same event + destination pairs (as illustrated in my example SM above), I'm not seeing how a parent state of ABC would help. – jlev Aug 31 '23 at 18:17
  • The basic idea is that if you leave a parent state, it will also automatically exit the child states. – Pepijn Kramer Aug 31 '23 at 18:17
  • And indeed your example will not be handled by nesting... – Pepijn Kramer Aug 31 '23 at 18:18

1 Answers1

0

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.

  • It uses C++20 for convenience. It should be backportable to C++14 if necessary.
  • Implementing move semantics & perfect forwarding is left to the reader.

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();
    }
};

>> Live demo (godbolt)