Types aren't helping as much as I would like

When I start a new project, I like to use derived types everywhere in case I change my mind. However, the Rust compiler isn't helping with the type checking part. For example,

type Foo = u32;
type Bar = u32;
fn test(a: Foo, b: Bar) {
  x(a, b);
  x(b, a);
}
fn x(a: Foo, b: Bar) { println!("{} {}", a, b) }

compiles, which is a surprise if all I look at is the method signature. The program doesn't compile if I change to

type Foo = u64;
which defeats a big part of why I define the types in the first place. I want to catch such errors even when the underlying types happen to be the same.

Is there a way to define those types so Rust does what I want, short of defining a struct for every type?

2 Likes

Check out newtype_derive, a crate that automates "defining a struct for every type" (for numeric types).

2 Likes

Thanks. That looks really useful, but I don't see how it will help with some of my cases,

type Foo = (u32, u32);
type Bar = Qux;

Besides, it seems pretty verbose for my simple case. What I'd really like is for the Rust compiler to do the type checking with the types I define, but use the actual types at runtime. I don't see any way a program that type checked at compile time could get the wrong types at run time.

Unfortunately the name is misleading. type does not create a new type. It creates a new name for an existing type.

To create a new type that is distinct and incompatible with the original, you need to wrap it in struct or enum. struct Foo(u32, u32) is used in general and this pattern is called a "newtype".

7 Likes

Right, in D you do that with the "alias", it's more intuitive:

alias T = int;

Here it's also nice to remember that you can pattern match in the function signature:

struct Foo(u32);
struct Bar(u32);
fn x(Foo(a): Foo, Bar(b): Bar) {
    println!("{} {}", a, b);
}
fn test(a: Foo, b: Bar) {
    x(a, b);
    x(b, a);
}
fn main() {}

The repetition "Foo(a): Foo" in the function signature is a bit unfortunate and I think there's a RFC that asks for a shorter syntax.

7 Likes

I finally got around to trying your suggestion. It is a bit more verbose, and it is a bit obscure when I need to dereference to get the actual value, especially when I've got a Foo(Bar(u32)). On the plus side, I did catch two type errors.

1 Like

Usually when I'm using the newtype pattern I'll add impls for Deref and DerefMut to make using them more ergonomic. You get type safety while explicitly using the newtype as the type it wraps just requires a *.

use std::ops::{Deref, DerefMut};

#[derive(Debug)]
struct Miles(u32);
#[derive(Debug)]
struct Kilometers(u32);

