7

The Rust book (2nd Edition) suggests that "Traits are similar to a feature often called ‘interfaces’ in other languages, though with some differences." For those not familiar with interfaces, the analogy doesn't illuminate. Can traits be reasonably thought of as mixins such as those found commonly in JavaScript?

They both seem to be a way to share code and add methods to multiple types/objects without inheritance, but how crucial are the differences for conceptual understanding?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ian Danforth
  • 779
  • 1
  • 9
  • 18

4 Answers4

13

"Traits" (or "Roles" in Perl) are a way to add multiple units of functionality to a class (or struct in Rust) without the problems of multiple inheritance. Traits are "cross cutting concerns" meaning they're not part of the class hierarchy, they can be potentially implemented on any class.

Traits define an interface, meaning in order for anything to implement that trait it must define all the required methods. Like you can require that method parameters be of a certain classes, you can require that certain parameters implement certain traits.

A good example is writing output. In many languages, you have to decide if you're writing to a FileHandle object or a Socket object. This can get frustrating because sometimes things will only write to files, but not sockets or vice versa, or maybe you want to capture the output in a string for debugging.

If you instead define a trait, you can write to anything that implements that trait. This is exactly what Rust does with std::io::Write.

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;

    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
        while !buf.is_empty() {
            match self.write(buf) {
                Ok(0) => return Err(Error::new(ErrorKind::WriteZero,
                                               "failed to write whole buffer")),
                Ok(n) => buf = &buf[n..],
                Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
                Err(e) => return Err(e),
            }
        }
        Ok(())
    }

    ...and a few more...
}

Anything which wants to implement Write must implement write and flush. A default write_all is provided, but you can implement your own if you like.

Here's how Vec<u8> implements Write so you can "print" to a vector of bytes.

impl Write for Vec<u8> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.extend_from_slice(buf);
        Ok(buf.len())
    }

    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
        self.extend_from_slice(buf);
        Ok(())
    }

    fn flush(&mut self) -> io::Result<()> { Ok(()) }
}

Now when you write something that needs to output stuff instead of deciding if it should write to a File or a TcpStream (a network socket) or whatever, you say it just has to have the Write trait.

fn display( out: Write ) {
    out.write(...whatever...)
}

Mixins are a severely watered down version of this. Mixins are a collection of methods which get injected into a class. That's about it. They solve the problem of multiple inheritance and cross-cutting concerns, but little else. There's no formal promise of an interface, you just call the methods and hope for the best.

Mixins are mostly functionally equivalent, but provide none of the compile time checks and high performance that traits do.

If you're familiar with mixins, traits will be a familiar way to compose functionality. The requirement to define an interface will be the struggle, but strong typing will be a struggle for anyone coming to Rust from JavaScript.


Unlike in JavaScript, where mixins are a neat add-on, traits are a fundamental part of Rust. They allow Rust to be strongly-typed, high-performance, very safe, but also extremely flexible. Traits allow Rust to perform extensive compile time checks on the validity of function arguments without the traditional restrictions of a strongly typed language.

Many core pieces of Rust are implemented with traits. std::io::Writer has already been mentioned. There's also std::cmp::PartialEq which handles == and !=. std::cmp::PartialOrd for >, >=, < and <=. std::fmt::Display for how a thing should be printed with {}. And so on.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Schwern
  • 153,029
  • 25
  • 195
  • 336
3

Thinking of traits as mixins will lead you away from, rather than towards, understanding. Traits are fundamentally about the strict type system, which will be quite alien to a programmer whose native language is JavaScript.

Like most programming constructs, traits are flexible enough that one could use them in a way that resembles how mixins are idiomatically used, but that won't resemble at all how most other programmers, including the standard library, use traits.

