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;
}