Abstraction
To abstract over the logic, the following needs to be solved:
- Storage: Single type to be held together in a collection.
- Instantiation: Easy to instantiate from functions or closures.
- Invocation: Able to be invoked with
Resources
.
The concept applies to function with any number of parameters1; the code snippets on this page shows implementations for a function with 2 parameters – one mutable parameter, and one immutable parameter.
Storage
We can store different types under a single abstraction by defining a trait, and implementing the trait for each of those different types.
// Trait for all logic types to implement.
pub trait FnRes {
// ..
/// Return type of the function.
type Ret;
/// Runs the function.
fn call(&self, resources: &Resources) -> Self::Ret;
}
In order to name the logic type in code, the logic is stored in a wrapper type called FnResource
.
FnRes
is implemented for each type parameter combination of FnResource
1:
// Intermediate type so that traits can be implemented on this.
pub struct FnResource<Fun, Ret, Args> {
pub func: Fun,
marker: PhantomData<(Fun, Ret, Args)>,
}
// Implement `FnRes` for `FnResource` with different type parameters.
impl<Fun, Ret, C, A> FnRes for FnResource<F, Ret, (&mut C, &A)>
where
Fun: Fn(&mut C, &A) -> Ret + 'static,
Ret: 'static,
{
// ..
}
Once a function or closure is wrapped in a FnResource
, then we can hold different functions and closures as Box<dyn FnRes>
.
Instantiation
To make it easy to transform functions and closures into Box<dyn FnRes>
, the IntoFnResource
and IntoFnRes
traits are provided generically:
// Turns a function or closure into the `FnResource` wrapper type.
impl<Fun, Ret, C, A> IntoFnResource<Fun, Ret, (&mut C, &A)> for Fun
where
Fun: Fn(&mut C, &A) -> Ret + 'static,
Ret: 'static,
{
fn into_fn_resource(self) -> FnResource<Fun, Ret, (&mut C, &A)> {
FnResource {
func: self,
marker: PhantomData,
}
}
}
// Turns a function or closure into a `Box<dyn FnRes>`
impl<Fun, Ret, C, A> IntoFnRes<Fun, Ret, (&mut C, &A)> for Fun
where
Fun: Fn(&mut C, &A) -> Ret + 'static,
Ret: 'static,
A: 'static,
B: 'static,
FnResource<Fun, Ret, (&mut C, &A)>: FnRes<Ret = Ret>,
{
fn into_fn_res(self) -> Box<dyn FnRes<Ret = Ret>> {
Box::new(self.into_fn_resource())
}
}
Usage:
let fn_res = (|c: &mut u32, a: &u32| *c += *a).into_fn_res();
Invocation
Now that we can easily hold different types under the Box<dyn FnRes>
abstraction, the remaining issue is to invoke the logic through that common abstraction.
To do this, FnRes
has a method that takes a &Resources
parameter, and each trait implementation will handle parameter fetching and function invocation:
// Trait for all logic types to implement.
pub trait FnRes {
/// Return type of the function.
type Ret;
/// Runs the function.
fn call(&self, resources: &Resources) -> Self::Ret;
}
// Implementation to fetch the parameters from `Resources`, and invoke the function.
impl<Fun, Ret, C, A> FnRes for FnResource<F, Ret, (&mut C, &A)>
where
Fun: Fn(&mut C, &A) -> Ret + 'static,
Ret: 'static,
{
pub fn call(&self, resources: &Resources) -> Ret {
let c = resources.borrow_mut::<C>();
let a = resources.borrow::<A>();
(self.func)(c, a)
}
}
With this, consumers are able to invoke functions generically:
let mut resources = Resources::new();
resources.insert(A::new(1));
resources.insert(C::new(0));
let fn_res: Box<dyn FnRes> = (|c: &mut C, a: &A| *c += *a).into_fn_res();
// Generically invoke logic with generic data type.
fn_res.call(&resources);
Notably:
- Parameters must be
&T
or&mut T
, asT
is owned byResources
. - Parameters must be distinct types, as it is invalid to borrow
&mut T
twice. - Borrowing and invocation is implemented by the function type –
FnResource
in this example. Out of the box, this lives in theresman
crate instead offn_graph
.
1 resman
implements FnRes
for functions with up to 6 parameters.