Method chaining for both self and &mut self

My struct has methods that can be chained like so:

struct Thing {}

impl Thing {
    fn new() -> Self { Thing {} }
    fn set_foo(self) -> Self { self }
    fn set_bar(self) -> Self { self }
}

fn main() {
    let thing = Thing::new()
        .set_foo()
        .set_bar();
}

However, I would like to be able to call those methods with both self and &mut self. I tried to do this with traits but unfortunately the compiler doesn't like self to be a generic type:

trait SelfOrMutSelf {}
impl SelfOrMutSelf for Thing {}
impl SelfOrMutSelf for &mut Thing {}

struct Thing {}

impl Thing {
    fn new() -> Self { Thing {} }
    // error[E0801]: invalid generic `self` parameter type: `T`
    fn set_foo<T: SelfOrMutSelf>(self: T) -> T { self }
    fn set_bar<T: SelfOrMutSelf>(self: T) -> T { self }
}

I know that instead, I could:

  • Make a separate set of methods with slightly different names, but this would needlessly complicate the API
  • Make the methods take &mut self only, but then I'd have to disallow the original method chaining pattern and replace it with a very slightly more verbose alternative

This is only a minor inconvenience but I'm wondering if there are any better solutions.

i would strongly disagree on that. taking self or &mut self does completely different things, and so making them the same name would actually make the API much more complex, as you always have to amke sure that you are calling the right one.

the owning one should probably be called with_foo and the &mut one set_foo.

edit : you can do what you want with a trait, it's just a horrible idea

Functions that take a &mut T and return the same &mut T seem like an anti-pattern to me given that you could simply return nothing, let the borrow expire and the caller can then still use the reference. If you want to return a modified object, return it by value.

I think I see what you’re saying but I already have the owning methods that are chainable and I’d like to keep the &mut ref API the same for the sake of consistency, even if it’s not technically necessary. But even if it didn’t return self, I would still have the problem of not being able to reuse the same method names.

I think I see what you’re saying, perhaps you’re right that this is not the best idea. But still, if you know how to do this with a trait, an example would be much appreciated.

That pattern is commonly used in builders. For example OpenOptions in std::fs - Rust.

here you go

impl Thing {
    fn new() -> Self { Thing {} }
    fn set_foo(&mut self) {  }
    fn set_bar(&mut self) { }
}

use std::borrow::BorrowMut;
trait ThingInnit : BorrowMut<Thing> + Sized { 
    fn foo(mut self) -> Self {
        self.borrow_mut().set_foo();
        self
    }
    fn bar(mut self) -> Self {
        self.borrow_mut().set_bar();
        self
    }
    
}
impl ThingInnit for Thing {}
impl ThingInnit for &mut Thing {}
3 Likes

Ah, perfect, thank you very much! I think I’ll experiment with this a bit and see if it turns out useful or confusing.

I think that's bad, confusing design. It should have used either pass-by-value, or mutation without chaining.

I think it works fine if all the options are Copy.

I am not sure what you mean. Both pass by value and pass by reference works for non-Copy types.

The only reason that std::fs::OpenOptions::new().chained().methods().open() is a one-liner is because .open() takes &OpenOptions.

Conceivably, a builder with options that cannot be cheaply cloned could take &mut Self for its final build method (perhaps storing !Copy fields in Options that can be taken) and panic or return an error if that method is called twice, but that doesn’t seem ideal.

The same one-liner works with pass by value builder methods and pass-by-reference .open():

struct Options {
    name: String,
}

struct File;

impl Options {
    fn new() -> Self {
        Self {
            name: String::new(),
        }
    }
    fn name(self, name: String) -> Self {
        Self { name, ..self }
    }
    fn open(&self) -> File {
        File
    }
}

fn main() {
    let file = Options::new().name(String::from("hello")).open();
}

I prefer &mut. It's less annoying when you want to conditionally apply builder options. (But builders should take note from cc and accept iterators, since you can call them with an Option<T>.) Taking self and returning Self encourages typed builders which are even harder to use with conditional code.