How to implement protected visibility?

Hey there!

I'm trying to model something that usually, in OOP languages, is implemented using abstract class and protected visibility, and I'm not quite sure how to achieve kinda the same thing in Rust.

Let me explain.

The idea is to allow users to import a library by yours truly, in order to write some structs like so:

pub struct MyStruct {
    some_value: i64
}

pub struct SomeValueUpdated(i64);

impl MyStruct {
    pub fn update_some_value(self, new_value: i64) -> Self {
        // NOTE: here we could do self.some_value = new_value,
        // but the purpose is to avoid direct state change.
        //
        // The idea instead is to have something like:
        self.record_that(SomeValueUpdated(new_value))
    }
}

impl MyLibraryTrait for MyStruct {
    type Action = SomeValueUpdated;

    fn apply(self, action: Self::Action) -> Self {
        // This is the actual state change happens:
        self.some_value = action.0;
        self
    }
} 

This record_that method should be provided by said library, and that method should internally trigger a state change of Self, and return the new Self value.

In OOP languages you could do something like so:

abstract class MyLibraryTrait {
    // here you can expect some state about what was recorded

    abstract function apply(action)
    protected function record_that(...) { ... }
}

class MyStruct extends Recorder {
    private some_value;

    public function apply(action) {
        if action instanceof SomeValueUpdated {
            this.some_value = action.new_value;
        }
    }

    public function update_some_value(new_value) {
        this.record_that(SomeValueUpdated(new_value));
    }
}

Do you have any idea on how I would be able to model this in Rust? :thinking:

Model... what, exactly?

You've presented a solution to some problem that appears in a hypothetical library written in some other language (maybe JS? Be aware that even different class-based languages have subtle differences in how they deal with stuff like this). What's missing is the actual problem. You may be thinking that any design that works in (let's say) C++ will work just as well in Rust, but that's just not the case – you need to design with the constraints of the implementation language in mind. C++ doesn't have enums, Rust doesn't have classes -- so the solutions will be different, because the problems are different.

At this point, I might point out that protected isn't really necessary in Rust because private (i.e., non-pub) items are visible anywhere in the module where they're defined. I could add that abstract classes are more like generic structs than traits, structurally speaking. But I'd be speculating and that seems like a waste of time.

So let's start with the problem you're trying to solve. What kind of library is this? When you strip away the abstract classes and the inheritance and methods etc., what does it actually do?

5 Likes

Hey @trentj,

Thanks for the answer!

Let me specify a bit more: the idea is to allow state change of self in public methods not through direct self reference (e.g. self.some_value = new_value from example above), but through other objects (e.g. SomeValueUpdated from example above) and apply the actual state change in a trait method (the apply method from example above).

This is so that you can keep track of the list of changes that happened to a certain object. (For reference, this is Event Sourcing).

In other languages, this is typically implemented with an abstract class that:

  • Records state change objects into a slice/array/list,
  • Calls apply method on self using the state change object received and updates self state

Hope that's clear enough :grinning_face_with_smiling_eyes:


In Go, which is closer to Rust than OOP languages, a way to implement this is a mix of embedding (composition) + interface with private methods.

In Rust, traits have only public methods by definition.

An idea that came to mind is:

  • Write MyStruct methods that change state return a Vec<StateChangeObjects>, instead of using a record_that "protected" method
  • Use a macro on MyStruct to generate a wrapper, call it Root<T>
  • Root<MyStruct> exposes the same method names as MyStruct, but returns Self instead of Vec<StateChangeObjects>, so that the whole job of recording things is offloaded to Root

All this really means is that the privacy of all the items in a trait are the same. You can have a private trait (or a pub trait in a private module), and only code that can access the trait definition will be able to call its methods.

2 Likes

This doesn't help though.
It could help if Rust was supporting embedding/composition, so that I could automatically implement private trait/methods for a user type, but there ain't such thing :slightly_frowning_face:

How about something like this:

// Library code:

pub trait Updatable {
    type Action;
    
    fn apply(&mut self, action: &Self::Action);
}

pub struct Recorder<U: Updatable> {
    actions: Vec<<U as Updatable>::Action>,
    data: U,
}

impl<U: Updatable> Recorder<U> {
    pub fn record_that(&mut self, action: <U as Updatable>::Action) {
        self.data.apply(&action);
        self.actions.push(action);
    }
}

// User code:

struct MyStruct {
    some_value: i64
}

pub struct SomeValueUpdated(i64);

impl Updatable for MyStruct {
    type Action = SomeValueUpdated;

    fn apply(&mut self, action: &Self::Action) {
        // This is the actual state change happens:
        self.some_value = action.0;
    }
} 

pub struct MyStructRecorded {
    recorder: Recorder<MyStruct>,
}

impl MyStructRecorded {
    pub fn update_some_value(&mut self, new_value: i64) {
        self.recorder.record_that(SomeValueUpdated(new_value))
    }
}

It's a bit verbose since you'll need to define two structs, MyStruct and MyStructRecorder, one with the data and the update logic and the other which just forwards to Recorder::record_that. For simplicity I changed all the self to &mut self but it should be possible to implement the same pattern with self if you really need that. I've also changed the apply's action parameter to take a shared reference since the actual action will end up stored in a Vec and that requires ownership.

6 Likes

Actually, this is quite similar to a transaction system I wrote recently. When I get to my other computer, I’ll extract some more complete code. This should give you an idea of how it works; for brevity I've left out other variants of apply and the UndoLog combinators to support them.

EDIT: Added implementation details & playground link.

use either::Either; // 1.6.1

pub trait RevertableOp<T: ?Sized> {
    type Err: std::error::Error;
    type Log: UndoLog<T>;

    fn apply(self, target: &mut T) -> Result<Self::Log, Self::Err>;
}

pub trait UndoLog<T: ?Sized> {
    fn revert(self, target: &mut T);
}

#[must_use]
pub struct Transaction<'a, T: 'a + ?Sized, Undo = (), E = std::convert::Infallible> {
    root: &'a mut T,
    status: Status<Undo, E>,
}

pub enum Status<Undo, E> {
    Poisoned(E),
    Active(Undo),
}

impl<'a, T: 'a + ?Sized> Transaction<'a, T> {
    /// Start a new transaction protecting the object `root`
    pub fn start(root: &'a mut T) -> Self {
        Transaction {
            root,
            status: Status::Active(()),
        }
    }
}

