Portability and Arc

I have a fairly simple Config struct (string, number and hashmap fields) that's widely used across different parts of a project, the API, background tasks, and so on. I want to wrap it in an Arc to prevent having to clone it numerous times since it's created once and then never modified. Unfortunately if I do that now everything that uses it needs an Arc<Config> type for function arguments, struct fields, etc instead of just Config which means that any other applications that use individual components will also have to wrap Config in an Arc even if they're only using it for one thing.

Is there any way I can rewrite things to use either a Config OR an Arc<Config> instead of only one or the other? My first thought was to use a Trait, but traits don't let you specify fields and Config is literally only fields so that's not really an option.

Take a <T: Borrow<Config>>.


Borrow is what I would say for generic structs and functions that deal with those structs, but for a function that just uses the contents of Config for its own purposes, you should probably just let it take &Config and let the caller figure out how to provide it (note that due to deref coercion, &foo will work whether foo is Config, Arc<Config>, &Config or anything else that transitively derefs to Config).

If you have a stack of functions such that f calls g calls h calls i, and they all take T: Borrow<Config>, you need to call .borrow() in each one to pass a &Config down the stack (or else you need to also require T: Clone, but that's ugly if T is actually Config). If they all take &Config you can just pass the reference and let the guy at the top of the stack (who calls f) figure out how to get it.

T: Borrow<Config> allows you to do whatever you want with T: drop it, leak it, put it in a struct, return it, send it to another thread and back... but most of those things will require additional bounds on T, like Send, Clone, or 'static. If that's not what a function is conceptually doing with T, it should perhaps just accept &Config.


If it were me, I'd store the config in long-lived objects as an Arc<Config> field, but for short-lived things like functions I'd just pass in a &Config as an argument.

That way the majority of your codebase just works on a reference to the config, with only a couple objects at the very top of the call stack knowing that the Config being referred to actually came from a Arc<Config>.

You probably don't want to make your code generic over anything that can give you a Config (e.g. any T: Borrow<Config> like &Config, Arc<Config> or Box<Config) because making things generic has a big impact on readability.

1 Like

Not sure I follow this logic. If my code doesn't care whether it's a Config or an Arc<Config> why would I want to restrict it to requiring an Arc<Config> even in situations where an Arc is unnecessary?

Because it's simpler. Generic functions would slow down the compilation(may not be significant), can bloat the binary(similar), be harder to read(even a bit), and prevents type inference(can be painful).

I'd say, spend the complexity budget wisely. You're probably doing something more interesting that will need a few generic parameters, and using another one for configuration might not be the best for you or the other users of the code (in terms of ease of understanding, reading, writing the code).

1 Like