Passing a closure to a method on a trait

I've got a trait in my application for working with users. I'm using a trait so that the clients aren't dependent on the actual implementation details - i.e. this way my HTTP handlers are dependent on State<Box<dyn UserService>> and not State<UserServiceImpl<Database, MessageQueue>>. That has been working great so far, up until now.

I'm now adding a method to my UserService to allow me to update user records. I'm doing this in a generic manner - to make it easy to implement PATCH requests - so my thinking was to do this:

trait UserService {
    fn update_user<F: FnOnce(UserEntity) -> UserEntity>(
        &self,
        user_id: &UserID,
        updater: F,
    ) -> Result<UserEntity, UpdateUserError>;
}

The idea is that I can then use this as follows:

let result = service.update_user(&user.identity.id, |user| {
    if let Some(new_email_address) = new_email_address {
        user.email_address = new_email_address;
    }
    if let Some(new_display_name) = new_display_name {
        user.display_name = new_display_name;
    }
});

And it now doesn't work. Because I now have a trait with a generic parameter, I can't create instances of it correctly. Instead I get:

error[E0038]: the trait `service::service::UserService` cannot be made into an object
  --> crates/users/src/service/implementation.rs:21:6
   |
21 | ) -> Box<dyn UserService> {
   |      ^^^^^^^^^^^^^^^^^^^^ the trait `service::service::UserService` cannot be made into an object
   |
  ::: crates/users/src/service/service.rs:4:11
   |
4  | pub trait UserService: Send + Sync {
   |           ----------- this trait cannot be made into an object...
...
43 |     fn update_user<F: FnOnce(UserEntity) -> UserEntity>(
   |        ----------- ...because method `update_user` has generic type parameters
   |
   = help: consider moving `update_user` to another trait

This seems like it can't be an uncommon desire, so how do I best do this?

Cheers

You can't have generic parameters on trait objects. The only way to solve this is by changing the signature to

fn update_user(
        &self,
        user_id: &UserID,
        updater: &mut dyn FnMut(UserEntity) -> UserEntity,
    ) -> Result<UserEntity, UpdateUserError>;

You can call it by passing references to closures.

Ok - That's got me confused.

That's not far off what I originally started with - I just had &dyn FnOnce instead. This gives:

error[E0161]: cannot move a value of type dyn std::ops::FnOnce(model::user::UserEntity) -> model::user::UserEntity: the size of dyn std::ops::FnOnce(model::user::UserEntity) -> model::user::UserEntity cannot be statically determined
  --> crates/users/src/service/implementation.rs:52:23
   |
52 |         let updated = updater(user);

However, using what you've said works fine. However, it requires both &mut and FnMut for it to work otherwise I get errors.

I can understand why &mut is needed from the error I get without that, but I can't see why FnMut works and FnOnce doesn't...?

If you have a get_user and set_user method on the trait, you can do this:

impl dyn UserService {
    fn update_user<F>(&mut self, user_id: &UserID, updater: F)
    where
        F: FnOnce(UserEntity) -> UserEntity
    {
        let user = self.get_user(user_id);
        self.set_user(user_id, updater(user));
    }
}

Because FnOnce consumes its own self parameter, therefore it is impossible to call through a reference (you'd need to move out of the referent which is not allowed).

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.