You should think of traits as a radical novelty.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
01d55
  • 1,872
  • 13
  • 22
  • 1
    "*You should think of traits as a radical novelty.*" I don't understand what this means. Traits are not a novelty in Rust. They implement basic functionality like `==` and `<` and formatting for printing. [`i32` implements dozens of traits](https://doc.rust-lang.org/std/primitive.i32.html#implementations). – Schwern Mar 14 '18 at 06:03
  • @Schwern I believe that the meaning is "traits are a novelty *when comparing Rust to other languages*", not that traits are a novelty within Rust. – Shepmaster Mar 14 '18 at 13:20
  • 1
    Traits are not a novelty to Rust - they are just Haskell type classes without HKTs renamed for a wider audience. If you want to understand traits really well, start from Haskell. – Centril Mar 14 '18 at 16:56
  • 1
    The phrase "radical novelty" is from EWD 1036. While Traits are indeed not novel to a Haskell programmer, the question clearly indicates that JavaScript, not Haskell, is the language from which the asker draws metaphors. – 01d55 Mar 15 '18 at 04:53
  • 1
    Ah; could you clarify that in the answer? – Centril Mar 15 '18 at 18:13
  • 1
    @01d55 Traits are not novelty in programming, so the final phrase of the answer is confusing and misleading. And what is EWD 1036? – Y. E. Apr 19 '21 at 02:05
  • @Y.E. It's an essay by E. W. Dijkstra, [On The Cruelty Of Really Teaching Computer Science](https://www.cs.utexas.edu/~EWD/transcriptions/EWD10xx/EWD1036.html) from 1988. I'm not sure how it applies here. – Schwern Apr 19 '21 at 03:01
  • @Schwern yep. I suggested to remove the obscure phrase that causes so many questions here. Not sure the edit will be accepted, though. – Y. E. Apr 19 '21 at 03:08
  • I was unfamiliar with the source of the term "radical novelty" but immediately understood it to mean "something unlike anything you've come across before, given your JS background." This was helpful. Great phrase; keep it! – chadoh Jul 06 '22 at 15:02
2

Traits or "type classes" (in Haskell, which is where Rust got traits from) are fundamentally about logical constraints on types. Traits are not fundamentally about values. Since JavaScript is unityped, mixins, which are about values, are nothing like traits/type-classes in a statically typed language like Rust or Haskell. Traits let us talk in a principled way about the commonalities between types. Unlike C++, which has "templates", Haskell and Rust type check implementations before monomorphization.

Assuming a generic function:

fn foo<T: Trait>(x: T) { /* .. */ }

or in Haskell:

foo :: Trait t => t -> IO ()
foo = ...

The bound T: Trait means that any type T you pick must satisfy the Trait. To satisfy the Trait, the type must explicitly say that it is implementing the Trait and therein provide a definition of all items required by the Trait. In order to be sound, Rust also guarantees that each type implements a given trait at most once - therefore, there can never be overlapping implementations.

Consider the following marker trait and a type which implements it:

trait Foo {}
struct Bar;
impl Foo for Bar {}

or in Haskell:

class Foo x where
data Bar = Bar
instance Foo Bar where

Notice that Foo does not have any methods, functions, or any other items. A difference between Haskell and Rust here is that x is absent in the Rust definition. This is because the first type parameter to a trait is implicit in Rust (and referred to by with Self) while it is explicit in Haskell.

Speaking of type parameters, we can define the trait StudentOf between two types like so:

trait StudentOf<A> {}
struct AlanTuring;
struct AlonzoChurch;
impl StudentOf<AlonzoChurch> for AlanTuring {}

or in Haskell:

class StudentOf self a where
data AlanTuring = AlanTuring
data AlonzoChurch = AlonzoChurch
instance StudentOf AlanTuring AlonzoChurch where

Until now, we've not introduced any functions - let's do that:

trait From<T> {
    fn from(x: T) -> Self;
}

struct WrapF64(f64);
impl From<f64> for WrapF64 {
    fn from(x: f64) -> Self {
        WrapF64(x)
    }
}

or in Haskell:

class From self t where
    from :: t -> self

newtype WrapDouble = WrapDouble Double
instance From WrapDouble Double where
    from d = WrapDouble d

What you've seen here is also a form of return type polymorphism. Let's make it a bit more clear and consider a Monoid trait:

trait Monoid {
    fn mzero() -> Self;
    fn mappend(self, rhs: Self) -> Self;
}

struct Sum(usize);
impl Monoid for Sum {
    fn mzero() -> Self { Sum(0) }
    fn mappend(self, rhs: Self) -> Self { Sum(self.0 + rhs.0) }
}

fn main() {
    let s: Sum = Monoid::mzero();
    let s2 = s.mappend(Sum(2));
    // or equivalently:
    let s2 = <Sum as Monoid>::mappend(s, Sum(2));
}

