Last active
November 13, 2018 19:29
-
-
Save aep/543d0e2fa6b11e7143bdbf6b273acb69 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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