Typeables: Rust crate of type aliases and struct tuples

Typeables is a Rust crate of semantic types, such as for representing unit names, content types, phone numbers, email addresses, and the like. Typeables is intended to help developers with domain driven design, knowledge transfer, compile-time safety, run-time diagnostics, and API integrations.

What does Typeables do?

Typeables provides two flavors of every concept: a type alias and a struct tuple.

A type alias is a nickname such as:

rust pub type Foo = i16; let x: Foo = 1; println!("x is {}", x)

A struct tuple is a wrapper such as:

rust pub struct Foo(pub i16); let x = Foo(1); println!("x is {}", x.0)

Example usage:

rust let x: YearAsTypeI16 = 2022; // Year as type alias

rust let x = YearAsStructI16(2022); // Year as struct tupple wrapper

Example usage for function definitions:

rust fn f(x: YearAsTypeI16) { // Year as typpe alias println!("Year {}", x) }

rust fn f(x: YearAsStructI16) { // Year as struct tuple println!("Year {}", x.0) // Use the struct tuple field }

Example usage for function calls:

rust f(2022 as YearAsTypeI16); // Year as type alias

rust f(YearAsStructI16(2022)); // Year as struct tuple

How do I upgrade code with Typeables?

Typeables aims to provide an upgrade path from weaker-type code to stronger-type code.

Example variables:

rust let x = 2022; // Without Typeables

rust let x = 2022 as YearAsTypeI16; // Upgrade 1 adds type alias

rust let x = YearAsStructI16(2022); // Upgrade 2 adds struct tuple

Example function definitions:

rust fn f(x: i16) { // Without typeables. println!("Year {}", x) }

rust fn f(x: YearAsTypeI16) { // Upgrade 1 adds type alias println!("Year {}", x) }

rust fn f(x: YearAsStructI16) { // Upgrade 2 adds struct tuple println!("Year {}", x.0) }

Example function calls:

rust f(1); // Without typeables.

rust f(1 as YearAsTypeI16); // Upgrade 1 adds type alias

rust f(YearAsStructI16(1)); // Upgrade 2 adds struct tuple

The upgrade path is purely because of refactoring:

Semantics

Semantic examples

Calendar examples:

rust let year = YearAsStructI16(2022); let month = MonthAsStructI8(12);

Geolocation examples of New York City Grand Central Terminal:

rust let latitude = LatitudeAsDecimalDegreeAsStructF32(40.75); let longitude = LongitudeAsDecimalDegreeAsStructF32(-73.97);

Date-time format examples of the NASA launch of the Mars Perseverance Rover:

rust let date_stamp = DateAsYYYYXMMXDDAsStructString(String::from("2020-07-30")); // Year 2020 on July 30th let time_stamp = TimeAsHHXMMXSSAsStructString(String::from("07:50:00")); // 7:50 in the morning let offset_stamp = TimeOffsetAsHHXMMAsStructString(String::from("-05:00")); // 5 hours ahead of UTC

Why use semantic names?

When you use semantic names, such as clear descriptions and purposeful naming conventions, then you help developers understand your code, and help compilers provide reliability, and help tools provide inspectability.

Suppose your code has this function:

rust fn f(year: i16, month: i16) { println!("Year {} Month {}", year, month) }

A developer can use your code like this:

```rust let year = 2022; let month = 12;

f(year, month); // right // f(month, year); // wrong, yet will compile and be a bug ```

You can make your code clearer by adding a type alias:

rust fn f(year: YearAsTypeI16, month: MonthAsTypeI16) { println!("Year {} Month {}", year, month) }

You can make your code stronger by using a struct tuple:

rust fn f(year: YearAsStructI16, month: MonthAsStructI16) { println!("Year {} Month {}", year.0, month.0) }

A developer can use your code like this:

```rust let year = YearAsStructI16(2022); let month = MonthAsStructI16(12);

f(year, month); // right // f(month, year); // wrong and won't compile ```

Why use semantic names, representation names, unit names, and implementation names?

Suppose you're writing an application for aircraft.

You want to keep track of:

You can use this naming convention:

The code looks like this:

rust pub struct AltitudeAsAboveGroundLevelAsMeterAsStructI16(pub i16); pub struct AltitudeAsAboveGroundLevelAsFootAsStructI16(pub i16); pub struct AltitudeAsMeanSeaLevelAsMeterAsStructI16(pub i16); pub struct AltitudeAsMeanSeaLevelAsFootAsStructI16(pub i16);

Suppose your app also needs to keep track of:

You can use the same naming convention, and the code looks like this:

rust pub struct ElevationAsAboveGroundLevelAsMeterAsStructI16(pub i16); pub struct ElevationAsAboveGroundLevelAsFootAsStructI16(pub i16); pub struct ElevationAsMeanSeaLevelAsMeterAsStructI16(pub i16); pub struct ElevationAsMeanSeaLevelAsFootAsStructI16(pub i16);

The naming convention is crystal clear and fully descriptive:

Use words rather than abbreviations

Examples of semantic names:

Examples of representation names:

Examples of unit names:

Examples of implementation names:

Naming conventions

Naming convention for type aliass:

```rust pub type FooAsTypeI8 = i8; pub type FooAsTypeI16 = i16; pub type FooAsTypeI32 = i32; pub type FooAsTypeI64 = i64; pub type FooAsTypeI128 = i128; pub type FooAsTypeISize = isize;

pub type FooAsTypeU8 = u8; pub type FooAsTypeU16 = u16; pub type FooAsTypeU32 = u32; pub type FooAsTypeU64 = u64; pub type FooAsTypeU128 = u128; pub type FooAsTypeUSize = usize;

pub type FooAsTypeF32 = f32; pub type FooAsTypeF64 = f64;

pub type FooAsTypeStr = str; pub type FooAsTypeString = String; ```

Naming convention for struct tuples for numbers:

```rust pub struct FooAsStructI8(pub i8); pub struct FooAsStructI16(pub i16); pub struct FooAsStructI32(pub i32); pub struct FooAsStructI64(pub i64); pub struct FooAsStructI128(pub i128); pub struct FooAsStructISize(pub isize);

pub struct FooAsStructU8(pub u8); pub struct FooAsStructU16(pub u16); pub struct FooAsStructU32(pub u32); pub struct FooAsStructU64(pub u64); pub struct FooAsStructU128(pub u128); pub struct FooAsStructUSize(pub usize);

pub struct FooAsStructF32(pub f32); pub struct FooAsStructF64(pub f64);

pub struct FooAsStructStr(&'static String); pub struct FooAsStructString(pub String); ```

Comparisons

We recommend looking at the Rust crate uom (unit of measure) and the Rust book examples of the newtype pattern.

Comparison with uom

Broadly speaking:

Quantities v. units v. primitives:

Normalization v. exactness:

Comparison with Rust "New Type Idiom" a.k.a. "New Type Pattern"

Broadly speaking:

Roll your own versus using Typeables crate:

Implementation

The type aliases are all for Rust primitives and standards such as strings (using str and String) and numbers (using i64, u64, f64, et al.).

Overhead

Typeables has zero or near-zero runtime overhead:

Typing

Typeables is deliberately verbose.

Macros

The Typeables source code does not use macros.