Proposal: Change terminology from "trait object" to "dynamic trait"

Does "dynamic trait type" feel different to you? That is actually what I meant to propose, I believe, but I wrote the summary incorrectly.

Maybe even just “dynamic type”, much of the time. After all, interestingly, dyn Foo is the only case in Rust (excluding lifetime erasure) where the dynamic type of a value and its static type can be distinguished (unlike, say, Java or C++ or something, where that happens commonly)

5 Likes

What do you think of “dynamic compatible” (short for “dynamic trait compatible”)? I love the idea of improving the name of this feature.

2 Likes

I like that too -- I meant to propose that actually, as an alternative, guess I forgot.

Hm… I’m torn on this one.

On the one hand, my experience with Java/C++ dictates this is a subtle category error that might do more harm than good. A variable x has a static type and a dynamic type. For some code snippet, the static type of x is Derived, and the dynamic type of x is Base. But it makes no sense to say "Base is a dynamic type". Base and Derived are both perfectly concrete types (and Base may have bases of its own), and having subclasses does not cause a concrete type to become “more dynamic”. In other words, dynamic type is a property of a value, not a property of a type, at least as it’s typically used in those languages.

On the other hand, that Java/C+±specific notion of “static/dynamic type” is one I largely associate with “puzzle questions” and gotchas and language design flaws. After discovering Rust, I believe this is just another symptom (much like C++'s object slicing problem) of these languages not making a clear distinction between references that know their concrete type and references that are deliberately polymorphic/type-erased. So in a sense, the meanings being suggested here for “static/dynamic type” are the meanings those terms always should have had, and make way more sense than the meanings they currently seem to have in those other languages.

Note that outside of the Java/C++ world, there is a precedent to marking a clear distinction between static and dynamic types in the form of Ada’s distinction between MyType (only accepts objects of type MyType, no subclass) and MyType'Class (encompasses MyType and its subclasses through dynamic dispatch).

The thing is that english is ambiguous and some people will parse that as "(dynamic trait) type" rather than the intended "dynamic (trait type)". I'd be fine with just "dynamic type".

Would using one of “existential” or “(type-)erased” here be too jargony and/or too inaccurate? “Dynamic,” while accurate in the “static/dynamic type” sense as @Ixrec describes, as well as the “dynamic dispatch” sense, is very generic and hard to use when talking about the value itself- “dynamic trait [type]” sounds like it’s talking about the trait or type, not the value, and “dynamic trait value” is quite a mouthful.

1 Like

Yes.

3 Likes

It might just be me, but can someone first provide a clear technical description of what the concept under discussion actually means? I have some vague understanding of passing around references to objects along with a vtable for dynamic dispatch, and so far neither the “dyn-capable” notion nor the “dynamic type traits” descriptions do anything to help me understand what is meant here.

If we can iterate on what exactly it is we’re trying to describe, that might also help coming up with better names.

If it’s just me and it’s preferrable to discuss mostly with people who already understand most of that stuff too, that’d be fine – just checking.

1 Like

Here’s my take on a beginner-level explanation.

Rust has so-called trait objects, which allow for dynamic dispatch. In user terms, this means that you can take a “reference to a trait” &Trait from any type that implements said trait, and use the trait methods through this reference. Beyond references, this also works with other pointer-like types such as Box. The underlying implementation uses fat pointers and vtables.

There are several usability issues with this feature, however:

  1. It does not work quite like a normal reference, and looks too much like it.
  2. It does not work with every trait, some conditions must be met for that. These conditions are not obvious, and it is easy to break compatibility by changing a trait’s definition.
  3. The “object” terminology feels too much like “normal” OO languages like C++ and Java, even though there are differences that will trip people from these languages.

For this reason, there are several proposed revamps of this feature for Rust 2018:

  1. Add a “dyn” keyword to trait object references, to clarify that they are different from regular references.
  2. Add an annotation to the trait declaration to clarify that they must be usable via &dyn references (or to forbid using them like that if the trait author does not desire to maintain compatibility with it).
  3. Consider using a different terminology to describe this dynamic dispatch feature.

The first proposal has already been accepted for Rust 2018, this thread is about the later two.

2 Likes

If I'm inclined to be overly unambiguous I would say that Box<dyn Trait> is a (boxed) dynamically dispatched existential type supporting the operations of Trait.

But that is quite the mouthful (and not user friendly).

Other buzzwords: heterogeneous, type erasure. Haskell/Existentially quantified types - Wikibooks, open books for an open world

Some thoughts on the discussion itself:

Terminology should always be kept as short and simple as possible. In this context, “dynamic trait type” seems a bit much to pronounce, and proposals which introduce expert jargon like “existential” or “type erasure” seem even worse. Overuse of type theory jargon is one of the top reasons why Haskell is so impenetrable for beginners, let’s not bring that to Rust.

Ideally, we should be able to stick with two words. I agree that the first word should be “dynamic” (to hint at the “dyn” syntax), and am wondering if we could express what we want with just a second common word.

Thoughts on ideas that were proposed before:

  • Dynamic type:
    • Pros: Emphasizes the dynamic nature, integrates notion of dynamic dispatch and the idea that you cannot manipulate values of this type directly because you don’t know about its full type (including its size).
    • Cons: Terminologically close to the notion of dynamic typing, which Rust’s trait object system is only a subset of. May confuse people from Javascript or Python. Also dangerously close to “dynamically sized type”, which is a different concept.
  • Dynamic trait:
    • Pros: Puts the emphasis on traits, which are at the heart of this feature.
    • Cons: Suggests that dyn Trait is like a trait, where it actually a dynamically sized type.
  • Dynamic reference/box/…:
    • Pros: Puts emphasis on the user-side entity rather than the mechanism behind it. We can use it in addition to another terminology for the general concept.
    • Cons: Does not solve the full problem by itself. If we tried, we would need one qualifier per concrete usage of a trait object and would be unable to name the general concept and dynamic sized type behind that.

As a conclusion, I could actually see a more extended proposal merge all of this terminology together.

  • A trait which can be used via &dyn Trait is a “dynamic trait”.
  • The dyn Trait dynamically sized type is a “dynamic type”.
  • An &dyn Trait is a dynamic reference, a Box<dyn Trait> is a dynamic box, etc.

In this context, the following syntax could be used for the dyn_capable proposal:

trait MyTrait {}  // Ambiguous, to be deprecated?

dyn trait MyTrait {}  // Equivalent of proposed dyn_capable

// Bikeshed fuel for dyn_capable(no)
dyn(false) trait MyTrait {}
nodyn trait MyTrait {}
static trait MyTrait {}

It seems clear to me that if dyn_capable annotations should become eventually mandatory, even for some restricted purpose, then they should be made into keywords, not attributes.

I also generally dislke the concept of dyn_capable(no). I think it should only be a transition feature until it becomes mandatory to declare a trait which is used via &dyn to be dyn_capable.

1 Like

@HadrienG thanks for the clear explanation!

In order to make all this understandable, it would be good to think more on how the more pre-existing notions of fat pointers and vtables might contribute to explaining these things. It would also help to have very clear explanations of what the limitations are for being dyn-capable or not.

In a sense, this actually makes me like the “trait object” description more, as it conveys something about the fatness and the fact there’s a structured thing being referenced. It also feels similar to autoboxing in Java, maybe there’s terminology to borrow there? Of course “box” evokes different things in Rust.

I'm not sure I'd call these "pre-existing notions". I've only ever heard of "fat pointers" in Rust language design discussions like these, and iirc it doesn't show up anywhere in the book. If it does, it wasn't prominent enough to seem like a term Rustaceans should be familiar with. "vtables" I've always understood to be a jargon-y name for a language implementation detail. In C++ we're technically not supposed to talk about vtables as if it's part of the language, although it's hard to imagine any other implementation, so likewise it's not a thing I'd expect every C++ novice to be familiar with. I might expect the "average" C++ programmer to know about vtables, but only because C++ is so full of gotchas that to be "average" you have to know a bunch of details about how the language is typically implemented; thankfully that is less true of Rust.

Yes! This is something I've googled several times, and I could never find a real explanation for some of them, even when looking at the ancient RFCs about it. The restriction on generic methods makes sense since it's "obviously impossible" to do dynamic and static dispatch at "the same time". The restriction on static methods... I still have no idea why that exists (maybe it was just to simplify the initial implementation, but if it was that simple, it seems like it wouldn't be so hard to find that written down somewhere).

Hm... I could live with this. Actually, I wouldn't be surprised if this is the least bad naming scheme available to us.

2 Likes

How about:

  • &dyn Trait is a dynamically typed reference?
  • Box<dyn Trait> is a dynamically typed box?

So, while Box<T> is an example of static polymorphism, Box<dyn T> is simply an example of dynamic polymorphism. Static / dynamic type parameter etc…

dynamically typed reference

When I hear "dynamically typed" I think "has no typing information at compile time" like javascript, ruby, etc or the dynamic keyword in C#. But dyn types do have typing information at compile time, you are guaranteed that it implements all of the methods on Trait.

3 Likes

Yes! The type is constrained by a dynamic type parameter. I'd still call a &dyn Trait a dynamically typed reference where the fat pointer carries the run-time type information.

What we wish to Communicate

Consider the following program fragment:

let x: Box<dyn Trait>;

What can we say about x, and what should we say about x? I'll try to jot down answers to the first part here.

1. Existentially quantified

To form a Box<dyn Trait> you must have some object y where typeof(y): Trait which you can then Box::new(y) on.

If we want to be a bit more clear about what is happening with Box<dyn Trait>, we could use this pseudo-Rust:

// "There exists some `T: Trait`"
let x: Box<for<T: Trait> T>;

or in actual Haskell (a larger example):

{-# LANGUAGE ExistentialQuantification, KindSignatures, ConstraintKinds #-}

import Data.Kind
import Control.Monad

-- This is morally equivalent to `dyn`.
data Dyn (c :: * -> Constraint) = forall (t :: *). (c t :: Constraint) => D t

-- Or unannotated:
-- data Dyn c = forall t. c t => D t

-- Morally equivalent to `Vec<Box<dyn Debug>>`.
heteroList :: [Dyn Show]
heteroList = [D (), D 5, D True]

-- A program that prints out each element of the list above
-- to the terminal on a separate line.
main :: IO ()
main = forM_ heteroList $ \(D x) -> print x

Another way to think about existential quantification is that the type variable t is erased here, and hence type erasure.

2. Supports the operations of Trait

We could say:

let x: Box<for<T> T>;

which might be well-formed in our pseudo-Rust, but it would also be useless (we can't do anything useful with x).

Instead x: Box<dyn Trait> supports all the methods that Trait does.

3. Trait must be object safe

Unlike Haskell (iirc), there are some traits Trait (type classes) which can not be used in dyn Trait and thus existentially quantified and dynamically dispatched. Only those traits that are "object safe" may be used in this way, which means that Trait must satisfy the conditions enumerated in RFC 255.

4. Dynamically dispatched

Rust has two modes of existential quantification, one which uses static dispatch (-> impl Trait) and one which uses dynamic dispatch (dyn Trait). When we have Box<dyn Trait>, a vtable is created for each type T: Trait which contains pointers to the actual implementations of the methods in impl Trait for T (and other details..).

5. Boxed and lives on the heap

Because a Box is involved, the value lives on the heap.

On "dynamic type"

A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.

[..]

Terms like "dynamically typed" are arguably misnomers and should probably be replaced by "dynamically checked," but the usage is standard

-- Benjamin C. Pierce, Types and Programming Languages

Types do not exist at run-time since type systems are a syntactic method (there's no syntax in this sense at run-time; you could argue that the bytes that make up the instructions themselves form a syntax, but this is not meaningful). A dynamically checked language like python is statically typed because it only has one type; so it is unityped. At the very least, if we are going to say that python is typed, then the types of python live in the same category as values in a language such as Rust or Haskell.

I believe that calling &(dyn Trait) a dynamic type only leads away from understanding; if we have x : &(dyn Trait), then x clearly has a type, which is &(dyn Trait).

In defense of Jargon

Jargon is a type of language that is used in a particular context and may not be well understood outside that context.

-- Jargon - Wikipedia

In any community or any field, it is only natural that there is some terms which are not well understood outside of the field. In Rust, we employ jargon such as:

  • lifetime -- Great and understandable jargon
  • outlives -- Inaccurate semantically
  • trait object -- Not accessible

The goal in communication is to find terms that:

  1. do not lose semantic content / intent or precision;
  2. are accessible and easy to comprehend for newcomers to the group and for people it.

Between these points, there exists a natural tension. In the effort to solve 2. you may lose critical information, and in an effort to preserve critical information, you may lose on accessibility. Thus, we should decide what is critical information, and what are details.

The problem is not jargon in general; it is bad jargon. We should try to find terms that strike a good balance between 1. and 2. but also tries to eat the cake and keeps it too.

6 Likes

The point being that it is normal to take an object polymorphically constrained "by vtable" in most OOP languages. Users of those languages are going to hear "dynamic typing" and think no typing constraints what so ever, like the dynamic keyword in C# or pretty much every dynamic language's type system. It would seem fortuitous to draw parallels between dyn Trait and interfaces in those languages when teaching Rust to folks coming from OOP backgrounds.

Moreover Box<dyn Trait>, &dyn Trait etc are concrete types at compile time; they are references to a dynamic type, not a reference of dynamic type. I suspect this distinction will be more important when Rust supports by value dyn Trait.

2 Likes