A struct member function that transforms the struct into another struct. Is this a good pattern?

I have a trait that implements a builder for struct Foo. The builder type has a field, which is a function, that transforms itself to Foo. I was wondering if this a good pattern and if there are any reasons for not doing this.

#[derive(Default)]
struct Foo {}

trait FooBuilder {
    fn build(self) -> Foo;
}

struct FooBuilderType {
    transform: fn(Self) -> Foo,
}

impl FooBuilder for FooBuilderType {
    fn build(self) -> Foo {
        (self.transform)(self)
    }
}

impl Default for FooBuilderType {
    fn default() -> Self {
        Self {
            transform: |_| Foo::default(),
        }
    }
}

impl FooBuilderType {
    fn new() -> Self {
        Self::default()
    }

    fn with_transform(mut self, f: fn(Self) -> Foo) -> Self {
        self.transform = f;
        self
    }
}

The main reason for my concern is that it is easy to cause a stack overflow by having the transformation be an infinite recursion and this does not get detected at compile time. Rust Playground

fn main() {
    let builder = FooBuilderType::new().with_transform(|x| (x.transform)(x));
    let _ = builder.build();
}

Within the builder pattern, having a fallible build() method is fine. For infallible transformation on the other hand, I'd implement the From trait.

What's your motivation for doing this?

The struct that the builder is for is the result of a system call. I wanted to emulate the system call for testing. So the implementation of the actual builder will perform the system call, while the mock implementations will call a custom function.

Looks like you're mixing two ways of having multiple implementations to me: indirection through function pointers and having a trait. You may be able to just be generic over the trait, similar to how I/O mocking is often done by being generic over readers and writers.

1 Like

Perhaps just make the transform field private so that users of this type cannot "accidently" call it

pub struct FooBuilderType {
    transform: fn(Self) -> Foo,
}

There also doesn't seem to be a need for the FooBuilder trait. If you have multiple different builders, they can each have their own non-Trait build method returning a Foo.

1 Like