c++c++14state-machineboost-extensionboost-sml

Boost SML: respond to determination made in action


I am trying to use a Boost SML state machine to implement a "receiver". As an example, lets say the SM receives ints and is "done" when it gets to a certain number:

I have done this so far with a guard, gNotDone, which returns true if the capacity is not reached.

And then while reading, having passed the guard, actually process the data in the aReceive action.

#include <boost/sml.hpp>
#include <iostream>

namespace sml = boost::sml;

namespace {
struct eReceive {
    int data;
};

struct sIdle{};
struct sReading{};

struct Context {

    Context(const std::string& name, unsigned capacity):
        name(name),
        received(0),
        capacity(capacity) {
    }

    std::string name;
    unsigned received;
    unsigned capacity;
};

struct SM {
    auto operator()() const noexcept {
        using namespace sml;

        const auto gNotDone = [this] (const Context& ctx) -> bool {
            return ctx.received < ctx.capacity;
        };

        const auto aReceive = [this] (const eReceive& e, Context& ctx) {
            ctx.received += e.data;
            std::cout << "Received " << e.data << ", so far: " << ctx.received << std::endl;
        };

        const auto aDone = [] {
            std::cout << "Done (reach capacity)" << std::endl;
        };

        // clang-format off
        return make_transition_table(
                // Start reading
               *state<sIdle>        + event<eReceive> [gNotDone] / aReceive     = state<sReading>
               // Keeps reading until "done"
            ,   state<sReading>     + event<eReceive> [gNotDone] / aReceive
            ,   state<sReading>     + event<eReceive> / aDone                = X
        );
        // clang-format on
    }
};
}  // namespace

int main() {

    Context theContext("The context", 30);
    sml::sm<SM> sm{theContext};

    sm.process_event(eReceive{11});
    sm.process_event(eReceive{22});
}

Try it here.

Received 11, so far: 11
Received 22, so far: 33

However, I don't feel this is quite right: it feels like the next state should somehow be determined by the aReceive action - if the limit was passed, move into the terminal state.

As it is, you'll only move into the terminal state after the next receive event.

It also doesn't feel right to "test" in guards if the event will pass the limit as this probably entails repeating the calculation (it's just a sum here, but it could be expensive).

What is the standard way in an SML state machine to decide the next state based on what happened with an event in the current state?


Solution

  • I think that your state machine can described as follows:

    UML Statemachine DIagram

    sml doesn't support choice puseudo state (rhombus shape in the diagram) but we can use "normal" state instead.

    The diagram use if/else branch from the choice pseudo state. But it doesn't supported in sml.

    So I define gDone and gNotDone. It is the similar as defining == and != operators for the C++ class.

    The code is updated as follows:

    
    struct eReceive {
        int data;
    };
    
    struct sReading{};
    // choice pseudo state (UML 2.x)
    // sml doesn't support choice pseudo state directly
    // but we can use (normal) state.
    struct psChoice{}; 
    
    struct Context {
    
        Context(const std::string& name, unsigned capacity):
            name(name),
            received(0),
            capacity(capacity) {
        }
    
        std::string name;
        unsigned received;
        unsigned capacity;
    };
    
    //              eReceive/aReceive           [gDone]/aDone
    // *-->sReading------------------->psChoice--------------->X
    //         A                         |
    //         |                         | [else] (=gNotDone)
    //         |                         |
    //         +-------------------------+                            
    //
    struct SM {
        auto operator()() const noexcept {
            using namespace sml;
    
            const auto gDone = [] (const Context& ctx) -> bool {
                return ctx.received >= ctx.capacity;
            };
            // sml doesn't support "else" guard. So I define it.
            const auto gNotDone = [gDone] (const Context& ctx) -> bool {
                return !gDone(ctx);
            };
    
            const auto aReceive = [] (const eReceive& e, Context& ctx) {
                ctx.received += e.data;
                std::cout << "Received " << e.data << ", so far: " << ctx.received << std::endl;
            };
    
            const auto aDone = [] {
                std::cout << "Done (reach capacity)" << std::endl;
            };
    
            // clang-format off
            return make_transition_table(
                    // From state      Event           Guard       Action     To state
                   *state<sReading>  + event<eReceive>           / aReceive = state<psChoice>
                ,   state<psChoice>                   [gNotDone]            = state<sReading>
                ,   state<psChoice>                   [gDone   ] / aDone    = X
            );
            // clang-format on
        }
    };
    

    All code is at https://cpp.godbolt.org/z/Mc9nYzMcr