Skip to content

Instantly share code, notes, and snippets.

@aep
Last active November 13, 2018 19:29
Show Gist options
  • Save aep/543d0e2fa6b11e7143bdbf6b273acb69 to your computer and use it in GitHub Desktop.
Save aep/543d0e2fa6b11e7143bdbf6b273acb69 to your computer and use it in GitHub Desktop.
/// this is describes how to build typed combinators to a friend
/// he wanted to build a generic job executor thing that can execute one step at a time and forward
/// the intermediate data to the next step without copying
/// but all type safe of course.
///
/// my approach is sort of a practical take on category theory that rust does with for example futures
// this is for mem::replace. it's a neat trick that you should know in rust
// sometimes you want to assign to something depending on a decision made based
// on the content of the thing you're assinging to.
// rust doesnt let you do that. most of the times for good reasons, other times because the borrow checker
// doesnt understand yet that it could just throw away the ref before you assign because you dont need it anymore.
// what i usually do is wrap the thing i want to mutate in a Option<Thing> and use mem::replace, which takes the value
// out of the Option without copying. then i can own the old data and assign a new one later.
use std::mem;
// ok so first the easy part
// we'll just have an abstract job trait
trait Job<In, Out> {
fn run(&mut self, i:In) -> Out;
}
// some concrete job that reads a file for example
struct ReaderJob(u32);
impl Job<(), u32> for ReaderJob {
fn run(&mut self, _: ()) -> u32 {
self.0
}
}
// a concrete job that decodes something we read from a file. whatever
struct DecodeJob();
impl Job<u32, bool> for DecodeJob{
fn run(&mut self, i: u32) -> bool{
i > 3
}
}
// this would be one go execution
// checking the basic types are good
fn not_main() {
let mut a = ReaderJob(2);
let mut b = DecodeJob();
let result = b.run(a.run(()));
}
// but we want each step separatly
// let's think of an explicit state engine that would do that
enum FoState {
Start,
Read,
Decode,
}
struct Fo(FoState);
impl Job<(), Option<bool>> for Fo{
fn run(&mut self, i: ()) -> Option<bool> {
match self.0 {
FoState::Start => self.0 = FoState::Read,
FoState::Read => self.0 = FoState::Decode,
FoState::Decode => return Some(false),
}
None
}
}
// you would drive this thing via run() as long as it returns None
// when all the Jobs are executed, it'll return Some(result)
// cool
// but we want some sort of generic state engine, and it needs to be typed.
// the most elegant solution is probably stacking combinators
// that means we think of the state engine as a stack of state engines that each have exactly two steps
// we can combine them infinitely to create an execution stack, hence they're called combinators
// same state engine as above, but generic
enum AndThenState<In,Inter,Out> {
Step1(In),
Step2(Inter),
Done(Out),
}
//again, we've seen this. we just have a state and the two functions that mutate state
//but this time the functions are generic Job traits
struct AndThen<In,Inter,Out>{
st: Option<AndThenState<In,Inter,Out>>,
from_in_to_inter: Box<Job<In,Inter>>,
from_inter_to_out: Box<Job<Inter,Out>>,
}
// same boilerplate. for each time run() is called, just forward the state engine until we're done
impl<In,Inter,Out> Job<In, Option<Out>> for AndThen<In, Inter, Out> {
fn run(&mut self, i: In) -> Option<Out> {
match mem::replace(&mut self.st, None).unwrap() {
AndThenState::Step1(i) => self.st = Some(AndThenState::Step2(self.from_in_to_inter.run(i))),
AndThenState::Step2(i) => self.st = Some(AndThenState::Done(self.from_inter_to_out.run(i))),
AndThenState::Done(i) => return Some(i),
}
None
}
}
// just some sugar to make creating a combinator less ugly
impl<In,Inter,Out> AndThen<In,Inter,Out> {
pub fn new<J1,J2>(i: In, step1: J1, step2: J2) -> Self
where J1: Job<In,Inter> + 'static,
J2: Job<Inter,Out> + 'static,
{
Self {
st: Some(AndThenState::Step1(i)),
from_in_to_inter: Box::new(step1),
from_inter_to_out: Box::new(step2),
}
}
}
pub fn main() {
let a = ReaderJob(2);
let b = DecodeJob();
// so this is a Job itself now. which executes one of the above for each time we call run
let executor = AndThen::new((), a, b);
// but the best part is that we can combine them like that for example\
// this is the only line that doesnt compile beecause a doesnt take a bool as input.
// You'd need other concrete jobs of course. You get the idea
// The fact that it doesnt compile also shows you the power of typed combinators tho.
let executor = AndThen::new((), a, AndThen::new((), a, b));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment