Trait inherent impl blocks are conceptually confusing

This post made me realize 2 things about traits:

  1. inherent impl blocks on traits are actually possible to define
  2. Once defined, inherent impl blocks are only available on trait objects (e.g. Box<dyn MyTrait>) as opposed to the trait bound approach (e.g. <T: MyTrait>).

This causes confusion for me: Why the distinction between inherent impl blocks and the methods in the trait itself? It feels unnatural, as intuitively any methods I wish to associate to a trait should go into the trait, and perhaps be marked as "object-safe only". Unnatural or not, this feature decidedly has a discoverability issue as I've been working on a daily basis with Rust for the past 2 years, and only now do I find this out.

The confusion comes from both sides:
At first when I saw an inherent impl block for a trait, that was confusing (i.e. "why is this even possible?").
And on another occasion, when I tried to write a trait with methods that use Self, I hit the object-safety wall. Now, regardless of whatever anyone says, those object safety rules (underpinned as they are with soundness) are black magick, at least in the sense that I haven't heard a single intuitive explanation for them that doesn't need to do a deep-dive into some aspect of PLT (at least one that is more in-depth than "it's for object safety reasons").

So rather than arguing for change, I guess I'm looking for a history and soundness lesson: Why the distinction between a trait itself and its inherent impl block(s)? Why not simply allow "trait-object-only items" to be marked as such within a trait definition?

1 Like

I thought the object safety rules were basically (But actually there's more to it):

  • Suppose you have a value, whose type implements this trait.
  • For each method...
  • What concrete type does that method return? If you can't answer this question, it's not object safe. Also apparently, it's very specifically returning Self that's a problem, apparently because associated types can be constrained?
  • Does it have a generic type parameter? If so, it's not object safe. I don't understand why, currently. I'll have to look into this more.
  • Okay, so, if there's a where Self: Sized bound on the method, then it's considered object safe because it's explicitly not accessible to trait objects.

If you can answer that question, it's object safe; if you can't, it's not.

I also had no idea about this, and went poking around. I found a question about this a few years ago on StackOverflow. One bit of the answer gives at least a technical justification:

Another thing to note is that trait methods are polymorphic, even when they have a default implementation and they're not overridden, while inherent methods on a trait objects are monomorphic (there's only one function, no matter what's behind the trait).

So, they're different kinds of function because the type-that-implements-the-trait is treated differently. I think. I think it makes sense to me that, if you define a function that should be monomorphic, you don't want to get by-default a corresponding polymorphic implementation, because that's potentially way more generated code.

... I'm going to try later with the object safety explanation.

1 Like

It's not an inherent impl on the trait itself, but on the type of a trait object. This is a lot more obvious if you always use dyn Trait syntax, and hopefully the bare Trait syntax will eventually be deprecated.

impl dyn Trait { ... }

dyn Trait is a type defined in the same crate and module as Trait, so it makes sense that you would be able to add inherent methods to it. But those methods will only be available on trait objects, not on ordinary implementors of Trait, so it's pretty unusual to do such a thing. I'm not surprised you didn't know about it.

When would you want to have an inherent method on a trait object? Well, usually it's because you want to do something with a trait object, but it can't go in the trait itself due to object safety rules. The one example I can think of in the standard library is Any: is<T> (with the other methods below it) is a generic method (not object safe), but it can clearly be implemented for a trait object because the only capability it needs is Any::get_type_id, which is object safe. If you tried to make is a provided method on Any, because it is generic, the trait would become non-object-safe, which would defeat the whole point.

6 Likes

This and other aspects of object safety are well-explained in Object Safety | Huon on the internet.

2 Likes

Now that I've read all this, I remember another example I found a while ago, which baffled me then: The downcast_ref() inherent method on Fail trait objects.

In Rust trait objects tend to be a last-resort kind of thing, specifically when a monomorphization/template based approach isn't going to get the job done. And that's fine, because trait objects are not a zero cost abstraction.

However, given the object safety rules being as complex as they are (eh @mwchase? :slight_smile: ), this is something I would have liked to know about sooner. Without the inherent method capability, trait objects are effectively a hampered (almost broken) feature, and this helps explain why I kept running into walls whenever I tried to use trait objects.

I think there's an opportunity here for better documentation. For example, there's no mention of it in the 2nd edition of the Book, while I'd argue that if you need trait objects, you very likely want to know about inherent impl blocks on trait objects.

1 Like