Does Rust have an idiomatic equivalent to F# typedefs?

Erik Uggeldahl picture Erik Uggeldahl · Jan 22, 2016 · Viewed 8.1k times · Source

I'm re-writing existing code of mine in Rust 1.6 and I've found it very convenient in the source language to label a type by typedef. For example, in my card game I have a rank value in F# defined as:

type Rank = uint8

Answer

Shepmaster picture Shepmaster · Jan 22, 2016

From The Rust Programming Language section titled Creating Type Synonyms with Type Aliases:

Rust provides the ability to declare a type alias to give an existing type another name. For this we use the type keyword. For example, we can create the alias Kilometers to i32 like so:

type Kilometers = i32;

Now, the alias Kilometers is a synonym for i32; [...], Kilometers is not a separate, new type. Values that have the type Kilometers will be treated the same as values of type i32:

type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;

println!("x + y = {}", x + y);

There's more that you should read, but this answers the question.


As a bit of editorial, I don't think that a type alias is a great fit in a lot of places that people use them. Assuming that your Rank type represents something to do with a deck of cards, I'd suggest either an enum or a newtype. The reason is that with a type alias you can do something like this:

let rank: Rank = 100;

Which is nonsensical for a typical deck of cards. An enum is a restricted set. This means you can never create an invalid Rank:

enum Rank {
    One, Two, Three, Four, Five,
    Six, Seven, Eight, Nine, Ten,
    Jack, Queen, King, Ace,
}

impl Rank {
    fn from_value(v: u8) -> Result<Rank, ()> {
        use Rank::*;

        let r = match v {
            1 => One,
            2 => Two,
            // ...
            _ => return Err(()),
        };
        Ok(r)
    }

    fn value(&self) -> u8 {
        use Rank::*;

        match *self {
            One => 1,
            Two => 2,
            // ...
        }
    }
}

A newtype is just a wrapper type. It consumes no extra space compared to the wrapped type, it just provides an actual new type that lets you implement methods that can restrict to valid values. It's possible to create invalid values, but only within your own code, not all client code:

struct Rank(u8);

impl Rank {
    fn from_value(v: u8) -> Result<Rank, ()> {
        if v >= 1 && v <= 14 {
            Ok(Rank(v))
        } else {
            Err(())
        }
    }

    fn value(&self) -> u8 {
        self.0
    }
}

I tend to use type aliases as quick placeholders of types. While writing the above examples, I actually wrote:

type Error = ();

And returned a Result<Rank, Error>, but then thought that would be confusing. :-)

The other case I use them is to shorten a larger type that I don't want to hide. This happens with types like iterators or Results, which you can see in the standard library. Something like:

type CardResult<T> = Result<T, Error>;

fn foo() -> CardResult<String> {
    // ..
}