1. 20
  1. 6

    Good observation. I like to generalize this to NVI (no-virtual interface) – try not to expose traits as the interface for the user. Wrap traits in concrete types and base interface on that types. Traits are great as an extension point, but are rather unergonomic as a primarily interface.

    Another observation along similar lines. Lets say you have a network and a database crate, and you want to plug one into another, without direct dependency. Let’s say we want the net to use the db. We could put trait Db in the db, but then net would have to depend on db, even if it uses only the interface. We could put trait Db in the net, but then, to write impl net::Db for db::MyDb we’d have db depend on net due to coherence. So the obvious solution is to pull trait Db to some third crate, core_interfaces, and make both db and net depend on that. This kind works, but doesn’t scale – the core_interfaces would soon include a laundry list of poorly defined interfaces. A more verbose solution which I non-the-less think works better for large projects is to define trait Db in the net, and, instead of implementing it directly for db::MyDb, write the implementation for a new-type wrapper in the leaf binary crate which ties net and db together. This leads to better factoring, as, if you have not only the net, but also, say email, there could be separate trait net::Db and trait email::Db, which specify exactly what they need.

    1. 3

      Are there recommended open-source projects to look at that use this pattern?

      I’d love more blog posts on patterns like this.

      1. 1

        I like to generalize this to NVI (no-virtual interface) – try not to expose traits as the interface for the user. Wrap traits in concrete types and base interface on that types. Traits are great as an extension point, but are rather unergonomic as a primarily interface.

        So the article is talking about the specific situation in which the interface to the trait is considered private, so it doesn’t make sense for the user to implement the trait themselves. In that case, yes, you certainly shouldn’t expose that trait to the user. It also means that the trait is closed instead of open – it cannot be extended outside of the library. Thus you could use an enum instead of a trait (and you might want to wrap the enum in a newtype for similar reasons).

        With your NVI advice here, are you referring to the same situation — don’t expose traits as the interface to the user when the trait is meant to be private and there’s actually only a finite set of implementations the user should be using that you have already provided them? If so, I agree; if not could you say more?

        For example, I’m writing a simple TUI that can render nested panes including colored output to a “screen”. The “screen” might just be a string (in which case colors will get ignored), or it might be a wrapper around printing to a terminal screen, or it might be a variety of other things, and I want the user to be able to write their own. So the interface is something like:

        fn pane_print<S: Screen>(doc: Document, screen: S) -> Result<(), Screen::Error>
        

        Are you suggesting I not do this, and if so what’s the alternative?

        EDIT: maybe this is just a matter of not knowing what you mean by a primary interface vs extension point?

        1. 2

          (I really should blog about this one day)

          Yours is a good example! Consider

          pub trait Screen {
              fn put_char(&mut self, ch: char);
              fn put_str(&mut self, s: &str) {
                  s.chars().for_each(|ch| self.put_char(ch))
              }
          }
          
          fn pane_print<S: Screen>(doc: Document, screen: S) { .. }
          

          I argue that this single trait embodies two interfaces. One is the interface for the implementors of screens, they need to override put_char. Another is the interface for users of screens, who call put_str. What I am saying is that:

          • It’s important to internalize the notion of two-sided abstractions, of implementor and user interface.
          • Sometimes, especially when programming in the large, it is useful to make the separation between the two explicit.

          In the above example, the separation would look like this:

          pub trait ScreenBackend {
              fn put_char(&mut self, ch: char);
          }
          
          pub struct Screen {
              backend: Box<dyn ScreenBackend>,
          }
          
          impl Screen {
              pub fn new<B: ScreenBackend>(backend: B) -> Screen {
                  Screen {
                      backend: Box::new(backend),
                  }
              }
          
              pub fn put_char(&mut self, ch: char) {
                  self.backend.put_char(ch);
              }
              pub fn put_str(&mut self, s: &str) {
                  s.chars().for_each(|ch| self.put_char(ch))
              }
          }
          
          fn pane_print(doc: Document, screen: Screen) { .. }
          

          The ScreenBackend is the extension point, its the interface for implementors. The Screen is the API, it’s the interface for the users. Note, in particular, how put_str now is non-overidable.

          I am not saying that this pattern is always a good idea (just look at the verbosity!). But it often is, and it’s not very intuitive. The main benefits here are:

          • Often, the best interface for the user and the best interface for the implementor are similar, but slightly different. If you don’t clearly separate them out, they might become entangled.
          • For the user, it’s much better to deal with a concrete type, rather than with a high-order type parameter. Also, compile times!
          • Explicit separation allows for easier changes: the Box can be replaced with an Arc or enum without affecting call sites.
          1. 1

            Ah, I agree 100% with this pattern! Screen is a better interface for users, and ScreenBackend better for implementers.

            There’s another pattern that achieves similar goals, where you have a huge trait with only a small number of required methods, like std::iter::Iterator. Though that feels.. leakier? And I feel like any given situation calls for one or the other though I couldn’t give you a rule for under what circumstances.

            Amusingly, I think your pattern is not a good fit for my code because of the way that Screen happens to be used. I’d like to use it as a case study for when your suggested pattern is essential, and when it’s not. The differentiating factors seem to be that in my case (i) there is exactly one user of Screen, which is the pretty printer itself, and (ii) there is no crate boundary between the definition of the Screen trait, and its use site.

            So my pretty printer is the only user of screens. ​It does put a wrapper called Pane around Screen, for its own personal use, with a type signature like struct Pane<S: Screen>. This achieves or avoids your goals:

            • The interfaces for the implementer (Screen, supplied by the person using the pretty printing library) and for the user (which is the wrapper type Pane<S>, used by the pretty printer itself) are different.
            • You mean that it’s easier for the end user to deal with a concrete type. Agreed. Not relevant in my exact case because the user of the Screen is not the end user.
            • “Explicit separation allows for easier changes” – the type variable S: Screen has leaked into my wrapper type Pane<S>. But it’s all internal with a single use site, so it would be easy to change anyways.

            Crate boundaries (or maybe package boundaries), feel very important for this, because you can refactor atomically within a crate, but not across a crate boundary. And also because you don’t know the arity of things that are happening across the boundary.

            Within a crate, if you have a trait T you can count all of the implementations A, B, and C, and replace the trait with an enum enum T { A, B, C }. Across a package boundary, you can’t, because you don’t know how many implementers there are. Across a crate but not package boundary is more grey. In a sufficiently large project, you probably want to split your package into crates, and treat crate boundaries as if they were package boundaries and go for as clean a separation as you can manage.

            Perhaps the rules are something like:

            • If you require a user outside of your package to implement a trait, have them implement the smallest thing possible. (This is what you’re calling the implementer of the trait.)
            • If you provide a user outside of your package with a thing that implements a trait, don’t expose the trait to them. Wrap it instead. (This is what you’re calling the user of the trait.)
            • If you have a large project, consider enforcing these sorts of boundaries internally too.

            Anyhow, that was kind of rambly. I do hope you write that blog post!