-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add variant-type-driven state machine to libs (#12223)
* Add variant-type-driven state machine to libs This commit introduces a functional approach for State Machine implementation that leverages the sdk's Variant and Optional types. By providing a clean separation between states, events, and transitions, state machines can be implemented in such a way that states and events may be provided in the core sdk, but with transitions defined in consuming applications to support individual use cases. And because states and events are associated to the state machine by inclusion in variants and not by inheritance, applications may also extend state machines with arbitrary, application-specific events and states. Co-Authored-by: Bill Schiller <bill.schiller@smartthings.com> * fix Wshadow * plausible simplified approach * Revert "plausible simplified approach" This reverts commit 93227b1. * Use preorder recursion to remove need for Dispatch queue * cleanup test state machine state construction * Add guardrails to disallow Dispatch from illegal contexts The following are made explicitly illegal and will abort: * Dispatch from Exit() or LogTransition() state methods * Dispatch from transitions table w/ returned transition * make StateMachine members variables private Co-authored-by: Bill Schiller <bill.schiller@smartthings.com>
- Loading branch information
Showing
5 changed files
with
499 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
/* | ||
* Copyright (c) 2021 Project CHIP Authors | ||
* Copyright (c) 2021 SmartThings | ||
* All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include <lib/core/Optional.h> | ||
#include <lib/support/Variant.h> | ||
|
||
namespace chip { | ||
namespace StateMachine { | ||
|
||
/** | ||
* An extension of the Optional class that removes the explicit requirement | ||
* for construction from a T value as a convenience to allow auto construction | ||
* of Optional<T>. | ||
*/ | ||
template <class T> | ||
class Optional : public chip::Optional<T> | ||
{ | ||
public: | ||
Optional(const T & value) : chip::Optional<T>(value) {} | ||
Optional() : chip::Optional<T>() {} | ||
}; | ||
|
||
/** | ||
* An extension of the Variant class offering pattern matching of State types | ||
* to dynamically dispatch execution of the required State interface methods: | ||
* Enter, Exit, GetName, LogTtransition. | ||
*/ | ||
template <typename... Ts> | ||
struct VariantState : Variant<Ts...> | ||
{ | ||
|
||
private: | ||
template <typename T> | ||
void Enter() | ||
{ | ||
if (chip::Variant<Ts...>::template Is<T>()) | ||
{ | ||
chip::Variant<Ts...>::template Get<T>().Enter(); | ||
} | ||
} | ||
|
||
template <typename T> | ||
void Exit() | ||
{ | ||
if (chip::Variant<Ts...>::template Is<T>()) | ||
{ | ||
chip::Variant<Ts...>::template Get<T>().Exit(); | ||
} | ||
} | ||
|
||
template <typename T> | ||
void GetName(const char ** name) | ||
{ | ||
if (name && chip::Variant<Ts...>::template Is<T>()) | ||
{ | ||
*name = chip::Variant<Ts...>::template Get<T>().GetName(); | ||
} | ||
} | ||
|
||
template <typename T> | ||
void LogTransition(const char * previous) | ||
{ | ||
if (chip::Variant<Ts...>::template Is<T>()) | ||
{ | ||
chip::Variant<Ts...>::template Get<T>().LogTransition(previous); | ||
} | ||
} | ||
|
||
public: | ||
template <typename T, typename... Args> | ||
static VariantState<Ts...> Create(Args &&... args) | ||
{ | ||
VariantState<Ts...> instance; | ||
instance.template Set<T>(std::forward<Args>(args)...); | ||
return instance; | ||
} | ||
|
||
void Enter() | ||
{ | ||
[](...) {}((this->template Enter<Ts>(), 0)...); | ||
} | ||
|
||
void Exit() | ||
{ | ||
[](...) {}((this->template Exit<Ts>(), 0)...); | ||
} | ||
|
||
const char * GetName() | ||
{ | ||
const char * name = nullptr; | ||
[](...) {}((this->template GetName<Ts>(&name), 0)...); | ||
return name; | ||
} | ||
|
||
void LogTransition(const char * previous) | ||
{ | ||
[](...) {}((this->template LogTransition<Ts>(previous), 0)...); | ||
} | ||
}; | ||
|
||
/** | ||
* The interface for dispatching events into the State Machine. | ||
* @tparam TEvent a variant holding the Events for the State Machine. | ||
*/ | ||
template <typename TEvent> | ||
class Context | ||
{ | ||
public: | ||
virtual ~Context() = default; | ||
|
||
/** | ||
* Dispatch an event to the current state. | ||
* @param evt a variant holding an Event for the State Machine. | ||
*/ | ||
virtual void Dispatch(const TEvent & evt) = 0; | ||
}; | ||
|
||
/** | ||
* This is a functional approach to the State Machine design pattern. The design is | ||
* borrowed from http://www.vishalchovatiya.com/state-design-pattern-in-modern-cpp | ||
* and extended for this application. | ||
* | ||
* At a high-level, the purpose of a State Machine is to switch between States. Each | ||
* State handles Events. The handling of Events may lead to Transitions. The purpose | ||
* of this design pattern is to decouple States, Events, and Transitions. For instance, | ||
* it is desirable to remove knowledge of next/previous States from each individual | ||
* State. This allows adding/removing States with minimal code change and leads to a | ||
* simpler implementation. | ||
* | ||
* This State Machine design emulates C++17 features to achieve the functional approach. | ||
* Instead of using an enum or inheritance for the Events, the Events are defined as | ||
* structs and placed in a variant. Likewise, the States are all defined as structs and | ||
* placed in a variant. With the Events and States in two different variants, the | ||
* Transitions table uses the type introspction feature of the variant object to match a | ||
* given state and event to an optional new-state return. | ||
* | ||
* For event dispatch, the State Machine implements the Context interface. The Context | ||
* interface is passed to States to allow Dispatch() of events when needed. | ||
* | ||
* The State held in the TState must provide four methods to support calls from | ||
* the State Machine: | ||
* @code | ||
* struct State { | ||
* void Enter() { } | ||
* void Exit() { } | ||
* void LogTransition(const char *) { } | ||
* const char *GetName() { return ""; } | ||
* } | ||
* @endcode | ||
* | ||
* The TTransitions table type is implemented with an overloaded callable operator method | ||
* to match the combinations of State / Event variants that may produce a new-state return. | ||
* This allows the Transition table to define how each State responds to Events. Below is | ||
* an example of a Transitions table implemented as a struct: | ||
* | ||
* @code | ||
* struct Transitions { | ||
* using State = chip::StateMachine::VariantState<State1, State2>; | ||
* chip::StateMachine::Optional<State> operator()(State &state, Event &event) | ||
* { | ||
* if (state.Is<State1>() && event.Is<Event2>()) | ||
* { | ||
* return State::Create<State2>(); | ||
* } | ||
* else if (state.Is<State2>() && event.Is<Event1>()) | ||
* { | ||
* return State::Create<State1>(); | ||
* } | ||
* else | ||
* { | ||
* return {} | ||
* } | ||
* } | ||
* } | ||
* @endcode | ||
* | ||
* The rules for calling Dispatch from within the state machien are as follows: | ||
* | ||
* (1) Only the State::Enter method should call Dispatch. Calls from Exit or | ||
* LogTransition will cause an abort. | ||
* (2) The transitions table may return a new state OR call Dispatch, but must | ||
* never do both. Doing both will cause an abort. | ||
* | ||
* @tparam TState a variant holding the States. | ||
* @tparam TEvent a variant holding the Events. | ||
* @tparam TTransitions an object that implements the () operator for transitions. | ||
*/ | ||
template <typename TState, typename TEvent, typename TTransitions> | ||
class StateMachine : public Context<TEvent> | ||
{ | ||
public: | ||
StateMachine(TTransitions & tr) : mCurrentState(tr.GetInitState()), mTransitions(tr), mSequence(0) {} | ||
~StateMachine() override = default; | ||
void Dispatch(const TEvent & evt) override | ||
{ | ||
++mSequence; | ||
auto prev = mSequence; | ||
auto newState = mTransitions(mCurrentState, evt); | ||
if (newState.HasValue()) | ||
{ | ||
auto oldState = mCurrentState; | ||
oldState.Exit(); | ||
mCurrentState = newState.Value(); | ||
mCurrentState.LogTransition(oldState.GetName()); | ||
// It is impermissible to dispatch events from Exit() or | ||
// LogTransition(), or from the transitions table when a transition | ||
// has also been returned. Verify that this hasn't occured. | ||
VerifyOrDie(prev == mSequence); | ||
mCurrentState.Enter(); | ||
} | ||
} | ||
TState GetState() { return mCurrentState; } | ||
|
||
private: | ||
TState mCurrentState; | ||
TTransitions & mTransitions; | ||
unsigned mSequence; | ||
}; | ||
|
||
} // namespace StateMachine | ||
} // namespace chip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.