Porting patterns from Object Oriented concepts in Rust do not always work, but there are certain cases they not only work but sometimes they are easier to follow when compared with the original OOP implementation.
Here I am going to demonstrate the concept of orchestration in Rust.
Taking a step back orchestration resembles vaguely of redux, with the key difference that a reducer has not only has to deal with the state but a payload as well.
Orchestration is basically a pipeline for which the previous state is passed in the next operation. The key thing in orchestration is the ability to pause and resume.
I classify this example as an intermediate one, but not that hard to follow.
We will start by talking about and defining our State object;
#[derive(Debug, Clone)]
pub struct State {
pub proceed: bool,
pub outcome: f32,
pub stage: Vec<bool>,
}
Our state has the following
- proceed: which marks each stage as successful or not, marking proceed as false, that will stop the execution of the remaining stages
- outcome: result of each stage
- stage: record the outcome of the stage
Then we will define the Orchestrate trait
trait Orchestrate {
fn execute(self, state: State) -> State;
}
and we will implement the Orchestrate trait on a Vector of Functions
impl Orchestrate for Vec<fn(State) -> Result<State, Error>> {
fn execute(self, state: State) -> State {
self.iter().enumerate().fold(state, |output, (i, func)| {
let new_state = output.clone();
if new_state.stage.len() > i {
if new_state.stage[i] {
return new_state;
} else {
let mut next_state = func(new_state).unwrap();
next_state.stage[i] = next_state.proceed;
return next_state;
}
}
let mut next_state = func(new_state).unwrap();
next_state.stage.push(next_state.proceed);
return next_state;
})
}
}
It's already easy to see where this is going.
- we received the array of functions
- for each execution we check if the operation has been completed in a previous execution (stage)
- we either execute the stage or ignore it
Next we will define 3 simple operations we want to perform in this orchestration. How are they going to be used, one would wonder.
fn pow2(n: f32) -> f32 {
n.powf(2.0)
}
fn pow3(n: f32) -> f32 {
n.powf(3.0)
}
fn sqrt(n: f32) -> f32 {
n.sqrt()
}
And now my favourite part, instead of just creating a function that gets a State and returns a state and has the pause resume logic in it I am going to create a macro that creates the functions by accepting a function as an argument.
macro_rules! state_function {
( $func:expr ) => {{
pub fn state_fn(c: State) -> Result<State, Error> {
let stage: Vec<bool> = c.stage.to_vec();
if c.proceed == false {
Ok(State {
proceed: false,
outcome: c.outcome,
stage: stage,
})
} else {
let mut rng = rand::thread_rng();
let y: bool = rng.gen();
Ok(State {
proceed: y,
outcome: $func(c.outcome),
stage: stage,
})
}
}
state_fn
}};
}
- If proceed is false, don't do anything
- else return the state with the new outcome.
And to spice things a bit up I used a random boolean, to disrupt the processing and make things a bit more interesting.
So finally on our main function,
Step 1: we create our functions
let a: fn(State) -> Result<State, Error> = state_function!(pow2);
let b: fn(State) -> Result<State, Error> = state_function!(pow3);
let c: fn(State) -> Result<State, Error> = state_function!(sqrt);
Step 2: we define our chain
let chain: Vec<fn(State) -> Result<State, Error>> = vec![a, b, c];
Step 3: we execute our chain
let result = chain
.execute(State {
proceed: true,
outcome: 6.,
stage: Vec::<bool>::new(),
});
println!("{:?}", result);
BONUS
Step 4: we execute our chain with assuming some steps have been completed
let result = chain
.execute(State {
proceed: true,
outcome: 6.,
stage: vec![true, false, false],
});
println!("{:?}", result);
I hope you enjoyed it... :D
Update: a generic version of this pattern can be found here https://github.com/elasticrash/orchestrator