• nous@programming.dev
    ·
    9 months ago

    Or do we use an Arc such that our dependent services hold onto an Arc>, allowing concurrent access of the owned resource?

    Arc is already heap allocated, there is no need or point in Boxing an Arc. They serve the same purpose except Box is single owner and Arc is multi owner. Box is not the only smart pointer that supports trait objects: Arc is also allowed, same goes for Rc and other smart pointer types.

    Though the answer to that question as it stands is: it depends. Both methods they suggest are valid approaches with different tradeoffs. Another is to just forgo trying to share a single field and share the whole object, ie have Arc or &Service instead. Or clone the whole thing between threads, or many other patterns that suite different needs. A lot depends on what this service is and how it needs to be passed around the application and how long it lives for. A lot of frameworks already give you good patterns for this.

    For instance axum has a way to pass around state to handlers, generally you pass ownership of the resource to axum and let it clone as required. Typically this means using an Arc> for things that are expensive to clone. Though a lot of types you share this way (like database connections) deal with that internally and so are already cheap to clone in these usecases.

    While we could have written this as a service where UserRepo is an injected value, doing so would introduce the complexities we’ve already explored

    Does it? OOP style methods are basically just syntactic sugar for the most part. You can largely interchange functions and methods all you want to. Also, pure functions does not mean to most people to what your example is showing. Typically a function is considered not pure if it modifies any of its arguments. So by taking a &mut as an argument you make the function not pure.

    Given that I can only assume you mean raw functions vs methods on types. At which point there is not as big a differences as they think. For instance, their example of

    async fn handle_session_completed(
        user_repo: &mut impl UserRepo,
        session: &CheckoutSession,
    ) -> anyhow::Result<()> {
    

    Can easily be written as (well, at least if you ignore the issues around async traits which is a technical limitation that is being worked on, for now the #[async_trait] macro helps a bit, and hopefully soon this will be solved at a language layer)

    trait UserRepo {
        async fn handle_session_completed(
            &mut self,
            session: &CheckoutSession,
        ) -> anyhow::Result<()> {
    

    And this form can even be called like a the former:

    UserRepo::handle_session_completed(&mut user_repo, &session).await
    

    There is little difference between a method on an type and a function that takes a argument to a type in rust. At least not in terms of how that author talks about them. Though I would lean towards the raw function here due to the async issues with traits. But in a lot of non-async contexts both are equally nice to use and would probably lean more on the trait/type method instead. More and more I just think of methods as type namespaced functions more than anything else, that is basically how you use them 95+% of the time.

    But yeah, overall don't write any language like it is a different language. Same goes for trying to write java like it is C or python like it is rust. Learn the patterns of the language you are using.