impl Deref for Kilometers {
  type Target = u32;
  
  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

impl DerefMut for Kilometers {
  fn deref_mut(&mut self) -> &mut Self::Target {
    &mut self.0
  }
}

impl Deref for Miles {
  type Target = u32;
  
  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

impl DerefMut for Miles {
  fn deref_mut(&mut self) -> &mut Self::Target {
    &mut self.0
  }
}

fn takes_miles(m: Miles) {
  println!("{:?}", m);
  println!("5 more than the Miles value: {}", *m + 5);
}

fn main() {
  let mi = Miles(5);
  takes_miles(mi);

  // takes_miles(Kilometers(7)); <-- compile error
}
5 Likes

Much nicer. Thanks. Looks like I've got some homework to do learning about all the traits I should be using.

1 Like

To add to @Michael-F-Bryan's post, there's another cool aspect about Deref/DerefMut: "iterative" deref coercions - compiler keeps applying them through layers of Deref impls. Here's an example (I'm on mobile so it's a bit rough): Rust Playground

Note how it can "drill through" the newtype layers to get to the &i32 at the end.

2 Likes

I can see where that can be convenient, but it defeats my purpose for using the pattern. I want a compiler error if I mistakenly pass &Foo to a "takes(x: &Bar)" function. Fortunately, I can get that compiler error if I change the function to "takes(x: Bar)" and pass Foo(Bar(5)). Unfortunately, I've just transferred ownership, so I have to be careful.

Thanks for pointing out this pattern. I don't think I ever would have figured out why your version compiles.

1 Like

You'd implement Deref like that only if that makes semantic sense. So if a Foo is a wrapper around a Bar, then whether you pass a "standalone" &Bar or one from a Foo shouldn't violate type semantics - it's a Bar either way.

1 Like

is there a shortcut for making a real newtype that does directly expose all the same methods,

I guess the case where you'd most want this sort of thing is numeric wrappers and that could be done with macros ('roll an int-like type') , but thats not the only scenario, and some extension of 'type' might be easier to remember than finding a macro. inbuilt syntax gets error message assistance (.. when you type to use a method baz of Bar on a struct Bar(Foo), the compiler could tell you Bar doesn't have baz, use <the true new type syntax> to get it

Haskell has a special keyword specifically for this case, which is where the newtype pattern described probably comes from.

Type Synonyms

The type keyword in Haskell works exactly as it does in rust, where we use an alias to the type, but the compiler will still type check it the same as the aliased type.

"Learn You a Haskell for Great Good" as the following example:

type String = [Char]

This is called a type synonym because it's just a different name for [Char], and it can be used interchangeably. You would use this because you want to give names to concrete types made from higher kinded types, as in the string example, or to give short-hand names to types.

newtype

Again, from "Learn You a Haskell for Great Good":

In Haskell we could wrap a type, much like in rust, by creating the new type with the Data keyword.

data ZipList a = ZipList [a]

However, creating a new data type has some overhead involved as the compiler has to do extra work compiling a new separate type if we want to use it with the base type, and extra work wrapping and unwrapping that type every time it's converted back to a regular list ([a]). Instead, in cases where we want to wrap a type we use

newtype ZipList a = ZipList [a]

This is the newtype pattern. You still have to explicitly derive the typeclasses (Traits) that you want to use for the newtype; however, newtype will type-check differently than the base type, and it allows you the flexibility to specify a different implementation of a typeclass (Trait).

Haskell's typeclasses (Traits) are very flexible and can be interpreted differently for some data types.

For example: the Monoid typeclass.

The main point of interest here is the function mconcat which can take a list of Monoid values and get a single value from them. However, for certain types this can be interpreted multiple ways: for Boolean values we can think of this as mapping either || or && on the list, because both take two Boolean values and return one.

newtype Any = Any { getAny :: Bool }  
    deriving (Eq, Ord, Read, Show, Bounded)

instance Monoid Any where  
        mempty = Any False  
        Any x `mappend` Any y = Any (x || y) -- Boolean or

newtype All = All {getAll :: Bool }
    deriving (Eq, Ord, Read, Show, Bounded)

instance Monoid All where  
        mempty = Any False  
        All x `mappend` All y = All (x && y) -- Boolean and

This lets you modify the behavior of types on the fly as in:

getAny . mconcat . map Any 
    $ [False, False, False, True]

will return True

getAll . mconcat . map All
    $ [False, False, False, True]

will return False

This is a toy example, but it's cool because there isn't any run-time overhead for selecting different typeclass (Trait) implementations for your data.

Note:

I would have provided more links but I'm limited to two.

1 Like

The difference between T and struct Wrapper(T); in Rust is far less pronounced than the difference between T and data Wrapper = Wrapper T in Haskell. A data type in Haskell has a runtime representation and adds a separate layer of laziness. And while rust doesn't make many guarantees about data layout, I don't think you have much to worry about in terms of overhead. Probably the worst thing I know of is that a repr(C) struct might not be ABI compatible with the wrapped type, but there's an accepted RFC to add a workaround for that.

The bigger issue IMO is deriving trait impls and inherent methods for these types. Neither Rust nor Haskell has this easy. Rust at least has a reasonably efficient macro system to help with this (Template Haskell eats children for breakfast), but then it suffers from a lack of higher ranked types and requiring difficult design choices around ownership. (Hmm, should I take by value, or maybe require Clone? Ah, I think borrows will be good... wait a second wtf why is there no AddAssign<&'a i32> for i32 adfgdaghsfgssfg)

Looking back in time, this is one of these simple things that Ada did just right with the type vs subtype distinction (a new type is a logically distinct entity, a new subtype can be used in functions that take the original type but may add extra constraints on values).

1 Like