Any workaround for generic mutability

In the code below, process and process_mut perform the same operations and should ideally be generic over the mutability of Context. Is there any way to achieve having just one process and one CallBack type (both of which are parameterized over mut).

pub struct Context {
    counter: u64,
}

pub type Callback = fn (&Context);
pub type CallbackMut = fn (&mut Context);

pub fn process(a: &Context, callback: Callback) {
    println!("{}", a.counter);
    callback(a);
}

pub fn process_mut(a: &mut Context, callback: CallbackMut) {
    println!("{}", a.counter);
    callback(a);
}

pub fn main() {
    
}

One way to achieve what I want is through macro_rules!, but it would be nice if I could do this without macros.

Related discussion: Generic mutability parameters

so if I understand this correctly, this function only need shared reference to do its own work, but wants to be transparent to the actual type of the argument so it can pass it to other functions?

I don't know the actual use case, but just for this particular example, it's enough to be generic over the argument a, e.g. Deref<Target=Context> could do it, since both &Context and &mut Context implemented Deref<Target = Context>.

fn process_generic<Ref, Callback>(a: Ref, callback: Callback)
where
	Ref: Deref<Target = Context>,
	Callback: FnOnce(Ref),
{
	println!("{}", a.counter);
	callback(a);
}

playground

1 Like

Thank you for the response.

so if I understand this correctly, this function only need shared reference to do its own work, but wants to be transparent to the actual type of the argument so it can pass it to other functions?

Yes, this is correct.

I don't know the actual use case, but just for this particular example, it's enough to be generic over the argument a, e.g. Deref<Target=Context> could do it, since both &Context and &mut Context implemented Deref<Target = Context>.

This works, but only for the exact snippet I posted (my bad). Particularly, if callback(a); happens before the print, then this ceases to work :-(. The compiler suggested that I add a Copy constraint too, but that won't help with the &mut Context case.

Using my generic-mutability crate:

use generic_mutability::{ GenRef, Mutability };

pub struct Context {
    counter: u64,
}

pub fn process<M: Mutability>(a: GenRef<'_, M, Context>, callback: impl FnOnce(GenRef<'_, M, Context>)) {
    println!("{}", a.counter);
    callback(a);
}

It does require using (but not witing) macros in several cases, but not in this one!


If you want to swap the order of operations, you need to use a manual reborrow:

use generic_mutability::{ GenRef, Mutability };

pub struct Context {
    counter: u64,
}

pub fn process<M: Mutability>(mut a: GenRef<'_, M, Context>, callback: impl FnOnce(GenRef<'_, M, Context>)) {
    callback(GenRef::reborrow(&mut a));
    println!("{}", a.counter);
}

exactly, and that's the limitation of a type based design. you cannot make a generic type complete "transparent": rust functions are type checked at definition time (as opposed to "instantiation" time, or "monomophizing" time). in the case of mut ref vs shared ref, it might be possible to come up with a solution for specific cases, but it's impossible in general.

I think the deeper problem is the "control inversion" nature of the design, yet rust's type system is not capable for this level of abstractions. if you could avoid the need of "forwarding" the parameter, and limit the scope of the function to just what it was actually suposed to do, then the problem simply doesn't exist! this function don't even need to be replicated or be generic, just a regular reference is good.

so, I think it'd be better to refactor the code, moving away from a callback based design, and maybe hoist the decision of "high level" control flow up to the caller.

but again, without context of the real application, this is very hypothetic and may not be applicable at all. you might just go with a macro based solution if that suits.

an aside

rust generics are unlike C++ templates (I'm talking about it before concept was added to the language). C++ templates are only checked at instantiation time, while at definition time, they just needs to be syntactically correct.

in this regard, C++ template is more like rust's macros than generics.

also, the so-called "perfect forwarding" in C++ is, IMO, a very cumbersome "solution" by abusing several esoteric language rules [1]


  1. template type deduction for rvalue references in the context of reference collapsing, yadiyadiyada â†Šī¸Ž

2 Likes

Thank you for the detailed response. I'll probably go with the macro based solution then.

Thank you, I'll give this a try.