Last active
January 30, 2022 07:11
-
-
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)
This file contains hidden or 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
#![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