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.

%3fn_res_traittraitFnRes {}fn1Fn1fn_res_trait->fn1fn2Fn2fn_res_trait->fn2fn3Fn3fn_res_trait->fn3fn4Fn4fn_res_trait->fn4fn5Fn5fn_res_trait->fn5
// 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 FnResource1:

%3fn_res_traittraitFnRes {}fn_resourcestructFnResource<Fun, Ret, Args> {}structFnResource<Fn1, (), (&mut A, &mut B)> {}structFnResource<Fn2, (), (&mut C, &A)> {}structFnResource<Fn3, (), (&mut C, &B)> {}structFnResource<Fn4, (), (&mut D, &C)> {}structFnResource<Fn5, (), (&A, &B, &C)> {}fn_res_trait->fn_resource
// 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, as T is owned by Resources.
  • 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 the resman crate instead of fn_graph.

1 resman implements FnRes for functions with up to 6 parameters.