impl<'a, Root: ?Sized + 'a, Undo: UndoLog<Root>, E> Transaction<'a, Root, Undo, E> {
    /// Discard the undo log, and return any error that occurred.
    pub fn commit(self) -> Result<(), E> {
        match self.status {
            Status::Poisoned(err) => Err(err),
            Status::Active(_) => Ok(()),
        }
    }

    /// Process the undo log to return the root to its original state.
    /// Discards errors.
    pub fn revert(mut self) {
        if let Status::Active(undo) = self.status {
            undo.revert(&mut self.root)
        }
    }

    /// Apply a `RevertableOp`
    pub fn apply<Op: RevertableOp<Root>>(
        self,
        op: Op,
    ) -> Transaction<'a, Root, (Op::Log, Undo), Either<Op::Err, E>> {
        match self.status {
            Status::Poisoned(e) => Transaction {
                root: self.root,
                status: Status::Poisoned(Either::Right(e)),
            },
            Status::Active(undo) => match op.apply(self.root) {
                Ok(log) => Transaction {
                    root: self.root,
                    status: Status::Active((log, undo)),
                },
                Err(e) => {
                    undo.revert(self.root);
                    Transaction {
                        root: self.root,
                        status: Status::Poisoned(Either::Left(e)),
                    }
                }
            },
        }
    }
}

/// The empty tuple is a No-op undo for everthing
impl<Root: ?Sized> UndoLog<Root> for () {
    fn revert(self, _: &mut Root) {}
}

/// A tuple of two logs (A,B) first reverts log A and then log B
impl<Root: ?Sized, A: UndoLog<Root>, B: UndoLog<Root>> UndoLog<Root> for (A, B) {
    fn revert(self, root: &mut Root) {
        self.0.revert(root);
        self.1.revert(root);
    }
}

(Playground)

1 Like

This is more or less what I had in mind, but I didn't want users to create the second MyStructRecorded part.

However, I think I got an idea on how I could solve this now, will experiment and then post my solution for posterity :grinning_face_with_smiling_eyes:

Much appreciated :bowing_man:

1 Like

How about pub(crate)?

To reiterate some high-level information about translating OOP to Rust that you seem to already know:

  • abstract - Instead of simply requiring methods, traits can have a default implementation of their methods. They cannot stop the implementer from overwriting it, though.

  • protected- Rust traits do not have their own visibility scope, nor do types. Instead, all visibility is based on modules.

It sounds like you want behavior (a function) to be available to a trait for it to use in its methods. But you don't want that function to be public, you want it visible only within the trait's default method bodies. Users could still write their own implementations of these methods but they would have to do so without the "protected" functions that you designed to do the job.

If this is correct then the solution is to separate the "protected" function from the trait but keep them in the same module. Then make the trait public and keep the functions private. I wrote an example where the user writes a getter/setter to suit their struct and then the library "magically" does the important things.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=45cad43b81f92aaf7b2ec01cc52229e6

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.