diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 7b6a3d133..6f5fc7369 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(basic) add_subdirectory(doxygen) +add_subdirectory(petri) if (RPP_BUILD_SFML_CODE) add_subdirectory(sfml) diff --git a/src/examples/petri/CMakeLists.txt b/src/examples/petri/CMakeLists.txt new file mode 100644 index 000000000..d5426b0a2 --- /dev/null +++ b/src/examples/petri/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(petri_executor + petri.cpp +) + +target_link_libraries(petri_executor PRIVATE rpp) +set_target_properties(petri_executor PROPERTIES FOLDER Examples) diff --git a/src/examples/petri/petri.cpp b/src/examples/petri/petri.cpp new file mode 100644 index 000000000..cdb07a45a --- /dev/null +++ b/src/examples/petri/petri.cpp @@ -0,0 +1,90 @@ +// This is a small example in which a 'petri net executor' is implemented using Reactive +// Streams. Petri nets can be seen as a generalization of state machines and are +// particulary effective for modeling concurrent programs. See +// https://en.wikipedia.org/wiki/Petri_net for a brief introduction to Petri nets. + +#include + +#include + +using Transition = int; +using Transitions = std::vector; +using Place = char; +using Places = std::vector; +struct Mutations +{ + Places consume, produce; +}; +using PetriNet = std::map; +using Marking = std::map; +typedef void (*const TransitionFunction)(void); + +using namespace std::ranges; + +void foo() { std::cout << "foo" << std::endl; } +void bar() { std::cout << "bar" << std::endl; } + +// The state of a system is determined by its Petri Net (constant) and its marking (changes) +const PetriNet net = {{0, {{'a', 'b'}, {'c'}}}, {1, {{'c', 'c'}, {'b', 'b', 'd'}}}}; +const Marking marking_start = {{'a', 4}, {'b', 2}, {'c', 0}, {'d', 0}}; + +const std::map store = {{0, &foo}, {1, &bar}}; + +const auto& run_transition(Transition t) +{ + store.at(t)(); // calls the function associated with the transition + return net.at(t).produce; // contains the post marking mutations. +} + +std::pair mutate_marking(Marking&& marking, const Places& produce) +{ + // Produce tokens + for_each(produce, [&marking](const auto& place) { marking[place] += 1; }); + // Consume tokens by exeucting enabled transitions + Transitions transitions; + for (const auto& [transition, mutations] : net) + { + const auto& c = mutations.consume; + if (!c.empty() && + all_of(c, [&](const auto& place) { return marking[place] >= count(c, place); })) + { + for_each(c, [&marking](const auto& place) { marking[place] -= 1; }); + transitions.push_back(transition); + } + } + + return std::make_pair(marking, transitions); +} + +int main() +{ + // A subject is used to create a stream of 'transitions' that should be 'executed'. + const auto transitions_subject = rpp::subjects::publish_subject{}; + + // In our case, we have a finite program and we expect the program to end up in a + // particular marking. Hence we create a predicate that compares the marking to the + // expected (end) marking. + const Marking marking_end = {{'a', 0}, {'b', 2}, {'c', 0}, {'d', 2}}; + const auto end_predicate = [=](const auto& marking) + { return !std::equal(marking.begin(), marking.end(), marking_end.begin()); }; + + // Reactive streams lend themselves very nicely to create a 'petri net executor': + transitions_subject.get_observable() + .map(&run_transition) + .start_with(Places{}) // we start with an empty set + .observe_on(rpp::schedulers::trampoline{}) + .scan(marking_start, + [&transitions_subject](auto&& marking, const auto& places) + { + Transitions transitions; + std::tie(marking, transitions) = mutate_marking(std::forward(marking), places); + for_each(transitions, + [dispatcher = transitions_subject.get_subscriber()](const auto& t) + { dispatcher.on_next(t); }); + return std::forward(marking); + }) + .take_while(end_predicate) + .subscribe([](const auto&) {}, [] { std::cout << "done" << std::endl; }); + + return 0; +}