Skip to content

Instantly share code, notes, and snippets.

@CMCDragonkai
Last active January 30, 2022 07:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CMCDragonkai/3353d67f33e3feb0c23f to your computer and use it in GitHub Desktop.
Save CMCDragonkai/3353d67f33e3feb0c23f to your computer and use it in GitHub Desktop.
Rust: Function Composition (Rust 1.9 nightly) (credit to https://www.youtube.com/watch?v=ZP93Ngeokio)
#![feature(box_syntax)]
// function composition is not part of the standard library
// see discussion here: https://internals.rust-lang.org/t/function-composition-in-the-standard-library/2615/11
fn main() {
// works on top-level functions, the 'a lifetime is static
// we need debugging display since we're displaying the Option monad, a wrapped type
println!("{:?}", compose(convert_string_to_float, double_float)(Some("2".to_string())));
// works with closures, the 'a lifetime is the lifetime of the 2 closures here
println!("{}", compose(|x| x + 1, |x| x + 2)(100));
let mut state1 = 1;
let mut state2 = 2;
let closure1 = |x| {
state1 += 1;
x + 1
};
let closure2 = |x| {
state2 += 2;
x + 2
};
let closure3 = |x| x + 3;
let closure4 = |x| x + 4;
println!("{}", compose(closure1, closure2)(100));
println!("{}", compose(closure3, closure4)(100));
let mut state3 = 3;
let mut state4 = 4;
let mut closure5 = |x| {
state3 += 1;
x + 1
};
let mut closure6 = |x| {
state4 += 2;
x + 2
};
{
// need to make sure composed_closure dies at the end of this scope
// in order to end the borrowing of closure3 and closure4
let mut composed_closure_5_6 = compose2(&mut closure5, &mut closure6);
println!("{}", composed_closure_5_6(100));
}
// inline compose means that the borrowing ends immediately within the subexpression
println!("{}", compose2(&mut closure5, &mut closure6)(100));
println!("{}", compose2(&mut closure5, &mut closure6)(100));
println!("{}", closure5(101));
let mut closure7 = box |x| x + 7;
let mut closure8 = box |x| x + 8;
{
let mut composed_closure_7_8 = compose2(&mut *closure7, &mut *closure8);
println!("{}", composed_closure_7_8(100));
}
println!("{}", closure7(101));
}
fn convert_string_to_float (n: Option<String>) -> Option<f32> {
// monadic bind (map and reduce) means that the closure here returns an Option as well
n.and_then(|n| n.parse().ok())
}
fn double_float (n: Option<f32>) -> Option<f32> {
// map just changes the interval value of the Option monad
n.map(|n| n * 2.0)
}
// composes 2 functions that share a lifetime 'a
// 'a denotes the lifetime of functions (like C functors)
// functors in Rust uses a struct containing the closure
// and fields of the environment
// it returns a "boxed closure", an owned pointer to a heap allocated function
// ownership is returned to the caller of compose
// the FnMut trait means closures can inhabit Fn or FnMut but not FnOnce
// note that top-level functions can easily be lifted to closures, but closures cannot be easily converted to top-level functions
// currently the function takes ownership of the closures, which prevents closures from being used again!
// it captures the entire state of the input functions, and transforms it into a new function
// think of it as like a function machine that eats up 2 functions and spits out a new function
// the functions that got eaten can't be used again!
fn compose<'a, T1, T2, T3, F1, F2> (mut f: F1, mut g: F2) -> Box<FnMut(T1) -> T3 + 'a>
where F1: FnMut(T1) -> T2 + 'a,
F2: FnMut(T2) -> T3 + 'a
{
box move |x| g(f(x))
}
// attempt 1000 at composing borrowed functions
// the problems with this, is that all functions needs to be passed with &mut lending a mutable reference
// furthermore, you can no longer use inline temporary closures, because of the lifetime problem, every closure needs to be within a let
// think about it this way, if our compose2 function is intended to take references to closures, those closures still need to exist
// somewhere, so that means the closures must still exist at the caller's scope
// inline closures are lost immediately, so they don't live long enough for you to use the composed function
// but this means the composed function also can't escape the scope of the caller, unless the lifetime of the referenced functions also escapes
fn compose2<'a, T1, T2, T3, F1, F2> (f: &'a mut F1, g: &'a mut F2) -> Box<FnMut(T1) -> T3 + 'a>
where F1: FnMut(T1) -> T2 + 'a,
F2: FnMut(T2) -> T3 + 'a
{
box move |x| g(f(x))
}
// perhaps instead of borrowing, we clone the input functions? That way it avoids the problems of borrowing
// the problems being illustrated by compose2, since the input functions must stay alive long enough until the composed function dies
// and the verbosity of always passing &mut, explicitly saying I'm passing a reference to mutable closure
// functionally, denotational semantics relies on the composed function to live as long as it wants, but for the purposes of programmer convenience, the old input functions should still be usable
// so cloning the input functions prior to passing them in might be the answer?
// also still can't get FnOnce to work!
// see: http://stackoverflow.com/questions/27883509/can-you-clone-a-closure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment