mode version 0.3 to 0.4This guide will help you upgrade your code from version 0.3 to 0.4 of mode in just a few easy steps!
Note: Most of the following examples have been adapted from the Activity example. If you diff examples/activity.rs
between versions 0.3 and 0.4, you will be able to see (roughly) the same list of changes detailed below.
mode version number in Cargo.tomlBefore doing anything else, you should bump the mode version number in your project’s Cargo.toml, like this:
mode = "^0.4"
After that, run the following command from your project’s root folder to update the dependency:
cargo update --package mode
With that, you should be ready to start refactoring.
Mode::swap() functionPreviously, there was a required swap() function for each Mode implementation. This function was called on the
active Mode whenever one of the Automaton::next() family of functions was invoked, in order to allow it to make
another Mode in as active, if desired:
fn swap(self : Box<Self>, _input : ()) -> Box<dyn Activity> {
if self.hours_worked == 4 || self.hours_worked >= 8 {
println!("Time for {}!", if self.hours_worked == 4 { "lunch" } else { "dinner" });
Box::new(Eating { hours_worked: self.hours_worked, calories_consumed: 0 })
}
else { self } // Returning self means that this Mode should remain current.
}
In 0.4, this function has been eliminated entirely. Instead, Automaton::next() takes a closure that is called on the
current Mode in order to transition it. The idea is that the callback can capture any necessary state from the calling
function and use it during the transition process. In the Activity example, that looks like this:
// Update the current Mode and/or transition to another Mode, when the current Mode requests it.
Automaton::next(&mut person, |current_mode| current_mode.update());
The great thing about this new system is that it provides a lot more flexibility when it comes to the transition
process. It doesn’t matter how transitioning is accomplished, so long as the callback returns a new Mode to be swapped
in at the end. The callback can be a closure, a free function, or any other FnOnce that consumes a Family::Mode as
the first argument and returns a new one to be swapped in.
You’ll notice that in the new Activity example, the code in the Mode::swap() function for each state has been moved
into Activity::update(), which now takes a self : Box<Self> and returns a Box<Activity>, as swap() once did:
impl Activity for Working {
fn update(mut self : Box<Self>) -> Box<dyn Activity> {
println!("Work, work, work...");
self.hours_worked += 1;
if self.hours_worked == 4 || self.hours_worked >= 8 {
println!("Time for {}!", if self.hours_worked == 4 { "lunch" } else { "dinner" });
Box::new(Eating { hours_worked: self.hours_worked, calories_consumed: 0 })
}
else { self }
}
}
This accomplishes the same thing that boxed::Mode::swap() did in the previous example: delegating the responsibility
for transitioning to the current Mode in the Automaton, except that we can now call it whatever we want and have
total control over the function signature. Activity::update() is just a normal trait function, which we can call on
current_mode because is moved into the closure as a Box<Activity>.
Obviously, you can solve this transition problem however you want. However, if you just want to get your code compiling again quickly, the easiest way to upgrade your code is outlined in the sections below.
Input and Output associated types from each Family implementationIn version 0.3 of mode, each Family struct was required to define several associated types, like this:
struct ActivityFamily;
impl Family for ActivityFamily {
type Base = dyn Activity;
type Mode = Box<dyn Activity>;
type Input = ();
type Output = Box<dyn Activity>;
}
With version 0.4, the Input and Output associated types are no longer necessary. Hence, you can remove those
associated types from the impl entirely. Once you’re done, it should look something like this:
struct ActivityFamily;
impl Family for ActivityFamily {
type Base = dyn Activity;
type Mode = Box<dyn Activity>;
}
impls for boxed::Mode, rc::Mode, and sync::Mode with impls for ModePreviously, in order to have an Automaton store the current Mode by pointer type, it was necessary to define a
swap() function for each Mode that took the pointer type as the self parameter. This was accomplished by
implementing one of various Mode traits corresponding to the pointer type being stored, e.g. boxed::Mode:
impl boxed::Mode for Working {
type Family = ActivityFamily;
fn swap(self : Box<Self>, _input : ()) -> Box<dyn Activity> {
// ...
}
}
In 0.4, these separate traits have disappeared entirely (along with the swap() function itself). Now, all that is
necessary to implement Mode for a type is to specify the Family to which the type belongs, like so:
impl Mode for Working {
type Family = ActivityFamily;
}
Hence, you can replace any replace any references to the separate pointer-specific traits, boxed::Mode, rc::Mode,
and sync::Mode, with the root-level Mode. The code in the swap() function will also need to move, as Mode no
longer defines a swap() function.
swap() function to the Base type for each FamilySince trait Mode no longer defines a swap() function, we need to find a new home for the swap() function on each
Mode implementation. Since the callback in Automaton::next() consumes an F::Base, we can call any function that is
defined on that type. Hence, if we define a swap() function on the Base type with the same signature as the old
Mode::swap() implementation, we can call it through the Base type when we call Automaton::next():
If we were to update the old Activity example in this way, it would look something like this:
trait Activity : Mode<Family = ActivityFamily> {
fn update(&mut self);
fn swap(self :Box<Self>, input : ()) -> Box<dyn Activity>; // TODO: Remove the unnecessary input parameter.
}
In this case, after adding a swap() function to Activity, we’re required to implement it for each Mode in
ActivityFamily. Since the signature is the same as the old Mode::swap() function, we could simply move the swap()
implementation for each impl Mode into the impl for Activity
impl Mode for Working {
type Family = ActivityFamily;
// NOTE: No more fn swap() in here!
}
impl Activity for Working {
fn update(&mut self) {
println!("Work, work, work...");
self.hours_worked += 1;
}
fn swap(self : Box<Self>, _input : ()) -> Box<dyn Activity> { // This is now defined when implementing Activity.
if self.hours_worked == 4 || self.hours_worked >= 8 {
println!("Time for {}!", if self.hours_worked == 4 { "lunch" } else { "dinner" });
Box::new(Eating { hours_worked: self.hours_worked, calories_consumed: 0 })
}
else { self }
}
}
Automaton::next*()In mode version 0.4, there is no more Automaton::next_with_input(), Automaton::next_with_output(), etc. Instead,
this next*() family of functions has been reduced down to two:
pub fn next<T>(automaton : &mut Self, transition_fn : T)
where T : FnOnce(F::Mode) -> F::Mode
Takes a &mut Automaton and a FnOnce(F::Mode) -> F::Mode callback. When called, the callback
is called on the current Mode, consuming it and producing another Mode to swap in as active. As in version 0.3,
if the callback returns the input Mode, the current Mode will remain active. This replaces Automaton::next()
and Automaton::next_with_input() from 0.3.
pub fn next_with_result<T, R>(automaton : &mut Self, transition_fn : T) -> R
where T : FnOnce(F::Mode) -> (F::Mode, R)
Same as the function above, except that the input callback has a signature of FnOnce(F::Mode) -> (F::Mode, R),
where R is an arbitrary return type that will be returned from the next_with_result() function when it exits.
This replaces Automaton::next_with_output() and Automaton::next_with_input_output() from 0.3.
Right now, all calls to Automaton::next*() in your own code will look something like one of these four use cases:
// CASE #1: Call swap() on the current Mode and transition to whatever Mode is returned.
Automaton::next(&mut automaton);
// CASE #2: Call swap() on the current Mode, passing in some input.
Automaton::next_with_input(&mut automaton, input);
// CASE #3: Call swap() on the current Mode, returning some result.
let result = Automaton::next_with_output(&mut automaton);
// CASE #4: Call swap() on the current Mode, passing in some input and returning some result.
let result = Automaton::next_with_input_output(&mut automaton, input);
Since we haven’t changed anything about the swap() function, except that we moved to our Base implementation, it’s
pretty easy for us to replace the old Automaton::next*() calls with their 0.4 equivalents:
// CASE #1: Call swap() on the current Mode and transition to whatever Mode is returned.
Automaton::next(&mut automaton, |current_mode| current_mode.swap(()));
// CASE #2: Call swap() on the current Mode, passing in some input.
Automaton::next(&mut automaton, |current_mode| current_mode.swap(input));
// CASE #3: Call swap() on the current Mode, returning some result.
let result = Automaton::next_with_result(&mut automaton, |current_mode| current_mode.swap(()));
// CASE #4: Call swap() on the current Mode, passing in some input and returning some result.
let result = Automaton::next_with_result(&mut automaton, |current_mode| current_mode.swap(input));
Here’s what changed:
next() function replaced next_with_input(), and next_with_result() replaced both next_with_output() and
next_with_input_output().swap() on the current Mode.next_with_input() or next_with_input_output(), we can just use
next() and next_with_result() and pass the input parameter directly into swap() from inside the closure.swap() functionIn the Activity example, you’ll notice that the input parameter is always an empty tuple, and the value is never
used in the swap() implementations of each Mode in ActivityFamily. Now that we can control the signature of
swap(), we could easily remove this parameter entirely, which would allow us to do this:
Automaton::next(&mut automaton, |current_mode| current_mode.swap()); // No more input parameter to swap()!
There’s also nothing that says we couldn’t pass more input parameters to swap(), if we needed them:
Automaton::next(&mut automaton, |current_mode| current_mode.swap(foo, bar, baz)); // Three input parameters!
As you can see, you have a lot more control now over how your Automaton transitions between Modes, which is a very
good thing.
After making these changes, everything should compile and you should be good to go! If you have any difficulty with
this, try diffing the code in the examples folder, to catch any changes that you may have missed. If you continue to
have trouble, feel free to join the mode
Gitter channel and
ask questions!
Enjoy!