Type Level State Machines
In this example we use type level state machines to encode functionality that is potentially available. See the example below for a sketch of how this can work.
#![allow(unused)] fn main() { /// The contract we're building, that can be in any type-state T. struct StatefulContract<T>(PhantomData<T>); /// We use empty structs as type tags. /// Note: we could add a `trait State`, but it is not required /// /// A contract can be in the open state or the closed state. struct Opened; struct Closed; /// The "state machine" defines functionality that may be available trait FunctionalityAtState where Self : Sized + Contract { /// empty declaration *could* be a default implementation, but we leave it empty /// so that other states may override it. decl_then!{do_something} } /// Override the impl when state is Opened impl FunctionalityAtState for StatefulContract<Opened> { /// Transition from Opened => Closed state #[then] fn do_something(self, ctx: Context) { ctx.template() .add_output( ctx.funds(), &StatefulContract::<Closed>(Default::default()), None, )? .into() } } /// do not override `do_something`, no branch will be generated impl FunctionalityAtState for StatefulContract<Closed> {} /// Register that all StatefulContract<T>'s that implement FunctionalityAtState /// are Contracts impl Contract for StatefulContract<T> where Self : FunctionalityAtState { declare!{then, Self::do_something} } }
This technique is ridiculously powerful. Imagine, for instance, that we wanted to have different sorts of state other than Open and Closed. E.g., Red and Green. We could then define Transition Rules that encode a graph like:
(Open, Green) ==> do_something ==> (Closed, Green)
(Open, Red) ==> do_something ==> (Closed, Red)
(Open, Green) ==> do_something_else ==> (Open, Red)
(Open, Red) ==> do_something_else ==> (Closed, Red)
using two separate FunctionalityAtState
like traits:
#![allow(unused)] fn main() { /// The contract we're building, that can be in any type-state T. struct StatefulContract<T1, T2>(PhantomData<(T1, T2)>); /// We use empty structs as type tags. /// Note: we could add a `trait State`, but it is not required /// /// A contract can be in the open state or the closed state. struct Opened; struct Closed; // And Red or Green struct Red; struct Green; /// The "state machine" defines functionality that may be available trait OpenAtState where Self : Sized + Contract { /// empty declaration *could* be a default implementation, but we leave it empty /// so that other states may override it. delc_then!{do_something} } trait ColorAtState where Self : Sized + Contract { /// empty declaration *could* be a default implementation, but we leave it empty /// so that other states may override it. decl_then!{do_something} } /// Override the impl when state is Opened impl OpenAtState<DontCare> for StatefulContract<Opened, DontCare> { /// Transition from Opened => Closed state #[then] fn do_something(self, ctx: Context) { ctx.template() .add_output( ctx.funds(), &StatefulContract::<Closed, DontCare>(Default::default()), None, )? .into() } } /// do not override `do_something`, no branch will be generated impl OpenAtState<DontCare> for StatefulContract<Closed, DontCare> {} /// Override the impl when state is Opened impl ColorAtState for StatefulContract<Open, Green> { /// Transition from Green => Red state #[then] fn do_something_else(self, ctx: Context) { ctx.template() .add_output( ctx.funds(), &StatefulContract::<Open, Red>(Default::default()), None, )? .into() } } impl ColorAtState for StatefulContract<Open, Red> { /// Transition from Open => Closed state #[then] fn do_something_else(self, ctx: Context) { ctx.template() .add_output( ctx.funds(), &StatefulContract::<Closed, Red>(Default::default()), None, )? .into() } } /// do not override `do_something_else`, no branch will be generated impl ColorAtState<DontCare> for StatefulContract<DontCare, Red> {} /// Register that all StatefulContract<T>'s that implement OpenAtState /// are Contracts impl Contract for StatefulContract<T> where Self : OpenAtState + ColorAtState { declare!{then, Self::do_something, Self::do_something_else} } }
This technique showcases how Sapio could encode very sophisticated logic in program generation.
It's also notable that following rustc v1.51, it is possible to use const
's
as generic type parameters which enables even more computation at the type level.