Traits and polymorphism in Rust


Most of the articles on this blog are really a way for me to document my learning process. This post is no exception: you are under no obligation to read any of this if it is totally obvious or old hat!

Today I’m going to briefly discuss Rust’s approach to polymorphism using traits. I will employ the medium of working code examples with embedded tests/assertions. A full working, Cargo-based project is available here.

First, we’ll define some common structs, Foo and Bar, and a trait Frobber:

Frobber is akin to an interface or type class in other languages. It defines a set of operations that another type can implement. Frobber is not a concrete type: it defines a family of types that implement a standard contract but which are not related through inheritance. In this respect, traits are much more similar to type classes in Haskell than, say, abstract classes or interfaces in common-or-garden “curly-brace” object-oriented languages such as Java or C#. They exhibit some of the characteristics of templates in C++ (especially in the presence of constraints and concepts) but do not support C++’s static duck typing behaviour (thankfully).

The first set of examples illustrate the “statically dispatched” (compile-time) use of traits:

Under this model, functions with type arguments are effectively template functions (in the C++ sense) that are used by the Rust compiler to generate families of functions. In this world, traits are the constraints on these type arguments, specifying the operations that otherwise-unknown types must implement. They, therefore, explicitly tell the compiler what operations the body of the function can perform on its arguments. In this respect constraints liberate. See inline comments in code for more discussion.

The second set of examples illustrate how we can use traits to perform dynamic dispatch:

Under this model, we do not use type arguments and instead make use of references to objects implementing a given trait (and Rust’s dyn keyword)s to perform dynamic (runtime) dispatch. This will be very familiar to anybody who has used interfaces or method overriding and virtual methods when using the inheritance-based subsets of languages such as C++, Java, C# and the like. Here we’re effectively passing pointers to virtual method tables and dispatching method calls based on the runtime type of an object.

Finally, the third set of examples illustrates the implications of this on homogeneous and heterogeneous collections of objects implementing traits. They also touch on the ownership semantics:

That’s enough for now. I’ve only scratched the surface, but maybe this will be useful to someone!

Related posts

String conversions in Rust follow-up
String conversions in Rust
Boilerplate for Rust error/result
Bracket pattern in Rust
Parsing MSBuild project XML in Rust
Richard’s Workspace Tool - more Rust programming



Content © 2024 Richard Cook. All rights reserved.