diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0efc6b3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mode" +version = "0.1.0" +authors = ["Andrew Thomas Christensen "] +edition = "2018" + +description = "A behavioral state machine library written in Rust" + +documentation = "https://docs.rs/mode" +repository = "https://github.com/andrewtc/mode" + +readme = "README.md" + +keywords = ["state", "machine", "behavior", "behavioral"] +categories = ["rust-patterns", "simulation", "data-structures"] + +license = "Apache-2.0 OR MIT" + +[dependencies] \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..124519e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT license + +Copyright (c) 2019 Andrew Thomas Christensen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5c7b2f --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# mode +A simple and effective behavioral state machine library, written in in idiomatic, 100% safe, stable Rust code. +This library provides three main types, `Automaton`, `Mode`, and `Transition`, that facilitate the creation of +behavioral state machines. An `Automaton` can be used to quickly create a state machine over a set of `Mode`s that +implement some `Base` type. Each struct that implements `Mode` represents a distinct state in the state machine, an +the `Automaton` allows function calls to be dispatched to the current `Mode` by providing access to it as a `Base` +reference. A flexible `Transition` system provides a way for the current `Mode` to swap in a new state when it is +ready. The `Transition` system is designed such that the current `Mode` can move data from itself directly into the +`Mode` being created, which can help prevent spikes in memory usage while switching from one state to the next. + +## Example +```rust +use mode::*; + +// This trait will be used as the Base type for the Automaton, defining a common interface +// for all states. +trait Activity { + fn update(&mut self); +} + +// Each state in the state machine implements both Activity (the Base type) and Mode. +struct Working { + pub hours_worked : u32, +} + +impl Activity for Working { + fn update(&mut self) { self.hours_worked += 1; } +} + +impl Mode for Working { + type Base = Activity; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + + // This function allows the current Mode to swap to another Mode, when ready. + fn get_transition(&mut self) -> Option>> { + if self.hours_worked == 4 || self.hours_worked >= 8 { + // To swap to another Mode, a Transition function is returned, which will consume + // the current Mode and return a new Mode to be swapped in as active. + Some(Box::new(|previous : Self| { + Eating { hours_worked: previous.hours_worked, calories_consumed: 0 } + })) + } + else { None } // None means don't transition. + } +} + +struct Eating { + pub hours_worked : u32, + pub calories_consumed : u32, +} + +impl Activity for Eating { + fn update(&mut self) { self.calories_consumed += 100; } // Yum! +} + +impl Mode for Eating { + type Base = Activity; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + fn get_transition(&mut self) -> Option>> { + if self.calories_consumed >= 500 { + if self.hours_worked >= 8 { + // Time for bed! + Some(Box::new(|_ : Self| { Sleeping { hours_rested: 0 } })) + } + else { + // Time to go back to work! + Some(Box::new(|previous : Self| { + Working { hours_worked: previous.hours_worked } + })) + } + } + else { None } + } +} + +struct Sleeping { + pub hours_rested : u32, +} + +impl Activity for Sleeping { + fn update(&mut self) { self.hours_rested += 1; } // ZzZzZzZz... +} + +impl Mode for Sleeping { + type Base = Activity; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + fn get_transition(&mut self) -> Option>> { + if self.hours_rested >= 8 { + // Time for breakfast! + Some(Box::new(|_| { Eating { hours_worked: 0, calories_consumed: 0 } })) + } + else { None } + } +} + +fn main() { + let mut person = Automaton::with_initial_mode(Working { hours_worked: 0 }); + + for age in (18..100) { + // Update the current Mode for the Automaton. + person.borrow_mode_mut().update(); + + // Allow the Automaton to switch Modes. + person.perform_transitions(); + } +} +``` + +# License +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/andrewtc/mode/blob/master/LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](https://github.com/andrewtc/mode/blob/master/LICENSE-MIT) or + http://opensource.org/licenses/MIT) + +at your option. + +## Contributing +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as +defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/examples/counter.rs b/examples/counter.rs new file mode 100644 index 0000000..1b274d5 --- /dev/null +++ b/examples/counter.rs @@ -0,0 +1,144 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +extern crate mode; + +use mode::*; + +// Defines the public interface of all `Mode`s below. +trait CounterMode { + // Tells the `CounterMode` to update once. + fn update(&mut self); + + // Returns an `i32` if the program is finished and a final result has been returned. + fn get_result(&self) -> Option { None } +} + +// `CounterMode` that increments a counter value until it reaches the target value. +struct UpMode { + pub counter : i32, + pub target : i32, +} + +impl CounterMode for UpMode { + fn update(&mut self) { + // Increment the counter until it reaches the target value. + self.counter += 1; + println!("+ {}", self.counter); + } +} + +impl Mode for UpMode { + type Base = CounterMode; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + fn get_transition(&mut self) -> Option>> { + if self.counter == self.target { + // If we've reached the target value, start counting down to (roughly) the median value. + Some(Box::new( + |previous : Self| { + DownMode { + counter: previous.counter, + target: (previous.counter / 2) + 1, + } + })) + } + else { None } + } +} + +// `CounterMode` that decrements a counter value until it reaches the target value. +struct DownMode { + pub counter : i32, + pub target : i32, +} + +impl CounterMode for DownMode { + fn update(&mut self) { + // Decrement the counter until it reaches the target value. + self.counter -= 1; + println!("- {}", self.counter); + } +} + +impl Mode for DownMode { + type Base = CounterMode; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + fn get_transition(&mut self) -> Option>> { + const GOAL : i32 = 10; + if self.counter == GOAL { + // When we finally count down to the goal value, end the program by swapping in a "finished" state. + Some(Box::new( + |previous : Self| { + FinishedMode { + result: previous.counter, + } + })) + } + else if self.counter == self.target { + // If we've reached the target value, start counting up to double the counter value. + Some(Box::new( + |previous : Self| { + UpMode { + counter: previous.counter, + target: previous.counter * 2, + } + })) + } + else { None } + } +} + +// Represents that we've finished counting and have a final result. +struct FinishedMode { + result : i32, +} + +impl CounterMode for FinishedMode { + fn update(&mut self) { } // We're finished. Do nothing. + fn get_result(&self) -> Option { Some(self.result) } +} + +impl Mode for FinishedMode { + type Base = CounterMode; + fn as_base(&self) -> &Self::Base { self } + fn as_base_mut(&mut self) -> &mut Self::Base { self } + fn get_transition(&mut self) -> Option>> { + // We're finished calculating, so we never want to transition. + None + } +} + +fn main() { + // Create a new Automaton with an initial CounterMode. + let mut automaton = + Automaton::with_initial_mode( + UpMode { + counter: 0, + target: 3, + }); + + loop { + // Update the inner Mode. + { + let mode = automaton.borrow_mode_mut(); + + if let Some(result) = mode.get_result() { + // If the current mode returns a result, print it and exit the program. + println!("Result: {}", result); + break; + } + else { + // Keep updating the current mode until it wants to transition or we get a result. + mode.update(); + } + } + + // Allow the Automaton to switch to another Mode after updating the current one, if desired. + automaton.perform_transitions(); + } +} \ No newline at end of file diff --git a/src/automaton.rs b/src/automaton.rs new file mode 100644 index 0000000..2cd6136 --- /dev/null +++ b/src/automaton.rs @@ -0,0 +1,221 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +use crate::{AnyModeWrapper, Mode, ModeWrapper}; +use std::fmt; + +/// Represents a state machine over a set of `Mode`s that can be referenced via some common interface `Base`. +/// +/// The `Automaton` contains a single, active `Mode` that represents the current state of the state machine. The current +/// `Mode` is accessible via the `borrow_mode()` and `borrow_mode_mut()` functions, which return a `Base` reference. The +/// `Automaton` also provides a `perform_transitions()` function that should be called at some point in order to allow +/// the current `Mode` to transition another `Mode` in, if desired. +/// +/// See [`Mode::get_transition()`](trait.Mode.html#tymethod.get_transition) for more details. +/// +/// # The `Base` parameter +/// +/// The `Base` parameter may be either a `trait` (e.g. `Automaton`) or a concrete type +/// (e.g. `Automaton`). Given a `trait`, the `Automaton` will be able to swap between **any** +/// `Mode`s that implement the trait. However, this means that the `Automaton` will **only** allow the inner `Mode` to +/// be borrowed via a trait reference, implying that **only** functions defined on the trait will be callable. +/// +/// By contrast, if given a `struct`, **all** functions defined on the inner type will be accessible from outside the +/// `Automaton`. However, this also implies that the `Automaton` will **only** be able to switch between states of the +/// same concrete type. +/// +/// For more on the `Base` parameter, see [`Mode`](trait.Mode.html). +/// +/// # Usage +/// ``` +/// use mode::*; +/// +/// # trait MyMode { +/// # fn some_fn(&self); +/// # fn some_mut_fn(&mut self); +/// # } +/// # +/// # struct SomeMode; +/// # impl MyMode for SomeMode { +/// # fn some_fn(&self) { println!("some_fn was called"); } +/// # fn some_mut_fn(&mut self) { println!("some_mut_fn was called"); } +/// # } +/// # +/// # impl Mode for SomeMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// # fn get_transition(&mut self) -> Option>> { None } +/// # } +/// # +/// // Use with_initial_mode() to create the Automaton with an initial state. +/// let mut automaton = Automaton::with_initial_mode(SomeMode); +/// +/// // To call functions on the inner Mode, use borrow_mode() or borrow_mode_mut(); +/// automaton.borrow_mode().some_fn(); +/// automaton.borrow_mode_mut().some_mut_fn(); +/// +/// // Let the Automaton handle transitions. +/// automaton.perform_transitions(); +/// ``` +/// +pub struct Automaton + where Base : ?Sized +{ + current_mode : Box>, +} + +impl Automaton + where Base : ?Sized +{ + /// Creates a new `Automaton` with the specified `initial_mode`, which will be the active `Mode` for the `Automaton` + /// that is returned. + /// + pub fn with_initial_mode(initial_mode : M) -> Self + where M : Mode + { + Self { + current_mode : Box::new(ModeWrapper::new(initial_mode)), + } + } + + /// Calls `get_transition()` on the current `Mode` to determine whether it wants to transition out. If a + /// `Transition` is returned, the `Transition` callback will be called on the current `Mode`, swapping in whichever + /// `Mode` it returns as a result. + /// + /// See [`Transition`](trait.Transition.html) and + /// [`Mode::get_transition()`](trait.Mode.html#tymethod.get_transition) for more details. + /// + pub fn perform_transitions(&mut self) { + if let Some(mode) = self.current_mode.perform_transitions() { + // If a transition was performed and a new `ModeWrapper` was returned, swap in the new `Mode`. + self.current_mode = mode; + } + } + + /// Returns an immutable reference to the current `Mode` as a `&Self::Base`, allowing immutable functions to be + /// called on the inner `Mode`. + /// + pub fn borrow_mode(&self) -> &Base { + self.current_mode.borrow_mode() + } + + /// Returns a mutable reference to the current `Mode` as a `&mut Self::Base`, allowing mutable functions to be + /// called on the inner `Mode`. + /// + pub fn borrow_mode_mut(&mut self) -> &mut Base { + self.current_mode.borrow_mode_mut() + } +} + +impl Automaton + where Base : Mode + Default +{ + /// Creates a new `Automaton` with a default `Mode` instance as the active `Mode`. + /// + /// **NOTE:** This only applies if `Base` is a **concrete** type (e.g. `Automaton`) that + /// implements `Default`. If `Base` is a **trait** type (e.g. `Automaton`) or you + /// would otherwise like to specify the initial mode of the created `Automaton`, use + /// [`with_initial_mode()`](struct.Automaton.html#method.with_initial_mode) instead. + /// + /// ``` + /// use mode::*; + /// + /// struct ConcreteMode { count : u32 }; + /// + /// impl Mode for ConcreteMode { + /// type Base = Self; + /// fn as_base(&self) -> &Self { self } + /// fn as_base_mut(&mut self) -> &mut Self { self } + /// fn get_transition(&mut self) -> Option>> { + /// // TODO: Logic for transitioning between states goes here. + /// Some(Box::new( + /// |previous : Self| { + /// ConcreteMode { count: previous.count + 1 } + /// })) + /// } + /// } + /// + /// impl Default for ConcreteMode { + /// fn default() -> Self { + /// ConcreteMode { count: 0 } + /// } + /// } + /// + /// // Create an Automaton with a default `Mode`. + /// let mut automaton = Automaton::::new(); + /// assert!(automaton.borrow_mode_mut().count == 0); + /// + /// // Keep transitioning the current Mode out until we reach the target state (i.e. a count of 10). + /// while automaton.borrow_mode_mut().count < 10 { + /// automaton.perform_transitions(); + /// } + /// ``` + /// + pub fn new() -> Self { + Self { + current_mode : Box::new(ModeWrapper::::new(Default::default())), + } + } +} + +impl Default for Automaton + where Base : Mode + Default +{ + /// Creates a new `Automaton` with the default `Mode` active. This is equivalent to calling `Automaton::new()`. + /// + /// See note on [`new()`](struct.Automaton.html#method.new) for more on when this function can be used. + /// + fn default() -> Self { + Self::new() + } +} + +/// If `Base` implements `std::fmt::Debug`, `Automaton` also implements `Debug`, and will print the `current_mode`. +/// +/// # Usage +/// ``` +/// use mode::*; +/// use std::fmt; +/// +/// trait MyBase : fmt::Debug { } // TODO: Add common interface. +/// +/// #[derive(Debug)] +/// struct MyMode { +/// pub foo : i32, +/// pub bar : &'static str, +/// } +/// +/// impl MyBase for MyMode { } // TODO: Implement common interface. +/// +/// impl Mode for MyMode { +/// type Base = MyBase; +/// fn as_base(&self) -> &Self::Base { self } +/// fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// fn get_transition(&mut self) -> Option>> { None } // TODO +/// } +/// +/// let automaton = Automaton::with_initial_mode(MyMode { foo: 3, bar: "Hello, World!" }); +/// dbg!(automaton); +/// ``` +/// +impl fmt::Debug for Automaton + where Base : fmt::Debug + ?Sized +{ + fn fmt(&self, formatter : &mut fmt::Formatter) -> fmt::Result { + formatter.debug_struct("Automaton") + .field("current_mode", &self.borrow_mode()) + .finish() + } +} + +impl fmt::Display for Automaton + where Base : fmt::Display + ?Sized +{ + fn fmt(&self, formatter : &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "{}", self.borrow_mode()) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6ff7551 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,131 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +//! A simple and effective behavioral state machine library, written in in idiomatic, 100% safe, stable Rust code. +//! +//! This library provides three main types, `Automaton`, `Mode`, and `Transition`, that facilitate the creation of +//! behavioral state machines. An `Automaton` can be used to quickly create a state machine over a set of `Mode`s that +//! implement some `Base` type. Each struct that implements `Mode` represents a distinct state in the state machine, and +//! the `Automaton` allows function calls to be dispatched to the current `Mode` by providing access to it as a `Base` +//! reference. A flexible `Transition` system provides a way for the current `Mode` to swap in a new state when it is +//! ready. The `Transition` system is designed such that the current `Mode` can move data from itself directly into the +//! `Mode` being created, which can help prevent spikes in memory usage while switching from one state to the next. +//! +//! # Example +//! ``` +//! use mode::*; +//! +//! // This trait will be used as the Base type for the Automaton, defining a common interface +//! // for all states. +//! trait Activity { +//! fn update(&mut self); +//! } +//! +//! // Each state in the state machine implements both Activity (the Base type) and Mode. +//! struct Working { +//! pub hours_worked : u32, +//! } +//! +//! impl Activity for Working { +//! fn update(&mut self) { self.hours_worked += 1; } +//! } +//! +//! impl Mode for Working { +//! type Base = Activity; +//! fn as_base(&self) -> &Self::Base { self } +//! fn as_base_mut(&mut self) -> &mut Self::Base { self } +//! +//! // This function allows the current Mode to swap to another Mode, when ready. +//! fn get_transition(&mut self) -> Option>> { +//! if self.hours_worked == 4 || self.hours_worked >= 8 { +//! // To swap to another Mode, a Transition function is returned, which will consume +//! // the current Mode and return a new Mode to be swapped in as active. +//! Some(Box::new(|previous : Self| { +//! Eating { hours_worked: previous.hours_worked, calories_consumed: 0 } +//! })) +//! } +//! else { None } // None means don't transition. +//! } +//! } +//! +//! struct Eating { +//! pub hours_worked : u32, +//! pub calories_consumed : u32, +//! } +//! +//! impl Activity for Eating { +//! fn update(&mut self) { self.calories_consumed += 100; } // Yum! +//! } +//! +//! impl Mode for Eating { +//! type Base = Activity; +//! fn as_base(&self) -> &Self::Base { self } +//! fn as_base_mut(&mut self) -> &mut Self::Base { self } +//! fn get_transition(&mut self) -> Option>> { +//! if self.calories_consumed >= 500 { +//! if self.hours_worked >= 8 { +//! // Time for bed! +//! Some(Box::new(|_ : Self| { Sleeping { hours_rested: 0 } })) +//! } +//! else { +//! // Time to go back to work! +//! Some(Box::new(|previous : Self| { +//! Working { hours_worked: previous.hours_worked } +//! })) +//! } +//! } +//! else { None } +//! } +//! } +//! +//! struct Sleeping { +//! pub hours_rested : u32, +//! } +//! +//! impl Activity for Sleeping { +//! fn update(&mut self) { self.hours_rested += 1; } // ZzZzZzZz... +//! } +//! +//! impl Mode for Sleeping { +//! type Base = Activity; +//! fn as_base(&self) -> &Self::Base { self } +//! fn as_base_mut(&mut self) -> &mut Self::Base { self } +//! fn get_transition(&mut self) -> Option>> { +//! if self.hours_rested >= 8 { +//! // Time for breakfast! +//! Some(Box::new(|_| { Eating { hours_worked: 0, calories_consumed: 0 } })) +//! } +//! else { None } +//! } +//! } +//! +//! fn main() { +//! let mut person = Automaton::with_initial_mode(Working { hours_worked: 0 }); +//! +//! for age in (18..100) { +//! // Update the current Mode for the Automaton. +//! person.borrow_mode_mut().update(); +//! +//! // Allow the Automaton to switch Modes. +//! person.perform_transitions(); +//! } +//! } +//! ``` +//! +//! # Getting started +//! A good place to start reading would be the [`Automaton`](struct.Automaton.html) documentation. For a full example +//! that demonstrates how to use `Automaton` and `Mode` to implement a simple state machine, see `examples/counter.rs`. +//! +mod automaton; +mod mode; +mod mode_wrapper; +mod transition; + +pub use self::automaton::*; +pub use self::mode::*; +pub use self::transition::*; + +use self::mode_wrapper::*; \ No newline at end of file diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 0000000..78efc18 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,104 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +use crate::Transition; + +/// Trait that represents a state within an `Automaton`. +/// +/// Every `Automaton` contains a single `Mode` instance that represents the active state of the state machine. An +/// `Automaton` can **only** switch between implementations of `Mode` of the same `Base` type. The `Automaton` +/// only allows its active `Mode` to be accessed as a `Base` reference, so only functions exposed on the `Base` type are +/// callable on the `Mode` from outside the `Automaton`. +/// +/// See [`Automaton`](struct.Automaton.html) for more details. +/// +/// # Transitions +/// `Mode`s can choose to transition to any other `Mode` with the same `Base` type. This is accomplished by returning a +/// `Transition` function from the `get_transition()` function, which will cause the parent `Automaton` to switch to +/// another `Mode` the next time `perform_transitions()` is called. +/// +/// See [`Transition`](trait.Transition.html) and +/// [`Automaton::perform_transitions()`](struct.Automaton.html#method.perform_transitions) for more details. +/// +/// # Usage +/// ``` +/// use mode::*; +/// +/// trait MyMode { +/// // TODO: Define some common interface for ModeA and ModeB. +/// } +/// +/// struct ModeA; // TODO: Add fields. +/// impl MyMode for ModeA { } +/// +/// impl Mode for ModeA { +/// type Base = MyMode; +/// fn as_base(&self) -> &Self::Base { self } +/// fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// fn get_transition(&mut self) -> Option>> { +/// // Transition to ModeB. ModeA can swap to ModeB because both share the same Base. +/// Some(Box::new(|previous : Self| { ModeB })) +/// } +/// } +/// +/// struct ModeB; // TODO: Add fields. +/// impl MyMode for ModeB { } +/// +/// impl Mode for ModeB { +/// type Base = MyMode; +/// fn as_base(&self) -> &Self::Base { self } +/// fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// fn get_transition(&mut self) -> Option>> { None } // None means don't transition. +/// } +/// ``` +/// +/// # The `Base` parameter +/// You will notice from the [example](#usage) above that `ModeA` and `ModeB` implement `Mode` and `MyMode` separately, +/// but the `MyMode` trait itself does **not** extend `Mode`, i.e. is defined as `trait MyMode` as opposed to +/// `trait MyMode : Mode`. We want to use `MyMode` as the `Base` type for `ModeA` and `ModeB`, but +/// unfortunately having `MyMode` extend `Mode` would create a circular dependency between the two types, +/// and would cause a compile error. Hence, while it is possible to cast `ModeA` or `ModeB` to `MyMode` or `Mode`, +/// casting between `MyMode` and `Mode` is not allowed. +/// +/// # `as_base()` and `as_base_mut()` +/// As mentioned above, a `Mode` reference **cannot** be cast to its `Base` type. What's more, the `Automaton` can only +/// require that the current `Mode` implements `Mode`, and **cannot** enforce that it is also convertible +/// to `Base`. That's because `Base` is a type parameter and not a trait, and therefore **cannot** be used as a +/// constraint in `where` clauses, e.g. `where T : Base`. +/// +/// Since the `Automaton` needs to be able to return a `Base` reference to the current `Mode`, each `Mode` is required +/// to implement `as_base()` and `as_base_mut()` functions that return `self` as a `&Base` and a `&mut Base`, +/// respectively. Unfortunately, these functions have to be defined manually for each struct that implements `Mode`. +/// +/// **NOTE:** This may change when [`Unsize`](https://github.com/rust-lang/rfcs/blob/master/text/0982-dst-coercion.md) +/// becomes stable, since it will provide a way for `struct`s to express that a generic parameter must be convertible to +/// a particular type. +/// +pub trait Mode : 'static { + /// Represents the user-facing interface for the `Mode` that will be exposed via the `Automaton`. In order to be + /// used with an `Automaton`, the `Base` type of the `Mode` **must** match the `Base` type of the `Automaton`. This + /// is so that the `Automaton` can provide `get_mode()` and `get_mode_mut()` functions that return a reference to + /// the `Mode` as the `Base` type. + /// + type Base : ?Sized; + + /// Returns an immutable reference to this `Mode` as a `&Self::Base`. + /// + fn as_base(&self) -> &Self::Base; + + /// Returns a mutable reference to this `Mode` as a `&mut Self::Base`. + /// + fn as_base_mut(&mut self) -> &mut Self::Base; + + /// Every time `perform_transitions()` is called on an `Automaton`, This function will be called on the current + /// `Mode` to determine whether it wants another `Mode` to become active. If this function returns `None`, the + /// current `Mode` will remain active. If it returns a valid `Transition` function, however, the `Automaton` will + /// call the function on the active `Mode`, consuming it and swapping in whichever `Mode` is produced as a result. + /// + /// See [`Transition`](trait.Transition.html) for more details. + /// + fn get_transition(&mut self) -> Option>>; +} \ No newline at end of file diff --git a/src/mode_wrapper.rs b/src/mode_wrapper.rs new file mode 100644 index 0000000..c16df3b --- /dev/null +++ b/src/mode_wrapper.rs @@ -0,0 +1,81 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +use crate::Mode; + +/// Defines the `Automaton`-facing interface for a `ModeWrapper`. +/// +pub trait AnyModeWrapper { + /// Represents the user-facing interface for the `Mode` that will be exposed via the `Automaton`. See `Mode::Base` + /// for more details. + /// + type Base : ?Sized; + + /// Returns an immutable reference to the inner `Mode` as a `&Self::Base`. + /// + fn borrow_mode(&self) -> &Self::Base; + + /// Returns a mutable reference to the inner `Mode` as a `&mut Self::Base`. + /// + fn borrow_mode_mut(&mut self) -> &mut Self::Base; + + /// Calls `get_transition()` on the inner `Mode` to determine whether it wants another `Mode` to become active. If + /// this yields a `Transition`, the `Transition` will be called on the inner `Mode` and a new `ModeWrapper` around + /// the `Mode` to be swapped in will be returned. + /// + fn perform_transitions(&mut self) -> Option>>; +} + +/// Wraps a specific instance of `Mode`, allowing the parent `Automaton` to handle `Transition`s between that instance +/// and other `Mode`s gracefully. +/// +/// **NOTE:** This `struct` mainly exists to allow `Transition`s to be scheduled as `FnOnce(A) -> B` instead of +/// requiring each user-defined `Mode` to know more about the implementation details of the `Automaton`. +/// +pub(crate) struct ModeWrapper + where T : Mode +{ + mode : Option, +} + +impl ModeWrapper + where T : Mode +{ + /// Creates and returns a new `ModeWrapper` around the specified `Mode`. + /// + pub fn new(mode : T) -> Self { + Self { + mode: Some(mode), + } + } +} + +impl AnyModeWrapper for ModeWrapper + where T : Mode +{ + type Base = T::Base; + + fn borrow_mode(&self) -> &Self::Base { + self.mode.as_ref().unwrap().as_base() + } + + fn borrow_mode_mut(&mut self) -> &mut Self::Base { + self.mode.as_mut().unwrap().as_base_mut() + } + + fn perform_transitions(&mut self) -> Option>> { + // Retrieve the desired transition, if any, from the inner Mode. + match self.mode.as_mut().unwrap().get_transition() { + None => None, + Some(transition) => { + // If a valid Transition was returned, call the Transition callback on the inner Mode and return a new + // wrapper around the Mode that was produced. + // NOTE: This will move the Mode into the callback, leaving this object empty. + Some(transition.invoke(self.mode.take().unwrap())) + } + } + } +} \ No newline at end of file diff --git a/src/transition.rs b/src/transition.rs new file mode 100644 index 0000000..d132c87 --- /dev/null +++ b/src/transition.rs @@ -0,0 +1,173 @@ +// Copyright 2019 Andrew Thomas Christensen +// +// Licensed under the Apache License, Version 2.0, or the +// MIT license , at your option. This file may not be copied, +// modified, or distributed except according to those terms. + +use crate::{AnyModeWrapper, Mode, ModeWrapper}; + +/// Trait that defines the call signature for a function that can switch an `Automaton` from one `Mode` (`A`) to +/// another. +/// +/// State machines, by definition, are systems that possess multiple states that can be switched in or out in order to +/// change the behavior of the system. `Automaton`s are no exception. Eventually, the `Automaton` will need to switch +/// `Mode`s in order to change its behavior. +/// +/// Rather than requiring the owner of the `Automaton` to check the status of the current `Mode` periodically and swap +/// in a new state from outside the object when appropriate, the `Automaton` instead delegates this responsibility to +/// the current `Mode` via the `perform_transitions()` function. Each time this function is called, the `Automaton` +/// calls `get_transition()` on the current `Mode` to determine whether it is ready to transition to another state. To +/// do this, the `Mode` may return a callback function of the form `FnOnce(A) -> B`, where `A` is the type of the +/// currently active `Mode` and `B` is another implementation of `Mode` with the same `Base` type. The `Transition` +/// trait is automatically implemented on any closure of this form, so that `get_transitions()` can return a boxed +/// closure instead of some wrapper type. +/// +/// **NOTE:** Although there is rarely a need to do this, it is perfectly valid for a `Transition` function to return +/// the input `Mode` as a result. This will result in the currently active `Mode` remaining active, even after the +/// `Transition` function has been called. +/// +/// # Usage +/// ``` +/// use mode::*; +/// +/// # trait MyMode { } +/// # +/// # struct SomeMode; +/// # impl MyMode for SomeMode { } +/// # +/// impl Mode for SomeMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// // ... +/// fn get_transition(&mut self) -> Option>> { +/// # let some_condition = true; +/// # let some_other_condition = true; +/// // ... +/// if some_condition { +/// // Returning a Transition function will cause the Automaton to switch the +/// // current Mode to whatever new Mode is produced by the callback. +/// Some(Box::new(|previous : Self| { SomeOtherMode })) +/// } +/// else if some_other_condition { +/// // NOTE: The Transition trait allows this function to return closures with +/// // completely different return types, so long as the parameter types match. +/// Some(Box::new(|previous : Self| { YetAnotherMode })) +/// } +/// else { None } // Returning None will keep the current Mode active. +/// } +/// } +/// # +/// # struct SomeOtherMode; +/// # impl MyMode for SomeOtherMode { } +/// # +/// # impl Mode for SomeOtherMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// # fn get_transition(&mut self) -> Option>> { None } +/// # } +/// # +/// # struct YetAnotherMode; +/// # impl MyMode for YetAnotherMode { } +/// # +/// # impl Mode for YetAnotherMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// # fn get_transition(&mut self) -> Option>> { None } +/// # } +/// ``` +/// +/// # Why `Transition` functions? +/// One of the most powerful features of the `Transition` system is that, while transitioning, the current `Mode` is +/// moved into the `Transition` callback. This allows `Mode` `B`, being produced, to steal pointers and other state from +/// `Mode` `A`, as opposed to allocating more memory for `B`, copying over state from `A`, and then deallocating `A`. +/// This won't make much of a difference for small `Mode`s, but for `Mode`s that contain a large amount of state (on the +/// order of several megabytes or even gigabytes, as is common in UI applications), this can make a huge difference for +/// both performance and heap memory usage. +/// +/// Instead of having the `get_transition()` function return a callback, the transition system could have been +/// implemented as a `fn do_transition(self) -> B` function on `Mode`. (This is a little bit of an oversimplification.) +/// However, having a callback allows `Transition`s to be scheduled in advance and performed later, capturing state from +/// the calling code, as necessary. This is especially convenient in cases where a `Mode` needs to stay active for a +/// little longer after a `Transition` is scheduled, e.g. allowing a `Mode`-driven UI system to finish playing a +/// transition animation or sound effect before swapping in a new `Mode`. +/// +/// ## Example +/// ``` +/// use mode::*; +/// +/// # trait MyMode { +/// # fn update(&mut self) { } +/// # } +/// # +/// struct SomeMode { +/// queued_transition : Option>> +/// } +/// +/// impl MyMode for SomeMode { +/// fn update(&mut self) { +/// # let some_condition = true; +/// // ... +/// if (some_condition) { +/// // Queue up a transition to be performed later. +/// self.queued_transition = Some(Box::new(|previous| { SomeOtherMode })) +/// } +/// +/// // TODO: Continue updating, animating, etc. +/// } +/// } +/// +/// impl Mode for SomeMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// // ... +/// fn get_transition(&mut self) -> Option>> { +/// # let ready_to_transition = true; +/// // ... +/// if ready_to_transition && self.queued_transition.is_some() { +/// // When we're finally finished updating, return the queued transition. +/// self.queued_transition.take() +/// } +/// else { None } // Returning None will keep the current Mode active. +/// } +/// } +/// # +/// # struct SomeOtherMode; +/// # impl MyMode for SomeOtherMode { } +/// # +/// # impl Mode for SomeOtherMode { +/// # type Base = MyMode; +/// # fn as_base(&self) -> &Self::Base { self } +/// # fn as_base_mut(&mut self) -> &mut Self::Base { self } +/// # fn get_transition(&mut self) -> Option>> { None } +/// # } +/// ``` +/// +pub trait Transition + where A : Mode + ?Sized +{ + /// Calls the `Transition` function on the specified `Mode`, consuming the `Transition` and the `mode` and returning + /// a wrapper around the new `Mode` to be swapped in as active. + /// + /// **NOTE:** You should not attempt to implement this function yourself, as the return type makes use of a private + /// trait (`AnyModeWrapper`). Ideally, this function would return a `Box` in order to abstract away all of the + /// implementation details of the `Automaton`. Unfortunately, that isn't possible, in this case, because the `Mode` + /// trait cannot be made into an object, and therefore cannot be boxed. + /// + fn invoke(self : Box, mode : A) -> Box>; +} + +impl Transition for T + where + T : FnOnce(A) -> B, + A : Mode, + B : Mode, +{ + fn invoke(self : Box, mode : A) -> Box> { + // Call the transition function and wrap the result with a ModeWrapper. + Box::new(ModeWrapper::::new((self)(mode))) + } +} \ No newline at end of file