Fn, closure, Trait, or Generics for reusable objects

What logic do you use to choose between Fn, Closures, Traits, or Generics when you need to utilise a common/reusable object.

For example, I might have some code which knows how to Retry an operation. Of course, it has no knowledge on what the operation is, it simply needs to signal to the consumer that it worked (and maybe some state about the number of retries it took etc.) or failed.

Would you have the Retry code take in a Retryable trait, an Fn or Closure? If a trait, would you pass the trait into the Retry's fn or use generics, i.e. Retry?

I'll know some code up if it helps but the questions a general design one rather than some specific code.

I think, to summarise your question, you are wondering the appropriate solution between the following four choices to make your retry function:

  • Pass a function
  • Pass a closure
  • Pass a struct "object". For this you have two choices
    • Make the function parameter a trait object
    • Make the function generic, and make it a trait bound
1 Like

Nice succinct summary - yes. I'm interested to know how to decide. In Java land it would be an interface. I'd lean towards a trait here too and probably specialise Retry<T: Retryable> but I don't know why.

Actually no, sorry, I will have a Retry struct with number of times, timeouts etc. and an impl Retry { fn retry(...)}

This is a design problem, and so there is no one correct answer. However, some pointers:

  • The closure vs function decision boils down to whether you want to "re-use" the function. Closures cannot be re-used. But closures also capture the environment, so it is sometimes a lot more convenient.
  • The generic-with-trait-bound vs trait object decision boils down to whether you need to store "objects" of different types but same trait bound. If that's the case, you need a trait object. Otherwise, it can be mostly be circumvented. In particular, fn something<T: Retry>(r: T) {} is actually better than fn something(r: Box<dyn Retry>) {}, because you are avoiding the vtable. It's not much of a cost, but it is non-zero.
  • The function vs trait decision depends on whether you need to store extra information to perform the operation. If you do need to store extra information which cannot be passed when the operation is performed (perhaps because you won't have it handy then or it is not required in all cases), then you need to use traits - the struct on which you implement the trait can store this information persistently. A closure can store it, but there are lifetime restrictions.
2 Likes

Absolutely perfect answer - exactly what I was looking for.

Am I right in thinking that Box<dyn Retry> avoids the compiler monomorphising for each impl of Retry, so compilation might be slower if there are many Retry?

I would strongly advice against worrying about compilation times and choose the option that works most efficiently in terms of performance, readability and ergonomics.
Build times can be subsumed with better hardware - and anyway, your production builds will probably be built on some CI/CD system.
Tuning compilation times is counter-productive unless you are interested on working on cargo/rustc itself.

2 Likes

The most general solution is a custom trait, because you can implement a trait for any type at all.

In contrast, if you restrict your "retriable" argument to be a fn or a closure (something that implements Fn…), then you won't be able to call it with anything else but a fn or a closure. However, if you accept a custom trait specific to the behavior you need, then you can just implement that trait for custom objects as well as fn and closures.

This doesn't make sense: generics in Rust are achieved via traits.


By the way, objects and closures are duals: they do exactly the same thing, but in the case of an object, the state/value is emphasized conceptually, and in the case of a closure, the behavior/functional nature is emphasized instead. There is no difference in expressive power between them, and closures are implemented using structs internally.

No. The compiler will still have to emit a set of concretely-typed functions for each and every concrete type you will create a dyn Retry from.

1 Like

Understood- thanks. My compile times are in the 20-25 minute range (per platform) so every little helps :wink:

Yes, I wasn't clear. I meant passing in a dyn Retryable to an fn or specialising the Retry object itself via Retry<T: Retryable>.

In this case the answer is that starting with generics is going to be more idiomatic. You rarely see a function that accepts a &dyn Anything. Trait objects are more useful for type-erasing containers, when you don't want generic arguments to leak into the API of a type, for example.

1 Like

While dyn Trait does not reduce the number of implementations, it is itself a concrete type. So if you have a dyn Trait-taking function instead of a generic-taking function, the former will have one instantiation, while the latter will monomorphize.

There are niche situations where this is a concern (e.g. extremely memory constrined systems), but as others said, usually the reasons for using dyn Trait are more about type erasure.

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.