or in Haskell:

class Monoid m where
    mzero :: m    -- Notice that we don't have any inputs here.
    mappend :: m -> m -> m

...

The implementation of mzero here is inferred by the required return type Sum, which is why it is called return type polymorphism. Another subtle difference here is the self syntax in mappend - this is mostly a syntactic difference that allows us to do s.mappend(Sum(2)); in Rust.

Traits also allow us to require that each type which implements the trait must provide an associated item, such as associated constants:

trait Identifiable {
    const ID: usize; // Each impl must provide a constant value.
}

impl Identifiable for bool {
    const ID: usize = 42;
}

or associated types:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Once<T>(Option<T>);
impl<T> Iterator for Once<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.0.take()
    }
}

Associated types also allow us to define functions on the type level rather than functions on the value level:

trait UnaryTypeFamily { type Output: Clone; }
impl UnaryTypeFamily for InputType { Output = String; }

fn main() {
    // Apply the function UnaryTypeFamily with InputType.
    let foo: <InputType as UnaryTypeFamily>::Output = String::new();
}

Some traits such as Iterator are also object safe. This means that you can erase the actual type behind a pointer, and a vtable will be created for you:

fn use_boxed_iter(iter: Box<Iterator<Item = u8>>) { /* .. */ }

The Haskell equivalent of trait objects are existentially quantified types, which in fact trait objects are in a type theoretical sense.

Finally, there's the issue of higher kinded types, which lets us be generic over type constructors. In Haskell, you can formulate what it means to be an (endo)functor like so:

class Functor (f :: * -> *) where
    fmap :: (a -> b) -> (f a -> f b)

At this point, Rust does not have an equivalent notion, but will be equally expressive with generic associated types (GATs) soon:

trait FunctorFamily {
    type Functor<T>;
    fn fmap<A, B, F>(self: Self::Functor<A>, mapper: F) -> Self::Functor<B>
    where F: Fn(A) -> B;
}
Centril
  • 2,549
  • 1
  • 22
  • 30
  • *Haskell, which is where Rust got traits from* — do you have any citations for that? I agree that they are similar, but I've never heard that there was a direct lineage / causal relationship. – Shepmaster Mar 14 '18 at 19:18
  • See: https://air.mozilla.org/rust-typeclasses/ and https://en.wikipedia.org/wiki/Type_class "Type classes first appeared in the Haskell programming language", "Rust supports traits, which are a limited form of type classes with coherence." -- If you don't trace lineage, you can still see that Rust traits are type classes modulo some syntactic differences (dot syntax) and that Rust doesn't have GATs, HKTs, ConstraintKinds, yet which Haskell does.. The term "associated type" is also borrowed directly from Haskell. – Centril Mar 14 '18 at 19:35
  • @FrancisGagné Rust's trait are certainly *influenced* by Haskell, but stating that Rust "got its traits from Haskell" is a much stronger statement than pure influence. I wouldn't say that "Rust got its channels from Newsqueak", for example. – Shepmaster Mar 15 '18 at 01:20
  • @Shepmaster Influenced and "got" are essentially the same verb to me. Of course, there were some minor changes along the way. The video from 2012 also shows that traits were based on type classes. Of mainstream languages, there are essentially two that have type classes: Haskell and Rust - and given that Haskell had them first, the reasonable conclusion must be that Rust got them from Haskell. To me, the assertion is even too weak, I'll do one better: **Traits are typeclasses**. – Centril Mar 15 '18 at 15:09
  • @Centril perhaps you would be interested in answering [What is the difference between traits in Rust and typeclasses in Haskell?](https://stackoverflow.com/questions/28123453/what-is-the-difference-between-traits-in-rust-and-typeclasses-in-haskell) then. – Shepmaster Mar 15 '18 at 15:10
1

To add to schwern's answer

A mixin is a subclass specification that may be applied to various parent classes in order to extend them with the same set of features. - Traits: Composable Units of Behaviour.

The major difference compared to trait is that they have "total ordering". Changing the order in which mixins are implemented for a class or strut can cause the behaviour of the class or struct to change. If mixins X, Y were applied to a struct or class A, then applying X after Y can give you a different behaviour compared to when you apply Y after X. Traits are independent of implementation order - i.e has flattened code.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Malice
  • 1,457
  • 16
  • 32