Help introspecting for specialization when abstracting via Traits

I'm using traits as an abstraction layer in earnest for the first time, so apologies in advance if I do not do a great job describing the problem well.

Problem statement:
I would like to call methods that may exist on only some structs implementing a particular trait, when passing them around as dyn TraitObject (but not using const generics, see below). This means I need to introspect, I suppose at runtime, on the trait object.

Specific Example
Here is a link to a minimal example:

In function f, I need to perform some arbitrary operation that is only applicable to some specific backend, for example, the Sql backend. However, f received the backend as a trait object and AFAICT there is no runtime information on its type.

Potential Solutions
Here are some potential solutions as I see them; I'm curious to know (a) what's most idiomatic, (b) what are the strengths and limitations of these proposed solutions, and (c) What other solutions am I missing?

  1. Rewrite the entire thing to use a single struct instead of a trait, and include a type parameter on the struct. Write multiple impl on the struct with const generic parameters specialized for enum variants or marker types. Use of const generic algebraic types seems like it could be elegant, but this solution is rejected due to need to use stable Rust (feature is gated on #![feature(adt_const_params)].
  2. Move the type-restricted method out of SqlBackend and into the trait; have the impl for an irrelevant type cause runtime error. Include a trait method that returns an enum variant indicating type of self. f can then match and only call for specific variants (which imply specific types).
  3. Same as above (move type-restricted method into trait), but do not include trait method for introspection and instead put f in the trait so it can be specialized. This means for inapplicable backend types, call would be a noop.
  4. Should I somehow use GATs? Why GATs? - Generic Associated Types Initiative
  5. ???

Thanks in advance -- I have previously written C++ and D where one can trivially specialize templates at compile-time for specific enum values and const generic ADTs sure would be nice to have now.

Does this fit your need? downcast_rs - Rust

1 Like

Here's a DIY version of downcasting for your playground;[1] there's more words about the process here.

+trait AsAny: Any {
+    fn as_any(&self) -> &dyn Any;
+}
+
+// N.b. only applies to sized types
+impl<T: Backend> AsAny for T {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+trait Backend: AsAny {
-trait Backend {
    if let Some(sql) = backend.as_any().downcast_ref::<SqlBackend>() {
        let conn = sql.conn();
        println!("{conn}");
    }

The main downside is probably that it imposes a 'static bound on implementations.


  1. Variation on 2: add f to the trait with a default body that does the no-op version, e.g.
    trait Backend {
        fn conn(&self) -> Option<String> {
            None
        }
    
  2. Implement the type for an enum and don't use dyn Backend
    • Main downside is you have to know all implementors, but it sounds like you do
    • Another downside is the amount of boilerplate, but there are crates that help if you prefer

A downside of 3 is that GATs make a trait non-object-safe and is thus a non-solution (and I'm not sure how they would have helped); additionally if they do become supported, they'll presumably have to be specified on the trait object like (non-generic) associated types are (e.g. dyn Iterator<Item = String>).


  1. references only ↩ī¸Ž

4 Likes

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.